diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2009-02-22 13:37:51 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2009-02-22 13:37:51 +0100 |
commit | b9b85843572bf283f48285001e276ba7e61b63f6 (patch) | |
tree | 4c6f4571552ada9ccfb4030481dcf77308f8b254 /includes | |
parent | d9a20acc4e789cca747ad360d87ee3f3e7aa58c1 (diff) |
updated to MediaWiki 1.14.0
Diffstat (limited to 'includes')
279 files changed, 26138 insertions, 18295 deletions
diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php index 9daca9e5..1a9adbca 100644 --- a/includes/AjaxFunctions.php +++ b/includes/AjaxFunctions.php @@ -14,7 +14,8 @@ if( !defined( 'MEDIAWIKI' ) ) { * 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. + * @param $iconv_to String destination character set will be used as second parameter + * in the iconv function. Default is UTF-8. * @return string */ function js_unescape($source, $iconv_to = 'UTF-8') { @@ -72,91 +73,6 @@ function code2utf($num){ return ''; } -define( 'AJAX_SEARCH_VERSION', 2 ); //AJAX search cache version - -function wfSajaxSearch( $term ) { - global $wgContLang, $wgUser, $wgCapitalLinks, $wgMemc; - $limit = 16; - $sk = $wgUser->getSkin(); - $output = ''; - - $term = trim( $term ); - $term = $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) ); - if ( $wgCapitalLinks ) - $term = $wgContLang->ucfirst( $term ); - $term_title = Title::newFromText( $term ); - - $memckey = $term_title ? wfMemcKey( 'ajaxsearch', md5( $term_title->getFullText() ) ) : wfMemcKey( 'ajaxsearch', md5( $term ) ); - $cached = $wgMemc->get($memckey); - if( is_array( $cached ) && $cached['version'] == AJAX_SEARCH_VERSION ) { - $response = new AjaxResponse( $cached['html'] ); - $response->setCacheDuration( 30*60 ); - return $response; - } - - $r = $more = ''; - $canSearch = true; - - $results = PrefixSearch::titleSearch( $term, $limit + 1 ); - foreach( array_slice( $results, 0, $limit ) as $titleText ) { - $r .= '<li>' . $sk->makeKnownLink( $titleText ) . "</li>\n"; - } - - // Hack to check for specials - if( $results ) { - $t = Title::newFromText( $results[0] ); - if( $t && $t->getNamespace() == NS_SPECIAL ) { - $canSearch = false; - if( count( $results ) > $limit ) { - $more = '<i>' . - $sk->makeKnownLinkObj( - SpecialPage::getTitleFor( 'Specialpages' ), - wfMsgHtml( 'moredotdotdot' ) ) . - '</i>'; - } - } else { - if( count( $results ) > $limit ) { - $more = '<i>' . - $sk->makeKnownLinkObj( - SpecialPage::getTitleFor( "Allpages", $term ), - wfMsgHtml( 'moredotdotdot' ) ) . - '</i>'; - } - } - } - - $valid = (bool) $term_title; - $term_url = urlencode( $term ); - $term_normalized = $valid ? $term_title->getFullText() : $term; - $term_display = htmlspecialchars( $term ); - $subtitlemsg = ( $valid ? 'searchsubtitle' : 'searchsubtitleinvalid' ); - $subtitle = wfMsgExt( $subtitlemsg, array( 'parse' ), wfEscapeWikiText( $term_normalized ) ); - $html = '<div id="searchTargetHide"><a onclick="Searching_Hide_Results();">' - . wfMsgHtml( 'hideresults' ) . '</a></div>' - . '<h1 class="firstHeading">'.wfMsgHtml('search') - . '</h1><div id="contentSub">'. $subtitle . '</div>'; - if( $canSearch ) { - $html .= '<ul><li>' - . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ), - wfMsgHtml( 'searchcontaining', $term_display ), - "search={$term_url}&fulltext=Search" ) - . '</li><li>' . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ), - wfMsgHtml( 'searchnamed', $term_display ) , - "search={$term_url}&go=Go" ) - . "</li></ul>"; - } - if( $r ) { - $html .= "<h2>" . wfMsgHtml( 'articletitles', $term_display ) . "</h2>" - . '<ul>' .$r .'</ul>' . $more; - } - - $wgMemc->set( $memckey, array( 'version' => AJAX_SEARCH_VERSION, 'html' => $html ), 30 * 60 ); - - $response = new AjaxResponse( $html ); - $response->setCacheDuration( 30*60 ); - return $response; -} - /** * Called for AJAX watch/unwatch requests. * @param $pagename Prefixed title string for page to watch/unwatch @@ -189,20 +105,54 @@ function wfAjaxWatch($pagename = "", $watch = "") { if(!$watching) { $dbw = wfGetDB(DB_MASTER); $dbw->begin(); - $article->doWatch(); + $ok = $article->doWatch(); $dbw->commit(); } } else { if($watching) { $dbw = wfGetDB(DB_MASTER); $dbw->begin(); - $article->doUnwatch(); + $ok = $article->doUnwatch(); $dbw->commit(); } } + // Something stopped the change + if( isset($ok) && !$ok ) { + return '<err#>'; + } if( $watch ) { return '<w#>'.wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() ); } else { return '<u#>'.wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() ); } } + +/** + * Called in some places (currently just extensions) + * to get the thumbnail URL for a given file at a given resolution. + */ +function wfAjaxGetThumbnailUrl( $file, $width, $height ) { + $file = wfFindFile( $file ); + + if ( !$file || !$file->exists() ) + return null; + + $url = $file->getThumbnail( $width, $height )->url; + + return $url; +} + +/** + * Called in some places (currently just extensions) + * to get the URL for a given file. + */ +function wfAjaxGetFileUrl( $file ) { + $file = wfFindFile( $file ); + + if ( !$file || !$file->exists() ) + return null; + + $url = $file->getUrl(); + + return $url; +}
\ No newline at end of file diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index c79e928b..63468a14 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -9,7 +9,9 @@ if( !defined( 'MEDIAWIKI' ) ) { } /** - * @todo document + * Handle responses for Ajax requests (send headers, print + * content, that sort of thing) + * * @ingroup Ajax */ class AjaxResponse { @@ -20,7 +22,7 @@ class AjaxResponse { /** HTTP header Content-Type */ private $mContentType; - /** @todo document */ + /** Disables output. Can be set by calling $AjaxResponse->disable() */ private $mDisabled; /** Date for the HTTP header Last-modified */ diff --git a/includes/Article.php b/includes/Article.php index 4d8277bb..3d9c2147 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -16,27 +16,30 @@ 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; //!< - var $mRedirectTarget; //!< - var $mIsRedirect; + var $mComment = ''; //!< + var $mContent; //!< + var $mContentLoaded = false; //!< + var $mCounter = -1; //!< Not loaded + var $mCurID = -1; //!< Not loaded + var $mDataLoaded = false; //!< + var $mForUpdate = false; //!< + var $mGoodAdjustment = 0; //!< + var $mIsRedirect = false; //!< + var $mLatest = false; //!< + var $mMinorEdit; //!< + var $mOldId; //!< + var $mPreparedEdit = false; //!< Title object if set + var $mRedirectedFrom = null; //!< Title object if set + var $mRedirectTarget = null; //!< Title object if set + var $mRedirectUrl = false; //!< + var $mRevIdFetched = 0; //!< + var $mRevision; //!< + var $mTimestamp = ''; //!< + var $mTitle; //!< + var $mTotalAdjustment = 0; //!< + var $mTouched = '19700101000000'; //!< + var $mUser = -1; //!< Not loaded + var $mUserText = ''; //!< /**@}}*/ /** @@ -44,10 +47,18 @@ class Article { * @param $title Reference to a Title object. * @param $oldId Integer revision ID, null to fetch from request, zero for current */ - function __construct( Title $title, $oldId = null ) { + public function __construct( Title $title, $oldId = null ) { $this->mTitle =& $title; $this->mOldId = $oldId; - $this->clear(); + } + + /** + * Constructor from an article article + * @param $id The article ID to load + */ + public static function newFromID( $id ) { + $t = Title::newFromID( $id ); + return $t == null ? null : new Article( $t ); } /** @@ -55,7 +66,7 @@ class Article { * from another page on the wiki. * @param $from Title object. */ - function setRedirectedFrom( $from ) { + public function setRedirectedFrom( $from ) { $this->mRedirectedFrom = $from; } @@ -67,22 +78,20 @@ class Article { * @return mixed Title object, or null if this page is not a redirect */ public function getRedirectTarget() { - if(!$this->mTitle || !$this->mTitle->isRedirect()) + if( !$this->mTitle || !$this->mTitle->isRedirect() ) return null; - if(!is_null($this->mRedirectTarget)) + if( !is_null($this->mRedirectTarget) ) return $this->mRedirectTarget; - # Query the redirect table - $dbr = wfGetDB(DB_SLAVE); - $res = $dbr->select('redirect', - array('rd_namespace', 'rd_title'), - array('rd_from' => $this->getID()), - __METHOD__ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'redirect', + array('rd_namespace', 'rd_title'), + array('rd_from' => $this->getID()), + __METHOD__ ); - $row = $dbr->fetchObject($res); - if($row) + if( $row = $dbr->fetchObject($res) ) { return $this->mRedirectTarget = Title::makeTitle($row->rd_namespace, $row->rd_title); - + } # This page doesn't have an entry in the redirect table return $this->mRedirectTarget = $this->insertRedirect(); } @@ -94,15 +103,19 @@ class Article { * @return Title object */ public function insertRedirect() { - $retval = Title::newFromRedirect($this->getContent()); - if(!$retval) + $retval = Title::newFromRedirect( $this->getContent() ); + if( !$retval ) { return null; - $dbw = wfGetDB(DB_MASTER); - $dbw->replace('redirect', array('rd_from'), array( + } + $dbw = wfGetDB( DB_MASTER ); + $dbw->replace( 'redirect', array('rd_from'), + array( 'rd_from' => $this->getID(), 'rd_namespace' => $retval->getNamespace(), 'rd_title' => $retval->getDBKey() - ), __METHOD__); + ), + __METHOD__ + ); return $retval; } @@ -113,9 +126,9 @@ class Article { */ public function followRedirect() { $text = $this->getContent(); - return self::followRedirectText( $text ); + return $this->followRedirectText( $text ); } - + /** * Get the Title object this text redirects to * @@ -131,7 +144,6 @@ class Article { // // 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 ) ); } @@ -142,7 +154,6 @@ class Article { // the rest of the page we're on. // // This can be hard to reverse, so they may be disabled. - if( $rt->isSpecial( 'Userlogout' ) ) { // rolleyes } else { @@ -159,15 +170,15 @@ class Article { /** * get the title object of the article */ - function getTitle() { + public function getTitle() { return $this->mTitle; } /** - * Clear the object - * @private - */ - function clear() { + * Clear the object + * @private + */ + public function clear() { $this->mDataLoaded = false; $this->mContentLoaded = false; @@ -190,30 +201,27 @@ class Article { * 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 $wgUser, $wgOut, $wgMessageCache; - + public function getContent() { + global $wgUser, $wgContLang, $wgOut, $wgMessageCache; wfProfileIn( __METHOD__ ); - - if ( 0 == $this->getID() ) { - wfProfileOut( __METHOD__ ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - $wgMessageCache->loadAllMessages(); - $ret = wfMsgWeirdKey ( $this->mTitle->getText() ) ; + if( $this->getID() === 0 ) { + # If this is a MediaWiki:x message, then load the messages + # and return the message value for x. + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + # If this is a system message, get the default text. + list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) ); + $wgMessageCache->loadAllMessages( $lang ); + $text = wfMsgGetKey( $message, false, $lang, false ); + if( wfEmptyMsg( $message, $text ) ) + $text = ''; } else { - $ret = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); + $text = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); } - - return "<div class='noarticletext'>\n$ret\n</div>"; + wfProfileOut( __METHOD__ ); + return $text; } else { $this->loadContent(); wfProfileOut( __METHOD__ ); @@ -233,7 +241,7 @@ class Article { * @return string text of the requested section * @deprecated */ - function getSection($text,$section) { + public function getSection( $text, $section ) { global $wgParser; return $wgParser->getSection( $text, $section ); } @@ -242,8 +250,8 @@ class Article { * @return int The oldid of the article that is to be shown, 0 for the * current revision */ - function getOldID() { - if ( is_null( $this->mOldId ) ) { + public function getOldID() { + if( is_null( $this->mOldId ) ) { $this->mOldId = $this->getOldIDFromRequest(); } return $this->mOldId; @@ -254,32 +262,27 @@ class Article { * * @return int The old id for the request */ - function getOldIDFromRequest() { + public function getOldIDFromRequest() { global $wgRequest; $this->mRedirectUrl = false; $oldid = $wgRequest->getVal( 'oldid' ); - if ( isset( $oldid ) ) { + if( isset( $oldid ) ) { $oldid = intval( $oldid ); - if ( $wgRequest->getVal( 'direction' ) == 'next' ) { + if( $wgRequest->getVal( 'direction' ) == 'next' ) { $nextid = $this->mTitle->getNextRevisionID( $oldid ); - if ( $nextid ) { + if( $nextid ) { $oldid = $nextid; } else { $this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' ); } - } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { + } elseif( $wgRequest->getVal( 'direction' ) == 'prev' ) { $previd = $this->mTitle->getPreviousRevisionID( $oldid ); - if ( $previd ) { + if( $previd ) { $oldid = $previd; - } else { - # TODO } } - # unused: - # $lastid = $oldid; } - - if ( !$oldid ) { + if( !$oldid ) { $oldid = 0; } return $oldid; @@ -289,25 +292,24 @@ class Article { * Load the revision (including text) into this object */ function loadContent() { - if ( $this->mContentLoaded ) return; - + if( $this->mContentLoaded ) return; + wfProfileIn( __METHOD__ ); # 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. $this->mOldId = $oldid; $this->fetchContent( $oldid ); + wfProfileOut( __METHOD__ ); } /** * Fetch a page record with the given conditions - * @param Database $dbr - * @param array $conditions - * @private + * @param $dbr Database object + * @param $conditions Array */ - function pageData( $dbr, $conditions ) { + protected function pageData( $dbr, $conditions ) { $fields = array( 'page_id', 'page_namespace', @@ -333,20 +335,20 @@ class Article { } /** - * @param Database $dbr - * @param Title $title + * @param $dbr Database object + * @param $title Title object */ - function pageDataFromTitle( $dbr, $title ) { + public function pageDataFromTitle( $dbr, $title ) { return $this->pageData( $dbr, array( 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() ) ); } /** - * @param Database $dbr - * @param int $id + * @param $dbr Database + * @param $id Integer */ - function pageDataFromId( $dbr, $id ) { + protected function pageDataFromId( $dbr, $id ) { return $this->pageData( $dbr, array( 'page_id' => $id ) ); } @@ -354,22 +356,21 @@ class Article { * Set the general counter, title etc data loaded from * some source. * - * @param object $data - * @private + * @param $data Database row object or "fromdb" */ - function loadPageData( $data = 'fromdb' ) { - if ( $data === 'fromdb' ) { + public function loadPageData( $data = 'fromdb' ) { + if( $data === 'fromdb' ) { $dbr = wfGetDB( DB_MASTER ); $data = $this->pageDataFromId( $dbr, $this->getId() ); } $lc = LinkCache::singleton(); - if ( $data ) { + if( $data ) { $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect ); $this->mTitle->mArticleID = $data->page_id; - # Old-fashioned restrictions. + # Old-fashioned restrictions $this->mTitle->loadRestrictions( $data->page_restrictions ); $this->mCounter = $data->page_counter; @@ -377,7 +378,7 @@ class Article { $this->mIsRedirect = $data->page_is_redirect; $this->mLatest = $data->page_latest; } else { - if ( is_object( $this->mTitle ) ) { + if( is_object( $this->mTitle ) ) { $lc->addBadLinkObj( $this->mTitle ); } $this->mTitle->mArticleID = 0; @@ -389,11 +390,11 @@ class Article { /** * Get text of an article from database * Does *NOT* follow redirects. - * @param int $oldid 0 for whatever the latest revision is + * @param $oldid Int: 0 for whatever the latest revision is * @return string */ function fetchContent( $oldid = 0 ) { - if ( $this->mContentLoaded ) { + if( $this->mContentLoaded ) { return $this->mContent; } @@ -429,14 +430,14 @@ class Article { } $revision = Revision::newFromId( $this->mLatest ); if( is_null( $revision ) ) { - wfDebug( __METHOD__." failed to retrieve current page, rev_id {$data->page_latest}\n" ); + wfDebug( __METHOD__." failed to retrieve current page, rev_id {$this->mLatest}\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->revText(); // Loads if user is allowed + $this->mContent = $revision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed $this->mUser = $revision->getUser(); $this->mUserText = $revision->getUserText(); @@ -457,7 +458,7 @@ class Article { * * @param $x Mixed: FIXME */ - function forUpdate( $x = NULL ) { + public function forUpdate( $x = NULL ) { return wfSetVar( $this->mForUpdate, $x ); } @@ -479,9 +480,9 @@ class Article { * the default * @return Array: options */ - function getSelectOptions( $options = '' ) { - if ( $this->mForUpdate ) { - if ( is_array( $options ) ) { + protected function getSelectOptions( $options = '' ) { + if( $this->mForUpdate ) { + if( is_array( $options ) ) { $options[] = 'FOR UPDATE'; } else { $options = 'FOR UPDATE'; @@ -493,7 +494,7 @@ class Article { /** * @return int Page ID */ - function getID() { + public function getID() { if( $this->mTitle ) { return $this->mTitle->getArticleID(); } else { @@ -504,22 +505,38 @@ class Article { /** * @return bool Whether or not the page exists in the database */ - function exists() { - return $this->getId() != 0; + public function exists() { + return $this->getId() > 0; + } + + /** + * Check if this page is something we're going to be showing + * some sort of sensible content for. If we return false, page + * views (plain action=view) will return an HTTP 404 response, + * so spiders and robots can know they're following a bad link. + * + * @return bool + */ + public function hasViewableContent() { + return $this->exists() || $this->mTitle->isAlwaysKnown(); } /** * @return int The view count for the page */ - function getCount() { - if ( -1 == $this->mCounter ) { + public function getCount() { + if( -1 == $this->mCounter ) { $id = $this->getID(); - if ( $id == 0 ) { + 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() ); + $this->mCounter = $dbr->selectField( 'page', + 'page_counter', + array( 'page_id' => $id ), + __METHOD__, + $this->getSelectOptions() + ); } } return $this->mCounter; @@ -532,14 +549,11 @@ class Article { * @param $text String: text to analyze * @return bool */ - function isCountable( $text ) { + public function isCountable( $text ) { global $wgUseCommaCount; $token = $wgUseCommaCount ? ',' : '[['; - return - $this->mTitle->isContentPage() - && !$this->isRedirect( $text ) - && in_string( $token, $text ); + return $this->mTitle->isContentPage() && !$this->isRedirect($text) && in_string($token,$text); } /** @@ -548,11 +562,11 @@ class Article { * @param $text String: FIXME * @return bool */ - function isRedirect( $text = false ) { - if ( $text === false ) { - if ( $this->mDataLoaded ) + public function isRedirect( $text = false ) { + if( $text === false ) { + if( $this->mDataLoaded ) { return $this->mIsRedirect; - + } // Apparently loadPageData was never called $this->loadContent(); $titleObj = Title::newFromRedirect( $this->fetchContent() ); @@ -567,28 +581,25 @@ class Article { * to this page (and it exists). * @return bool */ - function isCurrent() { + public function isCurrent() { # If no oldid, this is the current version. - if ($this->getOldID() == 0) + if( $this->getOldID() == 0 ) { return true; - - return $this->exists() && - isset( $this->mRevision ) && - $this->mRevision->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 ) + protected function loadLastEdit() { + if( -1 != $this->mUser ) return; # New or non-existent articles have no user information $id = $this->getID(); - if ( 0 == $id ) return; + if( 0 == $id ) return; $this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id ); if( !is_null( $this->mLastRevision ) ) { @@ -601,35 +612,36 @@ class Article { } } - function getTimestamp() { + public function getTimestamp() { // Check if the field has been filled by ParserCache::get() - if ( !$this->mTimestamp ) { + if( !$this->mTimestamp ) { $this->loadLastEdit(); } return wfTimestamp(TS_MW, $this->mTimestamp); } - function getUser() { + public function getUser() { $this->loadLastEdit(); return $this->mUser; } - function getUserText() { + public function getUserText() { $this->loadLastEdit(); return $this->mUserText; } - function getComment() { + public function getComment() { $this->loadLastEdit(); return $this->mComment; } - function getMinorEdit() { + public function getMinorEdit() { $this->loadLastEdit(); return $this->mMinorEdit; } - function getRevIdFetched() { + /* Use this to fetch the rev ID used on page views */ + public function getRevIdFetched() { $this->loadLastEdit(); return $this->mRevIdFetched; } @@ -638,7 +650,7 @@ class Article { * @param $limit Integer: default 0. * @param $offset Integer: default 0. */ - function getContributors($limit = 0, $offset = 0) { + public function getContributors($limit = 0, $offset = 0) { # XXX: this is expensive; cache this info somewhere. $contribs = array(); @@ -648,49 +660,62 @@ class Article { $user = $this->getUser(); $pageId = $this->getId(); - $sql = "SELECT rev_user, rev_user_text, user_real_name, MAX(rev_timestamp) as timestamp + $sql = "SELECT {$userTable}.*, 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; } - if ($offset > 0) { $sql .= ' OFFSET '.$offset; } - - $sql .= ' '. $this->getSelectOptions(); + if($limit > 0) { $sql .= ' LIMIT '.$limit; } + if($offset > 0) { $sql .= ' OFFSET '.$offset; } - $res = $dbr->query($sql, __METHOD__); + $sql .= ' '. $this->getSelectOptions(); - while ( $line = $dbr->fetchObject( $res ) ) { - $contribs[] = array($line->rev_user, $line->rev_user_text, $line->user_real_name); - } + $res = $dbr->query($sql, __METHOD__ ); - $dbr->freeResult($res); - return $contribs; + return new UserArrayFromResult( $res ); } /** * This is the default action of the script: just view the page of * the given title. */ - function view() { + public function view() { global $wgUser, $wgOut, $wgRequest, $wgContLang; global $wgEnableParserCache, $wgStylePath, $wgParser; global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; global $wgDefaultRobotPolicy; - $sk = $wgUser->getSkin(); wfProfileIn( __METHOD__ ); - $parserCache = ParserCache::singleton(); - $ns = $this->mTitle->getNamespace(); # shortcut - # Get variables from query string $oldid = $this->getOldID(); + # Try file cache + if( $oldid === 0 && $this->checkTouched() ) { + global $wgUseETag; + if( $wgUseETag ) { + $parserCache = ParserCache::singleton(); + $wgOut->setETag( $parserCache->getETag($this,$wgUser) ); + } + if( $wgOut->checkLastModified( $this->getTouched() ) ) { + wfProfileOut( __METHOD__ ); + return; + } else if( $this->tryFileCache() ) { + # tell wgOut that output is taken care of + $wgOut->disable(); + $this->viewUpdates(); + wfProfileOut( __METHOD__ ); + return; + } + } + + $ns = $this->mTitle->getNamespace(); # shortcut + $sk = $wgUser->getSkin(); + # getOldID may want us to redirect somewhere else - if ( $this->mRedirectUrl ) { + if( $this->mRedirectUrl ) { $wgOut->redirect( $this->mRedirectUrl ); wfProfileOut( __METHOD__ ); return; @@ -701,13 +726,14 @@ class Article { $rdfrom = $wgRequest->getVal( 'rdfrom' ); $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); $purge = $wgRequest->getVal( 'action' ) == 'purge'; + $return404 = false; $wgOut->setArticleFlag( true ); # Discourage indexing of printable versions, but encourage following if( $wgOut->isPrintable() ) { $policy = 'noindex,follow'; - } elseif ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { + } elseif( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { $policy = $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()]; } elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) { # Honour customised robot policies for this namespace @@ -720,10 +746,12 @@ class Article { # If we got diff and oldid in the query, we want to see a # diff page instead of the article. - if ( !is_null( $diff ) ) { + if( !is_null( $diff ) ) { $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge ); + $diff = $wgRequest->getVal( 'diff' ); + $htmldiff = $wgRequest->getVal( 'htmldiff' , false); + $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); @@ -738,51 +766,36 @@ class Article { 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 = $this->useParserCache( $oldid ); wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" ); - if ( $wgUser->getOption( 'stubthreshold' ) ) { + if( $wgUser->getOption( 'stubthreshold' ) ) { wfIncrStats( 'pcache_miss_stub' ); } $wasRedirected = false; - if ( isset( $this->mRedirectedFrom ) ) { + 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 ) ) ) { + if( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { $redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' ); - $s = wfMsg( 'redirectedfrom', $redir ); + $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); $wgOut->setSubtitle( $s ); // Set the fragment if one was specified in the redirect - if ( strval( $this->mTitle->getFragment() ) != '' ) { + if( strval( $this->mTitle->getFragment() ) != '' ) { $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() ); $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); } $wasRedirected = true; } - } elseif ( !empty( $rdfrom ) ) { + } 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 ) ) { $redir = $sk->makeExternalLink( $rdfrom, $rdfrom ); - $s = wfMsg( 'redirectedfrom', $redir ); + $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); $wgOut->setSubtitle( $s ); $wasRedirected = true; } @@ -790,18 +803,20 @@ class Article { $outputDone = false; wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$pcache ) ); - if ( $pcache ) { - if ( $wgOut->tryParserCache( $this, $wgUser ) ) { - // Ensure that UI elements requiring revision ID have - // the correct version information. - $wgOut->setRevisionId( $this->mLatest ); - $outputDone = true; - } + if( $pcache && $wgOut->tryParserCache( $this, $wgUser ) ) { + // Ensure that UI elements requiring revision ID have + // the correct version information. + $wgOut->setRevisionId( $this->mLatest ); + $outputDone = true; } # Fetch content and check for errors - if ( !$outputDone ) { + if( !$outputDone ) { + # If the article does not exist and was deleted, show the log + if( $this->getID() == 0 ) { + $this->showDeletionLog(); + } $text = $this->getContent(); - if ( $text === false ) { + if( $text === false ) { # Failed to load, replace text with error message $t = $this->mTitle->getPrefixedText(); if( $oldid ) { @@ -811,18 +826,38 @@ class Article { $text = wfMsg( 'noarticletext' ); } } + + # Non-existent pages + if( $this->getID() === 0 ) { + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $text = "<div class='noarticletext'>\n$text\n</div>"; + if( !$this->hasViewableContent() ) { + // If there's no backing content, send a 404 Not Found + // for better machine handling of broken links. + $return404 = true; + } + } + + if( $return404 ) { + $wgRequest->response()->header( "HTTP/1.x 404 Not Found" ); + } # Another whitelist check in case oldid is altering the title - if ( !$this->mTitle->userCanRead() ) { + if( !$this->mTitle->userCanRead() ) { $wgOut->loginToUse(); $wgOut->output(); + $wgOut->disable(); wfProfileOut( __METHOD__ ); - exit; + return; } + + # For ?curid=x urls, disallow indexing + if( $wgRequest->getInt('curid') ) + $wgOut->setRobotPolicy( 'noindex,follow' ); # We're looking at an old revision - if ( !empty( $oldid ) ) { - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + 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 { @@ -840,27 +875,27 @@ class Article { } } } - + $wgOut->setRevisionId( $this->getRevIdFetched() ); // Pages containing custom CSS or JavaScript get special treatment if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { - $wgOut->addHtml( wfMsgExt( 'clearyourcache', 'parse' ) ); + $wgOut->addHTML( wfMsgExt( 'clearyourcache', 'parse' ) ); // Give hooks a chance to customise the output if( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) { // Wrap the whole lot in a <pre> and don't parse $m = array(); preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); - $wgOut->addHtml( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); - $wgOut->addHtml( htmlspecialchars( $this->mContent ) ); - $wgOut->addHtml( "\n</pre>\n" ); + $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); + $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); + $wgOut->addHTML( "\n</pre>\n" ); } - } else if ( $rt = Title::newFromRedirect( $text ) ) { + } else if( $rt = Title::newFromRedirect( $text ) ) { # Don't append the subtitle if this was an old revision - $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ); + $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); $wgOut->addParserOutputNoText( $parseout ); - } else if ( $pcache ) { + } else if( $pcache ) { # Display content and save to parser cache $this->outputWikiText( $text ); } else { @@ -876,7 +911,7 @@ class Article { $time += wfTime(); # Timing hack - if ( $time > 3 ) { + if( $time > 3 ) { wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, $this->mTitle->getPrefixedDBkey())); } @@ -890,6 +925,14 @@ class Article { $t = $wgOut->getPageTitle(); if( empty( $t ) ) { $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + + # For the main page, overwrite the <title> element with the con- + # tents of 'pagetitle-view-mainpage' instead of the default (if + # that's not empty). + if( $this->mTitle->equals( Title::newMainPage() ) && + wfMsgForContent( 'pagetitle-view-mainpage' ) !== '' ) { + $wgOut->setHTMLTitle( wfMsgForContent( 'pagetitle-view-mainpage' ) ); + } } # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page @@ -899,7 +942,7 @@ class Article { # If we have been passed an &rcid= parameter, we want to give the user a # chance to mark this new article as patrolled. - if( !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) && $this->mTitle->exists() ) { + if( !empty($rcid) && $this->mTitle->exists() && $this->mTitle->userCan('patrol') ) { $wgOut->addHTML( "<div class='patrollink'>" . wfMsgHtml( 'markaspatrolledlink', @@ -911,19 +954,45 @@ class Article { } # Trackbacks - if ($wgUseTrackbacks) + if( $wgUseTrackbacks ) { $this->addTrackbacks(); + } $this->viewUpdates(); wfProfileOut( __METHOD__ ); } - /* + protected function showDeletionLog() { + global $wgUser, $wgOut; + $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut ); + $pager = new LogPager( $loglist, 'delete', false, $this->mTitle->getPrefixedText() ); + if( $pager->getNumRows() > 0 ) { + $pager->mLimit = 10; + $wgOut->addHTML( '<div class="mw-warning-with-logexcerpt">' ); + $wgOut->addWikiMsg( 'deleted-notice' ); + $wgOut->addHTML( + $loglist->beginLogEventsList() . + $pager->getBody() . + $loglist->endLogEventsList() + ); + if( $pager->getNumRows() > 10 ) { + $wgOut->addHTML( $wgUser->getSkin()->link( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'deletelog-fulllog' ), + array(), + array( 'type' => 'delete', 'page' => $this->mTitle->getPrefixedText() ) + ) ); + } + $wgOut->addHTML( '</div>' ); + } + } + + /* * Should the parser cache be used? */ protected function useParserCache( $oldid ) { global $wgUser, $wgEnableParserCache; - + return $wgEnableParserCache && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 && $this->exists() @@ -931,47 +1000,48 @@ class Article { && !$this->mTitle->isCssOrJsPage() && !$this->mTitle->isCssJsSubpage(); } - - protected function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { + + /** + * View redirect + * @param $target Title object of destination to redirect + * @param $appendSubtitle Boolean [optional] + * @param $forceKnown Boolean: should the image be shown as a bluelink regardless of existence? + */ + public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { global $wgParser, $wgOut, $wgContLang, $wgStylePath, $wgUser; - # Display redirect $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png'; - + if( $appendSubtitle ) { $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) ); } $sk = $wgUser->getSkin(); - if ( $forceKnown ) + if( $forceKnown ) { $link = $sk->makeKnownLinkObj( $target, htmlspecialchars( $target->getFullText() ) ); - else + } else { $link = $sk->makeLinkObj( $target, htmlspecialchars( $target->getFullText() ) ); + } + return '<img src="'.$imageUrl.'" alt="#REDIRECT " />' . + '<span class="redirectText">'.$link.'</span>'; - $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' . - '<span class="redirectText">'.$link.'</span>' ); - } - function addTrackbacks() { + public 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()) + $dbr = wfGetDB( DB_SLAVE ); + $tbs = $dbr->select( 'trackbacks', + array('tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name'), + array('tb_page' => $this->getID() ) ); - - if (!$dbr->numrows($tbs)) - return; + if( !$dbr->numRows($tbs) ) return; $tbtext = ""; - while ($o = $dbr->fetchObject($tbs)) { + while( $o = $dbr->fetchObject($tbs) ) { $rmvtxt = ""; - if ($wgUser->isAllowed( 'trackback' )) { - $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid=" - . $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) ); + if( $wgUser->isAllowed( 'trackback' ) ) { + $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid=" . + $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) ); $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) ); } $tbtext .= "\n"; @@ -983,33 +1053,31 @@ class Article { $rmvtxt); } $wgOut->addWikiMsg( 'trackbackbox', $tbtext ); + $this->mTitle->invalidateCache(); } - function deletetrackback() { + public function deletetrackback() { global $wgUser, $wgRequest, $wgOut, $wgTitle; - - if (!$wgUser->matchEditToken($wgRequest->getVal('token'))) { + if( !$wgUser->matchEditToken($wgRequest->getVal('token')) ) { $wgOut->addWikiMsg( 'sessionfailure' ); return; } $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - - if (count($permission_errors)>0) - { + if( count($permission_errors) ) { $wgOut->showPermissionsErrorPage( $permission_errors ); return; } - $db = wfGetDB(DB_MASTER); - $db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid'))); - $wgTitle->invalidateCache(); - $wgOut->addWikiMsg('trackbackdeleteok'); + $db = wfGetDB( DB_MASTER ); + $db->delete( 'trackbacks', array('tb_id' => $wgRequest->getInt('tbid')) ); + + $wgOut->addWikiMsg( 'trackbackdeleteok' ); + $this->mTitle->invalidateCache(); } - function render() { + public function render() { global $wgOut; - $wgOut->setArticleBodyOnly(true); $this->view(); } @@ -1017,37 +1085,36 @@ class Article { /** * Handle action=purge */ - function purge() { + public function purge() { global $wgUser, $wgRequest, $wgOut; - - if ( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) { + if( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) { if( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { $this->doPurge(); + $this->view(); } } else { - $msg = $wgOut->parse( wfMsg( 'confirm_purge' ) ); - $action = htmlspecialchars( $_SERVER['REQUEST_URI'] ); - $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 ); - + $action = htmlspecialchars( $wgRequest->getRequestURL() ); + $button = wfMsgExt( 'confirm_purge_button', array('escapenoentities') ); + $form = "<form method=\"post\" action=\"$action\">\n" . + "<input type=\"submit\" name=\"submit\" value=\"$button\" />\n" . + "</form>\n"; + $top = wfMsgExt( 'confirm-purge-top', array('parse') ); + $bottom = wfMsgExt( 'confirm-purge-bottom', array('parse') ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->addHTML( $msg ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->addHTML( $top . $form . $bottom ); } } /** * Perform the actions of a page purging */ - function doPurge() { + public function doPurge() { global $wgUseSquid; // Invalidate the cache $this->mTitle->invalidateCache(); - if ( $wgUseSquid ) { + if( $wgUseSquid ) { // Commit the transaction before the purge is sent $dbw = wfGetDB( DB_MASTER ); $dbw->immediateCommit(); @@ -1056,16 +1123,15 @@ class Article { $update = SquidUpdate::newSimplePurge( $this->mTitle ); $update->doUpdate(); } - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { global $wgMessageCache; - if ( $this->getID() == 0 ) { + if( $this->getID() == 0 ) { $text = false; } else { $text = $this->getContent(); } $wgMessageCache->replace( $this->mTitle->getDBkey(), $text ); } - $this->view(); } /** @@ -1075,11 +1141,11 @@ class Article { * or else the record will be left in a funky state. * Best if all done inside a transaction. * - * @param Database $dbw - * @return int The newly created page_id key + * @param $dbw Database + * @return int The newly created page_id key, or false if the title already existed * @private */ - function insertOn( $dbw ) { + public function insertOn( $dbw ) { wfProfileIn( __METHOD__ ); $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); @@ -1095,31 +1161,33 @@ class Article { '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 ); + ), __METHOD__, 'IGNORE' ); + $affected = $dbw->affectedRows(); + if( $affected ) { + $newid = $dbw->insertId(); + $this->mTitle->resetArticleId( $newid ); + } wfProfileOut( __METHOD__ ); - return $newid; + return $affected ? $newid : false; } /** * 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. - * @param bool $lastRevIsRedirect If given, will optimize adding and - * removing rows in redirect table. + * @param $dbw Database object + * @param $revision Revision: For ID number, and text used to set + length and redirect status fields + * @param $lastRevision Integer: 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. + * @param $lastRevIsRedirect Boolean: if given, will optimize adding and + * removing rows in redirect table. * @return bool true on success, false on failure * @private */ - function updateRevisionOn( &$dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { + public function updateRevisionOn( &$dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { wfProfileIn( __METHOD__ ); $text = $revision->getText(); @@ -1143,8 +1211,7 @@ class Article { __METHOD__ ); $result = $dbw->affectedRows() != 0; - - if ($result) { + if( $result ) { $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); } @@ -1155,46 +1222,40 @@ class Article { /** * Add row to the redirect table if this is a redirect, remove otherwise. * - * @param Database $dbw + * @param $dbw Database * @param $redirectTitle a title object pointing to the redirect target, - * or NULL if this is not a redirect - * @param bool $lastRevIsRedirect If given, will optimize adding and - * removing rows in redirect table. + * or NULL if this is not a redirect + * @param $lastRevIsRedirect If given, will optimize adding and + * removing rows in redirect table. * @return bool true on success, false on failure * @private */ - function updateRedirectOn( &$dbw, $redirectTitle, $lastRevIsRedirect = null ) { - + public function updateRedirectOn( &$dbw, $redirectTitle, $lastRevIsRedirect = null ) { // Always update redirects (target link might have changed) // Update/Insert if we don't know if the last revision was a redirect or not // Delete if changing from redirect to non-redirect $isRedirect = !is_null($redirectTitle); - if ($isRedirect || is_null($lastRevIsRedirect) || $lastRevIsRedirect !== $isRedirect) { - + if($isRedirect || is_null($lastRevIsRedirect) || $lastRevIsRedirect !== $isRedirect) { wfProfileIn( __METHOD__ ); - - if ($isRedirect) { - + if( $isRedirect ) { // This title is a redirect, Add/Update row in the redirect table $set = array( /* SET */ 'rd_namespace' => $redirectTitle->getNamespace(), 'rd_title' => $redirectTitle->getDBkey(), 'rd_from' => $this->getId(), ); - $dbw->replace( 'redirect', array( 'rd_from' ), $set, __METHOD__ ); } else { // This is not a redirect, remove row from redirect table $where = array( 'rd_from' => $this->getId() ); $dbw->delete( 'redirect', $where, __METHOD__); } - - if( $this->getTitle()->getNamespace() == NS_IMAGE ) + if( $this->getTitle()->getNamespace() == NS_FILE ) { RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); + } wfProfileOut( __METHOD__ ); return ( $dbw->affectedRows() != 0 ); } - return true; } @@ -1202,12 +1263,11 @@ class Article { * 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 + * @param $dbw Database object + * @param $revision Revision object */ - function updateIfNewerOn( &$dbw, $revision ) { + public function updateIfNewerOn( &$dbw, $revision ) { wfProfileIn( __METHOD__ ); - $row = $dbw->selectRow( array( 'revision', 'page' ), array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), @@ -1227,28 +1287,27 @@ class Article { $prev = 0; $lastRevIsRedirect = null; } - $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); wfProfileOut( __METHOD__ ); return $ret; } /** + * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...) * @return string Complete article text, or null if error */ - function replaceSection($section, $text, $summary = '', $edittime = NULL) { + public function replaceSection( $section, $text, $summary = '', $edittime = NULL ) { wfProfileIn( __METHOD__ ); - - if( $section == '' ) { - // Whole-page edit; let the text through unmolested. + if( strval( $section ) == '' ) { + // Whole-page edit; let the whole text through } else { - if( is_null( $edittime ) ) { + 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 ) ) { + if( !$rev ) { wfDebug( "Article::replaceSection asked for bogus section (page: " . $this->getId() . "; section: $section; edittime: $edittime)\n" ); return null; @@ -1266,9 +1325,7 @@ class Article { global $wgParser; $text = $wgParser->replaceSection( $oldtext, $section, $text ); } - } - wfProfileOut( __METHOD__ ); return $text; } @@ -1277,27 +1334,28 @@ class Article { * @deprecated use Article::doEdit() */ function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false, $bot=false ) { + wfDeprecated( __METHOD__ ); $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $isminor ? EDIT_MINOR : 0 ) | ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) | ( $bot ? EDIT_FORCE_BOT : 0 ); # If this is a comment, add the summary as headline - if ( $comment && $summary != "" ) { + if( $comment && $summary != "" ) { $text = wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n".$text; } $this->doEdit( $text, $summary, $flags ); $dbw = wfGetDB( DB_MASTER ); - if ($watchthis) { - if (!$this->mTitle->userIsWatching()) { + if($watchthis) { + if(!$this->mTitle->userIsWatching()) { $dbw->begin(); $this->doWatch(); $dbw->commit(); } } else { - if ( $this->mTitle->userIsWatching() ) { + if( $this->mTitle->userIsWatching() ) { $dbw->begin(); $this->doUnwatch(); $dbw->commit(); @@ -1310,33 +1368,36 @@ class Article { * @deprecated use Article::doEdit() */ function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) { + wfDeprecated( __METHOD__ ); $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $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(); - } + $status = $this->doEdit( $text, $summary, $flags ); + if( !$status->isOK() ) { + return false; + } + + $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(); + } + } - $extraQuery = ''; // Give extensions a chance to modify URL query on update - wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) ); + $extraQuery = ''; // Give extensions a chance to modify URL query on update + wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) ); - $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery ); - } - return $good; + $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery ); + return true; } /** @@ -1347,9 +1408,9 @@ class Article { * * $wgUser must be set before calling this function. * - * @param string $text New text - * @param string $summary Edit summary - * @param integer $flags bitfield: + * @param $text String: new text + * @param $summary String: edit summary + * @param $flags Integer bitfield: * EDIT_NEW * Article is known or assumed to be non-existent, create a new one * EDIT_UPDATE @@ -1366,40 +1427,67 @@ class Article { * Fill in blank summaries with generated text where possible * * 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. - * @param $baseRevId, the revision ID this edit was based off, if any + * If EDIT_UPDATE is specified and the article doesn't exist, the function will an + * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an + * edit-already-exists error will be returned. These two conditions are also possible with + * auto-detection due to MediaWiki's performance-optimised locking strategy. + * + * @param $baseRevId the revision ID this edit was based off, if any + * @param $user Optional user object, $wgUser will be used if not passed * - * @return bool success + * @return Status object. Possible errors: + * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status + * edit-gone-missing: In update mode, but the article didn't exist + * edit-conflict: In update mode, the article changed unexpectedly + * edit-no-change: Warning that the text was the same as before + * edit-already-exists: In creation mode, but the article already exists + * + * Extensions may define additional errors. + * + * $return->value will contain an associative array with members as follows: + * new: Boolean indicating if the function attempted to create a new article + * revision: The revision object for the inserted revision, or null + * + * Compatibility note: this function previously returned a boolean value indicating success/failure */ - function doEdit( $text, $summary, $flags = 0, $baseRevId = false ) { + public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries; + # Low-level sanity check + if( $this->mTitle->getText() == '' ) { + throw new MWException( 'Something is trying to edit an article with an empty title' ); + } + wfProfileIn( __METHOD__ ); - $good = true; - if ( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) { - $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); - if ( $aid ) { + $user = is_null($user) ? $wgUser : $user; + $status = Status::newGood( array() ); + + # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already + $this->loadPageData(); + + if( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) { + $aid = $this->mTitle->getArticleID(); + if( $aid ) { $flags |= EDIT_UPDATE; } else { $flags |= EDIT_NEW; } } - if( !wfRunHooks( 'ArticleSave', array( &$this, &$wgUser, &$text, - &$summary, $flags & EDIT_MINOR, - null, null, &$flags ) ) ) + if( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, + $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) ) { wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); wfProfileOut( __METHOD__ ); - return false; + if( $status->isOK() ) { + $status->fatal( 'edit-hook-aborted'); + } + return $status; } # Silently ignore EDIT_MINOR if not allowed - $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit'); + $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed('minoredit'); $bot = $flags & EDIT_FORCE_BOT; $oldtext = $this->getContent(); @@ -1417,32 +1505,29 @@ class Article { $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); - if ( $flags & EDIT_UPDATE ) { + if( $flags & EDIT_UPDATE ) { # Update article, but only if changed. - + $status->value['new'] = false; # Make sure the revision is either completely inserted or not inserted at all if( !$wgDBtransactions ) { $userAbort = ignore_user_abort( true ); } - $lastRevision = 0; $revisionId = 0; $changed = ( strcmp( $text, $oldtext ) != 0 ); - if ( $changed ) { + if( $changed ) { $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 ) { + if( !$this->mLatest ) { # Article gone missing wfDebug( __METHOD__.": EDIT_UPDATE specified but article doesn't exist\n" ); + $status->fatal( 'edit-gone-missing' ); wfProfileOut( __METHOD__ ); - return false; + return $status; } $revision = new Revision( array( @@ -1450,38 +1535,54 @@ class Article { 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $text, - 'parent_id' => $lastRevision + 'parent_id' => $this->mLatest, + 'user' => $user->getId(), + 'user_text' => $user->getName(), ) ); $dbw->begin(); $revisionId = $revision->insertOn( $dbw ); # Update page - $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision ); + # + # Note that we use $this->mLatest instead of fetching a value from the master DB + # during the course of this function. This makes sure that EditPage can detect + # edit conflicts reliably, either by $ok here, or by $article->getTimestamp() + # before this function is called. A previous function used a separate query, this + # creates a window where concurrent edits can cause an ignored edit conflict. + $ok = $this->updateRevisionOn( $dbw, $revision, $this->mLatest ); if( !$ok ) { /* Belated edit conflict! Run away!! */ - $good = false; + $status->fatal( 'edit-conflict' ); + # Delete the invalid revision if the DB is not transactional + if( !$wgDBtransactions ) { + $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ ); + } + $revisionId = 0; $dbw->rollback(); } else { - wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId ) ); - + global $wgUseRCPatrol; + wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, $baseRevId, $user) ); # 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 - if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) { - RecentChange::markPatrolled( $rcid ); - PatrolLog::record( $rcid, true ); + $patrolled = $wgUseRCPatrol && $this->mTitle->userCan('autopatrol'); + # Add RC row to the DB + $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, + $this->mLatest, $this->getTimestamp(), $bot, '', $oldsize, $newsize, + $revisionId, $patrolled + ); + # Log auto-patrolled edits + if( $patrolled ) { + PatrolLog::record( $rc, true ); } } - $wgUser->incEditCount(); + $user->incEditCount(); $dbw->commit(); } } else { + $status->warning( 'edit-no-change' ); $revision = null; // Keep the same revision ID, but do some updates on it $revisionId = $this->getRevIdFetched(); @@ -1493,17 +1594,20 @@ class Article { 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. - $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); + // Now that ignore_user_abort is restored, we can respond to fatal errors + if( !$status->isOK() ) { + wfProfileOut( __METHOD__ ); + return $status; } + + # Invalidate cache of this article and all pages using this article + # as a template. Partly deferred. Leave templatelinks for editUpdates(). + Article::onArticleEdit( $this->mTitle, 'skiptransclusions' ); + # Update links tables, site stats, etc. + $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); } else { # Create new article + $status->value['new'] = true; # Set statistics members # We work out if it's countable after PST to avoid counter drift @@ -1514,15 +1618,24 @@ class Article { $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 + # This will return false if the article already exists $newid = $this->insertOn( $dbw ); + if( $newid === false ) { + $dbw->rollback(); + $status->fatal( 'edit-already-exists' ); + wfProfileOut( __METHOD__ ); + return $status; + } + # Save the revision text... $revision = new Revision( array( 'page' => $newid, 'comment' => $summary, 'minor_edit' => $isminor, - 'text' => $text + 'text' => $text, + 'user' => $user->getId(), + 'user_text' => $user->getName(), ) ); $revisionId = $revision->insertOn( $dbw ); @@ -1530,19 +1643,22 @@ class Article { # Update the page record with revision data $this->updateRevisionOn( $dbw, $revision, 0 ); - - wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) ); + wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false, $user) ); + # Update recentchanges 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 - if( ($GLOBALS['wgUseRCPatrol'] || $GLOBALS['wgUseNPPatrol']) && $wgUser->isAllowed( 'autopatrol' ) ) { - RecentChange::markPatrolled( $rcid ); - PatrolLog::record( $rcid, true ); + global $wgUseRCPatrol, $wgUseNPPatrol; + # Mark as patrolled if the user can do so + $patrolled = ($wgUseRCPatrol || $wgUseNPPatrol) && $this->mTitle->userCan('autopatrol'); + # Add RC row to the DB + $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, + '', strlen($text), $revisionId, $patrolled ); + # Log auto-patrolled edits + if( $patrolled ) { + PatrolLog::record( $rc, true ); } } - $wgUser->incEditCount(); + $user->incEditCount(); $dbw->commit(); # Update links, etc. @@ -1551,27 +1667,30 @@ class Article { # Clear caches Article::onArticleCreate( $this->mTitle ); - wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); + wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); } - if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) { + # Do updates right now unless deferral was requested + if( !( $flags & EDIT_DEFER_UPDATES ) ) { wfDoUpdates(); } - if ( $good ) { - wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); - } + // Return the new revision (or null) to the caller + $status->value['revision'] = $revision; + + wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status ) ); wfProfileOut( __METHOD__ ); - return $good; + return $status; } /** * @deprecated wrapper for doRedirect */ - function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) { + public function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) { + wfDeprecated( __METHOD__ ); $this->doRedirect( $this->isRedirect( $text ), $sectionanchor ); } @@ -1579,13 +1698,13 @@ class Article { * 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 "#" - * @param string $extraQuery, extra query params + * @param $noRedir Boolean: add redirect=no + * @param $sectionAnchor String: section to redirect to, including "#" + * @param $extraQuery String: extra query params */ - function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { + public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { global $wgOut; - if ( $noRedir ) { + if( $noRedir ) { $query = 'redirect=no'; if( $extraQuery ) $query .= "&$query"; @@ -1598,77 +1717,45 @@ class Article { /** * Mark this particular edit/page as patrolled */ - function markpatrolled() { + public function markpatrolled() { global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUseNPPatrol, $wgUser; $wgOut->setRobotPolicy( 'noindex,nofollow' ); - # Check patrol config options - - if ( !($wgUseNPPatrol || $wgUseRCPatrol)) { - $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); - return; - } - # If we haven't been given an rc_id value, we can't do anything $rcid = (int) $wgRequest->getVal('rcid'); - $rc = $rcid ? RecentChange::newFromId($rcid) : null; - if ( is_null ( $rc ) ) - { + $rc = RecentChange::newFromId($rcid); + if( is_null($rc) ) { $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); return; } - if ( !$wgUseRCPatrol && $rc->getAttribute( 'rc_type' ) != RC_NEW) { - // Only new pages can be patrolled if the general patrolling is off....??? - // @fixme -- is this necessary? Shouldn't we only bother controlling the - // front end here? - $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); - return; - } + #It would be nice to see where the user had actually come from, but for now just guess + $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges'; + $return = Title::makeTitle( NS_SPECIAL, $returnto ); - # Check permissions - $permission_errors = $this->mTitle->getUserPermissionsErrors( 'patrol', $wgUser ); + $dbw = wfGetDB( DB_MASTER ); + $errors = $rc->doMarkPatrolled(); - if (count($permission_errors)>0) - { - $wgOut->showPermissionsErrorPage( $permission_errors ); + if( in_array(array('rcpatroldisabled'), $errors) ) { + $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); return; } - - # Handle the 'MarkPatrolled' hook - if( !wfRunHooks( 'MarkPatrolled', array( $rcid, &$wgUser, false ) ) ) { + + if( in_array(array('hookaborted'), $errors) ) { + // The hook itself has handled any output return; } - - #It would be nice to see where the user had actually come from, but for now just guess - $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges'; - $return = Title::makeTitle( NS_SPECIAL, $returnto ); - - # If it's left up to us, check that the user is allowed to patrol this edit - # If the user has the "autopatrol" right, then we'll assume there are no - # other conditions stopping them doing so - if( !$wgUser->isAllowed( 'autopatrol' ) ) { - $rc = RecentChange::newFromId( $rcid ); - # Graceful error handling, as we've done before here... - # (If the recent change doesn't exist, then it doesn't matter whether - # the user is allowed to patrol it or not; nothing is going to happen - if( is_object( $rc ) && $wgUser->getName() == $rc->getAttribute( 'rc_user_text' ) ) { - # The user made this edit, and can't patrol it - # Tell them so, and then back off - $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) ); - $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' ); - $wgOut->returnToMain( false, $return ); - return; - } + + if( in_array(array('markedaspatrollederror-noautopatrol'), $errors) ) { + $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) ); + $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' ); + $wgOut->returnToMain( false, $return ); + return; } - # Check that the revision isn't patrolled already - # Prevents duplicate log entries - if( !$rc->getAttribute( 'rc_patrolled' ) ) { - # Mark the edit as patrolled - RecentChange::markPatrolled( $rcid ); - PatrolLog::record( $rcid ); - wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); + if( !empty($errors) ) { + $wgOut->showPermissionsErrorPage( $errors ); + return; } # Inform the user @@ -1681,26 +1768,21 @@ class Article { * User-interface handler for the "watch" action */ - function watch() { - + public function watch() { global $wgUser, $wgOut; - - if ( $wgUser->isAnon() ) { + if( $wgUser->isAnon() ) { $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); return; } - if ( wfReadOnly() ) { + if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - if( $this->doWatch() ) { $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() ); } - $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); } @@ -1708,44 +1790,36 @@ class Article { * Add this page to $wgUser's watchlist * @return bool true on successful watch operation */ - function doWatch() { + public function doWatch() { global $wgUser; if( $wgUser->isAnon() ) { return false; } - - if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) { + if( wfRunHooks('WatchArticle', array(&$wgUser, &$this)) ) { $wgUser->addWatch( $this->mTitle ); - return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this)); } - return false; } /** * User interface handler for the "unwatch" action. */ - function unwatch() { - + public function unwatch() { global $wgUser, $wgOut; - - if ( $wgUser->isAnon() ) { + if( $wgUser->isAnon() ) { $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); return; } - if ( wfReadOnly() ) { + if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - if( $this->doUnwatch() ) { $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() ); } - $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); } @@ -1753,25 +1827,22 @@ class Article { * Stop watching a page * @return bool true on successful unwatch */ - function doUnwatch() { + public function doUnwatch() { global $wgUser; if( $wgUser->isAnon() ) { return false; } - - if (wfRunHooks('UnwatchArticle', array(&$wgUser, &$this))) { + if( wfRunHooks('UnwatchArticle', array(&$wgUser, &$this)) ) { $wgUser->removeWatch( $this->mTitle ); - return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this)); } - return false; } /** * action=protect handler */ - function protect() { + public function protect() { $form = new ProtectionForm( $this ); $form->execute(); } @@ -1779,26 +1850,28 @@ class Article { /** * action=unprotect handler (alias) */ - function unprotect() { + public 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 + * @param $limit Array: set of restriction keys + * @param $reason String + * @param &$cascade Integer. Set to false if cascading protection isn't allowed. + * @param $expiry Array: per restriction type expiration * @return bool true on success */ - function updateRestrictions( $limit = array(), $reason = '', $cascade = 0, $expiry = null ) { + public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) { global $wgUser, $wgRestrictionTypes, $wgContLang; $id = $this->mTitle->getArticleID(); - if( array() != $this->mTitle->getUserPermissionsErrors( 'protect', $wgUser ) || wfReadOnly() || $id == 0 ) { + if( $id <= 0 || wfReadOnly() || !$this->mTitle->userCan('protect') ) { return false; } - if (!$cascade) { + if( !$cascade ) { $cascade = false; } @@ -1808,34 +1881,39 @@ class Article { # 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 ) ); + $updated = Article::flattenRestrictions( $limit ); + $changed = false; + foreach( $wgRestrictionTypes as $action ) { + if( isset( $expiry[$action] ) ) { + # Get current restrictions on $action + $aLimits = $this->mTitle->getRestrictions( $action ); + $current[$action] = implode( '', $aLimits ); + # Are any actual restrictions being dealt with here? + $aRChanged = count($aLimits) || !empty($limit[$action]); + # If something changed, we need to log it. Checking $aRChanged + # assures that "unprotecting" a page that is not protected does + # not log just because the expiry was "changed". + if( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) { + $changed = true; + } + } + } $current = Article::flattenRestrictions( $current ); - $updated = Article::flattenRestrictions( $limit ); - $changed = ( $current != $updated ); + $changed = ($changed || $current != $updated ); $changed = $changed || ($updated && $this->mTitle->areRestrictionsCascading() != $cascade); - $changed = $changed || ($updated && $this->mTitle->mRestrictionsExpiry != $expiry); $protect = ( $updated != '' ); # If nothing's changed, do nothing if( $changed ) { - global $wgGroupPermissions; if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) { $dbw = wfGetDB( DB_MASTER ); - - $encodedExpiry = Block::encodeExpiry($expiry, $dbw ); - - $expiry_description = ''; - if ( $encodedExpiry != 'infinity' ) { - $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry, false, false ) ).')'; - } - + # Prepare a null revision to be added to the history $modified = $current != '' && $protect; - if ( $protect ) { + if( $protect ) { $comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle'; } else { $comment_type = 'unprotectedarticle'; @@ -1844,35 +1922,51 @@ class Article { # Only restrictions with the 'protect' right can cascade... # Otherwise, people who cannot normally protect can "protect" pages via transclusion - foreach( $limit as $action => $restriction ) { - # FIXME: can $restriction be an array or what? (same as fixme above) - if( $restriction != 'protect' && $restriction != 'sysop' ) { - $cascade = false; - break; - } - } - - $cascade_description = ''; - if ($cascade) { - $cascade_description = ' ['.wfMsg('protect-summary-cascade').']'; + $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' ); + # The schema allows multiple restrictions + if(!in_array('protect', $editrestriction) && !in_array('sysop', $editrestriction)) + $cascade = false; + $cascade_description = ''; + if( $cascade ) { + $cascade_description = ' ['.wfMsgForContent('protect-summary-cascade').']'; } if( $reason ) $comment .= ": $reason"; - if( $protect ) - $comment .= " [$updated]"; - if ( $expiry_description && $protect ) - $comment .= "$expiry_description"; - if ( $cascade ) - $comment .= "$cascade_description"; + $editComment = $comment; + $encodedExpiry = array(); + $protect_description = ''; + foreach( $limit as $action => $restrictions ) { + $encodedExpiry[$action] = Block::encodeExpiry($expiry[$action], $dbw ); + if( $restrictions != '' ) { + $protect_description .= "[$action=$restrictions] ("; + if( $encodedExpiry[$action] != 'infinity' ) { + $protect_description .= wfMsgForContent( 'protect-expiring', + $wgContLang->timeanddate( $expiry[$action], false, false ) , + $wgContLang->date( $expiry[$action], false, false ) , + $wgContLang->time( $expiry[$action], false, false ) ); + } else { + $protect_description .= wfMsgForContent( 'protect-expiry-indefinite' ); + } + $protect_description .= ') '; + } + } + $protect_description = trim($protect_description); + + if( $protect_description && $protect ) + $editComment .= " ($protect_description)"; + if( $cascade ) + $editComment .= "$cascade_description"; # Update restrictions table foreach( $limit as $action => $restrictions ) { - if ($restrictions != '' ) { + if($restrictions != '' ) { $dbw->replace( 'page_restrictions', array(array('pr_page', 'pr_type')), - array( 'pr_page' => $id, 'pr_type' => $action - , 'pr_level' => $restrictions, 'pr_cascade' => $cascade ? 1 : 0 - , 'pr_expiry' => $encodedExpiry ), __METHOD__ ); + array( 'pr_page' => $id, + 'pr_type' => $action, + 'pr_level' => $restrictions, + 'pr_cascade' => ($cascade && $action == 'edit') ? 1 : 0, + 'pr_expiry' => $encodedExpiry[$action] ), __METHOD__ ); } else { $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, 'pr_type' => $action ), __METHOD__ ); @@ -1880,9 +1974,10 @@ class Article { } # Insert a null revision - $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); + $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true ); $nullRevId = $nullRevision->insertOn( $dbw ); + $latest = $this->getLatest(); # Update page record $dbw->update( 'page', array( /* SET */ @@ -1893,15 +1988,15 @@ class Article { 'page_id' => $id ), 'Article::protect' ); - - wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, false) ); + + wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, $latest, $wgUser) ); wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) ); # Update the protection log $log = new LogPage( 'protect' ); if( $protect ) { - $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, - trim( $reason . " [$updated]$cascade_description$expiry_description" ) ); + $params = array($protect_description,$cascade ? 'cascade' : ''); + $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason), $params ); } else { $log->addEntry( 'unprotect', $this->mTitle, $reason ); } @@ -1915,11 +2010,10 @@ class Article { /** * 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 + * @param $limit Array + * @return String */ - function flattenRestrictions( $limit ) { + protected static function flattenRestrictions( $limit ) { if( !is_array( $limit ) ) { throw new MWException( 'Article::flattenRestrictions given non-array restriction set' ); } @@ -1935,26 +2029,24 @@ class Article { /** * Auto-generates a deletion reason - * @param bool &$hasHistory Whether the page has a history + * @param &$hasHistory Boolean: whether the page has a history */ - public function generateReason(&$hasHistory) - { + public function generateReason( &$hasHistory ) { global $wgContLang; - $dbw = wfGetDB(DB_MASTER); + $dbw = wfGetDB( DB_MASTER ); // Get the last revision - $rev = Revision::newFromTitle($this->mTitle); - if(is_null($rev)) + $rev = Revision::newFromTitle( $this->mTitle ); + if( is_null( $rev ) ) return false; + // Get the article's contents $contents = $rev->getText(); $blank = false; // If the page is blank, use the text from the previous revision, // which can only be blank if there's a move/import/protect dummy revision involved - if($contents == '') - { + if( $contents == '' ) { $prev = $rev->getPrevious(); - if($prev) - { + if( $prev ) { $contents = $prev->getText(); $blank = true; } @@ -1963,44 +2055,51 @@ class Article { // Find out if there was only one contributor // Only scan the last 20 revisions $limit = 20; - $res = $dbw->select('revision', 'rev_user_text', array('rev_page' => $this->getID()), __METHOD__, - array('LIMIT' => $limit)); - if($res === false) + $res = $dbw->select( 'revision', 'rev_user_text', + array( 'rev_page' => $this->getID() ), __METHOD__, + array( 'LIMIT' => $limit ) + ); + if( $res === false ) // This page has no revisions, which is very weird return false; - if($res->numRows() > 1) + if( $res->numRows() > 1 ) $hasHistory = true; else $hasHistory = false; - $row = $dbw->fetchObject($res); + $row = $dbw->fetchObject( $res ); $onlyAuthor = $row->rev_user_text; // Try to find a second contributor - while( $row = $dbw->fetchObject($res) ) { - if($row->rev_user_text != $onlyAuthor) { + foreach( $res as $row ) { + if( $row->rev_user_text != $onlyAuthor ) { $onlyAuthor = false; break; } } - $dbw->freeResult($res); + $dbw->freeResult( $res ); // Generate the summary with a '$1' placeholder - if($blank) { + if( $blank ) { // The current revision is blank and the one before is also // blank. It's just not our lucky day - $reason = wfMsgForContent('exbeforeblank', '$1'); + $reason = wfMsgForContent( 'exbeforeblank', '$1' ); } else { - if($onlyAuthor) - $reason = wfMsgForContent('excontentauthor', '$1', $onlyAuthor); + if( $onlyAuthor ) + $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor ); else - $reason = wfMsgForContent('excontent', '$1'); + $reason = wfMsgForContent( 'excontent', '$1' ); + } + + if( $reason == '-' ) { + // Allow these UI messages to be blanked out cleanly + return ''; } // Replace newlines with spaces to prevent uglyness - $contents = preg_replace("/[\n\r]/", ' ', $contents); + $contents = preg_replace( "/[\n\r]/", ' ', $contents ); // Calculate the maximum amount of chars to get // Max content length = max comment length - length of the comment (excl. $1) - '...' - $maxLength = 255 - (strlen($reason) - 2) - 3; - $contents = $wgContLang->truncate($contents, $maxLength, '...'); + $maxLength = 255 - (strlen( $reason ) - 2) - 3; + $contents = $wgContLang->truncate( $contents, $maxLength, '...' ); // Remove possible unfinished links $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); // Now replace the '$1' placeholder @@ -2012,7 +2111,7 @@ class Article { /* * UI entry point for page deletion */ - function delete() { + public function delete() { global $wgUser, $wgOut, $wgRequest; $confirm = $wgRequest->wasPosted() && @@ -2023,19 +2122,19 @@ class Article { $reason = $this->DeleteReasonList; - if ( $reason != 'other' && $this->DeleteReason != '') { + if( $reason != 'other' && $this->DeleteReason != '' ) { // Entry from drop down menu + additional comment $reason .= ': ' . $this->DeleteReason; - } elseif ( $reason == 'other' ) { + } elseif( $reason == 'other' ) { $reason = $this->DeleteReason; } # Flag to hide all contents of the archived revisions - $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('suppressrevision'); + $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' ); # This code desperately needs to be totally rewritten # Read-only check... - if ( wfReadOnly() ) { + if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } @@ -2043,7 +2142,7 @@ class Article { # Check permissions $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - if (count($permission_errors)>0) { + if( count( $permission_errors ) > 0 ) { $wgOut->showPermissionsErrorPage( $permission_errors ); return; } @@ -2054,8 +2153,10 @@ class Article { $dbw = wfGetDB( DB_MASTER ); $conds = $this->mTitle->pageCond(); $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); - if ( $latest === false ) { - $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + if( $latest === false ) { + $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array( 'parse' ) ) ); + $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); + LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() ); return; } @@ -2080,12 +2181,12 @@ class Article { // Generate deletion reason $hasHistory = false; - if ( !$reason ) $reason = $this->generateReason($hasHistory); + if( !$reason ) $reason = $this->generateReason($hasHistory); // If the page has a history, insert a warning if( $hasHistory && !$confirm ) { - $skin=$wgUser->getSkin(); - $wgOut->addHTML( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' ); + $skin = $wgUser->getSkin(); + $wgOut->addHTML( '<strong>' . wfMsgExt( 'historywarning', array( 'parseinline' ) ) . ' ' . $skin->historyLink() . '</strong>' ); if( $bigHistory ) { global $wgLang, $wgDeleteRevisionsLimit; $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", @@ -2099,7 +2200,7 @@ class Article { /** * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions */ - function isBigDeletion() { + public function isBigDeletion() { global $wgDeleteRevisionsLimit; if( $wgDeleteRevisionsLimit ) { $revCount = $this->estimateRevisionCount(); @@ -2111,8 +2212,8 @@ class Article { /** * @return int approximate revision count */ - function estimateRevisionCount() { - $dbr = wfGetDB(); + public function estimateRevisionCount() { + $dbr = wfGetDB( DB_SLAVE ); // For an exact count... //return $dbr->selectField( 'revision', 'COUNT(*)', // array( 'rev_page' => $this->getId() ), __METHOD__ ); @@ -2122,13 +2223,12 @@ class Article { /** * 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) + * @param $num Integer: number of revisions to get + * @param $revLatest String: the latest rev_id, selected from the master (optional) * @return array Array of authors, duplicates not removed */ - function getLastNAuthors( $num, $revLatest = 0 ) { + public function getLastNAuthors( $num, $revLatest = 0 ) { wfProfileIn( __METHOD__ ); - // First try the slave // If that doesn't have the latest revision, try the master $continue = 2; @@ -2145,12 +2245,12 @@ class Article { 'LIMIT' => $num ) ) ); - if ( !$res ) { + if( !$res ) { wfProfileOut( __METHOD__ ); return array(); } $row = $db->fetchObject( $res ); - if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { + if( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { $db = wfGetDB( DB_MASTER ); $continue--; } else { @@ -2168,59 +2268,67 @@ class Article { /** * Output deletion confirmation dialog - * @param $reason string Prefilled reason + * @param $reason String: prefilled reason */ - function confirmDelete( $reason ) { - global $wgOut, $wgUser, $wgContLang; - $align = $wgContLang->isRtl() ? 'left' : 'right'; + public function confirmDelete( $reason ) { + global $wgOut, $wgUser; wfDebug( "Article::confirmDelete\n" ); - $wgOut->setSubtitle( wfMsg( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'confirmdeletetext' ); if( $wgUser->isAllowed( 'suppressrevision' ) ) { - $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"><td></td><td>"; - $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) ); - $suppress .= "</td></tr>"; + $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"> + <td></td> + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'revdelete-suppress' ), + 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) . + "</td> + </tr>"; } else { $suppress = ''; } + $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(); - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) . Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) . - Xml::openElement( 'table' ) . + Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) . "<tr id=\"wpDeleteReasonListRow\"> - <td align='$align'>" . + <td class='mw-label'>" . Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) . "</td> - <td>" . + <td class='mw-input'>" . Xml::listDropDown( 'wpDeleteReasonList', wfMsgForContent( 'deletereason-dropdown' ), wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) . "</td> </tr> <tr id=\"wpDeleteReasonRow\"> - <td align='$align'>" . + <td class='mw-label'>" . Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) . "</td> - <td>" . - Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . + <td class='mw-input'>" . + Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', + 'tabindex' => '2', 'id' => 'wpReason' ) ) . "</td> </tr> <tr> <td></td> - <td>" . - Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) . + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'watchthis' ), + 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . "</td> </tr> $suppress <tr> <td></td> - <td>" . - Xml::submitButton( wfMsg( 'deletepage' ), array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '4' ) ) . + <td class='mw-submit'>" . + Xml::submitButton( wfMsg( 'deletepage' ), + array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) . "</td> </tr>" . Xml::closeElement( 'table' ) . @@ -2228,43 +2336,30 @@ class Article { Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . Xml::closeElement( 'form' ); - if ( $wgUser->isAllowed( 'editinterface' ) ) { + if( $wgUser->isAllowed( 'editinterface' ) ) { $skin = $wgUser->getSkin(); $link = $skin->makeLink ( 'MediaWiki:Deletereason-dropdown', wfMsgHtml( 'delete-edit-reasonlist' ) ); $form .= '<p class="mw-delete-editreasons">' . $link . '</p>'; } $wgOut->addHTML( $form ); - $this->showLogExtract( $wgOut ); - } - - - /** - * Show relevant lines from the deletion log - */ - function showLogExtract( $out ) { - $out->addHtml( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); - LogEventsList::showLogExtract( $out, 'delete', $this->mTitle->getPrefixedText() ); + LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() ); } - /** * Perform a deletion and output success or failure messages */ - function doDelete( $reason, $suppress = false ) { + public function doDelete( $reason, $suppress = false ) { global $wgOut, $wgUser; - wfDebug( __METHOD__."\n" ); - - $id = $this->getId(); - - $error = ''; + $id = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); - if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason, &$error))) { - if ( $this->doDeleteArticle( $reason, $suppress ) ) { + $error = ''; + if( wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason, &$error)) ) { + if( $this->doDeleteArticle( $reason, $suppress, $id ) ) { $deleted = $this->mTitle->getPrefixedText(); $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'; @@ -2272,10 +2367,13 @@ class Article { $wgOut->returnToMain( false ); wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason, $id)); } else { - if ($error = '') - $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); - else + if( $error == '' ) { + $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array( 'parse' ) ) ); + $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); + LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() ); + } else { $wgOut->showFatalError( $error ); + } } } } @@ -2285,7 +2383,7 @@ class Article { * Deletes the article with database consistency, writes logs, purges caches * Returns success */ - function doDeleteArticle( $reason, $suppress = false ) { + public function doDeleteArticle( $reason, $suppress = false, $id = 0 ) { global $wgUseSquid, $wgDeferredUpdateList; global $wgUseTrackbacks; @@ -2294,9 +2392,9 @@ class Article { $dbw = wfGetDB( DB_MASTER ); $ns = $this->mTitle->getNamespace(); $t = $this->mTitle->getDBkey(); - $id = $this->mTitle->getArticleID(); + $id = $id ? $id : $this->mTitle->getArticleID( GAID_FOR_UPDATE ); - if ( $t == '' || $id == 0 ) { + if( $t == '' || $id == 0 ) { return false; } @@ -2304,7 +2402,7 @@ class Article { array_push( $wgDeferredUpdateList, $u ); // Bitfields to further suppress the content - if ( $suppress ) { + if( $suppress ) { $bitfield = 0; // This should be 15... $bitfield |= Revision::DELETED_TEXT; @@ -2351,15 +2449,6 @@ class Article { # Delete restrictions for it $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); - # Fix category table counts - $cats = array(); - $res = $dbw->select( 'categorylinks', 'cl_to', - array( 'cl_from' => $id ), __METHOD__ ); - foreach( $res as $row ) { - $cats []= $row->cl_to; - } - $this->updateCategoryCounts( array(), $cats ); - # Now that it's safely backed up, delete it $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__); $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy @@ -2367,12 +2456,20 @@ class Article { $dbw->rollback(); return false; } + + # Fix category table counts + $cats = array(); + $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ ); + foreach( $res as $row ) { + $cats []= $row->cl_to; + } + $this->updateCategoryCounts( array(), $cats ); # If using cascading deletes, we can skip some explicit deletes - if ( !$dbw->cascadingDeletes() ) { + if( !$dbw->cascadingDeletes() ) { $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); - if ($wgUseTrackbacks) + if($wgUseTrackbacks) $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ ); # Delete outgoing links @@ -2386,14 +2483,17 @@ class Article { } # If using cleanup triggers, we can skip some manual deletes - if ( !$dbw->cleanupTriggers() ) { - + if( !$dbw->cleanupTriggers() ) { # Clean up recentchanges entries... $dbw->delete( 'recentchanges', - array( 'rc_namespace' => $ns, 'rc_title' => $t, 'rc_type != '.RC_LOG ), + array( 'rc_type != '.RC_LOG, + 'rc_namespace' => $this->mTitle->getNamespace(), + 'rc_title' => $this->mTitle->getDBKey() ), + __METHOD__ ); + $dbw->delete( 'recentchanges', + array( 'rc_type != '.RC_LOG, 'rc_cur_id' => $id ), __METHOD__ ); } - $dbw->commit(); # Clear caches Article::onArticleDelete( $this->mTitle ); @@ -2409,6 +2509,8 @@ class Article { # Make sure logging got through $log->addEntry( 'delete', $this->mTitle, $reason, array() ); + $dbw->commit(); + return true; } @@ -2419,12 +2521,12 @@ class Article { * performs permissions checks on $wgUser, then calls commitRollback() * to do the dirty work * - * @param string $fromP - Name of the user whose edits to rollback. - * @param string $summary - Custom summary. Set to default summary if empty. - * @param string $token - Rollback token. - * @param bool $bot - If true, mark all reverted edits as bot. + * @param $fromP String: Name of the user whose edits to rollback. + * @param $summary String: Custom summary. Set to default summary if empty. + * @param $token String: Rollback token. + * @param $bot Boolean: If true, mark all reverted edits as bot. * - * @param array $resultDetails contains result-specific array of additional values + * @param $resultDetails Array: contains result-specific array of additional values * 'alreadyrolled' : 'current' (rev) * success : 'summary' (str), 'current' (rev), 'target' (rev) * @@ -2438,16 +2540,18 @@ class Article { $resultDetails = null; # Check permissions - $errors = array_merge( $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ), - $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) ); + $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ); + $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ); + $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); + if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) $errors[] = array( 'sessionfailure' ); - if ( $wgUser->pingLimiter('rollback') || $wgUser->pingLimiter() ) { + if( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) { $errors[] = array( 'actionthrottledtext' ); } # If there were errors, bail out now - if(!empty($errors)) + if( !empty( $errors ) ) return $errors; return $this->commitRollback($fromP, $summary, $bot, $resultDetails); @@ -2493,7 +2597,7 @@ class Article { $s = $dbw->selectRow( 'revision', array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), array( 'rev_page' => $current->getPage(), - "rev_user <> {$user} OR rev_user_text <> {$user_text}" + "rev_user != {$user} OR rev_user_text != {$user_text}" ), __METHOD__, array( 'USE INDEX' => 'page_timestamp', 'ORDER BY' => 'rev_timestamp DESC' ) @@ -2507,16 +2611,16 @@ class Article { } $set = array(); - if ( $bot && $wgUser->isAllowed('markbotedits') ) { + if( $bot && $wgUser->isAllowed('markbotedits') ) { # Mark all reverted edits as bot $set['rc_bot'] = 1; } - if ( $wgUseRCPatrol ) { + if( $wgUseRCPatrol ) { # Mark all reverted edits as patrolled $set['rc_patrolled'] = 1; } - if ( $set ) { + if( $set ) { $dbw->update( 'recentchanges', $set, array( /* WHERE */ 'rc_cur_id' => $current->getPage(), @@ -2531,31 +2635,38 @@ class Article { if( empty( $summary ) ){ $summary = wfMsgForContent( 'revertpage' ); } - + # Allow the custom summary to use the same args as the default message $args = array( $target->getUserText(), $from, $s->rev_id, $wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true), $current->getId(), $wgLang->timeanddate($current->getTimestamp()) ); - $summary = wfMsgReplaceArgs( $summary, $args ); + $summary = wfMsgReplaceArgs( $summary, $args ); # Save $flags = EDIT_UPDATE; - if ($wgUser->isAllowed('minoredit')) + if( $wgUser->isAllowed('minoredit') ) $flags |= EDIT_MINOR; if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) ) $flags |= EDIT_FORCE_BOT; - $this->doEdit( $target->getText(), $summary, $flags, $target->getId() ); + # Actually store the edit + $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() ); + if( !empty( $status->value['revision'] ) ) { + $revId = $status->value['revision']->getId(); + } else { + $revId = false; + } - wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) ); + wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target, $current ) ); $resultDetails = array( 'summary' => $summary, 'current' => $current, 'target' => $target, + 'newid' => $revId ); return array(); } @@ -2563,7 +2674,7 @@ class Article { /** * User interface for rollback operations */ - function rollback() { + public function rollback() { global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; $details = null; @@ -2575,15 +2686,11 @@ class Article { $details ); - if( in_array( array( 'blocked' ), $result ) ) { - $wgOut->blockedPage(); - return; - } if( in_array( array( 'actionthrottledtext' ), $result ) ) { $wgOut->rateLimited(); return; } - if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ){ + if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ) { $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); $errArray = $result[0]; $errMsg = array_shift( $errArray ); @@ -2591,7 +2698,8 @@ class Article { if( isset( $details['current'] ) ){ $current = $details['current']; if( $current->getComment() != '' ) { - $wgOut->addWikiMsgArray( 'editcomment', array( $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) ); + $wgOut->addWikiMsgArray( 'editcomment', array( + $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) ); } } return; @@ -2599,7 +2707,7 @@ class Article { # Display permissions errors before read-only message -- there's no # point in misleading the user into thinking the inability to rollback # is only temporary. - if( !empty($result) && $result !== array( array('readonlytext') ) ) { + if( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) { # array_diff is completely broken for arrays of arrays, sigh. Re- # move any 'readonlytext' error manually. $out = array(); @@ -2611,24 +2719,25 @@ class Article { $wgOut->showPermissionsErrorPage( $out ); return; } - if( $result == array( array('readonlytext') ) ) { + if( $result == array( array( 'readonlytext' ) ) ) { $wgOut->readOnlyPage(); return; } $current = $details['current']; $target = $details['target']; + $newId = $details['newid']; $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() ) . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() ); $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() ) . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() ); - $wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) ); + $wgOut->addHTML( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) ); $wgOut->returnToMain( false, $this->mTitle ); - - if( !$wgRequest->getBool( 'hidediff', false ) ) { - $de = new DifferenceEngine( $this->mTitle, $current->getId(), 'next', false, true ); + + if( !$wgRequest->getBool( 'hidediff', false ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) { + $de = new DifferenceEngine( $this->mTitle, $current->getId(), $newId, false, true ); $de->showDiff( '', '' ); } } @@ -2636,21 +2745,15 @@ class Article { /** * Do standard deferred updates after page view - * @private */ - function viewUpdates() { - global $wgDeferredUpdateList, $wgUser; - - if ( 0 != $this->getID() ) { - # Don't update page view counters on views from bot users (bug 14044) - global $wgDisableCounters; - if( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) ) { - Article::incViewCount( $this->getID() ); - $u = new SiteStatsUpdate( 1, 0, 0 ); - array_push( $wgDeferredUpdateList, $u ); - } + public function viewUpdates() { + global $wgDeferredUpdateList, $wgDisableCounters, $wgUser; + # Don't update page view counters on views from bot users (bug 14044) + if( !$wgDisableCounters && !$wgUser->isAllowed('bot') && $this->getID() ) { + Article::incViewCount( $this->getID() ); + $u = new SiteStatsUpdate( 1, 0, 0 ); + array_push( $wgDeferredUpdateList, $u ); } - # Update newtalk / watchlist notification status $wgUser->clearNotification( $this->mTitle ); } @@ -2659,8 +2762,8 @@ class Article { * Prepare text which is about to be saved. * Returns a stdclass with source, pst and output members */ - function prepareTextForEdit( $text, $revid=null ) { - if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) { + public function prepareTextForEdit( $text, $revid=null ) { + if( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) { // Already prepared return $this->mPreparedEdit; } @@ -2681,6 +2784,7 @@ class Article { /** * Do standard deferred updates after page edit. * Update links tables, site stats, search index and message cache. + * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * * @private @@ -2691,14 +2795,14 @@ class Article { * @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 ) { + public function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) { global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser, $wgEnableParserCache; wfProfileIn( __METHOD__ ); # Parse the text # Be careful not to double-PST: $text is usually already PST-ed once - if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { + if( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); $editInfo = $this->prepareTextForEdit( $text, $newid ); } else { @@ -2707,17 +2811,20 @@ class Article { } # Save it to the parser cache - if ( $wgEnableParserCache ) { + if( $wgEnableParserCache ) { $parserCache = ParserCache::singleton(); $parserCache->save( $editInfo->output, $this, $wgUser ); } # Update the links tables - $u = new LinksUpdate( $this->mTitle, $editInfo->output ); + $u = new LinksUpdate( $this->mTitle, $editInfo->output, false ); + $u->setRecursiveTouch( $changed ); // refresh/invalidate including pages too $u->doUpdate(); + + wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $changed ) ); if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { - if ( 0 == mt_rand( 0, 99 ) ) { + if( 0 == mt_rand( 0, 99 ) ) { // Flush old entries from the `recentchanges` table; we do this on // random requests so as to avoid an increase in writes for no good reason global $wgRCMaxAge; @@ -2733,7 +2840,7 @@ class Article { $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); - if ( 0 == $id ) { + if( 0 == $id ) { wfProfileOut( __METHOD__ ); return; } @@ -2748,21 +2855,23 @@ class Article { # load of user talk pages and piss people off, nor if it's a minor edit # by a properly-flagged bot. if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed - && !($minoredit && $wgUser->isAllowed('nominornewtalk') ) ) { - if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this)) ) { - $other = User::newFromName( $shortTitle ); - if( is_null( $other ) && User::isIP( $shortTitle ) ) { + && !( $minoredit && $wgUser->isAllowed( 'nominornewtalk' ) ) ) { + if( wfRunHooks('ArticleEditUpdateNewTalk', array( &$this ) ) ) { + $other = User::newFromName( $shortTitle, false ); + if( !$other ) { + wfDebug( __METHOD__.": invalid username\n" ); + } elseif( User::isIP( $shortTitle ) ) { // An anonymous user - $other = new User(); - $other->setName( $shortTitle ); - } - if( $other ) { $other->setNewtalk( true ); + } elseif( $other->isLoggedIn() ) { + $other->setNewtalk( true ); + } else { + wfDebug( __METHOD__. ": don't need to notify a nonexistent user\n" ); } } } - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $wgMessageCache->replace( $shortTitle, $text ); } @@ -2772,13 +2881,13 @@ class Article { /** * Perform article updates on a special page creation. * - * @param Revision $rev + * @param $rev Revision object * * @todo This is a shitty interface function. Kill it and replace the * other shitty functions like editUpdates and such so it's not needed * anymore. */ - function createUpdates( $rev ) { + public function createUpdates( $rev ) { $this->mGoodAdjustment = $this->isCountable( $rev->getText() ); $this->mTotalAdjustment = 1; $this->editUpdates( $rev->getText(), $rev->getComment(), @@ -2791,14 +2900,13 @@ class Article { * Revision as of \<date\>; view current revision * \<- Previous version | Next Version -\> * - * @private - * @param string $oldid Revision ID of this article revision + * @param $oldid String: revision ID of this article revision */ - function setOldSubtitle( $oldid=0 ) { + public function setOldSubtitle( $oldid = 0 ) { global $wgLang, $wgOut, $wgUser; - if ( !wfRunHooks( 'DisplayOldSubtitle', array(&$this, &$oldid) ) ) { - return; + if( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { + return; } $revision = Revision::newFromId( $oldid ); @@ -2807,34 +2915,34 @@ class Article { $td = $wgLang->timeanddate( $this->mTimestamp, true ); $sk = $wgUser->getSkin(); $lnk = $current - ? wfMsg( 'currentrevisionlink' ) - : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) ); + ? wfMsgHtml( 'currentrevisionlink' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'currentrevisionlink' ) ); $curdiff = $current - ? wfMsg( 'diff' ) - : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=cur&oldid='.$oldid ); + ? wfMsgHtml( 'diff' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=cur&oldid='.$oldid ); $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ; $prevlink = $prev - ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'previousrevision' ), 'direction=prev&oldid='.$oldid ) - : wfMsg( 'previousrevision' ); + ? $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousrevision' ), 'direction=prev&oldid='.$oldid ) + : wfMsgHtml( 'previousrevision' ); $prevdiff = $prev - ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=prev&oldid='.$oldid ) - : wfMsg( 'diff' ); + ? $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=prev&oldid='.$oldid ) + : wfMsgHtml( 'diff' ); $nextlink = $current - ? wfMsg( 'nextrevision' ) - : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'nextrevision' ), 'direction=next&oldid='.$oldid ); + ? wfMsgHtml( 'nextrevision' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextrevision' ), 'direction=next&oldid='.$oldid ); $nextdiff = $current - ? wfMsg( 'diff' ) - : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid ); + ? wfMsgHtml( 'diff' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=next&oldid='.$oldid ); $cdel=''; if( $wgUser->isAllowed( 'deleterevision' ) ) { $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); if( $revision->isCurrent() ) { // We don't handle top deleted edits too well - $cdel = wfMsgHtml('rev-delundel'); + $cdel = wfMsgHtml( 'rev-delundel' ); } else if( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) { // If revision was hidden from sysops - $cdel = wfMsgHtml('rev-delundel'); + $cdel = wfMsgHtml( 'rev-delundel' ); } else { $cdel = $sk->makeKnownLinkObj( $revdel, wfMsgHtml('rev-delundel'), @@ -2855,9 +2963,10 @@ class Article { ? 'revision-info-current' : 'revision-info'; - $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsg( $infomsg, $td, $userlinks ) . "</div>\n" . + $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsgExt( $infomsg, array( 'parseinline', 'replaceafter' ), $td, $userlinks, $revision->getID() ) . "</div>\n" . - "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; + "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ), + $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; $wgOut->setSubtitle( $r ); } @@ -2865,9 +2974,9 @@ class Article { * This function is called right before saving the wikitext, * so we can do things like signatures and links-in-context. * - * @param string $text + * @param $text String */ - function preSaveTransform( $text ) { + public function preSaveTransform( $text ) { global $wgParser, $wgUser; return $wgParser->preSaveTransform( $text, $this->mTitle, $wgUser, ParserOptions::newFromUser( $wgUser ) ); } @@ -2879,17 +2988,16 @@ class Article { * output to the client that is necessary for this request. * (that is, it has sent a cached version of the page) */ - function tryFileCache() { + protected function tryFileCache() { static $called = false; if( $called ) { wfDebug( "Article::tryFileCache(): called twice!?\n" ); - return; + return false; } $called = true; - if($this->isFileCacheable()) { - $touched = $this->mTouched; + if( $this->isFileCacheable() ) { $cache = new HTMLFileCache( $this->mTitle ); - if($cache->isFileCacheGood( $touched )) { + if( $cache->isFileCacheGood( $this->mTouched ) ) { wfDebug( "Article::tryFileCache(): about to load file\n" ); $cache->loadFromFileCache(); return true; @@ -2900,46 +3008,22 @@ class Article { } else { wfDebug( "Article::tryFileCache(): not cacheable\n" ); } + return false; } /** * Check if the page can be cached * @return bool */ - function isFileCacheable() { - global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest, $wgLang, $wgContLang; - $action = $wgRequest->getVal( 'action' ); - $oldid = $wgRequest->getVal( 'oldid' ); - $diff = $wgRequest->getVal( 'diff' ); - $redirect = $wgRequest->getVal( 'redirect' ); - $printable = $wgRequest->getVal( 'printable' ); - $page = $wgRequest->getVal( 'page' ); - - //check for non-standard user language; this covers uselang, - //and extensions for auto-detecting user language. - $ulang = $wgLang->getCode(); - $clang = $wgContLang->getCode(); - - $cacheable = $wgUseFileCache - && (!$wgShowIPinHeader) - && ($this->getID() != 0) - && ($wgUser->isAnon()) - && (!$wgUser->getNewtalk()) - && ($this->mTitle->getNamespace() != NS_SPECIAL ) - && (empty( $action ) || $action == 'view') - && (!isset($oldid)) - && (!isset($diff)) - && (!isset($redirect)) - && (!isset($printable)) - && !isset($page) - && (!$this->mRedirectedFrom) - && ($ulang === $clang); - - if ( $cacheable ) { - //extension may have reason to disable file caching on some pages. - $cacheable = wfRunHooks( 'IsFileCacheable', array( $this ) ); + public function isFileCacheable() { + $cacheable = false; + if( HTMLFileCache::useFileCache() ) { + $cacheable = $this->getID() && !$this->mRedirectedFrom; + // Extension may have reason to disable file caching on some pages. + if( $cacheable ) { + $cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) ); + } } - return $cacheable; } @@ -2947,7 +3031,7 @@ class Article { * Loads page_touched and returns a value indicating if it should be used * */ - function checkTouched() { + public function checkTouched() { if( !$this->mDataLoaded ) { $this->loadPageData(); } @@ -2957,7 +3041,7 @@ class Article { /** * Get the page_touched field */ - function getTouched() { + public function getTouched() { # Ensure that page data has been loaded if( !$this->mDataLoaded ) { $this->loadPageData(); @@ -2968,8 +3052,8 @@ class Article { /** * Get the page_latest field */ - function getLatest() { - if ( !$this->mDataLoaded ) { + public function getLatest() { + if( !$this->mDataLoaded ) { $this->loadPageData(); } return $this->mLatest; @@ -2980,15 +3064,14 @@ class Article { * 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 + * @param $text String: text submitted + * @param $comment String: comment submitted + * @param $minor Boolean: whereas it's a minor modification */ - function quickEdit( $text, $comment = '', $minor = 0 ) { + public function quickEdit( $text, $comment = '', $minor = 0 ) { wfProfileIn( __METHOD__ ); $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); $revision = new Revision( array( 'page' => $this->getId(), 'text' => $text, @@ -2997,9 +3080,8 @@ class Article { ) ); $revision->insertOn( $dbw ); $this->updateRevisionOn( $dbw, $revision ); - $dbw->commit(); - - wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) ); + + wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false, $wgUser) ); wfProfileOut( __METHOD__ ); } @@ -3007,10 +3089,9 @@ class Article { /** * Used to increment the view counter * - * @static - * @param integer $id article id + * @param $id Integer: article id */ - function incViewCount( $id ) { + public static function incViewCount( $id ) { $id = intval( $id ); global $wgHitcounterUpdateFreq, $wgDBtype; @@ -3043,14 +3124,14 @@ class Article { wfProfileIn( 'Article::incViewCount-collect' ); $old_user_abort = ignore_user_abort( true ); - if ($wgDBtype == 'mysql') + if($wgDBtype == 'mysql') $dbw->query("LOCK TABLES $hitcounterTable WRITE"); $tabletype = $wgDBtype == 'mysql' ? "ENGINE=HEAP " : ''; $dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype AS ". "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable ". 'GROUP BY hc_id'); $dbw->query("DELETE FROM $hitcounterTable"); - if ($wgDBtype == 'mysql') { + if($wgDBtype == 'mysql') { $dbw->query('UNLOCK TABLES'); $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ". 'WHERE page_id = hc_id'); @@ -3075,13 +3156,13 @@ class Article { * 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 + * + * @param $title 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() ) { + public static function onArticleCreate( $title ) { + # Update existence markers on article/talk tabs... + if( $title->isTalkPage() ) { $other = $title->getSubjectPage(); } else { $other = $title->getTalkPage(); @@ -3094,10 +3175,9 @@ class Article { $title->deleteTitleProtection(); } - static function onArticleDelete( $title ) { - global $wgUseFileCache, $wgMessageCache; - - // Update existence markers on article/talk tabs... + public static function onArticleDelete( $title ) { + global $wgMessageCache; + # Update existence markers on article/talk tabs... if( $title->isTalkPage() ) { $other = $title->getSubjectPage(); } else { @@ -3110,17 +3190,14 @@ class Article { $title->purgeSquid(); # File cache - if ( $wgUseFileCache ) { - $cm = new HTMLFileCache( $title ); - @unlink( $cm->fileCacheName() ); - } + HTMLFileCache::clearFileCache( $title ); # Messages if( $title->getNamespace() == NS_MEDIAWIKI ) { $wgMessageCache->replace( $title->getDBkey(), false ); } # Images - if( $title->getNamespace() == NS_IMAGE ) { + if( $title->getNamespace() == NS_FILE ) { $update = new HTMLCacheUpdate( $title, 'imagelinks' ); $update->doUpdate(); } @@ -3134,11 +3211,12 @@ class Article { /** * Purge caches on page update etc */ - static function onArticleEdit( $title ) { - global $wgDeferredUpdateList, $wgUseFileCache; + public static function onArticleEdit( $title, $transclusions = 'transclusions' ) { + global $wgDeferredUpdateList; // Invalidate caches of articles which include this page - $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' ); + if( $transclusions !== 'skiptransclusions' ) + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' ); // Invalidate the caches of all pages which redirect here $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' ); @@ -3146,11 +3224,8 @@ class Article { # Purge squid for this page only $title->purgeSquid(); - # Clear file cache - if ( $wgUseFileCache ) { - $cm = new HTMLFileCache( $title ); - @unlink( $cm->fileCacheName() ); - } + # Clear file cache for this page only + HTMLFileCache::clearFileCache( $title ); } /**#@-*/ @@ -3159,7 +3234,7 @@ class Article { * Overriden by ImagePage class, only present here to avoid a fatal error * Called for ?action=revert */ - public function revert(){ + public function revert() { global $wgOut; $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); } @@ -3167,13 +3242,11 @@ class Article { /** * Info about this page * Called for ?action=info when $wgAllowPageInfo is on. - * - * @public */ - function info() { + public function info() { global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser; - if ( !$wgAllowPageInfo ) { + if( !$wgAllowPageInfo ) { $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); return; } @@ -3182,21 +3255,21 @@ class Article { $wgOut->setPagetitle( $page->getPrefixedText() ); $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) ); - $wgOut->setSubtitle( wfMsg( 'infosubtitle' ) ); + $wgOut->setSubtitle( wfMsgHtml( 'infosubtitle' ) ); if( !$this->mTitle->exists() ) { - $wgOut->addHtml( '<div class="noarticletext">' ); + $wgOut->addHTML( '<div class="noarticletext">' ); if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { // This doesn't quite make sense; the user is asking for // information about the _page_, not the message... -- RC - $wgOut->addHtml( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) ); + $wgOut->addHTML( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) ); } else { $msg = $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; - $wgOut->addHtml( wfMsgExt( $msg, 'parse' ) ); + $wgOut->addHTML( wfMsgExt( $msg, 'parse' ) ); } - $wgOut->addHtml( '</div>' ); + $wgOut->addHTML( '</div>' ); } else { $dbr = wfGetDB( DB_SLAVE ); $wl_clause = array( @@ -3222,7 +3295,6 @@ class Article { $wgOut->addHTML( '<li>' . wfMsg('numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' ); } $wgOut->addHTML( '</ul>' ); - } } @@ -3230,34 +3302,30 @@ class Article { * 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 + * @param $title Title object * @return array - * @private */ - function pageCountInfo( $title ) { + protected 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() ); - + $this->getSelectOptions() + ); $authors = $dbr->selectField( 'revision', 'COUNT(DISTINCT rev_user_text)', $rev_clause, __METHOD__, - $this->getSelectOptions() ); - + $this->getSelectOptions() + ); return array( 'edits' => $edits, 'authors' => $authors ); } @@ -3265,25 +3333,22 @@ class Article { * Return a list of templates used by this article. * Uses the templatelinks table * - * @return array Array of Title objects + * @return Array of Title objects */ - function getUsedTemplates() { + public 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 ); - } + __METHOD__ ); + if( $res !== false ) { + foreach( $res as $row ) { + $result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title ); } } $dbr->freeResult( $res ); @@ -3294,26 +3359,23 @@ class Article { * Returns a list of hidden categories this page is a member of. * Uses the page_props and categorylinks tables. * - * @return array Array of Title objects + * @return Array of Title objects */ - function getHiddenCategories() { + public function getHiddenCategories() { $result = array(); $id = $this->mTitle->getArticleID(); if( $id == 0 ) { return array(); } - $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), array( 'cl_to' ), array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', 'page_namespace' => NS_CATEGORY, 'page_title=cl_to'), - 'Article:getHiddenCategories' ); - if ( false !== $res ) { - if ( $dbr->numRows( $res ) ) { - while ( $row = $dbr->fetchObject( $res ) ) { - $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); - } + __METHOD__ ); + if( $res !== false ) { + foreach( $res as $row ) { + $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); } } $dbr->freeResult( $res ); @@ -3322,17 +3384,18 @@ class Article { /** * Return an applicable autosummary if one exists for the given edit. - * @param string $oldtext The previous text of the page. - * @param string $newtext The submitted text of the page. - * @param bitmask $flags A bitmask of flags submitted for the edit. + * @param $oldtext String: the previous text of the page. + * @param $newtext String: The submitted text of the page. + * @param $flags Bitmask: a bitmask of flags submitted for the edit. * @return string An appropriate autosummary, or an empty string. */ public static function getAutosummary( $oldtext, $newtext, $flags ) { # Decide what kind of autosummary is needed. # Redirect autosummaries + $ot = Title::newFromRedirect( $oldtext ); $rt = Title::newFromRedirect( $newtext ); - if( is_object( $rt ) ) { + if( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { return wfMsgForContent( 'autoredircomment', $rt->getFullText() ); } @@ -3342,14 +3405,14 @@ class Article { global $wgContLang; $truncatedtext = $wgContLang->truncate( str_replace("\n", ' ', $newtext), - max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new') ) ), + max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ), '...' ); return wfMsgForContent( 'autosumm-new', $truncatedtext ); } # Blanking autosummaries if( $oldtext != '' && $newtext == '' ) { - return wfMsgForContent('autosumm-blank'); + return wfMsgForContent( 'autosumm-blank' ); } elseif( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500) { # Removing more than 90% of the article global $wgContLang; @@ -3371,11 +3434,11 @@ class Article { * Saves the text into the parser cache if possible. * Updates templatelinks if it is out of date. * - * @param string $text - * @param bool $cache + * @param $text String + * @param $cache Boolean */ public function outputWikiText( $text, $cache = true ) { - global $wgParser, $wgUser, $wgOut, $wgEnableParserCache; + global $wgParser, $wgUser, $wgOut, $wgEnableParserCache, $wgUseFileCache; $popts = $wgOut->parserOptions(); $popts->setTidy(true); @@ -3384,12 +3447,18 @@ class Article { $popts, true, true, $this->getRevIdFetched() ); $popts->setTidy(false); $popts->enableLimitReport( false ); - if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { + if( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { $parserCache = ParserCache::singleton(); $parserCache->save( $parserOutput, $this, $wgUser ); } + // Make sure file cache is not used on uncacheable content. + // Output that has magic words in it can still use the parser cache + // (if enabled), though it will generally expire sooner. + if( $parserOutput->getCacheTime() == -1 || $parserOutput->containsOldMagic() ) { + $wgUseFileCache = false; + } - if ( !wfReadOnly() && $this->mTitle->areRestrictionsCascading() ) { + if( $this->isCurrent() && !wfReadOnly() && $this->mTitle->areRestrictionsCascading() ) { // templatelinks table may have become out of sync, // especially if using variable-based transclusions. // For paranoia, check if things have changed and if @@ -3406,15 +3475,13 @@ class Article { $res = $dbr->select( array( 'templatelinks' ), array( 'tl_namespace', 'tl_title' ), array( 'tl_from' => $id ), - 'Article:getUsedTemplates' ); + __METHOD__ ); global $wgContLang; - if ( false !== $res ) { - if ( $dbr->numRows( $res ) ) { - while ( $row = $dbr->fetchObject( $res ) ) { - $tlTemplates[] = $wgContLang->getNsText( $row->tl_namespace ) . ':' . $row->tl_title ; - } + if( $res !== false ) { + foreach( $res as $row ) { + $tlTemplates[] = $wgContLang->getNsText( $row->tl_namespace ) . ':' . $row->tl_title ; } } @@ -3429,16 +3496,10 @@ class Article { # Get the diff $templates_diff = array_diff( $poTemplates, $tlTemplates ); - if ( count( $templates_diff ) > 0 ) { + if( count( $templates_diff ) > 0 ) { # Whee, link updates time. $u = new LinksUpdate( $this->mTitle, $parserOutput ); - - $dbw = wfGetDb( DB_MASTER ); - $dbw->begin(); - $u->doUpdate(); - - $dbw->commit(); } } @@ -3479,12 +3540,12 @@ class Article { if( $ns == NS_CATEGORY ) { $addFields[] = 'cat_subcats = cat_subcats + 1'; $removeFields[] = 'cat_subcats = cat_subcats - 1'; - } elseif( $ns == NS_IMAGE ) { + } elseif( $ns == NS_FILE ) { $addFields[] = 'cat_files = cat_files + 1'; $removeFields[] = 'cat_files = cat_files - 1'; } - if ( $added ) { + if( $added ) { $dbw->update( 'category', $addFields, @@ -3492,7 +3553,7 @@ class Article { __METHOD__ ); } - if ( $deleted ) { + if( $deleted ) { $dbw->update( 'category', $removeFields, diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index 7717e001..b29e13f2 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -38,9 +38,8 @@ class AuthPlugin { * * @param $username String: username. * @return bool - * @public */ - function userExists( $username ) { + public function userExists( $username ) { # Override this! return false; } @@ -54,9 +53,8 @@ class AuthPlugin { * @param $username String: username. * @param $password String: user password. * @return bool - * @public */ - function authenticate( $username, $password ) { + public function authenticate( $username, $password ) { # Override this! return false; } @@ -65,9 +63,8 @@ class AuthPlugin { * Modify options in the login template. * * @param $template UserLoginTemplate object. - * @public */ - function modifyUITemplate( &$template ) { + public function modifyUITemplate( &$template ) { # Override this! $template->set( 'usedomain', false ); } @@ -76,9 +73,8 @@ class AuthPlugin { * Set the domain this plugin is supposed to use when authenticating. * * @param $domain String: authentication domain. - * @public */ - function setDomain( $domain ) { + public function setDomain( $domain ) { $this->domain = $domain; } @@ -87,9 +83,8 @@ class AuthPlugin { * * @param $domain String: authentication domain. * @return bool - * @public */ - function validDomain( $domain ) { + public function validDomain( $domain ) { # Override this! return true; } @@ -103,9 +98,8 @@ class AuthPlugin { * forget the & on your function declaration. * * @param User $user - * @public */ - function updateUser( &$user ) { + public function updateUser( &$user ) { # Override this and do something return true; } @@ -123,9 +117,8 @@ class AuthPlugin { * This is just a question, and shouldn't perform any actions. * * @return bool - * @public */ - function autoCreate() { + public function autoCreate() { return false; } @@ -134,7 +127,7 @@ class AuthPlugin { * * @return bool */ - function allowPasswordChange() { + public function allowPasswordChange() { return true; } @@ -149,9 +142,8 @@ class AuthPlugin { * @param $user User object. * @param $password String: password. * @return bool - * @public */ - function setPassword( $user, $password ) { + public function setPassword( $user, $password ) { return true; } @@ -161,9 +153,8 @@ class AuthPlugin { * * @param $user User object. * @return bool - * @public */ - function updateExternalDB( $user ) { + public function updateExternalDB( $user ) { return true; } @@ -171,9 +162,8 @@ class AuthPlugin { * Check to see if external accounts can be created. * Return true if external accounts can be created. * @return bool - * @public */ - function canCreateAccounts() { + public function canCreateAccounts() { return false; } @@ -186,9 +176,8 @@ class AuthPlugin { * @param string $email * @param string $realname * @return bool - * @public */ - function addUser( $user, $password, $email='', $realname='' ) { + public function addUser( $user, $password, $email='', $realname='' ) { return true; } @@ -200,9 +189,8 @@ class AuthPlugin { * This is just a question, and shouldn't perform any actions. * * @return bool - * @public */ - function strict() { + public function strict() { return false; } @@ -212,9 +200,8 @@ class AuthPlugin { * * @param $username String: username. * @return bool - * @public */ - function strictUserAuth( $username ) { + public function strictUserAuth( $username ) { return false; } @@ -228,9 +215,8 @@ class AuthPlugin { * * @param $user User object. * @param $autocreate bool True if user is being autocreated on login - * @public */ - function initUser( &$user, $autocreate=false ) { + public function initUser( &$user, $autocreate=false ) { # Override this to do something. } @@ -238,7 +224,43 @@ class AuthPlugin { * If you want to munge the case of an account name before the final * check, now is your chance. */ - function getCanonicalName( $username ) { + public function getCanonicalName( $username ) { return $username; } + + /** + * Get an instance of a User object + * + * @param $user User + * @public + */ + public function getUserInstance( User &$user ) { + return new AuthPluginUser( $user ); + } +} + +class AuthPluginUser { + function __construct( $user ) { + # Override this! + } + + public function getId() { + # Override this! + return -1; + } + + public function isLocked() { + # Override this! + return false; + } + + public function isHidden() { + # Override this! + return false; + } + + public function resetAuthToken() { + # Override this! + return true; + } } diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index de75b41d..ce1912ea 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -4,483 +4,544 @@ ini_set('unserialize_callback_func', '__autoload' ); -class AutoLoader { - # Locations of core classes - # Extension classes are specified with $wgAutoloadClasses - static $localClasses = array( - # Includes - 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', - 'AjaxResponse' => 'includes/AjaxResponse.php', - 'AlphabeticPager' => 'includes/Pager.php', - 'APCBagOStuff' => 'includes/BagOStuff.php', - 'ArrayDiffFormatter' => 'includes/DifferenceEngine.php', - 'Article' => 'includes/Article.php', - 'AtomFeed' => 'includes/Feed.php', - 'AuthPlugin' => 'includes/AuthPlugin.php', - 'Autopromote' => 'includes/Autopromote.php', - 'BagOStuff' => 'includes/BagOStuff.php', - 'Block' => 'includes/Block.php', - 'CacheDependency' => 'includes/CacheDependency.php', - 'Category' => 'includes/Category.php', - 'Categoryfinder' => 'includes/Categoryfinder.php', - 'CategoryPage' => 'includes/CategoryPage.php', - 'CategoryViewer' => 'includes/CategoryPage.php', - 'ChangesList' => 'includes/ChangesList.php', - 'ChangesFeed' => 'includes/ChangesFeed.php', - 'ChannelFeed' => 'includes/Feed.php', - 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', - 'ConstantDependency' => 'includes/CacheDependency.php', - 'DBABagOStuff' => 'includes/BagOStuff.php', - 'DependencyWrapper' => 'includes/CacheDependency.php', - '_DiffEngine' => 'includes/DifferenceEngine.php', - 'DifferenceEngine' => 'includes/DifferenceEngine.php', - 'DiffFormatter' => 'includes/DifferenceEngine.php', - 'Diff' => 'includes/DifferenceEngine.php', - '_DiffOp_Add' => 'includes/DifferenceEngine.php', - '_DiffOp_Change' => 'includes/DifferenceEngine.php', - '_DiffOp_Copy' => 'includes/DifferenceEngine.php', - '_DiffOp_Delete' => 'includes/DifferenceEngine.php', - '_DiffOp' => 'includes/DifferenceEngine.php', - 'DjVuImage' => 'includes/DjVuImage.php', - 'DoubleReplacer' => 'includes/StringUtils.php', - 'DoubleRedirectJob' => 'includes/DoubleRedirectJob.php', - 'Dump7ZipOutput' => 'includes/Export.php', - 'DumpBZip2Output' => 'includes/Export.php', - 'DumpFileOutput' => 'includes/Export.php', - 'DumpFilter' => 'includes/Export.php', - 'DumpGZipOutput' => 'includes/Export.php', - 'DumpLatestFilter' => 'includes/Export.php', - 'DumpMultiWriter' => 'includes/Export.php', - 'DumpNamespaceFilter' => 'includes/Export.php', - 'DumpNotalkFilter' => 'includes/Export.php', - 'DumpOutput' => 'includes/Export.php', - 'DumpPipeOutput' => 'includes/Export.php', - 'eAccelBagOStuff' => 'includes/BagOStuff.php', - 'EditPage' => 'includes/EditPage.php', - 'EmaillingJob' => 'includes/EmaillingJob.php', - 'EmailNotification' => 'includes/UserMailer.php', - 'EnhancedChangesList' => 'includes/ChangesList.php', - 'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php', - 'ErrorPageError' => 'includes/Exception.php', - 'Exif' => 'includes/Exif.php', - 'ExternalEdit' => 'includes/ExternalEdit.php', - 'ExternalStoreDB' => 'includes/ExternalStoreDB.php', - 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php', - 'ExternalStore' => 'includes/ExternalStore.php', - 'FatalError' => 'includes/Exception.php', - 'FakeTitle' => 'includes/FakeTitle.php', - 'FauxRequest' => 'includes/WebRequest.php', - 'FeedItem' => 'includes/Feed.php', - 'FeedUtils' => 'includes/FeedUtils.php', - 'FileDeleteForm' => 'includes/FileDeleteForm.php', - 'FileDependency' => 'includes/CacheDependency.php', - 'FileRevertForm' => 'includes/FileRevertForm.php', - 'FileStore' => 'includes/FileStore.php', - 'FormatExif' => 'includes/Exif.php', - 'FormOptions' => 'includes/FormOptions.php', - 'FSException' => 'includes/FileStore.php', - 'FSTransaction' => 'includes/FileStore.php', - 'GlobalDependency' => 'includes/CacheDependency.php', - 'HashBagOStuff' => 'includes/BagOStuff.php', - 'HashtableReplacer' => 'includes/StringUtils.php', - 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', - 'HistoryBlob' => 'includes/HistoryBlob.php', - 'HistoryBlobStub' => 'includes/HistoryBlob.php', - 'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php', - 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', - 'HTMLFileCache' => 'includes/HTMLFileCache.php', - 'Http' => 'includes/HttpFunctions.php', - '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php', - 'IEContentAnalyzer' => 'includes/IEContentAnalyzer.php', - 'ImageGallery' => 'includes/ImageGallery.php', - 'ImageHistoryList' => 'includes/ImagePage.php', - 'ImagePage' => 'includes/ImagePage.php', - 'ImageQueryPage' => 'includes/ImageQueryPage.php', - 'IncludableSpecialPage' => 'includes/SpecialPage.php', - 'IndexPager' => 'includes/Pager.php', - 'IP' => 'includes/IP.php', - 'Job' => 'includes/JobQueue.php', - 'License' => 'includes/Licenses.php', - 'Licenses' => 'includes/Licenses.php', - 'LinkBatch' => 'includes/LinkBatch.php', - 'LinkCache' => 'includes/LinkCache.php', - 'Linker' => 'includes/Linker.php', - 'LinkFilter' => 'includes/LinkFilter.php', - 'LinksUpdate' => 'includes/LinksUpdate.php', - 'LogPage' => 'includes/LogPage.php', - 'LogPager' => 'includes/LogEventsList.php', - 'LogEventsList' => 'includes/LogEventsList.php', - 'LogReader' => 'includes/LogEventsList.php', - 'LogViewer' => 'includes/LogEventsList.php', - 'MacBinary' => 'includes/MacBinary.php', - 'MagicWordArray' => 'includes/MagicWord.php', - 'MagicWord' => 'includes/MagicWord.php', - 'MailAddress' => 'includes/UserMailer.php', - 'MappedDiff' => 'includes/DifferenceEngine.php', - 'MathRenderer' => 'includes/Math.php', - 'MediaTransformError' => 'includes/MediaTransformOutput.php', - 'MediaTransformOutput' => 'includes/MediaTransformOutput.php', - 'MediaWikiBagOStuff' => 'includes/BagOStuff.php', - 'MediaWiki_I18N' => 'includes/SkinTemplate.php', - 'MediaWiki' => 'includes/Wiki.php', - 'memcached' => 'includes/memcached-client.php', - 'MessageCache' => 'includes/MessageCache.php', - 'MimeMagic' => 'includes/MimeMagic.php', - 'MWException' => 'includes/Exception.php', - 'MWNamespace' => 'includes/Namespace.php', - 'MySQLSearchResultSet' => 'includes/SearchMySQL.php', - 'Namespace' => 'includes/NamespaceCompat.php', // Compat - 'OldChangesList' => 'includes/ChangesList.php', - 'OracleSearchResultSet' => 'includes/SearchOracle.php', - 'OutputPage' => 'includes/OutputPage.php', - 'PageHistory' => 'includes/PageHistory.php', - 'PageHistoryPager' => 'includes/PageHistory.php', - 'PageQueryPage' => 'includes/PageQueryPage.php', - 'Pager' => 'includes/Pager.php', - 'PasswordError' => 'includes/User.php', - 'PatrolLog' => 'includes/PatrolLog.php', - 'PostgresSearchResult' => 'includes/SearchPostgres.php', - 'PostgresSearchResultSet' => 'includes/SearchPostgres.php', - 'PrefixSearch' => 'includes/PrefixSearch.php', - 'Profiler' => 'includes/Profiler.php', - 'ProfilerSimple' => 'includes/ProfilerSimple.php', - 'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php', - 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', - 'ProtectionForm' => 'includes/ProtectionForm.php', - 'QueryPage' => 'includes/QueryPage.php', - 'QuickTemplate' => 'includes/SkinTemplate.php', - 'RawPage' => 'includes/RawPage.php', - 'RCCacheEntry' => 'includes/ChangesList.php', - 'RecentChange' => 'includes/RecentChange.php', - 'RefreshLinksJob' => 'includes/RefreshLinksJob.php', - 'RegexlikeReplacer' => 'includes/StringUtils.php', - 'ReplacementArray' => 'includes/StringUtils.php', - 'Replacer' => 'includes/StringUtils.php', - 'ReverseChronologicalPager' => 'includes/Pager.php', - 'Revision' => 'includes/Revision.php', - 'RSSFeed' => 'includes/Feed.php', - 'Sanitizer' => 'includes/Sanitizer.php', - 'SearchEngineDummy' => 'includes/SearchEngine.php', - 'SearchEngine' => 'includes/SearchEngine.php', - 'SearchHighlighter' => 'includes/SearchEngine.php', - 'SearchMySQL4' => 'includes/SearchMySQL4.php', - 'SearchMySQL' => 'includes/SearchMySQL.php', - 'SearchOracle' => 'includes/SearchOracle.php', - 'SearchPostgres' => 'includes/SearchPostgres.php', - 'SearchResult' => 'includes/SearchEngine.php', - 'SearchResultSet' => 'includes/SearchEngine.php', - 'SearchResultTooMany' => 'includes/SearchEngine.php', - 'SearchUpdate' => 'includes/SearchUpdate.php', - 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php', - 'SiteConfiguration' => 'includes/SiteConfiguration.php', - 'SiteStats' => 'includes/SiteStats.php', - 'SiteStatsUpdate' => 'includes/SiteStats.php', - 'Skin' => 'includes/Skin.php', - 'SkinTemplate' => 'includes/SkinTemplate.php', - 'SpecialMycontributions' => 'includes/SpecialPage.php', - 'SpecialMypage' => 'includes/SpecialPage.php', - 'SpecialMytalk' => 'includes/SpecialPage.php', - 'SpecialPage' => 'includes/SpecialPage.php', - 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php', - 'SqlBagOStuff' => 'includes/BagOStuff.php', - 'SquidUpdate' => 'includes/SquidUpdate.php', - 'Status' => 'includes/Status.php', - 'StringUtils' => 'includes/StringUtils.php', - 'TableDiffFormatter' => 'includes/DifferenceEngine.php', - 'TablePager' => 'includes/Pager.php', - 'ThumbnailImage' => 'includes/MediaTransformOutput.php', - 'TitleDependency' => 'includes/CacheDependency.php', - 'Title' => 'includes/Title.php', - 'TitleListDependency' => 'includes/CacheDependency.php', - 'TransformParameterError' => 'includes/MediaTransformOutput.php', - 'TurckBagOStuff' => 'includes/BagOStuff.php', - 'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php', - 'UnlistedSpecialPage' => 'includes/SpecialPage.php', - 'User' => 'includes/User.php', - 'UserArray' => 'includes/UserArray.php', - 'UserArrayFromResult' => 'includes/UserArray.php', - 'UserMailer' => 'includes/UserMailer.php', - 'UserRightsProxy' => 'includes/UserRightsProxy.php', - 'WatchedItem' => 'includes/WatchedItem.php', - 'WatchlistEditor' => 'includes/WatchlistEditor.php', - 'WebRequest' => 'includes/WebRequest.php', - 'WebResponse' => 'includes/WebResponse.php', - 'WikiError' => 'includes/WikiError.php', - 'WikiErrorMsg' => 'includes/WikiError.php', - 'WikiExporter' => 'includes/Export.php', - 'WikiXmlError' => 'includes/WikiError.php', - 'WordLevelDiff' => 'includes/DifferenceEngine.php', - 'XCacheBagOStuff' => 'includes/BagOStuff.php', - 'XmlDumpWriter' => 'includes/Export.php', - 'Xml' => 'includes/Xml.php', - 'XmlSelect' => 'includes/Xml.php', - 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', - 'ZhClient' => 'includes/ZhClient.php', +# Locations of core classes +# Extension classes are specified with $wgAutoloadClasses +# This array is a global instead of a static member of AutoLoader to work around a bug in APC +global $wgAutoloadLocalClasses; +$wgAutoloadLocalClasses = array( + # Includes + 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', + 'AjaxResponse' => 'includes/AjaxResponse.php', + 'AlphabeticPager' => 'includes/Pager.php', + 'APCBagOStuff' => 'includes/BagOStuff.php', + 'Article' => 'includes/Article.php', + 'AtomFeed' => 'includes/Feed.php', + 'AuthPlugin' => 'includes/AuthPlugin.php', + 'AuthPluginUser' => 'includes/AuthPlugin.php', + 'Autopromote' => 'includes/Autopromote.php', + 'BagOStuff' => 'includes/BagOStuff.php', + 'Block' => 'includes/Block.php', + 'CacheDependency' => 'includes/CacheDependency.php', + 'Category' => 'includes/Category.php', + 'Categoryfinder' => 'includes/Categoryfinder.php', + 'CategoryPage' => 'includes/CategoryPage.php', + 'CategoryViewer' => 'includes/CategoryPage.php', + 'ChangesList' => 'includes/ChangesList.php', + 'ChangesFeed' => 'includes/ChangesFeed.php', + 'ChannelFeed' => 'includes/Feed.php', + 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', + 'ConstantDependency' => 'includes/CacheDependency.php', + 'CreativeCommonsRdf' => 'includes/Metadata.php', + 'Credits' => 'includes/Credits.php', + 'DBABagOStuff' => 'includes/BagOStuff.php', + 'DependencyWrapper' => 'includes/CacheDependency.php', + 'DiffHistoryBlob' => 'includes/HistoryBlob.php', + 'DjVuImage' => 'includes/DjVuImage.php', + 'DoubleReplacer' => 'includes/StringUtils.php', + 'DoubleRedirectJob' => 'includes/DoubleRedirectJob.php', + 'DublinCoreRdf' => 'includes/Metadata.php', + 'Dump7ZipOutput' => 'includes/Export.php', + 'DumpBZip2Output' => 'includes/Export.php', + 'DumpFileOutput' => 'includes/Export.php', + 'DumpFilter' => 'includes/Export.php', + 'DumpGZipOutput' => 'includes/Export.php', + 'DumpLatestFilter' => 'includes/Export.php', + 'DumpMultiWriter' => 'includes/Export.php', + 'DumpNamespaceFilter' => 'includes/Export.php', + 'DumpNotalkFilter' => 'includes/Export.php', + 'DumpOutput' => 'includes/Export.php', + 'DumpPipeOutput' => 'includes/Export.php', + 'eAccelBagOStuff' => 'includes/BagOStuff.php', + 'EditPage' => 'includes/EditPage.php', + 'EmaillingJob' => 'includes/EmaillingJob.php', + 'EmailNotification' => 'includes/UserMailer.php', + 'EnhancedChangesList' => 'includes/ChangesList.php', + 'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php', + 'ErrorPageError' => 'includes/Exception.php', + 'Exif' => 'includes/Exif.php', + 'ExplodeIterator' => 'includes/StringUtils.php', + 'ExternalEdit' => 'includes/ExternalEdit.php', + 'ExternalStoreDB' => 'includes/ExternalStoreDB.php', + 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php', + 'ExternalStore' => 'includes/ExternalStore.php', + 'FatalError' => 'includes/Exception.php', + 'FakeTitle' => 'includes/FakeTitle.php', + 'FauxRequest' => 'includes/WebRequest.php', + 'FeedItem' => 'includes/Feed.php', + 'FeedUtils' => 'includes/FeedUtils.php', + 'FileDeleteForm' => 'includes/FileDeleteForm.php', + 'FileDependency' => 'includes/CacheDependency.php', + 'FileRevertForm' => 'includes/FileRevertForm.php', + 'FileStore' => 'includes/FileStore.php', + 'FormatExif' => 'includes/Exif.php', + 'FormOptions' => 'includes/FormOptions.php', + 'FSException' => 'includes/FileStore.php', + 'FSTransaction' => 'includes/FileStore.php', + 'GlobalDependency' => 'includes/CacheDependency.php', + 'HashBagOStuff' => 'includes/BagOStuff.php', + 'HashtableReplacer' => 'includes/StringUtils.php', + 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', + 'HistoryBlob' => 'includes/HistoryBlob.php', + 'HistoryBlobStub' => 'includes/HistoryBlob.php', + 'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php', + 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', + 'HTMLFileCache' => 'includes/HTMLFileCache.php', + 'Http' => 'includes/HttpFunctions.php', + 'IEContentAnalyzer' => 'includes/IEContentAnalyzer.php', + 'ImageGallery' => 'includes/ImageGallery.php', + 'ImageHistoryList' => 'includes/ImagePage.php', + 'ImagePage' => 'includes/ImagePage.php', + 'ImageQueryPage' => 'includes/ImageQueryPage.php', + 'IncludableSpecialPage' => 'includes/SpecialPage.php', + 'IndexPager' => 'includes/Pager.php', + 'Interwiki' => 'includes/Interwiki.php', + 'IP' => 'includes/IP.php', + 'Job' => 'includes/JobQueue.php', + 'License' => 'includes/Licenses.php', + 'Licenses' => 'includes/Licenses.php', + 'LinkBatch' => 'includes/LinkBatch.php', + 'LinkCache' => 'includes/LinkCache.php', + 'Linker' => 'includes/Linker.php', + 'LinkFilter' => 'includes/LinkFilter.php', + 'LinksUpdate' => 'includes/LinksUpdate.php', + 'LogPage' => 'includes/LogPage.php', + 'LogPager' => 'includes/LogEventsList.php', + 'LogEventsList' => 'includes/LogEventsList.php', + 'LogReader' => 'includes/LogEventsList.php', + 'LogViewer' => 'includes/LogEventsList.php', + 'MacBinary' => 'includes/MacBinary.php', + 'MagicWordArray' => 'includes/MagicWord.php', + 'MagicWord' => 'includes/MagicWord.php', + 'MailAddress' => 'includes/UserMailer.php', + 'MathRenderer' => 'includes/Math.php', + 'MediaTransformError' => 'includes/MediaTransformOutput.php', + 'MediaTransformOutput' => 'includes/MediaTransformOutput.php', + 'MediaWikiBagOStuff' => 'includes/BagOStuff.php', + 'MediaWiki_I18N' => 'includes/SkinTemplate.php', + 'MediaWiki' => 'includes/Wiki.php', + 'memcached' => 'includes/memcached-client.php', + 'MessageCache' => 'includes/MessageCache.php', + 'MimeMagic' => 'includes/MimeMagic.php', + 'MWException' => 'includes/Exception.php', + 'MWNamespace' => 'includes/Namespace.php', + 'MySQLSearchResultSet' => 'includes/SearchMySQL.php', + 'Namespace' => 'includes/NamespaceCompat.php', // Compat + 'OldChangesList' => 'includes/ChangesList.php', + 'OracleSearchResultSet' => 'includes/SearchOracle.php', + 'OutputPage' => 'includes/OutputPage.php', + 'PageHistory' => 'includes/PageHistory.php', + 'PageHistoryPager' => 'includes/PageHistory.php', + 'PageQueryPage' => 'includes/PageQueryPage.php', + 'Pager' => 'includes/Pager.php', + 'PasswordError' => 'includes/User.php', + 'PatrolLog' => 'includes/PatrolLog.php', + 'PostgresSearchResult' => 'includes/SearchPostgres.php', + 'PostgresSearchResultSet' => 'includes/SearchPostgres.php', + 'PrefixSearch' => 'includes/PrefixSearch.php', + 'Profiler' => 'includes/Profiler.php', + 'ProfilerSimple' => 'includes/ProfilerSimple.php', + 'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php', + 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', + 'ProtectionForm' => 'includes/ProtectionForm.php', + 'QueryPage' => 'includes/QueryPage.php', + 'QuickTemplate' => 'includes/SkinTemplate.php', + 'RawPage' => 'includes/RawPage.php', + 'RCCacheEntry' => 'includes/ChangesList.php', + 'RdfMetaData' => 'includes/Metadata.php', + 'RecentChange' => 'includes/RecentChange.php', + 'RefreshLinksJob' => 'includes/RefreshLinksJob.php', + 'RefreshLinksJob2' => 'includes/RefreshLinksJob.php', + 'RegexlikeReplacer' => 'includes/StringUtils.php', + 'ReplacementArray' => 'includes/StringUtils.php', + 'Replacer' => 'includes/StringUtils.php', + 'ReverseChronologicalPager' => 'includes/Pager.php', + 'Revision' => 'includes/Revision.php', + 'RSSFeed' => 'includes/Feed.php', + 'Sanitizer' => 'includes/Sanitizer.php', + 'SearchEngineDummy' => 'includes/SearchEngine.php', + 'SearchEngine' => 'includes/SearchEngine.php', + 'SearchHighlighter' => 'includes/SearchEngine.php', + 'SearchMySQL4' => 'includes/SearchMySQL4.php', + 'SearchMySQL' => 'includes/SearchMySQL.php', + 'SearchOracle' => 'includes/SearchOracle.php', + 'SearchPostgres' => 'includes/SearchPostgres.php', + 'SearchResult' => 'includes/SearchEngine.php', + 'SearchResultSet' => 'includes/SearchEngine.php', + 'SearchResultTooMany' => 'includes/SearchEngine.php', + 'SearchUpdate' => 'includes/SearchUpdate.php', + 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php', + 'SiteConfiguration' => 'includes/SiteConfiguration.php', + 'SiteStats' => 'includes/SiteStats.php', + 'SiteStatsUpdate' => 'includes/SiteStats.php', + 'Skin' => 'includes/Skin.php', + 'SkinTemplate' => 'includes/SkinTemplate.php', + 'SpecialMycontributions' => 'includes/SpecialPage.php', + 'SpecialMypage' => 'includes/SpecialPage.php', + 'SpecialMytalk' => 'includes/SpecialPage.php', + 'SpecialPage' => 'includes/SpecialPage.php', + 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php', + 'SqlBagOStuff' => 'includes/BagOStuff.php', + 'SquidUpdate' => 'includes/SquidUpdate.php', + 'Status' => 'includes/Status.php', + 'StringUtils' => 'includes/StringUtils.php', + 'TablePager' => 'includes/Pager.php', + 'ThumbnailImage' => 'includes/MediaTransformOutput.php', + 'TitleDependency' => 'includes/CacheDependency.php', + 'Title' => 'includes/Title.php', + 'TitleArray' => 'includes/TitleArray.php', + 'TitleListDependency' => 'includes/CacheDependency.php', + 'TransformParameterError' => 'includes/MediaTransformOutput.php', + 'TurckBagOStuff' => 'includes/BagOStuff.php', + 'UnlistedSpecialPage' => 'includes/SpecialPage.php', + 'UploadBase' => 'includes/UploadBase.php', + 'UploadFromStash' => 'includes/UploadFromStash.php', + 'UploadFromUpload' => 'includes/UploadFromUpload.php', + 'UploadFromUrl' => 'includes/UploadFromUrl.php', + 'User' => 'includes/User.php', + 'UserArray' => 'includes/UserArray.php', + 'UserArrayFromResult' => 'includes/UserArray.php', + 'UserMailer' => 'includes/UserMailer.php', + 'UserRightsProxy' => 'includes/UserRightsProxy.php', + 'WatchedItem' => 'includes/WatchedItem.php', + 'WatchlistEditor' => 'includes/WatchlistEditor.php', + 'WebRequest' => 'includes/WebRequest.php', + 'WebResponse' => 'includes/WebResponse.php', + 'WikiError' => 'includes/WikiError.php', + 'WikiErrorMsg' => 'includes/WikiError.php', + 'WikiExporter' => 'includes/Export.php', + 'WikiXmlError' => 'includes/WikiError.php', + 'XCacheBagOStuff' => 'includes/BagOStuff.php', + 'XmlDumpWriter' => 'includes/Export.php', + 'Xml' => 'includes/Xml.php', + 'XmlSelect' => 'includes/Xml.php', + 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', + 'ZhClient' => 'includes/ZhClient.php', + + # includes/api + 'ApiBase' => 'includes/api/ApiBase.php', + 'ApiBlock' => 'includes/api/ApiBlock.php', + 'ApiDelete' => 'includes/api/ApiDelete.php', + 'ApiDisabled' => 'includes/api/ApiDisabled.php', + 'ApiEditPage' => 'includes/api/ApiEditPage.php', + 'ApiEmailUser' => 'includes/api/ApiEmailUser.php', + 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php', + 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', + 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', + 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php', + 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', + 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', + 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', + 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php', + 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php', + 'ApiFormatXml' => 'includes/api/ApiFormatXml.php', + 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', + 'ApiHelp' => 'includes/api/ApiHelp.php', + 'ApiLogin' => 'includes/api/ApiLogin.php', + 'ApiLogout' => 'includes/api/ApiLogout.php', + 'ApiMain' => 'includes/api/ApiMain.php', + 'ApiMove' => 'includes/api/ApiMove.php', + 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php', + 'ApiPageSet' => 'includes/api/ApiPageSet.php', + 'ApiParamInfo' => 'includes/api/ApiParamInfo.php', + 'ApiParse' => 'includes/api/ApiParse.php', + 'ApiPatrol' => 'includes/api/ApiPatrol.php', + 'ApiProtect' => 'includes/api/ApiProtect.php', + 'ApiPurge' => 'includes/api/ApiPurge.php', + 'ApiQuery' => 'includes/api/ApiQuery.php', + 'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php', + 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php', + 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php', + 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php', + 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php', + 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php', + 'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php', + 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', + 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php', + 'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php', + 'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php', + 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php', + 'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php', + 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php', + 'ApiQueryDisabled' => 'includes/api/ApiQueryDisabled.php', + 'ApiQueryDuplicateFiles' => 'includes/api/ApiQueryDuplicateFiles.php', + 'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php', + 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php', + 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php', + 'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php', + 'ApiQueryImages' => 'includes/api/ApiQueryImages.php', + 'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php', + 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php', + 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php', + 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php', + 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php', + 'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php', + 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', + 'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php', + 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', + 'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php', + 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php', + 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', + 'ApiQueryWatchlistRaw' => 'includes/api/ApiQueryWatchlistRaw.php', + 'ApiResult' => 'includes/api/ApiResult.php', + 'ApiRollback' => 'includes/api/ApiRollback.php', + 'ApiUnblock' => 'includes/api/ApiUnblock.php', + 'ApiUndelete' => 'includes/api/ApiUndelete.php', + 'ApiWatch' => 'includes/api/ApiWatch.php', + 'Services_JSON' => 'includes/api/ApiFormatJson_json.php', + 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php', + 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php', + 'UsageException' => 'includes/api/ApiMain.php', - # includes/api - 'ApiBase' => 'includes/api/ApiBase.php', - 'ApiBlock' => 'includes/api/ApiBlock.php', - 'ApiDelete' => 'includes/api/ApiDelete.php', - 'ApiEditPage' => 'includes/api/ApiEditPage.php', - 'ApiEmailUser' => 'includes/api/ApiEmailUser.php', - 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php', - 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', - 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', - 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php', - 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', - 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', - 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', - 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php', - 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php', - 'ApiFormatXml' => 'includes/api/ApiFormatXml.php', - 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', - 'ApiHelp' => 'includes/api/ApiHelp.php', - 'ApiLogin' => 'includes/api/ApiLogin.php', - 'ApiLogout' => 'includes/api/ApiLogout.php', - 'ApiMain' => 'includes/api/ApiMain.php', - 'ApiMove' => 'includes/api/ApiMove.php', - 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php', - 'ApiPageSet' => 'includes/api/ApiPageSet.php', - 'ApiParamInfo' => 'includes/api/ApiParamInfo.php', - 'ApiParse' => 'includes/api/ApiParse.php', - 'ApiProtect' => 'includes/api/ApiProtect.php', - 'ApiQuery' => 'includes/api/ApiQuery.php', - 'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php', - 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php', - 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php', - 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php', - 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php', - 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php', - 'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php', - 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', - 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php', - 'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php', - 'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php', - 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php', - 'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php', - 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php', - 'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php', - 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php', - 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php', - 'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php', - 'ApiQueryImages' => 'includes/api/ApiQueryImages.php', - 'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php', - 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php', - 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php', - 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php', - 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php', - 'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php', - 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', - 'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php', - 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', - 'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php', - 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php', - 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', - 'ApiResult' => 'includes/api/ApiResult.php', - 'ApiRollback' => 'includes/api/ApiRollback.php', - 'ApiUnblock' => 'includes/api/ApiUnblock.php', - 'ApiUndelete' => 'includes/api/ApiUndelete.php', - 'Services_JSON' => 'includes/api/ApiFormatJson_json.php', - 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php', - 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php', - 'UsageException' => 'includes/api/ApiMain.php', - 'YAMLNode' => 'includes/api/ApiFormatYaml_spyc.php', + # includes/db + 'Blob' => 'includes/db/Database.php', + 'ChronologyProtector' => 'includes/db/LBFactory.php', + 'Database' => 'includes/db/Database.php', + 'DatabaseMssql' => 'includes/db/DatabaseMssql.php', + 'DatabaseMysql' => 'includes/db/Database.php', + 'DatabaseOracle' => 'includes/db/DatabaseOracle.php', + 'DatabasePostgres' => 'includes/db/DatabasePostgres.php', + 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php', + 'DBConnectionError' => 'includes/db/Database.php', + 'DBError' => 'includes/db/Database.php', + 'DBObject' => 'includes/db/Database.php', + 'DBQueryError' => 'includes/db/Database.php', + 'DBUnexpectedError' => 'includes/db/Database.php', + 'LBFactory' => 'includes/db/LBFactory.php', + 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php', + 'LBFactory_Simple' => 'includes/db/LBFactory.php', + 'LoadBalancer' => 'includes/db/LoadBalancer.php', + 'LoadMonitor' => 'includes/db/LoadMonitor.php', + 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php', + 'MSSQLField' => 'includes/db/DatabaseMssql.php', + 'MySQLField' => 'includes/db/Database.php', + 'MySQLMasterPos' => 'includes/db/Database.php', + 'ORABlob' => 'includes/db/DatabaseOracle.php', + 'ORAResult' => 'includes/db/DatabaseOracle.php', + 'PostgresField' => 'includes/db/DatabasePostgres.php', + 'ResultWrapper' => 'includes/db/Database.php', + 'SQLiteField' => 'includes/db/DatabaseSqlite.php', - # includes/db - 'Blob' => 'includes/db/Database.php', - 'ChronologyProtector' => 'includes/db/LBFactory.php', - 'Database' => 'includes/db/Database.php', - 'DatabaseMssql' => 'includes/db/DatabaseMssql.php', - 'DatabaseMysql' => 'includes/db/Database.php', - 'DatabaseOracle' => 'includes/db/DatabaseOracle.php', - 'DatabasePostgres' => 'includes/db/DatabasePostgres.php', - 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php', - 'DBConnectionError' => 'includes/db/Database.php', - 'DBError' => 'includes/db/Database.php', - 'DBObject' => 'includes/db/Database.php', - 'DBQueryError' => 'includes/db/Database.php', - 'DBUnexpectedError' => 'includes/db/Database.php', - 'LBFactory' => 'includes/db/LBFactory.php', - 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php', - 'LBFactory_Simple' => 'includes/db/LBFactory.php', - 'LoadBalancer' => 'includes/db/LoadBalancer.php', - 'LoadMonitor' => 'includes/db/LoadMonitor.php', - 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php', - 'MSSQLField' => 'includes/db/DatabaseMssql.php', - 'MySQLField' => 'includes/db/Database.php', - 'MySQLMasterPos' => 'includes/db/Database.php', - 'ORABlob' => 'includes/db/DatabaseOracle.php', - 'ORAResult' => 'includes/db/DatabaseOracle.php', - 'PostgresField' => 'includes/db/DatabasePostgres.php', - 'ResultWrapper' => 'includes/db/Database.php', - 'SQLiteField' => 'includes/db/DatabaseSqlite.php', + # includes/diff + 'AncestorComparator' => 'includes/diff/HTMLDiff.php', + 'AnchorToString' => 'includes/diff/HTMLDiff.php', + 'ArrayDiffFormatter' => 'includes/diff/DifferenceEngine.php', + 'BodyNode' => 'includes/diff/Nodes.php', + 'ChangeText' => 'includes/diff/HTMLDiff.php', + 'ChangeTextGenerator' => 'includes/diff/HTMLDiff.php', + 'DelegatingContentHandler' => 'includes/diff/HTMLDiff.php', + '_DiffEngine' => 'includes/diff/DifferenceEngine.php', + 'DifferenceEngine' => 'includes/diff/DifferenceEngine.php', + 'DiffFormatter' => 'includes/diff/DifferenceEngine.php', + 'Diff' => 'includes/diff/DifferenceEngine.php', + '_DiffOp_Add' => 'includes/diff/DifferenceEngine.php', + '_DiffOp_Change' => 'includes/diff/DifferenceEngine.php', + '_DiffOp_Copy' => 'includes/diff/DifferenceEngine.php', + '_DiffOp_Delete' => 'includes/diff/DifferenceEngine.php', + '_DiffOp' => 'includes/diff/DifferenceEngine.php', + 'DomTreeBuilder' => 'includes/diff/HTMLDiff.php', + 'DummyNode' => 'includes/diff/Nodes.php', + 'HTMLDiffer' => 'includes/diff/HTMLDiff.php', + 'HTMLOutput' => 'includes/diff/HTMLDiff.php', + '_HWLDF_WordAccumulator' => 'includes/diff/DifferenceEngine.php', + 'ImageNode' => 'includes/diff/Nodes.php', + 'LastCommonParentResult' => 'includes/diff/HTMLDiff.php', + 'MappedDiff' => 'includes/diff/DifferenceEngine.php', + 'Modification' => 'includes/diff/HTMLDiff.php', + 'NoContentTagToString' => 'includes/diff/HTMLDiff.php', + 'Node' => 'includes/diff/Nodes.php', + 'RangeDifference' => 'includes/diff/Diff.php', + 'TableDiffFormatter' => 'includes/diff/DifferenceEngine.php', + 'TagNode' => 'includes/diff/Nodes.php', + 'TagToString' => 'includes/diff/HTMLDiff.php', + 'TagToStringFactory' => 'includes/diff/HTMLDiff.php', + 'TextNode' => 'includes/diff/Nodes.php', + 'TextNodeDiffer' => 'includes/diff/HTMLDiff.php', + 'TextOnlyComparator' => 'includes/diff/HTMLDiff.php', + 'UnifiedDiffFormatter' => 'includes/diff/DifferenceEngine.php', + 'WhiteSpaceNode' => 'includes/diff/Nodes.php', + 'WikiDiff3' => 'includes/diff/Diff.php', + 'WordLevelDiff' => 'includes/diff/DifferenceEngine.php', - # includes/filerepo - 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', - 'File' => 'includes/filerepo/File.php', - 'FileRepo' => 'includes/filerepo/FileRepo.php', - 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php', - 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php', - 'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php', - 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php', - 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', - 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php', - 'FSRepo' => 'includes/filerepo/FSRepo.php', - 'Image' => 'includes/filerepo/Image.php', - 'LocalFile' => 'includes/filerepo/LocalFile.php', - 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php', - 'LocalFileMoveBatch' => 'includes/filerepo/LocalFile.php', - 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php', - 'LocalRepo' => 'includes/filerepo/LocalRepo.php', - 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php', - 'RepoGroup' => 'includes/filerepo/RepoGroup.php', - 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php', + # includes/filerepo + 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', + 'File' => 'includes/filerepo/File.php', + 'FileCache' => 'includes/filerepo/FileCache.php', + 'FileRepo' => 'includes/filerepo/FileRepo.php', + 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php', + 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php', + 'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php', + 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php', + 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', + 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php', + 'FSRepo' => 'includes/filerepo/FSRepo.php', + 'Image' => 'includes/filerepo/Image.php', + 'LocalFile' => 'includes/filerepo/LocalFile.php', + 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php', + 'LocalFileMoveBatch' => 'includes/filerepo/LocalFile.php', + 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php', + 'LocalRepo' => 'includes/filerepo/LocalRepo.php', + 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php', + 'RepoGroup' => 'includes/filerepo/RepoGroup.php', + 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php', - # includes/media - 'BitmapHandler' => 'includes/media/Bitmap.php', - 'BmpHandler' => 'includes/media/BMP.php', - 'DjVuHandler' => 'includes/media/DjVu.php', - 'ImageHandler' => 'includes/media/Generic.php', - 'MediaHandler' => 'includes/media/Generic.php', - 'SvgHandler' => 'includes/media/SVG.php', + # includes/media + 'BitmapHandler' => 'includes/media/Bitmap.php', + 'BitmapHandler_ClientOnly' => 'includes/media/Bitmap_ClientOnly.php', + 'BmpHandler' => 'includes/media/BMP.php', + 'DjVuHandler' => 'includes/media/DjVu.php', + 'ImageHandler' => 'includes/media/Generic.php', + 'MediaHandler' => 'includes/media/Generic.php', + 'SvgHandler' => 'includes/media/SVG.php', - # includes/normal - 'UtfNormal' => 'includes/normal/UtfNormal.php', + # includes/normal + 'UtfNormal' => 'includes/normal/UtfNormal.php', - # includes/parser - 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', - 'DateFormatter' => 'includes/parser/DateFormatter.php', - 'OnlyIncludeReplacer' => 'includes/parser/Parser.php', - 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php', - 'PPDPart' => 'includes/parser/Preprocessor_DOM.php', - 'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php', - 'PPDStack' => 'includes/parser/Preprocessor_DOM.php', - 'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php', - 'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php', - 'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php', - 'PPFrame' => 'includes/parser/Preprocessor.php', - 'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', - 'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', - 'PPNode' => 'includes/parser/Preprocessor.php', - 'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php', - 'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php', - 'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php', - 'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php', - 'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php', - 'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', - 'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', - 'Parser' => 'includes/parser/Parser.php', - 'ParserCache' => 'includes/parser/ParserCache.php', - 'ParserOptions' => 'includes/parser/ParserOptions.php', - 'ParserOutput' => 'includes/parser/ParserOutput.php', - 'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php', - 'Parser_OldPP' => 'includes/parser/Parser_OldPP.php', - 'Preprocessor' => 'includes/parser/Preprocessor.php', - 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php', - 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php', - 'StripState' => 'includes/parser/Parser.php', + # includes/parser + 'CoreLinkFunctions' => 'includes/parser/CoreLinkFunctions.php', + 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', + 'DateFormatter' => 'includes/parser/DateFormatter.php', + 'LinkHolderArray' => 'includes/parser/LinkHolderArray.php', + 'LinkMarkerReplacer' => 'includes/parser/LinkMarkerReplacer.php', + 'OnlyIncludeReplacer' => 'includes/parser/Parser.php', + 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDPart' => 'includes/parser/Preprocessor_DOM.php', + 'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDStack' => 'includes/parser/Preprocessor_DOM.php', + 'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php', + 'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPFrame' => 'includes/parser/Preprocessor.php', + 'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode' => 'includes/parser/Preprocessor.php', + 'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php', + 'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'Parser' => 'includes/parser/Parser.php', + 'ParserCache' => 'includes/parser/ParserCache.php', + 'ParserOptions' => 'includes/parser/ParserOptions.php', + 'ParserOutput' => 'includes/parser/ParserOutput.php', + 'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php', + 'Parser_LinkHooks' => 'includes/parser/Parser_LinkHooks.php', + 'Preprocessor' => 'includes/parser/Preprocessor.php', + 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'StripState' => 'includes/parser/Parser.php', - # includes/specials - 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php', - 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php', - 'ContribsPager' => 'includes/specials/SpecialContributions.php', - 'DBLockForm' => 'includes/specials/SpecialLockdb.php', - 'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php', - 'DeadendPagesPage' => 'includes/specials/SpecialDeadendpages.php', - 'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php', - 'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php', - 'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php', - 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php', - 'EmailUserForm' => 'includes/specials/SpecialEmailuser.php', - 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php', - 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php', - 'IPBlockForm' => 'includes/specials/SpecialBlockip.php', - 'IPBlocklistPager' => 'includes/specials/SpecialIpblocklist.php', - 'IPUnblockForm' => 'includes/specials/SpecialIpblocklist.php', - 'ImportReporter' => 'includes/specials/SpecialImport.php', - 'ImportStreamSource' => 'includes/specials/SpecialImport.php', - 'ImportStringSource' => 'includes/specials/SpecialImport.php', - 'ListredirectsPage' => 'includes/specials/SpecialListredirects.php', - 'LoginForm' => 'includes/specials/SpecialUserlogin.php', - 'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php', - 'LongPagesPage' => 'includes/specials/SpecialLongpages.php', - 'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php', - 'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php', - 'MostimagesPage' => 'includes/specials/SpecialMostimages.php', - 'MostlinkedCategoriesPage' => 'includes/specials/SpecialMostlinkedcategories.php', - 'MostlinkedPage' => 'includes/specials/SpecialMostlinked.php', - 'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php', - 'MovePageForm' => 'includes/specials/SpecialMovepage.php', - 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php', - 'NewPagesPager' => 'includes/specials/SpecialNewpages.php', - 'PageArchive' => 'includes/specials/SpecialUndelete.php', - 'PasswordResetForm' => 'includes/specials/SpecialResetpass.php', - 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php', - 'PreferencesForm' => 'includes/specials/SpecialPreferences.php', - 'RandomPage' => 'includes/specials/SpecialRandompage.php', - 'RevisionDeleteForm' => 'includes/specials/SpecialRevisiondelete.php', - 'RevisionDeleter' => 'includes/specials/SpecialRevisiondelete.php', - 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php', - 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php', - 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', - 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', - 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php', - 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php', - 'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php', - 'SpecialRecentchanges' => 'includes/specials/SpecialRecentchanges.php', - 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', - 'SpecialSearch' => 'includes/specials/SpecialSearch.php', - 'SpecialVersion' => 'includes/specials/SpecialVersion.php', - 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php', - 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php', - 'UncategorizedTemplatesPage' => 'includes/specials/SpecialUncategorizedtemplates.php', - 'UndeleteForm' => 'includes/specials/SpecialUndelete.php', - 'UnusedCategoriesPage' => 'includes/specials/SpecialUnusedcategories.php', - 'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php', - 'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php', - 'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php', - 'UploadForm' => 'includes/specials/SpecialUpload.php', - 'UploadFormMogile' => 'includes/specials/SpecialUploadMogile.php', - 'UserrightsPage' => 'includes/specials/SpecialUserrights.php', - 'UsersPager' => 'includes/specials/SpecialListusers.php', - 'WantedCategoriesPage' => 'includes/specials/SpecialWantedcategories.php', - 'WantedPagesPage' => 'includes/specials/SpecialWantedpages.php', - 'WhatLinksHerePage' => 'includes/specials/SpecialWhatlinkshere.php', - 'WikiImporter' => 'includes/specials/SpecialImport.php', - 'WikiRevision' => 'includes/specials/SpecialImport.php', - 'WithoutInterwikiPage' => 'includes/specials/SpecialWithoutinterwiki.php', + # includes/specials + 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php', + 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php', + 'ContribsPager' => 'includes/specials/SpecialContributions.php', + 'DBLockForm' => 'includes/specials/SpecialLockdb.php', + 'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php', + 'DeadendPagesPage' => 'includes/specials/SpecialDeadendpages.php', + 'DeletedContributionsPage' => 'includes/specials/SpecialDeletedContributions.php', + 'DeletedContribsPager' => 'includes/specials/SpecialDeletedContributions.php', + 'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php', + 'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php', + 'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php', + 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php', + 'EmailUserForm' => 'includes/specials/SpecialEmailuser.php', + 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php', + 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php', + 'IPBlockForm' => 'includes/specials/SpecialBlockip.php', + 'IPBlocklistPager' => 'includes/specials/SpecialIpblocklist.php', + 'IPUnblockForm' => 'includes/specials/SpecialIpblocklist.php', + 'ImportReporter' => 'includes/specials/SpecialImport.php', + 'ImportStreamSource' => 'includes/Import.php', + 'ImportStringSource' => 'includes/Import.php', + 'LinkSearchPage' => 'includes/specials/SpecialLinkSearch.php', + 'ListredirectsPage' => 'includes/specials/SpecialListredirects.php', + 'LoginForm' => 'includes/specials/SpecialUserlogin.php', + 'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php', + 'LongPagesPage' => 'includes/specials/SpecialLongpages.php', + 'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php', + 'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php', + 'MostimagesPage' => 'includes/specials/SpecialMostimages.php', + 'MostlinkedCategoriesPage' => 'includes/specials/SpecialMostlinkedcategories.php', + 'MostlinkedPage' => 'includes/specials/SpecialMostlinked.php', + 'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php', + 'MovePageForm' => 'includes/specials/SpecialMovepage.php', + 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php', + 'SpecialContributions' => 'includes/specials/SpecialContributions.php', + 'NewPagesPager' => 'includes/specials/SpecialNewpages.php', + 'PageArchive' => 'includes/specials/SpecialUndelete.php', + 'SpecialResetpass' => 'includes/specials/SpecialResetpass.php', + 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php', + 'PreferencesForm' => 'includes/specials/SpecialPreferences.php', + 'RandomPage' => 'includes/specials/SpecialRandompage.php', + 'RevisionDeleteForm' => 'includes/specials/SpecialRevisiondelete.php', + 'RevisionDeleter' => 'includes/specials/SpecialRevisiondelete.php', + 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php', + 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php', + 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', + 'SpecialImport' => 'includes/specials/SpecialImport.php', + 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', + 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php', + 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php', + 'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php', + 'SpecialRecentchanges' => 'includes/specials/SpecialRecentchanges.php', + 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', + 'SpecialSearch' => 'includes/specials/SpecialSearch.php', + 'SpecialSearchOld' => 'includes/specials/SpecialSearch.php', + 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php', + 'SpecialVersion' => 'includes/specials/SpecialVersion.php', + 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php', + 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php', + 'UncategorizedTemplatesPage' => 'includes/specials/SpecialUncategorizedtemplates.php', + 'UndeleteForm' => 'includes/specials/SpecialUndelete.php', + 'UnusedCategoriesPage' => 'includes/specials/SpecialUnusedcategories.php', + 'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php', + 'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php', + 'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php', + 'UploadForm' => 'includes/specials/SpecialUpload.php', + 'UploadFormMogile' => 'includes/specials/SpecialUploadMogile.php', + 'UserrightsPage' => 'includes/specials/SpecialUserrights.php', + 'UsersPager' => 'includes/specials/SpecialListusers.php', + 'WantedCategoriesPage' => 'includes/specials/SpecialWantedcategories.php', + 'WantedFilesPage' => 'includes/specials/SpecialWantedfiles.php', + 'WantedPagesPage' => 'includes/specials/SpecialWantedpages.php', + 'WantedTemplatesPage' => 'includes/specials/SpecialWantedtemplates.php', + 'WhatLinksHerePage' => 'includes/specials/SpecialWhatlinkshere.php', + 'WikiImporter' => 'includes/Import.php', + 'WikiRevision' => 'includes/Import.php', + 'WithoutInterwikiPage' => 'includes/specials/SpecialWithoutinterwiki.php', - # includes/templates - 'UsercreateTemplate' => 'includes/templates/Userlogin.php', - 'UserloginTemplate' => 'includes/templates/Userlogin.php', + # includes/templates + 'UsercreateTemplate' => 'includes/templates/Userlogin.php', + 'UserloginTemplate' => 'includes/templates/Userlogin.php', - # languages - 'Language' => 'languages/Language.php', - 'FakeConverter' => 'languages/Language.php', + # languages + 'Language' => 'languages/Language.php', + 'FakeConverter' => 'languages/Language.php', - # maintenance/language - 'statsOutput' => 'maintenance/language/StatOutputs.php', - 'wikiStatsOutput' => 'maintenance/language/StatOutputs.php', - 'metawikiStatsOutput' => 'maintenance/language/StatOutputs.php', - 'textStatsOutput' => 'maintenance/language/StatOutputs.php', - 'csvStatsOutput' => 'maintenance/language/StatOutputs.php', + # maintenance/language + 'statsOutput' => 'maintenance/language/StatOutputs.php', + 'wikiStatsOutput' => 'maintenance/language/StatOutputs.php', + 'metawikiStatsOutput' => 'maintenance/language/StatOutputs.php', + 'textStatsOutput' => 'maintenance/language/StatOutputs.php', + 'csvStatsOutput' => 'maintenance/language/StatOutputs.php', - ); +); +class AutoLoader { /** * autoload - take a class name and attempt to load it - * + * * @param string $className Name of class we're looking for. * @return bool Returning false is important on failure as * it allows Zend to try and look in other registered autoloaders - * as well. + * as well. */ static function autoload( $className ) { - global $wgAutoloadClasses; + global $wgAutoloadClasses, $wgAutoloadLocalClasses; - wfProfileIn( __METHOD__ ); - if ( isset( self::$localClasses[$className] ) ) { - $filename = self::$localClasses[$className]; + if ( isset( $wgAutoloadLocalClasses[$className] ) ) { + $filename = $wgAutoloadLocalClasses[$className]; } elseif ( isset( $wgAutoloadClasses[$className] ) ) { $filename = $wgAutoloadClasses[$className]; } else { @@ -488,14 +549,15 @@ class AutoLoader { # The case can sometimes be wrong when unserializing PHP 4 objects $filename = false; $lowerClass = strtolower( $className ); - foreach ( self::$localClasses as $class2 => $file2 ) { + foreach ( $wgAutoloadLocalClasses as $class2 => $file2 ) { if ( strtolower( $class2 ) == $lowerClass ) { $filename = $file2; } } if ( !$filename ) { + if( function_exists( 'wfDebug' ) ) + wfDebug( "Class {$className} not found; skipped loading" ); # Give up - wfProfileOut( __METHOD__ ); return false; } } @@ -506,7 +568,6 @@ class AutoLoader { $filename = "$IP/$filename"; } require( $filename ); - wfProfileOut( __METHOD__ ); return true; } @@ -532,4 +593,3 @@ if ( function_exists( 'spl_autoload_register' ) ) { AutoLoader::autoload( $class ); } } - diff --git a/includes/Autopromote.php b/includes/Autopromote.php index 68fe6636..c8a4c03b 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -19,7 +19,7 @@ class Autopromote { $promote[] = $group; } - wfRunHooks( 'GetAutoPromoteGroups', array($user, &$promote) ); + wfRunHooks( 'GetAutoPromoteGroups', array( $user, &$promote ) ); return $promote; } @@ -106,9 +106,16 @@ class Autopromote { case APCOND_AGE: $age = time() - wfTimestampOrNull( TS_UNIX, $user->getRegistration() ); return $age >= $cond[1]; + case APCOND_AGE_FROM_EDIT: + $age = time() - wfTimestampOrNull( TS_UNIX, $user->getFirstEditTimestamp() ); + return $age >= $cond[1]; case APCOND_INGROUPS: $groups = array_slice( $cond, 1 ); return count( array_intersect( $groups, $user->getGroups() ) ) == count( $groups ); + case APCOND_ISIP: + return $cond[1] == wfGetIP(); + case APCOND_IPINRANGE: + return IP::isInRange( wfGetIP(), $cond[1] ); default: $result = null; wfRunHooks( 'AutopromoteCondition', array( $cond[0], array_slice( $cond, 1 ), $user, &$result ) ); diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php index 92311329..572dca6c 100644 --- a/includes/BagOStuff.php +++ b/includes/BagOStuff.php @@ -475,8 +475,19 @@ class MediaWikiBagOStuff extends SqlBagOStuff { function _fromunixtime($ts) { return $this->_getDB()->timestamp($ts); } + /*** + * Note -- this should *not* check wfReadOnly(). + * Read-only mode has been repurposed from the original + * "nothing must write to the database" to "users should not + * be able to edit or alter anything user-visible". + * + * Backend bits like the object cache should continue + * to work in this mode, otherwise things will blow up + * like the message cache failing to save its state, + * causing long delays (bug 11533). + */ function _readonly(){ - return wfReadOnly(); + return false; } function _strencode($s) { return $this->_getDB()->strencode($s); diff --git a/includes/Block.php b/includes/Block.php index b208fa8a..2c2227e2 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -13,11 +13,10 @@ * * @todo This could be used everywhere, but it isn't. */ -class Block -{ +class Block { /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry, $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName, - $mBlockEmail, $mByName, $mAngryAutoblock; + $mBlockEmail, $mByName, $mAngryAutoblock, $mAllowUsertalk; /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster; const EB_KEEP_EXPIRED = 1; @@ -26,7 +25,7 @@ class Block function __construct( $address = '', $user = 0, $by = 0, $reason = '', $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, - $hideName = 0, $blockEmail = 0 ) + $hideName = 0, $blockEmail = 0, $allowUsertalk = 0 ) { $this->mId = 0; # Expand valid IPv6 addresses @@ -43,6 +42,7 @@ class Block $this->mEnableAutoblock = $enableAutoblock; $this->mHideName = $hideName; $this->mBlockEmail = $blockEmail; + $this->mAllowUsertalk = $allowUsertalk; $this->mForUpdate = false; $this->mFromMaster = false; $this->mByName = false; @@ -50,9 +50,18 @@ class Block $this->initialiseRange(); } - static function newFromDB( $address, $user = 0, $killExpired = true ) - { - $block = new Block(); + /** + * Load a block from the database, using either the IP address or + * user ID. Tries the user ID first, and if that doesn't work, tries + * the address. + * + * @param $address String: IP address of user/anon + * @param $user Integer: user id of user + * @param $killExpired Boolean: delete expired blocks on load + * @return Block Object + */ + public static function newFromDB( $address, $user = 0, $killExpired = true ) { + $block = new Block; $block->load( $address, $user, $killExpired ); if ( $block->isValid() ) { return $block; @@ -61,8 +70,13 @@ class Block } } - static function newFromID( $id ) - { + /** + * Load a blocked user from their block id. + * + * @param $id Integer: Block id to search for + * @return Block object + */ + public static function newFromID( $id ) { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*', array( 'ipb_id' => $id ), __METHOD__ ) ); @@ -73,21 +87,47 @@ class Block return null; } } + + /** + * Check if two blocks are effectively equal + * + * @return Boolean + */ + public function equals( Block $block ) { + return ( + $this->mAddress == $block->mAddress + && $this->mUser == $block->mUser + && $this->mAuto == $block->mAuto + && $this->mAnonOnly == $block->mAnonOnly + && $this->mCreateAccount == $block->mCreateAccount + && $this->mExpiry == $block->mExpiry + && $this->mEnableAutoblock == $block->mEnableAutoblock + && $this->mHideName == $block->mHideName + && $this->mBlockEmail == $block->mBlockEmail + && $this->mAllowUsertalk == $block->mAllowUsertalk + ); + } - function clear() - { + /** + * Clear all member variables in the current object. Does not clear + * the block from the DB. + */ + public function clear() { $this->mAddress = $this->mReason = $this->mTimestamp = ''; $this->mId = $this->mAnonOnly = $this->mCreateAccount = $this->mEnableAutoblock = $this->mAuto = $this->mUser = - $this->mBy = $this->mHideName = $this->mBlockEmail = 0; + $this->mBy = $this->mHideName = $this->mBlockEmail = $this->mAllowUsertalk = 0; $this->mByName = false; } /** - * Get the DB object and set the reference parameter to the query options + * Get the DB object and set the reference parameter to the select options. + * The options array will contain FOR UPDATE if appropriate. + * + * @param $options Array + * @return Database */ - function &getDBOptions( &$options ) - { + protected function &getDBOptions( &$options ) { global $wgAntiLockFlags; if ( $this->mForUpdate || $this->mFromMaster ) { $db = wfGetDB( DB_MASTER ); @@ -104,15 +144,15 @@ class Block } /** - * Get a ban from the DB, with either the given address or the given username + * Get a block from the DB, with either the given address or the given username * - * @param string $address The IP address of the user, or blank to skip IP blocks - * @param integer $user The user ID, or zero for anonymous users - * @param bool $killExpired Whether to delete expired rows while loading + * @param $address string The IP address of the user, or blank to skip IP blocks + * @param $user int The user ID, or zero for anonymous users + * @param $killExpired bool Whether to delete expired rows while loading + * @return Boolean: the user is blocked from editing * */ - function load( $address = '', $user = 0, $killExpired = true ) - { + public function load( $address = '', $user = 0, $killExpired = true ) { wfDebug( "Block::load: '$address', '$user', $killExpired\n" ); $options = array(); @@ -143,7 +183,10 @@ class Block if ( $user && $this->mAnonOnly ) { # Block is marked anon-only # Whitelist this IP address against autoblocks and range blocks - $this->clear(); + # (but not account creation blocks -- bug 13611) + if( !$this->mCreateAccount ) { + $this->clear(); + } return false; } else { return true; @@ -154,7 +197,10 @@ class Block # Try range block if ( $this->loadRange( $address, $killExpired, $user ) ) { if ( $user && $this->mAnonOnly ) { - $this->clear(); + # Respect account creation blocks on logged-in users -- bug 13611 + if( !$this->mCreateAccount ) { + $this->clear(); + } return false; } else { return true; @@ -180,9 +226,12 @@ class Block /** * Fill in member variables from a result wrapper + * + * @param $res ResultWrapper: row from the ipblocks table + * @param $killExpired Boolean: whether to delete expired rows while loading + * @return Boolean */ - function loadFromResult( ResultWrapper $res, $killExpired = true ) - { + protected function loadFromResult( ResultWrapper $res, $killExpired = true ) { $ret = false; if ( 0 != $res->numRows() ) { # Get first block @@ -216,9 +265,13 @@ class Block /** * Search the database for any range blocks matching the given address, and * load the row if one is found. + * + * @param $address String: IP address range + * @param $killExpired Boolean: whether to delete expired rows while loading + * @param $userid Integer: if not 0, then sets ipb_anon_only + * @return Boolean */ - function loadRange( $address, $killExpired = true, $user = 0 ) - { + public function loadRange( $address, $killExpired = true, $user = 0 ) { $iaddr = IP::toHex( $address ); if ( $iaddr === false ) { # Invalid address @@ -247,15 +300,12 @@ class Block } /** - * Determine if a given integer IPv4 address is in a given CIDR network - * @deprecated Use IP::isInRange + * Given a database row from the ipblocks table, initialize + * member variables + * + * @param $row ResultWrapper: a row from the ipblocks table */ - function isAddressInRange( $addr, $range ) { - return IP::isInRange( $addr, $range ); - } - - function initFromRow( $row ) - { + public function initFromRow( $row ) { $this->mAddress = $row->ipb_address; $this->mReason = $row->ipb_reason; $this->mTimestamp = wfTimestamp(TS_MW,$row->ipb_timestamp); @@ -266,6 +316,7 @@ class Block $this->mCreateAccount = $row->ipb_create_account; $this->mEnableAutoblock = $row->ipb_enable_autoblock; $this->mBlockEmail = $row->ipb_block_email; + $this->mAllowUsertalk = $row->ipb_allow_usertalk; $this->mHideName = $row->ipb_deleted; $this->mId = $row->ipb_id; $this->mExpiry = self::decodeExpiry( $row->ipb_expiry ); @@ -278,8 +329,11 @@ class Block $this->mRangeEnd = $row->ipb_range_end; } - function initialiseRange() - { + /** + * Once $mAddress has been set, get the range they came from. + * Wrapper for IP::parseRange + */ + protected function initialiseRange() { $this->mRangeStart = ''; $this->mRangeEnd = ''; @@ -289,64 +343,12 @@ class Block } /** - * Callback with a Block object for every block - * @return integer number of blocks; + * Delete the row from the IP blocks table. + * + * @return Boolean */ - /*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(); - - list( $ipblocks, $user ) = $db->tableNamesN( '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 ); - } - } - $db->freeResult( $res ); - return $num_rows; - } - - function delete() - { - if (wfReadOnly()) { + public function delete() { + if ( wfReadOnly() ) { return false; } if ( !$this->mId ) { @@ -359,33 +361,17 @@ class Block } /** - * Insert a block into the block table. - * @return Whether or not the insertion was successful. - */ - function insert() - { + * Insert a block into the block table. Will fail if there is a conflicting + * block (same name and options) already in the database. + * + * @return Boolean: whether or not the insertion was successful. + */ + public function insert() { wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); $dbw = wfGetDB( DB_MASTER ); - # Unset ipb_anon_only for user blocks, makes no sense - if ( $this->mUser ) { - $this->mAnonOnly = 0; - } - - # Unset ipb_enable_autoblock for IP blocks, makes no sense - if ( !$this->mUser ) { - $this->mEnableAutoblock = 0; - $this->mBlockEmail = 0; //Same goes for email... - } - - if( !$this->mByName ) { - if( $this->mBy ) { - $this->mByName = User::whoIs( $this->mBy ); - } else { - global $wgUser; - $this->mByName = $wgUser->getName(); - } - } + $this->validateBlockParams(); + $this->initialiseRange(); # Don't collide with expired blocks Block::purgeExpired(); @@ -408,7 +394,8 @@ class Block 'ipb_range_start' => $this->mRangeStart, 'ipb_range_end' => $this->mRangeEnd, 'ipb_deleted' => $this->mHideName, - 'ipb_block_email' => $this->mBlockEmail + 'ipb_block_email' => $this->mBlockEmail, + 'ipb_allow_usertalk' => $this->mAllowUsertalk ), 'Block::insert', array( 'IGNORE' ) ); $affected = $dbw->affectedRows(); @@ -416,15 +403,76 @@ class Block if ($affected) $this->doRetroactiveAutoblock(); - return $affected; + return (bool)$affected; + } + + /** + * Update a block in the DB with new parameters. + * The ID field needs to be loaded first. + */ + public function update() { + wfDebug( "Block::update; timestamp {$this->mTimestamp}\n" ); + $dbw = wfGetDB( DB_MASTER ); + + $this->validateBlockParams(); + + $dbw->update( 'ipblocks', + array( + 'ipb_user' => $this->mUser, + 'ipb_by' => $this->mBy, + 'ipb_by_text' => $this->mByName, + 'ipb_reason' => $this->mReason, + 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), + 'ipb_auto' => $this->mAuto, + 'ipb_anon_only' => $this->mAnonOnly, + 'ipb_create_account' => $this->mCreateAccount, + 'ipb_enable_autoblock' => $this->mEnableAutoblock, + 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), + 'ipb_range_start' => $this->mRangeStart, + 'ipb_range_end' => $this->mRangeEnd, + 'ipb_deleted' => $this->mHideName, + 'ipb_block_email' => $this->mBlockEmail, + 'ipb_allow_usertalk' => $this->mAllowUsertalk ), + array( 'ipb_id' => $this->mId ), + 'Block::update' ); + + return $dbw->affectedRows(); } + + /** + * Make sure all the proper members are set to sane values + * before adding/updating a block + */ + protected function validateBlockParams() { + # Unset ipb_anon_only for user blocks, makes no sense + if ( $this->mUser ) { + $this->mAnonOnly = 0; + } + + # Unset ipb_enable_autoblock for IP blocks, makes no sense + if ( !$this->mUser ) { + $this->mEnableAutoblock = 0; + $this->mBlockEmail = 0; //Same goes for email... + } + if( !$this->mByName ) { + if( $this->mBy ) { + $this->mByName = User::whoIs( $this->mBy ); + } else { + global $wgUser; + $this->mByName = $wgUser->getName(); + } + } + } + + /** * Retroactively autoblocks the last IP used by the user (if it is a user) * blocked by this Block. - *@return Whether or not a retroactive autoblock was made. + * + * @return Boolean: whether or not a retroactive autoblock was made. */ - function doRetroactiveAutoblock() { + public function doRetroactiveAutoblock() { $dbr = wfGetDB( DB_SLAVE ); #If autoblock is enabled, autoblock the LAST IP used # - stolen shamelessly from CheckUser_body.php @@ -458,25 +506,25 @@ class Block } } } - + /** - * Autoblocks the given IP, referring to this Block. - * @param string $autoblockip The IP to autoblock. - * @param bool $justInserted The main block was just inserted - * @return bool Whether or not an autoblock was inserted. - */ - function doAutoblock( $autoblockip, $justInserted = false ) { - # If autoblocks are disabled, go away. - if ( !$this->mEnableAutoblock ) { - return; + * Checks whether a given IP is on the autoblock whitelist. + * + * @param $ip String: The IP to check + * @return Boolean + */ + public static function isWhitelistedFromAutoblocks( $ip ) { + global $wgMemc; + + // Try to get the autoblock_whitelist from the cache, as it's faster + // than getting the msg raw and explode()'ing it. + $key = wfMemcKey( 'ipb', 'autoblock', 'whitelist' ); + $lines = $wgMemc->get( $key ); + if ( !$lines ) { + $lines = explode( "\n", wfMsgForContentNoTrans( 'autoblock_whitelist' ) ); + $wgMemc->set( $key, $lines, 3600 * 24 ); } - # Check for presence on the autoblock whitelist - # TODO cache this? - $lines = explode( "\n", wfMsgForContentNoTrans( 'autoblock_whitelist' ) ); - - $ip = $autoblockip; - wfDebug("Checking the autoblock whitelist..\n"); foreach( $lines as $line ) { @@ -493,23 +541,42 @@ class Block # Is the IP in this range? if (IP::isInRange( $ip, $wlEntry )) { wfDebug(" IP $ip matches $wlEntry, not autoblocking\n"); - #$autoblockip = null; # Don't autoblock a whitelisted IP. - return; #This /SHOULD/ introduce a dummy block - but - # I don't know a safe way to do so. -werdna + return true; } else { wfDebug( " No match\n" ); } } + return false; + } + + /** + * Autoblocks the given IP, referring to this Block. + * + * @param $autoblockIP String: the IP to autoblock. + * @param $justInserted Boolean: the main block was just inserted + * @return Boolean: whether or not an autoblock was inserted. + */ + public function doAutoblock( $autoblockIP, $justInserted = false ) { + # If autoblocks are disabled, go away. + if ( !$this->mEnableAutoblock ) { + return; + } + + # Check for presence on the autoblock whitelist + if (Block::isWhitelistedFromAutoblocks($autoblockIP)) { + return; + } + ## Allow hooks to cancel the autoblock. - if (!wfRunHooks( 'AbortAutoblock', array( $autoblockip, &$this ) )) { + if (!wfRunHooks( 'AbortAutoblock', array( $autoblockIP, &$this ) )) { wfDebug( "Autoblock aborted by hook." ); return false; } # It's okay to autoblock. Go ahead and create/insert the block. - $ipblock = Block::newFromDB( $autoblockip ); + $ipblock = Block::newFromDB( $autoblockIP ); if ( $ipblock ) { # If the user is already blocked. Then check if the autoblock would # exceed the user block. If it would exceed, then do nothing, else @@ -528,8 +595,8 @@ class Block } # Make a new block object with the desired properties - wfDebug( "Autoblocking {$this->mAddress}@" . $autoblockip . "\n" ); - $ipblock->mAddress = $autoblockip; + wfDebug( "Autoblocking {$this->mAddress}@" . $autoblockIP . "\n" ); + $ipblock->mAddress = $autoblockIP; $ipblock->mUser = 0; $ipblock->mBy = $this->mBy; $ipblock->mByName = $this->mByName; @@ -539,7 +606,7 @@ class Block $ipblock->mCreateAccount = $this->mCreateAccount; # Continue suppressing the name if needed $ipblock->mHideName = $this->mHideName; - + $ipblock->mAllowUsertalk = $this->mAllowUsertalk; # If the user is already blocked with an expiry date, we don't # want to pile on top of that! if($this->mExpiry) { @@ -551,8 +618,11 @@ class Block return $ipblock->insert(); } - function deleteIfExpired() - { + /** + * Check if a block has expired. Delete it if it is. + * @return Boolean + */ + public function deleteIfExpired() { $fname = 'Block::deleteIfExpired'; wfProfileIn( $fname ); if ( $this->isExpired() ) { @@ -567,8 +637,11 @@ class Block return $retVal; } - function isExpired() - { + /** + * Has the block expired? + * @return Boolean + */ + public function isExpired() { wfDebug( "Block::isExpired() checking current " . wfTimestampNow() . " vs $this->mExpiry\n" ); if ( !$this->mExpiry ) { return false; @@ -577,13 +650,18 @@ class Block } } - function isValid() - { + /** + * Is the block address valid (i.e. not a null string?) + * @return Boolean + */ + public function isValid() { return $this->mAddress != ''; } - function updateTimestamp() - { + /** + * Update the timestamp on autoblocks. + */ + public function updateTimestamp() { if ( $this->mAuto ) { $this->mTimestamp = wfTimestamp(); $this->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); @@ -600,41 +678,43 @@ class Block } } - /* - function getIntegerAddr() - { - return $this->mIntegerAddr; - } - - function getNetworkBits() - { - return $this->mNetworkBits; - }*/ - /** - * @return The blocker user ID. + * Get the user id of the blocking sysop + * + * @return Integer */ public function getBy() { return $this->mBy; } /** - * @return The blocker user name. + * Get the username of the blocking sysop + * + * @return String */ - function getByName() - { + public function getByName() { return $this->mByName; } - function forUpdate( $x = NULL ) { + /** + * Get/set the SELECT ... FOR UPDATE flag + */ + public function forUpdate( $x = NULL ) { return wfSetVar( $this->mForUpdate, $x ); } - function fromMaster( $x = NULL ) { + /** + * Get/set a flag determining whether the master is used for reads + */ + public function fromMaster( $x = NULL ) { return wfSetVar( $this->mFromMaster, $x ); } - function getRedactedName() { + /** + * Get the block name, but with autoblocked IPs hidden as per standard privacy policy + * @return String + */ + public function getRedactedName() { if ( $this->mAuto ) { return '#' . $this->mId; } else { @@ -644,8 +724,12 @@ class Block /** * Encode expiry for DB + * + * @param $expiry String: timestamp for expiry, or + * @param $db Database object + * @return String */ - static function encodeExpiry( $expiry, $db ) { + public static function encodeExpiry( $expiry, $db ) { if ( $expiry == '' || $expiry == Block::infinity() ) { return Block::infinity(); } else { @@ -655,8 +739,12 @@ class Block /** * Decode expiry which has come from the DB + * + * @param $expiry String: Database expiry format + * @param $timestampType Requested timestamp format + * @return String */ - static function decodeExpiry( $expiry, $timestampType = TS_MW ) { + public static function decodeExpiry( $expiry, $timestampType = TS_MW ) { if ( $expiry == '' || $expiry == Block::infinity() ) { return Block::infinity(); } else { @@ -664,8 +752,12 @@ class Block } } - static function getAutoblockExpiry( $timestamp ) - { + /** + * Get a timestamp of the expiry for autoblocks + * + * @return String + */ + public static function getAutoblockExpiry( $timestamp ) { global $wgAutoblockExpiry; return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); } @@ -673,8 +765,10 @@ class Block /** * Gets rid of uneeded numbers in quad-dotted/octet IP strings * For example, 127.111.113.151/24 -> 127.111.113.0/24 + * @param $range String: IP address to normalize + * @return string */ - static function normaliseRange( $range ) { + public static function normaliseRange( $range ) { $parts = explode( '/', $range ); if ( count( $parts ) == 2 ) { // IPv6 @@ -706,31 +800,31 @@ class Block /** * Purge expired blocks from the ipblocks table */ - static function purgeExpired() { + public static function purgeExpired() { $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); } - static function infinity() { + /** + * Get a value to insert into expiry field of the database when infinite expiry + * is desired. In principle this could be DBMS-dependant, but currently all + * supported DBMS's support the string "infinity", so we just use that. + * + * @return String + */ + public static function infinity() { # This is a special keyword for timestamps in PostgreSQL, and # works with CHAR(14) as well because "i" sorts after all numbers. return 'infinity'; - - /* - static $infinity; - if ( !isset( $infinity ) ) { - $dbr = wfGetDB( DB_SLAVE ); - $infinity = $dbr->bigTimestamp(); - } - return $infinity; - */ } /** * Convert a DB-encoded expiry into a real string that humans can read. + * + * @param $encoded_expiry String: Database encoded expiry time + * @return String */ - static function formatExpiry( $encoded_expiry ) { - + public static function formatExpiry( $encoded_expiry ) { static $msg = null; if( is_null( $msg ) ) { @@ -749,14 +843,15 @@ class Block $expiretimestr = $wgLang->timeanddate( $expiry, true ); $expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array($expiretimestr) ); } - return $expirystr; } /** * Convert a typed-in expiry time into something we can put into the database. + * @param $expiry_input String: whatever was typed into the form + * @return String: more database friendly */ - static function parseExpiryInput( $expiry_input ) { + public static function parseExpiryInput( $expiry_input ) { if ( $expiry_input == 'infinite' || $expiry_input == 'indefinite' ) { $expiry = 'infinity'; } else { @@ -765,7 +860,6 @@ class Block return false; } } - return $expiry; } diff --git a/includes/Category.php b/includes/Category.php index acafc47a..78567add 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -1,6 +1,8 @@ <?php /** - * Category objects are immutable, strictly speaking. If you call methods that change the database, like to refresh link counts, the objects will be appropriately reinitialized. Member variables are lazy-initialized. + * Category objects are immutable, strictly speaking. If you call methods that change the database, + * like to refresh link counts, the objects will be appropriately reinitialized. + * Member variables are lazy-initialized. * * TODO: Move some stuff from CategoryPage.php to here, and use that. * @@ -79,7 +81,7 @@ class Category { /** * Factory function. * - * @param array $name A category name (no "Category:" prefix). It need + * @param $name Array: A category name (no "Category:" prefix). It need * not be normalized, with spaces replaced by underscores. * @return mixed Category, or false on a totally invalid name */ @@ -99,8 +101,8 @@ class Category { /** * Factory function. * - * @param array $title Title for the category page - * @return mixed Category, or false on a totally invalid name + * @param $title Title for the category page + * @return Mixed: category, or false on a totally invalid name */ public static function newFromTitle( $title ) { $cat = new self(); @@ -114,7 +116,7 @@ class Category { /** * Factory function. * - * @param array $id A category id + * @param $id Integer: a category id * @return Category */ public static function newFromID( $id ) { @@ -192,6 +194,33 @@ class Category { return $this->mTitle; } + /** + * Fetch a TitleArray of up to $limit category members, beginning after the + * category sort key $offset. + * @param $limit integer + * @param $offset string + * @return TitleArray object for category members. + */ + public function getMembers( $limit = false, $offset = '' ) { + $dbr = wfGetDB( DB_SLAVE ); + + $conds = array( 'cl_to' => $this->getName(), 'cl_from = page_id' ); + $options = array( 'ORDER BY' => 'cl_sortkey' ); + if( $limit ) $options[ 'LIMIT' ] = $limit; + if( $offset !== '' ) $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset ); + + return TitleArray::newFromResult( + $dbr->select( + array( 'page', 'categorylinks' ), + array( 'page_id', 'page_namespace','page_title', 'page_len', + 'page_is_redirect', 'page_latest' ), + $conds, + __METHOD__, + $options + ) + ); + } + /** Generic accessor */ private function getX( $key ) { if( !$this->initialize() ) { @@ -228,7 +257,7 @@ class Category { } $cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY, 1, 'NULL' ); - $cond2 = $dbw->conditional( 'page_namespace='.NS_IMAGE, 1, 'NULL' ); + $cond2 = $dbw->conditional( 'page_namespace='.NS_FILE, 1, 'NULL' ); $result = $dbw->selectRow( array( 'categorylinks', 'page' ), array( 'COUNT(*) AS pages', diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 92e4e279..4ac24b5f 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -36,18 +36,18 @@ class CategoryPage extends Article { $this->closeShowCategory(); } } - + /** - * This page should not be cached if 'from' or 'until' has been used - * @return bool + * Don't return a 404 for categories in use. */ - function isFileCacheable() { - global $wgRequest; - - return ( ! Article::isFileCacheable() - || $wgRequest->getVal( 'from' ) - || $wgRequest->getVal( 'until' ) - ) ? false : true; + function hasViewableContent() { + if( parent::hasViewableContent() ) { + return true; + } else { + $cat = Category::newFromTitle( $this->mTitle ); + return $cat->getId() != 0; + } + } function openShowCategory() { @@ -85,8 +85,6 @@ class CategoryViewer { /** * 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 */ @@ -144,7 +142,7 @@ class CategoryViewer { /** * Add a subcategory to the internal lists, using a title object - * @deprectated kept for compatibility, please use addSubcategoryObject instead + * @deprecated kept for compatibility, please use addSubcategoryObject instead */ function addSubcategory( $title, $sortkey, $pageLength ) { global $wgContLang; @@ -225,14 +223,14 @@ class CategoryViewer { array( 'page', 'categorylinks', 'category' ), array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ), - array( $pageCondition, - 'cl_to' => $this->title->getDBkey() ), + array( $pageCondition, 'cl_to' => $this->title->getDBkey() ), __METHOD__, array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey', - 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ), - 'LIMIT' => $this->limit + 1 ), + 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ), + 'LIMIT' => $this->limit + 1 ), array( 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ), - 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) ) ); + 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) ) + ); $count = 0; $this->nextPage = null; @@ -249,7 +247,7 @@ class CategoryViewer { if( $title->getNamespace() == NS_CATEGORY ) { $cat = Category::newFromRow( $x, $title ); $this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len ); - } elseif( $this->showGallery && $title->getNamespace() == NS_IMAGE ) { + } elseif( $this->showGallery && $title->getNamespace() == NS_FILE ) { $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); } else { $this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); @@ -339,10 +337,10 @@ class CategoryViewer { * 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 + * @param $articles Array + * @param $articles_start_char Array + * @param $cutoff Int + * @return String * @private */ function formatList( $articles, $articles_start_char, $cutoff = 6 ) { @@ -359,9 +357,9 @@ class CategoryViewer { * 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 + * @param $articles Array + * @param $articles_start_char Array + * @return String * @private */ function columnList( $articles, $articles_start_char ) { @@ -418,9 +416,9 @@ class CategoryViewer { /** * Format a list of articles chunked by letter in a bullet list. - * @param array $articles - * @param array $articles_start_char - * @return string + * @param $articles Array + * @param $articles_start_char Array + * @return String * @private */ function shortList( $articles, $articles_start_char ) { @@ -440,12 +438,12 @@ class CategoryViewer { } /** - * @param Title $title - * @param string $first - * @param string $last - * @param int $limit - * @param array $query - additional query options to pass - * @return string + * @param $title Title object + * @param $first String + * @param $last String + * @param $limit Int + * @param $query Array: additional query options to pass + * @return String * @private */ function pagingLinks( $title, $first, $last, $limit, $query = array() ) { @@ -477,10 +475,10 @@ class CategoryViewer { * category-subcat-count-limited, category-file-count, * category-file-count-limited. * - * @param int $rescnt The number of items returned by our database query. - * @param int $dbcnt The number of items according to the category table. - * @param string $type 'subcat', 'article', or 'file' - * @return string A message giving the number of items, to output to HTML. + * @param $rescnt Int: The number of items returned by our database query. + * @param $dbcnt Int: The number of items according to the category table. + * @param $type String: 'subcat', 'article', or 'file' + * @return String: A message giving the number of items, to output to HTML. */ private function getCountMessage( $rescnt, $dbcnt, $type ) { global $wgLang; @@ -500,8 +498,12 @@ class CategoryViewer { # Case 1: seems sane. $totalcnt = $dbcnt; } elseif($totalrescnt < $this->limit && !$this->from && !$this->until){ - # Case 2: not sane, but salvageable. + # Case 2: not sane, but salvageable. Use the number of results. + # Since there are fewer than 200, we can also take this opportunity + # to refresh the incorrect category table entry -- which should be + # quick due to the small number of entries. $totalcnt = $rescnt; + $this->cat->refreshCounts(); } else { # Case 3: hopeless. Don't give a total count at all. return wfMsgExt("category-$type-count-limited", 'parse', diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index d28f2eeb..4413bd1a 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -86,9 +86,15 @@ class Categoryfinder { * 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 + * @param $path used to check for recursion loops * @return bool Does this match the conditions? */ - function check ( $id , &$conds ) { + function check ( $id , &$conds, $path=array() ) { + // Check for loops and stop! + if( in_array( $id, $path ) ) + return false; + $path[] = $id; + # Shortcut (runtime paranoia): No contitions=all matched if ( count ( $conds ) == 0 ) return true ; @@ -120,7 +126,7 @@ class Categoryfinder { # No sub-parent continue ; } - $done = $this->check ( $this->name2id[$pname] , $conds ) ; + $done = $this->check ( $this->name2id[$pname] , $conds, $path ); if ( $done OR count ( $conds ) == 0 ) { # Subparents have done it! return true ; diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php index 9bee1790..f3c3e429 100644 --- a/includes/ChangesFeed.php +++ b/includes/ChangesFeed.php @@ -12,14 +12,15 @@ class ChangesFeed { public function getFeedObject( $title, $description ) { global $wgSitename, $wgContLanguageCode, $wgFeedClasses, $wgTitle; $feedTitle = "$wgSitename - {$title} [$wgContLanguageCode]"; - + if( !isset($wgFeedClasses[$this->format] ) ) + return false; return new $wgFeedClasses[$this->format]( $feedTitle, htmlspecialchars( $description ), $wgTitle->getFullUrl() ); } public function execute( $feed, $rows, $limit = 0 , $hideminor = false, $lastmod = false ) { global $messageMemc, $wgFeedCacheTimeout; - global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode; + global $wgFeedClasses, $wgSitename, $wgContLanguageCode; if ( !FeedUtils::checkFeedOutput( $this->format ) ) { return; @@ -85,7 +86,7 @@ class ChangesFeed { } /** - * @todo document + * Generate the feed items given a row from the database. * @param $rows Database resource with recentchanges rows * @param $feed Feed object */ diff --git a/includes/ChangesList.php b/includes/ChangesList.php index 436f006e..a8f5fff0 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -3,10 +3,9 @@ /** * @todo document */ -class RCCacheEntry extends RecentChange -{ +class RCCacheEntry extends RecentChange { var $secureName, $link; - var $curlink , $difflink, $lastlink , $usertalklink , $versionlink ; + var $curlink , $difflink, $lastlink, $usertalklink, $versionlink; var $userlink, $timestamp, $watched; static function newFromParent( $rc ) { @@ -15,7 +14,7 @@ class RCCacheEntry extends RecentChange $rc2->mExtra = $rc->mExtra; return $rc2; } -} ; +} /** * Class to show various lists of changes: @@ -25,13 +24,13 @@ class RCCacheEntry extends RecentChange */ class ChangesList { # Called by history lists and recent changes - # + public $skin; /** * Changeslist contructor * @param Skin $skin */ - function __construct( &$skin ) { + public function __construct( &$skin ) { $this->skin =& $skin; $this->preCacheMessages(); } @@ -47,7 +46,8 @@ class ChangesList { $sk = $user->getSkin(); $list = NULL; if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) { - return $user->getOption( 'usenewrc' ) ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); + return $user->getOption( 'usenewrc' ) ? + new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); } else { return $list; } @@ -58,7 +58,6 @@ class ChangesList { * they are called often, we call them once and save them in $this->message */ private function preCacheMessages() { - // Precache various messages if( !isset( $this->message ) ) { foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '. 'blocklink history boteditletter semicolon-separator' ) as $msg ) { @@ -78,10 +77,10 @@ class ChangesList { * @return string */ protected 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 = $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; @@ -99,6 +98,30 @@ class ChangesList { $this->rclistOpen = false; return ''; } + + /** + * Show formatted char difference + * @param int $old bytes + * @param int $new bytes + * @returns string + */ + public static function showCharacterDifference( $old, $new ) { + global $wgRCChangedSizeThreshold, $wgLang; + $szdiff = $new - $old; + $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape'), $wgLang->formatNum($szdiff) ); + if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) { + $tag = 'strong'; + } else { + $tag = 'span'; + } + if( $szdiff === 0 ) { + return "<$tag class='mw-plusminus-null'>($formatedSize)</$tag>"; + } elseif( $szdiff > 0 ) { + return "<$tag class='mw-plusminus-pos'>(+$formatedSize)</$tag>"; + } else { + return "<$tag class='mw-plusminus-neg'>($formatedSize)</$tag>"; + } + } /** * Returns text for the end of RC @@ -116,21 +139,18 @@ class ChangesList { # Diff $s .= '(' . $this->message['diff'] . ') ('; # Hist - $s .= $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), $this->message['hist'], 'action=history' ) . - ') . . '; - + $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(), '' ) ); } - protected function insertDateHeader(&$s, $rc_timestamp) { + protected 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"; @@ -141,21 +161,19 @@ class ChangesList { } } - protected function insertLog(&$s, $title, $logtype) { + protected function insertLog( &$s, $title, $logtype ) { $logname = LogPage::logName( $logtype ); $s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')'; } - protected function insertDiffHist(&$s, &$rc, $unpatrolled) { + protected function insertDiffHist( &$s, &$rc, $unpatrolled ) { # Diff link - if( !$this->userCan($rc,Revision::DELETED_TEXT) ) { + if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { $diffLink = $this->message['diff']; - } else if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { + } else if( !$this->userCan($rc,Revision::DELETED_TEXT) ) { $diffLink = $this->message['diff']; } else { - $rcidparam = $unpatrolled - ? array( 'rcid' => $rc->mAttribs['rc_id'] ) - : array(); + $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'], @@ -165,7 +183,6 @@ class ChangesList { '', '', ' tabindex="'.$rc->counter.'"'); } $s .= '('.$diffLink.') ('; - # History link $s .= $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['hist'], wfArrayToCGI( array( @@ -174,39 +191,40 @@ class ChangesList { $s .= ') . . '; } - protected function insertArticleLink(&$s, &$rc, $unpatrolled, $watched) { - # Article link + protected function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) { + global $wgContLang; # 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'] - : ''; + $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) ? + 'rcid='.$rc->mAttribs['rc_id'] : ''; if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) { $articlelink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); $articlelink = '<span class="history-deleted">'.$articlelink.'</span>'; } else { $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); } - if( $watched ) + # Bolden pages watched by this user + if( $watched ) { $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>"; - global $wgContLang; + } + # RTL/LTR marker $articlelink .= $wgContLang->getDirMark(); - wfRunHooks('ChangesListInsertArticleLink', - array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched)); + wfRunHooks( 'ChangesListInsertArticleLink', + array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched) ); - $s .= ' '.$articlelink; + $s .= " $articlelink"; } - protected function insertTimestamp(&$s, $rc) { + protected function insertTimestamp( &$s, $rc ) { global $wgLang; - # Timestamp - $s .= $this->message['semicolon-separator'] . ' ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; + $s .= $this->message['semicolon-separator'] . + $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; } /** Insert links to user page, user talk page and eventually a blocking link */ - protected function insertUserRelatedLinks(&$s, &$rc) { - if ( $this->isDeleted($rc,Revision::DELETED_USER) ) { + public function insertUserRelatedLinks(&$s, &$rc) { + if( $this->isDeleted($rc,Revision::DELETED_USER) ) { $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>'; } else { $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); @@ -216,13 +234,11 @@ class ChangesList { /** insert a formatted action */ protected function insertAction(&$s, &$rc) { - # Add action if( $rc->mAttribs['rc_type'] == RC_LOG ) { - // log action - if ( $this->isDeleted($rc,LogPage::DELETED_ACTION) ) { + if( $this->isDeleted($rc,LogPage::DELETED_ACTION) ) { $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; } else { - $s .= ' ' . LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'], + $s .= ' '.LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'], $rc->getTitle(), $this->skin, LogPage::extractParams($rc->mAttribs['rc_params']), true, true ); } } @@ -230,10 +246,8 @@ class ChangesList { /** insert a formatted comment */ protected function insertComment(&$s, &$rc) { - # Add comment if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { - // log comment - if ( $this->isDeleted($rc,Revision::DELETED_COMMENT) ) { + if( $this->isDeleted($rc,Revision::DELETED_COMMENT) ) { $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>'; } else { $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); @@ -256,8 +270,8 @@ class ChangesList { protected function numberofWatchingusers( $count ) { global $wgLang; static $cache = array(); - if ( $count > 0 ) { - if ( !isset( $cache[$count] ) ) { + if( $count > 0 ) { + if( !isset( $cache[$count] ) ) { $cache[$count] = wfMsgExt('number_of_watching_users_RCview', array('parsemag', 'escape'), $wgLang->formatNum($count)); } @@ -290,12 +304,20 @@ class ChangesList { $permission = ( $rc->mAttribs['rc_deleted'] & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED ? 'suppressrevision' : 'deleterevision'; - wfDebug( "Checking for $permission due to $field match on $rc->mAttribs['rc_deleted']\n" ); + wfDebug( "Checking for $permission due to $field match on {$rc->mAttribs['rc_deleted']}\n" ); return $wgUser->isAllowed( $permission ); } else { return true; } } + + protected function maybeWatchedLink( $link, $watched=false ) { + if( $watched ) { + return '<strong class="mw-watched">' . $link . '</strong>'; + } else { + return '<span class="mw-rc-unwatched">' . $link . '</span>'; + } + } } @@ -308,55 +330,43 @@ class OldChangesList extends ChangesList { */ public function recentChangesLine( &$rc, $watched = false ) { global $wgContLang, $wgRCShowChangedSize, $wgUser; - - $fname = 'ChangesList::recentChangesLineOld'; - wfProfileIn( $fname ); - - # Extract DB fields into local scope - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. - extract( $rc->mAttribs ); - + wfProfileIn( __METHOD__ ); # Should patrol-related stuff be shown? - $unpatrolled = $wgUser->useRCPatrol() && $rc_patrolled == 0; + $unpatrolled = $wgUser->useRCPatrol() && !$rc->mAttribs['rc_patrolled']; - $this->insertDateHeader($s,$rc_timestamp); - - $s .= '<li>'; + $dateheader = ''; // $s now contains only <li>...</li>, for hooks' convenience. + $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); + $s = ''; // Moved pages - if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( $rc->mAttribs['rc_type'] == RC_MOVE || $rc->mAttribs['rc_type'] == RC_MOVE_OVER_REDIRECT ) { $this->insertMove( $s, $rc ); // Log entries - } elseif( $rc_log_type ) { - $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL ); - $this->insertLog( $s, $logtitle, $rc_log_type ); + } elseif( $rc->mAttribs['rc_log_type'] ) { + $logtitle = Title::newFromText( 'Log/'.$rc->mAttribs['rc_log_type'], NS_SPECIAL ); + $this->insertLog( $s, $logtitle, $rc->mAttribs['rc_log_type'] ); // Log entries (old format) or log targets, and special pages - } elseif( $rc_namespace == NS_SPECIAL ) { - list( $specialName, $specialSubpage ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); - if ( $specialName == 'Log' ) { - $this->insertLog( $s, $rc->getTitle(), $specialSubpage ); - } else { - wfDebug( "Unexpected special page in recentchanges\n" ); + } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) { + list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $rc->mAttribs['rc_title'] ); + if( $name == 'Log' ) { + $this->insertLog( $s, $rc->getTitle(), $subpage ); } // Regular entries } else { - wfProfileIn($fname.'-page'); - - $this->insertDiffHist($s, $rc, $unpatrolled); - + $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'); + $s .= $this->recentChangesFlags( $rc->mAttribs['rc_new'], $rc->mAttribs['rc_minor'], + $unpatrolled, '', $rc->mAttribs['rc_bot'] ); + $this->insertArticleLink( $s, $rc, $unpatrolled, $watched ); } - - wfProfileIn( $fname.'-rest' ); - - $this->insertTimestamp($s,$rc); - + # Edit/log timestamp + $this->insertTimestamp( $s, $rc ); + # Bytes added or removed if( $wgRCShowChangedSize ) { - $s .= ( $rc->getCharacterDifference() == '' ? '' : $rc->getCharacterDifference() . ' . . ' ); + $cd = $rc->getCharacterDifference(); + if( $cd != '' ) { + $s .= "$cd . . "; + } } # User tool links $this->insertUserRelatedLinks($s,$rc); @@ -364,29 +374,45 @@ class OldChangesList extends ChangesList { $this->insertAction($s, $rc); # Edit or log comment $this->insertComment($s, $rc); - # Mark revision as deleted if so - if ( !$rc_log_type && $this->isDeleted($rc,Revision::DELETED_TEXT) ) + if( !$rc->mAttribs['rc_log_type'] && $this->isDeleted($rc,Revision::DELETED_TEXT) ) { $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; - if($rc->numberofWatchingusers > 0) { - $s .= ' ' . wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rc->numberofWatchingusers)); + } + # How many users watch this page + if( $rc->numberofWatchingusers > 0 ) { + $s .= ' ' . wfMsg( 'number_of_watching_users_RCview', + $wgContLang->formatNum($rc->numberofWatchingusers) ); } - $s .= "</li>\n"; - - wfProfileOut( $fname.'-rest' ); + wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) ); - wfProfileOut( $fname ); - return $s; + wfProfileOut( __METHOD__ ); + return "$dateheader<li>$s</li>\n"; } } /** - * Generate a list of changes using an Enhanced system (use javascript). + * Generate a list of changes using an Enhanced system (uses javascript). */ class EnhancedChangesList extends ChangesList { /** + * Add the JavaScript file for enhanced changeslist + * @ return string + */ + public function beginRecentChangesList() { + global $wgStylePath, $wgJsMimeType, $wgStyleVersion; + $this->rc_cache = array(); + $this->rcMoveIndex = 0; + $this->rcCacheIndex = 0; + $this->lastdate = ''; + $this->rclistOpen = false; + $script = Xml::tags( 'script', array( + 'type' => $wgJsMimeType, + 'src' => $wgStylePath . "/common/enhancedchanges.js?$wgStyleVersion" ), '' ); + return $script; + } + /** * Format a line for enhanced recentchange (aka with javascript and block of lines). */ public function recentChangesLine( &$baseRC, $watched = false ) { @@ -396,12 +422,13 @@ class EnhancedChangesList extends ChangesList { $rc = RCCacheEntry::newFromParent( $baseRC ); # Extract fields from DB into the function scope (rc_xxxx variables) - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // FIXME: Would be good to replace this extract() call with something + // that explicitly initializes 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); + $date = $wgLang->date( $rc_timestamp, true ); $ret = ''; if( $date != $this->lastdate ) { # Process current cache @@ -425,17 +452,6 @@ class EnhancedChangesList extends ChangesList { $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; $clink = wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); - // Log entries (old format) and special pages - } elseif( $rc_namespace == NS_SPECIAL ) { - list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); - if ( $specialName == 'Log' ) { - # Log updates, etc - $logname = LogPage::logName( $logtype ); - $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; - } else { - wfDebug( "Unexpected special page in recentchanges\n" ); - $clink = ''; - } // New unpatrolled pages } else if( $rc->unpatrolled && $rc_type == RC_NEW ) { $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" ); @@ -443,11 +459,23 @@ class EnhancedChangesList extends ChangesList { } else if( $rc_type == RC_LOG ) { if( $rc_log_type ) { $logtitle = SpecialPage::getTitleFor( 'Log', $rc_log_type ); - $clink = '(' . $this->skin->makeKnownLinkObj( $logtitle, LogPage::logName($rc_log_type) ) . ')'; + $clink = '(' . $this->skin->makeKnownLinkObj( $logtitle, + LogPage::logName($rc_log_type) ) . ')'; } else { $clink = $this->skin->makeLinkObj( $rc->getTitle(), '' ); } $watched = false; + // Log entries (old format) and special pages + } elseif( $rc_namespace == NS_SPECIAL ) { + list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); + if ( $specialName == 'Log' ) { + # Log updates, etc + $logname = LogPage::logName( $logtype ); + $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; + } else { + wfDebug( "Unexpected special page in recentchanges\n" ); + $clink = ''; + } // Edits } else { $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ); @@ -473,7 +501,8 @@ class EnhancedChangesList extends ChangesList { $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 ); + $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), + $this->message['cur'], $querycur, '' ,'', $aprops ); # Make "diff" an "cur" links if( !$showdifflinks ) { @@ -485,7 +514,8 @@ class EnhancedChangesList extends ChangesList { } $diffLink = $this->message['diff']; } else { - $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], $querydiff, '' ,'', $aprops ); + $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], + $querydiff, '' ,'', $aprops ); } # Make "last" link @@ -545,7 +575,7 @@ class EnhancedChangesList extends ChangesList { $curId = $currentRevision = 0; # Some catalyst variables... $namehidden = true; - $alllogs = true; + $allLogs = true; foreach( $block as $rcObj ) { $oldid = $rcObj->mAttribs['rc_last_oldid']; if( $rcObj->mAttribs['rc_new'] ) { @@ -564,7 +594,7 @@ class EnhancedChangesList extends ChangesList { $unpatrolled = true; } if( $rcObj->mAttribs['rc_type'] != RC_LOG ) { - $alllogs = false; + $allLogs = false; } # Get the latest entry with a page_id and oldid # since logs may not have these. @@ -587,20 +617,24 @@ class EnhancedChangesList extends ChangesList { $text = $userlink; $text .= $wgContLang->getDirMark(); if( $count > 1 ) { - $text .= ' ('.$count.'×)'; + $text .= ' (' . $wgLang->formatNum( $count ) . '×)'; } array_push( $users, $text ); } - $users = ' <span class="changedby">[' . implode( $this->message['semicolon-separator'] . ' ', $users ) . ']</span>'; + $users = ' <span class="changedby">[' . + implode( $this->message['semicolon-separator'], $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>'; + # ID for JS visibility toggle + $jsid = $this->rcCacheIndex; + # onclick handler to toggle hidden/expanded + $toggleLink = "onclick='toggleVisibility($jsid); return false'"; + # Title for <a> tags + $expandTitle = htmlspecialchars( wfMsg('rc-enhanced-expand') ); + $closeTitle = htmlspecialchars( wfMsg('rc-enhanced-hide') ); + + $tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>"; + $tl .= "<span id='mw-rc-closearrow-$jsid' class='mw-changeslist-hidden' style='display:none'><a href='#' $toggleLink title='$closeTitle'>" . $this->downArrow() . "</a></span>"; $r .= '<td valign="top" style="white-space: nowrap"><tt>'.$tl.' '; # Main line @@ -612,8 +646,10 @@ class EnhancedChangesList extends ChangesList { # Article link if( $namehidden ) { $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; - } else { + } else if( $allLogs ) { $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); + } else { + $this->insertArticleLink( $r, $block[0], $block[0]->unpatrolled, $block[0]->watched ); } $r .= $wgContLang->getDirMark(); @@ -627,7 +663,7 @@ class EnhancedChangesList extends ChangesList { } # Total change link $r .= ' '; - if( !$alllogs ) { + if( !$allLogs ) { $r .= '('; if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { $r .= $nchanges[$n]; @@ -637,11 +673,21 @@ class EnhancedChangesList extends ChangesList { $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), $nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" ); } - $r .= ') . . '; } + # History + if( $allLogs ) { + // don't show history link for logs + } else if( $namehidden || !$block[0]->getTitle()->exists() ) { + $r .= $this->message['semicolon-separator'] . $this->message['hist'] . ')'; + } else { + $r .= $this->message['semicolon-separator'] . $this->skin->makeKnownLinkObj( $block[0]->getTitle(), + $this->message['hist'], $curIdEq . '&action=history' ) . ')'; + } + $r .= ' . . '; + # Character difference (does not apply if only log items) - if( $wgRCShowChangedSize && !$alllogs ) { + if( $wgRCShowChangedSize && !$allLogs ) { $last = 0; $first = count($block) - 1; # Some events (like logs) have an "empty" size, so we need to skip those... @@ -662,26 +708,18 @@ class EnhancedChangesList extends ChangesList { } } - # History - if( $alllogs ) { - // don't show history link for logs - } else if( $namehidden || !$block[0]->getTitle()->exists() ) { - $r .= '(' . $this->message['history'] . ')'; - } else { - $r .= '(' . $this->skin->makeKnownLinkObj( $block[0]->getTitle(), - $this->message['history'], $curIdEq.'&action=history' ) . ')'; - } - $r .= $users; $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers); $r .= "</td></tr></table>\n"; # Sub-entries - $r .= '<div id="'.$rci.'" style="display:none;"><table cellpadding="0" cellspacing="0" border="0" style="background: none">'; + $r .= '<div id="mw-rc-subentries-'.$jsid.'" class="mw-changeslist-hidden">'; + $r .= '<table cellpadding="0" cellspacing="0" border="0" style="background: none">'; foreach( $block as $rcObj ) { - # Get rc_xxxx variables - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + # Extract fields from DB into the function scope (rc_xxxx variables) + // FIXME: Would be good to replace this extract() call with something + // that explicitly initializes variables. extract( $rcObj->mAttribs ); #$r .= '<tr><td valign="top">'.$this->spacerArrow(); @@ -701,9 +739,10 @@ class EnhancedChangesList extends ChangesList { } else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { $link = '<span class="history-deleted"><tt>'.$rcObj->timestamp.'</tt></span> '; } else { - $rcIdEq = ($rcObj->unpatrolled && $rc_type == RC_NEW) ? '&rcid='.$rcObj->mAttribs['rc_id'] : ''; - - $link = '<tt>'.$this->skin->makeKnownLinkObj( $rcObj->getTitle(), $rcObj->timestamp, $curIdEq.'&'.$o.$rcIdEq ).'</tt>'; + $rcIdEq = ($rcObj->unpatrolled && $rc_type == RC_NEW) ? + '&rcid='.$rcObj->mAttribs['rc_id'] : ''; + $link = '<tt>'.$this->skin->makeKnownLinkObj( $rcObj->getTitle(), + $rcObj->timestamp, $curIdEq.'&'.$o.$rcIdEq ).'</tt>'; if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) $link = '<span class="history-deleted">'.$link.'</span> '; } @@ -712,7 +751,7 @@ class EnhancedChangesList extends ChangesList { if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) { $r .= ' ('; $r .= $rcObj->curlink; - $r .= $this->message['semicolon-separator'] . ' '; + $r .= $this->message['semicolon-separator']; $r .= $rcObj->lastlink; $r .= ')'; } @@ -742,26 +781,19 @@ class EnhancedChangesList extends ChangesList { return $r; } - protected function maybeWatchedLink( $link, $watched=false ) { - if( $watched ) { - // FIXME: css style might be more appropriate - return '<strong class="mw-watched">' . $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 + * @param string $title text * @return string HTML <img> tag */ - protected function arrow( $dir, $alt='' ) { + protected function arrow( $dir, $alt='', $title='' ) { global $wgStylePath; $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' ); $encAlt = htmlspecialchars( $alt ); - return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" />"; + $encTitle = htmlspecialchars( $title ); + return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" title=\"$encTitle\" />"; } /** @@ -772,7 +804,7 @@ class EnhancedChangesList extends ChangesList { protected function sideArrow() { global $wgContLang; $dir = $wgContLang->isRTL() ? 'l' : 'r'; - return $this->arrow( $dir, '+' ); + return $this->arrow( $dir, '+', wfMsg('rc-enhanced-expand') ); } /** @@ -781,7 +813,7 @@ class EnhancedChangesList extends ChangesList { * @return string HTML <img> tag */ protected function downArrow() { - return $this->arrow( 'd', '-' ); + return $this->arrow( 'd', '-', wfMsg('rc-enhanced-hide') ); } /** @@ -789,7 +821,7 @@ class EnhancedChangesList extends ChangesList { * @return string HTML <img> tag */ protected function spacerArrow() { - return $this->arrow( '', ' ' ); + return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space } /** @@ -806,16 +838,14 @@ class EnhancedChangesList extends ChangesList { */ protected function recentChangesBlockLine( $rcObj ) { global $wgContLang, $wgRCShowChangedSize; - - # Get rc_xxxx variables - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + # Extract fields from DB into the function scope (rc_xxxx variables) + // FIXME: Would be good to replace this extract() call with something + // that explicitly initializes variables. extract( $rcObj->mAttribs ); - $curIdEq = 'curid='.$rc_cur_id; + $curIdEq = "curid={$rc_cur_id}"; $r = '<table cellspacing="0" cellpadding="0" border="0" style="background: none"><tr>'; - $r .= '<td valign="top" style="white-space: nowrap"><tt>' . $this->spacerArrow() . ' '; - # Flag and Timestamp if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $r .= ' '; // 4 flags -> 4 spaces @@ -823,33 +853,27 @@ class EnhancedChangesList extends ChangesList { $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); } $r .= ' '.$rcObj->timestamp.' </tt></td><td>'; - # Article or log link if( $rc_log_type ) { $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL ); $logname = LogPage::logName( $rc_log_type ); $r .= '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')'; - } else if( !$this->userCan($rcObj,Revision::DELETED_TEXT) ) { - $r .= '<span class="history-deleted">' . $rcObj->link . '</span>'; } else { - $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched ); + $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched ); } - # Diff and hist links if ( $rc_type != RC_LOG ) { - $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'] . ' '; - $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ')'; + $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator']; + $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), + $curIdEq.'&action=history' ) . ')'; } $r .= ' . . '; - # Character diff - if( $wgRCShowChangedSize ) { - $r .= ( $rcObj->getCharacterDifference() == '' ? '' : ' ' . $rcObj->getCharacterDifference() . ' . . ' ) ; + if( $wgRCShowChangedSize && ($cd = $rcObj->getCharacterDifference()) ) { + $r .= "$cd . . "; } - # User/talk $r .= ' '.$rcObj->userlink . $rcObj->usertalklink; - # Log action (if any) if( $rc_log_type ) { if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) { @@ -859,7 +883,6 @@ class EnhancedChangesList extends ChangesList { $this->skin, LogPage::extractParams($rc_params), true, true ); } } - # Edit or log comment if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { // log comment @@ -869,7 +892,6 @@ class EnhancedChangesList extends ChangesList { $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); } } - # Show how many people are watching this if enabled $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers); @@ -893,7 +915,6 @@ class EnhancedChangesList extends ChangesList { $blockOut .= $this->recentChangesBlockGroup( $block ); } } - return '<div>'.$blockOut.'</div>'; } diff --git a/includes/Credits.php b/includes/Credits.php index 6326e3a2..ae9377f2 100644 --- a/includes/Credits.php +++ b/includes/Credits.php @@ -20,167 +20,187 @@ * @author <evan@wikitravel.org> */ -/** - * 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); +class Credits { + + /** + * This is largely cadged from PageHistory::history + * @param $article Article object + */ + public static function showPage( Article $article ) { + global $wgOut; + + wfProfileIn( __METHOD__ ); + + $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 = self::getCredits($article, -1 ); } + + $wgOut->addHTML( $s ); + + wfProfileOut( __METHOD__ ); } - 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)); + /** + * Get a list of contributors of $article + * @param $article Article object + * @param $cnt Int: maximum list of contributors to show + * @param $showIfMax Bool: whether to contributors if there more than $cnt + * @return String: html + */ + public static function getCredits($article, $cnt, $showIfMax=true) { + wfProfileIn( __METHOD__ ); + $s = ''; + + if( isset( $cnt ) && $cnt != 0 ){ + $s = self::getAuthor( $article ); + if ($cnt > 1 || $cnt < 0) { + $s .= ' ' . self::getContributors( $article, $cnt - 1, $showIfMax ); + } } - } - $timestamp = $article->getTimestamp(); - if ($timestamp) { - $d = $wgLang->date($article->getTimestamp(), true); - $t = $wgLang->time($article->getTimestamp(), true); - } else { - $d = ''; - $t = ''; + wfProfileOut( __METHOD__ ); + return $s; } - return wfMsg('lastmodifiedatby', $d, $t, $author_credit); -} - -/** - * - */ -function getContributorCredits($article, $cnt, $showIfMax) { - - global $wgLang, $wgAllowRealName; - $contributors = $article->getContributors(); + /** + * Get the last author with the last modification time + * @param $article Article object + */ + protected static function getAuthor( Article $article ){ + global $wgLang, $wgAllowRealName; - $others_link = ''; + $user = User::newFromId( $article->getUser() ); - # Hmm... too many to fit! - - if ($cnt > 0 && count($contributors) > $cnt) { - $others_link = creditOthersLink($article); - if (!$showIfMax) { - return wfMsg('othercontribs', $others_link); + $timestamp = $article->getTimestamp(); + if( $timestamp ){ + $d = $wgLang->date( $article->getTimestamp(), true ); + $t = $wgLang->time( $article->getTimestamp(), true ); } else { - $contributors = array_slice($contributors, 0, $cnt); + $d = ''; + $t = ''; } + return wfMsg( 'lastmodifiedatby', $d, $t, self::userLink( $user ) ); } - $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]); + /** + * Get a list of contributors of $article + * @param $article Article object + * @param $cnt Int: maximum list of contributors to show + * @param $showIfMax Bool: whether to contributors if there more than $cnt + * @return String: html + */ + protected static function getContributors( Article $article, $cnt, $showIfMax ) { + global $wgLang, $wgAllowRealName; + + $contributors = $article->getContributors(); + + $others_link = ''; + + # Hmm... too many to fit! + if( $cnt > 0 && $contributors->count() > $cnt ){ + $others_link = self::othersLink( $article ); + if( !$showIfMax ) + return wfMsg( 'othercontribs', $others_link ); + } + + $real_names = array(); + $user_names = array(); + $anon = 0; + + # Sift for real versus user names + foreach( $contributors as $user ) { + $cnt--; + if( $user->isLoggedIn() ){ + $link = self::link( $user ); + if( $wgAllowRealName && $user->getRealName() ) + $real_names[] = $link; + else + $user_names[] = $link; } else { - $user_names[] = creditLink($user_parts[1]); + $anon++; + } + if( $cnt == 0 ) break; + } + + # Two strings: real names, and user names + $real = $wgLang->listToText( $real_names ); + $user = $wgLang->listToText( $user_names ); + if( $anon ) + $anon = wfMsgExt( 'anonymous', array( 'parseinline' ), $anon ); + + # "ThisSite user(s) A, B and C" + if( !empty( $user ) ){ + $user = wfMsgExt( 'siteusers', array( 'parsemag' ), $user, count( $user_names ) ); + } + + # 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 ); } - } 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" + # Make the list into text... + $creds = $wgLang->listToText( $fulllist ); - if (!empty($user)) { - $user = wfMsg('siteusers', $user); + # "Based on work by ..." + return empty( $creds ) ? '' : wfMsg( 'othercontribs', $creds ); } - # This is the big list, all mooshed together. We sift for blank strings - - $fulllist = array(); + /** + * Get a link to $user_name page + * @param $user User object + * @return String: html + */ + protected static function link( User $user ) { + global $wgUser, $wgAllowRealName; + if( $wgAllowRealName ) + $real = $user->getRealName(); + else + $real = false; + + $skin = $wgUser->getSkin(); + $page = $user->getUserPage(); + + return $skin->link( $page, htmlspecialchars( $real ? $real : $user->getName() ) ); + } - foreach (array($real, $user, $anon, $others_link) as $s) { - if (!empty($s)) { - array_push($fulllist, $s); + /** + * Get a link to $user_name page + * @param $user_name String: user name + * @param $linkText String: optional display + * @return String: html + */ + protected static function userLink( User $user ) { + global $wgUser, $wgAllowRealName; + if( $user->isAnon() ){ + return wfMsgExt( 'anonymous', array( 'parseinline' ), 1 ); + } else { + $link = self::link( $user ); + if( $wgAllowRealName && $user->getRealName() ) + return $link; + else + return wfMsgExt( 'siteuser', array( 'parseinline', 'replaceafter' ), $link ); } } - # 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'); -} + /** + * Get a link to action=credits of $article page + * @param $article Article object + * @return String: html + */ + protected static function othersLink( Article $article ) { + global $wgUser; + $skin = $wgUser->getSkin(); + return $skin->link( $article->getTitle(), wfMsgHtml( 'others' ), array(), array( 'action' => 'credits' ), array( 'known' ) ); + } +}
\ No newline at end of file diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php index ad6e7f6c..52e9a8c8 100644 --- a/includes/DatabaseFunctions.php +++ b/includes/DatabaseFunctions.php @@ -154,6 +154,7 @@ function wfFieldName( $res, $n, $dbi = DB_LAST ) /** * @todo document function + * @see Database::insertId() */ function wfInsertId( $dbi = DB_LAST ) { $db = wfGetDB( $dbi ); @@ -166,6 +167,7 @@ function wfInsertId( $dbi = DB_LAST ) { /** * @todo document function + * @see Database::dataSeek() */ function wfDataSeek( $res, $row, $dbi = DB_LAST ) { $db = wfGetDB( $dbi ); @@ -177,7 +179,8 @@ function wfDataSeek( $res, $row, $dbi = DB_LAST ) { } /** - * @todo document function + * Get the last error number + * @see Database::lastErrno() */ function wfLastErrno( $dbi = DB_LAST ) { $db = wfGetDB( $dbi ); @@ -189,7 +192,8 @@ function wfLastErrno( $dbi = DB_LAST ) { } /** - * @todo document function + * Get the last error + * @see Database::lastError() */ function wfLastError( $dbi = DB_LAST ) { $db = wfGetDB( $dbi ); @@ -201,7 +205,8 @@ function wfLastError( $dbi = DB_LAST ) { } /** - * @todo document function + * Get the number of affected rows + * @see Database::affectedRows() */ function wfAffectedRows( $dbi = DB_LAST ) { $db = wfGetDB( $dbi ); @@ -213,7 +218,8 @@ function wfAffectedRows( $dbi = DB_LAST ) { } /** - * @todo document function + * Get the last query ran + * @see Database::lastQuery */ function wfLastDBquery( $dbi = DB_LAST ) { $db = wfGetDB( $dbi ); @@ -245,8 +251,8 @@ function wfSetSQL( $table, $var, $value, $cond, $dbi = DB_MASTER ) /** + * Simple select wrapper, return one field * @see Database::selectField() - * @todo document function * @param $table * @param $var * @param $cond Default '' @@ -263,8 +269,8 @@ function wfGetSQL( $table, $var, $cond='', $dbi = DB_LAST ) } /** + * Does a given field exist on the specified table? * @see Database::fieldExists() - * @todo document function * @param $table * @param $field * @param $dbi Default DB_LAST @@ -280,8 +286,8 @@ function wfFieldExists( $table, $field, $dbi = DB_LAST ) { } /** + * Does the requested index exist on the specified table? * @see Database::indexExists() - * @todo document function * @param $table String * @param $index * @param $dbi Default DB_LAST @@ -354,7 +360,8 @@ function wfUpdateArray( $table, $values, $conds, $fname = 'wfUpdateArray', $dbi } /** - * @todo document function + * Get fully usable table name + * @see Database::tableName() */ function wfTableName( $name, $dbi = DB_LAST ) { $db = wfGetDB( $dbi ); @@ -367,6 +374,7 @@ function wfTableName( $name, $dbi = DB_LAST ) { /** * @todo document function + * @see Database::strencode() */ function wfStrencode( $s, $dbi = DB_LAST ) { $db = wfGetDB( $dbi ); @@ -379,6 +387,7 @@ function wfStrencode( $s, $dbi = DB_LAST ) { /** * @todo document function + * @see Database::nextSequenceValue() */ function wfNextSequenceValue( $seqName, $dbi = DB_MASTER ) { $db = wfGetDB( $dbi ); @@ -391,6 +400,7 @@ function wfNextSequenceValue( $seqName, $dbi = DB_MASTER ) { /** * @todo document function + * @see Database::useIndexClause() */ function wfUseIndexClause( $index, $dbi = DB_SLAVE ) { $db = wfGetDB( $dbi ); diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index aaf934f5..ed68fe7a 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -27,11 +27,13 @@ if( !defined( 'MEDIAWIKI' ) ) { * Create a site configuration object * Not used for much in a default install */ -require_once( "$IP/includes/SiteConfiguration.php" ); -$wgConf = new SiteConfiguration; +if ( !defined( 'MW_PHP4' ) ) { + require_once( "$IP/includes/SiteConfiguration.php" ); + $wgConf = new SiteConfiguration; +} /** MediaWiki version number */ -$wgVersion = '1.13.4'; +$wgVersion = '1.14.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; @@ -539,10 +541,10 @@ $wgSMTP = false; */ /** database host name or ip address */ $wgDBserver = 'localhost'; -/** database port number */ -$wgDBport = ''; +/** database port number (for PostgreSQL) */ +$wgDBport = 5432; /** name of the database */ -$wgDBname = 'wikidb'; +$wgDBname = 'my_wiki'; /** */ $wgDBconnection = ''; /** Database username */ @@ -572,6 +574,12 @@ $wgDBts2schema = 'public'; /** To override default SQLite data directory ($docroot/../data) */ $wgSQLiteDataDir = ''; +/** Default directory mode for SQLite data directory on creation. + * Note that this is different from the default directory mode used + * elsewhere. + */ +$wgSQLiteDataDirMode = 0700; + /** * Make all database connections secretly go to localhost. Fool the load balancer * thinking there is an arbitrarily large cluster of servers to connect to. @@ -672,14 +680,6 @@ $wgDBClusterTimeout = 10; */ $wgDBAvgStatusPoll = 2000; -/** - * 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. @@ -745,12 +745,6 @@ $wgLocalMessageCache = false; */ $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 */ @@ -844,7 +838,6 @@ $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; @@ -860,6 +853,14 @@ $wgMsgCacheExpiry = 86400; $wgMaxMsgCacheEntrySize = 10000; /** + * If true, serialized versions of the messages arrays will be + * read from the 'serialized' subdirectory if they are present. + * Set to false to always use the Messages files, regardless of + * whether they are up to date or not. + */ +$wgEnableSerializedMessages = true; + +/** * Set to false if you are thorough system admin who always remembers to keep * serialized files up to date to save few mtime calls. */ @@ -868,6 +869,9 @@ $wgCheckSerialized = true; /** Whether to enable language variant conversion. */ $wgDisableLangConversion = false; +/** Whether to enable language variant conversion for links. */ +$wgDisableTitleConversion = false; + /** Default variant code, if false, the default will be the language code */ $wgDefaultLanguageVariant = false; @@ -947,26 +951,68 @@ $wgMaxPPNodeCount = 1000000; # A complexity limit on template expansion $wgMaxTemplateDepth = 40; $wgMaxPPExpandDepth = 40; +/** + * If true, removes (substitutes) templates in "~~~~" signatures. + */ +$wgCleanSignatures = true; + $wgExtraSubtitle = ''; $wgSiteSupportPage = ''; # A page where you users can receive donations +/** + * Set this to a string to put the wiki into read-only mode. The text will be + * used as an explanation to users. + * + * This prevents most write operations via the web interface. Cache updates may + * still be possible. To prevent database writes completely, use the read_only + * option in MySQL. + */ +$wgReadOnly = null; + /*** - * If this lock file exists, the wiki will be forced into read-only mode. + * If this lock file exists (size > 0), the wiki will be forced into read-only mode. * Its contents will be shown to users as part of the read-only warning * message. */ $wgReadOnlyFile = false; ///< defaults to "{$wgUploadDirectory}/lock_yBgMBwiR"; /** + * Filename for debug logging. * The debug log file should be not be publicly accessible if it is used, as it - * may contain private data. */ + * may contain private data. + */ $wgDebugLogFile = ''; +/** + * Prefix for debug log lines + */ +$wgDebugLogPrefix = ''; + +/** + * If true, instead of redirecting, show a page with a link to the redirect + * destination. This allows for the inspection of PHP error messages, and easy + * resubmission of form data. For developer use only. + */ $wgDebugRedirects = false; -$wgDebugRawPage = false; # Avoid overlapping debug entries by leaving out CSS +/** + * If true, log debugging data from action=raw. + * This is normally false to avoid overlapping debug entries due to gen=css and + * gen=js requests. + */ +$wgDebugRawPage = false; + +/** + * Send debug data to an HTML comment in the output. + * + * This may occasionally be useful when supporting a non-technical end-user. It's + * more secure than exposing the debug log file to the web, since the output only + * contains private data for the current user. But it's not ideal for development + * use since data is lost on fatal errors and redirects. + */ $wgDebugComments = false; -$wgReadOnly = null; + +/** Does nothing. Obsolete? */ $wgLogQueries = false; /** @@ -1025,11 +1071,18 @@ $wgUseCategoryBrowser = false; * same options. * * This can provide a significant speedup for medium to large pages, - * so you probably want to keep it on. + * so you probably want to keep it on. Extensions that conflict with the + * parser cache should disable the cache on a per-page basis instead. */ $wgEnableParserCache = true; /** + * Append a configured value to the parser cache and the sitenotice key so + * that they can be kept separate for some class of activity. + */ +$wgRenderHashAppend = ''; + +/** * 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. @@ -1070,7 +1123,7 @@ $wgHitcounterUpdateFreq = 1; $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 +$wgBlockAllowsUTEdit = false; # Default setting for option on block form to allow self talkpage editing whilst blocked $wgSysopEmailBans = true; # Allow sysops to ban users from accessing Emailuser # Pages anonymous user may see as an array, e.g.: @@ -1110,40 +1163,42 @@ $wgEmailConfirmToEdit=false; $wgGroupPermissions = array(); // Implicit group for all visitors -$wgGroupPermissions['*' ]['createaccount'] = true; -$wgGroupPermissions['*' ]['read'] = true; -$wgGroupPermissions['*' ]['edit'] = true; -$wgGroupPermissions['*' ]['createpage'] = true; -$wgGroupPermissions['*' ]['createtalk'] = true; -$wgGroupPermissions['*' ]['writeapi'] = true; +$wgGroupPermissions['*']['createaccount'] = true; +$wgGroupPermissions['*']['read'] = true; +$wgGroupPermissions['*']['edit'] = true; +$wgGroupPermissions['*']['createpage'] = true; +$wgGroupPermissions['*']['createtalk'] = true; +$wgGroupPermissions['*']['writeapi'] = true; // Implicit group for all logged-in accounts -$wgGroupPermissions['user' ]['move'] = true; -$wgGroupPermissions['user' ]['move-subpages'] = true; -$wgGroupPermissions['user' ]['read'] = true; -$wgGroupPermissions['user' ]['edit'] = true; -$wgGroupPermissions['user' ]['createpage'] = true; -$wgGroupPermissions['user' ]['createtalk'] = true; -$wgGroupPermissions['user' ]['writeapi'] = true; -$wgGroupPermissions['user' ]['upload'] = true; -$wgGroupPermissions['user' ]['reupload'] = true; -$wgGroupPermissions['user' ]['reupload-shared'] = true; -$wgGroupPermissions['user' ]['minoredit'] = true; -$wgGroupPermissions['user' ]['purge'] = true; // can use ?action=purge without clicking "ok" +$wgGroupPermissions['user']['move'] = true; +$wgGroupPermissions['user']['move-subpages'] = true; +$wgGroupPermissions['user']['move-rootuserpages'] = true; // can move root userpages +//$wgGroupPermissions['user']['movefile'] = true; // Disabled for now due to possible bugs and security concerns +$wgGroupPermissions['user']['read'] = true; +$wgGroupPermissions['user']['edit'] = true; +$wgGroupPermissions['user']['createpage'] = true; +$wgGroupPermissions['user']['createtalk'] = true; +$wgGroupPermissions['user']['writeapi'] = true; +$wgGroupPermissions['user']['upload'] = true; +$wgGroupPermissions['user']['reupload'] = true; +$wgGroupPermissions['user']['reupload-shared'] = true; +$wgGroupPermissions['user']['minoredit'] = true; +$wgGroupPermissions['user']['purge'] = true; // can use ?action=purge without clicking "ok" // Implicit group for accounts that pass $wgAutoConfirmAge $wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true; // Users with bot privilege can have their edits hidden // from various log pages by default -$wgGroupPermissions['bot' ]['bot'] = true; -$wgGroupPermissions['bot' ]['autoconfirmed'] = true; -$wgGroupPermissions['bot' ]['nominornewtalk'] = true; -$wgGroupPermissions['bot' ]['autopatrol'] = true; -$wgGroupPermissions['bot' ]['suppressredirect'] = true; -$wgGroupPermissions['bot' ]['apihighlimits'] = true; -$wgGroupPermissions['bot' ]['writeapi'] = true; -#$wgGroupPermissions['bot' ]['editprotected'] = true; // can edit all protected pages without cascade protection enabled +$wgGroupPermissions['bot']['bot'] = true; +$wgGroupPermissions['bot']['autoconfirmed'] = true; +$wgGroupPermissions['bot']['nominornewtalk'] = true; +$wgGroupPermissions['bot']['autopatrol'] = true; +$wgGroupPermissions['bot']['suppressredirect'] = true; +$wgGroupPermissions['bot']['apihighlimits'] = true; +$wgGroupPermissions['bot']['writeapi'] = true; +#$wgGroupPermissions['bot']['editprotected'] = true; // can edit all protected pages without cascade protection enabled // Most extra permission abilities go to this group $wgGroupPermissions['sysop']['block'] = true; @@ -1158,6 +1213,7 @@ $wgGroupPermissions['sysop']['import'] = true; $wgGroupPermissions['sysop']['importupload'] = true; $wgGroupPermissions['sysop']['move'] = true; $wgGroupPermissions['sysop']['move-subpages'] = true; +$wgGroupPermissions['sysop']['move-rootuserpages'] = true; $wgGroupPermissions['sysop']['patrol'] = true; $wgGroupPermissions['sysop']['autopatrol'] = true; $wgGroupPermissions['sysop']['protect'] = true; @@ -1173,10 +1229,10 @@ $wgGroupPermissions['sysop']['upload_by_url'] = true; $wgGroupPermissions['sysop']['ipblock-exempt'] = true; $wgGroupPermissions['sysop']['blockemail'] = true; $wgGroupPermissions['sysop']['markbotedits'] = true; -$wgGroupPermissions['sysop']['suppressredirect'] = true; $wgGroupPermissions['sysop']['apihighlimits'] = true; $wgGroupPermissions['sysop']['browsearchive'] = true; $wgGroupPermissions['sysop']['noratelimit'] = true; +$wgGroupPermissions['sysop']['movefile'] = true; #$wgGroupPermissions['sysop']['mergehistory'] = true; // Permission to change users' group assignments @@ -1208,8 +1264,22 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true; $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' ); /** - * These are the groups that users are allowed to add to or remove from - * their own account via Special:Userrights. + * A map of group names that the user is in, to group names that those users + * are allowed to add or revoke. + * + * Setting the list of groups to add or revoke to true is equivalent to "any group". + * + * For example, to allow sysops to add themselves to the "bot" group: + * + * $wgGroupsAddToSelf = array( 'sysop' => array( 'bot' ) ); + * + * Implicit groups may be used for the source group, for instance: + * + * $wgGroupsRemoveFromSelf = array( '*' => true ); + * + * This allows users in the '*' group (i.e. any user) to remove themselves from + * any group that they happen to be in. + * */ $wgGroupsAddToSelf = array(); $wgGroupsRemoveFromSelf = array(); @@ -1237,9 +1307,10 @@ $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ); * Set the minimum permissions required to edit pages in each * namespace. If you list more than one permission, a user must * have all of them to edit pages in that namespace. + * + * Note: NS_MEDIAWIKI is implicitly restricted to editinterface. */ $wgNamespaceProtection = array(); -$wgNamespaceProtection[ NS_MEDIAWIKI ] = array( 'editinterface' ); /** * Pages in namespaces in this array can not be used as templates. @@ -1303,8 +1374,8 @@ $wgAutopromote = array( * // Sysops can disable other sysops in an emergency, and disable bots * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' ); */ -$wgAddGroups = $wgRemoveGroups = array(); - +$wgAddGroups = array(); +$wgRemoveGroups = array(); /** * A list of available rights, in addition to the ones defined by the core. @@ -1375,7 +1446,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '164'; +$wgStyleVersion = '195'; # Server-side caching: @@ -1439,6 +1510,9 @@ $wgEnotifMaxRecips = 500; # Send mails via the job queue. $wgEnotifUseJobQ = false; +# Use real name instead of username in e-mail "from" field +$wgEnotifUseRealName = false; + /** * Array of usernames who will be sent a notification email for every change which occurs on a wiki */ @@ -1456,14 +1530,17 @@ $wgRCShowChangedSize = true; * before and after the edit is below that value, the value will be * highlighted on the RC page. */ -$wgRCChangedSizeThreshold = -500; +$wgRCChangedSizeThreshold = 500; /** * Show "Updated (since my last visit)" marker in RC view, watchlist and history * view for watched pages with new changes */ $wgShowUpdatedMarker = true; -$wgCookieExpiration = 2592000; +/** + * Default cookie expiration time. Setting to 0 makes all cookies session-only. + */ +$wgCookieExpiration = 30*86400; /** 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 @@ -1523,6 +1600,9 @@ $wgHTCPMulticastTTL = 1; # $wgHTCPMulticastAddress = "224.0.0.85"; $wgHTCPMulticastAddress = false; +/** Should forwarded Private IPs be accepted? */ +$wgUsePrivateIPs = false; + # Cookie settings: # /** @@ -1572,14 +1652,26 @@ $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. + * You may also specify an array of strings to allow multiple sites * - * Example: + * Examples: * $wgAllowExternalImagesFrom = 'http://127.0.0.1/'; + * $wgAllowExternalImagesFrom = array( 'http://127.0.0.1/', 'http://example.com' ); */ $wgAllowExternalImagesFrom = ''; -/** Allows to move images and other media files. Experemintal, not sure if it always works */ -$wgAllowImageMoving = false; +/** If $wgAllowExternalImages is false, you can allow an on-wiki + * whitelist of regular expression fragments to match the image URL + * against. If the image matches one of the regular expression fragments, + * The image will be displayed. + * + * Set this to true to enable the on-wiki whitelist (MediaWiki:External image whitelist) + * Or false to disable it + */ +$wgEnableImageWhitelist = true; + +/** Allows to move images and other media files */ +$wgAllowImageMoving = true; /** Disable database-intensive features */ $wgMiserMode = false; @@ -1598,6 +1690,7 @@ $wgAllowSlowParserFunctions = false; */ $wgJobClasses = array( 'refreshLinks' => 'RefreshLinksJob', + 'refreshLinks2' => 'RefreshLinksJob2', 'htmlCacheUpdate' => 'HTMLCacheUpdateJob', 'html_cache_update' => 'HTMLCacheUpdateJob', // backwards-compatible 'sendMail' => 'EmaillingJob', @@ -1606,6 +1699,14 @@ $wgJobClasses = array( ); /** + * Additional functions to be performed with updateSpecialPages. + * Expensive Querypages are already updated. + */ +$wgSpecialPageCacheUpdates = array( + 'Statistics' => array('SiteStatsUpdate','cacheUpdate') +); + +/** * 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. @@ -1797,7 +1898,10 @@ $wgMimeTypeBlacklist= array( # Client-side hazards on Internet Explorer 'text/scriptlet', 'application/x-msdownload', # Windows metafile, client-side vulnerability on some systems - 'application/x-msmetafile' + 'application/x-msmetafile', + # A ZIP file may be a valid Java archive containing an applet which exploits the + # same-origin policy to steal cookies + 'application/zip', ); /** This is a flag to determine whether or not to check file extensions on upload. */ @@ -1823,7 +1927,7 @@ $wgNamespacesWithSubpages = array( NS_USER => true, NS_USER_TALK => true, NS_PROJECT_TALK => true, - NS_IMAGE_TALK => true, + NS_FILE_TALK => true, NS_MEDIAWIKI_TALK => true, NS_TEMPLATE_TALK => true, NS_HELP_TALK => true, @@ -1835,6 +1939,21 @@ $wgNamespacesToBeSearchedDefault = array( ); /** + * Additional namespaces to those in $wgNamespacesToBeSearchedDefault that + * will be added to default search for "project" page inclusive searches + * + * Same format as $wgNamespacesToBeSearchedDefault + */ +$wgNamespacesToBeSearchedProject = array( + NS_USER => true, + NS_PROJECT => true, + NS_HELP => true, + NS_CATEGORY => true, +); + +$wgUseOldSearchUI = true; // temp testing variable + +/** * Site notice shown at the top of each page * * This message can contain wiki text, and can also be set through the @@ -1883,6 +2002,12 @@ $wgSharpenParameter = '0x0.4'; /** Reduction in linear dimensions below which sharpening will be enabled */ $wgSharpenReductionThreshold = 0.85; +/** + * Temporary directory used for ImageMagick. The directory must exist. Leave + * this set to false to let ImageMagick decide for itself. + */ +$wgImageMagickTempDir = false; + /** * Use another resizing converter, e.g. GraphicMagick * %s will be replaced with the source path, %d with the destination @@ -1900,7 +2025,7 @@ $wgCustomConvertCommand = false; # # An external program is required to perform this conversion: $wgSVGConverters = array( - 'ImageMagick' => '$path/convert -background white -geometry $width $input PNG:$output', + 'ImageMagick' => '$path/convert -background white -thumbnail $widthx$height\! $input PNG:$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', @@ -1920,6 +2045,13 @@ $wgSVGMaxSize = 2048; */ $wgMaxImageArea = 1.25e7; /** + * Force thumbnailing of animated GIFs above this size to a single + * frame instead of an animated thumbnail. ImageMagick seems to + * get real unhappy and doesn't play well with resource limits. :P + * Defaulting to 1 megapixel (1000x1000) + */ +$wgMaxAnimatedGifArea = 1.0e6; +/** * 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 @@ -1988,17 +2120,54 @@ $wgRCFilterByAge = false; $wgRCLinkLimits = array( 50, 100, 250, 500 ); $wgRCLinkDays = array( 1, 3, 7, 14, 30 ); -# Send RC updates via UDP +/** + * Send recent changes updates via UDP. The updates will be formatted for IRC. + * Set this to the IP address of the receiver. + */ $wgRC2UDPAddress = false; + +/** + * Port number for RC updates + */ $wgRC2UDPPort = false; + +/** + * Prefix to prepend to each UDP packet. + * This can be used to identify the wiki. A script is available called + * mxircecho.py which listens on a UDP port, and uses a prefix ending in a + * tab to identify the IRC channel to send the log line to. + */ $wgRC2UDPPrefix = ''; + +/** + * If this is set to true, $wgLocalInterwiki will be prepended to links in the + * IRC feed. If this is set to a string, that string will be used as the prefix. + */ +$wgRC2UDPInterwikiPrefix = false; + +/** + * Set to true to omit "bot" edits (by users with the bot permission) from the + * UDP feed. + */ $wgRC2UDPOmitBots = false; -# Enable user search in Special:Newpages -# This is really a temporary hack around an index install bug on some Wikipedias. -# Kill it once fixed. +/** + * Enable user search in Special:Newpages + * This is really a temporary hack around an index install bug on some Wikipedias. + * Kill it once fixed. + */ $wgEnableNewpagesUserFilter = true; +/** + * Whether to use metadata edition + * This will put categories, language links and allowed templates in a separate text box + * while editing pages + * EXPERIMENTAL + */ +$wgUseMetadataEdit = false; +/** Full name (including namespace) of the page containing templates names that will be allowed as metadata */ +$wgMetadataWhitelist = ''; + # # Copyright and credits settings # @@ -2084,9 +2253,17 @@ $wgExportMaxHistory = 0; $wgExportAllowListContributors = false ; -/** Text matching this regular expression will be recognised as spam - * See http://en.wikipedia.org/wiki/Regular_expression */ -$wgSpamRegex = false; +/** + * Edits matching these regular expressions in body text or edit summary + * will be recognised as spam and rejected automatically. + * + * There's no administrator override on-wiki, so be careful what you set. :) + * May be an array of regexes or a single string for backwards compatibility. + * + * See http://en.wikipedia.org/wiki/Regular_expression + */ +$wgSpamRegex = array(); + /** Similarly you can get a function to do the job. The function will be given * the following args: * - a Title object for the article the edit is made on @@ -2145,6 +2322,35 @@ $wgValidateAllHtml = false; /** See list of skins and their symbolic names in languages/Language.php */ $wgDefaultSkin = 'monobook'; +/** Should we allow the user's to select their own skin that will override the default? */ +$wgAllowUserSkin = true; + +/** + * Optionally, we can specify a stylesheet to use for media="handheld". + * This is recognized by some, but not all, handheld/mobile/PDA browsers. + * If left empty, compliant handheld browsers won't pick up the skin + * stylesheet, which is specified for 'screen' media. + * + * Can be a complete URL, base-relative path, or $wgStylePath-relative path. + * Try 'chick/main.css' to apply the Chick styles to the MonoBook HTML. + * + * Will also be switched in when 'handheld=yes' is added to the URL, like + * the 'printable=yes' mode for print media. + */ +$wgHandheldStyle = false; + +/** + * If set, 'screen' and 'handheld' media specifiers for stylesheets are + * transformed such that they apply to the iPhone/iPod Touch Mobile Safari, + * which doesn't recognize 'handheld' but does support media queries on its + * screen size. + * + * Consider only using this if you have a *really good* handheld stylesheet, + * as iPhone users won't have any way to disable it and use the "grown-up" + * styles instead. + */ +$wgHandheldForIPhone = false; + /** * Settings added to this array will override the default globals for the user * preferences used by anonymous visitors and newly created accounts. @@ -2161,7 +2367,6 @@ $wgDefaultUserOptions = array( 'contextlines' => 5, 'contextchars' => 50, 'disablesuggest' => 0, - 'ajaxsearch' => 0, 'skin' => false, 'math' => 1, 'usenewrc' => 0, @@ -2184,6 +2389,10 @@ $wgDefaultUserOptions = array( 'imagesize' => 2, 'thumbsize' => 2, 'rememberpassword' => 0, + 'nocache' => 0, + 'diffonly' => 0, + 'showhiddencats' => 0, + 'norollbackdiff' => 0, 'enotifwatchlistpages' => 0, 'enotifusertalkpages' => 1, 'enotifminoredits' => 0, @@ -2192,7 +2401,9 @@ $wgDefaultUserOptions = array( 'fancysig' => 0, 'externaleditor' => 0, 'externaldiff' => 0, + 'forceeditsummary' => 0, 'showjumplinks' => 1, + 'justify' => 0, 'numberheadings' => 0, 'uselivepreview' => 0, 'watchlistdays' => 3.0, @@ -2200,10 +2411,13 @@ $wgDefaultUserOptions = array( 'watchlisthideminor' => 0, 'watchlisthidebots' => 0, 'watchlisthideown' => 0, + 'watchlisthideanons' => 0, + 'watchlisthideliu' => 0, 'watchcreations' => 0, 'watchdefault' => 0, 'watchmoves' => 0, 'watchdeletion' => 0, + 'noconvertlink' => 0, ); /** Whether or not to allow and use real name fields. Defaults to true. */ @@ -2290,7 +2504,7 @@ $wgAutoloadClasses = array(); * $wgExtensionCredits[$type][] = array( * 'name' => 'Example extension', * 'version' => 1.9, - * 'svn-revision' => '$LastChangedRevision: 46957 $', + * 'svn-revision' => '$LastChangedRevision: 47653 $', * 'author' => 'Foo Barstein', * 'url' => 'http://wwww.example.com/Example%20Extension/', * 'description' => 'An example extension', @@ -2337,6 +2551,9 @@ $wgMaxTocLevel = 999; /** Name of the external diff engine to use */ $wgExternalDiffEngine = false; +/** Whether to use inline diff */ +$wgEnableHtmlDiff = false; + /** Use RC Patrolling to check for vandalism */ $wgUseRCPatrol = true; @@ -2363,6 +2580,13 @@ $wgFeedCacheTimeout = 60; * pages larger than this size. */ $wgFeedDiffCutoff = 32768; +/** Override the site's default RSS/ATOM feed for recentchanges that appears on + * every page. Some sites might have a different feed they'd like to promote + * instead of the RC feed (maybe like a "Recent New Articles" or "Breaking news" one). + * Ex: $wgSiteFeed['format'] = "http://example.com/somefeed.xml"; Format can be one + * of either 'rss' or 'atom'. + */ +$wgOverrideSiteFeed = array(); /** * Additional namespaces. If the namespaces defined in Language.php and @@ -2448,6 +2672,12 @@ $wgCategoryMagicGallery = true; $wgCategoryPagingLimit = 200; /** + * Should the default category sortkey be the prefixed title? + * Run maintenance/refreshLinks.php after changing this. + */ +$wgCategoryPrefixedDefaultSortkey = true; + +/** * Browser Blacklist for unicode non compliant browsers * Contains a list of regexps : "/regexp/" matching problematic browsers */ @@ -2587,6 +2817,30 @@ $wgLogRestrictions = array( ); /** + * Show/hide links on Special:Log will be shown for these log types. + * + * This is associative array of log type => boolean "hide by default" + * + * See $wgLogTypes for a list of available log types. + * + * For example: + * $wgFilterLogTypes => array( + * 'move' => true, + * 'import' => false, + * ); + * + * Will display show/hide links for the move and import logs. Move logs will be + * hidden by default unless the link is clicked. Import logs will be shown by + * default, and hidden when the link is clicked. + * + * A message of the form log-show-hide-<type> should be added, and will be used + * for the link text. + */ +$wgFilterLogTypes = array( + 'patrol' => true +); + +/** * Lists the message key string for each log type. The localized messages * will be listed in the user interface. * @@ -2635,9 +2889,11 @@ $wgLogHeaders = array( $wgLogActions = array( 'block/block' => 'blocklogentry', 'block/unblock' => 'unblocklogentry', + 'block/reblock' => 'reblock-logentry', 'protect/protect' => 'protectedarticle', 'protect/modify' => 'modifiedarticleprotection', 'protect/unprotect' => 'unprotectedarticle', + 'protect/move_prot' => 'movedarticleprotection', 'rights/rights' => 'rightslogentry', 'delete/delete' => 'deletedarticle', 'delete/restore' => 'undeletedarticle', @@ -2656,6 +2912,7 @@ $wgLogActions = array( 'suppress/event' => 'logdelete-logentry', 'suppress/delete' => 'suppressedarticle', 'suppress/block' => 'blocklogentry', + 'suppress/reblock' => 'reblock-logentry', ); /** @@ -2665,6 +2922,11 @@ $wgLogActions = array( $wgLogActionsHandlers = array(); /** + * Maintain a log of newusers at Log/newusers? + */ +$wgNewUserLog = true; + +/** * List of special pages, followed by what subtitle they should go under * at Special:SpecialPages */ @@ -2688,6 +2950,8 @@ $wgSpecialPageGroups = array( 'Deadendpages' => 'maintenance', 'Wantedpages' => 'maintenance', 'Wantedcategories' => 'maintenance', + 'Wantedfiles' => 'maintenance', + 'Wantedtemplates' => 'maintenance', 'Unwatchedpages' => 'maintenance', 'Fewestrevisions' => 'maintenance', @@ -2703,7 +2967,7 @@ $wgSpecialPageGroups = array( 'Log' => 'changes', 'Upload' => 'media', - 'Imagelist' => 'media', + 'Listfiles' => 'media', 'MIMEsearch' => 'media', 'FileDuplicateSearch' => 'media', 'Filepath' => 'media', @@ -2719,6 +2983,7 @@ $wgSpecialPageGroups = array( 'Blockip' => 'users', 'Preferences' => 'users', 'Resetpass' => 'users', + 'DeletedContributions' => 'users', 'Mostlinked' => 'highuse', 'Mostlinkedcategories' => 'highuse', @@ -2739,6 +3004,7 @@ $wgSpecialPageGroups = array( 'Mytalk' => 'redirects', 'Mycontributions' => 'redirects', 'Search' => 'redirects', + 'LinkSearch' => 'redirects', 'Movepage' => 'pagetools', 'MergeHistory' => 'pagetools', @@ -2788,6 +3054,11 @@ $wgDisableInternalSearch = false; $wgSearchForwardUrl = null; /** + * Set a default target for external links, e.g. _blank to pop up a new window + */ +$wgExternalLinkTarget = false; + +/** * 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 @@ -2802,34 +3073,57 @@ $wgNoFollowLinks = true; $wgNoFollowNsExceptions = array(); /** - * Default robot policy. - * The default policy is to encourage indexing and following of links. - * It may be overridden on a per-namespace and/or per-page basis. + * Default robot policy. The default policy is to encourage indexing and fol- + * lowing of links. It may be overridden on a per-namespace and/or per-page + * basis. */ $wgDefaultRobotPolicy = 'index,follow'; /** - * Robot policies per namespaces. - * The default policy is given above, the array is made of namespace - * constants as defined in includes/Defines.php + * Robot policies per namespaces. The default policy is given above, the array + * is made of namespace constants as defined in includes/Defines.php. You can- + * not specify a different default policy for NS_SPECIAL: it is always noindex, + * nofollow. This is because a number of special pages (e.g., ListPages) have + * many permutations of options that display the same data under redundant + * URLs, so search engine spiders risk getting lost in a maze of twisty special + * pages, all alike, and never reaching your actual content. + * * Example: * $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' ); */ $wgNamespaceRobotPolicies = array(); /** - * Robot policies per article. - * These override the per-namespace robot policies. - * Must be in the form of an array where the key part is a properly - * canonicalised text form title and the value is a robot policy. + * Robot policies per article. These override the per-namespace robot policies. + * Must be in the form of an array where the key part is a properly canonical- + * ised text form title and the value is a robot policy. * Example: - * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex' ); + * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex,follow', + * 'User:Bob' => 'index,follow' ); + * Example that DOES NOT WORK because the names are not canonical text forms: + * $wgArticleRobotPolicies = array( + * # Underscore, not space! + * 'Main_Page' => 'noindex,follow', + * # "Project", not the actual project name! + * 'Project:X' => 'index,follow', + * # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false)! + * 'abc' => 'noindex,nofollow' + * ); */ $wgArticleRobotPolicies = array(); /** - * Specifies the minimal length of a user password. If set to - * 0, empty passwords are allowed. + * An array of namespace keys in which the __INDEX__/__NOINDEX__ magic words + * will not function, so users can't decide whether pages in that namespace are + * indexed by search engines. If set to null, default to $wgContentNamespaces. + * Example: + * $wgExemptFromUserRobotsControl = array( NS_MAIN, NS_TALK, NS_PROJECT ); + */ +$wgExemptFromUserRobotsControl = null; + +/** + * Specifies the minimal length of a user password. If set to 0, empty pass- + * words are allowed. */ $wgMinimalPasswordLength = 0; @@ -2844,9 +3138,8 @@ $wgUseExternalEditor = true; $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 + * Specify the name of a skin that should not be presented in the list of a- + * vailable skins. Use for blacklisting a skin which you do not want to remove * from the .../skins/ directory */ $wgSkipSkin = ''; @@ -2858,7 +3151,8 @@ $wgSkipSkins = array(); # More of the same $wgDisabledActions = array(); /** - * Disable redirects to special pages and interwiki redirects, which use a 302 and have no "redirected from" link + * Disable redirects to special pages and interwiki redirects, which use a 302 + * and have no "redirected from" link. */ $wgDisableHardRedirects = false; @@ -2869,21 +3163,19 @@ $wgEnableSorbs = false; $wgSorbsUrl = 'http.dnsbl.sorbs.net.'; /** - * Proxy whitelist, list of addresses that are assumed to be non-proxy despite what the other - * methods might say + * 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. + * Simple rate limiter options to brake edit floods. Maximum number actions + * allowed in the given number of seconds; after that the violating client re- + * ceives 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. + * This option set is experimental and likely to change. Requires memcached. */ $wgRateLimits = array( 'edit' => array( @@ -3045,17 +3337,10 @@ $wgUpdateRowsPerQuery = 10; $wgUseAjax = true; /** - * Enable auto suggestion for the search bar - * Requires $wgUseAjax to be true too. - * Causes wfSajaxSearch to be added to $wgAjaxExportList - */ -$wgAjaxSearch = false; - -/** * List of Ajax-callable functions. * Extensions acting as Ajax callbacks must register here */ -$wgAjaxExportList = array( ); +$wgAjaxExportList = array( 'wfAjaxGetThumbnailUrl', 'wfAjaxGetFileUrl' ); /** * Enable watching/unwatching pages using AJAX. @@ -3080,6 +3365,11 @@ $wgAjaxLicensePreview = true; $wgAllowDisplayTitle = true; /** + * for consistency, restrict DISPLAYTITLE to titles that normalize to the same canonical DB key + */ +$wgRestrictDisplayTitle = true; + +/** * Array of usernames which may not be registered or logged in from * Maintenance scripts can still use these */ @@ -3120,6 +3410,16 @@ $wgMaxShellMemory = 102400; $wgMaxShellFileSize = 102400; /** + * Maximum CPU time in seconds for shell processes under linux + */ +$wgMaxShellTime = 180; + +/** +* Executable name of PHP cli client (php/php5) +*/ +$wgPhpCli = 'php'; + +/** * DJVU settings * Path of the djvudump executable * Enable this and $wgDjvuRenderer to enable djvu rendering @@ -3171,7 +3471,7 @@ $wgEnableAPI = true; * (page edits, rollback, etc.) when an authorised user * accesses it */ -$wgEnableWriteAPI = false; +$wgEnableWriteAPI = true; /** * API module extensions @@ -3241,8 +3541,6 @@ $wgSlaveLagCritical = 30; * If this parameter is not given, it uses Preprocessor_DOM if the * DOM module is available, otherwise it uses Preprocessor_Hash. * - * Has no effect on Parser_OldPP. - * * The entire associative array will be passed through to the constructor as * the first parameter. Note that only Setup.php can use this variable -- * the configuration will change at runtime via $wgParser member functions, so @@ -3256,6 +3554,12 @@ $wgParserConf = array( ); /** + * LinkHolderArray batch size + * For debugging + */ +$wgLinkHolderBatchSize = 1000; + +/** * Hooks that are used for outputting exceptions. Format is: * $wgExceptionHooks[] = $funcname * or: @@ -3290,6 +3594,12 @@ $wgExpensiveParserFunctionLimit = 100; $wgMaximumMovedPages = 100; /** + * Fix double redirects after a page move. + * Tends to conflict with page move vandalism, use only on a private wiki. + */ +$wgFixDoubleRedirects = false; + +/** * Array of namespaces to generate a sitemap for when the * maintenance/generateSitemap.php script is run, or false if one is to be ge- * nerated for all namespaces. @@ -3303,3 +3613,28 @@ $wgSitemapNamespaces = false; * ting this variable false. */ $wgUseAutomaticEditSummaries = true; + +/** + * Limit password attempts to X attempts per Y seconds per IP per account. + * Requires memcached. + */ +$wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 ); + +/** + * Display user edit counts in various prominent places. + */ +$wgEdititis = false; + +/** +* Enable the UniversalEditButton for browsers that support it +* (currently only Firefox with an extension) +* See http://universaleditbutton.org for more background information +*/ +$wgUniversalEditButton = true; + +/** + * Allow id's that don't conform to HTML4 backward compatibility requirements. + * This is currently for testing; if all goes well, this option will be removed + * and the functionality will be enabled universally. + */ +$wgEnforceHtmlIds = true; diff --git a/includes/Defines.php b/includes/Defines.php index 98cee57d..8de6c5a1 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -52,8 +52,8 @@ 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_FILE', 6); +define('NS_FILE_TALK', 7); define('NS_MEDIAWIKI', 8); define('NS_MEDIAWIKI_TALK', 9); define('NS_TEMPLATE', 10); @@ -62,6 +62,16 @@ define('NS_HELP', 12); define('NS_HELP_TALK', 13); define('NS_CATEGORY', 14); define('NS_CATEGORY_TALK', 15); +/** + * NS_IMAGE and NS_IMAGE_TALK are the pre-v1.14 names for NS_FILE and + * NS_FILE_TALK respectively, and are kept for compatibility. + * + * When writing code that should be compatible with older MediaWiki + * versions, either stick to the old names or define the new constants + * yourself, if they're not defined already. + */ +define('NS_IMAGE', NS_FILE); +define('NS_IMAGE_TALK', NS_FILE_TALK); /**#@-*/ /** @@ -202,6 +212,9 @@ define( 'OT_MSG' , 3 ); // b/c alias for OT_PREPROCESS define( 'SFH_NO_HASH', 1 ); define( 'SFH_OBJECT_ARGS', 2 ); +# Flags for Parser::setLinkHook +define( 'SLH_PATTERN', 1 ); + # Flags for Parser::replaceLinkHolders define( 'RLH_FOR_UPDATE', 1 ); @@ -211,3 +224,6 @@ define( 'APCOND_EDITCOUNT', 1 ); define( 'APCOND_AGE', 2 ); define( 'APCOND_EMAILCONFIRMED', 3 ); define( 'APCOND_INGROUPS', 4 ); +define( 'APCOND_ISIP', 5 ); +define( 'APCOND_IPINRANGE', 6 ); +define( 'APCOND_AGE_FROM_EDIT', 7 ); diff --git a/includes/EditPage.php b/includes/EditPage.php index a34964bc..0193dc38 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -44,6 +44,7 @@ class EditPage { var $mArticle; var $mTitle; + var $action; var $mMetaData = ''; var $isConflict = false; var $isCssJsSubpage = false; @@ -61,7 +62,8 @@ class EditPage { var $allowBlankSummary = false; var $autoSumm = ''; var $hookError = ''; - var $mPreviewTemplates; + #var $mPreviewTemplates; + var $mParserOutput; var $mBaseRevision = false; # Form values @@ -92,6 +94,7 @@ class EditPage { function EditPage( $article ) { $this->mArticle =& $article; $this->mTitle = $article->getTitle(); + $this->action = 'submit'; # Placeholders for text injection by hooks (empty per default) $this->editFormPageTop = @@ -101,49 +104,47 @@ class EditPage { $this->editFormTextAfterTools = $this->editFormTextBottom = ""; } + + function getArticle() { + return $this->mArticle; + } /** * Fetch initial editing page content. * @private */ function getContent( $def_text = '' ) { - global $wgOut, $wgRequest, $wgParser, $wgMessageCache; + global $wgOut, $wgRequest, $wgParser, $wgContLang, $wgMessageCache; + wfProfileIn( __METHOD__ ); # Get variables from query string :P $section = $wgRequest->getVal( 'section' ); $preload = $wgRequest->getVal( 'preload' ); $undoafter = $wgRequest->getVal( 'undoafter' ); $undo = $wgRequest->getVal( 'undo' ); - wfProfileIn( __METHOD__ ); - $text = ''; - if( !$this->mTitle->exists() ) { + // For message page not locally set, use the i18n message. + // For other non-existent articles, use preload text if any. + if ( !$this->mTitle->exists() ) { if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - $wgMessageCache->loadAllMessages(); # If this is a system message, get the default text. - $text = wfMsgWeirdKey ( $this->mTitle->getText() ) ; + list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) ); + $wgMessageCache->loadAllMessages( $lang ); + $text = wfMsgGetKey( $message, false, $lang, false ); + if( wfEmptyMsg( $message, $text ) ) + $text = ''; } else { # 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. + // For existing pages, get text based on "undo" or section parameters. } 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 ($undo > 0 && $undoafter > 0 && $undo < $undoafter) { + if ( $undo > 0 && $undoafter > 0 && $undo < $undoafter ) { # If they got undoafter and undo round the wrong way, switch them list( $undo, $undoafter ) = array( $undoafter, $undo ); } - if ( $undo > 0 && $undo > $undoafter ) { # Undoing a specific edit overrides section editing; section-editing # doesn't work with undoing. @@ -158,7 +159,7 @@ class EditPage { # Sanity check, make sure it's the right page, # the revisions exist and they were not deleted. # Otherwise, $text will be left as-is. - if( !is_null( $undorev ) && !is_null( $oldrev ) && + if ( !is_null( $undorev ) && !is_null( $oldrev ) && $undorev->getPage() == $oldrev->getPage() && $undorev->getPage() == $this->mArticle->getID() && !$undorev->isDeleted( Revision::DELETED_TEXT ) && @@ -174,12 +175,12 @@ class EditPage { $text = $oldrev_text; $result = true; } - if( $result ) { + if ( $result ) { # Inform the user of our success and set an automatic edit summary $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); $firstrev = $oldrev->getNext(); # If we just undid one rev, use an autosummary - if( $firstrev->mId == $undo ) { + if ( $firstrev->mId == $undo ) { $this->summary = wfMsgForContent('undo-summary', $undo, $undorev->getUserText()); } $this->formtype = 'diff'; @@ -193,8 +194,8 @@ class EditPage { // was created, or we may simply have got bogus input. $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-norev' ) ); } - } else if( $section != '' ) { - if( $section == 'new' ) { + } else if ( $section != '' ) { + if ( $section == 'new' ) { $text = $this->getPreloadedText( $preload ); } else { $text = $wgParser->getSection( $text, $section, $def_text ); @@ -212,13 +213,13 @@ class EditPage { * @param $preload String: the title of the page. * @return string The contents of the page. */ - protected function getPreloadedText($preload) { - if ( $preload === '' ) + protected function getPreloadedText( $preload ) { + if ( $preload === '' ) { return ''; - else { + } else { $preloadTitle = Title::newFromText( $preload ); if ( isset( $preloadTitle ) && $preloadTitle->userCanRead() ) { - $rev=Revision::newFromTitle($preloadTitle); + $rev = Revision::newFromTitle($preloadTitle); if ( is_object( $rev ) ) { $text = $rev->getText(); // TODO FIXME: AAAAAAAAAAA, this shouldn't be implementing @@ -237,107 +238,103 @@ class EditPage { * 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 = '' ; + global $wgUseMetadataEdit, $wgMetadataWhitelist, $wgContLang; + $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 ) ) ; + $t = explode ( "\n" , $t ); + $catlow = strtolower ( $wgContLang->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 ( $wgContLang->getLanguageName( $ns ) || 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 ) ); + } else { + $x = implode ( ']]' , $y ); + $y = trim ( strtolower ( $x ) ); } } } - if ( count ( $cat ) ) $s .= implode ( ' ' , $cat ) . "\n" ; - if ( count ( $ll ) ) $s .= implode ( ' ' , $ll ) . "\n" ; - $t = implode ( "\n" , $t ) ; + 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 ) ) ; + $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 ) ; + if ( $isentry ) { + $sat[] = strtolower ( $x ); } } # Templates, but only some - $t = explode ( '{{' , $t ) ; + $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 ) ; + 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 $t[$key] = '{{' . $x; } - else if ( $key != 0 ) $t[$key] = '{{' . $x ; - else $t[$key] = $x ; + else if ( $key != 0 ) $t[$key] = '{{' . $x; + else $t[$key] = $x; } - if ( count ( $tl ) ) $s .= implode ( ' ' , $tl ) ; - $t = implode ( '' , $t ) ; + if ( count ( $tl ) ) $s .= implode ( ' ' , $tl ); + $t = implode ( '' , $t ); - $t = str_replace ( "\n\n\n" , "\n" , $t ) ; - $this->mArticle->mContent = $t ; - $this->mMetaData = $s ; + $t = str_replace ( "\n\n\n" , "\n" , $t ); + $this->mArticle->mContent = $t; + $this->mMetaData = $s; } + /* + * Check if a page was deleted while the user was editing it, before submit. + * Note that we rely on the logging table, which hasn't been always there, + * but that doesn't matter, because this only applies to brand new + * deletes. + */ protected function wasDeletedSinceLastEdit() { - /* Note that we rely on the logging table, which hasn't been always there, - * but that doesn't matter, because this only applies to brand new - * deletes. - */ if ( $this->deletedSinceEdit ) return true; if ( $this->mTitle->isDeleted() ) { $this->lastDelete = $this->getLastDelete(); - if ( !is_null($this->lastDelete) ) { - $deletetime = $this->lastDelete->log_timestamp; - if ( ($deletetime - $this->starttime) > 0 ) { + if ( $this->lastDelete ) { + $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); + if ( $deleteTime > $this->starttime ) { $this->deletedSinceEdit = true; } } @@ -362,61 +359,34 @@ class EditPage { */ function edit() { global $wgOut, $wgUser, $wgRequest; - - if ( !wfRunHooks( 'AlternateEdit', array( &$this ) ) ) + // Allow extensions to modify/prevent this form or submission + if ( !wfRunHooks( 'AlternateEdit', array( &$this ) ) ) { return; + } wfProfileIn( __METHOD__ ); wfDebug( __METHOD__.": enter\n" ); - // this is not an article - $wgOut->setArticleFlag(false); + // This is not an article + $wgOut->setArticleFlag( false ); $this->importFormData( $wgRequest ); $this->firsttime = false; - if( $this->live ) { + if ( $this->live ) { $this->livePreview(); wfProfileOut( __METHOD__ ); return; } - - $wgOut->addScriptFile( 'edit.js' ); - - if( wfReadOnly() ) { - $this->readOnlyPage( $this->getContent() ); - wfProfileOut( __METHOD__ ); - return; - } - $permErrors = $this->mTitle->getUserPermissionsErrors('edit', $wgUser); - - if( !$this->mTitle->exists() ) { - $permErrors = array_merge( $permErrors, - wfArrayDiff2( $this->mTitle->getUserPermissionsErrors('create', $wgUser), $permErrors ) ); + if ( wfReadOnly() && $this->save ) { + // Force preview + $this->save = false; + $this->preview = true; } - # Ignore some permissions errors. - $remove = array(); - foreach( $permErrors as $error ) { - if ( ( $this->preview || $this->diff ) && - ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext')) - { - // Don't worry about blocks when previewing/diffing - $remove[] = $error; - } - - if ($error[0] == 'readonlytext') - { - if ($this->edit) { - $this->formtype = 'preview'; - } elseif ($this->save || $this->preview || $this->diff) { - $remove[] = $error; - } - } - } - $permErrors = wfArrayDiff2( $permErrors, $remove ); - + $wgOut->addScriptFile( 'edit.js' ); + $permErrors = $this->getEditPermissionErrors(); if ( $permErrors ) { wfDebug( __METHOD__.": User can't edit\n" ); $this->readOnlyPage( $this->getContent(), true, $permErrors, 'edit' ); @@ -431,7 +401,7 @@ class EditPage { $this->formtype = 'diff'; } else { # First time through $this->firsttime = true; - if( $this->previewOnOpen() ) { + if ( $this->previewOnOpen() ) { $this->formtype = 'preview'; } else { $this->extractMetaDataFromArticle () ; @@ -448,13 +418,32 @@ class EditPage { $this->isValidCssJsSubpage = $this->mTitle->isValidCssJsSubpage(); # Show applicable editing introductions - if( $this->formtype == 'initial' || $this->firsttime ) + if ( $this->formtype == 'initial' || $this->firsttime ) $this->showIntro(); - if( $this->mTitle->isTalkPage() ) { + if ( $this->mTitle->isTalkPage() ) { $wgOut->addWikiMsg( 'talkpagetext' ); } + # Optional notices on a per-namespace and per-page basis + $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace(); + $editnotice_page = $editnotice_ns.'-'.$this->mTitle->getDBkey(); + if ( !wfEmptyMsg( $editnotice_ns, wfMsgForContent( $editnotice_ns ) ) ) { + $wgOut->addWikiText( wfMsgForContent( $editnotice_ns ) ); + } + if ( MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) { + $parts = explode( '/', $this->mTitle->getDBkey() ); + $editnotice_base = $editnotice_ns; + while ( count( $parts ) > 0 ) { + $editnotice_base .= '-'.array_shift( $parts ); + if ( !wfEmptyMsg( $editnotice_base, wfMsgForContent( $editnotice_base ) ) ) { + $wgOut->addWikiText( wfMsgForContent( $editnotice_base ) ); + } + } + } else if ( !wfEmptyMsg( $editnotice_page, wfMsgForContent( $editnotice_page ) ) ) { + $wgOut->addWikiText( wfMsgForContent( $editnotice_page ) ); + } + # 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 @@ -471,13 +460,13 @@ class EditPage { # First time through: get contents, set time for conflict # checking, etc. if ( 'initial' == $this->formtype || $this->firsttime ) { - if ($this->initialiseForm() === false) { + if ( $this->initialiseForm() === false) { $this->noSuchSectionPage(); wfProfileOut( __METHOD__."-business-end" ); wfProfileOut( __METHOD__ ); return; } - if( !$this->mTitle->getArticleId() ) + if ( !$this->mTitle->getArticleId() ) wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); } @@ -485,6 +474,27 @@ class EditPage { wfProfileOut( __METHOD__."-business-end" ); wfProfileOut( __METHOD__ ); } + + protected function getEditPermissionErrors() { + global $wgUser; + $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ); + # Can this title be created? + if ( !$this->mTitle->exists() ) { + $permErrors = array_merge( $permErrors, + wfArrayDiff2( $this->mTitle->getUserPermissionsErrors( 'create', $wgUser ), $permErrors ) ); + } + # Ignore some permissions errors when a user is just previewing/viewing diffs + $remove = array(); + foreach( $permErrors as $error ) { + if ( ($this->preview || $this->diff) && + ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext') ) + { + $remove[] = $error; + } + } + $permErrors = wfArrayDiff2( $permErrors, $remove ); + return $permErrors; + } /** * Show a read-only error @@ -510,19 +520,19 @@ class EditPage { */ protected function previewOnOpen() { global $wgRequest, $wgUser; - if( $wgRequest->getVal( 'preview' ) == 'yes' ) { + if ( $wgRequest->getVal( 'preview' ) == 'yes' ) { // Explicit override from request return true; - } elseif( $wgRequest->getVal( 'preview' ) == 'no' ) { + } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) { // Explicit override from request return false; - } elseif( $this->section == 'new' ) { + } elseif ( $this->section == 'new' ) { // Nothing *to* preview for new sections return false; - } elseif( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { + } elseif ( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { // Standard preference behaviour return true; - } elseif( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) { + } elseif ( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) { // Categories are special return true; } else { @@ -542,7 +552,7 @@ class EditPage { # Section edit can come from either the form or a link $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); - if( $request->wasPosted() ) { + 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. @@ -550,7 +560,7 @@ class EditPage { $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->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); # Remove extra headings from summaries and new sections. $this->summary = preg_replace('/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary); @@ -560,7 +570,7 @@ class EditPage { $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' ); - if( is_null( $this->edittime ) ) { + 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" ); @@ -591,11 +601,11 @@ class EditPage { } } $this->save = !$this->preview && !$this->diff; - if( !preg_match( '/^\d{14}$/', $this->edittime )) { + if ( !preg_match( '/^\d{14}$/', $this->edittime )) { $this->edittime = null; } - if( !preg_match( '/^\d{14}$/', $this->starttime )) { + if ( !preg_match( '/^\d{14}$/', $this->starttime )) { $this->starttime = null; } @@ -605,10 +615,12 @@ class EditPage { $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() ) { + 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->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) || !$wgUser->getOption( 'forceeditsummary'); } $this->autoSumm = $request->getText( 'wpAutoSummary' ); @@ -662,28 +674,30 @@ class EditPage { */ protected function showIntro() { global $wgOut, $wgUser; - if( $this->suppressIntro ) + if ( $this->suppressIntro ) { return; - + } # Show a warning message when someone creates/edits a user (talk) page but the user does not exists - if( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) { + if ( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) { $parts = explode( '/', $this->mTitle->getText(), 2 ); $username = $parts[0]; $id = User::idFromName( $username ); $ip = User::isIP( $username ); - if ( $id == 0 && !$ip ) { $wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>', array( 'userpage-userdoesnotexist', $username ) ); } } - - if( !$this->showCustomIntro() && !$this->mTitle->exists() ) { - if( $wgUser->isLoggedIn() ) { + # Try to add a custom edit intro, or use the standard one if this is not possible. + if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) { + if ( $wgUser->isLoggedIn() ) { $wgOut->wrapWikiMsg( '<div class="mw-newarticletext">$1</div>', 'newarticletext' ); } else { $wgOut->wrapWikiMsg( '<div class="mw-newarticletextanon">$1</div>', 'newarticletextanon' ); } + } + # Give a notice if the user is editing a deleted page... + if ( !$this->mTitle->exists() ) { $this->showDeletionLog( $wgOut ); } } @@ -694,9 +708,9 @@ class EditPage { * @return bool */ protected function showCustomIntro() { - if( $this->editintro ) { + if ( $this->editintro ) { $title = Title::newFromText( $this->editintro ); - if( $title instanceof Title && $title->exists() && $title->userCanRead() ) { + if ( $title instanceof Title && $title->exists() && $title->userCanRead() ) { global $wgOut; $revision = Revision::newFromTitle( $title ); $wgOut->addWikiTextTitleTidy( $revision->getText(), $this->mTitle ); @@ -714,24 +728,24 @@ class EditPage { * @return one of the constants describing the result */ function internalAttemptSave( &$result, $bot = false ) { - global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut, $wgParser; + global $wgFilterCallback, $wgUser, $wgOut, $wgParser; global $wgMaxArticleSize; $fname = 'EditPage::attemptSave'; wfProfileIn( $fname ); wfProfileIn( "$fname-checks" ); - if( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) ) + if ( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) ) { wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" ); return self::AS_HOOK_ERROR; } # Check image redirect - if ( $this->mTitle->getNamespace() == NS_IMAGE && + if ( $this->mTitle->getNamespace() == NS_FILE && Title::newFromRedirect( $this->textbox1 ) instanceof Title && !$wgUser->isAllowed( 'upload' ) ) { - if( $wgUser->isAnon() ) { + if ( $wgUser->isAnon() ) { return self::AS_IMAGE_REDIRECT_ANON; } else { return self::AS_IMAGE_REDIRECT_LOGGED; @@ -743,12 +757,15 @@ class EditPage { $this->mMetaData = '' ; # Check for spam - $matches = array(); - if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) { - $result['spam'] = $matches[0]; + $match = self::matchSpamRegex( $this->summary ); + if ( $match === false ) { + $match = self::matchSpamRegex( $this->textbox1 ); + } + if ( $match !== false ) { + $result['spam'] = $match; $ip = wfGetIP(); $pdbk = $this->mTitle->getPrefixedDBkey(); - $match = str_replace( "\n", '', $matches[0] ); + $match = str_replace( "\n", '', $match ); wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); @@ -765,7 +782,7 @@ class EditPage { wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); return self::AS_HOOK_ERROR; - } elseif( $this->hookError != '' ) { + } elseif ( $this->hookError != '' ) { # ...or the hook could be expecting us to produce an error wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); @@ -823,7 +840,6 @@ class EditPage { # 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->userCan( 'create' ) ) { wfDebug( "$fname: no create permission\n" ); @@ -833,8 +849,8 @@ class EditPage { # Don't save a new article if it's blank. if ( '' == $this->textbox1 ) { - wfProfileOut( $fname ); - return self::AS_BLANK_ARTICLE; + wfProfileOut( $fname ); + return self::AS_BLANK_ARTICLE; } // Run post-section-merge edit filter @@ -860,10 +876,10 @@ class EditPage { wfDebug("timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n"); - if( $this->mArticle->getTimestamp() != $this->edittime ) { + if ( $this->mArticle->getTimestamp() != $this->edittime ) { $this->isConflict = true; - if( $this->section == 'new' ) { - if( $this->mArticle->getUserText() == $wgUser->getName() && + 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 @@ -878,7 +894,7 @@ class EditPage { } $userid = $wgUser->getId(); - if ( $this->isConflict) { + 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); @@ -887,21 +903,21 @@ class EditPage { wfDebug( "EditPage::editForm getting section '$this->section'\n" ); $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary); } - if( is_null( $text ) ) { + 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 ) ) { + if ( $this->section == '' && $userid && $this->userWasLastToEdit($userid,$this->edittime) ) { wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" ); $this->isConflict = false; } else { # switch from section editing to normal editing in edit conflict - if($this->isConflict) { + if ( $this->isConflict ) { # Attempt merge - if( $this->mergeChangesInto( $text ) ){ + if ( $this->mergeChangesInto( $text ) ) { // Successful merge! Maybe we should tell the user the good news? $this->isConflict = false; wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" ); @@ -928,12 +944,10 @@ class EditPage { } # Handle the user preference to force summaries here, but not for null edits - if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary') && - 0 != strcmp($oldtext, $text) && + if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp($oldtext, $text) && !is_object( Title::newFromRedirect( $text ) ) # check if it's not a redirect ) { - - if( md5( $this->summary ) == $this->autoSumm ) { + if ( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; wfProfileOut( $fname ); return self::AS_SUMMARY_NEEDED; @@ -941,7 +955,7 @@ class EditPage { } # And a similar thing for new sections - if( $this->section == 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { + if ( $this->section == 'new' && !$this->allowBlankSummary ) { if (trim($this->summary) == '') { $this->missingSummary = true; wfProfileOut( $fname ); @@ -952,26 +966,26 @@ class EditPage { # All's well wfProfileIn( "$fname-sectionanchor" ); $sectionanchor = ''; - if( $this->section == 'new' ) { + if ( $this->section == 'new' ) { if ( $this->textbox1 == '' ) { $this->missingComment = true; return self::AS_TEXTBOX_EMPTY; } - if( $this->summary != '' ) { + if ( $this->summary != '' ) { $sectionanchor = $wgParser->guessSectionNameFromWikiText( $this->summary ); # This is a new section, so create a link to the new section # in the revision summary. $cleanSummary = $wgParser->stripSectionName( $this->summary ); $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); } - } elseif( $this->section != '' ) { + } 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) { + if ( $hasmatch and strlen($matches[2]) > 0 ) { $sectionanchor = $wgParser->guessSectionNameFromWikiText( $matches[2] ); } } @@ -993,7 +1007,7 @@ class EditPage { } # update the article here - if( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, + if ( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, $this->watchthis, $bot, $sectionanchor ) ) { wfProfileOut( $fname ); return self::AS_SUCCESS_UPDATE; @@ -1003,6 +1017,48 @@ class EditPage { wfProfileOut( $fname ); return self::AS_END; } + + /** + * Check if no edits were made by other users since + * the time a user started editing the page. Limit to + * 50 revisions for the sake of performance. + */ + protected function userWasLastToEdit( $id, $edittime ) { + $dbw = wfGetDB( DB_MASTER ); + $res = $dbw->select( 'revision', + 'rev_user', + array( + 'rev_page' => $this->mArticle->getId(), + 'rev_timestamp > '.$dbw->addQuotes( $dbw->timestamp($edittime) ) + ), + __METHOD__, + array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) ); + while( $row = $res->fetchObject() ) { + if( $row->rev_user != $id ) { + return false; + } + } + return true; + } + + /** + * Check given input text against $wgSpamRegex, and return the text of the first match. + * @return mixed -- matching string or false + */ + public static function matchSpamRegex( $text ) { + global $wgSpamRegex; + if ( $wgSpamRegex ) { + // For back compatibility, $wgSpamRegex may be a single string or an array of regexes. + $regexes = (array)$wgSpamRegex; + foreach( $regexes as $regex ) { + $matches = array(); + if ( preg_match( $regex, $text, $matches ) ) { + return $matches[0]; + } + } + } + return false; + } /** * Initialise form fields in the object @@ -1010,15 +1066,35 @@ class EditPage { */ function initialiseForm() { $this->edittime = $this->mArticle->getTimestamp(); - $this->textbox1 = $this->getContent(false); - if ($this->textbox1 === false) return false; - - if ( !$this->mArticle->exists() && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) - $this->textbox1 = wfMsgWeirdKey( $this->mTitle->getText() ); + $this->textbox1 = $this->getContent( false ); + if ( $this->textbox1 === false ) return false; wfProxyCheck(); return true; } + function setHeaders() { + global $wgOut, $wgTitle; + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + if ( $this->formtype == 'preview' ) { + $wgOut->setPageTitleActionText( wfMsg( 'preview' ) ); + } + if ( $this->isConflict ) { + $wgOut->setPageTitle( wfMsg( 'editconflict', $wgTitle->getPrefixedText() ) ); + } elseif ( $this->section != '' ) { + $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection'; + $wgOut->setPageTitle( wfMsg( $msg, $wgTitle->getPrefixedText() ) ); + } else { + # Use the title defined by DISPLAYTITLE magic word when present + if ( isset($this->mParserOutput) + && ( $dt = $this->mParserOutput->getDisplayTitle() ) !== false ) { + $title = $dt; + } else { + $title = $wgTitle->getPrefixedText(); + } + $wgOut->setPageTitle( wfMsg( 'editing', $title ) ); + } + } + /** * Send the edit form and related headers to $wgOut * @param $formCallback Optional callable that takes an OutputPage @@ -1026,13 +1102,13 @@ class EditPage { * near the top, for captchas and the like. */ function showEditForm( $formCallback=null ) { - global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle; + global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle, $wgRequest; # If $wgTitle is null, that means we're in API mode. # Some hook probably called this function without checking # for is_null($wgTitle) first. Bail out right here so we don't # do lots of work just to discard it right after. - if(is_null($wgTitle)) + if (is_null($wgTitle)) return; $fname = 'EditPage::showEditForm'; @@ -1042,60 +1118,55 @@ class EditPage { wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ) ; - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + #need to parse the preview early so that we know which templates are used, + #otherwise users with "show preview after edit box" will get a blank list + #we parse this near the beginning so that setHeaders can do the title + #setting work instead of leaving it in getPreviewText + $previewOutput = ''; + if ( $this->formtype == 'preview' ) { + $previewOutput = $this->getPreviewText(); + } + + $this->setHeaders(); # Enabled article-related sidebar, toplinks, etc. $wgOut->setArticleRelated( true ); - if ( $this->formtype == 'preview' ) { - $wgOut->setPageTitleActionText( wfMsg( 'preview' ) ); - } - if ( $this->isConflict ) { - $s = wfMsg( 'editconflict', $wgTitle->getPrefixedText() ); - $wgOut->setPageTitle( $s ); $wgOut->addWikiMsg( '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', $wgTitle->getPrefixedText() ); - } else { - $s = wfMsg('editingsection', $wgTitle->getPrefixedText() ); - $matches = array(); - if( !$this->summary && !$this->preview && !$this->diff ) { - preg_match( "/^(=+)(.+)\\1/mi", - $this->textbox1, - $matches ); - if( !empty( $matches[2] ) ) { - global $wgParser; - $this->summary = "/* " . - $wgParser->stripSectionName(trim($matches[2])) . - " */ "; - } + if ( $this->section != '' && $this->section != 'new' ) { + $matches = array(); + if ( !$this->summary && !$this->preview && !$this->diff ) { + preg_match( "/^(=+)(.+)\\1/mi", + $this->textbox1, + $matches ); + if ( !empty( $matches[2] ) ) { + global $wgParser; + $this->summary = "/* " . + $wgParser->stripSectionName(trim($matches[2])) . + " */ "; } } - } else { - $s = wfMsg( 'editing', $wgTitle->getPrefixedText() ); } - $wgOut->setPageTitle( $s ); if ( $this->missingComment ) { $wgOut->wrapWikiMsg( '<div id="mw-missingcommenttext">$1</div>', 'missingcommenttext' ); } - if( $this->missingSummary && $this->section != 'new' ) { + if ( $this->missingSummary && $this->section != 'new' ) { $wgOut->wrapWikiMsg( '<div id="mw-missingsummary">$1</div>', 'missingsummary' ); } - if( $this->missingSummary && $this->section == 'new' ) { + if ( $this->missingSummary && $this->section == 'new' ) { $wgOut->wrapWikiMsg( '<div id="mw-missingcommentheader">$1</div>', 'missingcommentheader' ); } - if( $this->hookError !== '' ) { + if ( $this->hookError !== '' ) { $wgOut->addWikiText( $this->hookError ); } @@ -1105,46 +1176,54 @@ class EditPage { if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) { // Let sysop know that this will make private content public if saved - if( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) { + if ( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) { $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); - } else if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + } else if ( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { $wgOut->addWikiMsg( 'rev-deleted-text-view' ); } - if( !$this->mArticle->mRevision->isCurrent() ) { + if ( !$this->mArticle->mRevision->isCurrent() ) { $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); $wgOut->addWikiMsg( 'editingold' ); } } } - if( wfReadOnly() ) { - $wgOut->addHTML( '<div id="mw-read-only-warning">'.wfMsgWikiHTML( 'readonlywarning' ).'</div>' ); - } elseif( $wgUser->isAnon() && $this->formtype != 'preview' ) { - $wgOut->addHTML( '<div id="mw-anon-edit-warning">'.wfMsgWikiHTML( 'anoneditwarning' ).'</div>' ); + if ( wfReadOnly() ) { + $wgOut->wrapWikiMsg( "<div id=\"mw-read-only-warning\">\n$1\n</div>", array( 'readonlywarning', wfReadOnlyReason() ) ); + } elseif ( $wgUser->isAnon() && $this->formtype != 'preview' ) { + $wgOut->wrapWikiMsg( '<div id="mw-anon-edit-warning">$1</div>', 'anoneditwarning' ); } else { - if( $this->isCssJsSubpage && $this->formtype != 'preview' ) { + if ( $this->isCssJsSubpage ) { # Check the skin exists - if( $this->isValidCssJsSubpage ) { - $wgOut->addWikiMsg( 'usercssjsyoucanpreview' ); + if ( $this->isValidCssJsSubpage ) { + if ( $this->formtype !== 'preview' ) { + $wgOut->addWikiMsg( 'usercssjsyoucanpreview' ); + } } else { $wgOut->addWikiMsg( 'userinvalidcssjstitle', $wgTitle->getSkinFromCssJsSubpage() ); } } } - if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $classes = array(); // Textarea CSS + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { # Show a warning if editing an interface message $wgOut->addWikiMsg( 'editinginterface' ); - } elseif( $this->mTitle->isProtected( 'edit' ) ) { + } elseif ( $this->mTitle->isProtected( 'edit' ) ) { # Is the title semi-protected? - if( $this->mTitle->isSemiProtected() ) { + if ( $this->mTitle->isSemiProtected() ) { $noticeMsg = 'semiprotectedpagewarning'; + $classes[] = 'mw-textarea-sprotected'; } else { # Then it must be protected based on static groups (regular) $noticeMsg = 'protectedpagewarning'; + $classes[] = 'mw-textarea-protected'; } + $wgOut->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" ); $wgOut->addWikiMsg( $noticeMsg ); + LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', 1 ); + $wgOut->addHTML( "</div>\n" ); } if ( $this->mTitle->isCascadeProtected() ) { # Is this page under cascading protection from some source pages? @@ -1158,7 +1237,7 @@ class EditPage { } $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', count($cascadeSources) ) ); } - if( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) != array() ){ + if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { $wgOut->addWikiMsg( 'titleprotectedwarning' ); } @@ -1166,30 +1245,21 @@ class EditPage { $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); } if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) { - $wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgMaxArticleSize ); - } elseif( $this->kblength > 29 ) { + $wgOut->addHTML( "<div class='error' id='mw-edit-longpageerror'>\n" ); + $wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ); + $wgOut->addHTML( "</div>\n" ); + } elseif ( $this->kblength > 29 ) { + $wgOut->addHTML( "<div id='mw-edit-longpagewarning'>\n" ); $wgOut->addWikiMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ); + $wgOut->addHTML( "</div>\n" ); } - #need to parse the preview early so that we know which templates are used, - #otherwise users with "show preview after edit box" will get a blank list - if ( $this->formtype == 'preview' ) { - $previewOutput = $this->getPreviewText(); - } - - $rows = $wgUser->getIntOption( 'rows' ); - $cols = $wgUser->getIntOption( 'cols' ); - - $ew = $wgUser->getOption( 'editwidth' ); - if ( $ew ) $ew = " style=\"width:100%\""; - else $ew = ''; - - $q = 'action=submit'; + $q = 'action='.$this->action; #if ( "no" == $redirect ) { $q .= "&redirect=no"; } $action = $wgTitle->escapeLocalURL( $q ); - $summary = wfMsg('summary'); - $subject = wfMsg('subject'); + $summary = wfMsg( 'summary' ); + $subject = wfMsg( 'subject' ); $cancel = $sk->makeKnownLink( $wgTitle->getPrefixedText(), wfMsgExt('cancel', array('parseinline')) ); @@ -1208,7 +1278,7 @@ class EditPage { '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' ); } - if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) { + if ( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) { # prepare toolbar for edit buttons $toolbar = EditPage::getEditToolbar(); } else { @@ -1216,35 +1286,31 @@ class EditPage { } // activate checkboxes if user wants them to be always active - if( !$this->preview && !$this->diff ) { + if ( !$this->preview && !$this->diff ) { # Sort out the "watch" checkbox - if( $wgUser->getOption( 'watchdefault' ) ) { + if ( $wgUser->getOption( 'watchdefault' ) ) { # Watch all edits $this->watchthis = true; - } elseif( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { # Watch creations $this->watchthis = true; - } elseif( $this->mTitle->userIsWatching() ) { + } elseif ( $this->mTitle->userIsWatching() ) { # Already watched $this->watchthis = true; } + + # May be overriden by request parameters + if( $wgRequest->getBool( 'watchthis' ) ) { + $this->watchthis = true; + } - if( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; + if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; } $wgOut->addHTML( $this->editFormPageTop ); if ( $wgUser->getOption( 'previewontop' ) ) { - - if ( 'preview' == $this->formtype ) { - $this->showPreview( $previewOutput ); - } else { - $wgOut->addHTML( '<div id="wikiPreview"></div>' ); - } - - if ( 'diff' == $this->formtype ) { - $this->showDiff(); - } + $this->displayPreviewArea( $previewOutput, true ); } @@ -1262,28 +1328,28 @@ class EditPage { # For a bit more sophisticated detection of blank summaries, hash the # automatic one and pass that in the hidden field wpAutoSummary. $summaryhiddens = ''; - if( $this->missingSummary ) $summaryhiddens .= Xml::hidden( 'wpIgnoreBlankSummary', true ); + if ( $this->missingSummary ) $summaryhiddens .= Xml::hidden( 'wpIgnoreBlankSummary', true ); $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); $summaryhiddens .= Xml::hidden( 'wpAutoSummary', $autosumm ); - if( $this->section == 'new' ) { - $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}:</label></span>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />"; + if ( $this->section == 'new' ) { + $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}</label></span>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />"; $editsummary = "<div class='editOptions'>\n"; global $wgParser; $formattedSummary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $this->summary ) ); - $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('subject-preview').':'.$sk->commentBlock( $formattedSummary, $this->mTitle, true )."</div>\n" : ''; + $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">". wfMsg('subject-preview') . $sk->commentBlock( $formattedSummary, $this->mTitle, true )."</div>\n" : ''; $summarypreview = ''; } else { $commentsubject = ''; - $editsummary="<div class='editOptions'>\n<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />"; - $summarypreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('summary-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; + $editsummary="<div class='editOptions'>\n<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}</label></span>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />"; + $summarypreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">". wfMsg('summary-preview') .$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; $subjectpreview = ''; } # 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 ) { + if ( !$this->preview && !$this->diff ) { $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); } - $templates = ($this->preview || $this->section != '') ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates(); + $templates = $this->getTemplates(); $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); $hiddencats = $this->mArticle->getHiddenCategories(); @@ -1294,20 +1360,24 @@ class EditPage { $metadata = $this->mMetaData ; $metadata = htmlspecialchars( $wgContLang->recodeForEdit( $metadata ) ) ; $top = wfMsgWikiHtml( 'metadata_help' ); + /* ToDo: Replace with clean code */ + $ew = $wgUser->getOption( 'editwidth' ); + if ( $ew ) $ew = " style=\"width:100%\""; + else $ew = ''; + $cols = $wgUser->getIntOption( 'cols' ); + /* /ToDo */ $metadata = $top . "<textarea name='metadata' rows='3' cols='{$cols}'{$ew}>{$metadata}</textarea>" ; } else $metadata = "" ; - $hidden = ''; $recreate = ''; - if ($this->wasDeletedSinceLastEdit()) { + if ( $this->wasDeletedSinceLastEdit() ) { if ( 'save' != $this->formtype ) { $wgOut->addWikiMsg('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' />". @@ -1317,7 +1387,7 @@ class EditPage { $tabindex = 2; - $checkboxes = self::getCheckboxes( $tabindex, $sk, + $checkboxes = $this->getCheckboxes( $tabindex, $sk, array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); $checkboxhtml = implode( $checkboxes, "\n" ); @@ -1334,47 +1404,34 @@ class EditPage { END ); - if( is_callable( $formCallback ) ) { + if ( is_callable( $formCallback ) ) { call_user_func_array( $formCallback, array( &$wgOut ) ); } wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$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" ); - - $encodedtext = htmlspecialchars( $this->safeUnicodeOutput( $this->textbox1 ) ); - if( $encodedtext !== '' ) { - // Ensure there's a newline at the end, otherwise adding lines - // is awkward. - // But don't add a newline if the ext is empty, or Firefox in XHTML - // mode will show an extra newline. A bit annoying. - $encodedtext .= "\n"; - } + $this->showFormBeforeText(); $wgOut->addHTML( <<<END -$recreate +{$recreate} {$commentsubject} {$subjectpreview} {$this->editFormTextBeforeContent} -<textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}' -cols='{$cols}'{$ew} $hidden>{$encodedtext}</textarea> END ); + $this->showTextbox1( $classes ); $wgOut->wrapWikiMsg( "<div id=\"editpage-copywarn\">\n$1\n</div>", $copywarnMsg ); - $wgOut->addHTML( $this->editFormTextAfterWarn ); - $wgOut->addHTML( " + $wgOut->addHTML( <<<END +{$this->editFormTextAfterWarn} {$metadata} {$editsummary} {$summarypreview} {$checkboxhtml} {$safemodehtml} -"); +END +); $wgOut->addHTML( "<div class='editButtons'> @@ -1398,20 +1455,18 @@ END $token = htmlspecialchars( $wgUser->editToken() ); $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); - $wgOut->addHtml( '<div class="mw-editTools">' ); - $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); - $wgOut->addHtml( '</div>' ); - - $wgOut->addHTML( $this->editFormTextAfterTools ); + $this->showEditTools(); - $wgOut->addHTML( " + $wgOut->addHTML( <<<END +{$this->editFormTextAfterTools} <div class='templatesUsed'> {$formattedtemplates} </div> <div class='hiddencats'> {$formattedhiddencats} </div> -"); +END +); if ( $this->isConflict && wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { $wgOut->wrapWikiMsg( '==$1==', "yourdiff" ); @@ -1421,26 +1476,88 @@ END $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); $wgOut->wrapWikiMsg( '==$1==', "yourtext" ); - $wgOut->addHTML( "<textarea tabindex='6' id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}'>" - . htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . "\n</textarea>" ); + $this->showTextbox2(); } $wgOut->addHTML( $this->editFormTextBottom ); $wgOut->addHTML( "</form>\n" ); if ( !$wgUser->getOption( 'previewontop' ) ) { + $this->displayPreviewArea( $previewOutput, false ); + } - if ( $this->formtype == 'preview') { - $this->showPreview( $previewOutput ); - } else { - $wgOut->addHTML( '<div id="wikiPreview"></div>' ); - } + wfProfileOut( $fname ); + } - if ( $this->formtype == 'diff') { - $this->showDiff(); - } + protected function showFormBeforeText() { + global $wgOut; + $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" ); + } + + protected function showTextbox1( $classes ) { + $attribs = array( 'tabindex' => 1 ); + + if ( $this->wasDeletedSinceLastEdit() ) + $attribs['type'] = 'hidden'; + if ( !empty($classes) ) + $attribs['class'] = implode(' ',$classes); + + $this->showTextbox( $this->textbox1, 'wpTextbox1', $attribs ); + } + + protected function showTextbox2() { + $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6 ) ); + } + + protected function showTextbox( $content, $name, $attribs = array() ) { + global $wgOut, $wgUser; + + $wikitext = $this->safeUnicodeOutput( $content ); + if ( $wikitext !== '' ) { + // Ensure there's a newline at the end, otherwise adding lines + // is awkward. + // But don't add a newline if the ext is empty, or Firefox in XHTML + // mode will show an extra newline. A bit annoying. + $wikitext .= "\n"; + } + + $attribs['accesskey'] = ','; + $attribs['id'] = $name; + + if ( $wgUser->getOption( 'editwidth' ) ) + $attribs['style'] = 'width: 100%'; + + $wgOut->addHTML( Xml::textarea( + $name, + $wikitext, + $wgUser->getIntOption( 'cols' ), $wgUser->getIntOption( 'rows' ), + $attribs ) ); + } + + protected function displayPreviewArea( $previewOutput, $isOnTop = false ) { + global $wgOut; + $classes = array(); + if ( $isOnTop ) + $classes[] = 'ontop'; + + $attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ); + + if ( $this->formtype != 'preview' ) + $attribs['style'] = 'display: none;'; + + $wgOut->addHTML( Xml::openElement( 'div', $attribs ) ); + if ( $this->formtype == 'preview' ) { + $this->showPreview( $previewOutput ); } - wfProfileOut( $fname ); + $wgOut->addHTML( '</div>' ); + + if ( $this->formtype == 'diff') { + $this->showDiff(); + } } /** @@ -1451,17 +1568,16 @@ END */ protected function showPreview( $text ) { global $wgOut; - - $wgOut->addHTML( '<div id="wikiPreview">' ); - if($this->mTitle->getNamespace() == NS_CATEGORY) { + if ( $this->mTitle->getNamespace() == NS_CATEGORY) { $this->mArticle->openShowCategory(); } + # This hook seems slightly odd here, but makes things more + # consistent for extensions. wfRunHooks( 'OutputPageBeforeHTML',array( &$wgOut, &$text ) ); $wgOut->addHTML( $text ); - if($this->mTitle->getNamespace() == NS_CATEGORY) { + if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { $this->mArticle->closeShowCategory(); } - $wgOut->addHTML( '</div>' ); } /** @@ -1477,16 +1593,22 @@ END function doLivePreviewScript() { global $wgOut, $wgTitle; $wgOut->addScriptFile( 'preview.js' ); - $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' ); + $liveAction = $wgTitle->getLocalUrl( "action={$this->action}&wpPreview=true&live=true" ); return "return !lpDoPreview(" . "editform.wpTextbox1.value," . '"' . $liveAction . '"' . ")"; } + protected function showEditTools() { + global $wgOut; + $wgOut->addHTML( '<div class="mw-editTools">' ); + $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); + $wgOut->addHTML( '</div>' ); + } + function getLastDelete() { $dbr = wfGetDB( DB_SLAVE ); - $fname = 'EditPage::getLastDelete'; - $res = $dbr->select( + $data = $dbr->selectRow( array( 'logging', 'user' ), array( 'log_type', 'log_action', @@ -1502,27 +1624,20 @@ END 'log_type' => 'delete', 'log_action' => 'delete', 'user_id=log_user' ), - $fname, + __METHOD__, 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 + * Get the rendered text for previewing. + * @return string */ function getPreviewText() { - global $wgOut, $wgUser, $wgTitle, $wgParser, $wgLang, $wgContLang; + global $wgOut, $wgUser, $wgTitle, $wgParser, $wgLang, $wgContLang, $wgMessageCache; - $fname = 'EditPage::getPreviewText'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { @@ -1538,7 +1653,7 @@ END $parserOptions->setEditSection( false ); global $wgRawHtml; - if( $wgRawHtml && !$this->mTokenOk ) { + 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'>" . @@ -1549,21 +1664,22 @@ END # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here if ( $this->isCssJsSubpage ) { - if(preg_match("/\\.css$/", $this->mTitle->getText() ) ) { + if (preg_match("/\\.css$/", $this->mTitle->getText() ) ) { $previewtext = wfMsg('usercsspreview'); - } else if(preg_match("/\\.js$/", $this->mTitle->getText() ) ) { + } else if (preg_match("/\\.js$/", $this->mTitle->getText() ) ) { $previewtext = wfMsg('userjspreview'); } $parserOptions->setTidy(true); - $parserOutput = $wgParser->parse( $previewtext , $this->mTitle, $parserOptions ); - $wgOut->addHTML( $parserOutput->mText ); - $previewHTML = ''; + $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); + $previewHTML = $parserOutput->mText; + } elseif ( $rt = Title::newFromRedirect( $this->textbox1 ) ) { + $previewHTML = $this->mArticle->viewRedirect( $rt, false ); } else { $toparse = $this->textbox1; # If we're adding a comment, we need to show the # summary as the headline - if($this->section=="new" && $this->summary!="") { + if ( $this->section=="new" && $this->summary!="" ) { $toparse="== {$this->summary} ==\n\n".$toparse; } @@ -1571,19 +1687,9 @@ END // Parse mediawiki messages with correct target language if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - $pos = strrpos( $this->mTitle->getText(), '/' ); - if ( $pos !== false ) { - $code = substr( $this->mTitle->getText(), $pos+1 ); - switch ($code) { - case $wgLang->getCode(): - $obj = $wgLang; break; - case $wgContLang->getCode(): - $obj = $wgContLang; break; - default: - $obj = Language::factory( $code ); - } - $parserOptions->setTargetLanguage( $obj ); - } + list( /* $unused */, $lang ) = $wgMessageCache->figureMessage( $this->mTitle->getText() ); + $obj = wfGetLangObj( $lang ); + $parserOptions->setTargetLanguage( $obj ); } @@ -1593,20 +1699,9 @@ END $this->mTitle, $parserOptions ); $previewHTML = $parserOutput->getText(); + $this->mParserOutput = $parserOutput; $wgOut->addParserOutputNoText( $parserOutput ); - # ParserOutput might have altered the page title, so reset it - # Also, use the title defined by DISPLAYTITLE magic word when present - if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) { - $wgOut->setPageTitle( wfMsg( 'editing', $dt ) ); - } else { - $wgOut->setPageTitle( wfMsg( 'editing', $wgTitle->getPrefixedText() ) ); - } - - foreach ( $parserOutput->getTemplates() as $ns => $template) - foreach ( array_keys( $template ) as $dbk) - $this->mPreviewTemplates[] = Title::makeTitle($ns, $dbk); - if ( count( $parserOutput->getWarnings() ) ) { $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); } @@ -1615,18 +1710,26 @@ END $previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" . "<div class='previewnote'>" . $wgOut->parse( $note ) . "</div>\n"; if ( $this->isConflict ) { - $previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; + $previewhead .='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; } - if( $wgUser->getOption( 'previewontop' ) ) { - // Spacer for the edit toolbar - $previewfoot = '<p><br /></p>'; + wfProfileOut( __METHOD__ ); + return $previewhead . $previewHTML; + } + + function getTemplates() { + if ( $this->preview || $this->section != '' ) { + $templates = array(); + if ( !isset($this->mParserOutput) ) return $templates; + foreach( $this->mParserOutput->getTemplates() as $ns => $template) { + foreach( array_keys( $template ) as $dbk ) { + $templates[] = Title::makeTitle($ns, $dbk); + } + } + return $templates; } else { - $previewfoot = ''; + return $this->mArticle->getUsedTemplates(); } - - wfProfileOut( $fname ); - return $previewhead . $previewHTML . $previewfoot; } /** @@ -1639,22 +1742,22 @@ END # 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 == '' ); - if( $first ) { + if ( $first ) { $source = $this->mTitle->exists() ? $this->getContent() : false; } else { $source = $this->textbox1; } # Spit out the source or the user's modified version - if( $source !== false ) { - $rows = $wgUser->getOption( 'rows' ); - $cols = $wgUser->getOption( 'cols' ); + if ( $source !== false ) { + $rows = $wgUser->getIntOption( 'rows' ); + $cols = $wgUser->getIntOption( 'cols' ); $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' ); - $wgOut->addHtml( '<hr />' ); + $wgOut->addHTML( '<hr />' ); $wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ); # Why we don't use Xml::element here? # Is it because if $source is '', it returns <textarea />? - $wgOut->addHtml( Xml::openElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . Xml::closeElement( 'textarea' ) ); + $wgOut->addHTML( Xml::openElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . Xml::closeElement( 'textarea' ) ); } } @@ -1672,13 +1775,13 @@ END $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addHtml( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) ); + $wgOut->addHTML( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) ); $wgOut->returnToMain( false, $wgTitle ); } /** * Creates a basic error page which informs the user that - * they have attempted to edit a nonexistant section. + * they have attempted to edit a nonexistent section. */ function noSuchSectionPage() { global $wgOut, $wgTitle; @@ -1703,11 +1806,11 @@ END $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addHtml( '<div id="spamprotected">' ); + $wgOut->addHTML( '<div id="spamprotected">' ); $wgOut->addWikiMsg( 'spamprotectiontext' ); if ( $match ) $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); - $wgOut->addHtml( '</div>' ); + $wgOut->addHTML( '</div>' ); $wgOut->returnToMain( false, $wgTitle ); } @@ -1724,7 +1827,7 @@ END // This is the revision the editor started from $baseRevision = $this->getBaseRevision(); - if( is_null( $baseRevision ) ) { + if ( is_null( $baseRevision ) ) { wfProfileOut( $fname ); return false; } @@ -1733,14 +1836,14 @@ END // The current state, we want to merge updates into it $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); - if( is_null( $currentRevision ) ) { + if ( is_null( $currentRevision ) ) { wfProfileOut( $fname ); return false; } $currentText = $currentRevision->getText(); $result = ''; - if( wfMerge( $baseText, $editText, $currentText, $result ) ){ + if ( wfMerge( $baseText, $editText, $currentText, $result ) ) { $editText = $result; wfProfileOut( $fname ); return true; @@ -1759,7 +1862,7 @@ END */ function checkUnicodeCompliantBrowser() { global $wgBrowserBlackList; - if( empty( $_SERVER["HTTP_USER_AGENT"] ) ) { + if ( empty( $_SERVER["HTTP_USER_AGENT"] ) ) { // No User-Agent header sent? Trust it by default... return true; } @@ -1861,7 +1964,7 @@ END array( 'image' => $wgLang->getImageFile('button-image'), 'id' => 'mw-editbutton-image', - 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).':', + 'open' => '[['.$wgContLang->getNsText(NS_FILE).':', 'close' => ']]', 'sample' => wfMsg('image_sample'), 'tip' => wfMsg('image_tip'), @@ -1951,7 +2054,7 @@ END * * @return array */ - public static function getCheckboxes( &$tabindex, $skin, $checked ) { + public function getCheckboxes( &$tabindex, $skin, $checked ) { global $wgUser; $checkboxes = array(); @@ -1981,6 +2084,7 @@ END Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . " <label for='wpWatchthis'".$skin->tooltip('watch', 'withaccess').">{$watchLabel}</label>"; } + wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) ); return $checkboxes; } @@ -2058,7 +2162,7 @@ END ); $buttons['diff'] = Xml::element('input', $temp, ''); - wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons ) ); + wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons, &$tabindex ) ); return $buttons; } @@ -2117,7 +2221,7 @@ END } global $wgOut; - $wgOut->addHtml( '<div id="wikiDiff">' . $difftext . '</div>' ); + $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' ); } /** @@ -2174,20 +2278,20 @@ END $working = 0; for( $i = 0; $i < strlen( $invalue ); $i++ ) { $bytevalue = ord( $invalue{$i} ); - if( $bytevalue <= 0x7F ) { //0xxx xxxx + if ( $bytevalue <= 0x7F ) { //0xxx xxxx $result .= chr( $bytevalue ); $bytesleft = 0; - } elseif( $bytevalue <= 0xBF ) { //10xx xxxx + } elseif ( $bytevalue <= 0xBF ) { //10xx xxxx $working = $working << 6; $working += ($bytevalue & 0x3F); $bytesleft--; - if( $bytesleft <= 0 ) { + if ( $bytesleft <= 0 ) { $result .= "&#x" . strtoupper( dechex( $working ) ) . ";"; } - } elseif( $bytevalue <= 0xDF ) { //110x xxxx + } elseif ( $bytevalue <= 0xDF ) { //110x xxxx $working = $bytevalue & 0x1F; $bytesleft = 1; - } elseif( $bytevalue <= 0xEF ) { //1110 xxxx + } elseif ( $bytevalue <= 0xEF ) { //1110 xxxx $working = $bytevalue & 0x0F; $bytesleft = 2; } else { //1111 0xxx @@ -2210,7 +2314,7 @@ END function unmakesafe( $invalue ) { $result = ""; for( $i = 0; $i < strlen( $invalue ); $i++ ) { - if( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue{$i+3} != '0' ) ) { + if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue{$i+3} != '0' ) ) { $i += 3; $hexstring = ""; do { @@ -2221,7 +2325,7 @@ END // 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)) { + if ( (substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6) ) { $codepoint = hexdec($hexstring); $result .= codepointToUtf8( $codepoint ); } else { @@ -2251,16 +2355,30 @@ END global $wgUser; $loglist = new LogEventsList( $wgUser->getSkin(), $out ); $pager = new LogPager( $loglist, 'delete', false, $this->mTitle->getPrefixedText() ); - if( $pager->getNumRows() > 0 ) { - $out->addHtml( '<div id="mw-recreate-deleted-warn">' ); + $count = $pager->getNumRows(); + if ( $count > 0 ) { + $pager->mLimit = 10; + $out->addHTML( '<div class="mw-warning-with-logexcerpt">' ); $out->addWikiMsg( 'recreate-deleted-warn' ); $out->addHTML( $loglist->beginLogEventsList() . $pager->getBody() . $loglist->endLogEventsList() ); - $out->addHtml( '</div>' ); + if($count > 10){ + $out->addHTML( $wgUser->getSkin()->link( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'deletelog-fulllog' ), + array(), + array( + 'type' => 'delete', + 'page' => $this->mTitle->getPrefixedText() ) ) ); + } + $out->addHTML( '</div>' ); + return true; } + + return false; } /** @@ -2273,7 +2391,7 @@ END $resultDetails = false; $value = $this->internalAttemptSave( $resultDetails, $wgUser->isAllowed('bot') && $wgRequest->getBool('bot', true) ); - if( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) { + if ( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) { $this->didSave = true; } @@ -2334,7 +2452,7 @@ END } function getBaseRevision() { - if ($this->mBaseRevision == false) { + if ( $this->mBaseRevision == false ) { $db = wfGetDB( DB_MASTER ); $baseRevision = Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime ); diff --git a/includes/Exception.php b/includes/Exception.php index ab25f0b8..eb715986 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -83,7 +83,7 @@ class MWException extends Exception { function getHTML() { global $wgShowExceptionDetails; if( $wgShowExceptionDetails ) { - return '<p>' . htmlspecialchars( $this->getMessage() ) . + return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) . '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . "</p>\n"; } else { @@ -129,7 +129,16 @@ class MWException extends Exception { $file = $this->getFile(); $line = $this->getLine(); $message = $this->getMessage(); - return $wgRequest->getRequestURL() . " Exception from line $line of $file: $message"; + if ( isset( $wgRequest ) ) { + $url = $wgRequest->getRequestURL(); + if ( !$url ) { + $url = '[no URL]'; + } + } else { + $url = '[no req]'; + } + + return "$url Exception from line $line of $file: $message"; } /** Output the exception report using HTML */ @@ -137,7 +146,7 @@ class MWException extends Exception { global $wgOut; if ( $this->useOutputPage() ) { $wgOut->setPageTitle( $this->getPageTitle() ); - $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setRobotPolicy( "noindex,nofollow" ); $wgOut->setArticleRelated( false ); $wgOut->enableClientCache( false ); $wgOut->redirect( '' ); @@ -169,7 +178,7 @@ class MWException extends Exception { wfDebugLog( 'exception', $log ); } if ( $wgCommandLineMode ) { - fwrite( STDERR, $this->getText() ); + wfPrintError( $this->getText() ); } else { $this->reportHTML(); } @@ -268,7 +277,7 @@ function wfReportException( Exception $e ) { $e2->__toString() . "\n"; if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) { - fwrite( STDERR, $message ); + wfPrintError( $message ); } else { echo nl2br( htmlspecialchars( $message ) ). "\n"; } @@ -288,6 +297,21 @@ function wfReportException( Exception $e ) { } /** + * Print a message, if possible to STDERR. + * Use this in command line mode only (see wgCommandLineMode) + */ +function wfPrintError( $message ) { + #NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602). + # Try to produce meaningful output anyway. Using echo may corrupt output to STDOUT though. + if ( defined( 'STDERR' ) ) { + fwrite( STDERR, $message ); + } + else { + echo( $message ); + } +} + +/** * Exception handler which simulates the appropriate catch() handling: * * try { diff --git a/includes/Exif.php b/includes/Exif.php index bd93eb76..d5cf09cf 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -48,7 +48,7 @@ class Exif { /** * 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. + * separated by commas. */ var $mExifTags; @@ -780,7 +780,28 @@ class FormatExif { } break; - // TODO: Flash + case 'Flash': + $flashDecode = array( + 'fired' => $val & bindec( '00000001' ), + 'return' => ($val & bindec( '00000110' )) >> 1, + 'mode' => ($val & bindec( '00011000' )) >> 3, + 'function' => ($val & bindec( '00100000' )) >> 5, + 'redeye' => ($val & bindec( '01000000' )) >> 6, +// 'reserved' => ($val & bindec( '10000000' )) >> 7, + ); + + # We do not need to handle unknown values since all are used. + foreach( $flashDecode as $subTag => $subValue ) { + # We do not need any message for zeroed values. + if( $subTag != 'fired' && $subValue == 0) { + continue; + } + $fullTag = $tag . '-' . $subTag ; + $flashMsgs[] = $this->msg( $fullTag, $subValue ); + } + $tags[$tag] = $wgLang->commaList( $flashMsgs ); + break; + case 'FocalPlaneResolutionUnit': switch( $val ) { case 2: diff --git a/includes/Export.php b/includes/Export.php index 7d0a824e..5f040b13 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -32,6 +32,7 @@ class WikiExporter { const FULL = 0; const CURRENT = 1; + const LOGS = 2; const BUFFER = 0; const STREAM = 1; @@ -71,16 +72,16 @@ class WikiExporter { * * @param $sink mixed */ - function setOutputSink( &$sink ) { + public function setOutputSink( &$sink ) { $this->sink =& $sink; } - function openStream() { + public function openStream() { $output = $this->writer->openStream(); $this->sink->writeOpenStream( $output ); } - function closeStream() { + public function closeStream() { $output = $this->writer->closeStream(); $this->sink->writeCloseStream( $output ); } @@ -90,7 +91,7 @@ class WikiExporter { * in the database, either including complete history or only * the most recent version. */ - function allPages() { + public function allPages() { return $this->dumpFrom( '' ); } @@ -101,7 +102,7 @@ class WikiExporter { * @param $end Int: Exclusive upper limit (this id is not included) * If 0, no upper limit. */ - function pagesByRange( $start, $end ) { + public function pagesByRange( $start, $end ) { $condition = 'page_id >= ' . intval( $start ); if( $end ) { $condition .= ' AND page_id < ' . intval( $end ); @@ -112,13 +113,13 @@ class WikiExporter { /** * @param $title Title */ - function pageByTitle( $title ) { + public function pageByTitle( $title ) { return $this->dumpFrom( 'page_namespace=' . $title->getNamespace() . ' AND page_title=' . $this->db->addQuotes( $title->getDBkey() ) ); } - function pageByName( $name ) { + public function pageByName( $name ) { $title = Title::newFromText( $name ); if( is_null( $title ) ) { return new WikiError( "Can't export invalid title" ); @@ -127,26 +128,36 @@ class WikiExporter { } } - function pagesByName( $names ) { + public function pagesByName( $names ) { foreach( $names as $name ) { $this->pageByName( $name ); } } + public function allLogs() { + return $this->dumpFrom( '' ); + } - // -------------------- private implementation below -------------------- + public function logsByRange( $start, $end ) { + $condition = 'log_id >= ' . intval( $start ); + if( $end ) { + $condition .= ' AND log_id < ' . intval( $end ); + } + return $this->dumpFrom( $condition ); + } # 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 ) { + protected function do_list_authors( $page , $revision , $cond ) { $fname = "do_list_authors" ; wfProfileIn( $fname ); $this->author_list = "<contributors>"; //rev_deleted $nothidden = '(rev_deleted & '.Revision::DELETED_USER.') = 0'; - $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} WHERE page_id=rev_page AND $nothidden AND " . $cond ; + $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} + WHERE page_id=rev_page AND $nothidden AND " . $cond ; $result = $this->db->query( $sql, $fname ); $resultset = $this->db->resultObject( $result ); while( $row = $resultset->fetchObject() ) { @@ -163,87 +174,101 @@ class WikiExporter { $this->author_list .= "</contributors>"; } - function dumpFrom( $cond = '' ) { + protected function dumpFrom( $cond = '' ) { $fname = 'WikiExporter::dumpFrom'; wfProfileIn( $fname ); + + # For logs dumps... + if( $this->history & self::LOGS ) { + $where = array( 'user_id = log_user' ); + # Hide private logs + $where[] = LogEventsList::getExcludeClause( $this->db ); + if( $cond ) $where[] = $cond; + $result = $this->db->select( array('logging','user'), + '*', + $where, + $fname, + array( 'ORDER BY' => 'log_id', 'USE INDEX' => array('logging' => 'PRIMARY') ) + ); + $wrapper = $this->db->resultObject( $result ); + $this->outputLogStream( $wrapper ); + # For page dumps... + } else { + list($page,$revision,$text) = $this->db->tableNamesN('page','revision','text'); - $page = $this->db->tableName( 'page' ); - $revision = $this->db->tableName( 'revision' ); - $text = $this->db->tableName( 'text' ); - - $order = 'ORDER BY page_id'; - $limit = ''; + $order = 'ORDER BY page_id'; + $limit = ''; - if( $this->history == WikiExporter::FULL ) { - $join = 'page_id=rev_page'; - } elseif( $this->history == WikiExporter::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'; - } elseif ( is_array( $this->history ) ) { - $join = 'page_id=rev_page'; - if ( $this->history['dir'] == 'asc' ) { - $op = '>'; - $order .= ', rev_timestamp'; + if( $this->history == WikiExporter::FULL ) { + $join = 'page_id=rev_page'; + } elseif( $this->history == WikiExporter::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'; + } elseif ( is_array( $this->history ) ) { + $join = 'page_id=rev_page'; + if ( $this->history['dir'] == 'asc' ) { + $op = '>'; + $order .= ', rev_timestamp'; + } else { + $op = '<'; + $order .= ', rev_timestamp DESC'; + } + if ( !empty( $this->history['offset'] ) ) { + $join .= " AND rev_timestamp $op " . $this->db->addQuotes( + $this->db->timestamp( $this->history['offset'] ) ); + } + if ( !empty( $this->history['limit'] ) ) { + $limitNum = intval( $this->history['limit'] ); + if ( $limitNum > 0 ) { + $limit = "LIMIT $limitNum"; + } + } } else { - $op = '<'; - $order .= ', rev_timestamp DESC'; + wfProfileOut( $fname ); + return new WikiError( "$fname given invalid history dump type." ); } - if ( !empty( $this->history['offset'] ) ) { - $join .= " AND rev_timestamp $op " . $this->db->addQuotes( - $this->db->timestamp( $this->history['offset'] ) ); + $where = ( $cond == '' ) ? '' : "$cond AND"; + + if( $this->buffer == WikiExporter::STREAM ) { + $prev = $this->db->bufferResults( false ); } - if ( !empty( $this->history['limit'] ) ) { - $limitNum = intval( $this->history['limit'] ); - if ( $limitNum > 0 ) { - $limit = "LIMIT $limitNum"; - } + if( $cond == '' ) { + // Optimization hack for full-database dump + $revindex = $pageindex = $this->db->useIndexClause("PRIMARY"); + $straight = ' /*! STRAIGHT_JOIN */ '; + } else { + $pageindex = ''; + $revindex = ''; + $straight = ''; } - } else { - wfProfileOut( $fname ); - return new WikiError( "$fname given invalid history dump type." ); - } - $where = ( $cond == '' ) ? '' : "$cond AND"; - - if( $this->buffer == WikiExporter::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 == WikiExporter::STUB ) { - $sql = "SELECT $straight * FROM + if( $this->text == WikiExporter::STUB ) { + $sql = "SELECT $straight * FROM $page $pageindex, $revision $revindex WHERE $where $join $order $limit"; - } else { - $sql = "SELECT $straight * FROM + } else { + $sql = "SELECT $straight * FROM $page $pageindex, $revision $revindex, $text WHERE $where $join AND rev_text_id=old_id $order $limit"; - } - $result = $this->db->query( $sql, $fname ); - $wrapper = $this->db->resultObject( $result ); - $this->outputStream( $wrapper ); + } + $result = $this->db->query( $sql, $fname ); + $wrapper = $this->db->resultObject( $result ); + $this->outputPageStream( $wrapper ); - if ( $this->list_authors ) { - $this->outputStream( $wrapper ); - } + if ( $this->list_authors ) { + $this->outputPageStream( $wrapper ); + } - if( $this->buffer == WikiExporter::STREAM ) { - $this->db->bufferResults( $prev ); + if( $this->buffer == WikiExporter::STREAM ) { + $this->db->bufferResults( $prev ); + } } - wfProfileOut( $fname ); } @@ -258,9 +283,8 @@ class WikiExporter { * blob storage types will make queries to pull source data. * * @param $resultset ResultWrapper - * @access private */ - function outputStream( $resultset ) { + protected function outputPageStream( $resultset ) { $last = null; while( $row = $resultset->fetchObject() ) { if( is_null( $last ) || @@ -292,6 +316,14 @@ class WikiExporter { } $resultset->free(); } + + protected function outputLogStream( $resultset ) { + while( $row = $resultset->fetchObject() ) { + $output = $this->writer->writeLogItem( $row ); + $this->sink->writeLogItem( $row, $output ); + } + $resultset->free(); + } } /** @@ -320,7 +352,7 @@ class XmlDumpWriter { function openStream() { global $wgContLanguageCode; $ver = $this->schemaVersion(); - return wfElement( 'mediawiki', array( + return Xml::element( '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/ " . @@ -346,30 +378,30 @@ class XmlDumpWriter { function sitename() { global $wgSitename; - return wfElement( 'sitename', array(), $wgSitename ); + return Xml::element( 'sitename', array(), $wgSitename ); } function generator() { global $wgVersion; - return wfElement( 'generator', array(), "MediaWiki $wgVersion" ); + return Xml::element( 'generator', array(), "MediaWiki $wgVersion" ); } function homelink() { - return wfElement( 'base', array(), Title::newMainPage()->getFullUrl() ); + return Xml::element( 'base', array(), Title::newMainPage()->getFullUrl() ); } function caseSetting() { global $wgCapitalLinks; // "case-insensitive" option is reserved for future $sensitivity = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; - return wfElement( 'case', array(), $sensitivity ); + return Xml::element( '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 .= ' ' . Xml::element( 'namespace', array( 'key' => $ns ), $title ) . "\n"; } $spaces .= " </namespaces>"; return $spaces; @@ -395,10 +427,10 @@ class XmlDumpWriter { 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"; + $out .= ' ' . Xml::elementClean( 'title', array(), $title->getPrefixedText() ) . "\n"; + $out .= ' ' . Xml::element( 'id', array(), strval( $row->page_id ) ) . "\n"; if( '' != $row->page_restrictions ) { - $out .= ' ' . wfElement( 'restrictions', array(), + $out .= ' ' . Xml::element( 'restrictions', array(), strval( $row->page_restrictions ) ) . "\n"; } return $out; @@ -426,12 +458,12 @@ class XmlDumpWriter { wfProfileIn( $fname ); $out = " <revision>\n"; - $out .= " " . wfElement( 'id', null, strval( $row->rev_id ) ) . "\n"; + $out .= " " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n"; $out .= $this->writeTimestamp( $row->rev_timestamp ); if( $row->rev_deleted & Revision::DELETED_USER ) { - $out .= " " . wfElement( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; + $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; } else { $out .= $this->writeContributor( $row->rev_user, $row->rev_user_text ); } @@ -440,22 +472,22 @@ class XmlDumpWriter { $out .= " <minor/>\n"; } if( $row->rev_deleted & Revision::DELETED_COMMENT ) { - $out .= " " . wfElement( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; + $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; } elseif( $row->rev_comment != '' ) { - $out .= " " . wfElementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n"; + $out .= " " . Xml::elementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n"; } if( $row->rev_deleted & Revision::DELETED_TEXT ) { - $out .= " " . wfElement( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; + $out .= " " . Xml::element( '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', + $out .= " " . Xml::elementClean( 'text', array( 'xml:space' => 'preserve' ), strval( $text ) ) . "\n"; } else { // Stub output - $out .= " " . wfElement( 'text', + $out .= " " . Xml::element( 'text', array( 'id' => $row->rev_text_id ), "" ) . "\n"; } @@ -465,19 +497,67 @@ class XmlDumpWriter { wfProfileOut( $fname ); return $out; } + + /** + * Dumps a <logitem> section on the output stream, with + * data filled in from the given database row. + * + * @param $row object + * @return string + * @access private + */ + function writeLogItem( $row ) { + $fname = 'WikiExporter::writeLogItem'; + wfProfileIn( $fname ); + + $out = " <logitem>\n"; + $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n"; + + $out .= $this->writeTimestamp( $row->log_timestamp ); + + if( $row->log_deleted & LogPage::DELETED_USER ) { + $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; + } else { + $out .= $this->writeContributor( $row->log_user, $row->user_name ); + } + + if( $row->log_deleted & LogPage::DELETED_COMMENT ) { + $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; + } elseif( $row->log_comment != '' ) { + $out .= " " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n"; + } + + $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n"; + $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n"; + + if( $row->log_deleted & LogPage::DELETED_ACTION ) { + $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; + } else { + $title = Title::makeTitle( $row->log_namespace, $row->log_title ); + $out .= " " . Xml::elementClean( 'logtitle', null, $title->getPrefixedText() ) . "\n"; + $out .= " " . Xml::elementClean( 'params', + array( 'xml:space' => 'preserve' ), + strval( $row->log_params ) ) . "\n"; + } + + $out .= " </logitem>\n"; + + wfProfileOut( $fname ); + return $out; + } function writeTimestamp( $timestamp ) { $ts = wfTimestamp( TS_ISO_8601, $timestamp ); - return " " . wfElement( 'timestamp', null, $ts ) . "\n"; + return " " . Xml::element( 'timestamp', null, $ts ) . "\n"; } function writeContributor( $id, $text ) { $out = " <contributor>\n"; if( $id ) { - $out .= " " . wfElementClean( 'username', null, strval( $text ) ) . "\n"; - $out .= " " . wfElement( 'id', null, strval( $id ) ) . "\n"; + $out .= " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n"; + $out .= " " . Xml::element( 'id', null, strval( $id ) ) . "\n"; } else { - $out .= " " . wfElementClean( 'ip', null, strval( $text ) ) . "\n"; + $out .= " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n"; } $out .= " </contributor>\n"; return $out; @@ -505,10 +585,10 @@ class XmlDumpWriter { return " <upload>\n" . $this->writeTimestamp( $file->getTimestamp() ) . $this->writeContributor( $file->getUser( 'id' ), $file->getUser( 'text' ) ) . - " " . wfElementClean( 'comment', null, $file->getDescription() ) . "\n" . - " " . wfElement( 'filename', null, $file->getName() ) . "\n" . - " " . wfElement( 'src', null, $file->getFullUrl() ) . "\n" . - " " . wfElement( 'size', null, $file->getSize() ) . "\n" . + " " . Xml::elementClean( 'comment', null, $file->getDescription() ) . "\n" . + " " . Xml::element( 'filename', null, $file->getName() ) . "\n" . + " " . Xml::element( 'src', null, $file->getFullUrl() ) . "\n" . + " " . Xml::element( 'size', null, $file->getSize() ) . "\n" . " </upload>\n"; } @@ -539,6 +619,10 @@ class DumpOutput { function writeRevision( $rev, $string ) { $this->write( $string ); } + + function writeLogItem( $rev, $string ) { + $this->write( $string ); + } /** * Override to write to a different stream type. @@ -654,6 +738,10 @@ class DumpFilter { $this->sink->writeRevision( $rev, $string ); } } + + function writeLogItem( $rev, $string ) { + $this->sink->writeRevision( $rev, $string ); + } /** * Override for page-based filter types. @@ -692,7 +780,9 @@ class DumpNamespaceFilter extends DumpFilter { "NS_USER_TALK" => NS_USER_TALK, "NS_PROJECT" => NS_PROJECT, "NS_PROJECT_TALK" => NS_PROJECT_TALK, - "NS_IMAGE" => NS_IMAGE, + "NS_FILE" => NS_FILE, + "NS_FILE_TALK" => NS_FILE_TALK, + "NS_IMAGE" => NS_IMAGE, // NS_IMAGE is an alias for NS_FILE "NS_IMAGE_TALK" => NS_IMAGE_TALK, "NS_MEDIAWIKI" => NS_MEDIAWIKI, "NS_MEDIAWIKI_TALK" => NS_MEDIAWIKI_TALK, diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index e2b78566..d095aba0 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -14,7 +14,7 @@ */ class ExternalStore { /* Fetch data from given URL */ - static function fetchFromURL($url) { + static function fetchFromURL( $url ) { global $wgExternalStores; if( !$wgExternalStores ) @@ -44,7 +44,7 @@ class ExternalStore { $class = 'ExternalStore' . ucfirst( $proto ); /* Any custom modules should be added to $wgAutoLoadClasses for on-demand loading */ - if( !class_exists( $class ) ){ + if( !class_exists( $class ) ) { return false; } @@ -66,4 +66,47 @@ class ExternalStore { return $store->store( $params, $data ); } } + + /** + * Like insert() above, but does more of the work for us. + * This function does not need a url param, it builds it by + * itself. It also fails-over to the next possible clusters. + * + * @param string $data + * Returns the URL of the stored data item, or false on error + */ + public static function insertToDefault( $data ) { + global $wgDefaultExternalStore; + $tryStores = (array)$wgDefaultExternalStore; + $error = false; + while ( count( $tryStores ) > 0 ) { + $index = mt_rand(0, count( $tryStores ) - 1); + $storeUrl = $tryStores[$index]; + wfDebug( __METHOD__.": trying $storeUrl\n" ); + list( $proto, $params ) = explode( '://', $storeUrl, 2 ); + $store = self::getStoreObject( $proto ); + if ( $store === false ) { + throw new MWException( "Invalid external storage protocol - $storeUrl" ); + } + try { + $url = $store->store( $params, $data ); // Try to save the object + } catch ( DBConnectionError $error ) { + $url = false; + } + if ( $url ) { + return $url; // Done! + } else { + unset( $tryStores[$index] ); // Don't try this one again! + $tryStores = array_values( $tryStores ); // Must have consecutive keys + wfDebugLog( 'ExternalStorage', "Unable to store text to external storage $storeUrl" ); + } + } + // All stores failed + if ( $error ) { + // Rethrow the last connection error + throw $error; + } else { + throw new MWException( "Unable to store text to external storage" ); + } + } } diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index 549412d1..9fa7d1b1 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -56,7 +56,7 @@ class ExternalStoreDB { * Fetch data from given URL * @param string $url An url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage. */ - function fetchFromURL($url) { + function fetchFromURL( $url ) { $path = explode( '/', $url ); $cluster = $path[2]; $id = $path[3]; @@ -122,12 +122,11 @@ class ExternalStoreDB { * @return string URL */ function store( $cluster, $data ) { - $fname = 'ExternalStoreDB::store'; - - $dbw =& $this->getMaster( $cluster ); - + $dbw = $this->getMaster( $cluster ); $id = $dbw->nextSequenceValue( 'blob_blob_id_seq' ); - $dbw->insert( $this->getTable( $dbw ), array( 'blob_id' => $id, 'blob_text' => $data ), $fname ); + $dbw->insert( $this->getTable( $dbw ), + array( 'blob_id' => $id, 'blob_text' => $data ), + __METHOD__ ); $id = $dbw->insertId(); if ( $dbw->getFlag( DBO_TRX ) ) { $dbw->immediateCommit(); diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php index 4c2eddc8..10bfa538 100644 --- a/includes/FakeTitle.php +++ b/includes/FakeTitle.php @@ -3,15 +3,13 @@ /** * Fake title class that triggers an error if any members are called */ -class FakeTitle { +class FakeTitle extends Title { function error() { throw new MWException( "Attempt to call member function of FakeTitle\n" ); } // PHP 5.1 method overload function __call( $name, $args ) { $this->error(); } // PHP <5.1 compatibility - function getInterwikiLink() { $this->error(); } - function getInterwikiCached() { $this->error(); } function isLocal() { $this->error(); } function isTrans() { $this->error(); } function getText() { $this->error(); } @@ -28,20 +26,20 @@ class FakeTitle { function getPrefixedText() { $this->error(); } function getFullText() { $this->error(); } function getPrefixedURL() { $this->error(); } - function getFullURL() {$this->error(); } - function getLocalURL() { $this->error(); } - function escapeLocalURL() { $this->error(); } - function escapeFullURL() { $this->error(); } - function getInternalURL() { $this->error(); } + function getFullURL( $query = '', $variant = false ) {$this->error(); } + function getLocalURL( $query = '', $variant = false ) { $this->error(); } + function escapeLocalURL( $query = '' ) { $this->error(); } + function escapeFullURL( $query = '' ) { $this->error(); } + function getInternalURL( $query = '', $variant = false ) { $this->error(); } function getEditURL() { $this->error(); } function getEscapedText() { $this->error(); } function isExternal() { $this->error(); } - function isSemiProtected() { $this->error(); } - function isProtected() { $this->error(); } + function isSemiProtected( $action = 'edit' ) { $this->error(); } + function isProtected( $action = '' ) { $this->error(); } function userIsWatching() { $this->error(); } - function userCan() { $this->error(); } + function userCan( $action, $doExpensiveQueries = true ) { $this->error(); } function userCanCreate() { $this->error(); } - function userCanEdit() { $this->error(); } + function userCanEdit( $doExpensiveQueries = true ) { $this->error(); } function userCanMove() { $this->error(); } function isMovable() { $this->error(); } function userCanRead() { $this->error(); } @@ -79,6 +77,7 @@ class FakeTitle { function equals() { $this->error(); } function exists() { $this->error(); } function isAlwaysKnown() { $this->error(); } + function isKnown() { $this->error(); } function touchLinks() { $this->error(); } function trackbackURL() { $this->error(); } function trackbackRDF() { $this->error(); } diff --git a/includes/Feed.php b/includes/Feed.php index 512057d9..fe6d8feb 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -52,25 +52,45 @@ class FeedItem { $this->Comments = $Comments; } - /** - * @static - */ - function xmlEncode( $string ) { + public function xmlEncode( $string ) { $string = str_replace( "\r\n", "\n", $string ); $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string ); return htmlspecialchars( $string ); } - function getTitle() { return $this->xmlEncode( $this->Title ); } - function getUrl() { return $this->xmlEncode( $this->Url ); } - function getDescription() { return $this->xmlEncode( $this->Description ); } - function getLanguage() { + public function getTitle() { + return $this->xmlEncode( $this->Title ); + } + + public function getUrl() { + return $this->xmlEncode( $this->Url ); + } + + public function getDescription() { + return $this->xmlEncode( $this->Description ); + } + + public function getLanguage() { global $wgContLanguageCode; return $wgContLanguageCode; } - function getDate() { return $this->Date; } - function getAuthor() { return $this->xmlEncode( $this->Author ); } - function getComments() { return $this->xmlEncode( $this->Comments ); } + + public function getDate() { + return $this->Date; + } + public function getAuthor() { + return $this->xmlEncode( $this->Author ); + } + public function getComments() { + return $this->xmlEncode( $this->Comments ); + } + + /** + * Quickie hack... strip out wikilinks to more legible form from the comment. + */ + public static function stripComment( $text ) { + return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); + } /**#@-*/ } @@ -149,7 +169,7 @@ class ChannelFeed extends FeedItem { global $wgStylePath, $wgStyleVersion; $this->httpHeaders(); - echo '<?xml version="1.0" encoding="utf-8"?>' . "\n"; + echo '<?xml version="1.0"?>' . "\n"; echo '<?xml-stylesheet type="text/css" href="' . htmlspecialchars( wfExpandUrl( "$wgStylePath/common/feed.css?$wgStyleVersion" ) ) . '"?' . ">\n"; diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index aa784c02..38bff363 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -75,17 +75,20 @@ class FeedUtils { if( $oldid ) { wfProfileIn( __FUNCTION__."-dodiff" ); - $de = new DifferenceEngine( $title, $oldid, $newid ); #$diffText = $de->getDiff( wfMsg( 'revisionasof', # $wgContLang->timeanddate( $timestamp ) ), # wfMsg( 'currentrev' ) ); - $diffText = $de->getDiff( - wfMsg( 'previousrevision' ), // hack - wfMsg( 'revisionasof', - $wgContLang->timeanddate( $timestamp ) ) ); - + + // Don't bother generating the diff if we won't be able to show it + if ( $wgFeedDiffCutoff > 0 ) { + $de = new DifferenceEngine( $title, $oldid, $newid ); + $diffText = $de->getDiff( + wfMsg( 'previousrevision' ), // hack + wfMsg( 'revisionasof', + $wgContLang->timeanddate( $timestamp ) ) ); + } - if ( strlen( $diffText ) > $wgFeedDiffCutoff ) { + if ( ( strlen( $diffText ) > $wgFeedDiffCutoff ) || ( $wgFeedDiffCutoff <= 0 ) ) { // Omit large diffs $diffLink = $title->escapeFullUrl( 'diff=' . $newid . diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index bc80c2b2..66086b0f 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -55,7 +55,7 @@ class FileDeleteForm { $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage ); if( !self::haveDeletableFile($this->file, $this->oldfile, $this->oldimage) ) { - $wgOut->addHtml( $this->prepareMessage( 'filedelete-nofile' ) ); + $wgOut->addHTML( $this->prepareMessage( 'filedelete-nofile' ) ); $wgOut->addReturnTo( $this->title ); return; } @@ -78,7 +78,7 @@ class FileDeleteForm { $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) ); if( $status->ok ) { $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->addHtml( $this->prepareMessage( 'filedelete-success' ) ); + $wgOut->addHTML( $this->prepareMessage( 'filedelete-success' ) ); // Return to the main page if we just deleted all versions of the // file, otherwise go back to the description page $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() ); @@ -105,16 +105,24 @@ class FileDeleteForm { } else { $status = $file->delete( $reason, $suppress ); if( $status->ok ) { + $id = $title->getArticleID( GAID_FOR_UPDATE ); // Need to delete the associated article $article = new Article( $title ); if( wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason)) ) { - if( $article->doDeleteArticle( $reason, $suppress ) ) - wfRunHooks('ArticleDeleteComplete', array(&$article, &$wgUser, $reason)); + if( $article->doDeleteArticle( $reason, $suppress, $id ) ) { + global $wgRequest; + if( $wgRequest->getCheck( 'wpWatch' ) ) { + $article->doWatch(); + } elseif( $title->userIsWatching() ) { + $article->doUnwatch(); + } + wfRunHooks('ArticleDeleteComplete', array(&$article, &$wgUser, $reason, $id)); + } } } } - if( $status->isGood() ) wfRunHooks('FileDeleteComplete', array( - &$file, &$oldimage, &$article, &$wgUser, &$reason)); + if( $status->isGood() ) + wfRunHooks('FileDeleteComplete', array( &$file, &$oldimage, &$article, &$wgUser, &$reason)); return $status; } @@ -123,46 +131,60 @@ class FileDeleteForm { * Show the confirmation form */ private function showForm() { - global $wgOut, $wgUser, $wgRequest, $wgContLang; - $align = $wgContLang->isRtl() ? 'left' : 'right'; + global $wgOut, $wgUser, $wgRequest; if( $wgUser->isAllowed( 'suppressrevision' ) ) { - $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"><td></td><td>"; - $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) ); - $suppress .= "</td></tr>"; + $suppress = "<tr id=\"wpDeleteSuppressRow\"> + <td></td> + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'revdelete-suppress' ), + 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '3' ) ) . + "</td> + </tr>"; } else { $suppress = ''; } - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ) . + $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->title->userIsWatching(); + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction(), + 'id' => 'mw-img-deleteconfirm' ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) . Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ) . $this->prepareMessage( 'filedelete-intro' ) . - Xml::openElement( 'table' ) . + Xml::openElement( 'table', array( 'id' => 'mw-img-deleteconfirm-table' ) ) . "<tr> - <td align='$align'>" . + <td class='mw-label'>" . Xml::label( wfMsg( 'filedelete-comment' ), 'wpDeleteReasonList' ) . "</td> - <td>" . + <td class='mw-input'>" . Xml::listDropDown( 'wpDeleteReasonList', wfMsgForContent( 'filedelete-reason-dropdown' ), wfMsgForContent( 'filedelete-reason-otherlist' ), '', 'wpReasonDropDown', 1 ) . "</td> </tr> <tr> - <td align='$align'>" . + <td class='mw-label'>" . Xml::label( wfMsg( 'filedelete-otherreason' ), 'wpReason' ) . "</td> - <td>" . - Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . + <td class='mw-input'>" . + Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), + array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . "</td> </tr> {$suppress} <tr> <td></td> - <td>" . - Xml::submitButton( wfMsg( 'filedelete-submit' ), array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '3' ) ) . + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'watchthis' ), + 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . + "</td> + </tr> + <tr> + <td></td> + <td class='mw-submit'>" . + Xml::submitButton( wfMsg( 'filedelete-submit' ), + array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '4' ) ) . "</td> </tr>" . Xml::closeElement( 'table' ) . @@ -175,7 +197,7 @@ class FileDeleteForm { $form .= '<p class="mw-filedelete-editreasons">' . $link . '</p>'; } - $wgOut->addHtml( $form ); + $wgOut->addHTML( $form ); } /** @@ -183,7 +205,7 @@ class FileDeleteForm { */ private function showLogEntries() { global $wgOut; - $wgOut->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); + $wgOut->addHTML( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); LogEventsList::showLogExtract( $wgOut, 'delete', $this->title->getPrefixedText() ); } diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php index 385d83bc..c7c73246 100644 --- a/includes/FileRevertForm.php +++ b/includes/FileRevertForm.php @@ -57,7 +57,7 @@ class FileRevertForm { } if( !$this->haveOldVersion() ) { - $wgOut->addHtml( wfMsgExt( 'filerevert-badversion', 'parse' ) ); + $wgOut->addHTML( wfMsgExt( 'filerevert-badversion', 'parse' ) ); $wgOut->returnToMain( false, $this->title ); return; } @@ -69,7 +69,7 @@ class FileRevertForm { // TODO: Preserve file properties from database instead of reloading from file $status = $this->file->upload( $source, $comment, $comment ); if( $status->isGood() ) { - $wgOut->addHtml( wfMsgExt( 'filerevert-success', 'parse', $this->title->getText(), + $wgOut->addHTML( wfMsgExt( 'filerevert-success', 'parse', $this->title->getText(), $wgLang->date( $this->getTimestamp(), true ), $wgLang->time( $this->getTimestamp(), true ), wfExpandUrl( $this->file->getArchiveUrl( $this->archiveName ) ) ) ); @@ -104,7 +104,7 @@ class FileRevertForm { $form .= '</fieldset>'; $form .= '</form>'; - $wgOut->addHtml( $form ); + $wgOut->addHTML( $form ); } /** diff --git a/includes/FileStore.php b/includes/FileStore.php index c01350c0..278777b4 100644 --- a/includes/FileStore.php +++ b/includes/FileStore.php @@ -35,39 +35,22 @@ class FileStore { * This is attached to your master database connection, so if you * suffer an uncaught error the lock will be released when the * connection is closed. - * - * @todo Probably only works on MySQL. Abstract to the Database class? + * @see Database::lock() */ static function lock() { - global $wgDBtype; - if ($wgDBtype != 'mysql') - return true; $dbw = wfGetDB( DB_MASTER ); $lockname = $dbw->addQuotes( FileStore::lockName() ); - $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", __METHOD__ ); - $row = $dbw->fetchObject( $result ); - $dbw->freeResult( $result ); - - if( $row->lockstatus == 1 ) { - return true; - } else { - wfDebug( __METHOD__." failed to acquire lock\n" ); - return false; - } + return $dbw->lock( $lockname, __METHOD__ ); } /** * Release the global file store lock. + * @see Database::unlock() */ static function unlock() { - global $wgDBtype; - if ($wgDBtype != 'mysql') - return true; $dbw = wfGetDB( DB_MASTER ); $lockname = $dbw->addQuotes( FileStore::lockName() ); - $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", __METHOD__ ); - $dbw->fetchObject( $result ); - $dbw->freeResult( $result ); + return $dbw->unlock( $lockname, __METHOD__ ); } private static function lockName() { @@ -123,7 +106,7 @@ class FileStore { } else { if( !file_exists( dirname( $destPath ) ) ) { wfSuppressWarnings(); - $ok = mkdir( dirname( $destPath ), 0777, true ); + $ok = wfMkdirParents( dirname( $destPath ) ); wfRestoreWarnings(); if( !$ok ) { diff --git a/includes/FormOptions.php b/includes/FormOptions.php index 5888a0c4..262c8c7f 100644 --- a/includes/FormOptions.php +++ b/includes/FormOptions.php @@ -176,8 +176,8 @@ class FormOptions implements ArrayAccess { throw new MWException( 'Unsupported datatype' ); } - if ( $value !== $default && $value !== null ) { - $this->options[$name]['value'] = $value; + if ( $value !== null ) { + $this->options[$name]['value'] = $value === $default ? null : $value; } } } diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index d1336d47..33f5831d 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -8,10 +8,12 @@ if ( !defined( 'MEDIAWIKI' ) ) { * Global functions used everywhere */ -require_once dirname(__FILE__) . '/LogPage.php'; require_once dirname(__FILE__) . '/normal/UtfNormalUtil.php'; require_once dirname(__FILE__) . '/XmlFunctions.php'; +// Hide compatibility functions from Doxygen +/// @cond + /** * Compatibility functions * @@ -87,6 +89,9 @@ if ( !function_exists( 'array_diff_key' ) ) { } } +/// @endcond + + /** * Like array_diff( $a, $b ) except that it works with two-dimensional arrays. */ @@ -145,16 +150,31 @@ function wfRandom() { } /** - * We want / and : to be included as literal characters in our title URLs. + * We want some things to be included as literal characters in our title URLs + * for prettiness, which urlencode encodes by default. According to RFC 1738, + * all of the following should be safe: + * + * ;:@&=$-_.+!*'(), + * + * But + is not safe because it's used to indicate a space; &= are only safe in + * paths and not in queries (and we don't distinguish here); ' seems kind of + * scary; and urlencode() doesn't touch -_. to begin with. Plus, although / + * is reserved, we don't care. So the list we unescape is: + * + * ;:@$!*(),/ + * * %2F in the page titles seems to fatally break for some reason. * * @param $s String: * @return string */ -function wfUrlencode ( $s ) { +function wfUrlencode( $s ) { $s = urlencode( $s ); - $s = preg_replace( '/%3[Aa]/', ':', $s ); - $s = preg_replace( '/%2[Ff]/', '/', $s ); + $s = str_ireplace( + array( '%3B','%3A','%40','%24','%21','%2A','%28','%29','%2C','%2F' ), + array( ';', ':', '@', '$', '!', '*', '(', ')', ',', '/' ), + $s + ); return $s; } @@ -174,6 +194,7 @@ function wfUrlencode ( $s ) { */ function wfDebug( $text, $logonly = false ) { global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage; + global $wgDebugLogPrefix; static $recursion = 0; static $cache = array(); // Cache of unoutputted messages @@ -206,11 +227,26 @@ function wfDebug( $text, $logonly = false ) { # Strip unprintables; they can switch terminal modes when binary data # gets dumped, which is pretty annoying. $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text ); + $text = $wgDebugLogPrefix . $text; wfErrorLog( $text, $wgDebugLogFile ); } } /** + * Send a line giving PHP memory usage. + * @param $exact Bool : print exact values instead of kilobytes (default: false) + */ +function wfDebugMem( $exact = false ) { + $mem = memory_get_usage(); + if( !$exact ) { + $mem = floor( $mem / 1024 ) . ' kilobytes'; + } else { + $mem .= ' bytes'; + } + wfDebug( "Memory usage: $mem\n" ); +} + +/** * Send a line to a supplementary debug log file, if configured, or main debug log if not. * $wgDebugLogGroups[$logGroup] should be set to a filename to send to a separate log. * @@ -220,12 +256,17 @@ function wfDebug( $text, $logonly = false ) { * log file is specified, (default true) */ function wfDebugLog( $logGroup, $text, $public = true ) { - global $wgDebugLogGroups; - if( $text{strlen( $text ) - 1} != "\n" ) $text .= "\n"; + global $wgDebugLogGroups, $wgShowHostnames; + $text = trim($text)."\n"; if( isset( $wgDebugLogGroups[$logGroup] ) ) { $time = wfTimestamp( TS_DB ); $wiki = wfWikiID(); - wfErrorLog( "$time $wiki: $text", $wgDebugLogGroups[$logGroup] ); + if ( $wgShowHostnames ) { + $host = wfHostname(); + } else { + $host = ''; + } + wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] ); } else if ( $public === true ) { wfDebug( $text, true ); } @@ -245,16 +286,50 @@ function wfLogDBError( $text ) { } /** - * Log to a file without getting "file size exceeded" signals + * Log to a file without getting "file size exceeded" signals. + * + * Can also log to TCP or UDP with the syntax udp://host:port/prefix. This will + * send lines to the specified port, prefixed by the specified prefix and a space. */ function wfErrorLog( $text, $file ) { - wfSuppressWarnings(); - $exists = file_exists( $file ); - $size = $exists ? filesize( $file ) : false; - if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) { - error_log( $text, 3, $file ); + if ( substr( $file, 0, 4 ) == 'udp:' ) { + if ( preg_match( '!^(tcp|udp):(?://)?\[([0-9a-fA-F:]+)\]:(\d+)(?:/(.*))?$!', $file, $m ) ) { + // IPv6 bracketed host + $protocol = $m[1]; + $host = $m[2]; + $port = $m[3]; + $prefix = isset( $m[4] ) ? $m[4] : false; + } elseif ( preg_match( '!^(tcp|udp):(?://)?([a-zA-Z0-9.-]+):(\d+)(?:/(.*))?$!', $file, $m ) ) { + $protocol = $m[1]; + $host = $m[2]; + $port = $m[3]; + $prefix = isset( $m[4] ) ? $m[4] : false; + } else { + throw new MWException( __METHOD__.": Invalid UDP specification" ); + } + // Clean it up for the multiplexer + if ( strval( $prefix ) !== '' ) { + $text = preg_replace( '/^/m', $prefix . ' ', $text ); + if ( substr( $text, -1 ) != "\n" ) { + $text .= "\n"; + } + } + + $sock = fsockopen( "$protocol://$host", $port ); + if ( !$sock ) { + return; + } + fwrite( $sock, $text ); + fclose( $sock ); + } else { + wfSuppressWarnings(); + $exists = file_exists( $file ); + $size = $exists ? filesize( $file ) : false; + if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) { + error_log( $text, 3, $file ); + } + wfRestoreWarnings(); } - wfRestoreWarnings(); } /** @@ -320,6 +395,47 @@ function wfReadOnlyReason() { } /** + * Return a Language object from $langcode + * @param $langcode Mixed: either: + * - a Language object + * - code of the language to get the message for, if it is + * a valid code create a language for that language, if + * it is a string but not a valid code then make a basic + * language object + * - a boolean: if it's false then use the current users + * language (as a fallback for the old parameter + * functionality), or if it is true then use the wikis + * @return Language object + */ +function wfGetLangObj( $langcode = false ){ + # Identify which language to get or create a language object for. + if( $langcode instanceof Language ) + # Great, we already have the object! + return $langcode; + + global $wgContLang; + if( $langcode === $wgContLang->getCode() || $langcode === true ) + # $langcode is the language code of the wikis content language object. + # or it is a boolean and value is true + return $wgContLang; + + global $wgLang; + if( $langcode === $wgLang->getCode() || $langcode === false ) + # $langcode is the language code of user language object. + # or it was a boolean and value is false + return $wgLang; + + $validCodes = array_keys( Language::getLanguageNames() ); + if( in_array( $langcode, $validCodes ) ) + # $langcode corresponds to a valid language. + return Language::factory( $langcode ); + + # $langcode is a string, but not a valid language code; use content language. + wfDebug( 'Invalid language code passed to wfGetLangObj, falling back to content language.' ); + return $wgContLang; +} + +/** * Get a message from anywhere, for the current user language. * * Use wfMsgForContent() instead if the message should NOT @@ -458,7 +574,7 @@ function wfMsgWeirdKey ( $key ) { * @private */ function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) { - global $wgParser, $wgContLang, $wgMessageCache, $wgLang; + global $wgContLang, $wgMessageCache; wfRunHooks('NormalizeMessageKey', array(&$key, &$useDB, &$langCode, &$transform)); @@ -469,21 +585,7 @@ function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) { $message = $wgMessageCache->transform( $message ); } } else { - if( $langCode === true ) { - $lang = &$wgContLang; - } elseif( $langCode === false ) { - $lang = &$wgLang; - } else { - $validCodes = array_keys( Language::getLanguageNames() ); - if( in_array( $langCode, $validCodes ) ) { - # $langcode corresponds to a valid language. - $lang = Language::factory( $langCode ); - } else { - # $langcode is a string, but not a valid language code; use content language. - $lang =& $wgContLang; - wfDebug( 'Invalid language code passed to wfMsgGetKey, falling back to content language.' ); - } - } + $lang = wfGetLangObj( $langCode ); # MessageCache::get() does this already, Language::getMessage() doesn't # ISSUE: Should we try to handle "message/lang" here too? @@ -565,40 +667,47 @@ function wfMsgWikiHtml( $key ) { /** * Returns message in the requested format * @param string $key Key of the message - * @param array $options Processing rules: - * <i>parse</i>: parses wikitext to html - * <i>parseinline</i>: parses wikitext to html and removes the surrounding p's added by parser or tidy - * <i>escape</i>: filters message through htmlspecialchars - * <i>escapenoentities</i>: same, but allows entity references like through - * <i>replaceafter</i>: parameters are substituted after parsing or escaping - * <i>parsemag</i>: transform the message using magic phrases - * <i>content</i>: fetch message for content language instead of interface - * <i>language</i>: language code to fetch message for (overriden by <i>content</i>), its behaviour - * with parser, parseinline and parsemag is undefined. + * @param array $options Processing rules. Can take the following options: + * <i>parse</i>: parses wikitext to html + * <i>parseinline</i>: parses wikitext to html and removes the surrounding + * p's added by parser or tidy + * <i>escape</i>: filters message through htmlspecialchars + * <i>escapenoentities</i>: same, but allows entity references like through + * <i>replaceafter</i>: parameters are substituted after parsing or escaping + * <i>parsemag</i>: transform the message using magic phrases + * <i>content</i>: fetch message for content language instead of interface + * Also can accept a single associative argument, of the form 'language' => 'xx': + * <i>language</i>: Language object or language code to fetch message for + * (overriden by <i>content</i>), its behaviour with parser, parseinline + * and parsemag is undefined. * Behavior for conflicting options (e.g., parse+parseinline) is undefined. */ function wfMsgExt( $key, $options ) { - global $wgOut, $wgParser; + global $wgOut; $args = func_get_args(); array_shift( $args ); array_shift( $args ); - - if( !is_array($options) ) { - $options = array($options); + $options = (array)$options; + + foreach( $options as $arrayKey => $option ) { + if( !preg_match( '/^[0-9]+|language$/', $arrayKey ) ) { + # An unknown index, neither numeric nor "language" + trigger_error( "wfMsgExt called with incorrect parameter key $arrayKey", E_USER_WARNING ); + } elseif( preg_match( '/^[0-9]+$/', $arrayKey ) && !in_array( $option, + array( 'parse', 'parseinline', 'escape', 'escapenoentities', + 'replaceafter', 'parsemag', 'content' ) ) ) { + # A numeric index with unknown value + trigger_error( "wfMsgExt called with incorrect parameter $option", E_USER_WARNING ); + } } - if( in_array('content', $options) ) { + if( in_array('content', $options, true ) ) { $forContent = true; $langCode = true; } elseif( array_key_exists('language', $options) ) { $forContent = false; - $langCode = $options['language']; - $validCodes = array_keys( Language::getLanguageNames() ); - if( !in_array($options['language'], $validCodes) ) { - # Fallback to en, instead of whatever interface language we might have - $langCode = 'en'; - } + $langCode = wfGetLangObj( $options['language'] ); } else { $forContent = false; $langCode = false; @@ -606,34 +715,34 @@ function wfMsgExt( $key, $options ) { $string = wfMsgGetKey( $key, /*DB*/true, $langCode, /*Transform*/false ); - if( !in_array('replaceafter', $options) ) { + if( !in_array('replaceafter', $options, true ) ) { $string = wfMsgReplaceArgs( $string, $args ); } - if( in_array('parse', $options) ) { + if( in_array('parse', $options, true ) ) { $string = $wgOut->parse( $string, true, !$forContent ); - } elseif ( in_array('parseinline', $options) ) { + } elseif ( in_array('parseinline', $options, true ) ) { $string = $wgOut->parse( $string, true, !$forContent ); $m = array(); if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) { $string = $m[1]; } - } elseif ( in_array('parsemag', $options) ) { + } elseif ( in_array('parsemag', $options, true ) ) { global $wgMessageCache; if ( isset( $wgMessageCache ) ) { - $string = $wgMessageCache->transform( $string, !$forContent ); + $string = $wgMessageCache->transform( $string, + !$forContent, + is_object( $langCode ) ? $langCode : null ); } } - if ( in_array('escape', $options) ) { + if ( in_array('escape', $options, true ) ) { $string = htmlspecialchars ( $string ); - } elseif ( in_array( 'escapenoentities', $options ) ) { - $string = htmlspecialchars( $string ); - $string = str_replace( '&', '&', $string ); - $string = Sanitizer::normalizeCharReferences( $string ); + } elseif ( in_array( 'escapenoentities', $options, true ) ) { + $string = Sanitizer::escapeHtmlAllowEntities( $string ); } - if( in_array('replaceafter', $options) ) { + if( in_array('replaceafter', $options, true ) ) { $string = wfMsgReplaceArgs( $string, $args ); } @@ -707,18 +816,25 @@ function wfDebugDieBacktrace( $msg = '' ) { * @return string */ function wfHostname() { - if ( function_exists( 'posix_uname' ) ) { - // This function not present on Windows - $uname = @posix_uname(); - } else { - $uname = false; - } - if( is_array( $uname ) && isset( $uname['nodename'] ) ) { - return $uname['nodename']; - } else { - # This may be a virtual server. - return $_SERVER['SERVER_NAME']; + static $host; + if ( is_null( $host ) ) { + if ( function_exists( 'posix_uname' ) ) { + // This function not present on Windows + $uname = @posix_uname(); + } else { + $uname = false; + } + if( is_array( $uname ) && isset( $uname['nodename'] ) ) { + $host = $uname['nodename']; + } elseif ( getenv( 'COMPUTERNAME' ) ) { + # Windows computer name + $host = getenv( 'COMPUTERNAME' ); + } else { + # This may be a virtual server. + $host = $_SERVER['SERVER_NAME']; + } } + return $host; } /** @@ -929,7 +1045,7 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { */ function wfEscapeWikiText( $text ) { $text = str_replace( - array( '[', '|', ']', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), + array( '[', '|', ']', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), # }} array( '[', '|', ']', ''', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), htmlspecialchars($text) ); return $text; @@ -1029,6 +1145,34 @@ function wfArrayToCGI( $array1, $array2 = NULL ) } /** + * This is the logical opposite of wfArrayToCGI(): it accepts a query string as + * its argument and returns the same string in array form. This allows compa- + * tibility with legacy functions that accept raw query strings instead of nice + * arrays. Of course, keys and values are urldecode()d. Don't try passing in- + * valid query strings, or it will explode. + * + * @param $query string Query string + * @return array Array version of input + */ +function wfCgiToArray( $query ) { + if( isset( $query[0] ) and $query[0] == '?' ) { + $query = substr( $query, 1 ); + } + $bits = explode( '&', $query ); + $ret = array(); + foreach( $bits as $bit ) { + if( $bit === '' ) { + continue; + } + list( $key, $value ) = explode( '=', $bit ); + $key = urldecode( $key ); + $value = urldecode( $value ); + $ret[$key] = $value; + } + return $ret; +} + +/** * Append a query string to an existing URL, which may or may not already * have query string parameters already. If so, they will be combined. * @@ -1132,7 +1276,7 @@ function wfMerge( $old, $mine, $yours, &$result ){ # This check may also protect against code injection in # case of broken installations. - if(! file_exists( $wgDiff3 ) ){ + if( !$wgDiff3 || !file_exists( $wgDiff3 ) ) { wfDebug( "diff3 not found\n" ); return false; } @@ -1246,7 +1390,10 @@ function wfDiff( $before, $after, $params = '-u' ) { } /** - * @todo document + * A wrapper around the PHP function var_export(). + * Either print it or add it to the regular output ($wgOut). + * + * @param $var A PHP variable to dump. */ function wfVarDump( $var ) { global $wgOut; @@ -1322,6 +1469,7 @@ function wfResetOutputBuffers( $resetGzipEncoding=true ) { // Reset the 'Content-Encoding' field set by this handler // so we can start fresh. header( 'Content-Encoding:' ); + break; } } } @@ -1568,11 +1716,11 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { # TS_ORACLE $uts = strtotime(preg_replace('/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", str_replace("+00:00", "UTC", $ts))); - } elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/', $ts, $da)) { + } elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z$/', $ts, $da)) { # TS_ISO_8601 - } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)[\+\- ](\d\d)$/',$ts,$da)) { + } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/',$ts,$da)) { # TS_POSTGRES - } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/',$ts,$da)) { + } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/',$ts,$da)) { # TS_POSTGRES } else { # Bogus value; fall back to the epoch... @@ -1648,7 +1796,7 @@ function swap( &$x, &$y ) { } function wfGetCachedNotice( $name ) { - global $wgOut, $parserMemc; + global $wgOut, $wgRenderHashAppend, $parserMemc; $fname = 'wfGetCachedNotice'; wfProfileIn( $fname ); @@ -1670,7 +1818,9 @@ function wfGetCachedNotice( $name ) { } } - $cachedNotice = $parserMemc->get( wfMemcKey( $name ) ); + // Use the extra hash appender to let eg SSL variants separately cache. + $key = wfMemcKey( $name . $wgRenderHashAppend ); + $cachedNotice = $parserMemc->get( $key ); if( is_array( $cachedNotice ) ) { if( md5( $notice ) == $cachedNotice['hash'] ) { $notice = $cachedNotice['html']; @@ -1684,7 +1834,7 @@ function wfGetCachedNotice( $name ) { if( $needParse ) { if( is_object( $wgOut ) ) { $parsed = $wgOut->parse( $notice ); - $parserMemc->set( wfMemcKey( $name ), array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); + $parserMemc->set( $key, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); $notice = $parsed; } else { wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' ); @@ -1777,69 +1927,20 @@ function wfTempDir() { /** * Make directory, and make all parent directories if they don't exist * - * @param string $fullDir Full path to directory to create + * @param string $dir Full path to directory to create * @param int $mode Chmod value to use, default is $wgDirectoryMode * @return bool */ -function wfMkdirParents( $fullDir, $mode = null ) { +function wfMkdirParents( $dir, $mode = null ) { global $wgDirectoryMode; - if( strval( $fullDir ) === '' ) - return true; - if( file_exists( $fullDir ) ) - return true; - // If not defined or isn't an int, set to default - if ( is_null( $mode ) ) { - $mode = $wgDirectoryMode; - } - - - # Go back through the paths to find the first directory that exists - $currentDir = $fullDir; - $createList = array(); - while ( strval( $currentDir ) !== '' && !file_exists( $currentDir ) ) { - # Strip trailing slashes - $currentDir = rtrim( $currentDir, '/\\' ); - # Add to create list - $createList[] = $currentDir; - - # Find next delimiter searching from the end - $p = max( strrpos( $currentDir, '/' ), strrpos( $currentDir, '\\' ) ); - if ( $p === false ) { - $currentDir = false; - } else { - $currentDir = substr( $currentDir, 0, $p ); - } - } - - if ( count( $createList ) == 0 ) { - # Directory specified already exists + if( strval( $dir ) === '' || file_exists( $dir ) ) return true; - } elseif ( $currentDir === false ) { - # Went all the way back to root and it apparently doesn't exist - wfDebugLog( 'mkdir', "Root doesn't exist?\n" ); - return false; - } - # Now go forward creating directories - $createList = array_reverse( $createList ); - # Is the parent directory writable? - if ( $currentDir === '' ) { - $currentDir = '/'; - } - if ( !is_writable( $currentDir ) ) { - wfDebugLog( 'mkdir', "Not writable: $currentDir\n" ); - return false; - } + if ( is_null( $mode ) ) + $mode = $wgDirectoryMode; - foreach ( $createList as $dir ) { - # use chmod to override the umask, as suggested by the PHP manual - if ( !mkdir( $dir, $mode ) || !chmod( $dir, $mode ) ) { - wfDebugLog( 'mkdir', "Unable to create directory $dir\n" ); - return false; - } - } - return true; + return mkdir( $dir, $mode, true ); // PHP5 <3 } /** @@ -1998,7 +2099,7 @@ function wfIniGetBool( $setting ) { * @return collected stdout as a string (trailing newlines stripped) */ function wfShellExec( $cmd, &$retval=null ) { - global $IP, $wgMaxShellMemory, $wgMaxShellFileSize; + global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime; if( wfIniGetBool( 'safe_mode' ) ) { wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); @@ -2008,7 +2109,7 @@ function wfShellExec( $cmd, &$retval=null ) { wfInitShellLocale(); if ( php_uname( 's' ) == 'Linux' ) { - $time = intval( ini_get( 'max_execution_time' ) ); + $time = intval( $wgMaxShellTime ); $mem = intval( $wgMaxShellMemory ); $filesize = intval( $wgMaxShellFileSize ); @@ -2030,6 +2131,10 @@ function wfShellExec( $cmd, &$retval=null ) { passthru( $cmd, $retval ); $output = ob_get_contents(); ob_end_clean(); + + if ( $retval == 127 ) { + wfDebugLog( 'exec', "Possibly missing executable file: $cmd\n" ); + } return $output; } @@ -2167,28 +2272,51 @@ function wfRelativePath( $path, $from ) { } /** - * array_merge() does awful things with "numeric" indexes, including - * string indexes when happen to look like integers. When we want - * to merge arrays with arbitrary string indexes, we don't want our - * arrays to be randomly corrupted just because some of them consist - * of numbers. - * - * Fuck you, PHP. Fuck you in the ear! + * Backwards array plus for people who haven't bothered to read the PHP manual + * XXX: will not darn your socks for you. * * @param array $array1, [$array2, [...]] * @return array */ function wfArrayMerge( $array1/* ... */ ) { - $out = $array1; - for( $i = 1; $i < func_num_args(); $i++ ) { - foreach( func_get_arg( $i ) as $key => $value ) { - $out[$key] = $value; - } + $args = func_get_args(); + $args = array_reverse( $args, true ); + $out = array(); + foreach ( $args as $arg ) { + $out += $arg; } return $out; } /** + * Merge arrays in the style of getUserPermissionsErrors, with duplicate removal + * e.g. + * wfMergeErrorArrays( + * array( array( 'x' ) ), + * array( array( 'x', '2' ) ), + * array( array( 'x' ) ), + * array( array( 'y') ) + * ); + * returns: + * array( + * array( 'x', '2' ), + * array( 'x' ), + * array( 'y' ) + * ) + */ +function wfMergeErrorArrays(/*...*/) { + $args = func_get_args(); + $out = array(); + foreach ( $args as $errors ) { + foreach ( $errors as $params ) { + $spec = implode( "\t", $params ); + $out[$spec] = $params; + } + } + return array_values( $out ); +} + +/** * Make a URL index, appropriate for the el_index field of externallinks. */ function wfMakeUrlIndex( $url ) { @@ -2560,7 +2688,7 @@ function wfSplitWikiID( $wiki ) { * will always return the same object, unless the underlying connection or load * balancer is manually destroyed. */ -function &wfGetDB( $db = DB_LAST, $groups = array(), $wiki = false ) { +function &wfGetDB( $db, $groups = array(), $wiki = false ) { return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki ); } @@ -2590,10 +2718,15 @@ function &wfGetLBFactory() { * current version. An image object will be returned which * was created at the specified time. * @param mixed $flags FileRepo::FIND_ flags + * @param boolean $bypass Bypass the file cache even if it could be used * @return File, or false if the file does not exist */ -function wfFindFile( $title, $time = false, $flags = 0 ) { - return RepoGroup::singleton()->findFile( $title, $time, $flags ); +function wfFindFile( $title, $time = false, $flags = 0, $bypass = false ) { + if( !$time && !$flags && !$bypass ) { + return FileCache::singleton()->findFile( $title ); + } else { + return RepoGroup::singleton()->findFile( $title, $time, $flags ); + } } /** @@ -2646,6 +2779,8 @@ function wfBoolToStr( $value ) { * @param string $extensionName Name of extension to load messages from\for. * @param string $langcode Language to load messages for, or false for default * behvaiour (en, content language and user language). + * @since r24808 (v1.11) Using this method of loading extension messages will not work + * on MediaWiki prior to that */ function wfLoadExtensionMessages( $extensionName, $langcode = false ) { global $wgExtensionMessagesFiles, $wgMessageCache, $wgLang, $wgContLang; diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php index 1f250214..402102ea 100644 --- a/includes/HTMLCacheUpdate.php +++ b/includes/HTMLCacheUpdate.php @@ -37,7 +37,7 @@ class HTMLCacheUpdate $this->mRowsPerQuery = $wgUpdateRowsPerQuery; } - function doUpdate() { + public function doUpdate() { # Fetch the IDs $cond = $this->getToCondition(); $dbr = wfGetDB( DB_SLAVE ); @@ -50,16 +50,17 @@ class HTMLCacheUpdate $this->invalidateIDs( $res ); } } + wfRunHooks( 'HTMLCacheUpdate::doUpdate', array($this->mTitle) ); } - function insertJobs( ResultWrapper $res ) { + protected function insertJobs( ResultWrapper $res ) { $numRows = $res->numRows(); $numBatches = ceil( $numRows / $this->mRowsPerJob ); $realBatchSize = $numRows / $numBatches; $start = false; $jobs = array(); do { - for ( $i = 0; $i < $realBatchSize - 1; $i++ ) { + for ( $i = 0; $i <= $realBatchSize - 1; $i++ ) { $row = $res->fetchRow(); if ( $row ) { $id = $row[0]; @@ -82,17 +83,13 @@ class HTMLCacheUpdate Job::batchInsert( $jobs ); } - function getPrefix() { + protected function getPrefix() { static $prefixes = array( 'pagelinks' => 'pl', 'imagelinks' => 'il', 'categorylinks' => 'cl', 'templatelinks' => 'tl', 'redirect' => 'rd', - - # Not needed - # 'externallinks' => 'el', - # 'langlinks' => 'll' ); if ( is_null( $this->mPrefix ) ) { @@ -104,11 +101,11 @@ class HTMLCacheUpdate return $this->mPrefix; } - function getFromField() { + public function getFromField() { return $this->getPrefix() . '_from'; } - function getToCondition() { + public function getToCondition() { $prefix = $this->getPrefix(); switch ( $this->mTable ) { case 'pagelinks': @@ -129,7 +126,7 @@ class HTMLCacheUpdate /** * Invalidate a set of IDs, right now */ - function invalidateIDs( ResultWrapper $res ) { + public function invalidateIDs( ResultWrapper $res ) { global $wgUseFileCache, $wgUseSquid; if ( $res->numRows() == 0 ) { @@ -175,8 +172,7 @@ class HTMLCacheUpdate # Update file cache if ( $wgUseFileCache ) { foreach ( $titles as $title ) { - $cm = new HTMLFileCache($title); - @unlink($cm->fileCacheName()); + HTMLFileCache::clearFileCache( $title ); } } } @@ -185,7 +181,9 @@ class HTMLCacheUpdate } /** - * @todo document (e.g. one-sentence top-level class description). + * Job wrapper for HTMLCacheUpdate. Gets run whenever a related + * job gets called from the queue. + * * @ingroup JobQueue */ class HTMLCacheUpdateJob extends Job { @@ -204,7 +202,7 @@ class HTMLCacheUpdateJob extends Job { $this->end = $params['end']; } - function run() { + public function run() { $update = new HTMLCacheUpdate( $this->title, $this->table ); $fromField = $update->getFromField(); diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php index ba2196eb..e267962c 100644 --- a/includes/HTMLFileCache.php +++ b/includes/HTMLFileCache.php @@ -20,25 +20,29 @@ * @ingroup Cache */ class HTMLFileCache { - var $mTitle, $mFileCache; + var $mTitle, $mFileCache, $mType; - function HTMLFileCache( &$title ) { - $this->mTitle =& $title; - $this->mFileCache = ''; + public function __construct( &$title, $type = 'view' ) { + $this->mTitle = $title; + $this->mType = ($type == 'raw' || $type == 'view' ) ? $type : false; + $this->fileCacheName(); // init name } - function fileCacheName() { - global $wgFileCacheDirectory; + public function fileCacheName() { if( !$this->mFileCache ) { + global $wgFileCacheDirectory, $wgRequest; + # Store raw pages (like CSS hits) elsewhere + $subdir = ($this->mType === 'raw') ? 'raw/' : ''; $key = $this->mTitle->getPrefixedDbkey(); $hash = md5( $key ); + # Avoid extension confusion $key = str_replace( '.', '%2E', urlencode( $key ) ); - + $hash1 = substr( $hash, 0, 1 ); $hash2 = substr( $hash, 0, 2 ); - $this->mFileCache = "{$wgFileCacheDirectory}/{$hash1}/{$hash2}/{$key}.html"; + $this->mFileCache = "{$wgFileCacheDirectory}/{$subdir}{$hash1}/{$hash2}/{$key}.html"; - if($this->useGzip()) + if( $this->useGzip() ) $this->mFileCache .= '.gz'; wfDebug( " fileCacheName() - {$this->mFileCache}\n" ); @@ -46,38 +50,72 @@ class HTMLFileCache { return $this->mFileCache; } - function isFileCached() { + public function isFileCached() { + if( $this->mType === false ) return false; return file_exists( $this->fileCacheName() ); } - function fileCacheTime() { + public function fileCacheTime() { return wfTimestamp( TS_MW, filemtime( $this->fileCacheName() ) ); } + + /** + * Check if pages can be cached for this request/user + * @return bool + */ + public static function useFileCache() { + global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest, $wgLang, $wgContLang; + if( !$wgUseFileCache ) return false; + // Get all query values + $queryVals = $wgRequest->getValues(); + foreach( $queryVals as $query => $val ) { + if( $query == 'title' || $query == 'curid' ) continue; + // Normal page view in query form can have action=view. + // Raw hits for pages also stored, like .css pages for example. + else if( $query == 'action' && ($val == 'view' || $val == 'raw') ) continue; + else if( $query == 'usemsgcache' && $val == 'yes' ) continue; + // Below are header setting params + else if( $query == 'maxage' || $query == 'smaxage' || $query == 'ctype' || $query == 'gen' ) + continue; + else + return false; + } + // Check for non-standard user language; this covers uselang, + // and extensions for auto-detecting user language. + $ulang = $wgLang->getCode(); + $clang = $wgContLang->getCode(); + // Check that there are no other sources of variation + return !$wgShowIPinHeader && !$wgUser->getId() && !$wgUser->getNewtalk() && $ulang == $clang; + } - function isFileCacheGood( $timestamp ) { + /* + * Check if up to date cache file exists + * @param $timestamp string + */ + public function isFileCacheGood( $timestamp = '' ) { global $wgCacheEpoch; if( !$this->isFileCached() ) return false; + if( !$timestamp ) return true; // should be invalidated on change $cachetime = $this->fileCacheTime(); - $good = (( $timestamp <= $cachetime ) && - ( $wgCacheEpoch <= $cachetime )); + $good = $timestamp <= $cachetime && $wgCacheEpoch <= $cachetime; - wfDebug(" isFileCacheGood() - cachetime $cachetime, touched {$timestamp} epoch {$wgCacheEpoch}, good $good\n"); + wfDebug(" isFileCacheGood() - cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n"); return $good; } - function useGzip() { + public function useGzip() { global $wgUseGzip; return $wgUseGzip; } /* In handy string packages */ - function fetchRawText() { + public function fetchRawText() { return file_get_contents( $this->fileCacheName() ); } - function fetchPageText() { + public function fetchPageText() { if( $this->useGzip() ) { /* Why is there no gzfile_get_contents() or gzdecode()? */ return implode( '', gzfile( $this->fileCacheName() ) ); @@ -87,15 +125,18 @@ class HTMLFileCache { } /* Working directory to/from output */ - function loadFromFileCache() { + public 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" ); + $filename = $this->fileCacheName(); + // Raw pages should handle cache control on their own, + // even when using file cache. This reduces hits from clients. + if( $this->mType !== 'raw' ) { + $wgOut->sendCacheControl(); + header( "Content-Type: $wgMimeType; charset={$wgOutputEncoding}" ); + header( "Content-Language: $wgContLanguageCode" ); + } if( $this->useGzip() ) { if( wfClientAcceptsGzip() ) { @@ -109,18 +150,22 @@ class HTMLFileCache { readfile( $filename ); } - function checkCacheDirs() { + protected function checkCacheDirs() { $filename = $this->fileCacheName(); - $mydir2=substr($filename,0,strrpos($filename,'/')); # subdirectory level 2 - $mydir1=substr($mydir2,0,strrpos($mydir2,'/')); # subdirectory level 1 + $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); } + wfMkdirParents( $mydir1 ); + wfMkdirParents( $mydir2 ); } - function saveToFileCache( $origtext ) { + public function saveToFileCache( $origtext ) { + global $wgUseFileCache; + if( !$wgUseFileCache ) { + return $origtext; // return to output + } $text = $origtext; - if(strcmp($text,'') == 0) return ''; + if( strcmp($text,'') == 0 ) return ''; wfDebug(" saveToFileCache()\n", false); @@ -155,4 +200,13 @@ class HTMLFileCache { return $text; } + public static function clearFileCache( $title ) { + global $wgUseFileCache; + if( !$wgUseFileCache ) return false; + $fc = new self( $title, 'view' ); + @unlink( $fc->fileCacheName() ); + $fc = new self( $title, 'raw' ); + @unlink( $fc->fileCacheName() ); + return true; + } } diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index 3772926d..664ceb4f 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -1,41 +1,33 @@ <?php /** - * Pure virtual parent - * @todo document (needs a one-sentence top-level class description, that answers the question: "what is a HistoryBlob?") + * Base class for general text storage via the "object" flag in old_flags, or + * two-part external storage URLs. Used for represent efficient concatenated + * storage, and migration-related pointer objects. */ interface HistoryBlob { /** - * setMeta and getMeta currently aren't used for anything, I just thought - * they might be useful in the future. - * @param $meta String: a single string. - */ - public function setMeta( $meta ); - - /** - * setMeta and getMeta currently aren't used for anything, I just thought - * they might be useful in the future. - * Gets the meta-value - */ - public function getMeta(); - - /** * Adds an item of text, returns a stub object which points to the item. * You must call setLocation() on the stub object before storing it to the * database + * Returns the key for getItem() */ public function addItem( $text ); /** - * Get item by hash + * Get item by key, or false if the key is not present */ - public function getItem( $hash ); + public function getItem( $key ); - # Set the "default text" - # This concept is an odd property of the current DB schema, whereby each text item has a revision - # associated with it. The default text is the text of the associated revision. There may, however, - # be other revisions in the same object + /** + * Set the "default text" + * This concept is an odd property of the current DB schema, whereby each text item has a revision + * associated with it. The default text is the text of the associated revision. There may, however, + * be other revisions in the same object. + * + * Default text is not required for two-part external storage URLs. + */ public function setText( $text ); /** @@ -45,13 +37,15 @@ interface HistoryBlob } /** - * The real object - * @todo document (needs one-sentence top-level class description + function descriptions). + * Concatenated gzip (CGZ) storage + * Improves compression ratio by concatenating like objects before gzipping */ class ConcatenatedGzipHistoryBlob implements HistoryBlob { public $mVersion = 0, $mCompressed = false, $mItems = array(), $mDefaultHash = ''; - public $mFast = 0, $mSize = 0; + public $mSize = 0; + public $mMaxSize = 10000000; + public $mMaxCount = 100; /** Constructor */ public function ConcatenatedGzipHistoryBlob() { @@ -60,34 +54,16 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob } } - # - # HistoryBlob implementation: - # - - /** @todo document */ - public function setMeta( $metaData ) { - $this->uncompress(); - $this->mItems['meta'] = $metaData; - } - - /** @todo document */ - public function getMeta() { - $this->uncompress(); - return $this->mItems['meta']; - } - - /** @todo document */ public function addItem( $text ) { $this->uncompress(); $hash = md5( $text ); - $this->mItems[$hash] = $text; - $this->mSize += strlen( $text ); - - $stub = new HistoryBlobStub( $hash ); - return $stub; + if ( !isset( $this->mItems[$hash] ) ) { + $this->mItems[$hash] = $text; + $this->mSize += strlen( $text ); + } + return $hash; } - /** @todo document */ public function getItem( $hash ) { $this->uncompress(); if ( array_key_exists( $hash, $this->mItems ) ) { @@ -97,29 +73,27 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob } } - /** @todo document */ public function setText( $text ) { $this->uncompress(); - $stub = $this->addItem( $text ); - $this->mDefaultHash = $stub->mHash; + $this->mDefaultHash = $this->addItem( $text ); } - /** @todo document */ public function getText() { $this->uncompress(); return $this->getItem( $this->mDefaultHash ); } - # HistoryBlob implemented. - - - /** @todo document */ + /** + * Remove an item + */ public function removeItem( $hash ) { $this->mSize -= strlen( $this->mItems[$hash] ); unset( $this->mItems[$hash] ); } - /** @todo document */ + /** + * Compress the bulk data in the object + */ public function compress() { if ( !$this->mCompressed ) { $this->mItems = gzdeflate( serialize( $this->mItems ) ); @@ -127,7 +101,9 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob } } - /** @todo document */ + /** + * Uncompress bulk data + */ public function uncompress() { if ( $this->mCompressed ) { $this->mItems = unserialize( gzinflate( $this->mItems ) ); @@ -136,39 +112,22 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob } - /** @todo document */ function __sleep() { $this->compress(); return array( 'mVersion', 'mCompressed', 'mItems', 'mDefaultHash' ); } - /** @todo document */ function __wakeup() { $this->uncompress(); } /** - * Determines if this object is happy + * Helper function for compression jobs + * Returns true until the object is "full" and ready to be committed */ - public function isHappy( $maxFactor, $factorThreshold ) { - if ( count( $this->mItems ) == 0 ) { - return true; - } - if ( !$this->mFast ) { - $this->uncompress(); - $record = serialize( $this->mItems ); - $size = strlen( $record ); - $avgUncompressed = $size / count( $this->mItems ); - $compressed = strlen( gzdeflate( $record ) ); - - if ( $compressed < $factorThreshold * 1024 ) { - return true; - } else { - return $avgUncompressed * $maxFactor < $compressed; - } - } else { - return count( $this->mItems ) <= 10; - } + public function isHappy() { + return $this->mSize < $this->mMaxSize + && count( $this->mItems ) < $this->mMaxCount; } } @@ -184,12 +143,15 @@ $wgBlobCache = array(); /** - * @todo document (needs one-sentence top-level class description + some function descriptions). + * Pointer object for an item within a CGZ blob stored in the text table. */ class HistoryBlobStub { var $mOldId, $mHash, $mRef; - /** @todo document */ + /** + * @param string $hash The content hash of the text + * @param integer $oldid The old_id for the CGZ object + */ function HistoryBlobStub( $hash = '', $oldid = 0 ) { $this->mHash = $hash; } @@ -216,7 +178,6 @@ class HistoryBlobStub { return $this->mRef; } - /** @todo document */ function getText() { $fname = 'HistoryBlobStub::getText'; global $wgBlobCache; @@ -264,7 +225,9 @@ class HistoryBlobStub { return $obj->getItem( $this->mHash ); } - /** @todo document */ + /** + * Get the content hash + */ function getHash() { return $this->mHash; } @@ -282,7 +245,9 @@ class HistoryBlobStub { class HistoryBlobCurStub { var $mCurId; - /** @todo document */ + /** + * @param integer $curid The cur_id pointed to + */ function HistoryBlobCurStub( $curid = 0 ) { $this->mCurId = $curid; } @@ -295,7 +260,6 @@ class HistoryBlobCurStub { $this->mCurId = $id; } - /** @todo document */ function getText() { $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'cur', array( 'cur_text' ), array( 'cur_id' => $this->mCurId ) ); @@ -305,3 +269,311 @@ class HistoryBlobCurStub { return $row->cur_text; } } + +/** + * Diff-based history compression + * Requires xdiff 1.5+ and zlib + */ +class DiffHistoryBlob implements HistoryBlob { + /** Uncompressed item cache */ + var $mItems = array(); + + /** Total uncompressed size */ + var $mSize = 0; + + /** + * Array of diffs. If a diff D from A to B is notated D = B - A, and Z is + * an empty string: + * + * { item[map[i]] - item[map[i-1]] where i > 0 + * diff[i] = { + * { item[map[i]] - Z where i = 0 + */ + var $mDiffs; + + /** The diff map, see above */ + var $mDiffMap; + + /** + * The key for getText() + */ + var $mDefaultKey; + + /** + * Compressed storage + */ + var $mCompressed; + + /** + * True if the object is locked against further writes + */ + var $mFrozen = false; + + /** + * The maximum uncompressed size before the object becomes sad + * Should be less than max_allowed_packet + */ + var $mMaxSize = 10000000; + + /** + * The maximum number of text items before the object becomes sad + */ + var $mMaxCount = 100; + + /** Constants from xdiff.h */ + const XDL_BDOP_INS = 1; + const XDL_BDOP_CPY = 2; + const XDL_BDOP_INSB = 3; + + function __construct() { + if ( !function_exists( 'gzdeflate' ) ) { + throw new MWException( "Need zlib support to read or write DiffHistoryBlob\n" ); + } + } + + function addItem( $text ) { + if ( $this->mFrozen ) { + throw new MWException( __METHOD__.": Cannot add more items after sleep/wakeup" ); + } + + $this->mItems[] = $text; + $this->mSize += strlen( $text ); + $this->mDiffs = null; // later + return count( $this->mItems ) - 1; + } + + function getItem( $key ) { + return $this->mItems[$key]; + } + + function setText( $text ) { + $this->mDefaultKey = $this->addItem( $text ); + } + + function getText() { + return $this->getItem( $this->mDefaultKey ); + } + + function compress() { + if ( !function_exists( 'xdiff_string_rabdiff' ) ){ + throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" ); + } + if ( isset( $this->mDiffs ) ) { + // Already compressed + return; + } + if ( !count( $this->mItems ) ) { + // Empty + return; + } + + // Create two diff sequences: one for main text and one for small text + $sequences = array( + 'small' => array( + 'tail' => '', + 'diffs' => array(), + 'map' => array(), + ), + 'main' => array( + 'tail' => '', + 'diffs' => array(), + 'map' => array(), + ), + ); + $smallFactor = 0.5; + + for ( $i = 0; $i < count( $this->mItems ); $i++ ) { + $text = $this->mItems[$i]; + if ( $i == 0 ) { + $seqName = 'main'; + } else { + $mainTail = $sequences['main']['tail']; + if ( strlen( $text ) < strlen( $mainTail ) * $smallFactor ) { + $seqName = 'small'; + } else { + $seqName = 'main'; + } + } + $seq =& $sequences[$seqName]; + $tail = $seq['tail']; + $diff = $this->diff( $tail, $text ); + $seq['diffs'][] = $diff; + $seq['map'][] = $i; + $seq['tail'] = $text; + } + unset( $seq ); // unlink dangerous alias + + // Knit the sequences together + $tail = ''; + $this->mDiffs = array(); + $this->mDiffMap = array(); + foreach ( $sequences as $seq ) { + if ( !count( $seq['diffs'] ) ) { + continue; + } + if ( $tail === '' ) { + $this->mDiffs[] = $seq['diffs'][0]; + } else { + $head = $this->patch( '', $seq['diffs'][0] ); + $this->mDiffs[] = $this->diff( $tail, $head ); + } + $this->mDiffMap[] = $seq['map'][0]; + for ( $i = 1; $i < count( $seq['diffs'] ); $i++ ) { + $this->mDiffs[] = $seq['diffs'][$i]; + $this->mDiffMap[] = $seq['map'][$i]; + } + $tail = $seq['tail']; + } + } + + function diff( $t1, $t2 ) { + # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff + # "String is not zero-terminated" + wfSuppressWarnings(); + $diff = xdiff_string_rabdiff( $t1, $t2 ) . ''; + wfRestoreWarnings(); + return $diff; + } + + function patch( $base, $diff ) { + if ( function_exists( 'xdiff_string_bpatch' ) ) { + wfSuppressWarnings(); + $text = xdiff_string_bpatch( $base, $diff ) . ''; + wfRestoreWarnings(); + return $text; + } + + # Pure PHP implementation + + $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) ); + + # Check the checksum if mhash is available + if ( extension_loaded( 'mhash' ) ) { + $ofp = mhash( MHASH_ADLER32, $base ); + if ( $ofp !== substr( $diff, 0, 4 ) ) { + wfDebug( __METHOD__. ": incorrect base checksum\n" ); + return false; + } + } + if ( $header['csize'] != strlen( $base ) ) { + wfDebug( __METHOD__. ": incorrect base length\n" ); + return false; + } + + $p = 8; + $out = ''; + while ( $p < strlen( $diff ) ) { + $x = unpack( 'Cop', substr( $diff, $p, 1 ) ); + $op = $x['op']; + ++$p; + switch ( $op ) { + case self::XDL_BDOP_INS: + $x = unpack( 'Csize', substr( $diff, $p, 1 ) ); + $p++; + $out .= substr( $diff, $p, $x['size'] ); + $p += $x['size']; + break; + case self::XDL_BDOP_INSB: + $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) ); + $p += 4; + $out .= substr( $diff, $p, $x['csize'] ); + $p += $x['csize']; + break; + case self::XDL_BDOP_CPY: + $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) ); + $p += 8; + $out .= substr( $base, $x['off'], $x['csize'] ); + break; + default: + wfDebug( __METHOD__.": invalid op\n" ); + return false; + } + } + return $out; + } + + function uncompress() { + if ( !$this->mDiffs ) { + return; + } + $tail = ''; + for ( $diffKey = 0; $diffKey < count( $this->mDiffs ); $diffKey++ ) { + $textKey = $this->mDiffMap[$diffKey]; + $text = $this->patch( $tail, $this->mDiffs[$diffKey] ); + $this->mItems[$textKey] = $text; + $tail = $text; + } + } + + function __sleep() { + $this->compress(); + if ( !count( $this->mItems ) ) { + // Empty object + $info = false; + } else { + // Take forward differences to improve the compression ratio for sequences + $map = ''; + $prev = 0; + foreach ( $this->mDiffMap as $i ) { + if ( $map !== '' ) { + $map .= ','; + } + $map .= $i - $prev; + $prev = $i; + } + $info = array( + 'diffs' => $this->mDiffs, + 'map' => $map + ); + } + if ( isset( $this->mDefaultKey ) ) { + $info['default'] = $this->mDefaultKey; + } + $this->mCompressed = gzdeflate( serialize( $info ) ); + return array( 'mCompressed' ); + } + + function __wakeup() { + // addItem() doesn't work if mItems is partially filled from mDiffs + $this->mFrozen = true; + $info = unserialize( gzinflate( $this->mCompressed ) ); + unset( $this->mCompressed ); + + if ( !$info ) { + // Empty object + return; + } + + if ( isset( $info['default'] ) ) { + $this->mDefaultKey = $info['default']; + } + $this->mDiffs = $info['diffs']; + if ( isset( $info['base'] ) ) { + // Old format + $this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 ); + array_unshift( $this->mDiffs, + pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) . + $info['base'] ); + } else { + // New format + $map = explode( ',', $info['map'] ); + $cur = 0; + $this->mDiffMap = array(); + foreach ( $map as $i ) { + $cur += $i; + $this->mDiffMap[] = $cur; + } + } + $this->uncompress(); + } + + /** + * Helper function for compression jobs + * Returns true until the object is "full" and ready to be committed + */ + function isHappy() { + return $this->mSize < $this->mMaxSize + && count( $this->mItems ) < $this->mMaxCount; + } + +} diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index 555a79b7..269d45ff 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -1,24 +1,48 @@ <?php /** + * @defgroup HTTP HTTP + * @file + * @ingroup HTTP + */ + +/** * Various HTTP related functions + * @ingroup HTTP */ class Http { - static function get( $url, $timeout = 'default' ) { - return Http::request( "GET", $url, $timeout ); + + /** + * Simple wrapper for Http::request( 'GET' ) + * @see Http::request() + */ + public static function get( $url, $timeout = 'default', $opts = array() ) { + return Http::request( "GET", $url, $timeout, $opts ); } - static function post( $url, $timeout = 'default' ) { - return Http::request( "POST", $url, $timeout ); + /** + * Simple wrapper for Http::request( 'POST' ) + * @see Http::request() + */ + public static function post( $url, $timeout = 'default', $opts = array() ) { + return Http::request( "POST", $url, $timeout, $opts ); } /** * Get the contents of a file by HTTP - * - * if $timeout is 'default', $wgHTTPTimeout is used + * @param $method string HTTP method. Usually GET/POST + * @param $url string Full URL to act on + * @param $timeout int Seconds to timeout. 'default' falls to $wgHTTPTimeout + * @param $curlOptions array Optional array of extra params to pass + * to curl_setopt() */ - static function request( $method, $url, $timeout = 'default' ) { - global $wgHTTPTimeout, $wgHTTPProxy, $wgVersion, $wgTitle; + public static function request( $method, $url, $timeout = 'default', $curlOptions = array() ) { + global $wgHTTPTimeout, $wgHTTPProxy, $wgTitle; + + // Go ahead and set the timeout if not otherwise specified + if ( $timeout == 'default' ) { + $timeout = $wgHTTPTimeout; + } wfDebug( __METHOD__ . ": $method $url\n" ); # Use curl if available @@ -30,13 +54,12 @@ class Http { curl_setopt($c, CURLOPT_PROXY, $wgHTTPProxy); } - if ( $timeout == 'default' ) { - $timeout = $wgHTTPTimeout; - } curl_setopt( $c, CURLOPT_TIMEOUT, $timeout ); - curl_setopt( $c, CURLOPT_USERAGENT, "MediaWiki/$wgVersion" ); - if ( $method == 'POST' ) + curl_setopt( $c, CURLOPT_USERAGENT, self :: userAgent() ); + if ( $method == 'POST' ) { curl_setopt( $c, CURLOPT_POST, true ); + curl_setopt( $c, CURLOPT_POSTFIELDS, '' ); + } else curl_setopt( $c, CURLOPT_CUSTOMREQUEST, $method ); @@ -48,6 +71,12 @@ class Http { if ( is_object( $wgTitle ) ) { curl_setopt( $c, CURLOPT_REFERER, $wgTitle->getFullURL() ); } + + if ( is_array( $curlOptions ) ) { + foreach( $curlOptions as $option => $value ) { + curl_setopt( $c, $option, $value ); + } + } ob_start(); curl_exec( $c ); @@ -55,20 +84,24 @@ class Http { ob_end_clean(); # Don't return the text of error messages, return false on error - if ( curl_getinfo( $c, CURLINFO_HTTP_CODE ) != 200 ) { + $retcode = curl_getinfo( $c, CURLINFO_HTTP_CODE ); + if ( $retcode != 200 ) { + wfDebug( __METHOD__ . ": HTTP return code $retcode\n" ); $text = false; } # Don't return truncated output - if ( curl_errno( $c ) != CURLE_OK ) { + $errno = curl_errno( $c ); + if ( $errno != CURLE_OK ) { + $errstr = curl_error( $c ); + wfDebug( __METHOD__ . ": CURL error code $errno: $errstr\n" ); $text = false; } curl_close( $c ); } else { # Otherwise use file_get_contents... - # This may take 3 minutes to time out, and doesn't have local fetch capabilities + # This doesn't have local fetch capabilities... - global $wgVersion; - $headers = array( "User-Agent: MediaWiki/$wgVersion" ); + $headers = array( "User-Agent: " . self :: userAgent() ); if( strcasecmp( $method, 'post' ) == 0 ) { // Required for HTTP 1.0 POSTs $headers[] = "Content-Length: 0"; @@ -76,20 +109,21 @@ class Http { $opts = array( 'http' => array( 'method' => $method, - 'header' => implode( "\r\n", $headers ) ) ); + 'header' => implode( "\r\n", $headers ), + 'timeout' => $timeout ) ); $ctx = stream_context_create($opts); - $url_fopen = ini_set( 'allow_url_fopen', 1 ); $text = file_get_contents( $url, false, $ctx ); - ini_set( 'allow_url_fopen', $url_fopen ); } return $text; } /** * Check if the URL can be served by localhost + * @param $url string Full url to check + * @return bool */ - static function isLocalURL( $url ) { + public static function isLocalURL( $url ) { global $wgCommandLineMode, $wgConf; if ( $wgCommandLineMode ) { return false; @@ -117,4 +151,12 @@ class Http { } return false; } + + /** + * Return a standard user-agent we can use for external requests. + */ + public static function userAgent() { + global $wgVersion; + return "MediaWiki/$wgVersion"; + } } diff --git a/includes/IEContentAnalyzer.php b/includes/IEContentAnalyzer.php index 59abc6a6..df4d36f0 100644 --- a/includes/IEContentAnalyzer.php +++ b/includes/IEContentAnalyzer.php @@ -569,8 +569,9 @@ class IEContentAnalyzer { $chunk3 = substr( $chunk, 0, 3 ); $chunk4 = substr( $chunk, 0, 4 ); $chunk5 = substr( $chunk, 0, 5 ); + $chunk5uc = strtoupper( $chunk5 ); $chunk8 = substr( $chunk, 0, 8 ); - if ( $chunk5 == 'GIF87' || $chunk5 == 'GIF89' ) { + if ( $chunk5uc == 'GIF87' || $chunk5uc == 'GIF89' ) { return 'image/gif'; } if ( $chunk2 == "\xff\xd8" ) { @@ -579,7 +580,7 @@ class IEContentAnalyzer { if ( $chunk2 == 'BM' && substr( $chunk, 6, 2 ) == "\000\000" - && substr( $chunk, 8, 2 ) != "\000\000" ) + && substr( $chunk, 8, 2 ) == "\000\000" ) { return 'image/bmp'; // another non-standard MIME } @@ -800,7 +801,7 @@ class IEContentAnalyzer { } // BinHex - if ( !strncasecmp( $remainder, $binhexMagic, strlen( $binhexMagic ) ) ) { + if ( !strncmp( $remainder, $binhexMagic, strlen( $binhexMagic ) ) ) { $found['binhex'] = true; } } diff --git a/includes/IP.php b/includes/IP.php index e76f66c1..e5973c2b 100644 --- a/includes/IP.php +++ b/includes/IP.php @@ -141,7 +141,7 @@ class IP { public static function toOctet( $ip_int ) { // Convert to padded uppercase hex $ip_hex = wfBaseConvert($ip_int, 10, 16, 32, false); - // Seperate into 8 octets + // Separate into 8 octets $ip_oct = substr( $ip_hex, 0, 4 ); for ($n=1; $n < 8; $n++) { $ip_oct .= ':' . substr($ip_hex, 4*$n, 4); @@ -150,6 +150,41 @@ class IP { $ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct ); return $ip_oct; } + + /** + * Given a hexadecimal number, returns to an IPv6 address in octet notation + * @param $ip string hex IP + * @return string + */ + public static function HextoOctet( $ip_hex ) { + // Convert to padded uppercase hex + $ip_hex = str_pad( strtoupper($ip_hex), 32, '0'); + // Separate into 8 octets + $ip_oct = substr( $ip_hex, 0, 4 ); + for ($n=1; $n < 8; $n++) { + $ip_oct .= ':' . substr($ip_hex, 4*$n, 4); + } + // NO leading zeroes + $ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct ); + return $ip_oct; + } + + /** + * Converts a hexadecimal number to an IPv4 address in octet notation + * @param $ip string Hex IP + * @return string + */ + public static function hexToQuad( $ip ) { + // Converts a hexadecimal IP to nnn.nnn.nnn.nnn format + $dec = wfBaseConvert( $ip, 16, 10 ); + $parts[3] = $dec % 256; + $dec /= 256; + $parts[2] = $dec % 256; + $dec /= 256; + $parts[1] = $dec % 256; + $parts[0] = $dec / 256; + return implode( '.', array_reverse( $parts ) ); + } /** * Convert a network specification in IPv6 CIDR notation to an integer network and a number of bits @@ -320,7 +355,7 @@ class IP { public static function toHex( $ip ) { $n = self::toUnsigned( $ip ); if ( $n !== false ) { - $n = ( self::isIPv6($ip) ) ? "v6-" . wfBaseConvert( $n, 10, 16, 32, false ) : wfBaseConvert( $n, 10, 16, 8, false ); + $n = self::isIPv6($ip) ? "v6-" . wfBaseConvert( $n, 10, 16, 32, false ) : wfBaseConvert( $n, 10, 16, 8, false ); } return $n; } @@ -426,12 +461,16 @@ class IP { } elseif ( strpos( $range, '-' ) !== false ) { # Explicit range list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); - $start = self::toUnsigned( $start ); $end = self::toUnsigned( $end ); - if ( $start > $end ) { - $start = $end = false; + if( self::isIPAddress( $start ) && self::isIPAddress( $end ) ) { + $start = self::toUnsigned( $start ); $end = self::toUnsigned( $end ); + if ( $start > $end ) { + $start = $end = false; + } else { + $start = sprintf( '%08X', $start ); + $end = sprintf( '%08X', $end ); + } } else { - $start = sprintf( '%08X', $start ); - $end = sprintf( '%08X', $end ); + $start = $end = false; } } else { # Single IP diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php index af05c1c9..73d935a7 100644 --- a/includes/ImageFunctions.php +++ b/includes/ImageFunctions.php @@ -4,9 +4,10 @@ * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers * * @param $length String: CSS/SVG length. - * @return Integer: length in pixels + * @param $viewportSize: Float optional scale for percentage units... + * @return float: length in pixels */ -function wfScaleSVGUnit( $length ) { +function wfScaleSVGUnit( $length, $viewportSize=512 ) { static $unitLength = array( 'px' => 1.0, 'pt' => 1.25, @@ -14,17 +15,74 @@ function wfScaleSVGUnit( $length ) { 'mm' => 3.543307, 'cm' => 35.43307, 'in' => 90.0, + 'em' => 16.0, // fake it? + 'ex' => 12.0, // fake it? '' => 1.0, // "User units" pixels by default - '%' => 2.0, // Fake it! ); $matches = array(); - if( preg_match( '/^(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)$/', $length, $matches ) ) { + if( preg_match( '/^\s*(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/', $length, $matches ) ) { $length = floatval( $matches[1] ); $unit = $matches[2]; - return round( $length * $unitLength[$unit] ); + if( $unit == '%' ) { + return $length * 0.01 * $viewportSize; + } else { + return $length * $unitLength[$unit]; + } } else { // Assume pixels - return round( floatval( $length ) ); + return floatval( $length ); + } +} + +class XmlSizeFilter { + const DEFAULT_WIDTH = 512; + const DEFAULT_HEIGHT = 512; + var $first = true; + var $width = self::DEFAULT_WIDTH; + var $height = self::DEFAULT_HEIGHT; + function filter( $name, $attribs ) { + if( $this->first ) { + $defaultWidth = self::DEFAULT_WIDTH; + $defaultHeight = self::DEFAULT_HEIGHT; + $aspect = 1.0; + $width = null; + $height = null; + + if( isset( $attribs['viewBox'] ) ) { + // min-x min-y width height + $viewBox = preg_split( '/\s+/', trim( $attribs['viewBox'] ) ); + if( count( $viewBox ) == 4 ) { + $viewWidth = wfScaleSVGUnit( $viewBox[2] ); + $viewHeight = wfScaleSVGUnit( $viewBox[3] ); + if( $viewWidth > 0 && $viewHeight > 0 ) { + $aspect = $viewWidth / $viewHeight; + $defaultHeight = $defaultWidth / $aspect; + } + } + } + if( isset( $attribs['width'] ) ) { + $width = wfScaleSVGUnit( $attribs['width'], $defaultWidth ); + } + if( isset( $attribs['height'] ) ) { + $height = wfScaleSVGUnit( $attribs['height'], $defaultHeight ); + } + + if( !isset( $width ) && !isset( $height ) ) { + $width = $defaultWidth; + $height = $width / $aspect; + } elseif( isset( $width ) && !isset( $height ) ) { + $height = $width / $aspect; + } elseif( isset( $height ) && !isset( $width ) ) { + $width = $height * $aspect; + } + + if( $width > 0 && $height > 0 ) { + $this->width = intval( round( $width ) ); + $this->height = intval( round( $height ) ); + } + + $this->first = false; + } } } @@ -38,30 +96,14 @@ function wfScaleSVGUnit( $length ) { * @return array */ function wfGetSVGsize( $filename ) { - $width = 256; - $height = 256; - - // Read a chunk of the file - $f = fopen( $filename, "rt" ); - if( !$f ) return false; - $chunk = fread( $f, 4096 ); - fclose( $f ); - - // Uber-crappy hack! Run through a real XML parser. - $matches = array(); - if( !preg_match( '/<svg\s*([^>]*)\s*>/s', $chunk, $matches ) ) { - return false; - } - $tag = $matches[1]; - if( preg_match( '/(?:^|\s)width\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) { - $width = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) ); + $filter = new XmlSizeFilter(); + $xml = new XmlTypeCheck( $filename, array( $filter, 'filter' ) ); + if( $xml->wellFormed ) { + return array( $filter->width, $filter->height, 'SVG', + "width=\"$filter->width\" height=\"$filter->height\"" ); } - if( preg_match( '/(?:^|\s)height\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) { - $height = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) ); - } - - return array( $width, $height, 'SVG', - "width=\"$width\" height=\"$height\"" ); + + return false; } /** diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 492a3e06..f3f525c1 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -244,7 +244,7 @@ class ImageGallery $img = wfFindFile( $nt, $time ); - if( $nt->getNamespace() != NS_IMAGE || !$img ) { + if( $nt->getNamespace() != NS_FILE || !$img ) { # We're dealing with a non-image, spit out the name and be done with it. $thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">' . htmlspecialchars( $nt->getText() ) . '</div>'; diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 30fcf13e..314d478e 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -22,22 +22,28 @@ class ImagePage extends Article { $this->dupes = null; $this->repo = null; } + + public function setFile( $file ) { + $this->displayImg = $file; + $this->img = $file; + $this->fileLoaded = true; + } protected function loadFile() { - if ( $this->fileLoaded ) { + if( $this->fileLoaded ) { return true; } $this->fileLoaded = true; $this->displayImg = $this->img = false; wfRunHooks( 'ImagePageFindFile', array( $this, &$this->img, &$this->displayImg ) ); - if ( !$this->img ) { + if( !$this->img ) { $this->img = wfFindFile( $this->mTitle ); - if ( !$this->img ) { + if( !$this->img ) { $this->img = wfLocalFile( $this->mTitle ); } } - if ( !$this->displayImg ) { + if( !$this->displayImg ) { $this->displayImg = $this->img; } $this->repo = $this->img->getRepo(); @@ -47,18 +53,18 @@ class ImagePage extends Article { * Handler for action=render * Include body text only; none of the image extras */ - function render() { + public function render() { global $wgOut; $wgOut->setArticleBodyOnly( true ); parent::view(); } - function view() { + public function view() { global $wgOut, $wgShowEXIF, $wgRequest, $wgUser; $this->loadFile(); - if ( $this->mTitle->getNamespace() == NS_IMAGE && $this->img->getRedirected() ) { - if ( $this->mTitle->getDBkey() == $this->img->getName() ) { + if( $this->mTitle->getNamespace() == NS_FILE && $this->img->getRedirected() ) { + if( $this->mTitle->getDBkey() == $this->img->getName() ) { // mTitle is the same as the redirect target so ask Article // to perform the redirect for us. return Article::view(); @@ -66,8 +72,8 @@ class ImagePage extends Article { // mTitle is not the same as the redirect target so it is // probably the redirect page itself. Fake the redirect symbol $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $this->viewRedirect( Title::makeTitle( NS_IMAGE, $this->img->getName() ), - /* $appendSubtitle */ true, /* $forceKnown */ true ); + $wgOut->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->img->getName() ), + /* $appendSubtitle */ true, /* $forceKnown */ true ) ); $this->viewUpdates(); return; } @@ -76,10 +82,10 @@ class ImagePage extends Article { $diff = $wgRequest->getVal( 'diff' ); $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); - if ( $this->mTitle->getNamespace() != NS_IMAGE || ( isset( $diff ) && $diffOnly ) ) + if( $this->mTitle->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) ) return Article::view(); - if ( $wgShowEXIF && $this->displayImg->exists() ) { + if( $wgShowEXIF && $this->displayImg->exists() ) { // FIXME: bad interface, see note on MediaHandler::formatMetadata(). $formattedMetadata = $this->displayImg->formatMetadata(); $showmeta = $formattedMetadata !== false; @@ -87,24 +93,25 @@ class ImagePage extends Article { $showmeta = false; } - if ( $this->displayImg->exists() ) + if( !$diff && $this->displayImg->exists() ) $wgOut->addHTML( $this->showTOC($showmeta) ); - $this->openShowImage(); + if( !$diff ) + $this->openShowImage(); # No need to display noarticletext, we use our own message, output in openShowImage() - if ( $this->getID() ) { + if( $this->getID() ) { Article::view(); } else { # Just need to set the right headers $wgOut->setArticleFlag( true ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); $this->viewUpdates(); } # Show shared description, if needed - if ( $this->mExtraDescription ) { + if( $this->mExtraDescription ) { $fol = wfMsgNoTrans( 'shareddescriptionfollows' ); if( $fol != '-' && !wfEmptyMsg( 'shareddescriptionfollows', $fol ) ) { $wgOut->addWikiText( $fol ); @@ -118,21 +125,21 @@ class ImagePage extends Article { $this->imageHistory(); // TODO: Cleanup the following - $wgOut->addHTML( Xml::element( 'h2', - array( 'id' => 'filelinks' ), + $wgOut->addHTML( Xml::element( 'h2', + array( 'id' => 'filelinks' ), wfMsg( 'imagelinks' ) ) . "\n" ); $this->imageDupes(); // TODO: We may want to find local images redirecting to a foreign // file: "The following local files redirect to this file" - if ( $this->img->isLocal() ) { + if( $this->img->isLocal() ) { $this->imageRedirects(); } $this->imageLinks(); - if ( $showmeta ) { + if( $showmeta ) { global $wgStylePath, $wgStyleVersion; - $expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) ); - $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) ); + $expand = htmlspecialchars( Xml::escapeJsString( wfMsg( 'metadata-expand' ) ) ); + $collapse = htmlspecialchars( Xml::escapeJsString( wfMsg( 'metadata-collapse' ) ) ); $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" ); $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) ); $wgOut->addScriptFile( 'metadata.js' ); @@ -143,32 +150,32 @@ class ImagePage extends Article { public function getRedirectTarget() { $this->loadFile(); - if ( $this->img->isLocal() ) { + if( $this->img->isLocal() ) { return parent::getRedirectTarget(); } // Foreign image page $from = $this->img->getRedirected(); $to = $this->img->getName(); - if ( $from == $to ) { + if( $from == $to ) { return null; } - return $this->mRedirectTarget = Title::makeTitle( NS_IMAGE, $to ); + return $this->mRedirectTarget = Title::makeTitle( NS_FILE, $to ); } public function followRedirect() { $this->loadFile(); - if ( $this->img->isLocal() ) { + if( $this->img->isLocal() ) { return parent::followRedirect(); } $from = $this->img->getRedirected(); $to = $this->img->getName(); - if ( $from == $to ) { + if( $from == $to ) { return false; } - return Title::makeTitle( NS_IMAGE, $to ); + return Title::makeTitle( NS_FILE, $to ); } public function isRedirect( $text = false ) { $this->loadFile(); - if ( $this->img->isLocal() ) + if( $this->img->isLocal() ) return parent::isRedirect( $text ); return (bool)$this->img->getRedirected(); @@ -191,10 +198,10 @@ class ImagePage extends Article { public function getDuplicates() { $this->loadFile(); - if ( !is_null($this->dupes) ) { + if( !is_null($this->dupes) ) { return $this->dupes; } - if ( !( $hash = $this->img->getSha1() ) ) { + if( !( $hash = $this->img->getSha1() ) ) { return $this->dupes = array(); } $dupes = RepoGroup::singleton()->findBySha1( $hash ); @@ -203,9 +210,9 @@ class ImagePage extends Article { $size = $this->img->getSize(); foreach ( $dupes as $index => $file ) { $key = $file->getRepoName().':'.$file->getName(); - if ( $key == $self ) + if( $key == $self ) unset( $dupes[$index] ); - if ( $file->getSize() != $size ) + if( $file->getSize() != $size ) unset( $dupes[$index] ); } return $this->dupes = $dupes; @@ -216,15 +223,13 @@ class ImagePage extends Article { /** * Create the TOC * - * @access private - * * @param bool $metadata Whether or not to show the metadata link * @return string */ - function showTOC( $metadata ) { + protected function showTOC( $metadata ) { global $wgLang; $r = '<ul id="filetoc"> - <li><a href="#file">' . $wgLang->getNsText( NS_IMAGE ) . '</a></li> + <li><a href="#file">' . $wgLang->getNsText( NS_FILE ) . '</a></li> <li><a href="#filehistory">' . wfMsgHtml( 'filehist' ) . '</a></li> <li><a href="#filelinks">' . wfMsgHtml( 'imagelinks' ) . '</a></li>' . ($metadata ? ' <li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>' : '') . ' @@ -237,16 +242,15 @@ class ImagePage extends Article { * * FIXME: bad interface, see note on MediaHandler::formatMetadata(). * - * @access private - * * @param array $exif The array containing the EXIF data * @return string */ - function makeMetadataTable( $metadata ) { + protected function makeMetadataTable( $metadata ) { $r = wfMsg( 'metadata-help' ) . "\n\n"; $r .= "{| id=mw_metadata class=mw_metadata\n"; foreach ( $metadata as $type => $stuff ) { foreach ( $stuff as $v ) { + # FIXME, why is this using escapeId for a class?! $class = Sanitizer::escapeId( $v['id'] ); if( $type == 'collapsed' ) { $class .= ' collapsable'; @@ -266,7 +270,7 @@ class ImagePage extends Article { * Omit noarticletext if sharedupload; text will be fetched from the * shared upload server if possible. */ - function getContent() { + public function getContent() { $this->loadFile(); if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) { return ''; @@ -274,7 +278,7 @@ class ImagePage extends Article { return Article::getContent(); } - function openShowImage() { + protected function openShowImage() { global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang, $wgContLang; $this->loadFile(); @@ -298,10 +302,10 @@ class ImagePage extends Article { $sk = $wgUser->getSkin(); $dirmark = $wgContLang->getDirMark(); - if ( $this->displayImg->exists() ) { + if( $this->displayImg->exists() ) { # image $page = $wgRequest->getIntOrNull( 'page' ); - if ( is_null( $page ) ) { + if( is_null( $page ) ) { $params = array(); $page = 1; } else { @@ -318,16 +322,16 @@ class ImagePage extends Article { wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ; - if ( $this->displayImg->allowInlineDisplay() ) { + if( $this->displayImg->allowInlineDisplay() ) { # image # "Download high res version" link below the image #$msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->displayImg->getSize() ), $mime ); # We'll show a thumbnail of this image - if ( $width > $maxWidth || $height > $maxHeight ) { + if( $width > $maxWidth || $height > $maxHeight ) { # Calculate the thumbnail size. # First case, the limiting factor is the width, not the height. - if ( $width / $height >= $maxWidth / $maxHeight ) { + if( $width / $height >= $maxWidth / $maxHeight ) { $height = round( $height * $maxWidth / $width); $width = $maxWidth; # Note that $height <= $maxHeight now. @@ -339,8 +343,10 @@ class ImagePage extends Article { # because of rounding. } $msgbig = wfMsgHtml( 'show-big-image' ); - $msgsmall = wfMsgExt( 'show-big-image-thumb', - array( 'parseinline' ), $wgLang->formatNum( $width ), $wgLang->formatNum( $height ) ); + $msgsmall = wfMsgExt( 'show-big-image-thumb', 'parseinline', + $wgLang->formatNum( $width ), + $wgLang->formatNum( $height ) + ); } else { # Image is small enough to show full size on image page $msgbig = htmlspecialchars( $this->displayImg->getName() ); @@ -359,11 +365,11 @@ class ImagePage extends Article { '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . "$dirmark " . $longDesc; } - if ( $this->displayImg->isMultipage() ) { + if( $this->displayImg->isMultipage() ) { $wgOut->addHTML( '<table class="multipageimage"><tr><td>' ); } - if ( $thumbnail ) { + if( $thumbnail ) { $options = array( 'alt' => $this->displayImg->getTitle()->getPrefixedText(), 'file-link' => true, @@ -373,10 +379,10 @@ class ImagePage extends Article { $anchorclose . '</div>' ); } - if ( $this->displayImg->isMultipage() ) { + if( $this->displayImg->isMultipage() ) { $count = $this->displayImg->pageCount(); - if ( $page > 1 ) { + if( $page > 1 ) { $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false ); $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) ); $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none', @@ -385,7 +391,7 @@ class ImagePage extends Article { $thumb1 = ''; } - if ( $page < $count ) { + if( $page < $count ) { $label = wfMsg( 'imgmultipagenext' ); $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) ); $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none', @@ -422,7 +428,7 @@ class ImagePage extends Article { } } else { #if direct link is allowed but it's not a renderable image, show an icon. - if ( $this->displayImg->isSafeFile() ) { + if( $this->displayImg->isSafeFile() ) { $icon= $this->displayImg->iconThumb(); $wgOut->addHTML( '<div class="fullImageLink" id="file">' . @@ -434,10 +440,10 @@ class ImagePage extends Article { } - if ($showLink) { + if($showLink) { $filename = wfEscapeWikiText( $this->displayImg->getName() ); - if ( !$this->displayImg->isSafeFile() ) { + if( !$this->displayImg->isSafeFile() ) { $warning = wfMsgNoTrans( 'mediawarning' ); $wgOut->addWikiText( <<<EOT <div class="fullMedia"> @@ -474,7 +480,7 @@ EOT /** * Show a notice that the file is from a shared repository */ - function printSharedImageText() { + protected function printSharedImageText() { global $wgOut, $wgUser; $this->loadFile(); @@ -482,12 +488,12 @@ EOT $descUrl = $this->img->getDescriptionUrl(); $descText = $this->img->getDescriptionText(); $s = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml( 'sharedupload' ); - if ( $descUrl ) { + if( $descUrl ) { $sk = $wgUser->getSkin(); $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadwiki-linktext' ) ); $msg = ( $descText ) ? 'shareduploadwiki-desc' : 'shareduploadwiki'; $msg = wfMsgExt( $msg, array( 'parseinline', 'replaceafter' ), $link ); - if ( $msg != '-' ) { + if( $msg != '-' ) { # Show message only if not voided by local sysops $s .= $msg; } @@ -495,7 +501,7 @@ EOT $s .= "</div>"; $wgOut->addHTML( $s ); - if ( $descText ) { + if( $descText ) { $this->mExtraDescription = $descText; } } @@ -503,7 +509,7 @@ EOT /* * Check for files with the same name on the foreign repos. */ - function checkSharedConflict() { + protected function checkSharedConflict() { global $wgOut, $wgUser; $repoGroup = RepoGroup::singleton(); @@ -538,7 +544,7 @@ EOT } } - function checkSharedConflictCallback( $repo ) { + public function checkSharedConflictCallback( $repo ) { $this->loadFile(); $dupfile = $repo->newFile( $this->img->getTitle() ); if( $dupfile && $dupfile->exists() ) { @@ -548,7 +554,7 @@ EOT return false; } - function getUploadUrl() { + public function getUploadUrl() { $this->loadFile(); $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); @@ -558,7 +564,7 @@ EOT * Print out the various links at the bottom of the image page, e.g. reupload, * external editing (and instructions link) etc. */ - function uploadLinksBox() { + protected function uploadLinksBox() { global $wgUser, $wgOut; $this->loadFile(); @@ -567,69 +573,49 @@ EOT $sk = $wgUser->getSkin(); - $wgOut->addHtml( '<br /><ul>' ); + $wgOut->addHTML( '<br /><ul>' ); # "Upload a new version of this file" link if( UploadForm::userCanReUpload($wgUser,$this->img->name) ) { $ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) ); - $wgOut->addHtml( "<li><div class='plainlinks'>{$ulink}</div></li>" ); + $wgOut->addHTML( "<li><div class='plainlinks'>{$ulink}</div></li>" ); } # Link to Special:FileDuplicateSearch $dupeLink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'FileDuplicateSearch', $this->mTitle->getDBkey() ), wfMsgHtml( 'imagepage-searchdupe' ) ); - $wgOut->addHtml( "<li>{$dupeLink}</li>" ); + $wgOut->addHTML( "<li>{$dupeLink}</li>" ); # External editing link $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); - $wgOut->addHtml( '<li>' . $elink . '<div>' . wfMsgWikiHtml( 'edit-externally-help' ) . '</div></li>' ); + $wgOut->addHTML( '<li>' . $elink . ' <small>' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . '</small></li>' ); - $wgOut->addHtml( '</ul>' ); + $wgOut->addHTML( '</ul>' ); } - function closeShowImage() - { - # For overloading - - } + protected function closeShowImage() {} # For overloading /** * If the page we've just displayed is in the "Image" namespace, * we follow it with an upload history of the image and its usage. */ - function imageHistory() - { + protected function imageHistory() { global $wgOut, $wgUseExternalEditor; $this->loadFile(); - if ( $this->img->exists() ) { - $list = new ImageHistoryList( $this ); - $file = $this->img; - $dims = $file->getDimensionsString(); - $s = $list->beginImageHistoryList(); - $s .= $list->imageHistoryLine( true, $file ); - // old image versions - $hist = $this->img->getHistory(); - foreach( $hist as $file ) { - $dims = $file->getDimensionsString(); - $s .= $list->imageHistoryLine( false, $file ); - } - $s .= $list->endImageHistoryList(); - } else { $s=''; } - $wgOut->addHTML( $s ); + $pager = new ImageHistoryPseudoPager( $this ); + $wgOut->addHTML( $pager->getBody() ); - $this->img->resetHistory(); // free db resources + $this->img->resetHistory(); // free db resources # Exist check because we don't want to show this on pages where an image # doesn't exist along with the noimage message, that would suck. -ævar if( $wgUseExternalEditor && $this->img->exists() ) { $this->uploadLinksBox(); } - } - function imageLinks() - { - global $wgUser, $wgOut; + protected function imageLinks() { + global $wgUser, $wgOut, $wgLang; $limit = 100; @@ -643,21 +629,30 @@ EOT array( 'LIMIT' => $limit + 1) ); $count = $dbr->numRows( $res ); - if ( $count == 0 ) { + if( $count == 0 ) { $wgOut->addHTML( "<div id='mw-imagepage-nolinkstoimage'>\n" ); $wgOut->addWikiMsg( 'nolinkstoimage' ); $wgOut->addHTML( "</div>\n" ); return; } + $wgOut->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" ); - $wgOut->addWikiMsg( 'linkstoimage', $count ); - $wgOut->addHTML( "<ul class='mw-imagepage-linktoimage'>\n" ); + if( $count <= $limit - 1 ) { + $wgOut->addWikiMsg( 'linkstoimage', $count ); + } else { + // More links than the limit. Add a link to [[Special:Whatlinkshere]] + $wgOut->addWikiMsg( 'linkstoimage-more', + $wgLang->formatNum( $limit ), + $this->mTitle->getPrefixedDBkey() + ); + } + $wgOut->addHTML( "<ul class='mw-imagepage-linkstoimage'>\n" ); $sk = $wgUser->getSkin(); $count = 0; while ( $s = $res->fetchObject() ) { $count++; - if ( $count <= $limit ) { + if( $count <= $limit ) { // We have not yet reached the extra one that tells us there is more to fetch $name = Title::makeTitle( $s->page_namespace, $s->page_title ); $link = $sk->makeKnownLinkObj( $name, "" ); @@ -668,19 +663,20 @@ EOT $res->free(); // Add a links to [[Special:Whatlinkshere]] - if ( $count > $limit ) + if( $count > $limit ) $wgOut->addWikiMsg( 'morelinkstoimage', $this->mTitle->getPrefixedDBkey() ); } - function imageRedirects() - { - global $wgUser, $wgOut; + protected function imageRedirects() { + global $wgUser, $wgOut, $wgLang; - $redirects = $this->getTitle()->getRedirectsHere( NS_IMAGE ); - if ( count( $redirects ) == 0 ) return; + $redirects = $this->getTitle()->getRedirectsHere( NS_FILE ); + if( count( $redirects ) == 0 ) return; $wgOut->addHTML( "<div id='mw-imagepage-section-redirectstofile'>\n" ); - $wgOut->addWikiMsg( 'redirectstofile', count( $redirects ) ); + $wgOut->addWikiMsg( 'redirectstofile', + $wgLang->formatNum( count( $redirects ) ) + ); $wgOut->addHTML( "<ul class='mw-imagepage-redirectstofile'>\n" ); $sk = $wgUser->getSkin(); @@ -692,25 +688,28 @@ EOT } - function imageDupes() { - global $wgOut, $wgUser; + protected function imageDupes() { + global $wgOut, $wgUser, $wgLang; $this->loadFile(); $dupes = $this->getDuplicates(); - if ( count( $dupes ) == 0 ) return; + if( count( $dupes ) == 0 ) return; $wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" ); - $wgOut->addWikiMsg( 'duplicatesoffile', count( $dupes ) ); + $wgOut->addWikiMsg( 'duplicatesoffile', + $wgLang->formatNum( count( $dupes ) ) + ); $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" ); $sk = $wgUser->getSkin(); foreach ( $dupes as $file ) { - if ( $file->isLocal() ) + if( $file->isLocal() ) $link = $sk->makeKnownLinkObj( $file->getTitle(), "" ); - else - $link = $sk->makeExternalLink( $file->getDescriptionUrl(), + else { + $link = $sk->makeExternalLink( $file->getDescriptionUrl(), $file->getTitle()->getPrefixedText() ); + } $wgOut->addHTML( "<li>{$link}</li>\n" ); } $wgOut->addHTML( "</ul></div>\n" ); @@ -742,7 +741,7 @@ EOT /** * Override handling of action=purge */ - function doPurge() { + public function doPurge() { $this->loadFile(); if( $this->img->exists() ) { wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" ); @@ -762,7 +761,7 @@ EOT function showError( $description ) { global $wgOut; $wgOut->setPageTitle( wfMsg( "internalerror" ) ); - $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setRobotPolicy( "noindex,nofollow" ); $wgOut->setArticleRelated( false ); $wgOut->enableClientCache( false ); $wgOut->addWikiText( $description ); @@ -788,34 +787,36 @@ class ImageHistoryList { $this->imagePage = $imagePage; } - function getImagePage() { + public function getImagePage() { return $this->imagePage; } - function getSkin() { + public function getSkin() { return $this->skin; } - function getFile() { + public function getFile() { return $this->img; } - public function beginImageHistoryList() { + public function beginImageHistoryList( $navLinks = '' ) { global $wgOut, $wgUser; return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) ) . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) ) + . $navLinks . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n" . '<tr><td></td>' . ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ? '<td></td>' : '' ) . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>' + . '<th>' . wfMsgHtml( 'filehist-thumb' ) . '</th>' . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>' - . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>' - . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>' + . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>' + . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>' . "</tr>\n"; } - public function endImageHistoryList() { - return "</table>\n"; + public function endImageHistoryList( $navLinks = '' ) { + return "</table>\n$navLinks\n"; } public function imageHistoryLine( $iscur, $file ) { @@ -910,6 +911,21 @@ class ImageHistoryList { $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeAndDate( $timestamp, true ) ); } + // Thumbnail + if( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE ) && !$file->isDeleted( File::DELETED_FILE ) ) { + $params = array( + 'width' => '120', + 'height' => '120', + ); + $thumbnail = $file->transform( $params ); + $options = array( + 'alt' => wfMsg( 'filehist-thumbtext', $wgLang->timeAndDate( $timestamp, true ) ), + 'file-link' => true, + ); + $row .= '</td><td>' . $thumbnail->toHtml( $options ); + } else { + $row .= '</td><td>' . wfMsgHtml( 'filehist-nothumb' ); + } $row .= "</td><td>"; // Image dimensions @@ -934,7 +950,7 @@ class ImageHistoryList { $row .= '</td><td>'; // Don't show deleted descriptions - if ( $file->isDeleted(File::DELETED_COMMENT) ) { + if( $file->isDeleted(File::DELETED_COMMENT) ) { $row .= '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>'; } else { $row .= $this->skin->commentBlock( $description, $this->title ); @@ -947,3 +963,128 @@ class ImageHistoryList { return "<tr{$classAttr}>{$row}</tr>\n"; } } + +class ImageHistoryPseudoPager extends ReverseChronologicalPager { + function __construct( $imagePage ) { + parent::__construct(); + $this->mImagePage = $imagePage; + $this->mTitle = clone( $imagePage->getTitle() ); + $this->mTitle->setFragment( '#filehistory' ); + $this->mImg = NULL; + $this->mHist = array(); + $this->mRange = array( 0, 0 ); // display range + } + + function getTitle() { + return $this->mTitle; + } + + function getQueryInfo() { + return false; + } + + function getIndexField() { + return ''; + } + + function formatRow( $row ) { + return ''; + } + + function getBody() { + $s = ''; + $this->doQuery(); + if( count($this->mHist) ) { + $list = new ImageHistoryList( $this->mImagePage ); + # Generate prev/next links + $navLink = $this->getNavigationBar(); + $s = $list->beginImageHistoryList($navLink); + // Skip rows there just for paging links + for( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) { + $file = $this->mHist[$i]; + $s .= $list->imageHistoryLine( !$file->isOld(), $file ); + } + $s .= $list->endImageHistoryList($navLink); + } + return $s; + } + + function doQuery() { + if( $this->mQueryDone ) return; + $this->mImg = $this->mImagePage->getFile(); // ensure loading + if( !$this->mImg->exists() ) { + return; + } + $queryLimit = $this->mLimit + 1; // limit plus extra row + if( $this->mIsBackwards ) { + // Fetch the file history + $this->mHist = $this->mImg->getHistory($queryLimit,null,$this->mOffset,false); + // The current rev may not meet the offset/limit + $numRows = count($this->mHist); + if( $numRows <= $this->mLimit && $this->mImg->getTimestamp() > $this->mOffset ) { + $this->mHist = array_merge( array($this->mImg), $this->mHist ); + } + } else { + // The current rev may not meet the offset + if( !$this->mOffset || $this->mImg->getTimestamp() < $this->mOffset ) { + $this->mHist[] = $this->mImg; + } + // Old image versions (fetch extra row for nav links) + $oiLimit = count($this->mHist) ? $this->mLimit : $this->mLimit+1; + // Fetch the file history + $this->mHist = array_merge( $this->mHist, + $this->mImg->getHistory($oiLimit,$this->mOffset,null,false) ); + } + $numRows = count($this->mHist); // Total number of query results + if( $numRows ) { + # Index value of top item in the list + $firstIndex = $this->mIsBackwards ? + $this->mHist[$numRows-1]->getTimestamp() : $this->mHist[0]->getTimestamp(); + # Discard the extra result row if there is one + if( $numRows > $this->mLimit && $numRows > 1 ) { + if( $this->mIsBackwards ) { + # Index value of item past the index + $this->mPastTheEndIndex = $this->mHist[0]->getTimestamp(); + # Index value of bottom item in the list + $lastIndex = $this->mHist[1]->getTimestamp(); + # Display range + $this->mRange = array( 1, $numRows-1 ); + } else { + # Index value of item past the index + $this->mPastTheEndIndex = $this->mHist[$numRows-1]->getTimestamp(); + # Index value of bottom item in the list + $lastIndex = $this->mHist[$numRows-2]->getTimestamp(); + # Display range + $this->mRange = array( 0, $numRows-2 ); + } + } else { + # Setting indexes to an empty string means that they will be + # omitted if they would otherwise appear in URLs. It just so + # happens that this is the right thing to do in the standard + # UI, in all the relevant cases. + $this->mPastTheEndIndex = ''; + # Index value of bottom item in the list + $lastIndex = $this->mIsBackwards ? + $this->mHist[0]->getTimestamp() : $this->mHist[$numRows-1]->getTimestamp(); + # Display range + $this->mRange = array( 0, $numRows-1 ); + } + } else { + $firstIndex = ''; + $lastIndex = ''; + $this->mPastTheEndIndex = ''; + } + if( $this->mIsBackwards ) { + $this->mIsFirst = ( $numRows < $queryLimit ); + $this->mIsLast = ( $this->mOffset == '' ); + $this->mLastShown = $firstIndex; + $this->mFirstShown = $lastIndex; + } else { + $this->mIsFirst = ( $this->mOffset == '' ); + $this->mIsLast = ( $numRows < $queryLimit ); + $this->mLastShown = $lastIndex; + $this->mFirstShown = $firstIndex; + } + $this->mQueryDone = true; + } +} diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php index da9b6fd6..3ab0b858 100644 --- a/includes/ImageQueryPage.php +++ b/includes/ImageQueryPage.php @@ -34,7 +34,7 @@ class ImageQueryPage extends QueryPage { } } - $out->addHtml( $gallery->toHtml() ); + $out->addHTML( $gallery->toHtml() ); } } @@ -45,9 +45,9 @@ class ImageQueryPage extends QueryPage { * @return Image */ private function prepareImage( $row ) { - $namespace = isset( $row->namespace ) ? $row->namespace : NS_IMAGE; + $namespace = isset( $row->namespace ) ? $row->namespace : NS_FILE; $title = Title::makeTitleSafe( $namespace, $row->title ); - return ( $title instanceof Title && $title->getNamespace() == NS_IMAGE ) + return ( $title instanceof Title && $title->getNamespace() == NS_FILE ) ? wfFindFile( $title ) : null; } diff --git a/includes/Import.php b/includes/Import.php new file mode 100644 index 00000000..56e7a7fb --- /dev/null +++ b/includes/Import.php @@ -0,0 +1,1133 @@ +<?php +/** + * MediaWiki page data importer + * Copyright (C) 2003,2005 Brion Vibber <brion@pobox.com> + * http://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup SpecialPage + */ + +/** + * + * @ingroup SpecialPage + */ +class WikiRevision { + var $title = null; + var $id = 0; + var $timestamp = "20010115000000"; + var $user = 0; + var $user_text = ""; + var $text = ""; + var $comment = ""; + var $minor = false; + var $type = ""; + var $action = ""; + var $params = ""; + + function setTitle( $title ) { + if( is_object( $title ) ) { + $this->title = $title; + } elseif( is_null( $title ) ) { + throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." ); + } else { + throw new MWException( "WikiRevision given non-object title in import." ); + } + } + + function setID( $id ) { + $this->id = $id; + } + + function setTimestamp( $ts ) { + # 2003-08-05T18:30:02Z + $this->timestamp = wfTimestamp( TS_MW, $ts ); + } + + function setUsername( $user ) { + $this->user_text = $user; + } + + function setUserIP( $ip ) { + $this->user_text = $ip; + } + + function setText( $text ) { + $this->text = $text; + } + + function setComment( $text ) { + $this->comment = $text; + } + + function setMinor( $minor ) { + $this->minor = (bool)$minor; + } + + function setSrc( $src ) { + $this->src = $src; + } + + function setFilename( $filename ) { + $this->filename = $filename; + } + + function setSize( $size ) { + $this->size = intval( $size ); + } + + function setType( $type ) { + $this->type = $type; + } + + function setAction( $action ) { + $this->action = $action; + } + + function setParams( $params ) { + $this->params = $params; + } + + function getTitle() { + return $this->title; + } + + function getID() { + return $this->id; + } + + function getTimestamp() { + return $this->timestamp; + } + + function getUser() { + return $this->user_text; + } + + function getText() { + return $this->text; + } + + function getComment() { + return $this->comment; + } + + function getMinor() { + return $this->minor; + } + + function getSrc() { + return $this->src; + } + + function getFilename() { + return $this->filename; + } + + function getSize() { + return $this->size; + } + + function getType() { + return $this->type; + } + + function getAction() { + return $this->action; + } + + function getParams() { + return $this->params; + } + + function importOldRevision() { + $dbw = wfGetDB( DB_MASTER ); + + # Sneak a single revision into place + $user = User::newFromName( $this->getUser() ); + if( $user ) { + $userId = intval( $user->getId() ); + $userText = $user->getName(); + } else { + $userId = 0; + $userText = $this->getUser(); + } + + // avoid memory leak...? + $linkCache = LinkCache::singleton(); + $linkCache->clear(); + + $article = new Article( $this->title ); + $pageId = $article->getId(); + if( $pageId == 0 ) { + # must create the page... + $pageId = $article->insertOn( $dbw ); + $created = true; + } else { + $created = false; + + $prior = $dbw->selectField( 'revision', '1', + array( 'rev_page' => $pageId, + 'rev_timestamp' => $dbw->timestamp( $this->timestamp ), + 'rev_user_text' => $userText, + 'rev_comment' => $this->getComment() ), + __METHOD__ + ); + if( $prior ) { + // FIXME: this could fail slightly for multiple matches :P + wfDebug( __METHOD__ . ": skipping existing revision for [[" . + $this->title->getPrefixedText() . "]], timestamp " . $this->timestamp . "\n" ); + return false; + } + } + + # FIXME: Use original rev_id optionally (better for backups) + # Insert the row + $revision = new Revision( array( + 'page' => $pageId, + 'text' => $this->getText(), + 'comment' => $this->getComment(), + 'user' => $userId, + 'user_text' => $userText, + 'timestamp' => $this->timestamp, + 'minor_edit' => $this->minor, + ) ); + $revId = $revision->insertOn( $dbw ); + $changed = $article->updateIfNewerOn( $dbw, $revision ); + + # To be on the safe side... + $tempTitle = $GLOBALS['wgTitle']; + $GLOBALS['wgTitle'] = $this->title; + + if( $created ) { + wfDebug( __METHOD__ . ": running onArticleCreate\n" ); + Article::onArticleCreate( $this->title ); + + wfDebug( __METHOD__ . ": running create updates\n" ); + $article->createUpdates( $revision ); + + } elseif( $changed ) { + wfDebug( __METHOD__ . ": running onArticleEdit\n" ); + Article::onArticleEdit( $this->title, 'skiptransclusions' ); // leave templatelinks for editUpdates() + + wfDebug( __METHOD__ . ": running edit updates\n" ); + $article->editUpdates( + $this->getText(), + $this->getComment(), + $this->minor, + $this->timestamp, + $revId ); + } + $GLOBALS['wgTitle'] = $tempTitle; + + return true; + } + + function importLogItem() { + $dbw = wfGetDB( DB_MASTER ); + # FIXME: this will not record autoblocks + if( !$this->getTitle() ) { + wfDebug( __METHOD__ . ": skipping invalid {$this->type}/{$this->action} log time, timestamp " . + $this->timestamp . "\n" ); + return; + } + # Check if it exists already + // FIXME: use original log ID (better for backups) + $prior = $dbw->selectField( 'logging', '1', + array( 'log_type' => $this->getType(), + 'log_action' => $this->getAction(), + 'log_timestamp' => $dbw->timestamp( $this->timestamp ), + 'log_namespace' => $this->getTitle()->getNamespace(), + 'log_title' => $this->getTitle()->getDBkey(), + 'log_comment' => $this->getComment(), + #'log_user_text' => $this->user_text, + 'log_params' => $this->params ), + __METHOD__ + ); + // FIXME: this could fail slightly for multiple matches :P + if( $prior ) { + wfDebug( __METHOD__ . ": skipping existing item for Log:{$this->type}/{$this->action}, timestamp " . + $this->timestamp . "\n" ); + return false; + } + $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' ); + $data = array( + 'log_id' => $log_id, + 'log_type' => $this->type, + 'log_action' => $this->action, + 'log_timestamp' => $dbw->timestamp( $this->timestamp ), + 'log_user' => User::idFromName( $this->user_text ), + #'log_user_text' => $this->user_text, + 'log_namespace' => $this->getTitle()->getNamespace(), + 'log_title' => $this->getTitle()->getDBkey(), + 'log_comment' => $this->getComment(), + 'log_params' => $this->params + ); + $dbw->insert( 'logging', $data, __METHOD__ ); + } + + function importUpload() { + wfDebug( __METHOD__ . ": STUB\n" ); + + /** + // from file revert... + $source = $this->file->getArchiveVirtualUrl( $this->oldimage ); + $comment = $wgRequest->getText( 'wpComment' ); + // TODO: Preserve file properties from database instead of reloading from file + $status = $this->file->upload( $source, $comment, $comment ); + if( $status->isGood() ) { + */ + + /** + // from file upload... + $this->mLocalFile = wfLocalFile( $nt ); + $this->mDestName = $this->mLocalFile->getName(); + //.... + $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, + File::DELETE_SOURCE, $this->mFileProps ); + if ( !$status->isGood() ) { + $resultDetails = array( 'internal' => $status->getWikiText() ); + */ + + // @fixme upload() uses $wgUser, which is wrong here + // it may also create a page without our desire, also wrong potentially. + // and, it will record a *current* upload, but we might want an archive version here + + $file = wfLocalFile( $this->getTitle() ); + if( !$file ) { + var_dump( $file ); + wfDebug( "IMPORT: Bad file. :(\n" ); + return false; + } + + $source = $this->downloadSource(); + if( !$source ) { + wfDebug( "IMPORT: Could not fetch remote file. :(\n" ); + return false; + } + + $status = $file->upload( $source, + $this->getComment(), + $this->getComment(), // Initial page, if none present... + File::DELETE_SOURCE, + false, // props... + $this->getTimestamp() ); + + if( $status->isGood() ) { + // yay? + wfDebug( "IMPORT: is ok?\n" ); + return true; + } + + wfDebug( "IMPORT: is bad? " . $status->getXml() . "\n" ); + return false; + + } + + function downloadSource() { + global $wgEnableUploads; + if( !$wgEnableUploads ) { + return false; + } + + $tempo = tempnam( wfTempDir(), 'download' ); + $f = fopen( $tempo, 'wb' ); + if( !$f ) { + wfDebug( "IMPORT: couldn't write to temp file $tempo\n" ); + return false; + } + + // @fixme! + $src = $this->getSrc(); + $data = Http::get( $src ); + if( !$data ) { + wfDebug( "IMPORT: couldn't fetch source $src\n" ); + fclose( $f ); + unlink( $tempo ); + return false; + } + + fwrite( $f, $data ); + fclose( $f ); + + return $tempo; + } + +} + +/** + * implements Special:Import + * @ingroup SpecialPage + */ +class WikiImporter { + var $mDebug = false; + var $mSource = null; + var $mPageCallback = null; + var $mPageOutCallback = null; + var $mRevisionCallback = null; + var $mLogItemCallback = null; + var $mUploadCallback = null; + var $mTargetNamespace = null; + var $mXmlNamespace = false; + var $lastfield; + var $tagStack = array(); + + function __construct( $source ) { + $this->setRevisionCallback( array( $this, "importRevision" ) ); + $this->setUploadCallback( array( $this, "importUpload" ) ); + $this->setLogItemCallback( array( $this, "importLogItem" ) ); + $this->mSource = $source; + } + + function throwXmlError( $err ) { + $this->debug( "FAILURE: $err" ); + wfDebug( "WikiImporter XML error: $err\n" ); + } + + function handleXmlNamespace ( $parser, $data, $prefix=false, $uri=false ) { + if( preg_match( '/www.mediawiki.org/',$prefix ) ) { + $prefix = str_replace( '/','\/',$prefix ); + $this->mXmlNamespace='/^'.$prefix.':/'; + } + } + + function stripXmlNamespace($name) { + if( $this->mXmlNamespace ) { + return(preg_replace($this->mXmlNamespace,'',$name,1)); + } + else { + return($name); + } + } + + # -------------- + + function doImport() { + if( empty( $this->mSource ) ) { + return new WikiErrorMsg( "importnotext" ); + } + + $parser = xml_parser_create_ns( "UTF-8" ); + + # case folding violates XML standard, turn it off + xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); + + xml_set_object( $parser, $this ); + xml_set_element_handler( $parser, "in_start", "" ); + xml_set_start_namespace_decl_handler( $parser, "handleXmlNamespace" ); + + $offset = 0; // for context extraction on error reporting + do { + $chunk = $this->mSource->readChunk(); + if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) { + wfDebug( "WikiImporter::doImport encountered XML parsing error\n" ); + return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset ); + } + $offset += strlen( $chunk ); + } while( $chunk !== false && !$this->mSource->atEnd() ); + xml_parser_free( $parser ); + + return true; + } + + function debug( $data ) { + if( $this->mDebug ) { + wfDebug( "IMPORT: $data\n" ); + } + } + + function notice( $data ) { + global $wgCommandLineMode; + if( $wgCommandLineMode ) { + print "$data\n"; + } else { + global $wgOut; + $wgOut->addHTML( "<li>" . htmlspecialchars( $data ) . "</li>\n" ); + } + } + + /** + * Set debug mode... + */ + function setDebug( $debug ) { + $this->mDebug = $debug; + } + + /** + * Sets the action to perform as each new page in the stream is reached. + * @param $callback callback + * @return callback + */ + function setPageCallback( $callback ) { + $previous = $this->mPageCallback; + $this->mPageCallback = $callback; + return $previous; + } + + /** + * Sets the action to perform as each page in the stream is completed. + * Callback accepts the page title (as a Title object), a second object + * with the original title form (in case it's been overridden into a + * local namespace), and a count of revisions. + * + * @param $callback callback + * @return callback + */ + function setPageOutCallback( $callback ) { + $previous = $this->mPageOutCallback; + $this->mPageOutCallback = $callback; + return $previous; + } + + /** + * Sets the action to perform as each page revision is reached. + * @param $callback callback + * @return callback + */ + function setRevisionCallback( $callback ) { + $previous = $this->mRevisionCallback; + $this->mRevisionCallback = $callback; + return $previous; + } + + /** + * Sets the action to perform as each file upload version is reached. + * @param $callback callback + * @return callback + */ + function setUploadCallback( $callback ) { + $previous = $this->mUploadCallback; + $this->mUploadCallback = $callback; + return $previous; + } + + /** + * Sets the action to perform as each log item reached. + * @param $callback callback + * @return callback + */ + function setLogItemCallback( $callback ) { + $previous = $this->mLogItemCallback; + $this->mLogItemCallback = $callback; + return $previous; + } + + /** + * Set a target namespace to override the defaults + */ + function setTargetNamespace( $namespace ) { + if( is_null( $namespace ) ) { + // Don't override namespaces + $this->mTargetNamespace = null; + } elseif( $namespace >= 0 ) { + // FIXME: Check for validity + $this->mTargetNamespace = intval( $namespace ); + } else { + return false; + } + } + + /** + * Default per-revision callback, performs the import. + * @param $revision WikiRevision + * @private + */ + function importRevision( $revision ) { + $dbw = wfGetDB( DB_MASTER ); + return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) ); + } + + /** + * Default per-revision callback, performs the import. + * @param $revision WikiRevision + * @private + */ + function importLogItem( $rev ) { + $dbw = wfGetDB( DB_MASTER ); + return $dbw->deadlockLoop( array( $rev, 'importLogItem' ) ); + } + + /** + * Dummy for now... + */ + function importUpload( $revision ) { + //$dbw = wfGetDB( DB_MASTER ); + //return $dbw->deadlockLoop( array( $revision, 'importUpload' ) ); + return false; + } + + /** + * Alternate per-revision callback, for debugging. + * @param $revision WikiRevision + * @private + */ + function debugRevisionHandler( &$revision ) { + $this->debug( "Got revision:" ); + if( is_object( $revision->title ) ) { + $this->debug( "-- Title: " . $revision->title->getPrefixedText() ); + } else { + $this->debug( "-- Title: <invalid>" ); + } + $this->debug( "-- User: " . $revision->user_text ); + $this->debug( "-- Timestamp: " . $revision->timestamp ); + $this->debug( "-- Comment: " . $revision->comment ); + $this->debug( "-- Text: " . $revision->text ); + } + + /** + * Notify the callback function when a new <page> is reached. + * @param $title Title + * @private + */ + function pageCallback( $title ) { + if( is_callable( $this->mPageCallback ) ) { + call_user_func( $this->mPageCallback, $title ); + } + } + + /** + * Notify the callback function when a </page> is closed. + * @param $title Title + * @param $origTitle Title + * @param $revisionCount int + * @param $successCount Int: number of revisions for which callback returned true + * @private + */ + function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) { + if( is_callable( $this->mPageOutCallback ) ) { + call_user_func( $this->mPageOutCallback, $title, $origTitle, + $revisionCount, $successCount ); + } + } + + # XML parser callbacks from here out -- beware! + function donothing( $parser, $x, $y="" ) { + #$this->debug( "donothing" ); + } + + function in_start( $parser, $name, $attribs ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "in_start $name" ); + if( $name != "mediawiki" ) { + return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" ); + } + xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); + } + + function in_mediawiki( $parser, $name, $attribs ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "in_mediawiki $name" ); + if( $name == 'siteinfo' ) { + xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" ); + } elseif( $name == 'page' ) { + $this->push( $name ); + $this->workRevisionCount = 0; + $this->workSuccessCount = 0; + $this->uploadCount = 0; + $this->uploadSuccessCount = 0; + xml_set_element_handler( $parser, "in_page", "out_page" ); + } elseif( $name == 'logitem' ) { + $this->push( $name ); + $this->workRevision = new WikiRevision; + xml_set_element_handler( $parser, "in_logitem", "out_logitem" ); + } else { + return $this->throwXMLerror( "Expected <page>, got <$name>" ); + } + } + function out_mediawiki( $parser, $name ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "out_mediawiki $name" ); + if( $name != "mediawiki" ) { + return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" ); + } + xml_set_element_handler( $parser, "donothing", "donothing" ); + } + + + function in_siteinfo( $parser, $name, $attribs ) { + // no-ops for now + $name = $this->stripXmlNamespace($name); + $this->debug( "in_siteinfo $name" ); + switch( $name ) { + case "sitename": + case "base": + case "generator": + case "case": + case "namespaces": + case "namespace": + break; + default: + return $this->throwXMLerror( "Element <$name> not allowed in <siteinfo>." ); + } + } + + function out_siteinfo( $parser, $name ) { + $name = $this->stripXmlNamespace($name); + if( $name == "siteinfo" ) { + xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); + } + } + + + function in_page( $parser, $name, $attribs ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "in_page $name" ); + switch( $name ) { + case "id": + case "title": + case "restrictions": + $this->appendfield = $name; + $this->appenddata = ""; + xml_set_element_handler( $parser, "in_nothing", "out_append" ); + xml_set_character_data_handler( $parser, "char_append" ); + break; + case "revision": + $this->push( "revision" ); + if( is_object( $this->pageTitle ) ) { + $this->workRevision = new WikiRevision; + $this->workRevision->setTitle( $this->pageTitle ); + $this->workRevisionCount++; + } else { + // Skipping items due to invalid page title + $this->workRevision = null; + } + xml_set_element_handler( $parser, "in_revision", "out_revision" ); + break; + case "upload": + $this->push( "upload" ); + if( is_object( $this->pageTitle ) ) { + $this->workRevision = new WikiRevision; + $this->workRevision->setTitle( $this->pageTitle ); + $this->uploadCount++; + } else { + // Skipping items due to invalid page title + $this->workRevision = null; + } + xml_set_element_handler( $parser, "in_upload", "out_upload" ); + break; + default: + return $this->throwXMLerror( "Element <$name> not allowed in a <page>." ); + } + } + + function out_page( $parser, $name ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "out_page $name" ); + $this->pop(); + if( $name != "page" ) { + return $this->throwXMLerror( "Expected </page>, got </$name>" ); + } + xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); + + $this->pageOutCallback( $this->pageTitle, $this->origTitle, + $this->workRevisionCount, $this->workSuccessCount ); + + $this->workTitle = null; + $this->workRevision = null; + $this->workRevisionCount = 0; + $this->workSuccessCount = 0; + $this->pageTitle = null; + $this->origTitle = null; + } + + function in_nothing( $parser, $name, $attribs ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "in_nothing $name" ); + return $this->throwXMLerror( "No child elements allowed here; got <$name>" ); + } + + function char_append( $parser, $data ) { + $this->debug( "char_append '$data'" ); + $this->appenddata .= $data; + } + + function out_append( $parser, $name ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "out_append $name" ); + if( $name != $this->appendfield ) { + return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" ); + } + + switch( $this->appendfield ) { + case "title": + $this->workTitle = $this->appenddata; + $this->origTitle = Title::newFromText( $this->workTitle ); + if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) { + $this->pageTitle = Title::makeTitle( $this->mTargetNamespace, + $this->origTitle->getDBkey() ); + } else { + $this->pageTitle = Title::newFromText( $this->workTitle ); + } + if( is_null( $this->pageTitle ) ) { + // Invalid page title? Ignore the page + $this->notice( "Skipping invalid page title '$this->workTitle'" ); + } elseif( $this->pageTitle->getInterwiki() != '' ) { + $this->notice( "Skipping interwiki page title '$this->workTitle'" ); + $this->pageTitle = null; + } else { + $this->pageCallback( $this->workTitle ); + } + break; + case "id": + if ( $this->parentTag() == 'revision' || $this->parentTag() == 'logitem' ) { + if( $this->workRevision ) + $this->workRevision->setID( $this->appenddata ); + } + break; + case "text": + if( $this->workRevision ) + $this->workRevision->setText( $this->appenddata ); + break; + case "username": + if( $this->workRevision ) + $this->workRevision->setUsername( $this->appenddata ); + break; + case "ip": + if( $this->workRevision ) + $this->workRevision->setUserIP( $this->appenddata ); + break; + case "timestamp": + if( $this->workRevision ) + $this->workRevision->setTimestamp( $this->appenddata ); + break; + case "comment": + if( $this->workRevision ) + $this->workRevision->setComment( $this->appenddata ); + break; + case "type": + if( $this->workRevision ) + $this->workRevision->setType( $this->appenddata ); + break; + case "action": + if( $this->workRevision ) + $this->workRevision->setAction( $this->appenddata ); + break; + case "logtitle": + if( $this->workRevision ) + $this->workRevision->setTitle( Title::newFromText( $this->appenddata ) ); + break; + case "params": + if( $this->workRevision ) + $this->workRevision->setParams( $this->appenddata ); + break; + case "minor": + if( $this->workRevision ) + $this->workRevision->setMinor( true ); + break; + case "filename": + if( $this->workRevision ) + $this->workRevision->setFilename( $this->appenddata ); + break; + case "src": + if( $this->workRevision ) + $this->workRevision->setSrc( $this->appenddata ); + break; + case "size": + if( $this->workRevision ) + $this->workRevision->setSize( intval( $this->appenddata ) ); + break; + default: + $this->debug( "Bad append: {$this->appendfield}" ); + } + $this->appendfield = ""; + $this->appenddata = ""; + + $parent = $this->parentTag(); + xml_set_element_handler( $parser, "in_$parent", "out_$parent" ); + xml_set_character_data_handler( $parser, "donothing" ); + } + + function in_revision( $parser, $name, $attribs ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "in_revision $name" ); + switch( $name ) { + case "id": + case "timestamp": + case "comment": + case "minor": + case "text": + $this->appendfield = $name; + xml_set_element_handler( $parser, "in_nothing", "out_append" ); + xml_set_character_data_handler( $parser, "char_append" ); + break; + case "contributor": + $this->push( "contributor" ); + xml_set_element_handler( $parser, "in_contributor", "out_contributor" ); + break; + default: + return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." ); + } + } + + function out_revision( $parser, $name ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "out_revision $name" ); + $this->pop(); + if( $name != "revision" ) { + return $this->throwXMLerror( "Expected </revision>, got </$name>" ); + } + xml_set_element_handler( $parser, "in_page", "out_page" ); + + if( $this->workRevision ) { + $ok = call_user_func_array( $this->mRevisionCallback, + array( $this->workRevision, $this ) ); + if( $ok ) { + $this->workSuccessCount++; + } + } + } + + function in_logitem( $parser, $name, $attribs ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "in_logitem $name" ); + switch( $name ) { + case "id": + case "timestamp": + case "comment": + case "type": + case "action": + case "logtitle": + case "params": + $this->appendfield = $name; + xml_set_element_handler( $parser, "in_nothing", "out_append" ); + xml_set_character_data_handler( $parser, "char_append" ); + break; + case "contributor": + $this->push( "contributor" ); + xml_set_element_handler( $parser, "in_contributor", "out_contributor" ); + break; + default: + return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." ); + } + } + + function out_logitem( $parser, $name ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "out_logitem $name" ); + $this->pop(); + if( $name != "logitem" ) { + return $this->throwXMLerror( "Expected </logitem>, got </$name>" ); + } + xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); + + if( $this->workRevision ) { + $ok = call_user_func_array( $this->mLogItemCallback, + array( $this->workRevision, $this ) ); + if( $ok ) { + $this->workSuccessCount++; + } + } + } + + function in_upload( $parser, $name, $attribs ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "in_upload $name" ); + switch( $name ) { + case "timestamp": + case "comment": + case "text": + case "filename": + case "src": + case "size": + $this->appendfield = $name; + xml_set_element_handler( $parser, "in_nothing", "out_append" ); + xml_set_character_data_handler( $parser, "char_append" ); + break; + case "contributor": + $this->push( "contributor" ); + xml_set_element_handler( $parser, "in_contributor", "out_contributor" ); + break; + default: + return $this->throwXMLerror( "Element <$name> not allowed in an <upload>." ); + } + } + + function out_upload( $parser, $name ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "out_revision $name" ); + $this->pop(); + if( $name != "upload" ) { + return $this->throwXMLerror( "Expected </upload>, got </$name>" ); + } + xml_set_element_handler( $parser, "in_page", "out_page" ); + + if( $this->workRevision ) { + $ok = call_user_func_array( $this->mUploadCallback, + array( $this->workRevision, $this ) ); + if( $ok ) { + $this->workUploadSuccessCount++; + } + } + } + + function in_contributor( $parser, $name, $attribs ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "in_contributor $name" ); + switch( $name ) { + case "username": + case "ip": + case "id": + $this->appendfield = $name; + xml_set_element_handler( $parser, "in_nothing", "out_append" ); + xml_set_character_data_handler( $parser, "char_append" ); + break; + default: + $this->throwXMLerror( "Invalid tag <$name> in <contributor>" ); + } + } + + function out_contributor( $parser, $name ) { + $name = $this->stripXmlNamespace($name); + $this->debug( "out_contributor $name" ); + $this->pop(); + if( $name != "contributor" ) { + return $this->throwXMLerror( "Expected </contributor>, got </$name>" ); + } + $parent = $this->parentTag(); + xml_set_element_handler( $parser, "in_$parent", "out_$parent" ); + } + + private function push( $name ) { + array_push( $this->tagStack, $name ); + $this->debug( "PUSH $name" ); + } + + private function pop() { + $name = array_pop( $this->tagStack ); + $this->debug( "POP $name" ); + return $name; + } + + private function parentTag() { + $name = $this->tagStack[count( $this->tagStack ) - 1]; + $this->debug( "PARENT $name" ); + return $name; + } + +} + +/** + * @todo document (e.g. one-sentence class description). + * @ingroup SpecialPage + */ +class ImportStringSource { + function __construct( $string ) { + $this->mString = $string; + $this->mRead = false; + } + + function atEnd() { + return $this->mRead; + } + + function readChunk() { + if( $this->atEnd() ) { + return false; + } else { + $this->mRead = true; + return $this->mString; + } + } +} + +/** + * @todo document (e.g. one-sentence class description). + * @ingroup SpecialPage + */ +class ImportStreamSource { + function __construct( $handle ) { + $this->mHandle = $handle; + } + + function atEnd() { + return feof( $this->mHandle ); + } + + function readChunk() { + return fread( $this->mHandle, 32768 ); + } + + static function newFromFile( $filename ) { + $file = @fopen( $filename, 'rt' ); + if( !$file ) { + return new WikiErrorMsg( "importcantopen" ); + } + return new ImportStreamSource( $file ); + } + + static function newFromUpload( $fieldname = "xmlimport" ) { + $upload =& $_FILES[$fieldname]; + + if( !isset( $upload ) || !$upload['name'] ) { + return new WikiErrorMsg( 'importnofile' ); + } + if( !empty( $upload['error'] ) ) { + switch($upload['error']){ + case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini. + return new WikiErrorMsg( 'importuploaderrorsize' ); + case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form. + return new WikiErrorMsg( 'importuploaderrorsize' ); + case 3: # The uploaded file was only partially uploaded + return new WikiErrorMsg( 'importuploaderrorpartial' ); + case 6: #Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3. + return new WikiErrorMsg( 'importuploaderrortemp' ); + # case else: # Currently impossible + } + + } + $fname = $upload['tmp_name']; + if( is_uploaded_file( $fname ) ) { + return ImportStreamSource::newFromFile( $fname ); + } else { + return new WikiErrorMsg( 'importnofile' ); + } + } + + static function newFromURL( $url, $method = 'GET' ) { + wfDebug( __METHOD__ . ": opening $url\n" ); + # Use the standard HTTP fetch function; it times out + # quicker and sorts out user-agent problems which might + # otherwise prevent importing from large sites, such + # as the Wikimedia cluster, etc. + $data = Http::request( $method, $url ); + if( $data !== false ) { + $file = tmpfile(); + fwrite( $file, $data ); + fflush( $file ); + fseek( $file, 0 ); + return new ImportStreamSource( $file ); + } else { + return new WikiErrorMsg( 'importcantopen' ); + } + } + + public static function newFromInterwiki( $interwiki, $page, $history=false ) { + if( $page == '' ) { + return new WikiErrorMsg( 'import-noarticle' ); + } + $link = Title::newFromText( "$interwiki:Special:Export/$page" ); + if( is_null( $link ) || $link->getInterwiki() == '' ) { + return new WikiErrorMsg( 'importbadinterwiki' ); + } else { + $params = $history ? 'history=1' : ''; + $url = $link->getFullUrl( $params ); + # For interwikis, use POST to avoid redirects. + return ImportStreamSource::newFromURL( $url, "POST" ); + } + } +} diff --git a/includes/Interwiki.php b/includes/Interwiki.php new file mode 100644 index 00000000..3522fadb --- /dev/null +++ b/includes/Interwiki.php @@ -0,0 +1,207 @@ +<?php +/** + * @file + * Interwiki table entry + */ + +/** + * The interwiki class + * All information is loaded on creation when called by Interwiki::fetch( $prefix ). + * All work is done on slave, because this should *never* change (except during schema updates etc, which arent wiki-related) + */ +class Interwiki { + + // Cache - removes oldest entry when it hits limit + protected static $smCache = array(); + const CACHE_LIMIT = 100; // 0 means unlimited, any other value is max number of entries. + + protected $mPrefix, $mURL, $mLocal, $mTrans; + + function __construct( $prefix = null, $url = '', $local = 0, $trans = 0 ) + { + $this->mPrefix = $prefix; + $this->mURL = $url; + $this->mLocal = $local; + $this->mTrans = $trans; + } + + /** + * Check whether an interwiki prefix exists + * + * @return bool Whether it exists + * @param $prefix string Interwiki prefix to use + */ + static public function isValidInterwiki( $prefix ){ + $result = self::fetch( $prefix ); + return (bool)$result; + } + + /** + * Fetch an Interwiki object + * + * @return Interwiki Object, or null if not valid + * @param $prefix string Interwiki prefix to use + */ + static public function fetch( $prefix ) { + global $wgContLang; + if( $prefix == '' ) { + return null; + } + $prefix = $wgContLang->lc( $prefix ); + if( isset( self::$smCache[$prefix] ) ){ + return self::$smCache[$prefix]; + } + global $wgInterwikiCache; + if ($wgInterwikiCache) { + $iw = Interwiki::getInterwikiCached( $prefix ); + } else { + $iw = Interwiki::load( $prefix ); + if( !$iw ){ + $iw = false; + } + } + if( self::CACHE_LIMIT && count( self::$smCache ) >= self::CACHE_LIMIT ){ + reset( self::$smCache ); + unset( self::$smCache[ key( self::$smCache ) ] ); + } + self::$smCache[$prefix] = $iw; + return $iw; + } + + /** + * Fetch interwiki prefix data from local cache in constant database. + * + * @note More logic is explained in DefaultSettings. + * + * @param $prefix \type{\string} Interwiki prefix + * @return \type{\Interwiki} An interwiki object + */ + protected static function getInterwikiCached( $prefix ) { + $value = self::getInterwikiCacheEntry( $prefix ); + + $s = new Interwiki( $prefix ); + if ( $value != '' ) { + // Split values + list( $local, $url ) = explode( ' ', $value, 2 ); + $s->mURL = $url; + $s->mLocal = (int)$local; + }else{ + $s = false; + } + return $s; + } + + /** + * Get entry from interwiki cache + * + * @note More logic is explained in DefaultSettings. + * + * @param $prefix \type{\string} Database key + * @return \type{\string) The entry + */ + protected static function getInterwikiCacheEntry( $prefix ){ + global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite; + static $db, $site; + + wfDebug( __METHOD__ . "( $prefix )\n" ); + if( !$db ){ + $db = dba_open( $wgInterwikiCache, 'r', 'cdb' ); + } + /* Resolve site name */ + if( $wgInterwikiScopes>=3 && !$site ) { + $site = dba_fetch( '__sites:' . wfWikiID(), $db ); + if ( $site == "" ){ + $site = $wgInterwikiFallbackSite; + } + } + + $value = dba_fetch( wfMemcKey( $prefix ), $db ); + // Site level + if ( $value == '' && $wgInterwikiScopes >= 3 ) { + $value = dba_fetch( "_{$site}:{$prefix}", $db ); + } + // Global Level + if ( $value == '' && $wgInterwikiScopes >= 2 ) { + $value = dba_fetch( "__global:{$prefix}", $db ); + } + if ( $value == 'undef' ) + $value = ''; + + return $value; + } + + /** + * Load the interwiki, trying first memcached then the DB + * + * @param $prefix The interwiki prefix + * @return bool The prefix is valid + * @static + * + */ + protected static function load( $prefix ) { + global $wgMemc, $wgInterwikiExpiry; + $key = wfMemcKey( 'interwiki', $prefix ); + $mc = $wgMemc->get( $key ); + $iw = false; + if( $mc && is_array( $mc ) ){ // is_array is hack for old keys + $iw = Interwiki::loadFromArray( $mc ); + if( $iw ){ + return $iw; + } + } + + $db = wfGetDB( DB_SLAVE ); + + $row = $db->fetchRow( $db->select( 'interwiki', '*', array( 'iw_prefix' => $prefix ), + __METHOD__ ) ); + $iw = Interwiki::loadFromArray( $row ); + if ( $iw ) { + $mc = array( 'iw_url' => $iw->mURL, 'iw_local' => $iw->mLocal, 'iw_trans' => $iw->mTrans ); + $wgMemc->add( $key, $mc, $wgInterwikiExpiry ); + return $iw; + } + + return false; + } + + /** + * Fill in member variables from an array (e.g. memcached result, Database::fetchRow, etc) + * + * @return bool Whether everything was there + * @param $res ResultWrapper Row from the interwiki table + * @static + */ + protected static function loadFromArray( $mc ) { + if( isset( $mc['iw_url'] ) && isset( $mc['iw_local'] ) && isset( $mc['iw_trans'] ) ){ + $iw = new Interwiki(); + $iw->mURL = $mc['iw_url']; + $iw->mLocal = $mc['iw_local']; + $iw->mTrans = $mc['iw_trans']; + return $iw; + } + return false; + } + + /** + * Get the URL for a particular title (or with $1 if no title given) + * + * @param $title string What text to put for the article name + * @return string The URL + */ + function getURL( $title = null ){ + $url = $this->mURL; + if( $title != null ){ + $url = str_replace( "$1", $title, $url ); + } + return $url; + } + + function isLocal(){ + return $this->mLocal; + } + + function isTranscludable(){ + return $this->mTrans; + } + +} diff --git a/includes/JobQueue.php b/includes/JobQueue.php index 8bfd1b3e..afa757d7 100644 --- a/includes/JobQueue.php +++ b/includes/JobQueue.php @@ -127,7 +127,7 @@ abstract class Job { // Failed, someone else beat us to it // Try getting a random row $row = $dbw->selectRow( 'job', array( 'MIN(job_id) as minjob', - 'MAX(job_id) as maxjob' ), "job_id >= $offset", __METHOD__ ); + 'MAX(job_id) as maxjob' ), '1=1', __METHOD__ ); if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) { // No jobs to get wfProfileOut( __METHOD__ ); diff --git a/includes/Licenses.php b/includes/Licenses.php index e76ac23c..6398c887 100644 --- a/includes/Licenses.php +++ b/includes/Licenses.php @@ -121,7 +121,7 @@ class Licenses { function outputOption( $val, $attribs = null, $depth ) { $val = str_repeat( /*   */ "\xc2\xa0", $depth * 2 ) . $val; - return str_repeat( "\t", $depth ) . wfElement( 'option', $attribs, $val ) . "\n"; + return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n"; } function msg( $str ) { diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php index bdc4b43a..d9a9666d 100644 --- a/includes/LinkBatch.php +++ b/includes/LinkBatch.php @@ -134,7 +134,7 @@ class LinkBatch { $sql = "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect FROM $page WHERE $set"; // Do query - $res = new ResultWrapper( $dbr, $dbr->query( $sql, __METHOD__ ) ); + $res = $dbr->query( $sql, __METHOD__ ); wfProfileOut( __METHOD__ ); return $res; } diff --git a/includes/LinkCache.php b/includes/LinkCache.php index 79727615..4f74cdd7 100644 --- a/includes/LinkCache.php +++ b/includes/LinkCache.php @@ -9,7 +9,6 @@ class LinkCache { // becomes incompatible with the new version. /* private */ var $mClassVer = 4; - /* private */ var $mPageLinks; /* private */ var $mGoodLinks, $mBadLinks; /* private */ var $mForUpdate; @@ -26,7 +25,6 @@ class LinkCache { function __construct() { $this->mForUpdate = false; - $this->mPageLinks = array(); $this->mGoodLinks = array(); $this->mGoodLinkFields = array(); $this->mBadLinks = array(); @@ -78,14 +76,12 @@ class LinkCache { $dbkey = $title->getPrefixedDbKey(); $this->mGoodLinks[$dbkey] = $id; $this->mGoodLinkFields[$dbkey] = array( 'length' => $len, 'redirect' => $redir ); - $this->mPageLinks[$dbkey] = $title; } public function addBadLinkObj( $title ) { $dbkey = $title->getPrefixedDbKey(); - if ( ! $this->isBadLink( $dbkey ) ) { + if ( !$this->isBadLink( $dbkey ) ) { $this->mBadLinks[$dbkey] = 1; - $this->mPageLinks[$dbkey] = $title; } } @@ -93,10 +89,19 @@ class LinkCache { unset( $this->mBadLinks[$title] ); } - /* obsolete, for old $wgLinkCacheMemcached stuff */ - public function clearLink( $title ) {} + public function clearLink( $title ) { + $dbkey = $title->getPrefixedDbKey(); + if( isset($this->mBadLinks[$dbkey]) ) { + unset($this->mBadLinks[$dbkey]); + } + if( isset($this->mGoodLinks[$dbkey]) ) { + unset($this->mGoodLinks[$dbkey]); + } + if( isset($this->mGoodLinkFields[$dbkey]) ) { + unset($this->mGoodLinkFields[$dbkey]); + } + } - public function getPageLinks() { return $this->mPageLinks; } public function getGoodLinks() { return $this->mGoodLinks; } public function getBadLinks() { return array_keys( $this->mBadLinks ); } @@ -125,27 +130,24 @@ class LinkCache { */ public function addLinkObj( &$nt, $len = -1, $redirect = NULL ) { global $wgAntiLockFlags, $wgProfiler; + wfProfileIn( __METHOD__ ); - $title = $nt->getPrefixedDBkey(); - if ( $this->isBadLink( $title ) ) { return 0; } - $id = $this->getGoodLinkID( $title ); - if ( 0 != $id ) { return $id; } - - $fname = 'LinkCache::addLinkObj'; - if ( isset( $wgProfiler ) ) { - $fname .= ' (' . $wgProfiler->getCurrentSection() . ')'; + $key = $nt->getPrefixedDBkey(); + if ( $this->isBadLink( $key ) ) { + wfProfileOut( __METHOD__ ); + return 0; + } + $id = $this->getGoodLinkID( $key ); + if ( $id != 0 ) { + wfProfileOut( __METHOD__ ); + return $id; } - wfProfileIn( $fname ); - - $ns = $nt->getNamespace(); - $t = $nt->getDBkey(); - - if ( '' == $title ) { - wfProfileOut( $fname ); + if ( $key === '' ) { + wfProfileOut( __METHOD__ ); return 0; } - + # Some fields heavily used for linking... if ( $this->mForUpdate ) { $db = wfGetDB( DB_MASTER ); @@ -161,19 +163,24 @@ class LinkCache { $s = $db->selectRow( 'page', array( 'page_id', 'page_len', 'page_is_redirect' ), - array( 'page_namespace' => $ns, 'page_title' => $t ), - $fname, $options ); + array( 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ), + __METHOD__, $options ); # Set fields... - $id = $s ? $s->page_id : 0; - $len = $s ? $s->page_len : -1; - $redirect = $s ? $s->page_is_redirect : 0; + if ( $s !== false ) { + $id = $s->page_id; + $len = $s->page_len; + $redirect = $s->page_is_redirect; + } else { + $len = -1; + $redirect = 0; + } - if( 0 == $id ) { + if ( $id == 0 ) { $this->addBadLinkObj( $nt ); } else { $this->addGoodLinkObj( $id, $nt, $len, $redirect ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $id; } @@ -181,7 +188,6 @@ class LinkCache { * Clears cache */ public function clear() { - $this->mPageLinks = array(); $this->mGoodLinks = array(); $this->mGoodLinkFields = array(); $this->mBadLinks = array(); diff --git a/includes/Linker.php b/includes/Linker.php index 32c506a4..f116fb4a 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -21,6 +21,7 @@ class Linker { * @deprecated */ function postParseLinkColour( $s = null ) { + wfDeprecated( __METHOD__ ); return null; } @@ -123,7 +124,9 @@ class Linker { if ( $t->isRedirect() ) { # Page is a redirect $colour = 'mw-redirect'; - } elseif ( $threshold > 0 && $t->getLength() < $threshold && MWNamespace::isContent( $t->getNamespace() ) ) { + } elseif ( $threshold > 0 && + $t->exists() && $t->getLength() < $threshold && + MWNamespace::isContent( $t->getNamespace() ) ) { # Page is a stub $colour = 'stub'; } @@ -131,6 +134,194 @@ class Linker { } /** + * This function returns an HTML link to the given target. It serves a few + * purposes: + * 1) If $target is a Title, the correct URL to link to will be figured + * out automatically. + * 2) It automatically adds the usual classes for various types of link + * targets: "new" for red links, "stub" for short articles, etc. + * 3) It escapes all attribute values safely so there's no risk of XSS. + * 4) It provides a default tooltip if the target is a Title (the page + * name of the target). + * link() replaces the old functions in the makeLink() family. + * + * @param $target Title Can currently only be a Title, but this may + * change to support Images, literal URLs, etc. + * @param $text string The HTML contents of the <a> element, i.e., + * the link text. This is raw HTML and will not be escaped. If null, + * defaults to the prefixed text of the Title; or if the Title is just a + * fragment, the contents of the fragment. + * @param $customAttribs array A key => value array of extra HTML attri- + * butes, such as title and class. (href is ignored.) Classes will be + * merged with the default classes, while other attributes will replace + * default attributes. All passed attribute values will be HTML-escaped. + * A false attribute value means to suppress that attribute. + * @param $query array The query string to append to the URL + * you're linking to, in key => value array form. Query keys and values + * will be URL-encoded. + * @param $options mixed String or array of strings: + * 'known': Page is known to exist, so don't check if it does. + * 'broken': Page is known not to exist, so don't check if it does. + * 'noclasses': Don't add any classes automatically (includes "new", + * "stub", "mw-redirect", "extiw"). Only use the class attribute + * provided, if any, so you get a simple blue link with no funny i- + * cons. + * 'forcearticlepath': Use the article path always, even with a querystring. + * Has compatibility issues on some setups, so avoid wherever possible. + * @return string HTML <a> attribute + */ + public function link( $target, $text = null, $customAttribs = array(), $query = array(), $options = array() ) { + wfProfileIn( __METHOD__ ); + if( !$target instanceof Title ) { + return "<!-- ERROR -->$text"; + } + $options = (array)$options; + + $ret = null; + if( !wfRunHooks( 'LinkBegin', array( $this, $target, &$text, + &$customAttribs, &$query, &$options, &$ret ) ) ) { + wfProfileOut( __METHOD__ ); + return $ret; + } + + # Normalize the Title if it's a special page + $target = $this->normaliseSpecialPage( $target ); + + # If we don't know whether the page exists, let's find out. + wfProfileIn( __METHOD__ . '-checkPageExistence' ); + if( !in_array( 'known', $options ) and !in_array( 'broken', $options ) ) { + if( $target->isKnown() ) { + $options []= 'known'; + } else { + $options []= 'broken'; + } + } + wfProfileOut( __METHOD__ . '-checkPageExistence' ); + + $oldquery = array(); + if( in_array( "forcearticlepath", $options ) && $query ){ + $oldquery = $query; + $query = array(); + } + + # Note: we want the href attribute first, for prettiness. + $attribs = array( 'href' => $this->linkUrl( $target, $query, $options ) ); + if( in_array( 'forcearticlepath', $options ) && $oldquery ){ + $attribs['href'] = wfAppendQuery( $attribs['href'], wfArrayToCgi( $oldquery ) ); + } + + $attribs = array_merge( + $attribs, + $this->linkAttribs( $target, $customAttribs, $options ) + ); + if( is_null( $text ) ) { + $text = $this->linkText( $target ); + } + + $ret = null; + if( wfRunHooks( 'LinkEnd', array( $this, $target, $options, &$text, &$attribs, &$ret ) ) ) { + $ret = Xml::openElement( 'a', $attribs ) . $text . Xml::closeElement( 'a' ); + } + + wfProfileOut( __METHOD__ ); + return $ret; + } + + private function linkUrl( $target, $query, $options ) { + wfProfileIn( __METHOD__ ); + # We don't want to include fragments for broken links, because they + # generally make no sense. + if( in_array( 'broken', $options ) and $target->mFragment !== '' ) { + $target = clone $target; + $target->mFragment = ''; + } + + # If it's a broken link, add the appropriate query pieces, unless + # there's already an action specified, or unless 'edit' makes no sense + # (i.e., for a nonexistent special page). + if( in_array( 'broken', $options ) and empty( $query['action'] ) + and $target->getNamespace() != NS_SPECIAL ) { + $query['action'] = 'edit'; + $query['redlink'] = '1'; + } + $ret = $target->getLinkUrl( $query ); + wfProfileOut( __METHOD__ ); + return $ret; + } + + private function linkAttribs( $target, $attribs, $options ) { + wfProfileIn( __METHOD__ ); + global $wgUser; + $defaults = array(); + + if( !in_array( 'noclasses', $options ) ) { + wfProfileIn( __METHOD__ . '-getClasses' ); + # Now build the classes. + $classes = array(); + + if( in_array( 'broken', $options ) ) { + $classes[] = 'new'; + } + + if( $target->isExternal() ) { + $classes[] = 'extiw'; + } + + # Note that redirects never count as stubs here. + if ( $target->isRedirect() ) { + $classes[] = 'mw-redirect'; + } elseif( $target->isContentPage() ) { + # Check for stub. + $threshold = $wgUser->getOption( 'stubthreshold' ); + if( $threshold > 0 and $target->exists() and $target->getLength() < $threshold ) { + $classes[] = 'stub'; + } + } + if( $classes != array() ) { + $defaults['class'] = implode( ' ', $classes ); + } + wfProfileOut( __METHOD__ . '-getClasses' ); + } + + # Get a default title attribute. + if( in_array( 'known', $options ) ) { + $defaults['title'] = $target->getPrefixedText(); + } else { + $defaults['title'] = wfMsg( 'red-link-title', $target->getPrefixedText() ); + } + + # Finally, merge the custom attribs with the default ones, and iterate + # over that, deleting all "false" attributes. + $ret = array(); + $merged = Sanitizer::mergeAttributes( $defaults, $attribs ); + foreach( $merged as $key => $val ) { + # A false value suppresses the attribute, and we don't want the + # href attribute to be overridden. + if( $key != 'href' and $val !== false ) { + $ret[$key] = $val; + } + } + wfProfileOut( __METHOD__ ); + return $ret; + } + + private function linkText( $target ) { + # We might be passed a non-Title by make*LinkObj(). Fail gracefully. + if( !$target instanceof Title ) { + return ''; + } + + # If the target is just a fragment, with no title, we return the frag- + # ment text. Otherwise, we return the title text itself. + if( $target->getPrefixedText() === '' and $target->getFragment() !== '' ) { + return htmlspecialchars( $target->getFragment() ); + } + return htmlspecialchars( $target->getPrefixedText() ); + } + + /** + * @deprecated Use link() + * * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeLinkObj for further documentation. * @@ -156,6 +347,8 @@ class Linker { } /** + * @deprecated Use link() + * * This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeKnownLinkObj for further documentation. * @@ -177,6 +370,8 @@ class Linker { } /** + * @deprecated Use link() + * * This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeBrokenLinkObj for further documentation. * @@ -198,7 +393,7 @@ class Linker { } /** - * @deprecated use makeColouredLinkObj + * @deprecated Use link() * * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeStubLinkObj for further documentation. @@ -211,6 +406,7 @@ class Linker { * the end of the link. */ function makeStubLink( $title, $text = '', $query = '', $trail = '' ) { + wfDeprecated( __METHOD__ ); $nt = Title::newFromText( $title ); if ( $nt instanceof Title ) { return $this->makeStubLinkObj( $nt, $text, $query, $trail ); @@ -221,6 +417,8 @@ class Linker { } /** + * @deprecated Use link() + * * Make a link for a title which may or may not be in the database. If you need to * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each * call to this will result in a DB query. @@ -238,67 +436,21 @@ class Linker { global $wgUser; wfProfileIn( __METHOD__ ); - if ( !$nt instanceof Title ) { - # Fail gracefully - wfProfileOut( __METHOD__ ); - return "<!-- ERROR -->{$prefix}{$text}{$trail}"; + $query = wfCgiToArray( $query ); + list( $inside, $trail ) = Linker::splitTrail( $trail ); + if( $text === '' ) { + $text = $this->linkText( $nt ); } - if ( $nt->isExternal() ) { - $u = $nt->getFullURL(); - $link = $nt->getPrefixedURL(); - if ( '' == $text ) { $text = $nt->getPrefixedText(); } - $style = $this->getInterwikiLinkAttributes( $link, $text, 'extiw' ); - - $inside = ''; - if ( '' != $trail ) { - $m = array(); - if ( preg_match( '/^([a-z]+)(.*)$$/sD', $trail, $m ) ) { - $inside = $m[1]; - $trail = $m[2]; - } - } - $t = "<a href=\"{$u}\"{$style}>{$text}{$inside}</a>"; - - wfProfileOut( __METHOD__ ); - return $t; - } elseif ( $nt->isAlwaysKnown() ) { - # Image links, special page links and self-links with fragments are always known. - $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); - } else { - wfProfileIn( __METHOD__.'-immediate' ); + $ret = $this->link( $nt, "$prefix$text$inside", array(), $query ) . $trail; - # Handles links to special pages which do not exist in the database: - if( $nt->getNamespace() == NS_SPECIAL ) { - if( SpecialPage::exists( $nt->getDBkey() ) ) { - $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); - } else { - $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix ); - } - wfProfileOut( __METHOD__.'-immediate' ); - wfProfileOut( __METHOD__ ); - return $retVal; - } - - # Work out link colour immediately - $aid = $nt->getArticleID() ; - if ( 0 == $aid ) { - $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix ); - } else { - $colour = ''; - if ( $nt->isContentPage() ) { - $threshold = $wgUser->getOption('stubthreshold'); - $colour = $this->getLinkColour( $nt, $threshold ); - } - $retVal = $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix ); - } - wfProfileOut( __METHOD__.'-immediate' ); - } wfProfileOut( __METHOD__ ); - return $retVal; + return $ret; } /** + * @deprecated Use link() + * * Make a link for a title which definitely exists. This is faster than makeLinkObj because * it doesn't have to do a database query. It's also valid for interwiki titles and special * pages. @@ -315,40 +467,26 @@ class Linker { function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) { wfProfileIn( __METHOD__ ); - if ( !$title instanceof Title ) { - # Fail gracefully - wfProfileOut( __METHOD__ ); - return "<!-- ERROR -->{$prefix}{$text}{$trail}"; - } - - $nt = $this->normaliseSpecialPage( $title ); - - $u = $nt->escapeLocalURL( $query ); - if ( $nt->getFragment() != '' ) { - if( $nt->getPrefixedDbkey() == '' ) { - $u = ''; - if ( '' == $text ) { - $text = htmlspecialchars( $nt->getFragment() ); - } - } - $u .= $nt->getFragmentForURL(); - } if ( $text == '' ) { - $text = htmlspecialchars( $nt->getPrefixedText() ); - } - if ( $style == '' ) { - $style = $this->getInternalLinkAttributesObj( $nt, $text ); + $text = $this->linkText( $title ); } + $attribs = Sanitizer::mergeAttributes( + Sanitizer::decodeTagAttributes( $aprops ), + Sanitizer::decodeTagAttributes( $style ) + ); + $query = wfCgiToArray( $query ); + list( $inside, $trail ) = Linker::splitTrail( $trail ); - if ( $aprops !== '' ) $aprops = ' ' . $aprops; + $ret = $this->link( $title, "$prefix$text$inside", $attribs, $query, + array( 'known', 'noclasses' ) ) . $trail; - list( $inside, $trail ) = Linker::splitTrail( $trail ); - $r = "<a href=\"{$u}\"{$style}{$aprops}>{$prefix}{$text}{$inside}</a>{$trail}"; wfProfileOut( __METHOD__ ); - return $r; + return $ret; } /** + * @deprecated Use link() + * * Make a red link to the edit page of a given title. * * @param $nt Title object of the target page @@ -361,40 +499,21 @@ class Linker { function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) { wfProfileIn( __METHOD__ ); - if ( !$title instanceof Title ) { - # Fail gracefully - wfProfileOut( __METHOD__ ); - return "<!-- ERROR -->{$prefix}{$text}{$trail}"; + list( $inside, $trail ) = Linker::splitTrail( $trail ); + if( $text === '' ) { + $text = $this->linkText( $title ); } - $nt = $this->normaliseSpecialPage( $title ); - if( $nt->getNamespace() == NS_SPECIAL ) { - $q = $query; - } else if ( '' == $query ) { - $q = 'action=edit&redlink=1'; - } else { - $q = 'action=edit&redlink=1&'.$query; - } - $u = $nt->escapeLocalURL( $q ); - - $titleText = $nt->getPrefixedText(); - if ( '' == $text ) { - $text = htmlspecialchars( $titleText ); - } - $titleAttr = wfMsg( 'red-link-title', $titleText ); - $style = $this->getInternalLinkAttributesObj( $nt, $text, 'new', $titleAttr ); - list( $inside, $trail ) = Linker::splitTrail( $trail ); - - wfRunHooks( 'BrokenLink', array( &$this, $nt, $query, &$u, &$style, &$prefix, &$text, &$inside, &$trail ) ); - $s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}"; + $ret = $this->link( $title, "$prefix$text$inside", array(), + wfCgiToArray( $query ), 'broken' ) . $trail; wfProfileOut( __METHOD__ ); - return $s; + return $ret; } /** - * @deprecated use makeColouredLinkObj + * @deprecated Use link() * * Make a brown link to a short article. * @@ -406,10 +525,13 @@ class Linker { * the end of the link. */ function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + wfDeprecated( __METHOD__ ); return $this->makeColouredLinkObj( $nt, 'stub', $text, $query, $trail, $prefix ); } /** + * @deprecated Use link() + * * Make a coloured link. * * @param $nt Title object of the target page @@ -421,7 +543,6 @@ class Linker { * the end of the link. */ function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) { - if($colour != ''){ $style = $this->getInternalLinkAttributesObj( $nt, $text, $colour ); } else $style = ''; @@ -464,7 +585,9 @@ class Linker { if ( $title->getNamespace() == NS_SPECIAL ) { list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $title->getDBkey() ); if ( !$name ) return $title; - return SpecialPage::getTitleFor( $name, $subpage ); + $ret = SpecialPage::getTitleFor( $name, $subpage ); + $ret->mFragment = $title->getFragment(); + return $ret; } else { return $title; } @@ -483,6 +606,7 @@ class Linker { /** Obsolete alias */ function makeImage( $url, $alt = '' ) { + wfDeprecated( __METHOD__ ); return $this->makeExternalImage( $url, $alt ); } @@ -564,6 +688,9 @@ class Linker { * bottom, text-bottom) * alt Alternate text for image (i.e. alt attribute). Plain text. * caption HTML for image caption. + * link-url URL to link to + * link-title Title object to link to + * no-link Boolean, suppress description link * * @param array $handlerParams Associative array of media handler parameters, to be passed * to transform(). Typical keys are "width" and "page". @@ -581,7 +708,7 @@ class Linker { global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright; if ( $file && !$file->allowInlineDisplay() ) { wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" ); - return $this->makeKnownLinkObj( $title ); + return $this->link( $title ); } // Shortcuts @@ -592,11 +719,12 @@ class Linker { $page = isset( $hp['page'] ) ? $hp['page'] : false; if ( !isset( $fp['align'] ) ) $fp['align'] = ''; if ( !isset( $fp['alt'] ) ) $fp['alt'] = ''; + # Backward compatibility, title used to always be equal to alt text + if ( !isset( $fp['title'] ) ) $fp['title'] = $fp['alt']; $prefix = $postfix = ''; - if ( 'center' == $fp['align'] ) - { + if ( 'center' == $fp['align'] ) { $prefix = '<div class="center">'; $postfix = '</div>'; $fp['align'] = 'none'; @@ -627,7 +755,6 @@ class Linker { } if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) { - # Create a thumbnail. Alignment depends on language # writing direction, # right aligned for left-to-right- # languages ("Western languages"), left-aligned @@ -660,15 +787,26 @@ class Linker { if ( !$thumb ) { $s = $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true ); } else { - $s = $thumb->toHtml( array( - 'desc-link' => true, - 'desc-query' => $query, + $params = array( 'alt' => $fp['alt'], + 'title' => $fp['title'], 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false , - 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ) ); + 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ); + if ( !empty( $fp['link-url'] ) ) { + $params['custom-url-link'] = $fp['link-url']; + } elseif ( !empty( $fp['link-title'] ) ) { + $params['custom-title-link'] = $fp['link-title']; + } elseif ( !empty( $fp['no-link'] ) ) { + // No link + } else { + $params['desc-link'] = true; + $params['desc-query'] = $query; + } + + $s = $thumb->toHtml( $params ); } if ( '' != $fp['align'] ) { - $s = "<div class=\"float{$fp['align']}\"><span>{$s}</span></div>"; + $s = "<div class=\"float{$fp['align']}\">{$s}</div>"; } return str_replace("\n", ' ',$prefix.$s.$postfix); } @@ -700,6 +838,8 @@ class Linker { $page = isset( $hp['page'] ) ? $hp['page'] : false; if ( !isset( $fp['align'] ) ) $fp['align'] = 'right'; if ( !isset( $fp['alt'] ) ) $fp['alt'] = ''; + # Backward compatibility, title used to always be equal to alt text + if ( !isset( $fp['title'] ) ) $fp['title'] = $fp['alt']; if ( !isset( $fp['caption'] ) ) $fp['caption'] = ''; if ( empty( $hp['width'] ) ) { @@ -713,7 +853,7 @@ class Linker { } else { if ( isset( $fp['manualthumb'] ) ) { # Use manually specified thumbnail - $manual_title = Title::makeTitleSafe( NS_IMAGE, $fp['manualthumb'] ); + $manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] ); if( $manual_title ) { $manual_img = wfFindFile( $manual_title ); if ( $manual_img ) { @@ -759,6 +899,7 @@ class Linker { } else { $s .= $thumb->toHtml( array( 'alt' => $fp['alt'], + 'title' => $fp['title'], 'img-class' => 'thumbimage', 'desc-link' => true, 'desc-query' => $query ) ); @@ -818,7 +959,7 @@ class Linker { /** @deprecated use Linker::makeMediaLinkObj() */ function makeMediaLink( $name, $unused = '', $text = '', $time = false ) { - $nt = Title::makeTitleSafe( NS_IMAGE, $name ); + $nt = Title::makeTitleSafe( NS_FILE, $name ); return $this->makeMediaLinkObj( $nt, $text, $time ); } @@ -867,11 +1008,10 @@ class Linker { } /** @todo document */ - function makeExternalLink( $url, $text, $escape = true, $linktype = '', $ns = null ) { - $style = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype ); - global $wgNoFollowLinks, $wgNoFollowNsExceptions; - if( $wgNoFollowLinks && !(isset($ns) && in_array($ns, $wgNoFollowNsExceptions)) ) { - $style .= ' rel="nofollow"'; + function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) { + $attribsText = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype ); + if ( $attribs ) { + $attribsText .= Xml::expandAttributes( $attribs ); } $url = htmlspecialchars( $url ); if( $escape ) { @@ -883,7 +1023,7 @@ class Linker { wfDebug("Hook LinkerMakeExternalLink changed the output of link with url {$url} and text {$text} to {$link}", true); return $link; } - return '<a href="'.$url.'"'.$style.'>'.$text.'</a>'; + return '<a href="'.$url.'"'.$attribsText.'>'.$text.'</a>'; } /** @@ -894,15 +1034,12 @@ class Linker { * @private */ function userLink( $userId, $userText ) { - $encName = htmlspecialchars( $userText ); if( $userId == 0 ) { - $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); - return $this->makeKnownLinkObj( $contribsPage, - $encName); + $page = SpecialPage::getTitleFor( 'Contributions', $userText ); } else { - $userPage = Title::makeTitle( NS_USER, $userText ); - return $this->makeLinkObj( $userPage, $encName ); + $page = Title::makeTitle( NS_USER, $userText ); } + return $this->link( $page, htmlspecialchars( $userText ), array( 'class' => 'mw-userlink' ) ); } /** @@ -926,22 +1063,23 @@ class Linker { } if( $userId ) { // check if the user has an edit + $attribs = array(); if( $redContribsWhenNoEdits ) { $count = !is_null($edits) ? $edits : User::edits( $userId ); - $style = ($count == 0) ? " class='new'" : ''; - } else { - $style = ''; + if( $count == 0 ) { + $attribs['class'] = 'new'; + } } $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); - $items[] = $this->makeKnownLinkObj( $contribsPage, wfMsgHtml( 'contribslink' ), '', '', '', '', $style ); + $items[] = $this->link( $contribsPage, wfMsgHtml( 'contribslink' ), $attribs ); } if( $blockable && $wgUser->isAllowed( 'block' ) ) { $items[] = $this->blockLink( $userId, $userText ); } if( $items ) { - return ' (' . implode( ' | ', $items ) . ')'; + return ' <span class="mw-usertoollinks">(' . implode( ' | ', $items ) . ')</span>'; } else { return ''; } @@ -966,7 +1104,7 @@ class Linker { */ function userTalkLink( $userId, $userText ) { $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText ); - $userTalkLink = $this->makeLinkObj( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) ); + $userTalkLink = $this->link( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) ); return $userTalkLink; } @@ -978,8 +1116,7 @@ class Linker { */ function blockLink( $userId, $userText ) { $blockPage = SpecialPage::getTitleFor( 'Blockip', $userText ); - $blockLink = $this->makeKnownLinkObj( $blockPage, - wfMsgHtml( 'blocklink' ) ); + $blockLink = $this->link( $blockPage, wfMsgHtml( 'blocklink' ) ); return $blockLink; } @@ -993,7 +1130,8 @@ class Linker { if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) { $link = wfMsgHtml( 'rev-deleted-user' ); } else if( $rev->userCan( Revision::DELETED_USER ) ) { - $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ); + $link = $this->userLink( $rev->getUser( Revision::FOR_THIS_USER ), + $rev->getUserText( Revision::FOR_THIS_USER ) ); } else { $link = wfMsgHtml( 'rev-deleted-user' ); } @@ -1013,8 +1151,10 @@ class Linker { if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) { $link = wfMsgHtml( 'rev-deleted-user' ); } else if( $rev->userCan( Revision::DELETED_USER ) ) { - $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) . - ' ' . $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() ); + $userId = $rev->getUser( Revision::FOR_THIS_USER ); + $userText = $rev->getUserText( Revision::FOR_THIS_USER ); + $link = $this->userLink( $userId, $userText ) . + ' ' . $this->userToolLinks( $userId, $userText ); } else { $link = wfMsgHtml( 'rev-deleted-user' ); } @@ -1045,7 +1185,8 @@ class Linker { # Sanitize text a bit: $comment = str_replace( "\n", " ", $comment ); - $comment = htmlspecialchars( $comment ); + # Allow HTML entities (for bug 13815) + $comment = Sanitizer::escapeHtmlAllowEntities( $comment ); # Render autocomments and make links: $comment = $this->formatAutoComments( $comment, $title, $local ); @@ -1068,45 +1209,63 @@ class Linker { * * @todo Document the $local parameter. */ - private function formatAutocomments( $comment, $title = NULL, $local = false ) { - $match = array(); - while (preg_match('!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment,$match)) { - $pre=$match[1]; - $auto=$match[2]; - $post=$match[3]; - $link=''; - if( $title ) { - $section = $auto; - - # Generate a valid anchor name from the section title. - # Hackish, but should generally work - we strip wiki - # syntax, including the magic [[: that is used to - # "link rather than show" in case of images and - # interlanguage links. - $section = str_replace( '[[:', '', $section ); - $section = str_replace( '[[', '', $section ); - $section = str_replace( ']]', '', $section ); - if ( $local ) { - $sectionTitle = Title::newFromText( '#' . $section); - } else { - $sectionTitle = wfClone( $title ); - $sectionTitle->mFragment = $section; - } - $link = $this->makeKnownLinkObj( $sectionTitle, wfMsgForContent( 'sectionlink' ) ); - } - $auto = $link . $auto; - if( $pre ) { - # written summary $presep autocomment (summary /* section */) - $auto = wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) ) . $auto; + private function formatAutocomments( $comment, $title = null, $local = false ) { + // Bah! + $this->autocommentTitle = $title; + $this->autocommentLocal = $local; + $comment = preg_replace_callback( + '!(.*)/\*\s*(.*?)\s*\*/(.*)!', + array( $this, 'formatAutocommentsCallback' ), + $comment ); + unset( $this->autocommentTitle ); + unset( $this->autocommentLocal ); + return $comment; + } + + private function formatAutocommentsCallback( $match ) { + $title = $this->autocommentTitle; + $local = $this->autocommentLocal; + + $pre=$match[1]; + $auto=$match[2]; + $post=$match[3]; + $link=''; + if( $title ) { + $section = $auto; + + # Generate a valid anchor name from the section title. + # Hackish, but should generally work - we strip wiki + # syntax, including the magic [[: that is used to + # "link rather than show" in case of images and + # interlanguage links. + $section = str_replace( '[[:', '', $section ); + $section = str_replace( '[[', '', $section ); + $section = str_replace( ']]', '', $section ); + if ( $local ) { + $sectionTitle = Title::newFromText( '#' . $section ); + } else { + $sectionTitle = Title::makeTitleSafe( $title->getNamespace(), + $title->getDBkey(), $section ); } - if( $post ) { - # autocomment $postsep written summary (/* section */ summary) - $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) ); + if ( $sectionTitle ) { + $link = $this->link( $sectionTitle, + wfMsgForContent( 'sectionlink' ), array(), array(), + 'noclasses' ); + } else { + $link = ''; } - $auto = '<span class="autocomment">' . $auto . '</span>'; - $comment = $pre . $auto . $post; } - + $auto = "$link$auto"; + if( $pre ) { + # written summary $presep autocomment (summary /* section */) + $auto = wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) ) . $auto; + } + if( $post ) { + # autocomment $postsep written summary (/* section */ summary) + $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) ); + } + $auto = '<span class="autocomment">' . $auto . '</span>'; + $comment = $pre . $auto . $post; return $comment; } @@ -1201,7 +1360,8 @@ class Linker { if( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) { $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>"; } else if( $rev->userCan( Revision::DELETED_COMMENT ) ) { - $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle(), $local ); + $block = $this->commentBlock( $rev->getComment( Revision::FOR_THIS_USER ), + $rev->getTitle(), $local ); } else { $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>"; } @@ -1261,8 +1421,8 @@ class Linker { . "</ul>\n</td></tr></table>" . '<script type="' . $wgJsMimeType . '">' . ' if (window.showTocToggle) {' - . ' var tocShowText = "' . wfEscapeJsString( wfMsg('showtoc') ) . '";' - . ' var tocHideText = "' . wfEscapeJsString( wfMsg('hidetoc') ) . '";' + . ' var tocShowText = "' . Xml::escapeJsString( wfMsg('showtoc') ) . '";' + . ' var tocHideText = "' . Xml::escapeJsString( wfMsg('hidetoc') ) . '";' . ' showTocToggle();' . ' } ' . "</script>\n"; @@ -1276,8 +1436,9 @@ class Linker { * @param $section Integer: section number. */ public function editSectionLinkForOther( $title, $section ) { + wfDeprecated( __METHOD__ ); $title = Title::newFromText( $title ); - return $this->doEditSectionLink( $title, $section, '', 'EditSectionLinkForOther' ); + return $this->doEditSectionLink( $title, $section ); } /** @@ -1285,49 +1446,64 @@ class Linker { * @param $section Integer: section number. * @param $hint Link String: title, or default if omitted or empty */ - public function editSectionLink( Title $nt, $section, $hint='' ) { - if( $hint != '' ) { - $hint = wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ); - $hint = " title=\"$hint\""; - } - return $this->doEditSectionLink( $nt, $section, $hint, 'EditSectionLink' ); + public function editSectionLink( Title $nt, $section, $hint = '' ) { + wfDeprecated( __METHOD__ ); + if( $hint === '' ) { + # No way to pass an actual empty $hint here! The new interface al- + # lows this, so we have to do this for compatibility. + $hint = null; + } + return $this->doEditSectionLink( $nt, $section, $hint ); } /** - * Implement editSectionLink and editSectionLinkForOther. + * Create a section edit link. This supersedes editSectionLink() and + * editSectionLinkForOther(). * - * @param $nt Title object - * @param $section Integer, section number - * @param $hint String, for HTML title attribute - * @param $hook String, name of hook to run - * @return String, HTML to use for edit link + * @param $nt Title The title being linked to (may not be the same as + * $wgTitle, if the section is included from a template) + * @param $section string The designation of the section being pointed to, + * to be included in the link, like "§ion=$section" + * @param $tooltip string The tooltip to use for the link: will be escaped + * and wrapped in the 'editsectionhint' message + * @return string HTML to use for edit link */ - protected function doEditSectionLink( Title $nt, $section, $hint, $hook ) { - global $wgContLang; - $editurl = '§ion='.$section; - $url = $this->makeKnownLinkObj( - $nt, - htmlspecialchars(wfMsg('editsection')), - 'action=edit'.$editurl, - '', '', '', $hint + public function doEditSectionLink( Title $nt, $section, $tooltip = null ) { + $attribs = array(); + if( !is_null( $tooltip ) ) { + $attribs['title'] = wfMsg( 'editsectionhint', $tooltip ); + } + $link = $this->link( $nt, wfMsg('editsection'), + $attribs, + array( 'action' => 'edit', 'section' => $section ), + array( 'noclasses', 'known' ) ); - $result = null; - // The two hooks have slightly different interfaces . . . - if( $hook == 'EditSectionLink' ) { - wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $hint, $url, &$result ) ); - } elseif( $hook == 'EditSectionLinkForOther' ) { - wfRunHooks( 'EditSectionLinkForOther', array( &$this, $nt, $section, $url, &$result ) ); + # Run the old hook. This takes up half of the function . . . hopefully + # we can rid of it someday. + $attribs = ''; + if( $tooltip ) { + $attribs = wfMsgHtml( 'editsectionhint', htmlspecialchars( $tooltip ) ); + $attribs = " title=\"$attribs\""; } - - // For reverse compatibility, add the brackets *after* the hook is run, - // and even add them to hook-provided text. - if( is_null( $result ) ) { - $result = wfMsgHtml( 'editsection-brackets', $url ); - } else { + $result = null; + wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $attribs, $link, &$result ) ); + if( !is_null( $result ) ) { + # For reverse compatibility, add the brackets *after* the hook is + # run, and even add them to hook-provided text. (This is the main + # reason that the EditSectionLink hook is deprecated in favor of + # DoEditSectionLink: it can't change the brackets or the span.) $result = wfMsgHtml( 'editsection-brackets', $result ); + return "<span class=\"editsection\">$result</span>"; } - return "<span class=\"editsection\">$result</span>"; + + # Add the brackets and the span, and *then* run the nice new hook, with + # clean and non-redundant arguments. + $result = wfMsgHtml( 'editsection-brackets', $link ); + $result = "<span class=\"editsection\">$result</span>"; + + wfRunHooks( 'DoEditSectionLink', array( $this, $nt, $section, $tooltip, &$result ) ); + return $result; } /** @@ -1339,11 +1515,21 @@ class Linker { * @param string $anchor The anchor to give the headline (the bit after the #) * @param string $text The text of the header * @param string $link HTML to add for the section edit link + * @param mixed $legacyAnchor A second, optional anchor to give for + * backward compatibility (false to omit) * * @return string HTML headline */ - public function makeHeadline( $level, $attribs, $anchor, $text, $link ) { - return "<a name=\"$anchor\"></a><h$level$attribs$link <span class=\"mw-headline\">$text</span></h$level>"; + public function makeHeadline( $level, $attribs, $anchor, $text, $link, $legacyAnchor = false ) { + $ret = "<a name=\"$anchor\" id=\"$anchor\"></a>" + . "<h$level$attribs" + . $link + . " <span class=\"mw-headline\">$text</span>" + . "</h$level>"; + if ( $legacyAnchor !== false ) { + $ret = "<a name=\"$legacyAnchor\" id=\"$legacyAnchor\"></a>$ret"; + } + return $ret; } /** @@ -1397,14 +1583,19 @@ class Linker { public function buildRollbackLink( $rev ) { global $wgRequest, $wgUser; $title = $rev->getTitle(); - $extra = $wgRequest->getBool( 'bot' ) ? '&bot=1' : ''; - $extra .= '&token=' . urlencode( $wgUser->editToken( array( $title->getPrefixedText(), - $rev->getUserText() ) ) ); - return $this->makeKnownLinkObj( - $title, - wfMsgHtml( 'rollbacklink' ), - 'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extra + $query = array( + 'action' => 'rollback', + 'from' => $rev->getUserText() ); + if( $wgRequest->getBool( 'bot' ) ) { + $query['bot'] = '1'; + $query['hidediff'] = '1'; // bug 15999 + } + $query['token'] = $wgUser->editToken( array( $title->getPrefixedText(), + $rev->getUserText() ) ); + return $this->link( $title, wfMsgHtml( 'rollbacklink' ), + array( 'title' => wfMsg( 'tooltip-rollback' ) ), + $query, array( 'known', 'noclasses' ) ); } /** @@ -1416,12 +1607,9 @@ class Linker { * @param bool $section Whether this is for a section edit * @return string HTML output */ - public function formatTemplates( $templates, $preview = false, $section = false) { - global $wgUser; + public function formatTemplates( $templates, $preview = false, $section = false ) { wfProfileIn( __METHOD__ ); - $sk = $wgUser->getSkin(); - $outText = ''; if ( count( $templates ) > 0 ) { # Do a batch existence check @@ -1440,7 +1628,7 @@ class Linker { } else { $outText .= wfMsgExt( 'templatesused', array( 'parse' ) ); } - $outText .= '</div><ul>'; + $outText .= "</div><ul>\n"; usort( $templates, array( 'Title', 'compare' ) ); foreach ( $templates as $titleObj ) { @@ -1452,7 +1640,12 @@ class Linker { } else { $protected = ''; } - $outText .= '<li>' . $sk->makeLinkObj( $titleObj ) . ' ' . $protected . '</li>'; + if( $titleObj->quickUserCan( 'edit' ) ) { + $editLink = $this->makeLinkObj( $titleObj, wfMsg('editlink'), 'action=edit' ); + } else { + $editLink = $this->makeLinkObj( $titleObj, wfMsg('viewsourcelink'), 'action=edit' ); + } + $outText .= '<li>' . $this->link( $titleObj ) . ' (' . $editLink . ') ' . $protected . '</li>'; } $outText .= '</ul>'; } @@ -1467,21 +1660,19 @@ class Linker { * or similar * @return string HTML output */ - public function formatHiddenCategories( $hiddencats) { - global $wgUser, $wgLang; + public function formatHiddenCategories( $hiddencats ) { + global $wgLang; wfProfileIn( __METHOD__ ); - $sk = $wgUser->getSkin(); - $outText = ''; if ( count( $hiddencats ) > 0 ) { # Construct the HTML $outText = '<div class="mw-hiddenCategoriesExplanation">'; $outText .= wfMsgExt( 'hiddencategories', array( 'parse' ), $wgLang->formatnum( count( $hiddencats ) ) ); - $outText .= '</div><ul>'; + $outText .= "</div><ul>\n"; foreach ( $hiddencats as $titleObj ) { - $outText .= '<li>' . $sk->makeKnownLinkObj( $titleObj ) . '</li>'; # If it's hidden, it must exist - no need to check with a LinkBatch + $outText .= '<li>' . $this->link( $titleObj, null, array(), array(), 'known' ) . "</li>\n"; # If it's hidden, it must exist - no need to check with a LinkBatch } $outText .= '</ul>'; } @@ -1502,38 +1693,37 @@ class Linker { } /** - * Given the id of an interface element, constructs the appropriate title - * and accesskey attributes from the system messages. (Note, this is usu- - * ally the id but isn't always, because sometimes the accesskey needs to - * go on a different element than the id, for reverse-compatibility, etc.) - * - * @param string $name Id of the element, minus prefixes. - * @return string title and accesskey attributes, ready to drop in an - * element (e.g., ' title="This does something [x]" accesskey="x"'). + * @deprecated Returns raw bits of HTML, use titleAttrib() and accesskey() */ public function tooltipAndAccesskey( $name ) { - wfProfileIn( __METHOD__ ); - $attribs = array(); - - $tooltip = wfMsg( "tooltip-$name" ); - if( !wfEmptyMsg( "tooltip-$name", $tooltip ) && $tooltip != '-' ) { - // Compatibility: formerly some tooltips had [alt-.] hardcoded - $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip ); - $attribs['title'] = $tooltip; + # FIXME: If Sanitizer::expandAttributes() treated "false" as "output + # no attribute" instead of "output '' as value for attribute", this + # would be three lines. + $attribs = array( + 'title' => $this->titleAttrib( $name, 'withaccess' ), + 'accesskey' => $this->accesskey( $name ) + ); + if ( $attribs['title'] === false ) { + unset( $attribs['title'] ); } - - $accesskey = wfMsg( "accesskey-$name" ); - if( $accesskey && $accesskey != '-' && - !wfEmptyMsg( "accesskey-$name", $accesskey ) ) { - if( isset( $attribs['title'] ) ) { - $attribs['title'] .= " [$accesskey]"; - } - $attribs['accesskey'] = $accesskey; + if ( $attribs['accesskey'] === false ) { + unset( $attribs['accesskey'] ); } + return Xml::expandAttributes( $attribs ); + } - $ret = Xml::expandAttributes( $attribs ); - wfProfileOut( __METHOD__ ); - return $ret; + /** @deprecated Returns raw bits of HTML, use titleAttrib() */ + public function tooltip( $name, $options = null ) { + # FIXME: If Sanitizer::expandAttributes() treated "false" as "output + # no attribute" instead of "output '' as value for attribute", this + # would be two lines. + $tooltip = $this->titleAttrib( $name, $options ); + if ( $tooltip === false ) { + return ''; + } + return Xml::expandAttributes( array( + 'title' => $this->titleAttrib( $name, $options ) + ) ); } /** @@ -1545,29 +1735,62 @@ class Linker { * @param string $name Id of the element, minus prefixes. * @param mixed $options null or the string 'withaccess' to add an access- * key hint - * @return string title attribute, ready to drop in an element - * (e.g., ' title="This does something"'). + * @return string Contents of the title attribute (which you must HTML- + * escape), or false for no title attribute */ - public function tooltip( $name, $options = null ) { + public function titleAttrib( $name, $options = null ) { wfProfileIn( __METHOD__ ); - $attribs = array(); - $tooltip = wfMsg( "tooltip-$name" ); - if( !wfEmptyMsg( "tooltip-$name", $tooltip ) && $tooltip != '-' ) { - $attribs['title'] = $tooltip; + # Compatibility: formerly some tooltips had [alt-.] hardcoded + $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip ); + + # Message equal to '-' means suppress it. + if ( wfEmptyMsg( "tooltip-$name", $tooltip ) || $tooltip == '-' ) { + $tooltip = false; } - if( isset( $attribs['title'] ) && $options == 'withaccess' ) { - $accesskey = wfMsg( "accesskey-$name" ); - if( $accesskey && $accesskey != '-' && - !wfEmptyMsg( "accesskey-$name", $accesskey ) ) { - $attribs['title'] .= " [$accesskey]"; + if ( $options == 'withaccess' ) { + $accesskey = $this->accesskey( $name ); + if( $accesskey !== false ) { + if ( $tooltip === false || $tooltip === '' ) { + $tooltip = "[$accesskey]"; + } else { + $tooltip .= " [$accesskey]"; + } } } - $ret = Xml::expandAttributes( $attribs ); wfProfileOut( __METHOD__ ); - return $ret; + return $tooltip; + } + + /** + * Given the id of an interface element, constructs the appropriate + * accesskey attribute from the system messages. (Note, this is usually + * the id but isn't always, because sometimes the accesskey needs to go on + * a different element than the id, for reverse-compatibility, etc.) + * + * @param string $name Id of the element, minus prefixes. + * @return string Contents of the accesskey attribute (which you must HTML- + * escape), or false for no accesskey attribute + */ + public function accesskey( $name ) { + wfProfileIn( __METHOD__ ); + + $accesskey = wfMsg( "accesskey-$name" ); + + # FIXME: Per standard MW behavior, a value of '-' means to suppress the + # attribute, but this is broken for accesskey: that might be a useful + # value. + if( $accesskey != '' + && $accesskey != '-' + && !wfEmptyMsg( "accesskey-$name", $accesskey ) ) { + wfProfileOut( __METHOD__ ); + return $accesskey; + } + + wfProfileOut( __METHOD__ ); + return false; } } diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index bb192fb9..13f35b5a 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -20,7 +20,8 @@ class LinksUpdate { $mProperties, //!< Map of arbitrary name to value $mDb, //!< Database connection reference $mOptions, //!< SELECT options to be used (array) - $mRecursive; //!< Whether to queue jobs for recursive updates + $mRecursive, //!< Whether to queue jobs for recursive updates + $mTouchTmplLinks; //!< Whether to queue HTMLCacheUpdate jobs IF recursive /**@}}*/ /** @@ -67,14 +68,24 @@ class LinksUpdate { } $this->mRecursive = $recursive; + $this->mTouchTmplLinks = false; wfRunHooks( 'LinksUpdateConstructed', array( &$this ) ); } + + /** + * Invalidate HTML cache of pages that include this page? + */ + public function setRecursiveTouch( $val ) { + $this->mTouchTmplLinks = (bool)$val; + if( $val ) // Cannot invalidate without queueRecursiveJobs() + $this->mRecursive = true; + } /** * Update link tables with outgoing links from an updated article */ - function doUpdate() { + public function doUpdate() { global $wgUseDumbLinkUpdate; wfRunHooks( 'LinksUpdate', array( &$this ) ); @@ -87,7 +98,7 @@ class LinksUpdate { } - function doIncrementalUpdate() { + protected function doIncrementalUpdate() { wfProfileIn( __METHOD__ ); # Page links @@ -158,7 +169,7 @@ class LinksUpdate { * May be slower or faster depending on level of lock contention and write speed of DB * Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php */ - function doDumbUpdate() { + protected function doDumbUpdate() { wfProfileIn( __METHOD__ ); # Refresh category pages and image description pages @@ -193,34 +204,54 @@ class LinksUpdate { } function queueRecursiveJobs() { + global $wgUpdateRowsPerJob; wfProfileIn( __METHOD__ ); - $batchSize = 100; $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'templatelinks', 'page' ), - array( 'page_namespace', 'page_title' ), - array( - 'page_id=tl_from', + $res = $dbr->select( 'templatelinks', + array( 'tl_from' ), + array( 'tl_namespace' => $this->mTitle->getNamespace(), 'tl_title' => $this->mTitle->getDBkey() ), __METHOD__ ); - $done = false; - while ( !$done ) { - $jobs = array(); - for ( $i = 0; $i < $batchSize; $i++ ) { - $row = $dbr->fetchObject( $res ); - if ( !$row ) { - $done = true; + $numRows = $res->numRows(); + if( !$numRows ) { + wfProfileOut( __METHOD__ ); + return; // nothing to do + } + $numBatches = ceil( $numRows / $wgUpdateRowsPerJob ); + $realBatchSize = $numRows / $numBatches; + $start = false; + $jobs = array(); + do { + for( $i = 0; $i <= $realBatchSize - 1; $i++ ) { + $row = $res->fetchRow(); + if( $row ) { + $id = $row[0]; + } else { + $id = false; break; } - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); - $jobs[] = new RefreshLinksJob( $title, '' ); } - Job::batchInsert( $jobs ); - } + $params = array( + 'start' => $start, + 'end' => ( $id !== false ? $id - 1 : false ), + ); + $jobs[] = new RefreshLinksJob2( $this->mTitle, $params ); + # Hit page caches while we're at it if set to do so... + if( $this->mTouchTmplLinks ) { + $params['table'] = 'templatelinks'; + $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); + } + $start = $id; + } while ( $start ); + $dbr->freeResult( $res ); + + Job::batchInsert( $jobs ); + wfProfileOut( __METHOD__ ); } @@ -286,7 +317,7 @@ class LinksUpdate { } function invalidateImageDescriptions( $images ) { - $this->invalidatePages( NS_IMAGE, array_keys( $images ) ); + $this->invalidatePages( NS_FILE, array_keys( $images ) ); } function dumbTableUpdate( $table, $insertions, $fromField ) { diff --git a/includes/LogEventsList.php b/includes/LogEventsList.php index d49f636b..528bd3aa 100644 --- a/includes/LogEventsList.php +++ b/includes/LogEventsList.php @@ -24,7 +24,7 @@ class LogEventsList { private $out; public $flags; - function __construct( $skin, $out, $flags = 0 ) { + public function __construct( $skin, $out, $flags = 0 ) { $this->skin = $skin; $this->out = $out; $this->flags = $flags; @@ -38,34 +38,38 @@ class LogEventsList { private function preCacheMessages() { // Precache various messages if( !isset( $this->message ) ) { - $messages = 'revertmerge protect_change unblocklink revertmove undeletelink revdel-restore rev-delundel'; - foreach( explode(' ', $messages ) as $msg ) { - $this->message[$msg] = wfMsgExt( $msg, array( 'escape') ); + $messages = array( 'revertmerge', 'protect_change', 'unblocklink', 'change-blocklink', + 'revertmove', 'undeletelink', 'revdel-restore', 'rev-delundel', 'hist', 'pipe-separator' ); + foreach( $messages as $msg ) { + $this->message[$msg] = wfMsgExt( $msg, array( 'escape' ) ); } } } /** * Set page title and show header for this log type - * @param string $type + * @param $type String */ public function showHeader( $type ) { if( LogPage::isLogType( $type ) ) { $this->out->setPageTitle( LogPage::logName( $type ) ); - $this->out->addHtml( LogPage::logHeader( $type ) ); + $this->out->addHTML( LogPage::logHeader( $type ) ); } } /** * Show options for the log list - * @param string $type, - * @param string $user, - * @param string $page, - * @param string $pattern - * @param int $year - * @parm int $month + * @param $type String + * @param $user String + * @param $page String + * @param $pattern String + * @param $year Integer: year + * @param $month Integer: month + * @param $filter Boolean */ - public function showOptions( $type='', $user='', $page='', $pattern='', $year='', $month='' ) { + public function showOptions( $type = '', $user = '', $page = '', $pattern = '', $year = '', + $month = '', $filter = null ) + { global $wgScript, $wgMiserMode; $action = htmlspecialchars( $wgScript ); $title = SpecialPage::getTitleFor( 'Log' ); @@ -79,13 +83,46 @@ class LogEventsList { $this->getTitleInput( $page ) . "\n" . ( !$wgMiserMode ? ($this->getTitlePattern( $pattern )."\n") : "" ) . "<p>" . $this->getDateMenu( $year, $month ) . "\n" . + ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "</p>\n" . - "</fieldset></form>" ); + "</fieldset></form>" + ); + } + + private function getFilterLinks( $logType, $filter ) { + global $wgTitle; + // show/hide links + $messages = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) ); + // Option value -> message mapping + $links = array(); + foreach( $filter as $type => $val ) { + $hideVal = 1 - intval($val); + $link = $this->skin->makeKnownLinkObj( $wgTitle, $messages[$hideVal], + wfArrayToCGI( array( "hide_{$type}_log" => $hideVal ), $this->getDefaultQuery() ) + ); + $links[$type] = wfMsgHtml( "log-show-hide-{$type}", $link ); + } + // Build links + return implode( ' | ', $links ); + } + + private function getDefaultQuery() { + if ( !isset( $this->mDefaultQuery ) ) { + $this->mDefaultQuery = $_GET; + unset( $this->mDefaultQuery['title'] ); + unset( $this->mDefaultQuery['dir'] ); + unset( $this->mDefaultQuery['offset'] ); + unset( $this->mDefaultQuery['limit'] ); + unset( $this->mDefaultQuery['order'] ); + unset( $this->mDefaultQuery['month'] ); + unset( $this->mDefaultQuery['year'] ); + } + return $this->mDefaultQuery; } /** - * @return string Formatted HTML - * @param string $queryType + * @param $queryType String + * @return String: Formatted HTML */ private function getTypeMenu( $queryType ) { global $wgLogRestrictions, $wgUser; @@ -93,19 +130,19 @@ class LogEventsList { $html = "<select name='type'>\n"; $validTypes = LogPage::validTypes(); - $m = array(); // Temporary array + $typesByName = array(); // Temporary array // First pass to load the log names foreach( $validTypes as $type ) { $text = LogPage::logName( $type ); - $m[$text] = $type; + $typesByName[$text] = $type; } // Second pass to sort by name - ksort($m); + ksort($typesByName); // Third pass generates sorted XHTML content - foreach( $m as $text => $type ) { + foreach( $typesByName as $text => $type ) { $selected = ($type == $queryType); // Restricted types if ( isset($wgLogRestrictions[$type]) ) { @@ -122,25 +159,25 @@ class LogEventsList { } /** - * @return string Formatted HTML - * @param string $user + * @param $user String + * @return String: Formatted HTML */ private function getUserInput( $user ) { return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'user', 15, $user ); } /** - * @return string Formatted HTML - * @param string $title + * @param $title String + * @return String: Formatted HTML */ private function getTitleInput( $title ) { return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'page', 20, $title ); } /** + * @param $year Integer + * @param $month Integer * @return string Formatted HTML - * @param int $year - * @param int $month */ private function getDateMenu( $year, $month ) { # Offset overrides year/month selection @@ -185,10 +222,9 @@ class LogEventsList { return "</ul>\n"; } - /** - * @param Row $row a single row from the result set - * @return string Formatted HTML list item - * @private + /** + * @param $row Row: a single row from the result set + * @return String: Formatted HTML list item */ public function logLine( $row ) { global $wgLang, $wgUser, $wgContLang; @@ -213,88 +249,127 @@ class LogEventsList { $revert = $del = ''; // Some user can hide log items and have review links if( $wgUser->isAllowed( 'deleterevision' ) ) { - $del = $this->showhideLinks( $row ) . ' '; + $del = $this->getShowHideLinks( $row ) . ' '; } // Add review links and such... - if( !($this->flags & self::NO_ACTION_LINK) && !($row->log_deleted & LogPage::DELETED_ACTION) ) { - if( self::typeAction($row,'move','move') && isset( $paramArray[0] ) && $wgUser->isAllowed( 'move' ) ) { - $destTitle = Title::newFromText( $paramArray[0] ); - if( $destTitle ) { - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), - $this->message['revertmove'], - 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) . - '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) . - '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) . - '&wpMovetalk=0' ) . ')'; - } - // Show undelete link - } else if( self::typeAction($row,array('delete','suppress'),'delete') && $wgUser->isAllowed( 'delete' ) ) { - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), - $this->message['undeletelink'], 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')'; - // Show unblock link - } else if( self::typeAction($row,array('block','suppress'),'block') && $wgUser->isAllowed( 'block' ) ) { - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ), + if( ($this->flags & self::NO_ACTION_LINK) || ($row->log_deleted & LogPage::DELETED_ACTION) ) { + // Action text is suppressed... + } else if( self::typeAction($row,'move','move','move') && !empty($paramArray[0]) ) { + $destTitle = Title::newFromText( $paramArray[0] ); + if( $destTitle ) { + $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), + $this->message['revertmove'], + 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) . + '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) . + '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) . + '&wpMovetalk=0' ) . ')'; + } + // Show undelete link + } else if( self::typeAction($row,array('delete','suppress'),'delete','delete') ) { + $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), + $this->message['undeletelink'], 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')'; + // Show unblock/change block link + } else if( self::typeAction($row,array('block','suppress'),array('block','reblock'),'block') ) { + $revert = '(' . + $this->skin->link( SpecialPage::getTitleFor( 'Ipblocklist' ), $this->message['unblocklink'], - 'action=unblock&ip=' . urlencode( $row->log_title ) ) . ')'; - // Show change protection link - } else if( self::typeAction($row,'protect','modify') && $wgUser->isAllowed( 'protect' ) ) { - $revert = '(' . $this->skin->makeKnownLinkObj( $title, $this->message['protect_change'], 'action=unprotect' ) . ')'; - // Show unmerge link - } else if ( self::typeAction($row,'merge','merge') ) { - $merge = SpecialPage::getTitleFor( 'Mergehistory' ); - $revert = '(' . $this->skin->makeKnownLinkObj( $merge, $this->message['revertmerge'], - wfArrayToCGI( array('target' => $paramArray[0], 'dest' => $title->getPrefixedDBkey(), - 'mergepoint' => $paramArray[1] ) ) ) . ')'; - // If an edit was hidden from a page give a review link to the history - } else if( self::typeAction($row,array('delete','suppress'),'revision') && $wgUser->isAllowed( 'deleterevision' ) ) { - if( count($paramArray) == 2 ) { - $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); - // Different revision types use different URL params... - $key = $paramArray[0]; - // Link to each hidden object ID, $paramArray[1] is the url param - $Ids = explode( ',', $paramArray[1] ); - $revParams = ''; - foreach( $Ids as $n => $id ) { - $revParams .= '&' . urlencode($key) . '[]=' . urlencode($id); - } - $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'], - 'target=' . $title->getPrefixedUrl() . $revParams ) . ')'; + array(), + array( 'action' => 'unblock', 'ip' => $row->log_title ), + 'known' ) + . ' ' . $this->message['pipe-separator'] . ' ' . + $this->skin->link( SpecialPage::getTitleFor( 'Blockip', $row->log_title ), + $this->message['change-blocklink'], + array(), array(), 'known' ) . + ')'; + // Show change protection link + } else if( self::typeAction( $row, 'protect', array( 'modify', 'protect', 'unprotect' ) ) ) { + $revert .= ' (' . + $this->skin->link( $title, + $this->message['hist'], + array(), + array( 'action' => 'history', 'offset' => $row->log_timestamp ) ); + if( $wgUser->isAllowed( 'protect' ) ) { + $revert .= ' ' . $this->message['pipe-separator'] . ' ' . + $this->skin->link( $title, + $this->message['protect_change'], + array(), + array( 'action' => 'protect' ), + 'known' ); + } + $revert .= ')'; + // Show unmerge link + } else if( self::typeAction($row,'merge','merge','mergehistory') ) { + $merge = SpecialPage::getTitleFor( 'Mergehistory' ); + $revert = '(' . $this->skin->makeKnownLinkObj( $merge, $this->message['revertmerge'], + wfArrayToCGI( array('target' => $paramArray[0], 'dest' => $title->getPrefixedDBkey(), + 'mergepoint' => $paramArray[1] ) ) ) . ')'; + // If an edit was hidden from a page give a review link to the history + } else if( self::typeAction($row,array('delete','suppress'),'revision','deleterevision') ) { + if( count($paramArray) == 2 ) { + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + // Different revision types use different URL params... + $key = $paramArray[0]; + // Link to each hidden object ID, $paramArray[1] is the url param + $Ids = explode( ',', $paramArray[1] ); + $revParams = ''; + foreach( $Ids as $n => $id ) { + $revParams .= '&' . urlencode($key) . '[]=' . urlencode($id); } - // Hidden log items, give review link - } else if( self::typeAction($row,array('delete','suppress'),'event') && $wgUser->isAllowed( 'deleterevision' ) ) { - if( count($paramArray) == 1 ) { - $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); - $Ids = explode( ',', $paramArray[0] ); - // Link to each hidden object ID, $paramArray[1] is the url param - $logParams = ''; - foreach( $Ids as $n => $id ) { - $logParams .= '&logid[]=' . intval($id); - } - $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'], - 'target=' . $title->getPrefixedUrl() . $logParams ) . ')'; + $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'], + 'target=' . $title->getPrefixedUrl() . $revParams ) . ')'; + } + // Hidden log items, give review link + } else if( self::typeAction($row,array('delete','suppress'),'event','deleterevision') ) { + if( count($paramArray) == 1 ) { + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + $Ids = explode( ',', $paramArray[0] ); + // Link to each hidden object ID, $paramArray[1] is the url param + $logParams = ''; + foreach( $Ids as $n => $id ) { + $logParams .= '&logid[]=' . intval($id); } + $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'], + 'target=' . $title->getPrefixedUrl() . $logParams ) . ')'; + } + // Self-created users + } else if( self::typeAction($row,'newusers','create2') ) { + if( isset( $paramArray[0] ) ) { + $revert = $this->skin->userToolLinks( $paramArray[0], $title->getDBkey(), true ); } else { - wfRunHooks( 'LogLine', array( $row->log_type, $row->log_action, $title, $paramArray, - &$comment, &$revert, $row->log_timestamp ) ); - // wfDebug( "Invoked LogLine hook for " $row->log_type . ", " . $row->log_action . "\n" ); - // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters. + # Fall back to a blue contributions link + $revert = $this->skin->userToolLinks( 1, $title->getDBkey() ); + } + if( $time < '20080129000000' ) { + # Suppress $comment from old entries (before 2008-01-29), + # not needed and can contain incorrect links + $comment = ''; } + // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters. + } else { + wfRunHooks( 'LogLine', array( $row->log_type, $row->log_action, $title, $paramArray, + &$comment, &$revert, $row->log_timestamp ) ); } // Event description if( self::isDeleted($row,LogPage::DELETED_ACTION) ) { $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; } else { - $action = LogPage::actionText( $row->log_type, $row->log_action, $title, $this->skin, $paramArray, true ); + $action = LogPage::actionText( $row->log_type, $row->log_action, $title, + $this->skin, $paramArray, true ); + } + + if( $revert != '' ) { + $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>'; } - return "<li>$del$time $userLink $action $comment $revert</li>\n"; + return Xml::tags( 'li', array( "class" => "mw-logline-$row->log_type" ), + $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert ); } /** - * @param Row $row + * @param $row Row * @return string */ - private function showhideLinks( $row ) { + private function getShowHideLinks( $row ) { $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); // If event was hidden from sysops if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) { @@ -314,25 +389,31 @@ class LogEventsList { } /** - * @param Row $row - * @param mixed $type (string/array) - * @param string $action + * @param $row Row + * @param $type Mixed: string/array + * @param $action Mixed: string/array + * @param $right string * @return bool */ - public static function typeAction( $row, $type, $action ) { - if( is_array($type) ) { - return ( in_array($row->log_type,$type) && $row->log_action == $action ); - } else { - return ( $row->log_type == $type && $row->log_action == $action ); + public static function typeAction( $row, $type, $action, $right='' ) { + $match = is_array($type) ? in_array($row->log_type,$type) : $row->log_type == $type; + if( $match ) { + $match = is_array($action) ? + in_array($row->log_action,$action) : $row->log_action == $action; + if( $match && $right ) { + global $wgUser; + $match = $wgUser->isAllowed( $right ); + } } + return $match; } /** * Determine if the current user is allowed to view a particular * field of this log row, if it's marked as deleted. - * @param Row $row - * @param int $field - * @return bool + * @param $row Row + * @param $field Integer + * @return Boolean */ public static function userCan( $row, $field ) { if( ( $row->log_deleted & $field ) == $field ) { @@ -348,9 +429,9 @@ class LogEventsList { } /** - * @param Row $row - * @param int $field one of DELETED_* bitfield constants - * @return bool + * @param $row Row + * @param $field Integer: one of DELETED_* bitfield constants + * @return Boolean */ public static function isDeleted( $row, $field ) { return ($row->log_deleted & $field) == $field; @@ -358,16 +439,19 @@ class LogEventsList { /** * Quick function to show a short log extract - * @param OutputPage $out - * @param string $type - * @param string $page - * @param string $user + * @param $out OutputPage + * @param $type String + * @param $page String + * @param $user String + * @param $lim Integer + * @param $conds Array */ - public static function showLogExtract( $out, $type='', $page='', $user='' ) { + public static function showLogExtract( $out, $type='', $page='', $user='', $lim=0, $conds=array() ) { global $wgUser; # Insert list of top 50 or so items $loglist = new LogEventsList( $wgUser->getSkin(), $out, 0 ); - $pager = new LogPager( $loglist, $type, $user, $page, '' ); + $pager = new LogPager( $loglist, $type, $user, $page, '', $conds ); + if( $lim > 0 ) $pager->mLimit = $lim; $logBody = $pager->getBody(); if( $logBody ) { $out->addHTML( @@ -378,27 +462,28 @@ class LogEventsList { } else { $out->addWikiMsg( 'logempty' ); } - } + return $pager->getNumRows(); + } - /** + /** * SQL clause to skip forbidden log types for this user - * @param Database $db - * @returns mixed (string or false) + * @param $db Database + * @return mixed (string or false) */ public static function getExcludeClause( $db ) { global $wgLogRestrictions, $wgUser; // Reset the array, clears extra "where" clauses when $par is used $hiddenLogs = array(); // Don't show private logs to unprivileged users - foreach( $wgLogRestrictions as $logtype => $right ) { + foreach( $wgLogRestrictions as $logType => $right ) { if( !$wgUser->isAllowed($right) ) { - $safetype = $db->strencode( $logtype ); - $hiddenLogs[] = $safetype; + $safeType = $db->strencode( $logType ); + $hiddenLogs[] = $safeType; } } if( count($hiddenLogs) == 1 ) { return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] ); - } elseif( !empty( $hiddenLogs ) ) { + } elseif( $hiddenLogs ) { return 'log_type NOT IN (' . $db->makeList($hiddenLogs) . ')'; } return false; @@ -409,18 +494,23 @@ class LogEventsList { * @ingroup Pager */ class LogPager extends ReverseChronologicalPager { - private $type = '', $user = '', $title = '', $pattern = '', $year = '', $month = ''; + private $type = '', $user = '', $title = '', $pattern = ''; public $mLogEventsList; + /** - * constructor - * @param LogEventsList $loglist, - * @param string $type, - * @param string $user, - * @param string $page, - * @param string $pattern - * @param array $conds - */ - function __construct( $list, $type='', $user='', $title='', $pattern='', $conds=array(), $y=false, $m=false ) { + * constructor + * @param $list LogEventsList + * @param $type String + * @param $user String + * @param $title String + * @param $pattern String + * @param $conds Array + * @param $year Integer + * @param $month Integer + */ + public function __construct( $list, $type = '', $user = '', $title = '', $pattern = '', + $conds = array(), $year = false, $month = false ) + { parent::__construct(); $this->mConds = $conds; @@ -429,22 +519,40 @@ class LogPager extends ReverseChronologicalPager { $this->limitType( $type ); $this->limitUser( $user ); $this->limitTitle( $title, $pattern ); - $this->limitDate( $y, $m ); + $this->getDateCond( $year, $month ); } - function getDefaultQuery() { + public function getDefaultQuery() { $query = parent::getDefaultQuery(); $query['type'] = $this->type; - $query['month'] = $this->month; - $query['year'] = $this->year; + $query['user'] = $this->user; + $query['month'] = $this->mMonth; + $query['year'] = $this->mYear; return $query; } + public function getFilterParams() { + global $wgFilterLogTypes, $wgUser, $wgRequest; + $filters = array(); + if( $this->type ) { + return $filters; + } + foreach( $wgFilterLogTypes as $type => $default ) { + // Avoid silly filtering + if( $type !== 'patrol' || $wgUser->useNPPatrol() ) { + $hide = $wgRequest->getInt( "hide_{$type}_log", $default ); + $filters[$type] = $hide; + if( $hide ) + $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type ); + } + } + return $filters; + } + /** * Set the log reader to return only entries of the given type. * Type restrictions enforced here - * @param string $type A log type ('upload', 'delete', etc) - * @private + * @param $type String: A log type ('upload', 'delete', etc) */ private function limitType( $type ) { global $wgLogRestrictions, $wgUser; @@ -457,7 +565,7 @@ class LogPager extends ReverseChronologicalPager { if( $hideLogs !== false ) { $this->mConds[] = $hideLogs; } - if( empty($type) ) { + if( !$type ) { return false; } $this->type = $type; @@ -466,10 +574,9 @@ class LogPager extends ReverseChronologicalPager { /** * Set the log reader to return only entries by the given user. - * @param string $name (In)valid user name - * @private + * @param $name String: (In)valid user name */ - function limitUser( $name ) { + private function limitUser( $name ) { if( $name == '' ) { return false; } @@ -492,10 +599,10 @@ class LogPager extends ReverseChronologicalPager { /** * Set the log reader to return only entries affecting the given page. * (For the block and rights logs, this is a user page.) - * @param string $page Title name as text - * @private + * @param $page String: Title name as text + * @param $pattern String */ - function limitTitle( $page, $pattern ) { + private function limitTitle( $page, $pattern ) { global $wgMiserMode; $title = Title::newFromText( $page ); @@ -527,46 +634,7 @@ class LogPager extends ReverseChronologicalPager { } } - /** - * Set the log reader to return only entries from given date. - * @param int $year - * @param int $month - * @private - */ - function limitDate( $year, $month ) { - $year = intval($year); - $month = intval($month); - - $this->year = ($year > 0 && $year < 10000) ? $year : ''; - $this->month = ($month > 0 && $month < 13) ? $month : ''; - - if( $this->year || $this->month ) { - // Assume this year if only a month is given - if( $this->year ) { - $year_start = $this->year; - } else { - $year_start = substr( wfTimestampNow(), 0, 4 ); - $thisMonth = gmdate( 'n' ); - if( $this->month > $thisMonth ) { - // Future contributions aren't supposed to happen. :) - $year_start--; - } - } - - if( $this->month ) { - $month_end = str_pad($this->month + 1, 2, '0', STR_PAD_LEFT); - $year_end = $year_start; - } else { - $month_end = 0; - $year_end = $year_start + 1; - } - $ts_end = str_pad($year_end . $month_end, 14, '0' ); - - $this->mOffset = $ts_end; - } - } - - function getQueryInfo() { + public function getQueryInfo() { $this->mConds[] = 'user_id = log_user'; # Don't use the wrong logging index if( $this->title || $this->pattern || $this->user ) { @@ -589,7 +657,7 @@ class LogPager extends ReverseChronologicalPager { return 'log_timestamp'; } - function getStartBody() { + public function getStartBody() { wfProfileIn( __METHOD__ ); # Do a link batch query if( $this->getNumRows() > 0 ) { @@ -606,7 +674,7 @@ class LogPager extends ReverseChronologicalPager { return ''; } - function formatRow( $row ) { + public function formatRow( $row ) { return $this->mLogEventsList->logLine( $row ); } @@ -627,11 +695,11 @@ class LogPager extends ReverseChronologicalPager { } public function getYear() { - return $this->year; + return $this->mYear; } public function getMonth() { - return $this->month; + return $this->mMonth; } } @@ -642,26 +710,27 @@ class LogPager extends ReverseChronologicalPager { class LogReader { var $pager; /** - * @param WebRequest $request For internal use use a FauxRequest object to pass arbitrary parameters. + * @param $request WebRequest: for internal use use a FauxRequest object to pass arbitrary parameters. */ function __construct( $request ) { global $wgUser, $wgOut; + wfDeprecated(__METHOD__); # Get parameters $type = $request->getVal( 'type' ); $user = $request->getText( 'user' ); $title = $request->getText( 'page' ); $pattern = $request->getBool( 'pattern' ); - $y = $request->getIntOrNull( 'year' ); - $m = $request->getIntOrNull( 'month' ); + $year = $request->getIntOrNull( 'year' ); + $month = $request->getIntOrNull( 'month' ); # Don't let the user get stuck with a certain date $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev'; if( $skip ) { - $y = ''; - $m = ''; + $year = ''; + $month = ''; } # Use new list class to output results $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 ); - $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $y, $m ); + $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $year, $month ); } /** @@ -679,17 +748,20 @@ class LogReader { */ class LogViewer { const NO_ACTION_LINK = 1; + /** - * @var LogReader $reader + * LogReader object */ var $reader; + /** - * @param LogReader &$reader where to get our data from - * @param integer $flags Bitwise combination of flags: + * @param &$reader LogReader: where to get our data from + * @param $flags Integer: Bitwise combination of flags: * LogEventsList::NO_ACTION_LINK Don't show restore/unblock/block links */ function __construct( &$reader, $flags = 0 ) { global $wgUser; + wfDeprecated(__METHOD__); $this->reader =& $reader; $this->reader->pager->mLogEventsList->flags = $flags; # Aliases for shorter code... @@ -725,7 +797,7 @@ class LogViewer { * Output just the list of entries given by the linked LogReader, * with extraneous UI elements. Use for displaying log fragments in * another page (eg at Special:Undelete) - * @param OutputPage $out where to send output + * @param $out OutputPage: where to send output */ public function showList( &$out ) { $logBody = $this->pager->getBody(); diff --git a/includes/LogPage.php b/includes/LogPage.php index 27554308..50a9a232 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -89,6 +89,9 @@ class LogPage { return true; } + /** + * Get the RC comment from the last addEntry() call + */ public function getRcComment() { $rcComment = $this->actionText; if( '' != $this->comment ) { @@ -101,6 +104,13 @@ class LogPage { } /** + * Get the comment from the last addEntry() call + */ + public function getComment() { + return $this->comment; + } + + /** * @static */ public static function validTypes() { @@ -136,7 +146,8 @@ class LogPage { * @return string Headertext of this logtype */ static function logHeader( $type ) { - global $wgLogHeaders; + global $wgLogHeaders, $wgMessageCache; + $wgMessageCache->loadAllMessages(); return wfMsgExt($wgLogHeaders[$type],array('parseinline')); } @@ -144,54 +155,24 @@ class LogPage { * @static * @return HTML string */ - static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false ) { - global $wgLang, $wgContLang, $wgLogActions; + static function actionText( $type, $action, $title = NULL, $skin = NULL, + $params = array(), $filterWikilinks = false ) + { + global $wgLang, $wgContLang, $wgLogActions, $wgMessageCache; + $wgMessageCache->loadAllMessages(); $key = "$type/$action"; - - if( $key == 'patrol/patrol' ) + # Defer patrol log to PatrolLog class + if( $key == 'patrol/patrol' ) { return PatrolLog::makeActionText( $title, $params, $skin ); - + } if( isset( $wgLogActions[$key] ) ) { if( is_null( $title ) ) { - $rv=wfMsg( $wgLogActions[$key] ); + $rv = wfMsg( $wgLogActions[$key] ); } else { - if( $skin ) { - - switch( $type ) { - case 'move': - $titleLink = $skin->makeLinkObj( $title, htmlspecialchars( $title->getPrefixedText() ), 'redirect=no' ); - $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) ); - break; - case 'block': - if( substr( $title->getText(), 0, 1 ) == '#' ) { - $titleLink = $title->getText(); - } else { - // TODO: Store the user identifier in the parameters - // to make this faster for future log entries - $id = User::idFromName( $title->getText() ); - $titleLink = $skin->userLink( $id, $title->getText() ) - . $skin->userToolLinks( $id, $title->getText(), false, Linker::TOOL_LINKS_NOBLOCK ); - } - break; - case 'rights': - $text = $wgContLang->ucfirst( $title->getText() ); - $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) ); - break; - case 'merge': - $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' ); - $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) ); - $params[1] = $wgLang->timeanddate( $params[1] ); - break; - default: - $titleLink = $skin->makeLinkObj( $title ); - } - - } else { - $titleLink = $title->getPrefixedText(); - } + $titleLink = self::getTitleLink( $type, $skin, $title, $params ); if( $key == 'rights/rights' ) { - if ($skin) { + if( $skin ) { $rightsnone = wfMsg( 'rightsnone' ); foreach ( $params as &$param ) { $groupArray = array_map( 'trim', explode( ',', $param ) ); @@ -213,18 +194,28 @@ class LogPage { $rv = wfMsgForContent( $wgLogActions[$key], $titleLink ); } } else { + $details = ''; array_unshift( $params, $titleLink ); - if ( $key == 'block/block' || $key == 'suppress/block' ) { + if ( $key == 'block/block' || $key == 'suppress/block' || $key == 'block/reblock' ) { if ( $skin ) { - $params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' . $wgLang->translateBlockExpiry( $params[1] ) . '</span>'; + $params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' . + $wgLang->translateBlockExpiry( $params[1] ) . '</span>'; } else { $params[1] = $wgContLang->translateBlockExpiry( $params[1] ); } - $params[2] = isset( $params[2] ) - ? self::formatBlockFlags( $params[2], is_null( $skin ) ) - : ''; + $params[2] = isset( $params[2] ) ? + self::formatBlockFlags( $params[2], is_null( $skin ) ) : ''; + } else if ( $type == 'protect' && count($params) == 3 ) { + $details .= " {$params[1]}"; // restrictions and expiries + if( $params[2] ) { + $details .= ' ['.wfMsg('protect-summary-cascade').']'; + } + } else if ( $type == 'move' && count( $params ) == 3 ) { + if( $params[2] ) { + $details .= ' [' . wfMsg( 'move-redirect-suppressed' ) . ']'; + } } - $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ); + $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ) . $details; } } } else { @@ -243,6 +234,59 @@ class LogPage { } return $rv; } + + protected static function getTitleLink( $type, $skin, $title, &$params ) { + global $wgLang, $wgContLang; + if( !$skin ) { + return $title->getPrefixedText(); + } + switch( $type ) { + case 'move': + $titleLink = $skin->makeLinkObj( $title, + htmlspecialchars( $title->getPrefixedText() ), 'redirect=no' ); + $targetTitle = Title::newFromText( $params[0] ); + if ( !$targetTitle ) { + # Workaround for broken database + $params[0] = htmlspecialchars( $params[0] ); + } else { + $params[0] = $skin->makeLinkObj( $targetTitle, htmlspecialchars( $params[0] ) ); + } + break; + case 'block': + if( substr( $title->getText(), 0, 1 ) == '#' ) { + $titleLink = $title->getText(); + } else { + // TODO: Store the user identifier in the parameters + // to make this faster for future log entries + $id = User::idFromName( $title->getText() ); + $titleLink = $skin->userLink( $id, $title->getText() ) + . $skin->userToolLinks( $id, $title->getText(), false, Linker::TOOL_LINKS_NOBLOCK ); + } + break; + case 'rights': + $text = $wgContLang->ucfirst( $title->getText() ); + $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) ); + break; + case 'merge': + $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' ); + $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) ); + $params[1] = $wgLang->timeanddate( $params[1] ); + break; + default: + if( $title->getNamespace() == NS_SPECIAL ) { + list( $name, $par ) = SpecialPage::resolveAliasWithSubpage( $title->getDBKey() ); + # Use the language name for log titles, rather than Log/X + if( $name == 'Log' ) { + $titleLink = '('.$skin->makeLinkObj( $title, LogPage::logName( $par ) ).')'; + } else { + $titleLink = $skin->makeLinkObj( $title ); + } + } else { + $titleLink = $skin->makeLinkObj( $title ); + } + } + return $titleLink; + } /** * Add a log entry diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 3b22cb9b..5b5b77f0 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -103,8 +103,12 @@ class MagicWord { 'contentlanguage', 'pagesinnamespace', 'numberofadmins', + 'numberofviews', 'defaultsort', 'pagesincategory', + 'index', + 'noindex', + 'numberingroup', ); /* Array of caching hints for ParserCache */ @@ -143,6 +147,8 @@ class MagicWord { 'localtimestamp' => 3600, 'pagesinnamespace' => 3600, 'numberofadmins' => 3600, + 'numberofviews' => 3600, + 'numberingroup' => 3600, ); static public $mDoubleUnderscoreIDs = array( @@ -153,6 +159,8 @@ class MagicWord { 'noeditsection', 'newsectionlink', 'hiddencat', + 'index', + 'noindex', 'staticredirect', ); diff --git a/includes/Math.php b/includes/Math.php index 871e9fc3..2ed16033 100644 --- a/includes/Math.php +++ b/includes/Math.php @@ -47,7 +47,7 @@ class MathRenderer { if( !$this->_recall() ) { # Ensure that the temp and output directories are available before continuing... if( !file_exists( $wgTmpDirectory ) ) { - if( !@mkdir( $wgTmpDirectory ) ) { + if( !wfMkdirParents( $wgTmpDirectory ) ) { return $this->_error( 'math_bad_tmpdir' ); } } elseif( !is_dir( $wgTmpDirectory ) || !is_writable( $wgTmpDirectory ) ) { @@ -145,6 +145,10 @@ class MathRenderer { return $this->_error( 'math_image_error' ); } + if( filesize( "$wgTmpDirectory/{$this->hash}.png" ) == 0 ) { + return $this->_error( 'math_image_error' ); + } + $hashpath = $this->_getHashPath(); if( !file_exists( $hashpath ) ) { if( !@wfMkdirParents( $hashpath, 0755 ) ) { @@ -172,10 +176,17 @@ class MathRenderer { 'math_html_conservativeness' => $this->conservativeness, 'math_html' => $this->html, 'math_mathml' => $this->mathml, - ), $fname, array( 'IGNORE' ) + ), $fname ); } - + + // If we're replacing an older version of the image, make sure it's current. + global $wgUseSquid; + if ( $wgUseSquid ) { + $urls = array( $this->_mathImageUrl() ); + $u = new SquidUpdate( $urls ); + $u->doUpdate(); + } } return $this->_doRender(); @@ -209,8 +220,14 @@ class MathRenderer { $this->html = $rpage->math_html; $this->mathml = $rpage->math_mathml; - if( file_exists( $this->_getHashPath() . "/{$this->hash}.png" ) ) { - return true; + $filename = $this->_getHashPath() . "/{$this->hash}.png"; + if( file_exists( $filename ) ) { + if( filesize( $filename ) == 0 ) { + // Some horrible error corrupted stuff :( + @unlink( $filename ); + } else { + return true; + } } if( file_exists( $wgMathDirectory . "/{$this->hash}.png" ) ) { @@ -268,10 +285,7 @@ class MathRenderer { } function _linkToMathImage() { - global $wgMathPath; - $url = "$wgMathPath/" . substr($this->hash, 0, 1) - .'/'. substr($this->hash, 1, 1) .'/'. substr($this->hash, 2, 1) - . "/{$this->hash}.png"; + $url = $this->_mathImageUrl(); return Xml::element( 'img', $this->_attribs( @@ -283,14 +297,24 @@ class MathRenderer { 'src' => $url ) ) ); } + function _mathImageUrl() { + global $wgMathPath; + $dir = $this->_getHashSubPath(); + return "$wgMathPath/$dir/{$this->hash}.png"; + } + function _getHashPath() { global $wgMathDirectory; - $path = $wgMathDirectory .'/'. substr($this->hash, 0, 1) - .'/'. substr($this->hash, 1, 1) - .'/'. substr($this->hash, 2, 1); + $path = $wgMathDirectory .'/' . $this->_getHashSubPath(); wfDebug( "TeX: getHashPath, hash is: $this->hash, path is: $path\n" ); return $path; } + + function _getHashSubPath() { + return substr($this->hash, 0, 1) + .'/'. substr($this->hash, 1, 1) + .'/'. substr($this->hash, 2, 1); + } public static function renderMath( $tex, $params=array() ) { global $wgUser; diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php index 9e94f06b..0367494f 100644 --- a/includes/MediaTransformOutput.php +++ b/includes/MediaTransformOutput.php @@ -50,6 +50,8 @@ abstract class MediaTransformOutput { * alt Alternate text or caption * desc-link Boolean, show a description link * file-link Boolean, show a file download link + * custom-url-link Custom URL to link to + * custom-title-link Custom Title object to link to * valign vertical-align property, if the output is an inline element * img-class Class applied to the <img> tag, if there is such a tag * @@ -127,12 +129,15 @@ class ThumbnailImage extends MediaTransformOutput { * should be indicated with a value of true for true, and false or * absent for false. * - * alt Alternate text or caption + * alt HTML alt attribute + * title HTML title attribute * desc-link Boolean, show a description link * file-link Boolean, show a file download link * valign vertical-align property, if the output is an inline element * img-class Class applied to the <img> tag, if there is such a tag * desc-query String, description link query params + * custom-url-link Custom URL to link to + * custom-title-link Custom Title object to link to * * For images, desc-link and file-link are implemented as a click-through. For * sounds and videos, they may be displayed in other ways. @@ -146,9 +151,18 @@ class ThumbnailImage extends MediaTransformOutput { } $alt = empty( $options['alt'] ) ? '' : $options['alt']; + # Note: if title is empty and alt is not, make the title empty, don't + # use alt; only use alt if title is not set + $title = !isset( $options['title'] ) ? $alt : $options['title']; $query = empty($options['desc-query']) ? '' : $options['desc-query']; - if ( !empty( $options['desc-link'] ) ) { - $linkAttribs = $this->getDescLinkAttribs( $alt, $query ); + + if ( !empty( $options['custom-url-link'] ) ) { + $linkAttribs = array( 'href' => $options['custom-url-link'] ); + } elseif ( !empty( $options['custom-title-link'] ) ) { + $title = $options['custom-title-link']; + $linkAttribs = array( 'href' => $title->getLinkUrl(), 'title' => $title->getFullText() ); + } elseif ( !empty( $options['desc-link'] ) ) { + $linkAttribs = $this->getDescLinkAttribs( $title, $query ); } elseif ( !empty( $options['file-link'] ) ) { $linkAttribs = array( 'href' => $this->file->getURL() ); } else { diff --git a/includes/MessageCache.php b/includes/MessageCache.php index f24d3b4d..a06b0cb9 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -25,7 +25,7 @@ class MessageCache { var $mKeys, $mParserOptions, $mParser; var $mExtensionMessages = array(); var $mInitialised = false; - var $mAllMessagesLoaded; // Extension messages + var $mAllMessagesLoaded = array(); // Extension messages // Variable for tracking which variables are loaded var $mLoadedLanguages = array(); @@ -44,7 +44,6 @@ class MessageCache { /** * ParserOptions is lazy initialised. - * Access should probably be protected. */ function getParserOptions() { if ( !$this->mParserOptions ) { @@ -110,7 +109,7 @@ class MessageCache { global $wgLocalMessageCache; $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code"; - wfMkdirParents( $wgLocalMessageCache, 0777 ); // might fail + wfMkdirParents( $wgLocalMessageCache ); // might fail wfSuppressWarnings(); $file = fopen( $filename, 'w' ); @@ -131,7 +130,7 @@ class MessageCache { $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code"; $tempFilename = $filename . '.tmp'; - wfMkdirParents( $wgLocalMessageCache, 0777 ); // might fail + wfMkdirParents( $wgLocalMessageCache ); // might fail wfSuppressWarnings(); $file = fopen( $tempFilename, 'w'); @@ -261,12 +260,23 @@ class MessageCache { $this->lock($cacheKey); - $cache = $this->loadFromDB( $code ); - $success = $this->setCache( $cache, $code ); + # Limit the concurrency of loadFromDB to a single process + # This prevents the site from going down when the cache expires + $statusKey = wfMemcKey( 'messages', $code, 'status' ); + $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ); if ( $success ) { - $this->saveToCaches( $cache, true, $code ); + $cache = $this->loadFromDB( $code ); + $success = $this->setCache( $cache, $code ); + } + if ( $success ) { + $success = $this->saveToCaches( $cache, true, $code ); + if ( $success ) { + $this->mMemc->delete( $statusKey ); + } else { + $this->mMemc->set( $statusKey, 'error', 60*5 ); + wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); + } } - $this->unlock($cacheKey); } @@ -414,10 +424,6 @@ class MessageCache { global $wgLocalMessageCache, $wgLocalMessageCacheSerialized; $cacheKey = wfMemcKey( 'messages', $code ); - $statusKey = wfMemcKey( 'messages', $code, 'status' ); - - $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ); - if ( !$success ) return true; # Other process should be updating them now $i = 0; if ( $memc ) { @@ -444,11 +450,8 @@ class MessageCache { } if ( $i == 20 ) { - $this->mMemc->set( $statusKey, 'error', 60*5 ); - wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); $success = false; } else { - $this->mMemc->delete( $statusKey ); $success = true; } wfProfileOut( __METHOD__ ); @@ -498,29 +501,9 @@ class MessageCache { * @param bool $isFullKey Specifies whether $key is a two part key "lang/msg". */ function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { - global $wgContLanguageCode, $wgContLang, $wgLang; - - # Identify which language to get or create a language object for. - if( $langcode === $wgContLang->getCode() || $langcode === true ) { - # $langcode is the language code of the wikis content language object. - # or it is a boolean and value is true - $lang =& $wgContLang; - } elseif( $langcode === $wgLang->getCode() || $langcode === false ) { - # $langcode is the language code of user language object. - # or it was a boolean and value is false - $lang =& $wgLang; - } else { - $validCodes = array_keys( Language::getLanguageNames() ); - if( in_array( $langcode, $validCodes ) ) { - # $langcode corresponds to a valid language. - $lang = Language::factory( $langcode ); - } else { - # $langcode is a string, but not a valid language code; use content language. - $lang =& $wgContLang; - wfDebug( 'Invalid language code passed to MessageCache::get, falling back to content language.' ); - } - } + global $wgContLanguageCode, $wgContLang; + $lang = wfGetLangObj( $langcode ); $langcode = $lang->getCode(); # If uninitialised, someone is trying to call this halfway through Setup.php @@ -664,23 +647,30 @@ class MessageCache { return $message; } - function transform( $message, $interface = false ) { + function transform( $message, $interface = false, $language = null ) { // Avoid creating parser if nothing to transfrom if( strpos( $message, '{{' ) === false ) { return $message; } - global $wgParser; + global $wgParser, $wgParserConf; if ( !$this->mParser && isset( $wgParser ) ) { # Do some initialisation so that we don't have to do it twice $wgParser->firstCallInit(); # Clone it and store it - $this->mParser = clone $wgParser; + $class = $wgParserConf['class']; + if ( $class == 'Parser_DiffTest' ) { + # Uncloneable + $this->mParser = new $class( $wgParserConf ); + } else { + $this->mParser = clone $wgParser; + } #wfDebug( __METHOD__ . ": following contents triggered transform: $message\n" ); } if ( $this->mParser ) { $popts = $this->getParserOptions(); $popts->setInterfaceMessage( $interface ); + $popts->setTargetLanguage( $language ); $message = $this->mParser->transformMsg( $message, $popts ); } return $message; @@ -781,12 +771,13 @@ class MessageCache { } } - function loadAllMessages() { + function loadAllMessages( $lang = false ) { global $wgExtensionMessagesFiles; - if ( $this->mAllMessagesLoaded ) { + $key = $lang === false ? '*' : $lang; + if ( isset( $this->mAllMessagesLoaded[$key] ) ) { return; } - $this->mAllMessagesLoaded = true; + $this->mAllMessagesLoaded[$key] = true; # Some extensions will load their messages when you load their class file wfLoadAllExtensions(); @@ -794,7 +785,7 @@ class MessageCache { wfRunHooks( 'LoadAllMessages' ); # Some register their messages in $wgExtensionMessagesFiles foreach ( $wgExtensionMessagesFiles as $name => $file ) { - wfLoadExtensionMessages( $name ); + wfLoadExtensionMessages( $name, $lang ); } # Still others will respond to neither, they are EVIL. We sometimes need to know! } @@ -855,13 +846,17 @@ class MessageCache { public function figureMessage( $key ) { global $wgContLanguageCode; - $pieces = explode('/', $key, 2); + $pieces = explode( '/', $key ); + if( count( $pieces ) < 2 ) + return array( $key, $wgContLanguageCode ); - $key = $pieces[0]; + $lang = array_pop( $pieces ); + $validCodes = Language::getLanguageNames(); + if( !array_key_exists( $lang, $validCodes ) ) + return array( $key, $wgContLanguageCode ); - # Language the user is translating to - $langCode = isset($pieces[1]) ? $pieces[1] : $wgContLanguageCode; - return array( $key, $langCode ); + $message = implode( '/', $pieces ); + return array( $message, $lang ); } } diff --git a/includes/Metadata.php b/includes/Metadata.php index a543c73c..0b4fbf8c 100644 --- a/includes/Metadata.php +++ b/includes/Metadata.php @@ -20,347 +20,299 @@ * @author Evan Prodromou <evan@wikitravel.org> */ -/** - * TODO: Perhaps make this file into a Metadata class, with static methods (declared - * as private where indicated), to move these functions out of the global namespace? - */ -define('RDF_TYPE_PREFS', "application/rdf+xml,text/xml;q=0.7,application/xml;q=0.5,text/rdf;q=0.1"); - -function wfDublinCoreRdf($article) { - - $url = dcReallyFullUrl($article->mTitle); - - if (rdfSetup()) { - dcPrologue($url); - dcBasics($article); - dcEpilogue(); +abstract class RdfMetaData { + const RDF_TYPE_PREFS = 'application/rdf+xml,text/xml;q=0.7,application/xml;q=0.5,text/rdf;q=0.1'; + + /** + * Constructor + * @param $article Article object + */ + public function __construct( Article $article ){ + $this->mArticle = $article; } -} -function wfCreativeCommonsRdf($article) { + public abstract function show(); - if (rdfSetup()) { - global $wgRightsUrl; + /** + * + */ + protected function setup() { + global $wgOut, $wgRequest; - $url = dcReallyFullUrl($article->mTitle); + $httpaccept = isset( $_SERVER['HTTP_ACCEPT'] ) ? $_SERVER['HTTP_ACCEPT'] : null; + $rdftype = wfNegotiateType( wfAcceptToPrefs( $httpaccept ), wfAcceptToPrefs( self::RDF_TYPE_PREFS ) ); - ccPrologue(); - ccSubPrologue('Work', $url); - dcBasics($article); - if (isset($wgRightsUrl)) { - $url = htmlspecialchars( $wgRightsUrl ); - print " <cc:license rdf:resource=\"$url\" />\n"; + if( !$rdftype ){ + wfHttpError( 406, 'Not Acceptable', wfMsg( 'notacceptable' ) ); + return false; + } else { + $wgOut->disable(); + $wgRequest->response()->header( "Content-type: {$rdftype}; charset=utf-8" ); + $wgOut->sendCacheControl(); + return true; } + } - ccSubEpilogue('Work'); - - if (isset($wgRightsUrl)) { - $terms = ccGetTerms($wgRightsUrl); - if ($terms) { - ccSubPrologue('License', $wgRightsUrl); - ccLicense($terms); - ccSubEpilogue('License'); - } - } + /** + * + */ + protected function reallyFullUrl() { + return $this->mArticle->getTitle()->getFullURL(); } - ccEpilogue(); -} + protected function basics() { + global $wgContLanguageCode, $wgSitename; -/** - * @private - */ -function rdfSetup() { - global $wgOut, $_SERVER; + $this->element( 'title', $this->mArticle->mTitle->getText() ); + $this->pageOrString( 'publisher', wfMsg( 'aboutpage' ), $wgSitename ); + $this->element( 'language', $wgContLanguageCode ); + $this->element( 'type', 'Text' ); + $this->element( 'format', 'text/html' ); + $this->element( 'identifier', $this->reallyFullUrl() ); + $this->element( 'date', $this->date( $this->mArticle->getTimestamp() ) ); - $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : null; + $lastEditor = User::newFromId( $this->mArticle->getUser() ); + $this->person( 'creator', $lastEditor ); - $rdftype = wfNegotiateType(wfAcceptToPrefs($httpaccept), wfAcceptToPrefs(RDF_TYPE_PREFS)); + foreach( $this->mArticle->getContributors() as $user ){ + $this->person( 'contributor', $user ); + } - if (!$rdftype) { - wfHttpError(406, "Not Acceptable", wfMsg("notacceptable")); - return false; - } else { - $wgOut->disable(); - header( "Content-type: {$rdftype}; charset=utf-8" ); - $wgOut->sendCacheControl(); - return true; + $this->rights(); } -} -/** - * @private - */ -function dcPrologue($url) { - global $wgOutputEncoding; + protected function element( $name, $value ) { + $value = htmlspecialchars( $value ); + print "\t\t<dc:{$name}>{$value}</dc:{$name}>\n"; + } - $url = htmlspecialchars( $url ); - print "<" . "?xml version=\"1.0\" encoding=\"{$wgOutputEncoding}\" ?" . "> + protected function date($timestamp) { + return substr($timestamp, 0, 4) . '-' + . substr($timestamp, 4, 2) . '-' + . substr($timestamp, 6, 2); + } - <!DOCTYPE rdf:RDF PUBLIC \"-//DUBLIN CORE//DCMES DTD 2002/07/31//EN\" \"http://dublincore.org/documents/2002/07/31/dcmes-xml/dcmes-xml-dtd.dtd\"> + protected function pageOrString( $name, $page, $str ){ + if( $page instanceof Title ) + $nt = $page; + else + $nt = Title::newFromText( $page ); - <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" - xmlns:dc=\"http://purl.org/dc/elements/1.1/\"> - <rdf:Description rdf:about=\"$url\"> - "; -} + if( !$nt || $nt->getArticleID() == 0 ){ + $this->element( $name, $str ); + } else { + $this->page( $name, $nt ); + } + } -/** - * @private - */ -function dcEpilogue() { - print " - </rdf:Description> - </rdf:RDF> - "; -} + protected function page( $name, $title ){ + $this->url( $name, $title->getFullUrl() ); + } -/** - * @private - */ -function dcBasics($article) { - global $wgContLanguageCode, $wgSitename; - - dcElement('title', $article->mTitle->getText()); - dcPageOrString('publisher', wfMsg('aboutpage'), $wgSitename); - dcElement('language', $wgContLanguageCode); - dcElement('type', 'Text'); - dcElement('format', 'text/html'); - dcElement('identifier', dcReallyFullUrl($article->mTitle)); - dcElement('date', dcDate($article->getTimestamp())); - - $last_editor = $article->getUser(); - - if ($last_editor == 0) { - dcPerson('creator', 0); - } else { - dcPerson('creator', $last_editor, $article->getUserText(), - User::whoIsReal($last_editor)); + protected function url($name, $url) { + $url = htmlspecialchars( $url ); + print "\t\t<dc:{$name} rdf:resource=\"{$url}\" />\n"; } - $contributors = $article->getContributors(); + protected function person($name, User $user ){ + global $wgContLang; - foreach ($contributors as $user_parts) { - dcPerson('contributor', $user_parts[0], $user_parts[1], $user_parts[2]); + if( $user->isAnon() ){ + $this->element( $name, wfMsgExt( 'anonymous', array( 'parsemag' ), 1 ) ); + } else if( $real = $user->getRealName() ) { + $this->element( $name, $real ); + } else { + $this->pageOrString( $name, $user->getUserPage(), wfMsg( 'siteuser', $user->getName() ) ); + } } - dcRights(); -} + /** + * Takes an arg, for future enhancement with different rights for + * different pages. + */ + protected function rights() { + global $wgRightsPage, $wgRightsUrl, $wgRightsText; + + if( $wgRightsPage && ( $nt = Title::newFromText( $wgRightsPage ) ) + && ($nt->getArticleID() != 0)) { + $this->page('rights', $nt); + } else if( $wgRightsUrl ){ + $this->url('rights', $wgRightsUrl); + } else if( $wgRightsText ){ + $this->element( 'rights', $wgRightsText ); + } + } -/** - * @private - */ -function ccPrologue() { - global $wgOutputEncoding; + protected function getTerms( $url ){ + global $wgLicenseTerms; - echo "<" . "?xml version='1.0' encoding='{$wgOutputEncoding}' ?" . "> + if( $wgLicenseTerms ){ + return $wgLicenseTerms; + } else { + $known = $this->getKnownLicenses(); + if( isset( $known[$url] ) ) { + return $known[$url]; + } else { + return array(); + } + } + } - <rdf:RDF xmlns:cc=\"http://web.resource.org/cc/\" - xmlns:dc=\"http://purl.org/dc/elements/1.1/\" - xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"> - "; -} + protected function getKnownLicenses() { + $ccLicenses = array('by', 'by-nd', 'by-nd-nc', 'by-nc', + 'by-nc-sa', 'by-sa'); + $ccVersions = array('1.0', '2.0'); + $knownLicenses = array(); + + foreach ($ccVersions as $version) { + foreach ($ccLicenses as $license) { + if( $version == '2.0' && substr( $license, 0, 2) != 'by' ) { + # 2.0 dropped the non-attribs licenses + continue; + } + $lurl = "http://creativecommons.org/licenses/{$license}/{$version}/"; + $knownLicenses[$lurl] = explode('-', $license); + $knownLicenses[$lurl][] = 're'; + $knownLicenses[$lurl][] = 'di'; + $knownLicenses[$lurl][] = 'no'; + if (!in_array('nd', $knownLicenses[$lurl])) { + $knownLicenses[$lurl][] = 'de'; + } + } + } -/** - * @private - */ -function ccSubPrologue($type, $url) { - $url = htmlspecialchars( $url ); - echo " <cc:{$type} rdf:about=\"{$url}\">\n"; -} + /* Handle the GPL and LGPL, too. */ -/** - * @private - */ -function ccSubEpilogue($type) { - echo " </cc:{$type}>\n"; -} + $knownLicenses['http://creativecommons.org/licenses/GPL/2.0/'] = + array('de', 're', 'di', 'no', 'sa', 'sc'); + $knownLicenses['http://creativecommons.org/licenses/LGPL/2.1/'] = + array('de', 're', 'di', 'no', 'sa', 'sc'); + $knownLicenses['http://www.gnu.org/copyleft/fdl.html'] = + array('de', 're', 'di', 'no', 'sa', 'sc'); -/** - * @private - */ -function ccLicense($terms) { - - foreach ($terms as $term) { - switch ($term) { - case 're': - ccTerm('permits', 'Reproduction'); break; - case 'di': - ccTerm('permits', 'Distribution'); break; - case 'de': - ccTerm('permits', 'DerivativeWorks'); break; - case 'nc': - ccTerm('prohibits', 'CommercialUse'); break; - case 'no': - ccTerm('requires', 'Notice'); break; - case 'by': - ccTerm('requires', 'Attribution'); break; - case 'sa': - ccTerm('requires', 'ShareAlike'); break; - case 'sc': - ccTerm('requires', 'SourceCode'); break; - } + return $knownLicenses; } } -/** - * @private - */ -function ccTerm($term, $name) { - print " <cc:{$term} rdf:resource=\"http://web.resource.org/cc/{$name}\" />\n"; -} +class DublinCoreRdf extends RdfMetaData { -/** - * @private - */ -function ccEpilogue() { - echo "</rdf:RDF>\n"; -} + public function show(){ + if( $this->setup() ){ + $this->prologue(); + $this->basics(); + $this->epilogue(); + } + } -/** - * @private - */ -function dcElement($name, $value) { - $value = htmlspecialchars( $value ); - print " <dc:{$name}>{$value}</dc:{$name}>\n"; -} + /** + * begin of the page + */ + protected function prologue() { + global $wgOutputEncoding; + + $url = htmlspecialchars( $this->reallyFullUrl() ); + print <<<PROLOGUE +<?xml version="1.0" encoding="{$wgOutputEncoding}" ?> +<!DOCTYPE rdf:RDF PUBLIC "-//DUBLIN CORE//DCMES DTD 2002/07/31//EN" "http://dublincore.org/documents/2002/07/31/dcmes-xml/dcmes-xml-dtd.dtd"> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <rdf:Description rdf:about="{$url}"> + +PROLOGUE; + } -/** - * @private - */ -function dcDate($timestamp) { - return substr($timestamp, 0, 4) . '-' - . substr($timestamp, 4, 2) . '-' - . substr($timestamp, 6, 2); + /** + * end of the page + */ + protected function epilogue() { + print <<<EPILOGUE + </rdf:Description> +</rdf:RDF> +EPILOGUE; + } } -/** - * @private - */ -function dcReallyFullUrl($title) { - return $title->getFullURL(); -} +class CreativeCommonsRdf extends RdfMetaData { -/** - * @private - */ -function dcPageOrString($name, $page, $str) { - $nt = Title::newFromText($page); + public function show(){ + if( $this->setup() ){ + global $wgRightsUrl; - if (!$nt || $nt->getArticleID() == 0) { - dcElement($name, $str); - } else { - dcPage($name, $nt); - } -} + $url = $this->reallyFullUrl(); -/** - * @private - */ -function dcPage($name, $title) { - dcUrl($name, dcReallyFullUrl($title)); -} + $this->prologue(); + $this->subPrologue('Work', $url); -/** - * @private - */ -function dcUrl($name, $url) { - $url = htmlspecialchars( $url ); - print " <dc:{$name} rdf:resource=\"{$url}\" />\n"; -} + $this->basics(); + if( $wgRightsUrl ){ + $url = htmlspecialchars( $wgRightsUrl ); + print "\t\t<cc:license rdf:resource=\"$url\" />\n"; + } -/** - * @private - */ -function dcPerson($name, $id, $user_name='', $user_real_name='') { - global $wgContLang; - - if ($id == 0) { - dcElement($name, wfMsg('anonymous')); - } else if ( !empty($user_real_name) ) { - dcElement($name, $user_real_name); - } else { - # XXX: This shouldn't happen. - if( empty( $user_name ) ) { - $user_name = User::whoIs($id); + $this->subEpilogue('Work'); + + if( $wgRightsUrl ){ + $terms = $this->getTerms( $wgRightsUrl ); + if( $terms ){ + $this->subPrologue( 'License', $wgRightsUrl ); + $this->license( $terms ); + $this->subEpilogue( 'License' ); + } + } } - dcPageOrString($name, $wgContLang->getNsText(NS_USER) . ':' . $user_name, wfMsg('siteuser', $user_name)); + + $this->epilogue(); } -} -/** - * Takes an arg, for future enhancement with different rights for - * different pages. - * @private - */ -function dcRights() { - - global $wgRightsPage, $wgRightsUrl, $wgRightsText; - - if (isset($wgRightsPage) && - ($nt = Title::newFromText($wgRightsPage)) - && ($nt->getArticleID() != 0)) { - dcPage('rights', $nt); - } else if (isset($wgRightsUrl)) { - dcUrl('rights', $wgRightsUrl); - } else if (isset($wgRightsText)) { - dcElement('rights', $wgRightsText); + protected function prologue() { + global $wgOutputEncoding; + echo <<<PROLOGUE +<?xml version='1.0' encoding="{$wgOutputEncoding}" ?> +<rdf:RDF xmlns:cc="http://web.resource.org/cc/" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + +PROLOGUE; } -} -/** - * @private - */ -function ccGetTerms($url) { - global $wgLicenseTerms; - - if (isset($wgLicenseTerms)) { - return $wgLicenseTerms; - } else { - $known = getKnownLicenses(); - if( isset( $known[$url] ) ) { - return $known[$url]; - } else { - return array(); - } + protected function subPrologue( $type, $url ){ + $url = htmlspecialchars( $url ); + echo "\t<cc:{$type} rdf:about=\"{$url}\">\n"; } -} -/** - * @private - */ -function getKnownLicenses() { - - $ccLicenses = array('by', 'by-nd', 'by-nd-nc', 'by-nc', - 'by-nc-sa', 'by-sa'); - $ccVersions = array('1.0', '2.0'); - $knownLicenses = array(); - - foreach ($ccVersions as $version) { - foreach ($ccLicenses as $license) { - if( $version == '2.0' && substr( $license, 0, 2) != 'by' ) { - # 2.0 dropped the non-attribs licenses - continue; - } - $lurl = "http://creativecommons.org/licenses/{$license}/{$version}/"; - $knownLicenses[$lurl] = explode('-', $license); - $knownLicenses[$lurl][] = 're'; - $knownLicenses[$lurl][] = 'di'; - $knownLicenses[$lurl][] = 'no'; - if (!in_array('nd', $knownLicenses[$lurl])) { - $knownLicenses[$lurl][] = 'de'; + protected function subEpilogue($type) { + echo "\t</cc:{$type}>\n"; + } + + protected function license($terms) { + + foreach( $terms as $term ){ + switch( $term ) { + case 're': + $this->term('permits', 'Reproduction'); break; + case 'di': + $this->term('permits', 'Distribution'); break; + case 'de': + $this->term('permits', 'DerivativeWorks'); break; + case 'nc': + $this->term('prohibits', 'CommercialUse'); break; + case 'no': + $this->term('requires', 'Notice'); break; + case 'by': + $this->term('requires', 'Attribution'); break; + case 'sa': + $this->term('requires', 'ShareAlike'); break; + case 'sc': + $this->term('requires', 'SourceCode'); break; } } } - /* Handle the GPL and LGPL, too. */ - - $knownLicenses['http://creativecommons.org/licenses/GPL/2.0/'] = - array('de', 're', 'di', 'no', 'sa', 'sc'); - $knownLicenses['http://creativecommons.org/licenses/LGPL/2.1/'] = - array('de', 're', 'di', 'no', 'sa', 'sc'); - $knownLicenses['http://www.gnu.org/copyleft/fdl.html'] = - array('de', 're', 'di', 'no', 'sa', 'sc'); + protected function term( $term, $name ){ + print "\t\t<cc:{$term} rdf:resource=\"http://web.resource.org/cc/{$name}\" />\n"; + } - return $knownLicenses; -} + protected function epilogue() { + echo "</rdf:RDF>\n"; + } +}
\ No newline at end of file diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index e33b1c0a..4797752d 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -11,6 +11,22 @@ define('MM_WELL_KNOWN_MIME_TYPES',<<<END_STRING application/ogg ogg ogm ogv application/pdf pdf +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template otf +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-master otm +application/vnd.oasis.opendocument.text-web oth application/x-javascript js application/x-shockwave-flash swf audio/midi mid midi kar @@ -41,6 +57,22 @@ END_STRING */ define('MM_WELL_KNOWN_MIME_INFO', <<<END_STRING application/pdf [OFFICE] +application/vnd.oasis.opendocument.chart [OFFICE] +application/vnd.oasis.opendocument.chart-template [OFFICE] +application/vnd.oasis.opendocument.formula [OFFICE] +application/vnd.oasis.opendocument.formula-template [OFFICE] +application/vnd.oasis.opendocument.graphics [OFFICE] +application/vnd.oasis.opendocument.graphics-template [OFFICE] +application/vnd.oasis.opendocument.image [OFFICE] +application/vnd.oasis.opendocument.image-template [OFFICE] +application/vnd.oasis.opendocument.presentation [OFFICE] +application/vnd.oasis.opendocument.presentation-template [OFFICE] +application/vnd.oasis.opendocument.spreadsheet [OFFICE] +application/vnd.oasis.opendocument.spreadsheet-template [OFFICE] +application/vnd.oasis.opendocument.text [OFFICE] +application/vnd.oasis.opendocument.text-template [OFFICE] +application/vnd.oasis.opendocument.text-master [OFFICE] +application/vnd.oasis.opendocument.text-web [OFFICE] text/javascript application/x-javascript [EXECUTABLE] application/x-shockwave-flash [MULTIMEDIA] audio/midi [AUDIO] @@ -406,6 +438,8 @@ class MimeMagic { wfRestoreWarnings(); if( !$f ) return "unknown/unknown"; $head = fread( $f, 1024 ); + fseek( $f, -65558, SEEK_END ); + $tail = fread( $f, 65558 ); // 65558 = maximum size of a zip EOCDR fclose( $f ); // Hardcode a few magic number checks... @@ -462,8 +496,8 @@ class MimeMagic { $xml = new XmlTypeCheck( $file ); if( $xml->wellFormed ) { global $wgXMLMimeTypes; - if( isset( $wgXMLMimeTypes[$xml->rootElement] ) ) { - return $wgXMLMimeTypes[$xml->rootElement]; + if( isset( $wgXMLMimeTypes[$xml->getRootElement()] ) ) { + return $wgXMLMimeTypes[$xml->getRootElement()]; } else { return 'application/xml'; } @@ -509,6 +543,12 @@ class MimeMagic { } } + // Check for ZIP (before getimagesize) + if ( strpos( $tail, "PK\x05\x06" ) !== false ) { + wfDebug( __METHOD__.": ZIP header present at end of $file\n" ); + return $this->detectZipType( $head ); + } + wfSuppressWarnings(); $gis = getimagesize( $file ); wfRestoreWarnings(); @@ -517,8 +557,6 @@ class MimeMagic { $mime = $gis['mime']; wfDebug( __METHOD__.": getimagesize detected $file as $mime\n" ); return $mime; - } else { - return false; } // Also test DjVu @@ -527,6 +565,50 @@ class MimeMagic { wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" ); return 'image/vnd.djvu'; } + + return false; + } + + /** + * Detect application-specific file type of a given ZIP file from its + * header data. Currently works for OpenDocument types... + * If can't tell, returns 'application/zip'. + * + * @param string $header Some reasonably-sized chunk of file header + * @return string + */ + function detectZipType( $header ) { + $opendocTypes = array( + 'chart', + 'chart-template', + 'formula', + 'formula-template', + 'graphics', + 'graphics-template', + 'image', + 'image-template', + 'presentation', + 'presentation-template', + 'spreadsheet', + 'spreadsheet-template', + 'text', + 'text-template', + 'text-master', + 'text-web' ); + + // http://lists.oasis-open.org/archives/office/200505/msg00006.html + $types = '(?:' . implode( '|', $opendocTypes ) . ')'; + $opendocRegex = "/^mimetype(application\/vnd\.oasis\.opendocument\.$types)/"; + wfDebug( __METHOD__.": $opendocRegex\n" ); + + if( preg_match( $opendocRegex, substr( $header, 30 ), $matches ) ) { + $mime = $matches[1]; + wfDebug( __METHOD__.": detected $mime from ZIP archive\n" ); + return $mime; + } else { + wfDebug( __METHOD__.": unable to identify type of ZIP archive\n" ); + return 'application/zip'; + } } /** Internal mime type detection, please use guessMimeType() for application code instead. diff --git a/includes/Namespace.php b/includes/Namespace.php index 7c7b7ded..3d618e64 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -16,8 +16,8 @@ $wgCanonicalNamespaceNames = array( NS_USER_TALK => 'User_talk', NS_PROJECT => 'Project', NS_PROJECT_TALK => 'Project_talk', - NS_IMAGE => 'Image', - NS_IMAGE_TALK => 'Image_talk', + NS_FILE => 'File', + NS_FILE_TALK => 'File_talk', NS_MEDIAWIKI => 'MediaWiki', NS_MEDIAWIKI_TALK => 'MediaWiki_talk', NS_TEMPLATE => 'Template', @@ -53,7 +53,7 @@ class MWNamespace { */ public static function isMovable( $index ) { global $wgAllowImageMoving; - return !( $index < NS_MAIN || ($index == NS_IMAGE && !$wgAllowImageMoving) || $index == NS_CATEGORY ); + return !( $index < NS_MAIN || ($index == NS_FILE && !$wgAllowImageMoving) || $index == NS_CATEGORY ); } /** @@ -105,11 +105,15 @@ class MWNamespace { * Returns the canonical (English Wikipedia) name for a given index * * @param $index Int: namespace index - * @return string + * @return string or false if no canonical definition. */ public static function getCanonicalName( $index ) { global $wgCanonicalNamespaceNames; - return $wgCanonicalNamespaceNames[$index]; + if( isset( $wgCanonicalNamespaceNames[$index] ) ) { + return $wgCanonicalNamespaceNames[$index]; + } else { + return false; + } } /** diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php index 01b61dfb..6cfb2340 100644 --- a/includes/ObjectCache.php +++ b/includes/ObjectCache.php @@ -32,7 +32,10 @@ class FakeMemCachedClient { global $wgCaches; $wgCaches = array(); -/** @todo document */ +/** + * Get a cache object. + * @param int $inputType cache type, one the the CACHE_* constants. + */ function &wfGetCache( $inputType ) { global $wgCaches, $wgMemCachedServers, $wgMemCachedDebug, $wgMemCachedPersistent; $cache = false; @@ -48,23 +51,20 @@ function &wfGetCache( $inputType ) { } if ( $type == CACHE_MEMCACHED ) { - if ( !array_key_exists( CACHE_MEMCACHED, $wgCaches ) ){ - require_once( 'memcached-client.php' ); - - if (!class_exists("MemcachedClientforWiki")) { + if ( !array_key_exists( CACHE_MEMCACHED, $wgCaches ) ) { + if ( !class_exists( 'MemcachedClientforWiki' ) ) { class MemCachedClientforWiki extends memcached { function _debugprint( $text ) { wfDebug( "memcached: $text" ); } } } - - $wgCaches[CACHE_DB] = new MemCachedClientforWiki( + $wgCaches[CACHE_MEMCACHED] = new MemCachedClientforWiki( array('persistant' => $wgMemCachedPersistent, 'compress_threshold' => 1500 ) ); - $cache =& $wgCaches[CACHE_DB]; - $cache->set_servers( $wgMemCachedServers ); - $cache->set_debug( $wgMemCachedDebug ); + $wgCaches[CACHE_MEMCACHED]->set_servers( $wgMemCachedServers ); + $wgCaches[CACHE_MEMCACHED]->set_debug( $wgMemCachedDebug ); } + $cache =& $wgCaches[CACHE_MEMCACHED]; } elseif ( $type == CACHE_ACCEL ) { if ( !array_key_exists( CACHE_ACCEL, $wgCaches ) ) { if ( function_exists( 'eaccelerator_get' ) ) { @@ -106,18 +106,21 @@ function &wfGetCache( $inputType ) { return $cache; } +/** Get the main cache object */ function &wfGetMainCache() { global $wgMainCacheType; $ret =& wfGetCache( $wgMainCacheType ); return $ret; } +/** Get the cache object used by the message cache */ function &wfGetMessageCacheStorage() { global $wgMessageCacheType; $ret =& wfGetCache( $wgMessageCacheType ); return $ret; } +/** Get the cache object used by the parser cache */ function &wfGetParserCacheStorage() { global $wgParserCacheType; $ret =& wfGetCache( $wgParserCacheType ); diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 8226cb2f..f8dba714 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -6,20 +6,23 @@ if ( ! defined( 'MEDIAWIKI' ) ) * @todo document */ class OutputPage { - var $mMetatags, $mKeywords; - var $mLinktags, $mPagetitle, $mBodytext, $mDebugtext; - var $mHTMLtitle, $mRobotpolicy, $mIsarticle, $mPrintable; - var $mSubtitle, $mRedirect, $mStatusCode; - var $mLastModified, $mETag, $mCategoryLinks; - var $mScripts, $mLinkColours, $mPageLinkTitle; + var $mMetatags = array(), $mKeywords = array(), $mLinktags = array(); + var $mExtStyles = array(); + var $mPagetitle = '', $mBodytext = '', $mDebugtext = ''; + var $mHTMLtitle = '', $mIsarticle = true, $mPrintable = false; + var $mSubtitle = '', $mRedirect = '', $mStatusCode; + var $mLastModified = '', $mETag = false; + var $mCategoryLinks = array(), $mLanguageLinks = array(); + var $mScripts = '', $mLinkColours, $mPageLinkTitle = '', $mHeadItems = array(); + var $mTemplateIds = array(); var $mAllowUserJs; - var $mSuppressQuickbar; - var $mOnloadHandler; - var $mDoNothing; - var $mContainsOldMagic, $mContainsNewMagic; - var $mIsArticleRelated; - protected $mParserOptions; // lazy initialised, use parserOptions() + var $mSuppressQuickbar = false; + var $mOnloadHandler = ''; + var $mDoNothing = false; + var $mContainsOldMagic = 0, $mContainsNewMagic = 0; + var $mIsArticleRelated = true; + protected $mParserOptions = null; // lazy initialised, use parserOptions() var $mShowFeedLinks = false; var $mFeedLinksAppendQuery = false; var $mEnableClientCache = true; @@ -29,6 +32,18 @@ class OutputPage { var $mNoGallery = false; var $mPageTitleActionText = ''; var $mParseWarnings = array(); + var $mSquidMaxage = 0; + var $mRevisionId = null; + + /** + * An array of stylesheet filenames (relative from skins path), with options + * for CSS media, IE conditions, and RTL/LTR direction. + * For internal use; add settings in the skin via $this->addStyle() + */ + var $styles = array(); + + private $mIndexPolicy = 'index'; + private $mFollowPolicy = 'follow'; /** * Constructor @@ -37,25 +52,6 @@ class OutputPage { function __construct() { global $wgAllowUserJs; $this->mAllowUserJs = $wgAllowUserJs; - $this->mMetatags = $this->mKeywords = $this->mLinktags = array(); - $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext = - $this->mRedirect = $this->mLastModified = - $this->mSubtitle = $this->mDebugtext = $this->mRobotpolicy = - $this->mOnloadHandler = $this->mPageLinkTitle = ''; - $this->mIsArticleRelated = $this->mIsarticle = $this->mPrintable = true; - $this->mSuppressQuickbar = $this->mPrintable = false; - $this->mLanguageLinks = array(); - $this->mCategoryLinks = array(); - $this->mDoNothing = false; - $this->mContainsOldMagic = $this->mContainsNewMagic = 0; - $this->mParserOptions = null; - $this->mSquidMaxage = 0; - $this->mScripts = ''; - $this->mHeadItems = array(); - $this->mETag = false; - $this->mRevisionId = null; - $this->mNewSectionLink = false; - $this->mTemplateIds = array(); } public function redirect( $url, $responsecode = '302' ) { @@ -76,17 +72,23 @@ class OutputPage { */ function setStatusCode( $statusCode ) { $this->mStatusCode = $statusCode; } - # To add an http-equiv meta tag, precede the name with "http:" - function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); } + /** + * Add a new <meta> tag + * To add an http-equiv meta tag, precede the name with "http:" + * + * @param $name tag name + * @param $val tag value + */ + function addMeta( $name, $val ) { + array_push( $this->mMetatags, array( $name, $val ) ); + } + function addKeyword( $text ) { array_push( $this->mKeywords, $text ); } function addScript( $script ) { $this->mScripts .= "\t\t".$script; } - function addStyle( $style ) { - global $wgStylePath, $wgStyleVersion; - $this->addLink( - array( - 'rel' => 'stylesheet', - 'href' => $wgStylePath . '/' . $style . '?' . $wgStyleVersion, - 'type' => 'text/css' ) ); + + function addExtensionStyle( $url ) { + $linkarr = array( 'rel' => 'stylesheet', 'href' => $url, 'type' => 'text/css' ); + array_push( $this->mExtStyles, $linkarr ); } /** @@ -100,7 +102,6 @@ class OutputPage { } else { $path = "{$wgStylePath}/common/{$file}"; } - $encPath = htmlspecialchars( $path ); $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"$path?$wgStyleVersion\"></script>\n" ); } @@ -141,6 +142,11 @@ class OutputPage { # $linkarr should be an associative array of attributes. We'll escape on output. array_push( $this->mLinktags, $linkarr ); } + + # Get all links added by extensions + function getExtStyle() { + return $this->mExtStyles; + } function addMetadataLink( $linkarr ) { # note: buggy CC software only reads first "meta" link @@ -155,62 +161,87 @@ class OutputPage { * possible. If sucessful, the OutputPage is disabled so that * any future call to OutputPage->output() have no effect. * + * Side effect: sets mLastModified for Last-Modified header + * * @return bool True iff cache-ok headers was sent. */ function checkLastModified ( $timestamp ) { global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest; - + if ( !$timestamp || $timestamp == '19700101000000' ) { wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" ); - return; + return false; } if( !$wgCachePages ) { wfDebug( __METHOD__ . ": CACHE DISABLED\n", false ); - return; + return false; } if( $wgUser->getOption( 'nocache' ) ) { wfDebug( __METHOD__ . ": USER DISABLED CACHE\n", false ); - return; + return false; } - $timestamp=wfTimestamp(TS_MW,$timestamp); - $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) ); + $timestamp = wfTimestamp( TS_MW, $timestamp ); + $modifiedTimes = array( + 'page' => $timestamp, + 'user' => $wgUser->getTouched(), + 'epoch' => $wgCacheEpoch + ); + wfRunHooks( 'OutputPageCheckLastModified', array( &$modifiedTimes ) ); - if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { - # IE sends sizes after the date like this: - # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 - # this breaks strtotime(). - $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); + $maxModified = max( $modifiedTimes ); + $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified ); - wfSuppressWarnings(); // E_STRICT system time bitching - $modsinceTime = strtotime( $modsince ); - wfRestoreWarnings(); + if( empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", false ); + return false; + } - $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 ); - wfDebug( __METHOD__ . ": -- client send If-Modified-Since: " . $modsince . "\n", false ); - wfDebug( __METHOD__ . ": -- we might send Last-Modified : $lastmod\n", false ); - if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { - # Make sure you're in a place you can leave when you call us! - $wgRequest->response()->header( "HTTP/1.0 304 Not Modified" ); - $this->mLastModified = $lastmod; - $this->sendCacheControl(); - wfDebug( __METHOD__ . ": CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); - $this->disable(); + # Make debug info + $info = ''; + foreach ( $modifiedTimes as $name => $value ) { + if ( $info !== '' ) { + $info .= ', '; + } + $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value ); + } - // Don't output a compressed blob when using ob_gzhandler; - // it's technically against HTTP spec and seems to confuse - // Firefox when the response gets split over two packets. - wfClearOutputBuffers(); + # IE sends sizes after the date like this: + # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 + # this breaks strtotime(). + $clientHeader = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); - return true; - } else { - wfDebug( __METHOD__ . ": READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); - $this->mLastModified = $lastmod; - } - } else { - wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", false ); - $this->mLastModified = $lastmod; + wfSuppressWarnings(); // E_STRICT system time bitching + $clientHeaderTime = strtotime( $clientHeader ); + wfRestoreWarnings(); + if ( !$clientHeaderTime ) { + wfDebug( __METHOD__ . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" ); + return false; } + $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime ); + + wfDebug( __METHOD__ . ": client sent If-Modified-Since: " . + wfTimestamp( TS_ISO_8601, $clientHeaderTime ) . "\n", false ); + wfDebug( __METHOD__ . ": effective Last-Modified: " . + wfTimestamp( TS_ISO_8601, $maxModified ) . "\n", false ); + if( $clientHeaderTime < $maxModified ) { + wfDebug( __METHOD__ . ": STALE, $info\n", false ); + return false; + } + + # Not modified + # Give a 304 response code and disable body output + wfDebug( __METHOD__ . ": NOT MODIFIED, $info\n", false ); + $wgRequest->response()->header( "HTTP/1.1 304 Not Modified" ); + $this->sendCacheControl(); + $this->disable(); + + // Don't output a compressed blob when using ob_gzhandler; + // it's technically against HTTP spec and seems to confuse + // Firefox when the response gets split over two packets. + wfClearOutputBuffers(); + + return true; } function setPageTitleActionText( $text ) { @@ -223,7 +254,61 @@ class OutputPage { } } - public function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; } + /** + * Set the robot policy for the page: <http://www.robotstxt.org/meta.html> + * + * @param $policy string The literal string to output as the contents of + * the meta tag. Will be parsed according to the spec and output in + * standardized form. + * @return null + */ + public function setRobotPolicy( $policy ) { + $policy = explode( ',', $policy ); + $policy = array_map( 'trim', $policy ); + + # The default policy is follow, so if nothing is said explicitly, we + # do that. + if( in_array( 'nofollow', $policy ) ) { + $this->mFollowPolicy = 'nofollow'; + } else { + $this->mFollowPolicy = 'follow'; + } + + if( in_array( 'noindex', $policy ) ) { + $this->mIndexPolicy = 'noindex'; + } else { + $this->mIndexPolicy = 'index'; + } + } + + /** + * Set the index policy for the page, but leave the follow policy un- + * touched. + * + * @param $policy string Either 'index' or 'noindex'. + * @return null + */ + public function setIndexPolicy( $policy ) { + $policy = trim( $policy ); + if( in_array( $policy, array( 'index', 'noindex' ) ) ) { + $this->mIndexPolicy = $policy; + } + } + + /** + * Set the follow policy for the page, but leave the index policy un- + * touched. + * + * @param $policy string Either 'follow' or 'nofollow'. + * @return null + */ + public function setFollowPolicy( $policy ) { + $policy = trim( $policy ); + if( in_array( $policy, array( 'follow', 'nofollow' ) ) ) { + $this->mFollowPolicy = $policy; + } + } + public function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; } public function setPageTitle( $name ) { global $action, $wgContLang; @@ -341,6 +426,7 @@ class OutputPage { public function disallowUserJs() { $this->mAllowUserJs = false; } public function isUserJsAllowed() { return $this->mAllowUserJs; } + public function prependHTML( $text ) { $this->mBodytext = $text . $this->mBodytext; } public function addHTML( $text ) { $this->mBodytext .= $text; } public function clearHTML() { $this->mBodytext = ''; } public function getHTML() { return $this->mBodytext; } @@ -369,6 +455,10 @@ class OutputPage { $val = is_null( $revid ) ? null : intval( $revid ); return wfSetVar( $this->mRevisionId, $val ); } + + public function getRevisionId() { + return $this->mRevisionId; + } /** * Convert wikitext to HTML and add it to the buffer @@ -416,9 +506,23 @@ class OutputPage { * @param ParserOutput object &$parserOutput */ public function addParserOutputNoText( &$parserOutput ) { + global $wgTitle, $wgExemptFromUserRobotsControl, $wgContentNamespaces; + $this->mLanguageLinks += $parserOutput->getLanguageLinks(); $this->addCategoryLinks( $parserOutput->getCategories() ); $this->mNewSectionLink = $parserOutput->getNewSection(); + + if( is_null( $wgExemptFromUserRobotsControl ) ) { + $bannedNamespaces = $wgContentNamespaces; + } else { + $bannedNamespaces = $wgExemptFromUserRobotsControl; + } + if( !in_array( $wgTitle->getNamespace(), $bannedNamespaces ) ) { + # FIXME (bug 14900): This overrides $wgArticleRobotPolicies, and it + # shouldn't + $this->setIndexPolicy( $parserOutput->getIndexPolicy() ); + } + $this->addKeywords( $parserOutput ); $this->mParseWarnings = $parserOutput->getWarnings(); if ( $parserOutput->getCacheTime() == -1 ) { @@ -427,8 +531,13 @@ class OutputPage { $this->mNoGallery = $parserOutput->getNoGallery(); $this->mHeadItems = array_merge( $this->mHeadItems, (array)$parserOutput->mHeadItems ); // Versioning... - $this->mTemplateIds = wfArrayMerge( $this->mTemplateIds, (array)$parserOutput->mTemplateIds ); - + foreach ( (array)$parserOutput->mTemplateIds as $ns => $dbks ) { + if ( isset( $this->mTemplateIds[$ns] ) ) { + $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns]; + } else { + $this->mTemplateIds[$ns] = $dbks; + } + } // Display title if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) $this->setPageTitle( $dt ); @@ -522,6 +631,9 @@ class OutputPage { */ public function parse( $text, $linestart = true, $interface = false ) { global $wgParser, $wgTitle; + if( is_null( $wgTitle ) ) { + throw new MWException( 'Empty $wgTitle in ' . __METHOD__ ); + } $popts = $this->parserOptions(); if ( $interface) { $popts->setInterfaceMessage(true); } $parserOutput = $wgParser->parse( $text, $wgTitle, $popts, @@ -590,7 +702,7 @@ class OutputPage { * If it does, it's very important that we don't allow public caching */ function haveCacheVaryCookies() { - global $wgRequest, $wgCookiePrefix; + global $wgRequest; $cookieHeader = $wgRequest->getHeader( 'cookie' ); if ( $cookieHeader === false ) { return false; @@ -609,7 +721,6 @@ class OutputPage { /** Get a complete X-Vary-Options header */ public function getXVO() { - global $wgCookiePrefix; $cvCookies = $this->getCacheVaryCookies(); $xvo = 'X-Vary-Options: Accept-Encoding;list-contains=gzip,Cookie;'; $first = true; @@ -668,7 +779,9 @@ class OutputPage { $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); $response->header( "Cache-Control: private, must-revalidate, max-age=0" ); } - if($this->mLastModified) $response->header( "Last-modified: {$this->mLastModified}" ); + if($this->mLastModified) { + $response->header( "Last-Modified: {$this->mLastModified}" ); + } } else { wfDebug( __METHOD__ . ": no caching **\n", false ); @@ -687,8 +800,9 @@ class OutputPage { public function output() { global $wgUser, $wgOutputEncoding, $wgRequest; global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType; - global $wgJsMimeType, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch; - global $wgServer, $wgEnableMWSuggest; + global $wgJsMimeType, $wgUseAjax, $wgAjaxWatch; + global $wgEnableMWSuggest, $wgUniversalEditButton; + global $wgArticle, $wgTitle; if( $this->mDoNothing ){ return; @@ -782,11 +896,6 @@ class OutputPage { wfRunHooks( 'AjaxAddScript', array( &$this ) ); - if( $wgAjaxSearch && $wgUser->getBoolOption( 'ajaxsearch' ) ) { - $this->addScriptFile( 'ajaxsearch.js' ); - $this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" ); - } - if( $wgAjaxWatch && $wgUser->isLoggedIn() ) { $this->addScriptFile( 'ajaxwatch.js' ); } @@ -800,13 +909,28 @@ class OutputPage { $this->addScriptFile( 'rightclickedit.js' ); } - + if( $wgUniversalEditButton ) { + if( isset( $wgArticle ) && isset( $wgTitle ) && $wgTitle->quickUserCan( 'edit' ) + && ( $wgTitle->exists() || $wgTitle->quickUserCan( 'create' ) ) ) { + // Original UniversalEditButton + $this->addLink( array( + 'rel' => 'alternate', + 'type' => 'application/x-wiki', + 'title' => wfMsg( 'edit' ), + 'href' => $wgTitle->getFullURL( 'action=edit' ) + ) ); + // Alternate edit link + $this->addLink( array( + 'rel' => 'edit', + 'title' => wfMsg( 'edit' ), + 'href' => $wgTitle->getFullURL( 'action=edit' ) + ) ); + } + } + # Buffer output; final headers may depend on later processing ob_start(); - # Disable temporary placeholders, so that the skin produces HTML - $sk->postParseLinkColour( false ); - $wgRequest->response()->header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); $wgRequest->response()->header( 'Content-language: '.$wgContLanguageCode ); @@ -879,7 +1003,7 @@ class OutputPage { global $wgUser, $wgContLang, $wgTitle, $wgLang; $this->setPageTitle( wfMsg( 'blockedtitle' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); $name = User::whoIs( $wgUser->blockedBy() ); @@ -945,7 +1069,7 @@ class OutputPage { } $this->setPageTitle( wfMsg( $title ) ); $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); $this->enableClientCache( false ); $this->mRedirect = ''; @@ -953,7 +1077,7 @@ class OutputPage { array_unshift( $params, 'parse' ); array_unshift( $params, $msg ); - $this->addHtml( call_user_func_array( 'wfMsgExt', $params ) ); + $this->addHTML( call_user_func_array( 'wfMsgExt', $params ) ); $this->returnToMain(); } @@ -971,7 +1095,7 @@ class OutputPage { $wgTitle->getPrefixedText() . "\n"; $this->setPageTitle( wfMsg( 'permissionserrors' ) ); $this->setHTMLTitle( wfMsg( 'permissionserrors' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); $this->enableClientCache( false ); $this->mRedirect = ''; @@ -994,7 +1118,7 @@ class OutputPage { public function versionRequired( $version ) { $this->setPageTitle( wfMsg( 'versionrequired', $version ) ); $this->setHTMLTitle( wfMsg( 'versionrequired', $version ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); $this->mBodytext = ''; @@ -1008,39 +1132,23 @@ class OutputPage { * @param string $permission key required */ public function permissionRequired( $permission ) { - global $wgGroupPermissions, $wgUser; + global $wgUser; $this->setPageTitle( wfMsg( 'badaccess' ) ); $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); $this->mBodytext = ''; - $groups = array(); - foreach( $wgGroupPermissions as $key => $value ) { - if( isset( $value[$permission] ) && $value[$permission] == true ) { - $groupName = User::getGroupName( $key ); - $groupPage = User::getGroupPage( $key ); - if( $groupPage ) { - $skin = $wgUser->getSkin(); - $groups[] = $skin->makeLinkObj( $groupPage, $groupName ); - } else { - $groups[] = $groupName; - } - } + $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), + User::getGroupsWithPermission( $permission ) ); + if( $groups ) { + $this->addWikiMsg( 'badaccess-groups', + implode( ', ', $groups ), + count( $groups) ); + } else { + $this->addWikiMsg( 'badaccess-group0' ); } - $n = count( $groups ); - $groups = implode( ', ', $groups ); - switch( $n ) { - case 0: - case 1: - case 2: - $message = wfMsgHtml( "badaccess-group$n", $groups ); - break; - default: - $message = wfMsgHtml( 'badaccess-groups', $groups ); - } - $this->addHtml( $message ); $this->returnToMain(); } @@ -1080,8 +1188,8 @@ class OutputPage { $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() ); - $this->addHtml( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) ); - $this->addHtml( "\n<!--" . $wgTitle->getPrefixedUrl() . "-->" ); + $this->addHTML( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) ); + $this->addHTML( "\n<!--" . $wgTitle->getPrefixedUrl() . "-->" ); # Don't return to the main page if the user can't read it # otherwise we'll end up in a pointless loop @@ -1103,8 +1211,8 @@ class OutputPage { if ($action == null) { $text = wfMsgNoTrans( 'permissionserrorstext', count($errors)). "\n\n"; } else { - $action_desc = wfMsg( "right-$action" ); - $action_desc[0] = strtolower($action_desc[0]); + global $wgLang; + $action_desc = wfMsg( "action-$action" ); $text = wfMsgNoTrans( 'permissionserrorstext-withaction', count($errors), $action_desc ) . "\n\n"; } @@ -1148,7 +1256,7 @@ class OutputPage { global $wgUser, $wgTitle; $skin = $wgUser->getSkin(); - $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); // If no reason is given, just supply a default "I can't let you do @@ -1170,7 +1278,7 @@ class OutputPage { // Wiki is read only $this->setPageTitle( wfMsg( 'readonly' ) ); $reason = wfReadOnlyReason(); - $this->addWikiMsg( 'readonlytext', $reason ); + $this->wrapWikiMsg( '<div class="mw-readonly-error">$1</div>', array( 'readonlytext', $reason ) ); } // Show source, if supplied @@ -1189,7 +1297,10 @@ class OutputPage { // Show templates used by this article $skin = $wgUser->getSkin(); $article = new Article( $wgTitle ); - $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) ); + $this->addHTML( "<div class='templatesUsed'> +{$skin->formatTemplates( $article->getUsedTemplates() )} +</div> +" ); } # If the title doesn't exist, it's fairly pointless to print a return @@ -1238,7 +1349,7 @@ class OutputPage { public function showFatalError( $message ) { $this->setPageTitle( wfMsg( "internalerror" ) ); - $this->setRobotpolicy( "noindex,nofollow" ); + $this->setRobotPolicy( "noindex,nofollow" ); $this->setArticleRelated( false ); $this->enableClientCache( false ); $this->mRedirect = ''; @@ -1272,8 +1383,9 @@ class OutputPage { */ public function addReturnTo( $title ) { global $wgUser; + $this->addLink( array( 'rel' => 'next', 'href' => $title->getFullUrl() ) ); $link = wfMsg( 'returnto', $wgUser->getSkin()->makeLinkObj( $title ) ); - $this->addHtml( "<p>{$link}</p>\n" ); + $this->addHTML( "<p>{$link}</p>\n" ); } /** @@ -1333,15 +1445,19 @@ class OutputPage { /** * @return string The doctype, opening <html>, and head element. */ - public function headElement() { + public function headElement( Skin $sk ) { global $wgDocType, $wgDTD, $wgContLanguageCode, $wgOutputEncoding, $wgMimeType; global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces; global $wgUser, $wgContLang, $wgUseTrackbacks, $wgTitle, $wgStyleVersion; + $this->addMeta( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" ); + $this->addStyle( 'common/wikiprintable.css', 'print' ); + $sk->setupUserCss( $this ); + + $ret = ''; + if( $wgMimeType == 'text/xml' || $wgMimeType == 'application/xhtml+xml' || $wgMimeType == 'application/xml' ) { - $ret = "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?>\n"; - } else { - $ret = ''; + $ret .= "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?>\n"; } $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\"\n \"$wgDTD\">\n"; @@ -1356,24 +1472,17 @@ class OutputPage { $ret .= "xmlns:{$tag}=\"{$ns}\" "; } $ret .= "xml:lang=\"$wgContLanguageCode\" lang=\"$wgContLanguageCode\" $rtl>\n"; - $ret .= "<head>\n<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n"; - $this->addMeta( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" ); - - $ret .= $this->getHeadLinks(); - global $wgStylePath; - if( $this->isPrintable() ) { - $media = ''; - } else { - $media = "media='print'"; + $ret .= "<head>\n<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n\t\t"; + $ret .= implode( "\t\t", array( + $this->getHeadLinks(), + $this->buildCssLinks(), + $sk->getHeadScripts( $this->mAllowUserJs ), + $this->mScripts, + $this->getHeadItems(), + )); + if( $sk->usercss ){ + $ret .= "<style type='text/css'>{$sk->usercss}</style>"; } - $printsheet = htmlspecialchars( "$wgStylePath/common/wikiprintable.css?$wgStyleVersion" ); - $ret .= "<link rel='stylesheet' type='text/css' $media href='$printsheet' />\n"; - - $sk = $wgUser->getSkin(); - $ret .= $sk->getHeadScripts( $this->mAllowUserJs ); - $ret .= $this->mScripts; - $ret .= $sk->getUserStyles(); - $ret .= $this->getHeadItems(); if ($wgUseTrackbacks && $this->isArticleRelated()) $ret .= $wgTitle->trackbackRDF(); @@ -1384,10 +1493,11 @@ class OutputPage { protected function addDefaultMeta() { global $wgVersion; - $this->addMeta( "generator", "MediaWiki $wgVersion" ); + $this->addMeta( 'http:Content-Style-Type', 'text/css' ); //bug 15835 + $this->addMeta( 'generator', "MediaWiki $wgVersion" ); - $p = $this->mRobotpolicy; - if( $p !== '' && $p != 'index,follow' ) { + $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}"; + if( $p !== 'index,follow' ) { // http://www.robotstxt.org/wc/meta-user.html // Only show if it's different from the default robots policy $this->addMeta( 'robots', $p ); @@ -1446,20 +1556,29 @@ class OutputPage { # Recent changes feed should appear on every page (except recentchanges, # that would be redundant). Put it after the per-page feed to avoid # changing existing behavior. It's still available, probably via a - # menu in your browser. - + # menu in your browser. Some sites might have a different feed they'd + # like to promote instead of the RC feed (maybe like a "Recent New Articles" + # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined. + # If so, use it instead. + + global $wgOverrideSiteFeed, $wgSitename, $wgFeedClasses; $rctitle = SpecialPage::getTitleFor( 'Recentchanges' ); - if ( $wgTitle->getPrefixedText() != $rctitle->getPrefixedText() ) { - global $wgSitename; - - $tags[] = $this->feedLink( - 'rss', - $rctitle->getFullURL( 'feed=rss' ), - wfMsg( 'site-rss-feed', $wgSitename ) ); - $tags[] = $this->feedLink( - 'atom', - $rctitle->getFullURL( 'feed=atom' ), - wfMsg( 'site-atom-feed', $wgSitename ) ); + + if ( $wgOverrideSiteFeed ) { + foreach ( $wgOverrideSiteFeed as $type => $feedUrl ) { + $tags[] = $this->feedLink ( + $type, + htmlspecialchars( $feedUrl ), + wfMsg( "site-{$type}-feed", $wgSitename ) ); + } + } + else if ( $wgTitle->getPrefixedText() != $rctitle->getPrefixedText() ) { + foreach( $wgFeedClasses as $format => $class ) { + $tags[] = $this->feedLink( + $format, + $rctitle->getFullURL( "feed={$format}" ), + wfMsg( "site-{$format}-feed", $wgSitename ) ); # For grep: 'site-rss-feed', 'site-atom-feed'. + } } } @@ -1500,6 +1619,118 @@ class OutputPage { } /** + * Add a local or specified stylesheet, with the given media options. + * Meant primarily for internal use... + * + * @param $media -- to specify a media type, 'screen', 'printable', 'handheld' or any. + * @param $conditional -- for IE conditional comments, specifying an IE version + * @param $dir -- set to 'rtl' or 'ltr' for direction-specific sheets + */ + public function addStyle( $style, $media='', $condition='', $dir='' ) { + $options = array(); + if( $media ) + $options['media'] = $media; + if( $condition ) + $options['condition'] = $condition; + if( $dir ) + $options['dir'] = $dir; + $this->styles[$style] = $options; + } + + /** + * Build a set of <link>s for the stylesheets specified in the $this->styles array. + * These will be applied to various media & IE conditionals. + */ + public function buildCssLinks() { + $links = array(); + foreach( $this->styles as $file => $options ) { + $link = $this->styleLink( $file, $options ); + if( $link ) + $links[] = $link; + } + + return implode( "\n\t\t", $links ); + } + + protected function styleLink( $style, $options ) { + global $wgRequest; + + if( isset( $options['dir'] ) ) { + global $wgContLang; + $siteDir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; + if( $siteDir != $options['dir'] ) + return ''; + } + + if( isset( $options['media'] ) ) { + $media = $this->transformCssMedia( $options['media'] ); + if( is_null( $media ) ) { + return ''; + } + } else { + $media = ''; + } + + if( substr( $style, 0, 1 ) == '/' || + substr( $style, 0, 5 ) == 'http:' || + substr( $style, 0, 6 ) == 'https:' ) { + $url = $style; + } else { + global $wgStylePath, $wgStyleVersion; + $url = $wgStylePath . '/' . $style . '?' . $wgStyleVersion; + } + + $attribs = array( + 'rel' => 'stylesheet', + 'href' => $url, + 'type' => 'text/css' ); + if( $media ) { + $attribs['media'] = $media; + } + + $link = Xml::element( 'link', $attribs ); + + if( isset( $options['condition'] ) ) { + $condition = htmlspecialchars( $options['condition'] ); + $link = "<!--[if $condition]>$link<![endif]-->"; + } + return $link; + } + + function transformCssMedia( $media ) { + global $wgRequest, $wgHandheldForIPhone; + + // Switch in on-screen display for media testing + $switches = array( + 'printable' => 'print', + 'handheld' => 'handheld', + ); + foreach( $switches as $switch => $targetMedia ) { + if( $wgRequest->getBool( $switch ) ) { + if( $media == $targetMedia ) { + $media = ''; + } elseif( $media == 'screen' ) { + return null; + } + } + } + + // Expand longer media queries as iPhone doesn't grok 'handheld' + if( $wgHandheldForIPhone ) { + $mediaAliases = array( + 'screen' => 'screen and (min-device-width: 481px)', + 'handheld' => 'handheld, only screen and (max-device-width: 480px)', + ); + + if( isset( $mediaAliases[$media] ) ) { + $media = $mediaAliases[$media]; + } + } + + return $media; + } + + /** * Turn off regular page output and return an error reponse * for when rate limiting has triggered. */ @@ -1543,7 +1774,7 @@ class OutputPage { ? 'lag-warn-normal' : 'lag-warn-high'; $warning = wfMsgExt( $message, 'parse', $lag ); - $this->addHtml( "<div class=\"mw-{$message}\">\n{$warning}\n</div>\n" ); + $this->addHTML( "<div class=\"mw-{$message}\">\n{$warning}\n</div>\n" ); } } diff --git a/includes/PageHistory.php b/includes/PageHistory.php index 870b57b7..b01b485e 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -22,7 +22,6 @@ class PageHistory { var $mArticle, $mTitle, $mSkin; var $lastdate; var $linesonpage; - var $mNotificationTimestamp; var $mLatestId = null; /** @@ -31,12 +30,10 @@ class PageHistory { * @param Article $article * @returns nothing */ - function __construct($article) { + function __construct( $article ) { global $wgUser; - $this->mArticle =& $article; $this->mTitle =& $article->mTitle; - $this->mNotificationTimestamp = NULL; $this->mSkin = $wgUser->getSkin(); $this->preCacheMessages(); } @@ -44,7 +41,7 @@ class PageHistory { function getArticle() { return $this->mArticle; } - + function getTitle() { return $this->mTitle; } @@ -68,15 +65,13 @@ class PageHistory { * @returns nothing */ function history() { - global $wgOut, $wgRequest, $wgTitle; + global $wgOut, $wgRequest, $wgTitle, $wgScript; /* * Allow client caching. */ - if( $wgOut->checkLastModified( $this->mArticle->getTouched() ) ) - /* Client cache fresh and headers sent, nothing more to do. */ - return; + return; // Client cache fresh and headers sent, nothing more to do. wfProfileIn( __METHOD__ ); @@ -87,13 +82,14 @@ class PageHistory { $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) ); $wgOut->setArticleFlag( false ); $wgOut->setArticleRelated( true ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setSyndicated( true ); $wgOut->setFeedAppendQuery( 'action=history' ); $wgOut->addScriptFile( 'history.js' ); $logPage = SpecialPage::getTitleFor( 'Log' ); - $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() ); + $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), + 'page=' . $this->mTitle->getPrefixedUrl() ); $wgOut->setSubtitle( $logLink ); $feedType = $wgRequest->getVal( 'feed' ); @@ -111,26 +107,29 @@ class PageHistory { return; } - /* - * "go=first" means to jump to the last (earliest) history page. - * This is deprecated, it no longer appears in the user interface + /** + * Add date selector to quickly get to a certain time */ - if ( $wgRequest->getText("go") == 'first' ) { - $limit = $wgRequest->getInt( 'limit', 50 ); - global $wgFeedLimit; - if( $limit > $wgFeedLimit ) { - $limit = $wgFeedLimit; - } - $wgOut->redirect( $wgTitle->getLocalURL( "action=history&limit={$limit}&dir=prev" ) ); - return; - } + $year = $wgRequest->getInt( 'year' ); + $month = $wgRequest->getInt( 'month' ); + + $action = htmlspecialchars( $wgScript ); + $wgOut->addHTML( + "<form action=\"$action\" method=\"get\" id=\"mw-history-searchform\">" . + Xml::fieldset( wfMsg( 'history-fieldset-title' ), false, array( 'id' => 'mw-history-search' ) ) . + Xml::hidden( 'title', $this->mTitle->getPrefixedDBKey() ) . "\n" . + Xml::hidden( 'action', 'history' ) . "\n" . + $this->getDateMenu( $year, $month ) . ' ' . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . + '</fieldset></form>' + ); wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) ); /** * Do the list */ - $pager = new PageHistoryPager( $this ); + $pager = new PageHistoryPager( $this, $year, $month ); $this->linesonpage = $pager->getNumRows(); $wgOut->addHTML( $pager->getNavigationBar() . @@ -139,21 +138,77 @@ class PageHistory { $this->endHistoryList() . $pager->getNavigationBar() ); + wfProfileOut( __METHOD__ ); } /** + * @return string Formatted HTML + * @param int $year + * @param int $month + */ + private function getDateMenu( $year, $month ) { + # Offset overrides year/month selection + if( $month && $month !== -1 ) { + $encMonth = intval( $month ); + } else { + $encMonth = ''; + } + if( $year ) { + $encYear = intval( $year ); + } else if( $encMonth ) { + $thisMonth = intval( gmdate( 'n' ) ); + $thisYear = intval( gmdate( 'Y' ) ); + if( intval($encMonth) > $thisMonth ) { + $thisYear--; + } + $encYear = $thisYear; + } else { + $encYear = ''; + } + return Xml::label( wfMsg( 'year' ), 'year' ) . ' '. + Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . + ' '. + Xml::label( wfMsg( 'month' ), 'month' ) . ' '. + Xml::monthSelector( $encMonth, -1 ); + } + + /** * Creates begin of history list with a submit button * * @return string HTML output */ function beginHistoryList() { - global $wgTitle, $wgScript; + global $wgTitle, $wgScript, $wgEnableHtmlDiff; $this->lastdate = ''; $s = wfMsgExt( 'histlegend', array( 'parse') ); - $s .= Xml::openElement( 'form', array( 'action' => $wgScript ) ); + $s .= Xml::openElement( 'form', array( 'action' => $wgScript, 'id' => 'mw-history-compare' ) ); $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); - $s .= $this->submitButton(); + if( $wgEnableHtmlDiff ) { + $s .= $this->submitButton( wfMsg( 'visualcomparison'), + array( + 'name' => 'htmldiff', + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-visualcomparison' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + $s .= $this->submitButton( wfMsg( 'wikicodecomparison'), + array( + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } else { + $s .= $this->submitButton( wfMsg( 'compareselectedversions'), + array( + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } $s .= '<ul id="pagehistory">' . "\n"; return $s; } @@ -164,8 +219,33 @@ class PageHistory { * @return string HTML output */ function endHistoryList() { + global $wgEnableHtmlDiff; $s = '</ul>'; - $s .= $this->submitButton( array( 'id' => 'historysubmit' ) ); + if( $wgEnableHtmlDiff ) { + $s .= $this->submitButton( wfMsg( 'visualcomparison'), + array( + 'name' => 'htmldiff', + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-visualcomparison' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + $s .= $this->submitButton( wfMsg( 'wikicodecomparison'), + array( + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } else { + $s .= $this->submitButton( wfMsg( 'compareselectedversions'), + array( + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } $s .= '</form>'; return $s; } @@ -173,19 +253,13 @@ class PageHistory { /** * Creates a submit button * - * @param array $bits optional CSS ID + * @param array $attributes attributes * @return string HTML output for the submit button */ - function submitButton( $bits = array() ) { + function submitButton($message, $attributes = array() ) { # Disable submit button if history has 1 revision only - if ( $this->linesonpage > 1 ) { - return Xml::submitButton( wfMsg( 'compareselectedversions' ), - $bits + array( - 'class' => 'historysubmit', - 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), - 'title' => wfMsg( 'tooltip-compareselectedversions' ), - ) - ); + if( $this->linesonpage > 1 ) { + return Xml::submitButton( $message , $attributes ); } else { return ''; } @@ -209,30 +283,29 @@ class PageHistory { $rev = new Revision( $row ); $rev->setTitle( $this->mTitle ); - $s = ''; $curlink = $this->curLink( $rev, $latest ); $lastlink = $this->lastLink( $rev, $next, $counter ); $arbitrary = $this->diffButtons( $rev, $firstInList, $counter ); $link = $this->revLink( $rev ); - $s .= "($curlink) ($lastlink) $arbitrary"; + $s = "($curlink) ($lastlink) $arbitrary"; if( $wgUser->isAllowed( 'deleterevision' ) ) { $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); if( $firstInList ) { - // We don't currently handle well changing the top revision's settings + // We don't currently handle well changing the top revision's settings $del = $this->message['rev-delundel']; } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { - // If revision was hidden from sysops + // If revision was hidden from sysops $del = $this->message['rev-delundel']; } else { $del = $this->mSkin->makeKnownLinkObj( $revdel, - $this->message['rev-delundel'], + $this->message['rev-delundel'], 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . '&oldid=' . urlencode( $rev->getId() ) ); // Bolden oversighted content if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) - $del = "<strong>$del</strong>"; + $del = "<strong>$del</strong>"; } $s .= " <tt>(<small>$del</small>)</tt> "; } @@ -244,38 +317,39 @@ class PageHistory { $s .= ' ' . Xml::element( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); } - if ( !is_null( $size = $rev->getSize() ) && $rev->userCan( Revision::DELETED_TEXT ) ) { + if( !is_null( $size = $rev->getSize() ) && $rev->userCan( Revision::DELETED_TEXT ) ) { $s .= ' ' . $this->mSkin->formatRevisionSize( $size ); } $s .= $this->mSkin->revComment( $rev, false, true ); - if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) { + if( $notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp) ) { $s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>'; } - #add blurb about text having been deleted if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; } $tools = array(); - if ( !is_null( $next ) && is_object( $next ) ) { - if( !$this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) - && !$this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ) - && $latest ) { - $tools[] = '<span class="mw-rollback-link">' - . $this->mSkin->buildRollbackLink( $rev ) - . '</span>'; + if( !is_null( $next ) && is_object( $next ) ) { + if( $latest && $this->mTitle->userCan( 'rollback' ) && $this->mTitle->userCan( 'edit' ) ) { + $tools[] = '<span class="mw-rollback-link">'.$this->mSkin->buildRollbackLink( $rev ).'</span>'; } - if( $this->mTitle->quickUserCan( 'edit' ) && - !$rev->isDeleted( Revision::DELETED_TEXT ) && - !$next->rev_deleted & Revision::DELETED_TEXT ) { - $undolink = $this->mSkin->makeKnownLinkObj( + if( $this->mTitle->quickUserCan( 'edit' ) && !$rev->isDeleted( Revision::DELETED_TEXT ) && + !$next->rev_deleted & Revision::DELETED_TEXT ) + { + # Create undo tooltip for the first (=latest) line only + $undoTooltip = $latest + ? array( 'title' => wfMsg( 'tooltip-undo' ) ) + : array(); + $undolink = $this->mSkin->link( $this->mTitle, wfMsgHtml( 'editundo' ), - 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId() + $undoTooltip, + array( 'action' => 'edit', 'undoafter' => $next->rev_id, 'undo' => $rev->getId() ), + array( 'known', 'noclasses' ) ); $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>"; } @@ -291,16 +365,16 @@ class PageHistory { } /** - * Create a link to view this revision of the page - * @param Revision $rev - * @returns string - */ + * Create a link to view this revision of the page + * @param Revision $rev + * @returns string + */ function revLink( $rev ) { global $wgLang; $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true ); if( $rev->userCan( Revision::DELETED_TEXT ) ) { $link = $this->mSkin->makeKnownLinkObj( - $this->mTitle, $date, "oldid=" . $rev->getId() ); + $this->mTitle, $date, "oldid=" . $rev->getId() ); } else { $link = $date; } @@ -311,30 +385,28 @@ class PageHistory { } /** - * Create a diff-to-current link for this revision for this page - * @param Revision $rev - * @param Bool $latest, this is the latest revision of the page? - * @returns string - */ + * Create a diff-to-current link for this revision for this page + * @param Revision $rev + * @param Bool $latest, this is the latest revision of the page? + * @returns string + */ function curLink( $rev, $latest ) { $cur = $this->message['cur']; if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) { return $cur; } else { - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, $cur, - 'diff=' . $this->getLatestID() . - "&oldid=" . $rev->getId() ); + return $this->mSkin->makeKnownLinkObj( $this->mTitle, $cur, + 'diff=' . $this->mTitle->getLatestRevID() . "&oldid=" . $rev->getId() ); } } /** - * Create a diff-to-previous link for this revision for this page. - * @param Revision $prevRev, the previous revision - * @param mixed $next, the newer revision - * @param int $counter, what row on the history list this is - * @returns string - */ + * Create a diff-to-previous link for this revision for this page. + * @param Revision $prevRev, the previous revision + * @param mixed $next, the newer revision + * @param int $counter, what row on the history list this is + * @returns string + */ function lastLink( $prevRev, $next, $counter ) { $last = $this->message['last']; # $next may either be a Row, null, or "unkown" @@ -344,21 +416,13 @@ class PageHistory { return $last; } elseif( $next === 'unknown' ) { # Next row probably exists but is unknown, use an oldid=prev link - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, - $last, + return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last, "diff=" . $prevRev->getId() . "&oldid=prev" ); } elseif( !$prevRev->userCan(Revision::DELETED_TEXT) || !$nextRev->userCan(Revision::DELETED_TEXT) ) { return $last; } else { - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, - $last, - "diff=" . $prevRev->getId() . "&oldid={$next->rev_id}" - /*, - '', - '', - "tabindex={$counter}"*/ ); + return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last, + "diff=" . $prevRev->getId() . "&oldid={$next->rev_id}" ); } } @@ -382,10 +446,10 @@ class PageHistory { } /** @todo: move title texts to javascript */ - if ( $firstInList ) { + if( $firstInList ) { $first = Xml::element( 'input', array_merge( - $radio, - array( + $radio, + array( 'style' => 'visibility:hidden', 'name' => 'oldid' ) ) ); $checkmark = array( 'checked' => 'checked' ); @@ -396,34 +460,21 @@ class PageHistory { $checkmark = array(); } $first = Xml::element( 'input', array_merge( - $radio, - $checkmark, - array( 'name' => 'oldid' ) ) ); + $radio, + $checkmark, + array( 'name' => 'oldid' ) ) ); $checkmark = array(); } $second = Xml::element( 'input', array_merge( - $radio, - $checkmark, - array( 'name' => 'diff' ) ) ); + $radio, + $checkmark, + array( 'name' => 'diff' ) ) ); return $first . $second; } else { return ''; } } - /** @todo document */ - function getLatestId() { - if( is_null( $this->mLatestId ) ) { - $id = $this->mTitle->getArticleID(); - $db = wfGetDB( DB_SLAVE ); - $this->mLatestId = $db->selectField( 'page', - "page_latest", - array( 'page_id' => $id ), - __METHOD__ ); - } - return $this->mLatestId; - } - /** * Fetch an array of revisions, specified by a given limit, offset and * direction. This is now only used by the feeds. It was previously @@ -432,61 +483,25 @@ class PageHistory { function fetchRevisions($limit, $offset, $direction) { $dbr = wfGetDB( DB_SLAVE ); - if ($direction == PageHistory::DIR_PREV) + if( $direction == PageHistory::DIR_PREV ) list($dirs, $oper) = array("ASC", ">="); else /* $direction == PageHistory::DIR_NEXT */ list($dirs, $oper) = array("DESC", "<="); - if ($offset) + if( $offset ) $offsets = array("rev_timestamp $oper '$offset'"); else $offsets = array(); $page_id = $this->mTitle->getArticleID(); - $res = $dbr->select( - 'revision', + return $dbr->select( 'revision', Revision::selectFields(), array_merge(array("rev_page=$page_id"), $offsets), __METHOD__, - array('ORDER BY' => "rev_timestamp $dirs", + array( 'ORDER BY' => "rev_timestamp $dirs", 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit) - ); - - $result = array(); - while (($obj = $dbr->fetchObject($res)) != NULL) - $result[] = $obj; - - return $result; - } - - /** @todo document */ - function getNotificationTimestamp() { - global $wgUser, $wgShowUpdatedMarker; - - if ($this->mNotificationTimestamp !== NULL) - return $this->mNotificationTimestamp; - - if ($wgUser->isAnon() || !$wgShowUpdatedMarker) - return $this->mNotificationTimestamp = false; - - $dbr = wfGetDB(DB_SLAVE); - - $this->mNotificationTimestamp = $dbr->selectField( - 'watchlist', - 'wl_notificationtimestamp', - array( 'wl_namespace' => $this->mTitle->getNamespace(), - 'wl_title' => $this->mTitle->getDBkey(), - 'wl_user' => $wgUser->getId() - ), - __METHOD__ ); - - // Don't use the special value reserved for telling whether the field is filled - if ( is_null( $this->mNotificationTimestamp ) ) { - $this->mNotificationTimestamp = false; - } - - return $this->mNotificationTimestamp; + ); } /** @@ -495,15 +510,15 @@ class PageHistory { */ function feed( $type ) { global $wgFeedClasses, $wgRequest, $wgFeedLimit; - if ( !FeedUtils::checkFeedOutput($type) ) { + if( !FeedUtils::checkFeedOutput($type) ) { return; } $feed = new $wgFeedClasses[$type]( - $this->mTitle->getPrefixedText() . ' - ' . - wfMsgForContent( 'history-feed-title' ), - wfMsgForContent( 'history-feed-description' ), - $this->mTitle->getFullUrl( 'action=history' ) ); + $this->mTitle->getPrefixedText() . ' - ' . + wfMsgForContent( 'history-feed-title' ), + wfMsgForContent( 'history-feed-description' ), + $this->mTitle->getFullUrl( 'action=history' ) ); // Get a limit on number of feed entries. Provide a sane default // of 10 if none is defined (but limit to $wgFeedLimit max) @@ -511,7 +526,7 @@ class PageHistory { if( $limit > $wgFeedLimit || $limit < 1 ) { $limit = 10; } - $items = $this->fetchRevisions($limit, 0, PageHistory::DIR_NEXT); + $items = $this->fetchRevisions($limit, 0, PageHistory::DIR_NEXT); $feed->outHeader(); if( $items ) { @@ -531,7 +546,7 @@ class PageHistory { $wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ), $this->mTitle->getFullUrl(), wfTimestamp( TS_MW ), - '', + '', $this->mTitle->getTalkPage()->getFullUrl() ); } @@ -547,18 +562,18 @@ class PageHistory { $rev = new Revision( $row ); $rev->setTitle( $this->mTitle ); $text = FeedUtils::formatDiffRow( $this->mTitle, - $this->mTitle->getPreviousRevisionID( $rev->getId() ), - $rev->getId(), - $rev->getTimestamp(), - $rev->getComment() ); + $this->mTitle->getPreviousRevisionID( $rev->getId() ), + $rev->getId(), + $rev->getTimestamp(), + $rev->getComment() ); if( $rev->getComment() == '' ) { global $wgContLang; $title = wfMsgForContent( 'history-feed-item-nocomment', - $rev->getUserText(), - $wgContLang->timeanddate( $rev->getTimestamp() ) ); + $rev->getUserText(), + $wgContLang->timeanddate( $rev->getTimestamp() ) ); } else { - $title = $rev->getUserText() . ": " . $this->stripComment( $rev->getComment() ); + $title = $rev->getUserText() . ": " . FeedItem::stripComment( $rev->getComment() ); } return new FeedItem( @@ -569,13 +584,6 @@ class PageHistory { $rev->getUserText(), $this->mTitle->getTalkPage()->getFullUrl() ); } - - /** - * Quickie hack... strip out wikilinks to more legible form from the comment. - */ - function stripComment( $text ) { - return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); - } } @@ -583,11 +591,13 @@ class PageHistory { * @ingroup Pager */ class PageHistoryPager extends ReverseChronologicalPager { - public $mLastRow = false, $mPageHistory; + public $mLastRow = false, $mPageHistory, $mTitle; - function __construct( $pageHistory ) { + function __construct( $pageHistory, $year='', $month='' ) { parent::__construct(); $this->mPageHistory = $pageHistory; + $this->mTitle =& $this->mPageHistory->mTitle; + $this->getDateCond( $year, $month ); } function getQueryInfo() { @@ -606,11 +616,11 @@ class PageHistoryPager extends ReverseChronologicalPager { } function formatRow( $row ) { - if ( $this->mLastRow ) { + if( $this->mLastRow ) { $latest = $this->mCounter == 1 && $this->mIsFirst; $firstInList = $this->mCounter == 1; $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++, - $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); + $this->mTitle->getNotificationTimestamp(), $latest, $firstInList ); } else { $s = ''; } @@ -625,12 +635,12 @@ class PageHistoryPager extends ReverseChronologicalPager { } function getEndBody() { - if ( $this->mLastRow ) { + if( $this->mLastRow ) { $latest = $this->mCounter == 1 && $this->mIsFirst; $firstInList = $this->mCounter == 1; - if ( $this->mIsBackwards ) { + if( $this->mIsBackwards ) { # Next row is unknown, but for UI reasons, probably exists if an offset has been specified - if ( $this->mOffset == '' ) { + if( $this->mOffset == '' ) { $next = null; } else { $next = 'unknown'; @@ -640,7 +650,7 @@ class PageHistoryPager extends ReverseChronologicalPager { $next = $this->mPastTheEndRow; } $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++, - $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); + $this->mTitle->getNotificationTimestamp(), $latest, $firstInList ); } else { $s = ''; } diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php index 0d1789ee..a2091e8b 100644 --- a/includes/PageQueryPage.php +++ b/includes/PageQueryPage.php @@ -17,7 +17,9 @@ class PageQueryPage extends QueryPage { public function formatResult( $skin, $row ) { global $wgContLang; $title = Title::makeTitleSafe( $row->namespace, $row->title ); - return $skin->makeKnownLinkObj( $title, - htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) ); + $text = $row->title; + if ($title instanceof Title) + $text = $wgContLang->convert( $title->getPrefixedText() ); + return $skin->link( $title, htmlspecialchars($text), array(), array(), array('known', 'noclasses') ); } } diff --git a/includes/Pager.php b/includes/Pager.php index 62c4e551..8ec32ff4 100644 --- a/includes/Pager.php +++ b/includes/Pager.php @@ -154,6 +154,26 @@ abstract class IndexPager implements Pager { wfProfileOut( $fname ); } + + /** + * Return the result wrapper. + */ + function getResult() { + return $this->mResult; + } + + /** + * Set the offset from an other source than $wgRequest + */ + function setOffset( $offset ) { + $this->mOffset = $offset; + } + /** + * Set the limit from an other source than $wgRequest + */ + function setLimit( $limit ) { + $this->mLimit = $limit; + } /** * Extract some useful data from the result object for use by @@ -292,9 +312,12 @@ abstract class IndexPager implements Pager { # HTML 4 has no rel="end" . . . $attrs = ''; } + + if( $type ) { + $attrs .= " class=\"mw-{$type}link\"" ; + } return $this->getSkin()->makeKnownLinkObj( $this->getTitle(), $text, - wfArrayToCGI( $query, $this->getDefaultQuery() ), '', '', - $attrs ); + wfArrayToCGI( $query, $this->getDefaultQuery() ), '', '', $attrs ); } /** @@ -352,6 +375,8 @@ abstract class IndexPager implements Pager { unset( $this->mDefaultQuery['offset'] ); unset( $this->mDefaultQuery['limit'] ); unset( $this->mDefaultQuery['order'] ); + unset( $this->mDefaultQuery['month'] ); + unset( $this->mDefaultQuery['year'] ); } return $this->mDefaultQuery; } @@ -425,7 +450,7 @@ abstract class IndexPager implements Pager { } foreach ( $this->mLimitsShown as $limit ) { $links[] = $this->makeLink( $wgLang->formatNum( $limit ), - array( 'offset' => $offset, 'limit' => $limit ) ); + array( 'offset' => $offset, 'limit' => $limit ), 'num' ); } return $links; } @@ -564,6 +589,8 @@ abstract class AlphabeticPager extends IndexPager { */ abstract class ReverseChronologicalPager extends IndexPager { public $mDefaultDirection = true; + public $mYear; + public $mMonth; function __construct() { parent::__construct(); @@ -591,6 +618,53 @@ abstract class ReverseChronologicalPager extends IndexPager { wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); return $this->mNavigationBar; } + + function getDateCond( $year, $month ) { + $year = intval($year); + $month = intval($month); + // Basic validity checks + $this->mYear = $year > 0 ? $year : false; + $this->mMonth = ($month > 0 && $month < 13) ? $month : false; + // Given an optional year and month, we need to generate a timestamp + // to use as "WHERE rev_timestamp <= result" + // Examples: year = 2006 equals < 20070101 (+000000) + // year=2005, month=1 equals < 20050201 + // year=2005, month=12 equals < 20060101 + if ( !$this->mYear && !$this->mMonth ) { + return; + } + if ( $this->mYear ) { + $year = $this->mYear; + } else { + // If no year given, assume the current one + $year = gmdate( 'Y' ); + // If this month hasn't happened yet this year, go back to last year's month + if( $this->mMonth > gmdate( 'n' ) ) { + $year--; + } + } + if ( $this->mMonth ) { + $month = $this->mMonth + 1; + // For December, we want January 1 of the next year + if ($month > 12) { + $month = 1; + $year++; + } + } else { + // No month implies we want up to the end of the year in question + $month = 1; + $year++; + } + // Y2K38 bug + if ( $year > 2032 ) { + $year = 2032; + } + $ymd = (int)sprintf( "%04d%02d01", $year, $month ); + if ( $ymd > 20320101 ) { + $ymd = 20320101; + } + $this->mOffset = $this->mDb->timestamp( "${ymd}000000" ); + } } /** @@ -795,7 +869,7 @@ abstract class TablePager extends IndexPager { "<form method=\"get\" action=\"$url\">" . wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) . "\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" . - $this->getHiddenFields( 'limit' ) . + $this->getHiddenFields( array('limit','title') ) . "</form>\n"; } diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php index a3ff05e2..af569112 100644 --- a/includes/PrefixSearch.php +++ b/includes/PrefixSearch.php @@ -1,5 +1,12 @@ <?php +/** + * PrefixSearch - Handles searching prefixes of titles and finding any page + * names that match. Used largely by the OpenSearch implementation. + * + * @ingroup Search + */ + class PrefixSearch { /** * Do a prefix search of titles and return a list of matching page names. @@ -48,7 +55,7 @@ class PrefixSearch { if( count($namespaces) == 1 ){ $ns = $namespaces[0]; if( $ns == NS_MEDIA ) { - $namespaces = array(NS_IMAGE); + $namespaces = array(NS_FILE); } elseif( $ns == NS_SPECIAL ) { return self::specialSearch( $search, $limit ); } @@ -96,7 +103,9 @@ class PrefixSearch { /** * Unless overridden by PrefixSearchBackend hook... - * This is case-sensitive except the first letter (per $wgCapitalLinks) + * This is case-sensitive (First character may + * be automatically capitalized by Title::secureAndSpit() + * later on depending on $wgCapitalLinks) * * @param array $namespaces Namespaces to search in * @param string $search term @@ -104,12 +113,6 @@ class PrefixSearch { * @return array of title strings */ protected static function defaultSearchBackend( $namespaces, $search, $limit ) { - global $wgCapitalLinks, $wgContLang; - - if( $wgCapitalLinks ) { - $search = $wgContLang->ucfirst( $search ); - } - $ns = array_shift($namespaces); // support only one namespace if( in_array(NS_MAIN,$namespaces)) $ns = NS_MAIN; // if searching on many always default to main diff --git a/includes/Profiler.php b/includes/Profiler.php index cef89dd3..ffb48978 100644 --- a/includes/Profiler.php +++ b/includes/Profiler.php @@ -355,8 +355,7 @@ class Profiler { # Do not log anything if database is readonly (bug 5375) if( wfReadOnly() ) { return; } - # Warning: $wguname is a live patch, it should be moved to Setup.php - global $wguname, $wgProfilePerHost; + global $wgProfilePerHost; $dbw = wfGetDB( DB_MASTER ); if( !is_object( $dbw ) ) @@ -366,7 +365,7 @@ class Profiler { $name = substr($name, 0, 255); if( $wgProfilePerHost ){ - $pfhost = $wguname['nodename']; + $pfhost = wfHostname(); } else { $pfhost = ''; } diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index e7787822..372edfcd 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -20,79 +20,146 @@ */ /** - * @todo document, briefly. + * Handles the page protection UI and backend */ class ProtectionForm { + /** A map of action to restriction level, from request or default */ var $mRestrictions = array(); + + /** The custom/additional protection reason */ var $mReason = ''; + + /** The reason selected from the list, blank for other/additional */ + var $mReasonSelection = ''; + + /** True if the restrictions are cascading, from request or existing protection */ var $mCascade = false; - var $mExpiry = null; + + /** Map of action to "other" expiry time. Used in preference to mExpirySelection. */ + var $mExpiry = array(); + + /** + * Map of action to value selected in expiry drop-down list. + * Will be set to 'othertime' whenever mExpiry is set. + */ + var $mExpirySelection = array(); + + /** Permissions errors for the protect action */ var $mPermErrors = array(); + + /** Types (i.e. actions) for which levels can be selected */ var $mApplicableTypes = array(); - function __construct( &$article ) { + /** Map of action to the expiry time of the existing protection */ + var $mExistingExpiry = array(); + + function __construct( Article $article ) { global $wgRequest, $wgUser; global $wgRestrictionTypes, $wgRestrictionLevels; - $this->mArticle =& $article; - $this->mTitle =& $article->mTitle; + $this->mArticle = $article; + $this->mTitle = $article->mTitle; $this->mApplicableTypes = $this->mTitle->exists() ? $wgRestrictionTypes : array('create'); - if( $this->mTitle ) { - $this->mTitle->loadRestrictions(); + $this->mCascade = $this->mTitle->areRestrictionsCascading(); - foreach( $this->mApplicableTypes as $action ) { - // Fixme: this form currently requires individual selections, - // but the db allows multiples separated by commas. - $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); - } + // The form will be available in read-only to show levels. + $this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser); + $this->disabled = wfReadOnly() || $this->mPermErrors != array(); + $this->disabledAttrib = $this->disabled + ? array( 'disabled' => 'disabled' ) + : array(); + + $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); + $this->mReasonSelection = $wgRequest->getText( 'wpProtectReasonSelection' ); + $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade', $this->mCascade ); - $this->mCascade = $this->mTitle->areRestrictionsCascading(); + foreach( $this->mApplicableTypes as $action ) { + // Fixme: this form currently requires individual selections, + // but the db allows multiples separated by commas. + $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); - if ( $this->mTitle->mRestrictionsExpiry == 'infinity' ) { - $this->mExpiry = 'infinite'; - } else if ( strlen($this->mTitle->mRestrictionsExpiry) == 0 ) { - $this->mExpiry = ''; + if ( !$this->mRestrictions[$action] ) { + // No existing expiry + $existingExpiry = ''; } else { - // FIXME: this format is not user friendly - $this->mExpiry = wfTimestamp( TS_ISO_8601, $this->mTitle->mRestrictionsExpiry ); + $existingExpiry = $this->mTitle->getRestrictionExpiry( $action ); + } + $this->mExistingExpiry[$action] = $existingExpiry; + + $requestExpiry = $wgRequest->getText( "mwProtect-expiry-$action" ); + $requestExpirySelection = $wgRequest->getVal( "wpProtectExpirySelection-$action" ); + + if ( $requestExpiry ) { + // Custom expiry takes precedence + $this->mExpiry[$action] = $requestExpiry; + $this->mExpirySelection[$action] = 'othertime'; + } elseif ( $requestExpirySelection ) { + // Expiry selected from list + $this->mExpiry[$action] = ''; + $this->mExpirySelection[$action] = $requestExpirySelection; + } elseif ( $existingExpiry == 'infinity' ) { + // Existing expiry is infinite, use "infinite" in drop-down + $this->mExpiry[$action] = ''; + $this->mExpirySelection[$action] = 'infinite'; + } elseif ( $existingExpiry ) { + // Use existing expiry in its own list item + $this->mExpiry[$action] = ''; + $this->mExpirySelection[$action] = $existingExpiry; + } else { + // Final default: infinite + $this->mExpiry[$action] = ''; + $this->mExpirySelection[$action] = 'infinite'; + } + + $val = $wgRequest->getVal( "mwProtect-level-$action" ); + if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) { + // Prevent users from setting levels that they cannot later unset + if( $val == 'sysop' ) { + // Special case, rewrite sysop to either protect and editprotected + if( !$wgUser->isAllowed('protect') && !$wgUser->isAllowed('editprotected') ) + continue; + } else { + if( !$wgUser->isAllowed($val) ) + continue; + } + $this->mRestrictions[$action] = $val; } } + } - // The form will be available in read-only to show levels. - $this->disabled = wfReadOnly() || ($this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser)) != array(); - $this->disabledAttrib = $this->disabled - ? array( 'disabled' => 'disabled' ) - : array(); + /** + * Get the expiry time for a given action, by combining the relevant inputs. + * Returns a 14-char timestamp or "infinity", or false if the input was invalid + */ + function getExpiry( $action ) { + if ( $this->mExpirySelection[$action] == 'existing' ) { + return $this->mExistingExpiry[$action]; + } elseif ( $this->mExpirySelection[$action] == 'othertime' ) { + $value = $this->mExpiry[$action]; + } else { + $value = $this->mExpirySelection[$action]; + } + if ( $value == 'infinite' || $value == 'indefinite' || $value == 'infinity' ) { + $time = Block::infinity(); + } else { + $unix = strtotime( $value ); - if( $wgRequest->wasPosted() ) { - $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); - $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' ); - $this->mExpiry = $wgRequest->getText( 'mwProtect-expiry' ); - - foreach( $this->mApplicableTypes as $action ) { - $val = $wgRequest->getVal( "mwProtect-level-$action" ); - if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) { - //prevent users from setting levels that they cannot later unset - if( $val == 'sysop' ) { - //special case, rewrite sysop to either protect and editprotected - if( !$wgUser->isAllowed('protect') && !$wgUser->isAllowed('editprotected') ) - continue; - } else { - if( !$wgUser->isAllowed($val) ) - continue; - } - $this->mRestrictions[$action] = $val; - } + if ( !$unix || $unix === -1 ) { + return false; } + + // Fixme: non-qualified absolute times are not in users specified timezone + // and there isn't notice about it in the ui + $time = wfTimestamp( TS_MW, $unix ); } + return $time; } function execute() { global $wgRequest, $wgOut; if( $wgRequest->wasPosted() ) { if( $this->save() ) { - $article = new Article( $this->mTitle ); - $q = $article->isRedirect() ? 'redirect=no' : ''; + $q = $this->mArticle->isRedirect() ? 'redirect=no' : ''; $wgOut->redirect( $this->mTitle->getFullUrl( $q ) ); } } else { @@ -103,7 +170,7 @@ class ProtectionForm { function show( $err = null ) { global $wgOut, $wgUser; - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); if( is_null( $this->mTitle ) || $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { @@ -152,42 +219,39 @@ class ProtectionForm { function save() { global $wgRequest, $wgUser, $wgOut; - - if( $this->disabled ) { + # Permission check! + if ( $this->disabled ) { $this->show(); return false; } $token = $wgRequest->getVal( 'wpEditToken' ); - if( !$wgUser->matchEditToken( $token ) ) { + if ( !$wgUser->matchEditToken( $token ) ) { $this->show( wfMsg( 'sessionfailure' ) ); return false; } - - if ( strlen( $this->mExpiry ) == 0 ) { - $this->mExpiry = 'infinite'; + + # Create reason string. Use list and/or custom string. + $reasonstr = $this->mReasonSelection; + if ( $reasonstr != 'other' && $this->mReason != '' ) { + // Entry from drop down menu + additional comment + $reasonstr .= ': ' . $this->mReason; + } elseif ( $reasonstr == 'other' ) { + $reasonstr = $this->mReason; } - - if ( $this->mExpiry == 'infinite' || $this->mExpiry == 'indefinite' ) { - $expiry = Block::infinity(); - } else { - # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1 - $expiry = strtotime( $this->mExpiry ); - - if ( $expiry < 0 || $expiry === false ) { + $expiry = array(); + foreach( $this->mApplicableTypes as $action ) { + $expiry[$action] = $this->getExpiry( $action ); + if( empty($this->mRestrictions[$action]) ) + continue; // unprotected + if ( !$expiry[$action] ) { $this->show( wfMsg( 'protect_expiry_invalid' ) ); return false; } - - // Fixme: non-qualified absolute times are not in users specified timezone - // and there isn't notice about it in the ui - $expiry = wfTimestamp( TS_MW, $expiry ); - - if ( $expiry < wfTimestampNow() ) { + if ( $expiry[$action] < wfTimestampNow() ) { $this->show( wfMsg( 'protect_expiry_old' ) ); return false; } - } # They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied @@ -195,15 +259,15 @@ class ProtectionForm { global $wgGroupPermissions; $edit_restriction = $this->mRestrictions['edit']; - + $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' ); if ($this->mCascade && ($edit_restriction != 'protect') && !(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) ) $this->mCascade = false; if ($this->mTitle->exists()) { - $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade, $expiry ); + $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $reasonstr, $this->mCascade, $expiry ); } else { - $ok = $this->mTitle->updateTitleProtection( $this->mRestrictions['create'], $this->mReason, $expiry ); + $ok = $this->mTitle->updateTitleProtection( $this->mRestrictions['create'], $reasonstr, $expiry['create'] ); } if( !$ok ) { @@ -215,7 +279,6 @@ class ProtectionForm { } elseif( $this->mTitle->userIsWatching() ) { $this->mArticle->doUnwatch(); } - return $ok; } @@ -225,73 +288,143 @@ class ProtectionForm { * @return $out string HTML form */ function buildForm() { - global $wgUser; + global $wgUser, $wgLang; + + $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonSelection' ); + $mProtectreason = Xml::label( wfMsg( 'protect-otherreason' ), 'mwProtect-reason' ); $out = ''; if( !$this->disabled ) { $out .= $this->buildScript(); - // The submission needs to reenable the move permission selector - // if it's in locked mode, or some browsers won't submit the data. - $out .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalUrl( 'action=protect' ), 'id' => 'mw-Protect-Form', 'onsubmit' => 'protectEnable(true)' ) ) . - Xml::hidden( 'wpEditToken',$wgUser->editToken() ); + $out .= Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $this->mTitle->getLocalUrl( 'action=protect' ), + 'id' => 'mw-Protect-Form', 'onsubmit' => 'ProtectionForm.enableUnchainedInputs(true)' ) ); + $out .= Xml::hidden( 'wpEditToken',$wgUser->editToken() ); } $out .= Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'protect-legend' ) ) . Xml::openElement( 'table', array( 'id' => 'mwProtectSet' ) ) . - Xml::openElement( 'tbody' ) . - "<tr>\n"; + Xml::openElement( 'tbody' ); - foreach( $this->mRestrictions as $action => $required ) { - /* Not all languages have V_x <-> N_x relation */ - $label = Xml::element( 'label', - array( 'for' => "mwProtect-level-$action" ), - wfMsg( 'restriction-' . $action ) ); - $out .= "<th>$label</th>"; - } - $out .= "</tr> - <tr>\n"; foreach( $this->mRestrictions as $action => $selected ) { - $out .= "<td>" . - $this->buildSelector( $action, $selected ) . - "</td>"; + /* Not all languages have V_x <-> N_x relation */ + $msg = wfMsg( 'restriction-' . $action ); + if( wfEmptyMsg( 'restriction-' . $action, $msg ) ) { + $msg = $action; + } + $out .= "<tr><td>". + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, $msg ) . + Xml::openElement( 'table', array( 'id' => "mw-protect-table-$action" ) ) . + "<tr><td>" . $this->buildSelector( $action, $selected ) . "</td></tr><tr><td>"; + + $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection', + wfMsgForContent( 'protect-dropdown' ), + wfMsgForContent( 'protect-otherreason-op' ), + $this->mReasonSelection, + 'mwProtect-reason', 4 ); + $scExpiryOptions = wfMsgForContent( 'protect-expiry-options' ); + + $showProtectOptions = ($scExpiryOptions !== '-' && !$this->disabled); + + $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpirySelection-$action" ); + $mProtectother = Xml::label( wfMsg( 'protect-othertime' ), "mwProtect-$action-expires" ); + + $expiryFormOptions = ''; + if ( $this->mExistingExpiry[$action] && $this->mExistingExpiry[$action] != 'infinity' ) { + $timestamp = $wgLang->timeanddate( $this->mExistingExpiry[$action] ); + $d = $wgLang->date( $this->mExistingExpiry[$action] ); + $t = $wgLang->time( $this->mExistingExpiry[$action] ); + $expiryFormOptions .= + Xml::option( + wfMsg( 'protect-existing-expiry', $timestamp, $d, $t ), + 'existing', + $this->mExpirySelection[$action] == 'existing' + ) . "\n"; + } + + $expiryFormOptions .= Xml::option( wfMsg( 'protect-othertime-op' ), "othertime" ) . "\n"; + foreach( explode(',', $scExpiryOptions) as $option ) { + if ( strpos($option, ":") === false ) { + $show = $value = $option; + } else { + list($show, $value) = explode(":", $option); + } + $show = htmlspecialchars($show); + $value = htmlspecialchars($value); + $expiryFormOptions .= Xml::option( $show, $value, $this->mExpirySelection[$action] === $value ) . "\n"; + } + # Add expiry dropdown + if( $showProtectOptions && !$this->disabled ) { + $out .= " + <table><tr> + <td class='mw-label'> + {$mProtectexpiry} + </td> + <td class='mw-input'>" . + Xml::tags( 'select', + array( + 'id' => "mwProtectExpirySelection-$action", + 'name' => "wpProtectExpirySelection-$action", + 'onchange' => "ProtectionForm.updateExpiryList(this)", + 'tabindex' => '2' ) + $this->disabledAttrib, + $expiryFormOptions ) . + "</td> + </tr></table>"; + } + # Add custom expiry field + $attribs = array( 'id' => "mwProtect-$action-expires", 'onkeyup' => 'ProtectionForm.updateExpiry(this)' ) + $this->disabledAttrib; + $out .= "<table><tr> + <td class='mw-label'>" . + $mProtectother . + '</td> + <td class="mw-input">' . + Xml::input( "mwProtect-expiry-$action", 50, $this->mExpiry[$action], $attribs ) . + '</td> + </tr></table>'; + $out .= "</td></tr>" . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + "</td></tr>"; } - $out .= "</tr>\n"; - - // JavaScript will add another row with a value-chaining checkbox - $out .= Xml::closeElement( 'tbody' ) . - Xml::closeElement( 'table' ) . - Xml::openElement( 'table', array( 'id' => 'mw-protect-table2' ) ) . - Xml::openElement( 'tbody' ); + $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); + // JavaScript will add another row with a value-chaining checkbox if( $this->mTitle->exists() ) { + $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table2' ) ) . + Xml::openElement( 'tbody' ); $out .= '<tr> <td></td> <td class="mw-input">' . - Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade', $this->mCascade, $this->disabledAttrib ) . + Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade', + $this->mCascade, $this->disabledAttrib ) . "</td> </tr>\n"; + $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); } - - $attribs = array( 'id' => 'expires' ) + $this->disabledAttrib; - $out .= "<tr> - <td class='mw-label'>" . - Xml::label( wfMsgExt( 'protectexpiry', array( 'parseinline' ) ), 'expires' ) . - '</td> - <td class="mw-input">' . - Xml::input( 'mwProtect-expiry', 60, $this->mExpiry, $attribs ) . - '</td> - </tr>'; - + + # Add manual and custom reason field/selects as well as submit if( !$this->disabled ) { - $id = 'mwProtect-reason'; - $out .= "<tr> - <td class='mw-label'>" . - Xml::label( wfMsg( 'protectcomment' ), $id ) . - '</td> - <td class="mw-input">' . - Xml::input( $id, 60, $this->mReason, array( 'type' => 'text', 'id' => $id, 'maxlength' => 255 ) ) . + $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table3' ) ) . + Xml::openElement( 'tbody' ); + $out .= " + <tr> + <td class='mw-label'> + {$mProtectreasonother} + </td> + <td class='mw-input'> + {$reasonDropDown} + </td> + </tr> + <tr> + <td class='mw-label'> + {$mProtectreason} + </td> + <td class='mw-input'>" . + Xml::input( 'mwProtect-reason', 60, $this->mReason, array( 'type' => 'text', + 'id' => 'mwProtect-reason', 'maxlength' => 255 ) ) . "</td> </tr> <tr> @@ -308,11 +441,15 @@ class ProtectionForm { Xml::submitButton( wfMsg( 'confirm' ), array( 'id' => 'mw-Protect-submit' ) ) . "</td> </tr>\n"; + $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); } + $out .= Xml::closeElement( 'fieldset' ); - $out .= Xml::closeElement( 'tbody' ) . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ); + if ( $wgUser->isAllowed( 'editinterface' ) ) { + $linkTitle = Title::makeTitleSafe( NS_MEDIAWIKI, 'protect-dropdown' ); + $link = $wgUser->getSkin()->Link ( $linkTitle, wfMsgHtml( 'protect-edit-reasonlist' ) ); + $out .= '<p class="mw-protect-editreasons">' . $link . '</p>'; + } if ( !$this->disabled ) { $out .= Xml::closeElement( 'form' ) . @@ -324,15 +461,8 @@ class ProtectionForm { function buildSelector( $action, $selected ) { global $wgRestrictionLevels, $wgUser; - $id = 'mwProtect-level-' . $action; - $attribs = array( - 'id' => $id, - 'name' => $id, - 'size' => count( $wgRestrictionLevels ), - 'onchange' => 'protectLevelsUpdate(this)', - ) + $this->disabledAttrib; - $out = Xml::openElement( 'select', $attribs ); + $levels = array(); foreach( $wgRestrictionLevels as $key ) { //don't let them choose levels above their own (aka so they can still unprotect and edit the page). but only when the form isn't disabled if( $key == 'sysop' ) { @@ -343,6 +473,19 @@ class ProtectionForm { if( !$wgUser->isAllowed($key) && !$this->disabled ) continue; } + $levels[] = $key; + } + + $id = 'mwProtect-level-' . $action; + $attribs = array( + 'id' => $id, + 'name' => $id, + 'size' => count( $levels ), + 'onchange' => 'ProtectionForm.updateLevels(this)', + ) + $this->disabledAttrib; + + $out = Xml::openElement( 'select', $attribs ); + foreach( $levels as $key ) { $out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected ); } $out .= Xml::closeElement( 'select' ); @@ -371,7 +514,7 @@ class ProtectionForm { global $wgStylePath, $wgStyleVersion; return Xml::tags( 'script', array( 'type' => 'text/javascript', - 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion" ), '' ); + 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion.1" ), '' ); } function buildCleanupScript() { @@ -384,7 +527,15 @@ class ProtectionForm { } } $script .= "[" . implode(',',$CascadeableLevels) . "];\n"; - $script .= 'protectInitialize("mwProtectSet","' . Xml::escapeJsString( wfMsg( 'protect-unchain' ) ) . '","' . count($this->mApplicableTypes) . '")'; + $options = (object)array( + 'tableId' => 'mw-protect-table-move', + 'labelText' => wfMsg( 'protect-unchain' ), + 'numTypes' => count($this->mApplicableTypes), + 'existingMatch' => 1 == count( array_unique( $this->mExistingExpiry ) ), + ); + $encOptions = Xml::encodeJsVar( $options ); + + $script .= "ProtectionForm.init($encOptions)"; return Xml::tags( 'script', array( 'type' => 'text/javascript' ), $script ); } diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php index 0f010421..771fd577 100644 --- a/includes/ProxyTools.php +++ b/includes/ProxyTools.php @@ -67,7 +67,7 @@ function wfGetAgent() { * @return string */ function wfGetIP() { - global $wgIP; + global $wgIP, $wgUsePrivateIPs; # Return cached result if ( !empty( $wgIP ) ) { @@ -97,8 +97,10 @@ function wfGetIP() { foreach ( $ipchain as $i => $curIP ) { $curIP = IP::canonicalize( $curIP ); if ( wfIsTrustedProxy( $curIP ) ) { - if ( isset( $ipchain[$i + 1] ) && IP::isPublic( $ipchain[$i + 1] ) ) { - $ip = $ipchain[$i + 1]; + if ( isset( $ipchain[$i + 1] ) ) { + if( $wgUsePrivateIPs || IP::isPublic( $ipchain[$i + 1 ] ) ) { + $ip = $ipchain[$i + 1]; + } } } else { break; @@ -121,8 +123,7 @@ function wfIsTrustedProxy( $ip ) { global $wgSquidServers, $wgSquidServersNoPurge; if ( in_array( $ip, $wgSquidServers ) || - in_array( $ip, $wgSquidServersNoPurge ) || - wfIsAOLProxy( $ip ) + in_array( $ip, $wgSquidServersNoPurge ) ) { $trusted = true; } else { @@ -212,50 +213,3 @@ function wfIsLocallyBlockedProxy( $ip ) { return $ret; } -/** - * TODO: move this list to the database in a global IP info table incorporating - * trusted ISP proxies, blocked IP addresses and open proxies. - * @return bool - */ -function wfIsAOLProxy( $ip ) { - # From http://webmaster.info.aol.com/proxyinfo.html - $ranges = array( - '64.12.96.0/19', - '149.174.160.0/20', - '152.163.240.0/21', - '152.163.248.0/22', - '152.163.252.0/23', - '152.163.96.0/22', - '152.163.100.0/23', - '195.93.32.0/22', - '195.93.48.0/22', - '195.93.64.0/19', - '195.93.96.0/19', - '195.93.16.0/20', - '198.81.0.0/22', - '198.81.16.0/20', - '198.81.8.0/23', - '202.67.64.128/25', - '205.188.192.0/20', - '205.188.208.0/23', - '205.188.112.0/20', - '205.188.146.144/30', - '207.200.112.0/21', - ); - - static $parsedRanges; - if ( is_null( $parsedRanges ) ) { - $parsedRanges = array(); - foreach ( $ranges as $range ) { - $parsedRanges[] = IP::parseRange( $range ); - } - } - - $hex = IP::toHex( $ip ); - foreach ( $parsedRanges as $range ) { - if ( $hex >= $range[0] && $hex <= $range[1] ) { - return true; - } - } - return false; -} diff --git a/includes/QueryPage.php b/includes/QueryPage.php index 16dc7c04..0b587508 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -21,6 +21,7 @@ $wgQueryPages = array( array( 'DeadendPagesPage', 'Deadendpages' ), array( 'DisambiguationsPage', 'Disambiguations' ), array( 'DoubleRedirectsPage', 'DoubleRedirects' ), + array( 'LinkSearchPage', 'LinkSearch' ), array( 'ListredirectsPage', 'Listredirects' ), array( 'LonelyPagesPage', 'Lonelypages' ), array( 'LongPagesPage', 'Longpages' ), @@ -39,7 +40,9 @@ $wgQueryPages = array( array( 'UnusedCategoriesPage', 'Unusedcategories' ), array( 'UnusedimagesPage', 'Unusedimages' ), array( 'WantedCategoriesPage', 'Wantedcategories' ), + array( 'WantedFilesPage', 'Wantedfiles' ), array( 'WantedPagesPage', 'Wantedpages' ), + array( 'WantedTemplatesPage', 'Wantedtemplates' ), array( 'UnwatchedPagesPage', 'Unwatchedpages' ), array( 'UnusedtemplatesPage', 'Unusedtemplates' ), array( 'WithoutInterwikiPage', 'Withoutinterwiki' ), @@ -334,22 +337,22 @@ class QueryPage { $this->preprocessResults( $dbr, $res ); - $wgOut->addHtml( XML::openElement( 'div', array('class' => 'mw-spcontent') ) ); + $wgOut->addHTML( XML::openElement( 'div', array('class' => 'mw-spcontent') ) ); # Top header and navigation if( $shownavigation ) { - $wgOut->addHtml( $this->getPageHeader() ); + $wgOut->addHTML( $this->getPageHeader() ); if( $num > 0 ) { - $wgOut->addHtml( '<p>' . wfShowingResults( $offset, $num ) . '</p>' ); + $wgOut->addHTML( '<p>' . wfShowingResults( $offset, $num ) . '</p>' ); # Disable the "next" link when we reach the end $paging = wfViewPrevNext( $offset, $limit, $wgContLang->specialPage( $sname ), wfArrayToCGI( $this->linkParameters() ), ( $num < $limit ) ); - $wgOut->addHtml( '<p>' . $paging . '</p>' ); + $wgOut->addHTML( '<p>' . $paging . '</p>' ); } else { # No results to show, so don't bother with "showing X of Y" etc. # -- just let the user know and give up now - $wgOut->addHtml( '<p>' . wfMsgHtml( 'specialpage-empty' ) . '</p>' ); - $wgOut->addHtml( XML::closeElement( 'div' ) ); + $wgOut->addHTML( '<p>' . wfMsgHtml( 'specialpage-empty' ) . '</p>' ); + $wgOut->addHTML( XML::closeElement( 'div' ) ); return; } } @@ -366,10 +369,10 @@ class QueryPage { # Repeat the paging links at the bottom if( $shownavigation ) { - $wgOut->addHtml( '<p>' . $paging . '</p>' ); + $wgOut->addHTML( '<p>' . $paging . '</p>' ); } - $wgOut->addHtml( XML::closeElement( 'div' ) ); + $wgOut->addHTML( XML::closeElement( 'div' ) ); return $num; } @@ -428,7 +431,7 @@ class QueryPage { ? $wgContLang->listToText( $html ) : implode( '', $html ); - $out->addHtml( $html ); + $out->addHTML( $html ); } } @@ -531,7 +534,7 @@ class QueryPage { } function feedDesc() { - return wfMsg( 'tagline' ); + return wfMsgExt( 'tagline', 'parsemag' ); } function feedUrl() { diff --git a/includes/RawPage.php b/includes/RawPage.php index b1e2539a..7093367f 100644 --- a/includes/RawPage.php +++ b/includes/RawPage.php @@ -21,20 +21,20 @@ class RawPage { var $mContentType, $mExpandTemplates; function __construct( &$article, $request = false ) { - global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType, $wgForcedRawSMaxage, $wgGroupPermissions; + global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType, $wgGroupPermissions; $allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit'); $this->mArticle =& $article; $this->mTitle =& $article->mTitle; - if ( $request === false ) { + if( $request === false ) { $this->mRequest =& $wgRequest; } else { $this->mRequest = $request; } $ctype = $this->mRequest->getVal( 'ctype' ); - $smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage ); + $smaxage = $this->mRequest->getIntOrNull( 'smaxage' ); $maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage ); $this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand'; @@ -44,17 +44,17 @@ class RawPage { $oldid = $this->mRequest->getInt( 'oldid' ); - switch ( $wgRequest->getText( 'direction' ) ) { + switch( $wgRequest->getText( 'direction' ) ) { case 'next': # output next revision, or nothing if there isn't one - if ( $oldid ) { + if( $oldid ) { $oldid = $this->mTitle->getNextRevisionId( $oldid ); } $oldid = $oldid ? $oldid : -1; break; case 'prev': # output previous revision, or nothing if there isn't one - if ( ! $oldid ) { + if( ! $oldid ) { # get the current revision so we can get the penultimate one $this->mArticle->getTouched(); $oldid = $this->mArticle->mLatest; @@ -71,11 +71,11 @@ class RawPage { # special case for 'generated' raw things: user css/js $gen = $this->mRequest->getVal( 'gen' ); - if($gen == 'css') { + if( $gen == 'css' ) { $this->mGen = $gen; if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage; if($ctype == '') $ctype = 'text/css'; - } elseif ($gen == 'js') { + } elseif( $gen == 'js' ) { $this->mGen = $gen; if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage; if($ctype == '') $ctype = $wgJsMimeType; @@ -85,7 +85,8 @@ class RawPage { $this->mCharset = $wgInputEncoding; # Force caching for CSS and JS raw content, default: 5 minutes - if (is_null($smaxage) and ($ctype=='text/css' or $ctype==$wgJsMimeType)) { + if( is_null($smaxage) and ($ctype=='text/css' or $ctype==$wgJsMimeType) ) { + global $wgForcedRawSMaxage; $this->mSmaxage = intval($wgForcedRawSMaxage); } else { $this->mSmaxage = intval( $smaxage ); @@ -94,14 +95,13 @@ class RawPage { # Output may contain user-specific data; # vary generated content for open sessions and private wikis - if ($this->mGen or !$wgGroupPermissions['*']['read']) { - $this->mPrivateCache = ( $this->mSmaxage == 0 ) || - ( session_id() != '' ); + if( $this->mGen or !$wgGroupPermissions['*']['read'] ) { + $this->mPrivateCache = $this->mSmaxage == 0 || session_id() != ''; } else { $this->mPrivateCache = false; } - if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { + if( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { $this->mContentType = 'text/x-wiki'; } else { $this->mContentType = $ctype; @@ -149,6 +149,18 @@ class RawPage { # allow the client to cache this for 24 hours $mode = $this->mPrivateCache ? 'private' : 'public'; header( 'Cache-Control: '.$mode.', s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage ); + + if( HTMLFileCache::useFileCache() ) { + $cache = new HTMLFileCache( $this->mTitle, 'raw' ); + if( $cache->isFileCacheGood( /* Assume up to date */ ) ) { + $cache->loadFromFileCache(); + $wgOut->disable(); + return; + } else { + ob_start( array(&$cache, 'saveToFileCache' ) ); + } + } + $text = $this->getRawText(); if( !wfRunHooks( 'RawPageViewBeforeOutput', array( &$this, &$text ) ) ) { @@ -161,13 +173,15 @@ class RawPage { function getRawText() { global $wgUser, $wgOut, $wgRequest; - if($this->mGen) { + if( $this->mGen ) { $sk = $wgUser->getSkin(); - $sk->initPage($wgOut); - if($this->mGen == 'css') { - return $sk->getUserStylesheet(); - } else if($this->mGen == 'js') { - return $sk->getUserJs(); + if( !StubObject::isRealObject( $wgOut ) ) + $wgOut->_unstub( 2 ); + $sk->initPage( $wgOut ); + if( $this->mGen == 'css' ) { + return $sk->generateUserStylesheet(); + } else if( $this->mGen == 'js' ) { + return $sk->generateUserJs(); } } else { return $this->getArticleText(); @@ -179,7 +193,7 @@ class RawPage { $text = ''; if( $this->mTitle ) { // If it's a MediaWiki message we can just hit the message cache - if ( $this->mUseMessageCache && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if( $this->mUseMessageCache && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $key = $this->mTitle->getDBkey(); $text = wfMsgForContentNoTrans( $key ); # If the message doesn't exist, return a blank @@ -189,11 +203,11 @@ class RawPage { } else { // Get it from the DB $rev = Revision::newFromTitle( $this->mTitle, $this->mOldId ); - if ( $rev ) { + if( $rev ) { $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() ); header( "Last-modified: $lastmod" ); - if ( !is_null($this->mSection ) ) { + if( !is_null($this->mSection ) ) { global $wgParser; $text = $wgParser->getSection ( $rev->getText(), $this->mSection ); } else @@ -230,10 +244,10 @@ class RawPage { } function parseArticleText( $text ) { - if ( $text === '' ) + if( $text === '' ) return ''; else - if ( $this->mExpandTemplates ) { + if( $this->mExpandTemplates ) { global $wgParser; return $wgParser->preprocess( $text, $this->mTitle, new ParserOptions() ); } else diff --git a/includes/RecentChange.php b/includes/RecentChange.php index 4daf6f87..f03fbcbb 100644 --- a/includes/RecentChange.php +++ b/includes/RecentChange.php @@ -49,15 +49,13 @@ class RecentChange # Factory methods - public static function newFromRow( $row ) - { + public static function newFromRow( $row ) { $rc = new RecentChange; $rc->loadFromRow( $row ); return $rc; } - public static function newFromCurRow( $row ) - { + public static function newFromCurRow( $row ) { $rc = new RecentChange; $rc->loadFromCurRow( $row ); $rc->notificationtimestamp = false; @@ -110,27 +108,23 @@ class RecentChange # Accessors - function setAttribs( $attribs ) - { + public function setAttribs( $attribs ) { $this->mAttribs = $attribs; } - function setExtra( $extra ) - { + public function setExtra( $extra ) { $this->mExtra = $extra; } - function &getTitle() - { - if ( $this->mTitle === false ) { + public function &getTitle() { + if( $this->mTitle === false ) { $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); } return $this->mTitle; } - function getMovedToTitle() - { - if ( $this->mMovedToTitle === false ) { + public function getMovedToTitle() { + if( $this->mMovedToTitle === false ) { $this->mMovedToTitle = Title::makeTitle( $this->mAttribs['rc_moved_to_ns'], $this->mAttribs['rc_moved_to_title'] ); } @@ -138,24 +132,22 @@ class RecentChange } # Writes the data in this object to the database - function save() - { - global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, - $wgRC2UDPPort, $wgRC2UDPPrefix, $wgRC2UDPOmitBots; + public function save() { + global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPOmitBots; $fname = 'RecentChange::save'; $dbw = wfGetDB( DB_MASTER ); - if ( !is_array($this->mExtra) ) { + if( !is_array($this->mExtra) ) { $this->mExtra = array(); } $this->mExtra['lang'] = $wgLocalInterwiki; - if ( !$wgPutIPinRC ) { + if( !$wgPutIPinRC ) { $this->mAttribs['rc_ip'] = ''; } - ## If our database is strict about IP addresses, use NULL instead of an empty string - if ( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) { + # If our database is strict about IP addresses, use NULL instead of an empty string + if( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) { unset( $this->mAttribs['rc_ip'] ); } @@ -165,7 +157,7 @@ class RecentChange $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'rc_rc_id_seq' ); ## If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL - if ( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) { + if( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) { unset ( $this->mAttribs['rc_cur_id'] ); } @@ -175,48 +167,9 @@ class RecentChange # Set the ID $this->mAttribs['rc_id'] = $dbw->insertId(); - # Update old rows, if necessary - if ( $this->mAttribs['rc_type'] == RC_EDIT ) { - $lastTime = $this->mExtra['lastTimestamp']; - #$now = $this->mAttribs['rc_timestamp']; - #$curId = $this->mAttribs['rc_cur_id']; - - # Don't bother looking for entries that have probably - # been purged, it just locks up the indexes needlessly. - global $wgRCMaxAge; - $age = time() - wfTimestamp( TS_UNIX, $lastTime ); - if( $age < $wgRCMaxAge ) { - # live hack, will commit once tested - kate - # Update rc_this_oldid for the entries which were current - # - #$oldid = $this->mAttribs['rc_last_oldid']; - #$ns = $this->mAttribs['rc_namespace']; - #$title = $this->mAttribs['rc_title']; - # - #$dbw->update( 'recentchanges', - # array( /* SET */ - # 'rc_this_oldid' => $oldid - # ), array( /* WHERE */ - # 'rc_namespace' => $ns, - # 'rc_title' => $title, - # 'rc_timestamp' => $dbw->timestamp( $lastTime ) - # ), $fname - #); - } - - # Update rc_cur_time - #$dbw->update( 'recentchanges', array( 'rc_cur_time' => $now ), - # array( 'rc_cur_id' => $curId ), $fname ); - } - # Notify external application via UDP - if ( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) { - $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); - if ( $conn ) { - $line = $wgRC2UDPPrefix . $this->getIRCLine(); - socket_sendto( $conn, $line, strlen($line), 0, $wgRC2UDPAddress, $wgRC2UDPPort ); - socket_close( $conn ); - } + if( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) { + self::sendToUDP( $this->getIRCLine() ); } # E-mail notifications @@ -246,15 +199,105 @@ class RecentChange } /** + * Send some text to UDP + * @param string $line + * @param string $prefix + * @param string $address + * @return bool success + */ + public static function sendToUDP( $line, $address = '', $prefix = '' ) { + global $wgRC2UDPAddress, $wgRC2UDPPrefix, $wgRC2UDPPort; + # Assume default for standard RC case + $address = $address ? $address : $wgRC2UDPAddress; + $prefix = $prefix ? $prefix : $wgRC2UDPPrefix; + # Notify external application via UDP + if( $address ) { + $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); + if( $conn ) { + $line = $prefix . $line; + wfDebug( __METHOD__ . ": sending UDP line: $line\n" ); + socket_sendto( $conn, $line, strlen($line), 0, $address, $wgRC2UDPPort ); + socket_close( $conn ); + return true; + } else { + wfDebug( __METHOD__ . ": failed to create UDP socket\n" ); + } + } + return false; + } + + /** + * Remove newlines and carriage returns + * @param string $line + * @return string + */ + public static function cleanupForIRC( $text ) { + return str_replace(array("\n", "\r"), array("", ""), $text); + } + + /** * Mark a given change as patrolled * * @param mixed $change RecentChange or corresponding rc_id - * @returns integer number of affected rows + * @param bool $auto for automatic patrol + * @return See doMarkPatrolled(), or null if $change is not an existing rc_id + */ + public static function markPatrolled( $change, $auto = false ) { + $change = $change instanceof RecentChange + ? $change + : RecentChange::newFromId($change); + if( !$change instanceof RecentChange ) { + return null; + } + return $change->doMarkPatrolled( $auto ); + } + + /** + * Mark this RecentChange as patrolled + * + * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and 'markedaspatrollederror-noautopatrol' as errors + * @param bool $auto for automatic patrol + * @return array of permissions errors, see Title::getUserPermissionsErrors() + */ + public function doMarkPatrolled( $auto = false ) { + global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol; + $errors = array(); + // If recentchanges patrol is disabled, only new pages + // can be patrolled + if( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute('rc_type') != RC_NEW ) ) { + $errors[] = array('rcpatroldisabled'); + } + // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol" + $right = $auto ? 'autopatrol' : 'patrol'; + $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $wgUser ) ); + if( !wfRunHooks('MarkPatrolled', array($this->getAttribute('rc_id'), &$wgUser, false)) ) { + $errors[] = array('hookaborted'); + } + // Users without the 'autopatrol' right can't patrol their + // own revisions + if( $wgUser->getName() == $this->getAttribute('rc_user_text') && !$wgUser->isAllowed('autopatrol') ) { + $errors[] = array('markedaspatrollederror-noautopatrol'); + } + if( $errors ) { + return $errors; + } + // If the change was patrolled already, do nothing + if( $this->getAttribute('rc_patrolled') ) { + return array(); + } + // Actually set the 'patrolled' flag in RC + $this->reallyMarkPatrolled(); + // Log this patrol event + PatrolLog::record( $this, $auto ); + wfRunHooks( 'MarkPatrolledComplete', array($this->getAttribute('rc_id'), &$wgUser, false) ); + return array(); + } + + /** + * Mark this RecentChange patrolled, without error checking + * @return int Number of affected rows */ - public static function markPatrolled( $change ) { - $rcid = $change instanceof RecentChange - ? $change->mAttribs['rc_id'] - : $change; + public function reallyMarkPatrolled() { $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'recentchanges', @@ -262,7 +305,7 @@ class RecentChange 'rc_patrolled' => 1 ), array( - 'rc_id' => $rcid + 'rc_id' => $this->getAttribute('rc_id') ), __METHOD__ ); @@ -270,13 +313,12 @@ class RecentChange } # Makes an entry in the database corresponding to an edit - public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, - $oldId, $lastTimestamp, $bot, $ip = '', $oldSize = 0, $newSize = 0, - $newId = 0) + public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId, + $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0 ) { - if ( !$ip ) { + if( !$ip ) { $ip = wfGetIP(); - if ( !$ip ) { + if( !$ip ) { $ip = ''; } } @@ -299,7 +341,7 @@ class RecentChange 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, - 'rc_patrolled' => 0, + 'rc_patrolled' => intval($patrol), 'rc_new' => 0, # obsolete 'rc_old_len' => $oldSize, 'rc_new_len' => $newSize, @@ -317,7 +359,7 @@ class RecentChange 'newSize' => $newSize, ); $rc->save(); - return( $rc->mAttribs['rc_id'] ); + return $rc; } /** @@ -326,11 +368,11 @@ class RecentChange * @todo Document parameters and return */ public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot, - $ip='', $size = 0, $newId = 0 ) + $ip='', $size=0, $newId=0, $patrol=0 ) { - if ( !$ip ) { + if( !$ip ) { $ip = wfGetIP(); - if ( !$ip ) { + if( !$ip ) { $ip = ''; } } @@ -353,7 +395,7 @@ class RecentChange 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, - 'rc_patrolled' => 0, + 'rc_patrolled' => intval($patrol), 'rc_new' => 1, # obsolete 'rc_old_len' => 0, 'rc_new_len' => $size, @@ -371,7 +413,7 @@ class RecentChange 'newSize' => $size ); $rc->save(); - return( $rc->mAttribs['rc_id'] ); + return $rc; } # Makes an entry in the database corresponding to a rename @@ -379,9 +421,9 @@ class RecentChange { global $wgRequest; - if ( !$ip ) { + if( !$ip ) { $ip = wfGetIP(); - if ( !$ip ) { + if( !$ip ) { $ip = ''; } } @@ -431,15 +473,14 @@ class RecentChange RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true ); } - # A log entry is different to an edit in that previous revisions are not kept public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip='', $type, $action, $target, $logComment, $params, $newId=0 ) { global $wgRequest; - if ( !$ip ) { + if( !$ip ) { $ip = wfGetIP(); - if ( !$ip ) { + if( !$ip ) { $ip = ''; } } @@ -458,7 +499,7 @@ class RecentChange 'rc_comment' => $logComment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, @@ -481,16 +522,14 @@ class RecentChange } # Initialises the members of this object from a mysql row object - function loadFromRow( $row ) - { + public function loadFromRow( $row ) { $this->mAttribs = get_object_vars( $row ); - $this->mAttribs["rc_timestamp"] = wfTimestamp(TS_MW, $this->mAttribs["rc_timestamp"]); - $this->mExtra = array(); + $this->mAttribs['rc_timestamp'] = wfTimestamp(TS_MW, $this->mAttribs['rc_timestamp']); + $this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set } # Makes a pseudo-RC entry from a cur row - function loadFromCurRow( $row ) - { + public function loadFromCurRow( $row ) { $this->mAttribs = array( 'rc_timestamp' => wfTimestamp(TS_MW, $row->rev_timestamp), 'rc_cur_time' => $row->rev_timestamp, @@ -517,11 +556,8 @@ class RecentChange 'rc_log_type' => isset($row->rc_log_type) ? $row->rc_log_type : null, 'rc_log_action' => isset($row->rc_log_action) ? $row->rc_log_action : null, 'rc_log_id' => isset($row->rc_log_id) ? $row->rc_log_id: 0, - // this one REALLY should be set... - 'rc_deleted' => isset($row->rc_deleted) ? $row->rc_deleted: 0, + 'rc_deleted' => $row->rc_deleted // MUST be set ); - - $this->mExtra = array(); } /** @@ -538,12 +574,11 @@ class RecentChange * Gets the end part of the diff URL associated with this object * Blank if no diff link should be displayed */ - function diffLinkTrail( $forceCur ) - { - if ( $this->mAttribs['rc_type'] == RC_EDIT ) { + public function diffLinkTrail( $forceCur ) { + if( $this->mAttribs['rc_type'] == RC_EDIT ) { $trail = "curid=" . (int)($this->mAttribs['rc_cur_id']) . "&oldid=" . (int)($this->mAttribs['rc_last_oldid']); - if ( $forceCur ) { + if( $forceCur ) { $trail .= '&diff=0' ; } else { $trail .= '&diff=' . (int)($this->mAttribs['rc_this_oldid']); @@ -554,44 +589,45 @@ class RecentChange return $trail; } - function cleanupForIRC( $text ) { - return str_replace(array("\n", "\r"), array("", ""), $text); - } - - function getIRCLine() { - global $wgUseRCPatrol; + protected function getIRCLine() { + global $wgUseRCPatrol, $wgUseNPPatrol, $wgRC2UDPInterwikiPrefix, $wgLocalInterwiki; // FIXME: Would be good to replace these 2 extract() calls with something more explicit // e.g. list ($rc_type, $rc_id) = array_values ($this->mAttribs); [or something like that] extract($this->mAttribs); extract($this->mExtra); - if ( $rc_type == RC_LOG ) { + if( $rc_type == RC_LOG ) { $titleObj = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL ); } else { $titleObj =& $this->getTitle(); } $title = $titleObj->getPrefixedText(); - $title = $this->cleanupForIRC( $title ); + $title = self::cleanupForIRC( $title ); - // FIXME: *HACK* these should be getFullURL(), hacked for SSL madness --brion 2005-12-26 - if ( $rc_type == RC_LOG ) { + if( $rc_type == RC_LOG ) { $url = ''; - } elseif ( $rc_new && $wgUseRCPatrol ) { - $url = $titleObj->getInternalURL("rcid=$rc_id"); - } else if ( $rc_new ) { - $url = $titleObj->getInternalURL(); - } else if ( $wgUseRCPatrol ) { - $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid&rcid=$rc_id"); } else { - $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid"); + if( $rc_type == RC_NEW ) { + $url = "oldid=$rc_this_oldid"; + } else { + $url = "diff=$rc_this_oldid&oldid=$rc_last_oldid"; + } + if( $wgUseRCPatrol || ($rc_type == RC_NEW && $wgUseNPPatrol) ) { + $url .= "&rcid=$rc_id"; + } + // XXX: *HACK* this should use getFullURL(), hacked for SSL madness --brion 2005-12-26 + // XXX: *HACK^2* the preg_replace() undoes much of what getInternalURL() does, but we + // XXX: need to call it so that URL paths on the Wikimedia secure server can be fixed + // XXX: by a custom GetInternalURL hook --vyznev 2008-12-10 + $url = preg_replace( '/title=[^&]*&/', '', $titleObj->getInternalURL( $url ) ); } - if ( isset( $oldSize ) && isset( $newSize ) ) { + if( isset( $oldSize ) && isset( $newSize ) ) { $szdiff = $newSize - $oldSize; - if ($szdiff < -500) { + if($szdiff < -500) { $szdiff = "\002$szdiff\002"; - } elseif ($szdiff >= 0) { + } elseif($szdiff >= 0) { $szdiff = '+' . $szdiff ; } $szdiff = '(' . $szdiff . ')' ; @@ -599,20 +635,35 @@ class RecentChange $szdiff = ''; } - $user = $this->cleanupForIRC( $rc_user_text ); + $user = self::cleanupForIRC( $rc_user_text ); - if ( $rc_type == RC_LOG ) { - $logTargetText = $this->getTitle()->getPrefixedText(); - $comment = $this->cleanupForIRC( str_replace($logTargetText,"\00302$logTargetText\00310",$actionComment) ); + if( $rc_type == RC_LOG ) { + $targetText = $this->getTitle()->getPrefixedText(); + $comment = self::cleanupForIRC( str_replace("[[$targetText]]","[[\00302$targetText\00310]]",$actionComment) ); $flag = $rc_log_action; } else { - $comment = $this->cleanupForIRC( $rc_comment ); + $comment = self::cleanupForIRC( $rc_comment ); $flag = ($rc_new ? "N" : "") . ($rc_minor ? "M" : "") . ($rc_bot ? "B" : ""); } + + if ( $wgRC2UDPInterwikiPrefix === true ) { + $prefix = $wgLocalInterwiki; + } elseif ( $wgRC2UDPInterwikiPrefix ) { + $prefix = $wgRC2UDPInterwikiPrefix; + } else { + $prefix = false; + } + if ( $prefix !== false ) { + $titleString = "\00314[[\00303$prefix:\00307$title\00314]]"; + } else { + $titleString = "\00314[[\00307$title\00314]]"; + } + # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003, # no colour (\003) switches back to the term default - $fullString = "\00314[[\00307$title\00314]]\0034 $flag\00310 " . + $fullString = "$titleString\0034 $flag\00310 " . "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n"; + return $fullString; } @@ -620,32 +671,16 @@ class RecentChange * Returns the change size (HTML). * The lengths can be given optionally. */ - function getCharacterDifference( $old = 0, $new = 0 ) { - global $wgRCChangedSizeThreshold, $wgLang; - + public function getCharacterDifference( $old = 0, $new = 0 ) { if( $old === 0 ) { $old = $this->mAttribs['rc_old_len']; } if( $new === 0 ) { $new = $this->mAttribs['rc_new_len']; } - if( $old === NULL || $new === NULL ) { return ''; } - - $szdiff = $new - $old; - $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape'), - $wgLang->formatNum($szdiff) ); - - if( $szdiff < $wgRCChangedSizeThreshold ) { - return '<strong class=\'mw-plusminus-neg\'>(' . $formatedSize . ')</strong>'; - } elseif( $szdiff === 0 ) { - return '<span class=\'mw-plusminus-null\'>(' . $formatedSize . ')</span>'; - } elseif( $szdiff > 0 ) { - return '<span class=\'mw-plusminus-pos\'>(+' . $formatedSize . ')</span>'; - } else { - return '<span class=\'mw-plusminus-neg\'>(' . $formatedSize . ')</span>'; - } + return ChangesList::showCharacterDifference( $old, $new ); } } diff --git a/includes/RefreshLinksJob.php b/includes/RefreshLinksJob.php index f95e5a50..1c119a8d 100644 --- a/includes/RefreshLinksJob.php +++ b/includes/RefreshLinksJob.php @@ -47,3 +47,87 @@ class RefreshLinksJob extends Job { return true; } } + +/** + * Background job to update links for a given title. + * Newer version for high use templates. + * + * @ingroup JobQueue + */ +class RefreshLinksJob2 extends Job { + + function __construct( $title, $params, $id = 0 ) { + parent::__construct( 'refreshLinks2', $title, $params, $id ); + } + + /** + * Run a refreshLinks2 job + * @return boolean success + */ + function run() { + global $wgParser; + + wfProfileIn( __METHOD__ ); + + $linkCache = LinkCache::singleton(); + $linkCache->clear(); + + if( is_null( $this->title ) ) { + $this->error = "refreshLinks2: Invalid title"; + wfProfileOut( __METHOD__ ); + return false; + } + if( !isset($this->params['start']) || !isset($this->params['end']) ) { + $this->error = "refreshLinks2: Invalid params"; + wfProfileOut( __METHOD__ ); + return false; + } + $start = intval($this->params['start']); + $end = intval($this->params['end']); + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks', 'page' ), + array( 'page_namespace', 'page_title' ), + array( + 'page_id=tl_from', + "tl_from >= '$start'", + "tl_from <= '$end'", + 'tl_namespace' => $this->title->getNamespace(), + 'tl_title' => $this->title->getDBkey() + ), __METHOD__ + ); + + # Not suitable for page load triggered job running! + # Gracefully switch to refreshLinks jobs if this happens. + if( php_sapi_name() != 'cli' ) { + $jobs = array(); + while( $row = $dbr->fetchObject( $res ) ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $jobs[] = new RefreshLinksJob( $title, '' ); + } + Job::batchInsert( $jobs ); + return true; + } + # Re-parse each page that transcludes this page and update their tracking links... + while( $row = $dbr->fetchObject( $res ) ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $revision = Revision::newFromTitle( $title ); + if ( !$revision ) { + $this->error = 'refreshLinks: Article not found "' . $title->getPrefixedDBkey() . '"'; + wfProfileOut( __METHOD__ ); + return false; + } + wfProfileIn( __METHOD__.'-parse' ); + $options = new ParserOptions; + $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() ); + wfProfileOut( __METHOD__.'-parse' ); + wfProfileIn( __METHOD__.'-update' ); + $update = new LinksUpdate( $title, $parserOutput, false ); + $update->doUpdate(); + wfProfileOut( __METHOD__.'-update' ); + wfProfileOut( __METHOD__ ); + } + + return true; + } +} diff --git a/includes/Revision.php b/includes/Revision.php index d0ccb46d..7938d88a 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -13,6 +13,11 @@ class Revision { const DELETED_USER = 4; const DELETED_RESTRICTED = 8; + // Audience options for Revision::getText() + const FOR_PUBLIC = 1; + const FOR_THIS_USER = 2; + const RAW = 3; + /** * Load a page revision from a given revision ID number. * Returns null if no such revision can be found. @@ -37,16 +42,24 @@ class Revision { * @return Revision */ public static function newFromTitle( $title, $id = 0 ) { - if( $id ) { - $matchId = intval( $id ); + $conds = array( + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey() + ); + if ( $id ) { + // Use the specified ID + $conds['rev_id'] = $id; + } elseif ( wfGetLB()->getServerCount() > 1 ) { + // Get the latest revision ID from the master + $dbw = wfGetDB( DB_MASTER ); + $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); + $conds['rev_id'] = $latest; } else { - $matchId = 'page_latest'; + // Use a join to get the latest revision + $conds[] = 'rev_id=page_latest'; } - return Revision::newFromConds( - array( "rev_id=$matchId", - 'page_id=rev_page', - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() ) ); + $conds[] = 'page_id=rev_page'; + return Revision::newFromConds( $conds ); } /** @@ -144,7 +157,7 @@ class Revision { private static function newFromConds( $conditions ) { $db = wfGetDB( DB_SLAVE ); $row = Revision::loadFromConds( $db, $conditions ); - if( is_null( $row ) ) { + if( is_null( $row ) && wfGetLB()->getServerCount() > 1 ) { $dbw = wfGetDB( DB_MASTER ); $row = Revision::loadFromConds( $dbw, $conditions ); } @@ -232,7 +245,7 @@ class Revision { array( 'page', 'revision' ), $fields, $conditions, - 'Revision::fetchRow', + __METHOD__, array( 'LIMIT' => 1 ) ); $ret = $db->resultObject( $res ); return $ret; @@ -306,9 +319,9 @@ class Revision { $this->mSize = intval( $row->rev_len ); if( isset( $row->page_latest ) ) { - $this->mCurrent = ( $row->rev_id == $row->page_latest ); - $this->mTitle = Title::makeTitle( $row->page_namespace, - $row->page_title ); + $this->mCurrent = ( $row->rev_id == $row->page_latest ); + $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); + $this->mTitle->resetArticleID( $this->mPage ); } else { $this->mCurrent = false; $this->mTitle = null; @@ -427,11 +440,22 @@ class Revision { } /** - * Fetch revision's user id if it's available to all users + * Fetch revision's user id if it's available to the specified audience. + * If the specified audience does not have access to it, zero will be + * returned. + * + * @param integer $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the ID regardless of permissions + * + * * @return int */ - public function getUser() { - if( $this->isDeleted( self::DELETED_USER ) ) { + public function getUser( $audience = self::FOR_PUBLIC ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { + return 0; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) { return 0; } else { return $this->mUser; @@ -447,11 +471,21 @@ class Revision { } /** - * Fetch revision's username if it's available to all users + * Fetch revision's username if it's available to the specified audience. + * If the specified audience does not have access to the username, an + * empty string will be returned. + * + * @param integer $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * * @return string */ - public function getUserText() { - if( $this->isDeleted( self::DELETED_USER ) ) { + public function getUserText( $audience = self::FOR_PUBLIC ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { + return ""; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) { return ""; } else { return $this->mUserText; @@ -467,11 +501,21 @@ class Revision { } /** - * Fetch revision comment if it's available to all users + * Fetch revision comment if it's available to the specified audience. + * If the specified audience does not have access to the comment, an + * empty string will be returned. + * + * @param integer $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * * @return string */ - function getComment() { - if( $this->isDeleted( self::DELETED_COMMENT ) ) { + function getComment( $audience = self::FOR_PUBLIC ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) { + return ""; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT ) ) { return ""; } else { return $this->mComment; @@ -500,13 +544,31 @@ class Revision { public function isDeleted( $field ) { return ($this->mDeleted & $field) == $field; } + + /** + * Get the deletion bitfield of the revision + */ + public function getVisibility() { + return (int)$this->mDeleted; + } /** - * Fetch revision text if it's available to all users + * Fetch revision text if it's available to the specified audience. + * If the specified audience does not have the ability to view this + * revision, an empty string will be returned. + * + * @param integer $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * + * * @return string */ - public function getText() { - if( $this->isDeleted( self::DELETED_TEXT ) ) { + public function getText( $audience = self::FOR_PUBLIC ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { + return ""; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT ) ) { return ""; } else { return $this->getRawText(); @@ -514,6 +576,13 @@ class Revision { } /** + * Alias for getText(Revision::FOR_THIS_USER) + */ + public function revText() { + return $this->getText( self::FOR_THIS_USER ); + } + + /** * Fetch revision text without regard for view restrictions * @return string */ @@ -526,18 +595,6 @@ class Revision { } /** - * Fetch revision text if it's available to THIS user - * @return string - */ - public function revText() { - if( !$this->userCan( self::DELETED_TEXT ) ) { - return ""; - } else { - return $this->getRawText(); - } - } - - /** * @return string */ public function getTimestamp() { @@ -607,7 +664,7 @@ class Revision { * $row is usually an object from wfFetchRow(), both the flags and the text * field must be included * - * @param integer $row Id of a row + * @param object $row The text data * @param string $prefix table prefix (default 'old_') * @return string $text|false the text requested */ @@ -663,9 +720,11 @@ class Revision { } global $wgLegacyEncoding; - if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) { + if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) ) { # Old revisions kept around in a legacy encoding? # Upconvert on demand. + # ("utf8" checked for compatibility with some broken + # conversion scripts 2008-12-30) global $wgInputEncoding, $wgContLang; $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text ); } @@ -719,20 +778,13 @@ class Revision { $flags = Revision::compressRevisionText( $data ); # Write to external storage if required - if ( $wgDefaultExternalStore ) { - if ( is_array( $wgDefaultExternalStore ) ) { - // Distribute storage across multiple clusters - $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)]; - } else { - $store = $wgDefaultExternalStore; - } + if( $wgDefaultExternalStore ) { // Store and get the URL - $data = ExternalStore::insert( $store, $data ); - if ( !$data ) { - # This should only happen in the case of a configuration error, where the external store is not valid - throw new MWException( "Unable to store text to external storage $store" ); + $data = ExternalStore::insertToDefault( $data ); + if( !$data ) { + throw new MWException( "Unable to store text to external storage" ); } - if ( $flags ) { + if( $flags ) { $flags .= ','; } $flags .= 'external'; @@ -816,7 +868,7 @@ class Revision { __METHOD__ ); } - if( !$row ) { + if( !$row && wfGetLB()->getServerCount() > 1 ) { // Possible slave lag! $dbw = wfGetDB( DB_MASTER ); $row = $dbw->selectRow( 'text', @@ -827,7 +879,8 @@ class Revision { $text = self::getRevisionText( $row ); - if( $wgRevisionCacheExpiry ) { + # No negative caching -- negative hits on text rows may be due to corrupted slave servers + if( $wgRevisionCacheExpiry && $text !== false ) { $wgMemc->set( $key, $text, $wgRevisionCacheExpiry ); } @@ -855,7 +908,7 @@ class Revision { $current = $dbw->selectRow( array( 'page', 'revision' ), - array( 'page_latest', 'rev_text_id' ), + array( 'page_latest', 'rev_text_id', 'rev_len' ), array( 'page_id' => $pageId, 'page_latest=rev_id', @@ -868,7 +921,8 @@ class Revision { 'comment' => $summary, 'minor_edit' => $minor, 'text_id' => $current->rev_text_id, - 'parent_id' => $current->page_latest + 'parent_id' => $current->page_latest, + 'len' => $current->rev_len ) ); } else { $revision = null; @@ -902,17 +956,15 @@ class Revision { /** * Get rev_timestamp from rev_id, without loading the rest of the row + * @param Title $title * @param integer $id - * @param integer $pageid, optional */ - static function getTimestampFromId( $id, $pageId = 0 ) { + static function getTimestampFromId( $title, $id ) { $dbr = wfGetDB( DB_SLAVE ); $conds = array( 'rev_id' => $id ); - if( $pageId ) { - $conds['rev_page'] = $pageId; - } + $conds['rev_page'] = $title->getArticleId(); $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); - if ( $timestamp === false ) { + if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) { # Not in slave, try master $dbw = wfGetDB( DB_MASTER ); $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index 28b1c275..5d58b036 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -331,9 +331,6 @@ $wgHtmlEntityAliases = array( * @ingroup Parser */ class Sanitizer { - const NONE = 0; - const INITIAL_NONLETTER = 1; - /** * Cleans up HTML, removes dangerous tags and attributes, and * removes HTML comments @@ -616,8 +613,11 @@ class Sanitizer { } } - if ( $attribute === 'id' ) - $value = Sanitizer::escapeId( $value ); + if ( $attribute === 'id' ) { + global $wgEnforceHtmlIds; + $value = Sanitizer::escapeId( $value, + $wgEnforceHtmlIds ? 'noninitial' : 'xml' ); + } // If this attribute was previously set, override it. // Output should only have one attribute of each name. @@ -627,10 +627,9 @@ class Sanitizer { } /** - * Merge two sets of HTML attributes. - * Conflicting items in the second set will override those - * in the first, except for 'class' attributes which will be - * combined. + * Merge two sets of HTML attributes. Conflicting items in the second set + * will override those in the first, except for 'class' attributes which + * will be combined (if they're both strings). * * @todo implement merging for other attributes such as style * @param array $a @@ -639,16 +638,12 @@ class Sanitizer { */ static function mergeAttributes( $a, $b ) { $out = array_merge( $a, $b ); - if( isset( $a['class'] ) - && isset( $b['class'] ) - && $a['class'] !== $b['class'] ) { - - $out['class'] = implode( ' ', - array_unique( - preg_split( '/\s+/', - $a['class'] . ' ' . $b['class'], - -1, - PREG_SPLIT_NO_EMPTY ) ) ); + if( isset( $a['class'] ) && isset( $b['class'] ) + && is_string( $a['class'] ) && is_string( $b['class'] ) + && $a['class'] !== $b['class'] ) { + $classes = preg_split( '/\s+/', "{$a['class']} {$b['class']}", + -1, PREG_SPLIT_NO_EMPTY ); + $out['class'] = implode( ' ', array_unique( $classes ) ); } return $out; } @@ -782,28 +777,55 @@ class Sanitizer { * name attributes * @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute * - * @param string $id Id to validate - * @param int $flags Currently only two values: Sanitizer::INITIAL_NONLETTER - * (default) permits initial non-letter characters, - * such as if you're adding a prefix to them. - * Sanitizer::NONE will prepend an 'x' if the id - * would otherwise start with a nonletter. + * @param string $id Id to validate + * @param mixed $options String or array of strings (default is array()): + * 'noninitial': This is a non-initial fragment of an id, not a full id, + * so don't pay attention if the first character isn't valid at the + * beginning of an id. + * 'xml': Don't restrict the id to be HTML4-compatible. This option + * allows any alphabetic character to be used, per the XML standard. + * Therefore, it also completely changes the type of escaping: instead + * of weird dot-encoding, runs of invalid characters (mostly + * whitespace) are just compressed into a single underscore. * @return string */ - static function escapeId( $id, $flags = Sanitizer::INITIAL_NONLETTER ) { - static $replace = array( - '%3A' => ':', - '%' => '.' - ); - - $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) ); - $id = str_replace( array_keys( $replace ), array_values( $replace ), $id ); - - if( ~$flags & Sanitizer::INITIAL_NONLETTER - && !preg_match( '/[a-zA-Z]/', $id[0] ) ) { - // Initial character must be a letter! - $id = "x$id"; + static function escapeId( $id, $options = array() ) { + $options = (array)$options; + + if ( !in_array( 'xml', $options ) ) { + # HTML4-style escaping + static $replace = array( + '%3A' => ':', + '%' => '.' + ); + + $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) ); + $id = str_replace( array_keys( $replace ), array_values( $replace ), $id ); + + if ( !preg_match( '/^[a-zA-Z]/', $id ) + && !in_array( 'noninitial', $options ) ) { + // Initial character must be a letter! + $id = "x$id"; + } + return $id; + } + + # XML-style escaping. For the patterns used, see the XML 1.0 standard, + # 5th edition, NameStartChar and NameChar: <http://www.w3.org/TR/REC-xml/> + $nameStartChar = ':a-zA-Z_\xC0-\xD6\xD8-\xF6\xF8-\x{2FF}\x{370}-\x{37D}' + . '\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}' + . '\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}'; + $nameChar = $nameStartChar . '.\-0-9\xB7\x{0300}-\x{036F}' + . '\x{203F}-\x{2040}'; + # Replace _ as well so we don't get multiple consecutive underscores + $id = preg_replace( "/([^$nameChar]|_)+/u", '_', $id ); + $id = trim( $id, '_' ); + + if ( !preg_match( "/^[$nameStartChar]/u", $id ) + && !in_array( 'noninitial', $options ) ) { + $id = "_$id"; } + return $id; } @@ -827,6 +849,22 @@ class Sanitizer { } /** + * Given HTML input, escape with htmlspecialchars but un-escape entites. + * This allows (generally harmless) entities like to survive. + * + * @param string $html String to escape + * @return string Escaped input + */ + static function escapeHtmlAllowEntities( $html ) { + # It seems wise to escape ' as well as ", as a matter of course. Can't + # hurt. + $html = htmlspecialchars( $html, ENT_QUOTES ); + $html = str_replace( '&', '&', $html ); + $html = Sanitizer::normalizeCharReferences( $html ); + return $html; + } + + /** * Regex replace callback for armoring links against further processing. * @param array $matches * @return string @@ -844,7 +882,7 @@ class Sanitizer { * @param string * @return array */ - static function decodeTagAttributes( $text ) { + public static function decodeTagAttributes( $text ) { $attribs = array(); if( trim( $text ) == '' ) { @@ -1111,7 +1149,8 @@ class Sanitizer { } /** - * @todo Document it a bit + * Foreach array key (an allowed HTML element), return an array + * of allowed attributes * @return array */ static function setupAttributeWhitelist() { @@ -1301,7 +1340,7 @@ class Sanitizer { return $out; } - static function cleanUrl( $url, $hostname=true ) { + static function cleanUrl( $url ) { # Normalize any HTML entities in input. They will be # re-escaped by makeExternalLink(). $url = Sanitizer::decodeCharReferences( $url ); diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php index edd93cce..3ea0341d 100644 --- a/includes/SearchEngine.php +++ b/includes/SearchEngine.php @@ -13,6 +13,7 @@ class SearchEngine { var $limit = 10; var $offset = 0; + var $prefix = ''; var $searchTerms = array(); var $namespaces = array( NS_MAIN ); var $showRedirects = false; @@ -43,6 +44,19 @@ class SearchEngine { return null; } + /** If this search backend can list/unlist redirects */ + function acceptListRedirects() { + return true; + } + + /** + * Transform search term in cases when parts of the query came as different GET params (when supported) + * e.g. for prefix queries: search=test&prefix=Main_Page/Archive -> test prefix:Main Page/Archive + */ + function transformSearchTerm( $term ) { + return $term; + } + /** * If an exact title match can be find, or a very slightly close match, * return the title. If no match, returns NULL. @@ -98,19 +112,6 @@ class SearchEngine { return $title; } - global $wgCapitalLinks, $wgContLang; - if( !$wgCapitalLinks ) { - // Catch differs-by-first-letter-case-only - $title = Title::newFromText( $wgContLang->ucfirst( $term ) ); - if ( $title && $title->exists() ) { - return $title; - } - $title = Title::newFromText( $wgContLang->lcfirst( $term ) ); - if ( $title && $title->exists() ) { - return $title; - } - } - // Give hooks a chance at better match variants $title = null; if( !wfRunHooks( 'SearchGetNearMatch', array( $term, &$title ) ) ) { @@ -135,7 +136,7 @@ class SearchEngine { # Go to images that exist even if there's no local page. # There may have been a funny upload, or it may be on a shared # file repository such as Wikimedia Commons. - if( $title->getNamespace() == NS_IMAGE ) { + if( $title->getNamespace() == NS_FILE ) { $image = wfFindFile( $title ); if( $image ) { return $title; @@ -158,7 +159,7 @@ class SearchEngine { } public static function legalSearchChars() { - return "A-Za-z_'0-9\\x80-\\xFF\\-"; + return "A-Za-z_'.0-9\\x80-\\xFF\\-"; } /** @@ -275,7 +276,51 @@ class SearchEngine { return array_keys($wgNamespacesToBeSearchedDefault, true); } - + + /** + * Get a list of namespace names useful for showing in tooltips + * and preferences + * + * @param unknown_type $namespaces + */ + public static function namespacesAsText( $namespaces ){ + global $wgContLang; + + $formatted = array_map( array($wgContLang,'getFormattedNsText'), $namespaces ); + foreach( $formatted as $key => $ns ){ + if ( empty($ns) ) + $formatted[$key] = wfMsg( 'blanknamespace' ); + } + return $formatted; + } + + /** + * An array of "project" namespaces indexes typically searched + * by logged-in users + * + * @return array + * @static + */ + public static function projectNamespaces() { + global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject; + + return array_keys( $wgNamespacesToBeSearchedProject, true ); + } + + /** + * An array of "project" namespaces indexes typically searched + * by logged-in users in addition to the default namespaces + * + * @return array + * @static + */ + public static function defaultAndProjectNamespaces() { + global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject; + + return array_keys( $wgNamespacesToBeSearchedDefault + + $wgNamespacesToBeSearchedProject, true); + } + /** * Return a 'cleaned up' search string * @@ -290,24 +335,17 @@ class SearchEngine { * Load up the appropriate search engine class for the currently * active database backend, and return a configured instance. * - * @fixme Ask the database class for his default search class - * instead of knowing about every backend here. * @return SearchEngine */ public static function create() { - global $wgDBtype, $wgSearchType; + global $wgSearchType; + $dbr = wfGetDB( DB_SLAVE ); if( $wgSearchType ) { $class = $wgSearchType; - } elseif( $wgDBtype == 'mysql' ) { - $class = 'SearchMySQL'; - } else if ( $wgDBtype == 'postgres' ) { - $class = 'SearchPostgres'; - } else if ( $wgDBtype == 'oracle' ) { - $class = 'SearchOracle'; } else { - $class = 'SearchEngineDummy'; + $class = $dbr->getSearchEngine(); } - $search = new $class( wfGetDB( DB_SLAVE ) ); + $search = new $class( $dbr ); $search->setLimitOffset(0,0); return $search; } @@ -345,11 +383,11 @@ class SearchEngine { */ public static function getOpenSearchTemplate() { global $wgOpenSearchTemplate, $wgServer, $wgScriptPath; - if($wgOpenSearchTemplate) + if( $wgOpenSearchTemplate ) { return $wgOpenSearchTemplate; - else{ - $ns = implode(',',SearchEngine::defaultNamespaces()); - if(!$ns) $ns = "0"; + } else { + $ns = implode( '|', SearchEngine::defaultNamespaces() ); + if( !$ns ) $ns = "0"; return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace='.$ns; } } @@ -432,7 +470,7 @@ class SearchResultSet { } /** - * @return string highlighted suggested query, '' if none + * @return string HTML highlighted suggested query, '' if none */ function getSuggestionSnippet(){ return ''; @@ -503,11 +541,15 @@ class SearchResultTooMany { */ class SearchResult { var $mRevision = null; + var $mImage = null; - function SearchResult( $row ) { + function __construct( $row ) { $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); - if( !is_null($this->mTitle) ) + if( !is_null($this->mTitle) ){ $this->mRevision = Revision::newFromTitle( $this->mTitle ); + if( $this->mTitle->getNamespace() === NS_FILE ) + $this->mImage = wfFindFile( $this->mTitle ); + } } /** @@ -529,9 +571,7 @@ class SearchResult { * @access public */ function isMissingRevision(){ - if( !$this->mRevision ) - return true; - return false; + return !$this->mRevision && !$this->mImage; } /** @@ -554,7 +594,11 @@ class SearchResult { */ protected function initText(){ if( !isset($this->mText) ){ - $this->mText = $this->mRevision->getText(); + if($this->mRevision != null) + $this->mText = $this->mRevision->getText(); + else // TODO: can we fetch raw wikitext for commons images? + $this->mText = ''; + } } @@ -614,7 +658,11 @@ class SearchResult { * @return string timestamp */ function getTimestamp(){ - return $this->mRevision->getTimestamp(); + if( $this->mRevision ) + return $this->mRevision->getTimestamp(); + else if( $this->mImage ) + return $this->mImage->getTimestamp(); + return ''; } /** @@ -706,7 +754,7 @@ class SearchHighlighter { if($key == 2){ // see if this is an image link $ns = substr($val[0],2,-1); - if( $wgContLang->getNsIndex($ns) != NS_IMAGE ) + if( $wgContLang->getNsIndex($ns) != NS_FILE ) break; } @@ -761,13 +809,12 @@ class SearchHighlighter { // prepare regexps foreach( $terms as $index => $term ) { - $terms[$index] = preg_quote( $term, '/' ); // manually do upper/lowercase stuff for utf-8 since PHP won't do it if(preg_match('/[\x80-\xff]/', $term) ){ $terms[$index] = preg_replace_callback('/./us',array($this,'caseCallback'),$terms[$index]); + } else { + $terms[$index] = $term; } - - } $anyterm = implode( '|', $terms ); $phrase = implode("$wgSearchHighlightBoundaries+", $terms ); @@ -1077,7 +1124,7 @@ class SearchHighlighter { global $wgContLang; $ns = substr( $matches[1], 0, $colon ); $index = $wgContLang->getNsIndex($ns); - if( $index !== false && ($index == NS_IMAGE || $index == NS_CATEGORY) ) + if( $index !== false && ($index == NS_FILE || $index == NS_CATEGORY) ) return $matches[0]; // return the whole thing else return $matches[2]; @@ -1097,11 +1144,10 @@ class SearchHighlighter { public function highlightSimple( $text, $terms, $contextlines, $contextchars ) { global $wgLang, $wgContLang; $fname = __METHOD__; - + $lines = explode( "\n", $text ); $terms = implode( '|', $terms ); - $terms = str_replace( '/', "\\/", $terms); $max = intval( $contextchars ) + 1; $pat1 = "/(.*)($terms)(.{0,$max})/i"; diff --git a/includes/SearchMySQL.php b/includes/SearchMySQL.php index f9b71c8e..5fc06790 100644 --- a/includes/SearchMySQL.php +++ b/includes/SearchMySQL.php @@ -34,7 +34,10 @@ class SearchMySQL extends SearchEngine { $this->db = $db; } - /** @todo document */ + /** + * Parse the user's query and transform it into an SQL fragment which will + * become part of a WHERE clause + */ function parseQuery( $filteredText, $fulltext ) { global $wgContLang; $lc = SearchEngine::legalSearchChars(); // Minus format chars @@ -54,7 +57,11 @@ class SearchMySQL extends SearchEngine { if( !empty( $terms[3] ) ) { // Match individual terms in result highlighting... $regexp = preg_quote( $terms[3], '/' ); - if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+"; + if( $terms[4] ) { + $regexp = "\b$regexp"; // foo* + } else { + $regexp = "\b$regexp\b"; + } } else { // Match the quoted term in result highlighting... $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' ); @@ -122,9 +129,10 @@ class SearchMySQL extends SearchEngine { function queryNamespaces() { if( is_null($this->namespaces) ) return ''; # search all - $namespaces = implode( ',', $this->namespaces ); - if ($namespaces == '') { + if ( !count( $this->namespaces ) ) { $namespaces = '0'; + } else { + $namespaces = $this->db->makeList( $this->namespaces ); } return 'AND page_namespace IN (' . $namespaces . ')'; } diff --git a/includes/SearchOracle.php b/includes/SearchOracle.php index bf9368d1..b48d5e6e 100644 --- a/includes/SearchOracle.php +++ b/includes/SearchOracle.php @@ -77,9 +77,10 @@ class SearchOracle extends SearchEngine { function queryNamespaces() { if( is_null($this->namespaces) ) return ''; - $namespaces = implode(',', $this->namespaces); - if ($namespaces == '') { + if ( !count( $this->namespaces ) ) { $namespaces = '0'; + } else { + $namespaces = $this->db->makeList( $this->namespaces ); } return 'AND page_namespace IN (' . $namespaces . ')'; } @@ -144,7 +145,10 @@ class SearchOracle extends SearchEngine { 'WHERE page_id=si_page AND ' . $match; } - /** @todo document */ + /** + * Parse a user input search string, and return an SQL fragment to be used + * as part of a WHERE clause + */ function parseQuery($filteredText, $fulltext) { global $wgContLang; $lc = SearchEngine::legalSearchChars(); @@ -170,9 +174,9 @@ class SearchOracle extends SearchEngine { } } - $searchon = $this->db->strencode(join(',', $q)); + $searchon = $this->db->addQuotes(join(',', $q)); $field = $this->getIndexField($fulltext); - return " CONTAINS($field, '$searchon', 1) > 0 "; + return " CONTAINS($field, $searchon, 1) > 0 "; } /** diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php index 88e4a0da..4862a44e 100644 --- a/includes/SearchPostgres.php +++ b/includes/SearchPostgres.php @@ -66,6 +66,7 @@ class SearchPostgres extends SearchEngine { /* * Transform the user's search string into a better form for tsearch2 + * Returns an SQL fragment consisting of quoted text to search for. */ function parseQuery( $term ) { @@ -142,6 +143,7 @@ class SearchPostgres extends SearchEngine { } $prefix = $wgDBversion < 8.3 ? "'default'," : ''; + # Get the SQL fragment for the given term $searchstring = $this->parseQuery( $term ); ## We need a separate query here so gin does not complain about empty searches @@ -183,7 +185,7 @@ class SearchPostgres extends SearchEngine { if ( count($this->namespaces) < 1) $query .= ' AND page_namespace = 0'; else { - $namespaces = implode( ',', $this->namespaces ); + $namespaces = $this->db->makeList( $this->namespaces ); $query .= " AND page_namespace IN ($namespaces)"; } } @@ -201,9 +203,9 @@ class SearchPostgres extends SearchEngine { function update( $pageid, $title, $text ) { ## We don't want to index older revisions - $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id = ". - "(SELECT rev_text_id FROM revision WHERE rev_page = $pageid ". - "ORDER BY rev_text_id DESC LIMIT 1 OFFSET 1)"; + $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id IN ". + "(SELECT rev_text_id FROM revision WHERE rev_page = " . intval( $pageid ) . + " ORDER BY rev_text_id DESC OFFSET 1)"; $this->db->doQuery($SQL); return true; } diff --git a/includes/Setup.php b/includes/Setup.php index 36c4d965..859ad008 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -59,6 +59,23 @@ if ( empty( $wgFileStore['deleted']['directory'] ) ) { } /** + * Unconditional protection for NS_MEDIAWIKI since otherwise it's too easy for a + * sysadmin to set $wgNamespaceProtection incorrectly and leave the wiki insecure. + * + * Note that this is the definition of editinterface and it can be granted to + * all users if desired. + */ +$wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; + +/** + * The canonical names of namespaces 6 and 7 are, as of v1.14, "File" + * and "File_talk". The old names "Image" and "Image_talk" are + * retained as aliases for backwards compatibility. + */ +$wgNamespaceAliases['Image'] = NS_FILE; +$wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; + +/** * Initialise $wgLocalFileRepo from backwards-compatible settings */ if ( !$wgLocalFileRepo ) { @@ -137,12 +154,6 @@ wfProfileIn( $fname.'-misc1' ); $wgIP = false; # Load on demand # Can't stub this one, it sets up $_GET and $_REQUEST in its constructor $wgRequest = new WebRequest; -if ( function_exists( 'posix_uname' ) ) { - $wguname = posix_uname(); - $wgNodeName = $wguname['nodename']; -} else { - $wgNodeName = ''; -} # Useful debug output if ( $wgCommandLineMode ) { @@ -198,15 +209,19 @@ wfDebug( 'Main cache: ' . get_class( $wgMemc ) . "\nParser cache: " . get_class( $parserMemc ) . "\n" ); wfProfileOut( $fname.'-memcached' ); + +## Most of the config is out, some might want to run hooks here. +wfRunHooks( 'SetupAfterCache' ); + wfProfileIn( $fname.'-SetupSession' ); # Set default shared prefix if( $wgSharedPrefix === false ) $wgSharedPrefix = $wgDBprefix; if( !$wgCookiePrefix ) { - if ( in_array('user', $wgSharedTables) && $wgSharedDB && $wgSharedPrefix ) { + if ( $wgSharedDB && $wgSharedPrefix && in_array('user',$wgSharedTables) ) { $wgCookiePrefix = $wgSharedDB . '_' . $wgSharedPrefix; - } elseif ( in_array('user', $wgSharedTables) && $wgSharedDB ) { + } elseif ( $wgSharedDB && in_array('user',$wgSharedTables) ) { $wgCookiePrefix = $wgSharedDB; } elseif ( $wgDBprefix ) { $wgCookiePrefix = $wgDBname . '_' . $wgDBprefix; @@ -269,7 +284,6 @@ wfProfileIn( $fname.'-misc2' ); $wgDeferredUpdateList = array(); $wgPostCommitUpdateList = array(); -if ( $wgAjaxSearch ) $wgAjaxExportList[] = 'wfSajaxSearch'; if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch'; if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'UploadForm::ajaxGetExistsWarning'; if( $wgAjaxLicensePreview ) @@ -299,6 +313,16 @@ wfRunHooks( 'LogPageLogName', array( &$wgLogNames ) ); wfRunHooks( 'LogPageLogHeader', array( &$wgLogHeaders ) ); wfRunHooks( 'LogPageActionText', array( &$wgLogActions ) ); +if( !empty($wgNewUserLog) ) { + # Add a new log type + $wgLogTypes[] = 'newusers'; + $wgLogNames['newusers'] = 'newuserlogpage'; + $wgLogHeaders['newusers'] = 'newuserlogpagetext'; + $wgLogActions['newusers/newusers'] = 'newuserlogentry'; // For compatibility with older log entries + $wgLogActions['newusers/create'] = 'newuserlog-create-entry'; + $wgLogActions['newusers/create2'] = 'newuserlog-create2-entry'; + $wgLogActions['newusers/autocreate'] = 'newuserlog-autocreate-entry'; +} wfDebug( "Fully initialised\n" ); $wgFullyInitialised = true; diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php index 6cdd5082..2ed28139 100644 --- a/includes/SiteConfiguration.php +++ b/includes/SiteConfiguration.php @@ -5,57 +5,143 @@ * meaning that require_once() fails to detect that it is including the same * file again. We use DIY C-style protection as a workaround. */ -if (!defined('SITE_CONFIGURATION')) { -define('SITE_CONFIGURATION', 1); + +// Hide this pattern from Doxygen, which spazzes out at it +/// @cond +if( !defined( 'SITE_CONFIGURATION' ) ){ +define( 'SITE_CONFIGURATION', 1 ); +/// @endcond /** * This is a class used to hold configuration settings, particularly for multi-wiki sites. - * */ class SiteConfiguration { - var $suffixes = array(); - var $wikis = array(); - var $settings = array(); - var $localVHosts = array(); - - /** */ - function get( $settingName, $wiki, $suffix, $params = array(), $wikiTags = array() ) { - if ( array_key_exists( $settingName, $this->settings ) ) { + + /** + * Array of suffixes, for self::siteFromDB() + */ + public $suffixes = array(); + + /** + * Array of wikis, should be the same as $wgLocalDatabases + */ + public $wikis = array(); + + /** + * The whole array of settings + */ + public $settings = array(); + + /** + * Array of domains that are local and can be handled by the same server + */ + public $localVHosts = array(); + + /** + * A callback function that returns an array with the following keys (all + * optional): + * - suffix: site's suffix + * - lang: site's lang + * - tags: array of wiki tags + * - params: array of parameters to be replaced + * The function will receive the SiteConfiguration instance in the first + * argument and the wiki in the second one. + * if suffix and lang are passed they will be used for the return value of + * self::siteFromDB() and self::$suffixes will be ignored + */ + public $siteParamsCallback = null; + + /** + * Retrieves a configuration setting for a given wiki. + * @param $settingName String ID of the setting name to retrieve + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + * @return Mixed the value of the setting requested. + */ + public function get( $settingName, $wiki, $suffix = null, $params = array(), $wikiTags = array() ) { + $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags ); + return $this->getSetting( $settingName, $wiki, $params ); + } + + /** + * Really retrieves a configuration setting for a given wiki. + * + * @param $settingName String ID of the setting name to retrieve. + * @param $wiki String Wiki ID of the wiki in question. + * @param $params Array: array of parameters. + * @return Mixed the value of the setting requested. + */ + protected function getSetting( $settingName, $wiki, /*array*/ $params ){ + $retval = null; + if( array_key_exists( $settingName, $this->settings ) ) { $thisSetting =& $this->settings[$settingName]; do { - if ( array_key_exists( $wiki, $thisSetting ) ) { + // Do individual wiki settings + if( array_key_exists( $wiki, $thisSetting ) ) { $retval = $thisSetting[$wiki]; break; + } elseif( array_key_exists( "+$wiki", $thisSetting ) && is_array( $thisSetting["+$wiki"] ) ) { + $retval = $thisSetting["+$wiki"]; } - foreach ( $wikiTags as $tag ) { - if ( array_key_exists( $tag, $thisSetting ) ) { - $retval = $thisSetting[$tag]; + + // Do tag settings + foreach( $params['tags'] as $tag ) { + if( array_key_exists( $tag, $thisSetting ) ) { + if ( isset( $retval ) && is_array( $retval ) && is_array( $thisSetting[$tag] ) ) { + $retval = self::arrayMerge( $retval, $thisSetting[$tag] ); + } else { + $retval = $thisSetting[$tag]; + } break 2; + } elseif( array_key_exists( "+$tag", $thisSetting ) && is_array($thisSetting["+$tag"]) ) { + if( !isset( $retval ) ) + $retval = array(); + $retval = self::arrayMerge( $retval, $thisSetting["+$tag"] ); } } - if ( array_key_exists( $suffix, $thisSetting ) ) { - $retval = $thisSetting[$suffix]; - break; + // Do suffix settings + $suffix = $params['suffix']; + if( !is_null( $suffix ) ) { + if( array_key_exists( $suffix, $thisSetting ) ) { + if ( isset($retval) && is_array($retval) && is_array($thisSetting[$suffix]) ) { + $retval = self::arrayMerge( $retval, $thisSetting[$suffix] ); + } else { + $retval = $thisSetting[$suffix]; + } + break; + } elseif( array_key_exists( "+$suffix", $thisSetting ) && is_array($thisSetting["+$suffix"]) ) { + if (!isset($retval)) + $retval = array(); + $retval = self::arrayMerge( $retval, $thisSetting["+$suffix"] ); + } } - if ( array_key_exists( 'default', $thisSetting ) ) { - $retval = $thisSetting['default']; + + // Fall back to default. + if( array_key_exists( 'default', $thisSetting ) ) { + if( is_array( $retval ) && is_array( $thisSetting['default'] ) ) { + $retval = self::arrayMerge( $retval, $thisSetting['default'] ); + } else { + $retval = $thisSetting['default']; + } break; } - $retval = null; } while ( false ); - } else { - $retval = NULL; } - if ( !is_null( $retval ) && count( $params ) ) { - foreach ( $params as $key => $value ) { + if( !is_null( $retval ) && count( $params['params'] ) ) { + foreach ( $params['params'] as $key => $value ) { $retval = $this->doReplace( '$' . $key, $value, $retval ); } } return $retval; } - /** Type-safe string replace; won't do replacements on non-strings */ + /** + * Type-safe string replace; won't do replacements on non-strings + * private? + */ function doReplace( $from, $to, $in ) { if( is_string( $in ) ) { return str_replace( $from, $to, $in ); @@ -69,62 +155,191 @@ class SiteConfiguration { } } - /** */ - function getAll( $wiki, $suffix, $params, $wikiTags = array() ) { + /** + * Gets all settings for a wiki + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + * @return Array Array of settings requested. + */ + public function getAll( $wiki, $suffix = null, $params = array(), $wikiTags = array() ) { + $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags ); $localSettings = array(); - foreach ( $this->settings as $varname => $stuff ) { - $value = $this->get( $varname, $wiki, $suffix, $params, $wikiTags ); + foreach( $this->settings as $varname => $stuff ) { + $append = false; + $var = $varname; + if ( substr( $varname, 0, 1 ) == '+' ) { + $append = true; + $var = substr( $varname, 1 ); + } + + $value = $this->getSetting( $varname, $wiki, $params ); + if ( $append && is_array( $value ) && is_array( $GLOBALS[$var] ) ) + $value = self::arrayMerge( $value, $GLOBALS[$var] ); if ( !is_null( $value ) ) { - $localSettings[$varname] = $value; + $localSettings[$var] = $value; } } return $localSettings; } - /** */ - function getBool( $setting, $wiki, $suffix, $wikiTags = array() ) { + /** + * Retrieves a configuration setting for a given wiki, forced to a boolean. + * @param $settingName String ID of the setting name to retrieve + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + * @return bool The value of the setting requested. + */ + public function getBool( $setting, $wiki, $suffix = null, $wikiTags = array() ) { return (bool)($this->get( $setting, $wiki, $suffix, array(), $wikiTags ) ); } - /** */ + /** Retrieves an array of local databases */ function &getLocalDatabases() { return $this->wikis; } - /** */ + /** A no-op */ function initialise() { } - /** */ - function extractVar( $setting, $wiki, $suffix, &$var, $params, $wikiTags = array() ) { + /** + * Retrieves the value of a given setting, and places it in a variable passed by reference. + * @param $settingName String ID of the setting name to retrieve + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $var Reference The variable to insert the value into. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + */ + public function extractVar( $setting, $wiki, $suffix, &$var, $params = array(), $wikiTags = array() ) { $value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags ); if ( !is_null( $value ) ) { $var = $value; } } - /** */ - function extractGlobal( $setting, $wiki, $suffix, $params, $wikiTags = array() ) { - $value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags ); + /** + * Retrieves the value of a given setting, and places it in its corresponding global variable. + * @param $settingName String ID of the setting name to retrieve + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + */ + public function extractGlobal( $setting, $wiki, $suffix = null, $params = array(), $wikiTags = array() ) { + $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags ); + $this->extractGlobalSetting( $setting, $wiki, $params ); + } + + public function extractGlobalSetting( $setting, $wiki, $params ) { + $value = $this->getSetting( $setting, $wiki, $params ); if ( !is_null( $value ) ) { - $GLOBALS[$setting] = $value; + if (substr($setting,0,1) == '+' && is_array($value)) { + $setting = substr($setting,1); + if ( is_array($GLOBALS[$setting]) ) { + $GLOBALS[$setting] = self::arrayMerge( $GLOBALS[$setting], $value ); + } else { + $GLOBALS[$setting] = $value; + } + } else { + $GLOBALS[$setting] = $value; + } } } - /** */ - function extractAllGlobals( $wiki, $suffix, $params, $wikiTags = array() ) { + /** + * Retrieves the values of all settings, and places them in their corresponding global variables. + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + */ + public function extractAllGlobals( $wiki, $suffix = null, $params = array(), $wikiTags = array() ) { + $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags ); foreach ( $this->settings as $varName => $setting ) { - $this->extractGlobal( $varName, $wiki, $suffix, $params, $wikiTags ); + $this->extractGlobalSetting( $varName, $wiki, $params ); } } /** + * Return specific settings for $wiki + * See the documentation of self::$siteParamsCallback for more in-depth + * documentation about this function + * + * @param $wiki String + * @return array + */ + protected function getWikiParams( $wiki ){ + static $default = array( + 'suffix' => null, + 'lang' => null, + 'tags' => array(), + 'params' => array(), + ); + + if( !is_callable( $this->siteParamsCallback ) ) + return $default; + + $ret = call_user_func_array( $this->siteParamsCallback, array( $this, $wiki ) ); + # Validate the returned value + if( !is_array( $ret ) ) + return $default; + + foreach( $default as $name => $def ){ + if( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) ) + $ret[$name] = $default[$name]; + } + + return $ret; + } + + /** + * Merge params beetween the ones passed to the function and the ones given + * by self::$siteParamsCallback for backward compatibility + * Values returned by self::getWikiParams() have the priority. + * + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in + * all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + * @return array + */ + protected function mergeParams( $wiki, $suffix, /*array*/ $params, /*array*/ $wikiTags ){ + $ret = $this->getWikiParams( $wiki ); + + if( is_null( $ret['suffix'] ) ) + $ret['suffix'] = $suffix; + + $ret['tags'] = array_unique( array_merge( $ret['tags'], $wikiTags ) ); + + $ret['params'] += $params; + + // Automatically fill that ones if needed + if( !isset( $ret['params']['lang'] ) && !is_null( $ret['lang'] ) ) + $ret['params']['lang'] = $ret['lang']; + if( !isset( $ret['params']['site'] ) && !is_null( $ret['suffix'] ) ) + $ret['params']['site'] = $ret['suffix']; + + return $ret; + } + + /** * Work out the site and language name from a database name * @param $db */ - function siteFromDB( $db ) { - $site = NULL; - $lang = NULL; + public function siteFromDB( $db ) { + // Allow override + $def = $this->getWikiParams( $db ); + if( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) ) + return array( $def['suffix'], $def['lang'] ); + + $site = null; + $lang = null; foreach ( $this->suffixes as $suffix ) { if ( $suffix === '' ) { $site = ''; @@ -140,9 +355,37 @@ class SiteConfiguration { return array( $site, $lang ); } - /** */ - function isLocalVHost( $vhost ) { + /** + * Returns true if the given vhost is handled locally. + * @param $vhost String + * @return bool + */ + public function isLocalVHost( $vhost ) { return in_array( $vhost, $this->localVHosts ); } + + /** + * Merge multiple arrays together. + * On encountering duplicate keys, merge the two, but ONLY if they're arrays. + * PHP's array_merge_recursive() merges ANY duplicate values into arrays, + * which is not fun + */ + static function arrayMerge( $array1/* ... */ ) { + $out = $array1; + for( $i=1; $i < func_num_args(); $i++ ) { + foreach( func_get_arg( $i ) as $key => $value ) { + if ( isset($out[$key]) && is_array($out[$key]) && is_array($value) ) { + $out[$key] = self::arrayMerge( $out[$key], $value ); + } elseif ( !isset($out[$key]) || !$out[$key] && !is_numeric($key) ) { + // Values that evaluate to true given precedence, for the primary purpose of merging permissions arrays. + $out[$key] = $value; + } elseif ( is_numeric( $key ) ) { + $out[] = $value; + } + } + } + + return $out; + } } } diff --git a/includes/SiteStats.php b/includes/SiteStats.php index 3b10f4a0..ab0caa7e 100644 --- a/includes/SiteStats.php +++ b/includes/SiteStats.php @@ -7,6 +7,7 @@ class SiteStats { static $row, $loaded = false; static $admins, $jobs; static $pageCount = array(); + static $groupMemberCounts = array(); static function recache() { self::load( true ); @@ -92,18 +93,44 @@ class SiteStats { self::load(); return self::$row->ss_users; } + + static function activeUsers() { + self::load(); + return self::$row->ss_active_users; + } static function images() { self::load(); return self::$row->ss_images; } + /** + * @deprecated Use self::numberingroup('sysop') instead + */ static function admins() { - if ( !isset( self::$admins ) ) { - $dbr = wfGetDB( DB_SLAVE ); - self::$admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), __METHOD__ ); + wfDeprecated(__METHOD__); + return self::numberingroup('sysop'); + } + + /** + * Find the number of users in a given user group. + * @param string $group Name of group + * @return int + */ + static function numberingroup($group) { + if ( !isset( self::$groupMemberCounts[$group] ) ) { + global $wgMemc; + $key = wfMemcKey( 'SiteStats', 'groupcounts', $group ); + $hit = $wgMemc->get( $key ); + if ( !$hit ) { + $dbr = wfGetDB( DB_SLAVE ); + $hit = $dbr->selectField( 'user_groups', 'COUNT(*)', + array( 'ug_group' => $group ), __METHOD__ ); + $wgMemc->set( $key, $hit, 3600 ); + } + self::$groupMemberCounts[$group] = $hit; } - return self::$admins; + return self::$groupMemberCounts[$group]; } static function jobs() { @@ -185,54 +212,35 @@ class SiteStatsUpdate { $fname = 'SiteStatsUpdate::doUpdate'; $dbw = wfGetDB( DB_MASTER ); - # First retrieve the row just to find out which schema we're in - $row = $dbw->selectRow( 'site_stats', '*', false, $fname ); - $updates = ''; $this->appendUpdate( $updates, 'ss_total_views', $this->mViews ); $this->appendUpdate( $updates, 'ss_total_edits', $this->mEdits ); $this->appendUpdate( $updates, 'ss_good_articles', $this->mGood ); + $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages ); + $this->appendUpdate( $updates, 'ss_users', $this->mUsers ); - if ( isset( $row->ss_total_pages ) ) { - # Update schema if required - if ( $row->ss_total_pages == -1 && !$this->mViews ) { - $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') ); - list( $page, $user ) = $dbr->tableNamesN( 'page', 'user' ); - - $sql = "SELECT COUNT(page_namespace) AS total FROM $page"; - $res = $dbr->query( $sql, $fname ); - $pageRow = $dbr->fetchObject( $res ); - $pages = $pageRow->total + $this->mPages; - - $sql = "SELECT COUNT(user_id) AS total FROM $user"; - $res = $dbr->query( $sql, $fname ); - $userRow = $dbr->fetchObject( $res ); - $users = $userRow->total + $this->mUsers; - - if ( $updates ) { - $updates .= ','; - } - $updates .= "ss_total_pages=$pages, ss_users=$users"; - } else { - $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages ); - $this->appendUpdate( $updates, 'ss_users', $this->mUsers ); - } - } if ( $updates ) { $site_stats = $dbw->tableName( 'site_stats' ); $sql = $dbw->limitResultForUpdate("UPDATE $site_stats SET $updates", 1); + + # Need a separate transaction because this a global lock $dbw->begin(); $dbw->query( $sql, $fname ); $dbw->commit(); } - - /* - global $wgDBname, $wgTitle; - if ( $this->mGood && $wgDBname == 'enwiki' ) { - $good = $dbw->selectField( 'site_stats', 'ss_good_articles', '', $fname ); - error_log( $good . ' ' . $wgTitle->getPrefixedDBkey() . "\n", 3, '/home/wikipedia/logs/million.log' ); - } - */ + } + + public static function cacheUpdate( $dbw ) { + $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') ); + # Get non-bot users than did some recent action other than making accounts. + # If account creation is included, the number gets inflated ~20+ fold on enwiki. + $activeUsers = $dbr->selectField( 'recentchanges', 'COUNT( DISTINCT rc_user_text )', + array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers' OR rc_log_type IS NULL" ), + __METHOD__ ); + $dbw->update( 'site_stats', + array( 'ss_active_users' => intval($activeUsers) ), + array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 ) + ); } } diff --git a/includes/Skin.php b/includes/Skin.php index a9e44ab4..636b96bf 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -60,6 +60,21 @@ class Skin extends Linker { } return $wgValidSkinNames; } + + /** + * Fetch the list of usable skins in regards to $wgSkipSkins. + * Useful for Special:Preferences and other places where you + * only want to show skins users _can_ use. + * @return array of strings + */ + public static function getUsableSkins() { + global $wgSkipSkins; + $usableSkins = self::getSkinNames(); + foreach ( $wgSkipSkins as $skip ) { + unset( $usableSkins[$skip] ); + } + return $usableSkins; + } /** * Normalize a skin preference value to a form that can be loaded. @@ -156,24 +171,28 @@ class Skin extends Linker { return $q; } - function initPage( &$out ) { - global $wgFavicon, $wgAppleTouchIcon, $wgScriptPath, $wgScriptExtension; + function initPage( OutputPage $out ) { + global $wgFavicon, $wgAppleTouchIcon; wfProfileIn( __METHOD__ ); - if( false !== $wgFavicon ) { - $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) ); - } - + # Generally the order of the favicon and apple-touch-icon links + # should not matter, but Konqueror (3.5.9 at least) incorrectly + # uses whichever one appears later in the HTML source. Make sure + # apple-touch-icon is specified first to avoid this. if( false !== $wgAppleTouchIcon ) { $out->addLink( array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) ); } + if( false !== $wgFavicon ) { + $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) ); + } + # OpenSearch description link $out->addLink( array( 'rel' => 'search', 'type' => 'application/opensearchdescription+xml', - 'href' => "$wgScriptPath/opensearch_desc{$wgScriptExtension}", + 'href' => wfScript( 'opensearch_desc' ), 'title' => wfMsgForContent( 'opensearch-desc' ), )); @@ -208,7 +227,7 @@ class Skin extends Linker { $lb->execute(); } - function addMetadataLinks( &$out ) { + function addMetadataLinks( OutputPage $out ) { global $wgTitle, $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf; global $wgRightsPage, $wgRightsUrl; @@ -244,13 +263,25 @@ class Skin extends Linker { } } - function outputPage( &$out ) { - global $wgDebugComments; + function setMembers(){ + global $wgTitle, $wgUser; + $this->mTitle = $wgTitle; + $this->mUser = $wgUser; + $this->userpage = $wgUser->getUserPage()->getPrefixedText(); + $this->usercss = false; + } + function outputPage( OutputPage $out ) { + global $wgDebugComments; wfProfileIn( __METHOD__ ); + + $this->setMembers(); $this->initPage( $out ); - $out->out( $out->headElement() ); + // See self::afterContentHook() for documentation + $afterContent = $this->afterContentHook(); + + $out->out( $out->headElement( $this ) ); $out->out( "\n<body" ); $ops = $this->getBodyOptions(); @@ -268,6 +299,8 @@ class Skin extends Linker { $out->out( $out->mBodytext . "\n" ); $out->out( $this->afterContent() ); + + $out->out( $afterContent ); $out->out( $this->bottomScripts() ); @@ -280,14 +313,14 @@ class Skin extends Linker { static function makeVariablesScript( $data ) { global $wgJsMimeType; - $r = "<script type= \"$wgJsMimeType\">/*<![CDATA[*/\n"; + $r = array( "<script type= \"$wgJsMimeType\">/*<![CDATA[*/" ); foreach ( $data as $name => $value ) { $encValue = Xml::encodeJsVar( $value ); - $r .= "var $name = $encValue;\n"; + $r[] = "var $name = $encValue;"; } - $r .= "/*]]>*/</script>\n"; + $r[] = "/*]]>*/</script>\n"; - return $r; + return implode( "\n\t\t", $r ); } /** @@ -308,6 +341,18 @@ class Skin extends Linker { $ns = $wgTitle->getNamespace(); $nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText(); + $separatorTransTable = $wgContLang->separatorTransformTable(); + $separatorTransTable = $separatorTransTable ? $separatorTransTable : array(); + $compactSeparatorTransTable = array( + implode( "\t", array_keys( $separatorTransTable ) ), + implode( "\t", $separatorTransTable ), + ); + $digitTransTable = $wgContLang->digitTransformTable(); + $digitTransTable = $digitTransTable ? $digitTransTable : array(); + $compactDigitTransTable = array( + implode( "\t", array_keys( $digitTransTable ) ), + implode( "\t", $digitTransTable ), + ); $vars = array( 'skin' => $data['skinname'], @@ -316,7 +361,7 @@ class Skin extends Linker { 'wgScriptPath' => $wgScriptPath, 'wgScript' => $wgScript, 'wgVariantArticlePath' => $wgVariantArticlePath, - 'wgActionPaths' => $wgActionPaths, + 'wgActionPaths' => (object)$wgActionPaths, 'wgServer' => $wgServer, 'wgCanonicalNamespace' => $nsname, 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBkey() ), @@ -335,6 +380,8 @@ class Skin extends Linker { 'wgVersion' => $wgVersion, 'wgEnableAPI' => $wgEnableAPI, 'wgEnableWriteAPI' => $wgEnableWriteAPI, + 'wgSeparatorTransformTable' => $compactSeparatorTransTable, + 'wgDigitTransformTable' => $compactDigitTransTable, ); if( $wgUseAjax && $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false )){ @@ -362,32 +409,34 @@ class Skin extends Linker { $vars['wgAjaxWatch'] = $msgs; } + wfRunHooks('MakeGlobalVariablesScript', array(&$vars)); + return self::makeVariablesScript( $vars ); } function getHeadScripts( $allowUserJs ) { global $wgStylePath, $wgUser, $wgJsMimeType, $wgStyleVersion; - $r = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) ); + $vars = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) ); - $r .= "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js?$wgStyleVersion\"></script>\n"; + $r = array( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js?$wgStyleVersion\"></script>" ); global $wgUseSiteJs; if ($wgUseSiteJs) { $jsCache = $wgUser->isLoggedIn() ? '&smaxage=0' : ''; - $r .= "<script type=\"$wgJsMimeType\" src=\"". + $r[] = "<script type=\"$wgJsMimeType\" src=\"". htmlspecialchars(self::makeUrl('-', "action=raw$jsCache&gen=js&useskin=" . urlencode( $this->getSkinName() ) ) ) . - "\"><!-- site js --></script>\n"; + "\"><!-- site js --></script>"; } if( $allowUserJs && $wgUser->isLoggedIn() ) { $userpage = $wgUser->getUserPage(); $userjs = htmlspecialchars( self::makeUrl( $userpage->getPrefixedText().'/'.$this->getSkinName().'.js', 'action=raw&ctype='.$wgJsMimeType)); - $r .= '<script type="'.$wgJsMimeType.'" src="'.$userjs."\"></script>\n"; + $r[] = '<script type="'.$wgJsMimeType.'" src="'.$userjs."\"></script>"; } - return $r; + return $vars . "\t\t" . implode ( "\n\t\t", $r ); } /** @@ -414,38 +463,24 @@ class Skin extends Linker { $wgRequest->getVal( 'wpEditToken' ) ); } - # get the user/site-specific stylesheet, SkinTemplate loads via RawPage.php (settings are cached that way) - function getUserStylesheet() { - global $wgStylePath, $wgRequest, $wgContLang, $wgSquidMaxage, $wgStyleVersion; - $sheet = $this->getStylesheet(); - $s = "@import \"$wgStylePath/common/shared.css?$wgStyleVersion\";\n"; - $s .= "@import \"$wgStylePath/common/oldshared.css?$wgStyleVersion\";\n"; - $s .= "@import \"$wgStylePath/$sheet?$wgStyleVersion\";\n"; - if($wgContLang->isRTL()) $s .= "@import \"$wgStylePath/common/common_rtl.css?$wgStyleVersion\";\n"; - - $query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; - $s .= '@import "' . self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) . "\";\n" . - '@import "' . self::makeNSUrl( ucfirst( $this->getSkinName() . '.css' ), $query, NS_MEDIAWIKI ) . "\";\n"; - - $s .= $this->doGetUserStyles(); - return $s."\n"; - } - /** - * This returns MediaWiki:Common.js, and derived classes may add other JS. - * Despite its name, it does *not* return any custom user JS from user - * subpages. The returned script is sitewide and publicly cacheable and - * therefore must not include anything that varies according to user, - * interface language, etc. (although it may vary by skin). See - * makeGlobalVariablesScript for things that can vary per page view and are - * not cacheable. + * generated JavaScript action=raw&gen=js + * This returns MediaWiki:Common.js and MediaWiki:[Skinname].js concate- + * nated together. For some bizarre reason, it does *not* return any + * custom user JS from subpages. Huh? + * + * There's absolutely no reason to have separate Monobook/Common JSes. + * Any JS that cares can just check the skin variable generated at the + * top. For now Monobook.js will be maintained, but it should be consi- + * dered deprecated. * - * @return string Raw JavaScript to be returned + * @return string */ - public function getUserJs() { + public function generateUserJs() { + global $wgStylePath; + wfProfileIn( __METHOD__ ); - global $wgStylePath; $s = "/* generated javascript */\n"; $s .= "var skin = '" . Xml::escapeJsString( $this->getSkinName() ) . "';\n"; $s .= "var stylepath = '" . Xml::escapeJsString( $wgStylePath ) . "';"; @@ -454,45 +489,35 @@ class Skin extends Linker { if ( !wfEmptyMsg ( 'common.js', $commonJs ) ) { $s .= $commonJs; } + + $s .= "\n\n/* MediaWiki:".ucfirst( $this->getSkinName() ).".js */\n"; + // avoid inclusion of non defined user JavaScript (with custom skins only) + // by checking for default message content + $msgKey = ucfirst( $this->getSkinName() ).'.js'; + $userJS = wfMsgForContent($msgKey); + if ( !wfEmptyMsg( $msgKey, $userJS ) ) { + $s .= $userJS; + } + wfProfileOut( __METHOD__ ); return $s; } /** - * Return html code that include User stylesheets + * generate user stylesheet for action=raw&gen=css */ - function getUserStyles() { - $s = "<style type='text/css'>\n"; - $s .= "/*/*/ /*<![CDATA[*/\n"; # <-- Hide the styles from Netscape 4 without hiding them from IE/Mac - $s .= $this->getUserStylesheet(); - $s .= "/*]]>*/ /* */\n"; - $s .= "</style>\n"; + public function generateUserStylesheet() { + wfProfileIn( __METHOD__ ); + $s = "/* generated user stylesheet */\n" . + $this->reallyGenerateUserStylesheet(); + wfProfileOut( __METHOD__ ); return $s; } - + /** - * Some styles that are set by user through the user settings interface. + * Split for easier subclassing in SkinSimple, SkinStandard and SkinCologneBlue */ - function doGetUserStyles() { - global $wgUser, $wgUser, $wgRequest, $wgTitle, $wgAllowUserCss; - - $s = ''; - - if( $wgAllowUserCss && $wgUser->isLoggedIn() ) { # logged in - if($wgTitle->isCssSubpage() && $this->userCanPreview( $wgRequest->getText( 'action' ) ) ) { - $s .= $wgRequest->getText('wpTextbox1'); - } else { - $userpage = $wgUser->getUserPage(); - $s.= '@import "'.self::makeUrl( - $userpage->getPrefixedText().'/'.$this->getSkinName().'.css', - 'action=raw&ctype=text/css').'";'."\n"; - } - } - - return $s . $this->reallyDoGetUserStyles(); - } - - function reallyDoGetUserStyles() { + protected function reallyGenerateUserStylesheet(){ global $wgUser; $s = ''; if (($undopt = $wgUser->getOption("underline")) < 2) { @@ -529,6 +554,86 @@ END; return $s; } + /** + * @private + */ + function setupUserCss( OutputPage $out ) { + global $wgRequest, $wgContLang, $wgUser; + global $wgAllowUserCss, $wgUseSiteCss, $wgSquidMaxage, $wgStylePath; + + wfProfileIn( __METHOD__ ); + + $this->setupSkinUserCss( $out ); + + $siteargs = array( + 'action' => 'raw', + 'maxage' => $wgSquidMaxage, + ); + + // Add any extension CSS + foreach( $out->getExtStyle() as $tag ) { + $out->addStyle( $tag['href'] ); + } + + // If we use the site's dynamic CSS, throw that in, too + // Per-site custom styles + if( $wgUseSiteCss ) { + global $wgHandheldStyle; + $query = wfArrayToCGI( array( + 'usemsgcache' => 'yes', + 'ctype' => 'text/css', + 'smaxage' => $wgSquidMaxage + ) + $siteargs ); + # Site settings must override extension css! (bug 15025) + $out->addStyle( self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) ); + $out->addStyle( self::makeNSUrl( 'Print.css', $query, NS_MEDIAWIKI ), 'print' ); + if( $wgHandheldStyle ) { + $out->addStyle( self::makeNSUrl( 'Handheld.css', $query, NS_MEDIAWIKI ), 'handheld' ); + } + $out->addStyle( self::makeNSUrl( $this->getSkinName() . '.css', $query, NS_MEDIAWIKI ) ); + } + + if( $wgUser->isLoggedIn() ) { + // Ensure that logged-in users' generated CSS isn't clobbered + // by anons' publicly cacheable generated CSS. + $siteargs['smaxage'] = '0'; + $siteargs['ts'] = $wgUser->mTouched; + } + // Per-user styles based on preferences + $siteargs['gen'] = 'css'; + if( ( $us = $wgRequest->getVal( 'useskin', '' ) ) !== '' ) { + $siteargs['useskin'] = $us; + } + $out->addStyle( self::makeUrl( '-', wfArrayToCGI( $siteargs ) ) ); + + // Per-user custom style pages + if( $wgAllowUserCss && $wgUser->isLoggedIn() ) { + $action = $wgRequest->getVal('action'); + # If we're previewing the CSS page, use it + if( $this->mTitle->isCssSubpage() && $this->userCanPreview( $action ) ) { + $previewCss = $wgRequest->getText('wpTextbox1'); + // @FIXME: properly escape the cdata! + $this->usercss = "/*<![CDATA[*/\n" . $previewCss . "/*]]>*/"; + } else { + $out->addStyle( self::makeUrl($this->userpage . '/' . $this->getSkinName() .'.css', + 'action=raw&ctype=text/css' ) ); + } + } + + wfProfileOut( __METHOD__ ); + } + + /** + * Add skin specific stylesheets + * @param $out OutputPage + */ + function setupSkinUserCss( OutputPage $out ) { + $out->addStyle( 'common/shared.css' ); + $out->addStyle( 'common/oldshared.css' ); + $out->addStyle( $this->getStylesheet() ); + $out->addStyle( 'common/common_rtl.css', '', '', 'rtl' ); + } + function getBodyOptions() { global $wgUser, $wgTitle, $wgOut, $wgRequest, $wgContLang; @@ -539,19 +644,33 @@ END; } else $a = array( 'bgcolor' => '#FFFFFF' ); if($wgOut->isArticle() && $wgUser->getOption('editondblclick') && - $wgTitle->userCan( 'edit' ) ) { + $wgTitle->quickUserCan( 'edit' ) ) { $s = $wgTitle->getFullURL( $this->editUrlOptions() ); - $s = 'document.location = "' .wfEscapeJSString( $s ) .'";'; + $s = 'document.location = "' .Xml::escapeJsString( $s ) .'";'; $a += array ('ondblclick' => $s); } $a['onload'] = $wgOut->getOnloadHandler(); $a['class'] = - 'mediawiki ns-'.$wgTitle->getNamespace(). - ' '.($wgContLang->isRTL() ? "rtl" : "ltr"). - ' '.Sanitizer::escapeClass( 'page-'.$wgTitle->getPrefixedText() ); + 'mediawiki' . + ' '.( $wgContLang->isRTL() ? "rtl" : "ltr" ). + ' '.$this->getPageClasses( $wgTitle ) . + ' skin-'. Sanitizer::escapeClass( $this->getSkinName( ) ); return $a; } + + function getPageClasses( $title ) { + $numeric = 'ns-'.$title->getNamespace(); + if( $title->getNamespace() == NS_SPECIAL ) { + $type = "ns-special"; + } elseif( $title->isTalkPage() ) { + $type = "ns-talk"; + } else { + $type = "ns-subject"; + } + $name = Sanitizer::escapeClass( 'page-'.$title->getPrefixedText() ); + return "$numeric $type $name"; + } /** * URL to the logo @@ -589,11 +708,11 @@ END; $s .= "\n<div id='content'>\n<div id='topbar'>\n" . "<table border='0' cellspacing='0' width='98%'>\n<tr>\n"; - $shove = ($qb != 0); - $left = ($qb == 1 || $qb == 3); - if($wgContLang->isRTL()) $left = !$left; + $shove = ( $qb != 0 ); + $left = ( $qb == 1 || $qb == 3 ); + if( $wgContLang->isRTL() ) $left = !$left; - if ( !$shove ) { + if( !$shove ) { $s .= "<td class='top' align='left' valign='top' rowspan='{$rows}'>\n" . $this->logoText() . '</td>'; } elseif( $left ) { @@ -655,7 +774,7 @@ END; $msg = wfMsgExt( 'pagecategories', array( 'parsemag', 'escapenoentities' ), count( $allCats['normal'] ) ); $s .= '<div id="mw-normal-catlinks">' . - $this->makeLinkObj( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg ) + $this->link( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg ) . $colon . $t . '</div>'; } @@ -676,7 +795,7 @@ END; # optional 'dmoz-like' category browser. Will be shown under the list # of categories an article belong to - if($wgUseCategoryBrowser) { + if( $wgUseCategoryBrowser ){ $s .= '<br /><hr />'; # get a big array of the parents tree @@ -699,7 +818,7 @@ END; * @param &skin Object: skin passed by reference * @return String separated by >, terminate with "\n" */ - function drawCategoryBrowser($tree, &$skin) { + function drawCategoryBrowser( $tree, &$skin ){ $return = ''; foreach ($tree as $element => $parent) { if (empty($parent)) { @@ -710,8 +829,8 @@ END; $return .= Skin::drawCategoryBrowser($parent, $skin) . ' > '; } # add our current element to the list - $eltitle = Title::NewFromText($element); - $return .= $skin->makeLinkObj( $eltitle, $eltitle->getText() ) ; + $eltitle = Title::newFromText($element); + $return .= $skin->link( $eltitle, $eltitle->getText() ) ; } return $return; } @@ -736,8 +855,43 @@ END; } /** - * This gets called shortly before the \</body\> tag. - * @return String HTML to be put before \</body\> + * This runs a hook to allow extensions placing their stuff after content + * and article metadata (e.g. categories). + * Note: This function has nothing to do with afterContent(). + * + * This hook is placed here in order to allow using the same hook for all + * skins, both the SkinTemplate based ones and the older ones, which directly + * use this class to get their data. + * + * The output of this function gets processed in SkinTemplate::outputPage() for + * the SkinTemplate based skins, all other skins should directly echo it. + * + * Returns an empty string by default, if not changed by any hook function. + */ + protected function afterContentHook() { + $data = ""; + + if( wfRunHooks( 'SkinAfterContent', array( &$data ) ) ){ + // adding just some spaces shouldn't toggle the output + // of the whole <div/>, so we use trim() here + if( trim( $data ) != '' ){ + // Doing this here instead of in the skins to + // ensure that the div has the same ID in all + // skins + $data = "<div id='mw-data-after-content'>\n" . + "\t$data\n" . + "</div>\n"; + } + } else { + wfDebug( "Hook SkinAfterContent changed output processing.\n" ); + } + + return $data; + } + + /** + * This gets called shortly before the </body> tag. + * @return String HTML to be put before </body> */ function afterContent() { $printfooter = "<div class=\"printfooter\">\n" . $this->printFooter() . "</div>\n"; @@ -745,8 +899,8 @@ END; } /** - * This gets called shortly before the \</body\> tag. - * @return String HTML-wrapped JS code to be put before \</body\> + * This gets called shortly before the </body> tag. + * @return String HTML-wrapped JS code to be put before </body> */ function bottomScripts() { global $wgJsMimeType; @@ -768,7 +922,7 @@ END; } /** overloaded by derived classes */ - function doAfterContent() { } + function doAfterContent() { return "</div></div>"; } function pageTitleLinks() { global $wgOut, $wgTitle, $wgUser, $wgRequest; @@ -788,7 +942,7 @@ END; } if ( $wgOut->isArticleRelated() ) { - if ( $wgTitle->getNamespace() == NS_IMAGE ) { + if ( $wgTitle->getNamespace() == NS_FILE ) { $name = $wgTitle->getDBkey(); $image = wfFindFile( $wgTitle ); if( $image ) { @@ -859,7 +1013,7 @@ END; function pageTitle() { global $wgOut; - $s = '<h1 class="pagetitle">' . htmlspecialchars( $wgOut->getPageTitle() ) . '</h1>'; + $s = '<h1 class="pagetitle">' . $wgOut->getPageTitle() . '</h1>'; return $s; } @@ -869,7 +1023,7 @@ END; $sub = $wgOut->getSubtitle(); if ( '' == $sub ) { global $wgExtraSubtitle; - $sub = wfMsg( 'tagline' ) . $wgExtraSubtitle; + $sub = wfMsgExt( 'tagline', 'parsemag' ) . $wgExtraSubtitle; } $subpages = $this->subPageSubtitle(); $sub .= !empty($subpages)?"</p><p class='subpages'>$subpages":''; @@ -926,50 +1080,54 @@ END; function nameAndLogin() { global $wgUser, $wgTitle, $wgLang, $wgContLang; - $lo = $wgContLang->specialPage( 'Userlogout' ); + $logoutPage = $wgContLang->specialPage( 'Userlogout' ); - $s = ''; + $ret = ''; if ( $wgUser->isAnon() ) { if( $this->showIPinHeader() ) { - $n = wfGetIP(); + $name = wfGetIP(); - $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), - $wgLang->getNsText( NS_TALK ) ); + $talkLink = $this->link( $wgUser->getTalkPage(), + $wgLang->getNsText( NS_TALK ) ); - $s .= $n . ' ('.$tl.')'; + $ret .= "$name ($talkLink)"; } else { - $s .= wfMsg('notloggedin'); + $ret .= wfMsg( 'notloggedin' ); } - $rt = $wgTitle->getPrefixedURL(); - if ( 0 == strcasecmp( urlencode( $lo ), $rt ) ) { - $q = ''; - } else { $q = "returnto={$rt}"; } + $returnTo = $wgTitle->getPrefixedDBkey(); + $query = array(); + if ( $logoutPage != $returnTo ) { + $query['returnto'] = $returnTo; + } $loginlink = $wgUser->isAllowed( 'createaccount' ) ? 'nav-login-createaccount' : 'login'; - $s .= "\n<br />" . $this->makeKnownLinkObj( + $ret .= "\n<br />" . $this->link( SpecialPage::getTitleFor( 'Userlogin' ), - wfMsg( $loginlink ), $q ); + wfMsg( $loginlink ), array(), $query + ); } else { - $n = $wgUser->getName(); - $rt = $wgTitle->getPrefixedURL(); - $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), - $wgLang->getNsText( NS_TALK ) ); - - $tl = " ({$tl})"; - - $s .= $this->makeKnownLinkObj( $wgUser->getUserPage(), - $n ) . "{$tl}<br />" . - $this->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ), - "returnto={$rt}" ) . ' | ' . - $this->specialLink( 'preferences' ); + $returnTo = $wgTitle->getPrefixedDBkey(); + $talkLink = $this->link( $wgUser->getTalkPage(), + $wgLang->getNsText( NS_TALK ) ); + + $ret .= $this->link( $wgUser->getUserPage(), + htmlspecialchars( $wgUser->getName() ) ); + $ret .= " ($talkLink)<br />"; + $ret .= $this->link( + SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ), + array(), array( 'returnto' => $returnTo ) + ); + $ret .= ' | ' . $this->specialLink( 'preferences' ); } - $s .= ' | ' . $this->makeKnownLink( wfMsgForContent( 'helppage' ), - wfMsg( 'help' ) ); + $ret .= ' | ' . $this->link( + Title::newFromText( wfMsgForContent( 'helppage' ) ), + wfMsg( 'help' ) + ); - return $s; + return $ret; } function getSearchLink() { @@ -1107,6 +1265,7 @@ END; $oldid = $wgRequest->getVal( 'oldid' ); $diff = $wgRequest->getVal( 'diff' ); if ( ! $wgOut->isArticle() ) { return ''; } + if( !$wgArticle instanceOf Article ) { return ''; } if ( isset( $oldid ) || isset( $diff ) ) { return ''; } if ( 0 == $wgArticle->getID() ) { return ''; } @@ -1118,14 +1277,13 @@ END; } } - if (isset($wgMaxCredits) && $wgMaxCredits != 0) { - require_once('Credits.php'); - $s .= ' ' . getCredits($wgArticle, $wgMaxCredits, $wgShowCreditsIfMax); + if( $wgMaxCredits != 0 ){ + $s .= ' ' . Credits::getCredits( $wgArticle, $wgMaxCredits, $wgShowCreditsIfMax ); } else { - $s .= $this->lastModified(); + $s .= $this->lastModified(); } - if ($wgPageShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) { + if( $wgPageShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' ) ) { $dbr = wfGetDB( DB_SLAVE ); $watchlist = $dbr->tableName( 'watchlist' ); $sql = "SELECT COUNT(*) AS n FROM $watchlist @@ -1143,13 +1301,12 @@ END; } function getCopyright( $type = 'detect' ) { - global $wgRightsPage, $wgRightsUrl, $wgRightsText, $wgRequest; + global $wgRightsPage, $wgRightsUrl, $wgRightsText, $wgRequest, $wgArticle; if ( $type == 'detect' ) { - $oldid = $wgRequest->getVal( 'oldid' ); $diff = $wgRequest->getVal( 'diff' ); - - if ( !is_null( $oldid ) && is_null( $diff ) && wfMsgForContent( 'history_copyright' ) !== '-' ) { + $isCur = $wgArticle && $wgArticle->isCurrent(); + if ( is_null( $diff ) && !$isCur && wfMsgForContent( 'history_copyright' ) !== '-' ) { $type = 'history'; } else { $type = 'normal'; @@ -1167,6 +1324,8 @@ END; $link = $this->makeKnownLink( $wgRightsPage, $wgRightsText ); } elseif( $wgRightsUrl ) { $link = $this->makeExternalLink( $wgRightsUrl, $wgRightsText ); + } elseif( $wgRightsText ) { + $link = $wgRightsText; } else { # Give up now return $out; @@ -1205,7 +1364,7 @@ END; function lastModified() { global $wgLang, $wgArticle; if( $this->mRevisionId ) { - $timestamp = Revision::getTimestampFromId( $this->mRevisionId, $wgArticle->getId() ); + $timestamp = Revision::getTimestampFromId( $wgArticle->getTitle(), $this->mRevisionId ); } else { $timestamp = $wgArticle->getTimestamp(); } @@ -1249,7 +1408,7 @@ END; $sp = wfMsg( 'specialpages' ); $spp = $wgContLang->specialPage( 'Specialpages' ); - $s = '<form id="specialpages" method="get" class="inline" ' . + $s = '<form id="specialpages" method="get" ' . 'action="' . htmlspecialchars( "{$wgServer}{$wgRedirectScript}" ) . "\">\n"; $s .= "<select name=\"wpDropdown\">\n"; $s .= "<option value=\"{$spp}\">{$sp}</option>\n"; @@ -1308,9 +1467,9 @@ END; if ( !$wgOut->isArticleRelated() ) { $s = wfMsg( 'protectedpage' ); } else { - if( $wgTitle->userCan( 'edit' ) && $wgTitle->exists() ) { + if( $wgTitle->quickUserCan( 'edit' ) && $wgTitle->exists() ) { $t = wfMsg( 'editthispage' ); - } elseif( $wgTitle->userCan( 'create' ) && !$wgTitle->exists() ) { + } elseif( $wgTitle->quickUserCan( 'create' ) && !$wgTitle->exists() ) { $t = wfMsg( 'create-this-page' ); } else { $t = wfMsg( 'viewsource' ); @@ -1395,7 +1554,7 @@ END; function moveThisPage() { global $wgTitle; - if ( $wgTitle->userCan( 'move' ) ) { + if ( $wgTitle->quickUserCan( 'move' ) ) { return $this->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), wfMsg( 'movethispage' ), 'target=' . $wgTitle->getPrefixedURL() ); } else { @@ -1428,14 +1587,10 @@ END; } function showEmailUser( $id ) { - global $wgEnableEmail, $wgEnableUserEmail, $wgUser; - return $wgEnableEmail && - $wgEnableUserEmail && - $wgUser->isLoggedIn() && # show only to signed in users - 0 != $id; # we can only email to non-anons .. -# '' != $id->getEmail() && # who must have an email address stored .. -# 0 != $id->getEmailauthenticationtimestamp() && # .. which is authenticated -# 1 != $wgUser->getOption('disablemail'); # and not disabled + global $wgUser; + $targetUser = User::newFromId( $id ); + return $wgUser->canSendEmail() && # the sending user must have a confirmed email address + $targetUser->canReceiveEmail(); # the target user must have a confirmed email address and allow emails from users } function emailUserLink() { @@ -1496,12 +1651,6 @@ END; return $s; } - function bugReportsLink() { - $s = $this->makeKnownLink( wfMsgForContent( 'bugreportspage' ), - wfMsg( 'bugreports' ) ); - return $s; - } - function talkLink() { global $wgTitle; @@ -1510,6 +1659,8 @@ END; return ''; } + $linkOptions = array(); + if( $wgTitle->isTalkPage() ) { $link = $wgTitle->getSubjectPage(); switch( $link->getNamespace() ) { @@ -1522,8 +1673,11 @@ END; case NS_PROJECT: $text = wfMsg( 'projectpage' ); break; - case NS_IMAGE: + case NS_FILE: $text = wfMsg( 'imagepage' ); + # Make link known if image exists, even if the desc. page doesn't. + if( wfFindFile( $link ) ) + $linkOptions[] = 'known'; break; case NS_MEDIAWIKI: $text = wfMsg( 'mediawikipage' ); @@ -1545,7 +1699,7 @@ END; $text = wfMsg( 'talkpage' ); } - $s = $this->makeLinkObj( $link, $text ); + $s = $this->link( $link, $text, array(), array(), $linkOptions ); return $s; } @@ -1677,19 +1831,17 @@ END; continue; if (strpos($line, '**') !== 0) { $line = trim($line, '* '); - if ( $line == 'SEARCH' || $line == 'TOOLBOX' || $line == 'LANGUAGES' ) { - # Special box type - $bar[$line] = array(); - } else { - $heading = $line; - } + $heading = $line; + if( !array_key_exists($heading, $bar) ) $bar[$heading] = array(); } else { if (strpos($line, '|') !== false) { // sanity check $line = array_map('trim', explode( '|' , trim($line, '* '), 2 ) ); $link = wfMsgForContent( $line[0] ); if ($link == '-') continue; - if (wfEmptyMsg($line[1], $text = wfMsg($line[1]))) + + $text = wfMsgExt($line[1], 'parsemag'); + if (wfEmptyMsg($line[1], $text)) $text = $line[1]; if (wfEmptyMsg($line[0], $link)) $link = $line[0]; @@ -1715,6 +1867,7 @@ END; } else { continue; } } } + wfRunHooks('SkinBuildSidebar', array($this, &$bar)); if ( $wgEnableSidebarCache ) $parserMemc->set( $key, $bar, $wgSidebarCacheExpiry ); wfProfileOut( __METHOD__ ); return $bar; diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php index 506d1024..4f13571a 100644 --- a/includes/SkinTemplate.php +++ b/includes/SkinTemplate.php @@ -87,12 +87,6 @@ class SkinTemplate extends Skin { */ var $template; - /** - * An array of strings representing extra CSS files to load. May include: - * 'IE', 'IE50', 'IE55', 'IE60', 'IE70', 'rtl'. - */ - var $cssfiles; - /**#@-*/ /** @@ -100,14 +94,23 @@ class SkinTemplate extends Skin { * Child classes should override this to set the name, * style subdirectory, and template filler callback. * - * @param OutputPage $out + * @param $out OutputPage */ - function initPage( &$out ) { + function initPage( OutputPage $out ) { parent::initPage( $out ); $this->skinname = 'monobook'; $this->stylename = 'monobook'; $this->template = 'QuickTemplate'; - $this->cssfiles = array(); + } + + /** + * Add specific styles for this skin + * + * @param $out OutputPage + */ + function setupSkinUserCss( OutputPage $out ){ + $out->addStyle( 'common/shared.css', 'screen' ); + $out->addStyle( 'common/commonPrint.css', 'print' ); } /** @@ -115,9 +118,9 @@ class SkinTemplate extends Skin { * and eventually it spits out some HTML. Should have interface * roughly equivalent to PHPTAL 0.7. * - * @param string $callback (or file) - * @param string $repository subdirectory where we keep template files - * @param string $cache_dir + * @param $callback string (or file) + * @param $repository string: subdirectory where we keep template files + * @param $cache_dir string * @return object * @private */ @@ -128,18 +131,17 @@ class SkinTemplate extends Skin { /** * initialize various variables and generate the template * - * @param OutputPage $out - * @public + * @param $out OutputPage */ - function outputPage( &$out ) { - global $wgTitle, $wgArticle, $wgUser, $wgLang, $wgContLang, $wgOut; + function outputPage( OutputPage $out ) { + global $wgTitle, $wgArticle, $wgUser, $wgLang, $wgContLang; global $wgScript, $wgStylePath, $wgContLanguageCode; global $wgMimeType, $wgJsMimeType, $wgOutputEncoding, $wgRequest; global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces; global $wgDisableCounters, $wgLogo, $action, $wgFeedClasses, $wgHideInterlanguageLinks; global $wgMaxCredits, $wgShowCreditsIfMax; global $wgPageShowWatchingUsers; - global $wgUseTrackbacks; + global $wgUseTrackbacks, $wgUseSiteJs; global $wgArticlePath, $wgScriptPath, $wgServer, $wgLang, $wgCanonicalNamespaceNames; wfProfileIn( __METHOD__ ); @@ -150,9 +152,7 @@ class SkinTemplate extends Skin { wfProfileIn( __METHOD__."-init" ); $this->initPage( $out ); - $this->mTitle =& $wgTitle; - $this->mUser =& $wgUser; - + $this->setMembers(); $tpl = $this->setupTemplate( $this->template, 'skins' ); #if ( $wgUseDatabaseMessages ) { // uncomment this to fall back to GetText @@ -167,8 +167,6 @@ class SkinTemplate extends Skin { $this->iscontent = ($this->mTitle->getNamespace() != NS_SPECIAL ); $this->iseditable = ($this->iscontent and !($action == 'edit' or $action == 'submit')); $this->username = $wgUser->getName(); - $userPage = $wgUser->getUserPage(); - $this->userpage = $userPage->getPrefixedText(); if ( $wgUser->isLoggedIn() || $this->showIPinHeader() ) { $this->userpageUrlDetails = self::makeUrlDetails( $this->userpage ); @@ -178,17 +176,18 @@ class SkinTemplate extends Skin { $this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage ); } - $this->usercss = $this->userjs = $this->userjsprev = false; - $this->setupUserCss(); + $this->userjs = $this->userjsprev = false; + $this->setupUserCss( $out ); $this->setupUserJs( $out->isUserJsAllowed() ); $this->titletxt = $this->mTitle->getPrefixedText(); wfProfileOut( __METHOD__."-stuff" ); wfProfileIn( __METHOD__."-stuff2" ); - $tpl->set( 'title', $wgOut->getPageTitle() ); - $tpl->set( 'pagetitle', $wgOut->getHTMLTitle() ); - $tpl->set( 'displaytitle', $wgOut->mPageLinkTitle ); - $tpl->set( 'pageclass', Sanitizer::escapeClass( 'page-'.$this->mTitle->getPrefixedText() ) ); + $tpl->set( 'title', $out->getPageTitle() ); + $tpl->set( 'pagetitle', $out->getHTMLTitle() ); + $tpl->set( 'displaytitle', $out->mPageLinkTitle ); + $tpl->set( 'pageclass', $this->getPageClasses( $this->mTitle ) ); + $tpl->set( 'skinnameclass', ( "skin-" . Sanitizer::escapeClass( $this->getSkinName ( ) ) ) ); $nsname = isset( $wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ] ) ? $wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ] : @@ -201,7 +200,7 @@ class SkinTemplate extends Skin { $tpl->set( 'articleid', $this->mTitle->getArticleId() ); $tpl->set( 'currevisionid', isset( $wgArticle ) ? $wgArticle->getLatest() : 0 ); - $tpl->set( 'isarticle', $wgOut->isArticle() ); + $tpl->set( 'isarticle', $out->isArticle() ); $tpl->setRef( "thispage", $this->thispage ); $subpagestr = $this->subPageSubtitle(); @@ -218,9 +217,9 @@ class SkinTemplate extends Skin { ); $tpl->set( 'catlinks', $this->getCategories()); - if( $wgOut->isSyndicated() ) { + if( $out->isSyndicated() ) { $feeds = array(); - foreach( $wgOut->getSyndicationLinks() as $format => $link ) { + foreach( $out->getSyndicationLinks() as $format => $link ) { $feeds[$format] = array( 'text' => wfMsg( "feed-$format" ), 'href' => $link ); @@ -241,14 +240,15 @@ class SkinTemplate extends Skin { $tpl->setRef( 'jsmimetype', $wgJsMimeType ); $tpl->setRef( 'charset', $wgOutputEncoding ); $tpl->set( 'headlinks', $out->getHeadLinks() ); - $tpl->set('headscripts', $out->getScript() ); + $tpl->set( 'headscripts', $out->getScript() ); + $tpl->set( 'csslinks', $out->buildCssLinks() ); $tpl->setRef( 'wgScript', $wgScript ); $tpl->setRef( 'skinname', $this->skinname ); $tpl->set( 'skinclass', get_class( $this ) ); $tpl->setRef( 'stylename', $this->stylename ); $tpl->set( 'printable', $wgRequest->getBool( 'printable' ) ); + $tpl->set( 'handheld', $wgRequest->getBool( 'handheld' ) ); $tpl->setRef( 'loggedin', $this->loggedin ); - $tpl->set('nsclass', 'ns-'.$this->mTitle->getNamespace()); $tpl->set('notspecialpage', $this->mTitle->getNamespace() != NS_SPECIAL); /* XXX currently unused, might get useful later $tpl->set( "editable", ($this->mTitle->getNamespace() != NS_SPECIAL ) ); @@ -274,12 +274,10 @@ class SkinTemplate extends Skin { $tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href']); $tpl->set( 'userlang', $wgLang->getCode() ); $tpl->set( 'pagecss', $this->setupPageCss() ); - $tpl->set( 'printcss', $this->getPrintCss() ); $tpl->setRef( 'usercss', $this->usercss); $tpl->setRef( 'userjs', $this->userjs); $tpl->setRef( 'userjsprev', $this->userjsprev); - global $wgUseSiteJs; - if ($wgUseSiteJs) { + if( $wgUseSiteJs ) { $jsCache = $this->loggedin ? '&smaxage=0' : ''; $tpl->set( 'jsvarurl', self::makeUrl('-', @@ -307,18 +305,18 @@ class SkinTemplate extends Skin { ) ); # Disable Cache - $wgOut->setSquidMaxage(0); + $out->setSquidMaxage(0); } } else if (count($newtalks)) { - $sep = str_replace("_", " ", wfMsgHtml("newtalkseperator")); + $sep = str_replace("_", " ", wfMsgHtml("newtalkseparator")); $msgs = array(); foreach ($newtalks as $newtalk) { - $msgs[] = wfElement("a", + $msgs[] = Xml::element("a", array('href' => $newtalk["link"]), $newtalk["wiki"]); } $parts = implode($sep, $msgs); $ntl = wfMsgHtml('youhavenewmessagesmulti', $parts); - $wgOut->setSquidMaxage(0); + $out->setSquidMaxage(0); } else { $ntl = ''; } @@ -326,9 +324,9 @@ class SkinTemplate extends Skin { wfProfileIn( __METHOD__."-stuff3" ); $tpl->setRef( 'newtalk', $ntl ); - $tpl->setRef( 'skin', $this); + $tpl->setRef( 'skin', $this ); $tpl->set( 'logo', $this->logoText() ); - if ( $wgOut->isArticle() and (!isset( $oldid ) or isset( $diff )) and + if ( $out->isArticle() and (!isset( $oldid ) or isset( $diff )) and $wgArticle and 0 != $wgArticle->getID() ) { if ( !$wgDisableCounters ) { @@ -367,11 +365,10 @@ class SkinTemplate extends Skin { $this->credits = false; - if (isset($wgMaxCredits) && $wgMaxCredits != 0) { - require_once("Credits.php"); - $this->credits = getCredits($wgArticle, $wgMaxCredits, $wgShowCreditsIfMax); + if( $wgMaxCredits != 0 ){ + $this->credits = Credits::getCredits( $wgArticle, $wgMaxCredits, $wgShowCreditsIfMax ); } else { - $tpl->set('lastmod', $this->lastModified()); + $tpl->set( 'lastmod', $this->lastModified() ); } $tpl->setRef( 'credits', $this->credits ); @@ -411,16 +408,18 @@ class SkinTemplate extends Skin { $language_urls = array(); if ( !$wgHideInterlanguageLinks ) { - foreach( $wgOut->getLanguageLinks() as $l ) { + foreach( $out->getLanguageLinks() as $l ) { $tmp = explode( ':', $l, 2 ); $class = 'interwiki-' . $tmp[0]; unset($tmp); $nt = Title::newFromText( $l ); - $language_urls[] = array( - 'href' => $nt->getFullURL(), - 'text' => ($wgContLang->getLanguageName( $nt->getInterwiki()) != ''?$wgContLang->getLanguageName( $nt->getInterwiki()) : $l), - 'class' => $class - ); + if ( $nt ) { + $language_urls[] = array( + 'href' => $nt->getFullURL(), + 'text' => ($wgContLang->getLanguageName( $nt->getInterwiki()) != ''?$wgContLang->getLanguageName( $nt->getInterwiki()) : $l), + 'class' => $class + ); + } } } if(count($language_urls)) { @@ -430,6 +429,7 @@ class SkinTemplate extends Skin { } wfProfileOut( __METHOD__."-stuff4" ); + wfProfileIn( __METHOD__."-stuff5" ); # Personal toolbar $tpl->set('personal_urls', $this->buildPersonalUrls()); $content_actions = $this->buildContentActionUrls(); @@ -438,7 +438,7 @@ class SkinTemplate extends Skin { // XXX: attach this from javascript, same with section editing if($this->iseditable && $wgUser->getOption("editondblclick") ) { - $encEditUrl = wfEscapeJsString( $this->mTitle->getLocalUrl( $this->editUrlOptions() ) ); + $encEditUrl = Xml::escapeJsString( $this->mTitle->getLocalUrl( $this->editUrlOptions() ) ); $tpl->set('body_ondblclick', 'document.location = "' . $encEditUrl . '";'); } else { $tpl->set('body_ondblclick', false); @@ -452,6 +452,11 @@ class SkinTemplate extends Skin { wfDebug( __METHOD__ . ': Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!' ); } + // allow extensions adding stuff after the page content. + // See Skin::afterContentHook() for further documentation. + $tpl->set ('dataAfterContent', $this->afterContentHook()); + wfProfileOut( __METHOD__."-stuff5" ); + // execute template wfProfileIn( __METHOD__."-execute" ); $res = $tpl->execute(); @@ -589,9 +594,9 @@ class SkinTemplate extends Skin { if( $selected ) { $classes[] = 'selected'; } - if( $checkEdit && !$title->isAlwaysKnown() && $title->getArticleId() == 0 ) { + if( $checkEdit && !$title->isKnown() ) { $classes[] = 'new'; - $query = 'action=edit'; + $query = 'action=edit&redlink=1'; } $text = wfMsg( $message ); @@ -641,7 +646,7 @@ class SkinTemplate extends Skin { * @return array * @private */ - function buildContentActionUrls () { + function buildContentActionUrls() { global $wgContLang, $wgLang, $wgOut; wfProfileIn( __METHOD__ ); @@ -690,7 +695,7 @@ class SkinTemplate extends Skin { 'href' => $this->mTitle->getLocalUrl( 'action=edit§ion=new' ) ); } - } elseif ( $this->mTitle->exists() || $this->mTitle->isAlwaysKnown() ) { + } elseif ( $this->mTitle->isKnown() ) { $content_actions['viewsource'] = array( 'class' => ($action == 'edit') ? 'selected' : false, 'text' => wfMsg('viewsource'), @@ -700,7 +705,7 @@ class SkinTemplate extends Skin { wfProfileOut( __METHOD__."-edit" ); wfProfileIn( __METHOD__."-live" ); - if ( $this->mTitle->getArticleId() ) { + if ( $this->mTitle->exists() ) { $content_actions['history'] = array( 'class' => ($action == 'history') ? 'selected' : false, @@ -708,7 +713,7 @@ class SkinTemplate extends Skin { 'href' => $this->mTitle->getLocalUrl( 'action=history') ); - if($wgUser->isAllowed('delete')){ + if( $wgUser->isAllowed('delete') ) { $content_actions['delete'] = array( 'class' => ($action == 'delete') ? 'selected' : false, 'text' => wfMsg('delete'), @@ -725,7 +730,7 @@ class SkinTemplate extends Skin { } if ( $this->mTitle->getNamespace() !== NS_MEDIAWIKI && $wgUser->isAllowed( 'protect' ) ) { - if(!$this->mTitle->isProtected()){ + if( !$this->mTitle->isProtected() ){ $content_actions['protect'] = array( 'class' => ($action == 'protect') ? 'selected' : false, 'text' => wfMsg('protect'), @@ -837,7 +842,7 @@ class SkinTemplate extends Skin { * @return array * @private */ - function buildNavUrls () { + function buildNavUrls() { global $wgUseTrackbacks, $wgTitle, $wgUser, $wgRequest; global $wgEnableUploads, $wgUploadNavigationUrl; @@ -847,7 +852,7 @@ class SkinTemplate extends Skin { $nav_urls = array(); $nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() ); - if( $wgEnableUploads ) { + if( $wgEnableUploads && $wgUser->isAllowed( 'upload' ) ) { if ($wgUploadNavigationUrl) { $nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl ); } else { @@ -952,70 +957,13 @@ class SkinTemplate extends Skin { * @return string * @private */ - function getNameSpaceKey () { + function getNameSpaceKey() { return $this->mTitle->getNamespaceKey(); } /** * @private */ - function setupUserCss() { - wfProfileIn( __METHOD__ ); - - global $wgRequest, $wgAllowUserCss, $wgUseSiteCss, $wgContLang, $wgSquidMaxage, $wgStylePath, $wgUser; - - $sitecss = ''; - $usercss = ''; - $siteargs = '&maxage=' . $wgSquidMaxage; - if( $this->loggedin ) { - // Ensure that logged-in users' generated CSS isn't clobbered - // by anons' publicly cacheable generated CSS. - $siteargs .= '&smaxage=0'; - } - - # Add user-specific code if this is a user and we allow that kind of thing - - if ( $wgAllowUserCss && $this->loggedin ) { - $action = $wgRequest->getText('action'); - - # if we're previewing the CSS page, use it - if( $this->mTitle->isCssSubpage() and $this->userCanPreview( $action ) ) { - $siteargs = "&smaxage=0&maxage=0"; - $usercss = $wgRequest->getText('wpTextbox1'); - } else { - $usercss = '@import "' . - self::makeUrl($this->userpage . '/'.$this->skinname.'.css', - 'action=raw&ctype=text/css') . '";' ."\n"; - } - - $siteargs .= '&ts=' . $wgUser->mTouched; - } - - if( $wgContLang->isRTL() && in_array( 'rtl', $this->cssfiles ) ) { - global $wgStyleVersion; - $sitecss .= "@import \"$wgStylePath/$this->stylename/rtl.css?$wgStyleVersion\";\n"; - } - - # If we use the site's dynamic CSS, throw that in, too - if ( $wgUseSiteCss ) { - $query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; - $skinquery = "&useskin=" . urlencode( $this->getSkinName() ); - $sitecss .= '@import "' . self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI) . '";' . "\n"; - $sitecss .= '@import "' . self::makeNSUrl( ucfirst( $this->skinname ) . '.css', $query, NS_MEDIAWIKI ) . '";' . "\n"; - $sitecss .= '@import "' . self::makeUrl( '-', "action=raw&gen=css$siteargs$skinquery" ) . '";' . "\n"; - } - - # If we use any dynamic CSS, make a little CDATA block out of it. - - if ( !empty($sitecss) || !empty($usercss) ) { - $this->usercss = "/*<![CDATA[*/\n" . $sitecss . $usercss . '/*]]>*/'; - } - wfProfileOut( __METHOD__ ); - } - - /** - * @private - */ function setupUserJs( $allowUserJs ) { wfProfileIn( __METHOD__ ); @@ -1043,63 +991,9 @@ class SkinTemplate extends Skin { wfProfileIn( __METHOD__ ); $out = false; wfRunHooks( 'SkinTemplateSetupPageCss', array( &$out ) ); - wfProfileOut( __METHOD__ ); return $out; } - - /** - * returns css with user-specific options - */ - public function getUserStylesheet() { - wfProfileIn( __METHOD__ ); - - $s = "/* generated user stylesheet */\n"; - $s .= $this->reallyDoGetUserStyles(); - wfProfileOut( __METHOD__ ); - return $s; - } - - /** - * Returns the print stylesheet for this skin. In all default skins this - * is just commonPrint.css, but third-party skins may want to modify it. - * - * @return string - */ - protected function getPrintCss() { - global $wgStylePath; - return $wgStylePath . "/common/commonPrint.css"; - } - - /** - * This returns MediaWiki:Common.js and MediaWiki:[Skinname].js concate- - * nated together. For some bizarre reason, it does *not* return any - * custom user JS from subpages. Huh? - * - * There's absolutely no reason to have separate Monobook/Common JSes. - * Any JS that cares can just check the skin variable generated at the - * top. For now Monobook.js will be maintained, but it should be consi- - * dered deprecated. - * - * @return string - */ - public function getUserJs() { - wfProfileIn( __METHOD__ ); - - $s = parent::getUserJs(); - $s .= "\n\n/* MediaWiki:".ucfirst($this->skinname).".js */\n"; - - // avoid inclusion of non defined user JavaScript (with custom skins only) - // by checking for default message content - $msgKey = ucfirst($this->skinname).'.js'; - $userJS = wfMsgForContent($msgKey); - if ( !wfEmptyMsg( $msgKey, $userJS ) ) { - $s .= $userJS; - } - - wfProfileOut( __METHOD__ ); - return $s; - } } /** diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index d6ad6e6e..00eacd1e 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -89,14 +89,17 @@ class SpecialPage 'CreateAccount' => array( 'SpecialRedirectToSpecial', 'CreateAccount', 'Userlogin', 'signup', array( 'uselang' ) ), 'Preferences' => array( 'SpecialPage', 'Preferences' ), 'Watchlist' => array( 'SpecialPage', 'Watchlist' ), + 'Resetpass' => 'SpecialResetpass', + 'Recentchanges' => 'SpecialRecentchanges', 'Upload' => array( 'SpecialPage', 'Upload' ), - 'Imagelist' => array( 'SpecialPage', 'Imagelist' ), + 'Listfiles' => array( 'SpecialPage', 'Listfiles' ), 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ), 'Listusers' => array( 'SpecialPage', 'Listusers' ), 'Listgrouprights' => 'SpecialListGroupRights', - 'Statistics' => array( 'SpecialPage', 'Statistics' ), + 'DeletedContributions' => 'DeletedContributionsPage', + 'Statistics' => 'SpecialStatistics', 'Randompage' => 'Randompage', 'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ), 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ), @@ -107,6 +110,8 @@ class SpecialPage 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ), 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ), 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ), + 'Wantedfiles' => array( 'SpecialPage', 'Wantedfiles' ), + 'Wantedtemplates' => array( 'SpecialPage', 'Wantedtemplates' ), 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ), 'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ), 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ), @@ -121,27 +126,27 @@ class SpecialPage 'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ), 'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ), 'Protectedtitles' => array( 'SpecialPage', 'Protectedtitles' ), - 'Allpages' => array( 'IncludableSpecialPage', 'Allpages' ), - 'Prefixindex' => array( 'IncludableSpecialPage', 'Prefixindex' ) , + 'Allpages' => 'SpecialAllpages', + 'Prefixindex' => 'SpecialPrefixindex', 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ), 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ), - 'Contributions' => array( 'SpecialPage', 'Contributions' ), + 'Contributions' => 'SpecialContributions', 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ), 'Whatlinkshere' => array( 'SpecialPage', 'Whatlinkshere' ), + 'LinkSearch' => array( 'SpecialPage', 'LinkSearch' ), 'Recentchangeslinked' => 'SpecialRecentchangeslinked', 'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ), 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ), - 'Resetpass' => array( 'UnlistedSpecialPage', 'Resetpass' ), 'Booksources' => 'SpecialBookSources', 'Categories' => array( 'SpecialPage', 'Categories' ), 'Export' => array( 'SpecialPage', 'Export' ), - 'Version' => array( 'SpecialPage', 'Version' ), + 'Version' => 'SpecialVersion', 'Blankpage' => array( 'UnlistedSpecialPage', 'Blankpage' ), 'Allmessages' => array( 'SpecialPage', 'Allmessages' ), 'Log' => array( 'SpecialPage', 'Log' ), 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ), 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ), - 'Import' => array( 'SpecialPage', 'Import', 'import' ), + 'Import' => 'SpecialImport', 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ), 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ), 'Userrights' => 'UserrightsPage', @@ -484,7 +489,7 @@ class SpecialPage if ( !$page ) { if ( !$including ) { $wgOut->setArticleRelated( false ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setStatusCode( 404 ); $wgOut->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' ); } @@ -576,7 +581,7 @@ class SpecialPage if ( $subpage !== false && !is_null( $subpage ) ) { $name = "$name/$subpage"; } - return $name; + return ucfirst( $name ); } /** @@ -740,14 +745,8 @@ class SpecialPage if(!is_callable($func) and $this->mFile) { require_once( $this->mFile ); } - # FIXME: these hooks are broken for extensions and anything else that subclasses SpecialPage. - if ( wfRunHooks( 'SpecialPageExecuteBeforeHeader', array( &$this, &$par, &$func ) ) ) - $this->outputHeader(); - if ( ! wfRunHooks( 'SpecialPageExecuteBeforePage', array( &$this, &$par, &$func ) ) ) - return; + $this->outputHeader(); call_user_func( $func, $par, $this ); - if ( ! wfRunHooks( 'SpecialPageExecuteAfterPage', array( &$this, &$par, &$func ) ) ) - return; } else { $this->displayRestrictionError(); } diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php index f69d1f0b..c8497a83 100644 --- a/includes/SquidUpdate.php +++ b/includes/SquidUpdate.php @@ -6,7 +6,7 @@ */ /** - * @todo document + * Handles purging appropriate Squid URLs given a title (or titles) * @ingroup Cache */ class SquidUpdate { diff --git a/includes/StringUtils.php b/includes/StringUtils.php index 70d0bff1..c437b3c1 100644 --- a/includes/StringUtils.php +++ b/includes/StringUtils.php @@ -167,6 +167,18 @@ class StringUtils { $string = str_replace( '$', '\\$', $string ); return $string; } + + /** + * Workalike for explode() with limited memory usage. + * Returns an Iterator + */ + static function explode( $separator, $subject ) { + if ( substr_count( $subject, $separator ) > 1000 ) { + return new ExplodeIterator( $separator, $subject ); + } else { + return new ArrayIterator( explode( $separator, $subject ) ); + } + } } /** @@ -310,3 +322,90 @@ class ReplacementArray { return $result; } } + +/** + * An iterator which works exactly like: + * + * foreach ( explode( $delim, $s ) as $element ) { + * ... + * } + * + * Except it doesn't use 193 byte per element + */ +class ExplodeIterator implements Iterator { + // The subject string + var $subject, $subjectLength; + + // The delimiter + var $delim, $delimLength; + + // The position of the start of the line + var $curPos; + + // The position after the end of the next delimiter + var $endPos; + + // The current token + var $current; + + /** + * Construct a DelimIterator + */ + function __construct( $delim, $s ) { + $this->subject = $s; + $this->delim = $delim; + + // Micro-optimisation (theoretical) + $this->subjectLength = strlen( $s ); + $this->delimLength = strlen( $delim ); + + $this->rewind(); + } + + function rewind() { + $this->curPos = 0; + $this->endPos = strpos( $this->subject, $this->delim ); + $this->refreshCurrent(); + } + + + function refreshCurrent() { + if ( $this->curPos === false ) { + $this->current = false; + } elseif ( $this->curPos >= $this->subjectLength ) { + $this->current = ''; + } elseif ( $this->endPos === false ) { + $this->current = substr( $this->subject, $this->curPos ); + } else { + $this->current = substr( $this->subject, $this->curPos, $this->endPos - $this->curPos ); + } + } + + function current() { + return $this->current; + } + + function key() { + return $this->curPos; + } + + function next() { + if ( $this->endPos === false ) { + $this->curPos = false; + } else { + $this->curPos = $this->endPos + $this->delimLength; + if ( $this->curPos >= $this->subjectLength ) { + $this->endPos = false; + } else { + $this->endPos = strpos( $this->subject, $this->delim, $this->curPos ); + } + } + $this->refreshCurrent(); + return $this->current; + } + + function valid() { + return $this->curPos !== false; + } +} + diff --git a/includes/StubObject.php b/includes/StubObject.php index ec52e7f4..e27f0b25 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -95,7 +95,7 @@ class StubObject { if ( ++$recursionLevel > 2 ) { throw new MWException( "Unstub loop detected on call of \${$this->mGlobal}->$name from $caller\n" ); } - wfDebug( "Unstubbing \${$this->mGlobal} on call of \${$this->mGlobal}->$name from $caller\n" ); + wfDebug( "Unstubbing \${$this->mGlobal} on call of \${$this->mGlobal}::$name from $caller\n" ); $GLOBALS[$this->mGlobal] = $this->_newObject(); --$recursionLevel; wfProfileOut( $fname ); diff --git a/includes/Title.php b/includes/Title.php index 6326240c..515a3b65 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -4,97 +4,86 @@ * @file */ -/** */ if ( !class_exists( 'UtfNormal' ) ) { require_once( dirname(__FILE__) . '/normal/UtfNormal.php' ); } define ( 'GAID_FOR_UPDATE', 1 ); -# Title::newFromTitle maintains a cache to avoid -# expensive re-normalization of commonly used titles. -# On a batch operation this can become a memory leak -# if not bounded. After hitting this many titles, -# reset the cache. -define( 'MW_TITLECACHE_MAX', 1000 ); -# Constants for pr_cascade bitfield +/** + * Constants for pr_cascade bitfield + */ define( 'CASCADE', 1 ); /** - * Title class - * - Represents a title, which may contain an interwiki designation or namespace - * - Can fetch various kinds of data from the database, albeit inefficiently. - * + * Represents a title within MediaWiki. + * Optionally may contain an interwiki designation or namespace. + * @note This class can fetch various kinds of data from the database; + * however, it does so inefficiently. */ class Title { - /** - * Static cache variables - */ + /** @name Static cache variables */ + //@{ static private $titleCache=array(); static private $interwikiCache=array(); - + //@} /** - * All member variables should be considered private - * Please use the accessor functions + * Title::newFromText maintains a cache to avoid expensive re-normalization of + * commonly used titles. On a batch operation this can become a memory leak + * if not bounded. After hitting this many titles reset the cache. */ + const CACHE_MAX = 1000; + - /**#@+ + /** + * @name Private member variables + * Please use the accessor functions instead. * @private */ - - var $mTextform; # Text form (spaces not underscores) of the main part - var $mUrlform; # URL-encoded form of the main part - var $mDbkeyform; # Main part with underscores - var $mUserCaseDBKey; # DB key with the initial letter in the case specified by the user - var $mNamespace; # Namespace index, i.e. one of the NS_xxxx constants - var $mInterwiki; # Interwiki prefix (or null string) - var $mFragment; # Title fragment (i.e. the bit after the #) - var $mArticleID; # Article ID, fetched from the link cache on demand - var $mLatestID; # ID of most recent revision - var $mRestrictions; # Array of groups allowed to edit this article - var $mCascadeRestriction; # Cascade restrictions on this page to included templates and images? - var $mRestrictionsExpiry; # When do the restrictions on this page expire? - var $mHasCascadingRestrictions; # Are cascading restrictions in effect on this page? - var $mCascadeRestrictionSources;# Where are the cascading restrictions coming from on this page? - var $mRestrictionsLoaded; # Boolean for initialisation on demand - var $mPrefixedText; # Text form including namespace/interwiki, initialised on demand - var $mDefaultNamespace; # Namespace index when there is no namespace - # Zero except in {{transclusion}} tags - var $mWatched; # Is $wgUser watching this page? NULL if unfilled, accessed through userIsWatching() - var $mLength; # The page length, 0 for special pages - var $mRedirect; # Is the article at this title a redirect? - /**#@-*/ + //@{ + + var $mTextform = ''; ///< Text form (spaces not underscores) of the main part + var $mUrlform = ''; ///< URL-encoded form of the main part + var $mDbkeyform = ''; ///< Main part with underscores + var $mUserCaseDBKey; ///< DB key with the initial letter in the case specified by the user + var $mNamespace = NS_MAIN; ///< Namespace index, i.e. one of the NS_xxxx constants + var $mInterwiki = ''; ///< Interwiki prefix (or null string) + var $mFragment; ///< Title fragment (i.e. the bit after the #) + var $mArticleID = -1; ///< Article ID, fetched from the link cache on demand + var $mLatestID = false; ///< ID of most recent revision + var $mRestrictions = array(); ///< Array of groups allowed to edit this article + var $mOldRestrictions = false; + var $mCascadeRestriction; ///< Cascade restrictions on this page to included templates and images? + var $mRestrictionsExpiry = array(); ///< When do the restrictions on this page expire? + var $mHasCascadingRestrictions; ///< Are cascading restrictions in effect on this page? + var $mCascadeSources; ///< Where are the cascading restrictions coming from on this page? + var $mRestrictionsLoaded = false; ///< Boolean for initialisation on demand + var $mPrefixedText; ///< Text form including namespace/interwiki, initialised on demand + # Don't change the following default, NS_MAIN is hardcoded in several + # places. See bug 696. + var $mDefaultNamespace = NS_MAIN; ///< Namespace index when there is no namespace + # Zero except in {{transclusion}} tags + var $mWatched = null; ///< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching() + var $mLength = -1; ///< The page length, 0 for special pages + var $mRedirect = null; ///< Is the article at this title a redirect? + var $mNotificationTimestamp = array(); ///< Associative array of user ID -> timestamp/false + //@} /** * Constructor * @private */ - /* private */ function __construct() { - $this->mInterwiki = $this->mUrlform = - $this->mTextform = $this->mDbkeyform = ''; - $this->mArticleID = -1; - $this->mNamespace = NS_MAIN; - $this->mRestrictionsLoaded = false; - $this->mRestrictions = array(); - # Dont change the following, NS_MAIN is hardcoded in several place - # See bug #696 - $this->mDefaultNamespace = NS_MAIN; - $this->mWatched = NULL; - $this->mLatestID = false; - $this->mOldRestrictions = false; - $this->mLength = -1; - $this->mRedirect = NULL; - } + /* private */ function __construct() {} /** * Create a new Title from a prefixed DB key - * @param string $key The database key, which has underscores + * @param $key \type{\string} The database key, which has underscores * instead of spaces, possibly including namespace and * interwiki prefixes - * @return Title the new object, or NULL on an error + * @return \type{Title} the new object, or NULL on an error */ public static function newFromDBkey( $key ) { $t = new Title(); @@ -106,15 +95,16 @@ class Title { } /** - * Create a new Title from text, such as what one would - * find in a link. Decodes any HTML entities in the text. + * Create a new Title from text, such as what one would find in a link. De- + * codes any HTML entities in the text. * - * @param string $text the link text; spaces, prefixes, - * and an initial ':' indicating the main namespace - * are accepted - * @param int $defaultNamespace the namespace to use if - * none is specified by a prefix - * @return Title the new object, or NULL on an error + * @param $text string The link text; spaces, prefixes, and an + * initial ':' indicating the main namespace are accepted. + * @param $defaultNamespace int The namespace to use if none is speci- + * fied by a prefix. If you want to force a specific namespace even if + * $text might begin with a namespace prefix, use makeTitle() or + * makeTitleSafe(). + * @return Title The new object, or null on an error. */ public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { if( is_object( $text ) ) { @@ -145,7 +135,7 @@ class Title { static $cachedcount = 0 ; if( $t->secureAndSplit() ) { if( $defaultNamespace == NS_MAIN ) { - if( $cachedcount >= MW_TITLECACHE_MAX ) { + if( $cachedcount >= self::CACHE_MAX ) { # Avoid memory leaks on mass operations... Title::$titleCache = array(); $cachedcount=0; @@ -163,8 +153,8 @@ class Title { /** * Create a new Title from URL-encoded text. Ensures that * the given title's length does not exceed the maximum. - * @param string $url the title, as might be taken from a URL - * @return Title the new object, or NULL on an error + * @param $url \type{\string} the title, as might be taken from a URL + * @return \type{Title} the new object, or NULL on an error */ public static function newFromURL( $url ) { global $wgLegalTitleChars; @@ -191,9 +181,9 @@ class Title { * @todo This is inefficiently implemented, the page row is requested * but not used for anything else * - * @param int $id the page_id corresponding to the Title to create - * @param int $flags, use GAID_FOR_UPDATE to use master - * @return Title the new object, or NULL on an error + * @param $id \type{\int} the page_id corresponding to the Title to create + * @param $flags \type{\int} use GAID_FOR_UPDATE to use master + * @return \type{Title} the new object, or NULL on an error */ public static function newFromID( $id, $flags = 0 ) { $fname = 'Title::newFromID'; @@ -210,6 +200,8 @@ class Title { /** * Make an array of titles from an array of IDs + * @param $ids \type{\arrayof{\int}} Array of IDs + * @return \type{\arrayof{Title}} Array of Titles */ public static function newFromIDs( $ids ) { if ( !count( $ids ) ) { @@ -220,7 +212,7 @@ class Title { 'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ ); $titles = array(); - while ( $row = $dbr->fetchObject( $res ) ) { + foreach( $res as $row ) { $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title ); } return $titles; @@ -228,7 +220,8 @@ class Title { /** * Make a Title object from a DB row - * @param Row $row (needs at least page_title,page_namespace) + * @param $row \type{Row} (needs at least page_title,page_namespace) + * @return \type{Title} corresponding Title */ public static function newFromRow( $row ) { $t = self::makeTitle( $row->page_namespace, $row->page_title ); @@ -248,10 +241,10 @@ class Title { * For convenience, spaces are converted to underscores so that * eg user_text fields can be used directly. * - * @param int $ns the namespace of the article - * @param string $title the unprefixed database key form - * @param string $fragment The link fragment (after the "#") - * @return Title the new object + * @param $ns \type{\int} the namespace of the article + * @param $title \type{\string} the unprefixed database key form + * @param $fragment \type{\string} The link fragment (after the "#") + * @return \type{Title} the new object */ public static function &makeTitle( $ns, $title, $fragment = '' ) { $t = new Title(); @@ -270,10 +263,10 @@ class Title { * The parameters will be checked for validity, which is a bit slower * than makeTitle() but safer for user-provided data. * - * @param int $ns the namespace of the article - * @param string $title the database key form - * @param string $fragment The link fragment (after the "#") - * @return Title the new object, or NULL on an error + * @param $ns \type{\int} the namespace of the article + * @param $title \type{\string} the database key form + * @param $fragment \type{\string} The link fragment (after the "#") + * @return \type{Title} the new object, or NULL on an error */ public static function makeTitleSafe( $ns, $title, $fragment = '' ) { $t = new Title(); @@ -287,7 +280,7 @@ class Title { /** * Create a new Title for the Main Page - * @return Title the new object + * @return \type{Title} the new object */ public static function newMainPage() { $title = Title::newFromText( wfMsgForContent( 'mainpage' ) ); @@ -302,15 +295,18 @@ class Title { * Extract a redirect destination from a string and return the * Title, or null if the text doesn't contain a valid redirect * - * @param string $text Text with possible redirect - * @return Title + * @param $text \type{String} Text with possible redirect + * @return \type{Title} The corresponding Title */ public static function newFromRedirect( $text ) { $redir = MagicWord::get( 'redirect' ); - if( $redir->matchStart( trim($text) ) ) { + $text = trim($text); + if( $redir->matchStartAndRemove( $text ) ) { // Extract the first link and see if it's usable + // Ensure that it really does come directly after #REDIRECT + // Some older redirects included a colon, so don't freak about that! $m = array(); - if( preg_match( '!\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) { + if( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) { // Strip preceding colon used to "escape" categories, etc. // and URL-decode links if( strpos( $m[1], '%' ) !== false ) { @@ -338,26 +334,26 @@ class Title { /** * Get the prefixed DB key associated with an ID - * @param int $id the page_id of the article - * @return Title an object representing the article, or NULL + * @param $id \type{\int} the page_id of the article + * @return \type{Title} an object representing the article, or NULL * if no such article was found - * @static - * @access public */ - function nameOf( $id ) { - $fname = 'Title::nameOf'; + public static function nameOf( $id ) { $dbr = wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( 'page', array( 'page_namespace','page_title' ), array( 'page_id' => $id ), $fname ); + $s = $dbr->selectRow( 'page', + array( 'page_namespace','page_title' ), + array( 'page_id' => $id ), + __METHOD__ ); if ( $s === false ) { return NULL; } - $n = Title::makeName( $s->page_namespace, $s->page_title ); + $n = self::makeName( $s->page_namespace, $s->page_title ); return $n; } /** * Get a regex character class describing the legal characters in a link - * @return string the list of characters, not delimited + * @return \type{\string} the list of characters, not delimited */ public static function legalChars() { global $wgLegalTitleChars; @@ -368,9 +364,9 @@ class Title { * Get a string representation of a title suitable for * including in a search index * - * @param int $ns a namespace index - * @param string $title text-form main part - * @return string a stripped-down title string ready for the + * @param $ns \type{\int} a namespace index + * @param $title \type{\string} text-form main part + * @return \type{\string} a stripped-down title string ready for the * search index */ public static function indexTitle( $ns, $title ) { @@ -387,7 +383,7 @@ class Title { $t = preg_replace( "/\\s+/", ' ', $t ); - if ( $ns == NS_IMAGE ) { + if ( $ns == NS_FILE ) { $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t ); } return trim( $t ); @@ -395,10 +391,10 @@ class Title { /* * Make a prefixed DB key from a DB key and a namespace index - * @param int $ns numerical representation of the namespace - * @param string $title the DB key form the title - * @param string $fragment The link fragment (after the "#") - * @return string the prefixed form of the title + * @param $ns \type{\int} numerical representation of the namespace + * @param $title \type{\string} the DB key form the title + * @param $fragment \type{\string} The link fragment (after the "#") + * @return \type{\string} the prefixed form of the title */ public static function makeName( $ns, $title, $fragment = '' ) { global $wgContLang; @@ -413,111 +409,26 @@ class Title { /** * Returns the URL associated with an interwiki prefix - * @param string $key the interwiki prefix (e.g. "MeatBall") - * @return the associated URL, containing "$1", which should be - * replaced by an article title + * @param $key \type{\string} the interwiki prefix (e.g. "MeatBall") + * @return \type{\string} the associated URL, containing "$1", + * which should be replaced by an article title * @static (arguably) + * @deprecated See Interwiki class */ public function getInterwikiLink( $key ) { - global $wgMemc, $wgInterwikiExpiry; - global $wgInterwikiCache, $wgContLang; - $fname = 'Title::getInterwikiLink'; - - $key = $wgContLang->lc( $key ); - - $k = wfMemcKey( 'interwiki', $key ); - if( array_key_exists( $k, Title::$interwikiCache ) ) { - return Title::$interwikiCache[$k]->iw_url; - } - - if ($wgInterwikiCache) { - return Title::getInterwikiCached( $key ); - } - - $s = $wgMemc->get( $k ); - # Ignore old keys with no iw_local - if( $s && isset( $s->iw_local ) && isset($s->iw_trans)) { - Title::$interwikiCache[$k] = $s; - return $s->iw_url; - } - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'interwiki', - array( 'iw_url', 'iw_local', 'iw_trans' ), - array( 'iw_prefix' => $key ), $fname ); - if( !$res ) { - return ''; - } - - $s = $dbr->fetchObject( $res ); - if( !$s ) { - # Cache non-existence: create a blank object and save it to memcached - $s = (object)false; - $s->iw_url = ''; - $s->iw_local = 0; - $s->iw_trans = 0; - } - $wgMemc->set( $k, $s, $wgInterwikiExpiry ); - Title::$interwikiCache[$k] = $s; - - return $s->iw_url; + return Interwiki::fetch( $key )->getURL( ); } /** - * Fetch interwiki prefix data from local cache in constant database - * - * More logic is explained in DefaultSettings - * - * @return string URL of interwiki site - */ - public static function getInterwikiCached( $key ) { - global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite; - static $db, $site; - - if (!$db) - $db=dba_open($wgInterwikiCache,'r','cdb'); - /* Resolve site name */ - if ($wgInterwikiScopes>=3 and !$site) { - $site = dba_fetch('__sites:' . wfWikiID(), $db); - if ($site=="") - $site = $wgInterwikiFallbackSite; - } - $value = dba_fetch( wfMemcKey( $key ), $db); - if ($value=='' and $wgInterwikiScopes>=3) { - /* try site-level */ - $value = dba_fetch("_{$site}:{$key}", $db); - } - if ($value=='' and $wgInterwikiScopes>=2) { - /* try globals */ - $value = dba_fetch("__global:{$key}", $db); - } - if ($value=='undef') - $value=''; - $s = (object)false; - $s->iw_url = ''; - $s->iw_local = 0; - $s->iw_trans = 0; - if ($value!='') { - list($local,$url)=explode(' ',$value,2); - $s->iw_url=$url; - $s->iw_local=(int)$local; - } - Title::$interwikiCache[wfMemcKey( 'interwiki', $key )] = $s; - return $s->iw_url; - } - /** * Determine whether the object refers to a page within * this project. * - * @return bool TRUE if this is an in-project interwiki link + * @return \type{\bool} TRUE if this is an in-project interwiki link * or a wikilink, FALSE otherwise */ public function isLocal() { if ( $this->mInterwiki != '' ) { - # Make sure key is loaded into cache - $this->getInterwikiLink( $this->mInterwiki ); - $k = wfMemcKey( 'interwiki', $this->mInterwiki ); - return (bool)(Title::$interwikiCache[$k]->iw_local); + return Interwiki::fetch( $this->mInterwiki )->isLocal(); } else { return true; } @@ -527,28 +438,26 @@ class Title { * Determine whether the object refers to a page within * this project and is transcludable. * - * @return bool TRUE if this is transcludable + * @return \type{\bool} TRUE if this is transcludable */ public function isTrans() { if ($this->mInterwiki == '') return false; - # Make sure key is loaded into cache - $this->getInterwikiLink( $this->mInterwiki ); - $k = wfMemcKey( 'interwiki', $this->mInterwiki ); - return (bool)(Title::$interwikiCache[$k]->iw_trans); + + return Interwiki::fetch( $this->mInterwiki )->isTranscludable(); } /** * Escape a text fragment, say from a link, for a URL */ static function escapeFragmentForURL( $fragment ) { - $fragment = str_replace( ' ', '_', $fragment ); - $fragment = urlencode( Sanitizer::decodeCharReferences( $fragment ) ); - $replaceArray = array( - '%3A' => ':', - '%' => '.' - ); - return strtr( $fragment, $replaceArray ); + global $wgEnforceHtmlIds; + # Note that we don't urlencode the fragment. urlencoded Unicode + # fragments appear not to work in IE (at least up to 7) or in at least + # one version of Opera 9.x. The W3C validator, for one, doesn't seem + # to care if they aren't encoded. + return Sanitizer::escapeId( $fragment, + $wgEnforceHtmlIds ? 'noninitial' : 'xml' ); } #---------------------------------------------------------------------------- @@ -558,27 +467,27 @@ class Title { /** Simple accessors */ /** * Get the text form (spaces not underscores) of the main part - * @return string + * @return \type{\string} Main part of the title */ public function getText() { return $this->mTextform; } /** * Get the URL-encoded form of the main part - * @return string + * @return \type{\string} Main part of the title, URL-encoded */ public function getPartialURL() { return $this->mUrlform; } /** * Get the main part with underscores - * @return string + * @return \type{\string} Main part of the title, with underscores */ public function getDBkey() { return $this->mDbkeyform; } /** - * Get the namespace index, i.e. one of the NS_xxxx constants - * @return int + * Get the namespace index, i.e.\ one of the NS_xxxx constants. + * @return \type{\int} Namespace index */ public function getNamespace() { return $this->mNamespace; } /** * Get the namespace text - * @return string + * @return \type{\string} Namespace text */ public function getNsText() { global $wgContLang, $wgCanonicalNamespaceNames; @@ -598,49 +507,47 @@ class Title { } /** * Get the DB key with the initial letter case as specified by the user + * @return \type{\string} DB key */ function getUserCaseDBKey() { return $this->mUserCaseDBKey; } /** * Get the namespace text of the subject (rather than talk) page - * @return string + * @return \type{\string} Namespace text */ public function getSubjectNsText() { global $wgContLang; return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) ); } - /** * Get the namespace text of the talk page - * @return string + * @return \type{\string} Namespace text */ public function getTalkNsText() { global $wgContLang; return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) ); } - /** * Could this title have a corresponding talk page? - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function canTalk() { return( MWNamespace::canTalk( $this->mNamespace ) ); } - /** * Get the interwiki prefix (or null string) - * @return string + * @return \type{\string} Interwiki prefix */ public function getInterwiki() { return $this->mInterwiki; } /** - * Get the Title fragment (i.e. the bit after the #) in text form - * @return string + * Get the Title fragment (i.e.\ the bit after the #) in text form + * @return \type{\string} Title fragment */ public function getFragment() { return $this->mFragment; } /** * Get the fragment in URL form, including the "#" character if there is one - * @return string + * @return \type{\string} Fragment in URL form */ public function getFragmentForURL() { if ( $this->mFragment == '' ) { @@ -651,13 +558,13 @@ class Title { } /** * Get the default namespace index, for when there is no namespace - * @return int + * @return \type{\int} Default namespace index */ public function getDefaultNamespace() { return $this->mDefaultNamespace; } /** * Get title for search index - * @return string a stripped-down title string ready for the + * @return \type{\string} a stripped-down title string ready for the * search index */ public function getIndexTitle() { @@ -666,7 +573,7 @@ class Title { /** * Get the prefixed database key form - * @return string the prefixed title, with underscores and + * @return \type{\string} the prefixed title, with underscores and * any interwiki and namespace prefixes */ public function getPrefixedDBkey() { @@ -678,7 +585,7 @@ class Title { /** * Get the prefixed title with spaces. * This is the form usually used for display - * @return string the prefixed title, with spaces + * @return \type{\string} the prefixed title, with spaces */ public function getPrefixedText() { if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ? @@ -692,7 +599,7 @@ class Title { /** * Get the prefixed title with spaces, plus any fragment * (part beginning with '#') - * @return string the prefixed title, with spaces and + * @return \type{\string} the prefixed title, with spaces and * the fragment, including '#' */ public function getFullText() { @@ -705,7 +612,7 @@ class Title { /** * Get the base name, i.e. the leftmost parts before the / - * @return string Base name + * @return \type{\string} Base name */ public function getBaseText() { if( !MWNamespace::hasSubpages( $this->mNamespace ) ) { @@ -721,7 +628,7 @@ class Title { /** * Get the lowest-level subpage name, i.e. the rightmost part after / - * @return string Subpage name + * @return \type{\string} Subpage name */ public function getSubpageText() { if( !MWNamespace::hasSubpages( $this->mNamespace ) ) { @@ -733,29 +640,21 @@ class Title { /** * Get a URL-encoded form of the subpage text - * @return string URL-encoded subpage name + * @return \type{\string} URL-encoded subpage name */ public function getSubpageUrlForm() { $text = $this->getSubpageText(); $text = wfUrlencode( str_replace( ' ', '_', $text ) ); - $text = str_replace( '%28', '(', str_replace( '%29', ')', $text ) ); # Clean up the URL; per below, this might not be safe return( $text ); } /** * Get a URL-encoded title (not an actual URL) including interwiki - * @return string the URL-encoded form + * @return \type{\string} the URL-encoded form */ public function getPrefixedURL() { $s = $this->prefix( $this->mDbkeyform ); - $s = str_replace( ' ', '_', $s ); - - $s = wfUrlencode ( $s ) ; - - # Cleaning up URL to make it look nice -- is this safe? - $s = str_replace( '%28', '(', $s ); - $s = str_replace( '%29', ')', $s ); - + $s = wfUrlencode( str_replace( ' ', '_', $s ) ); return $s; } @@ -763,15 +662,21 @@ class Title { * Get a real URL referring to this title, with interwiki link and * fragment * - * @param string $query an optional query string, not used - * for interwiki links - * @param string $variant language variant of url (for sr, zh..) - * @return string the URL + * @param $query \twotypes{\string,\array} an optional query string, not used for interwiki + * links. Can be specified as an associative array as well, e.g., + * array( 'action' => 'edit' ) (keys and values will be URL-escaped). + * @param $variant \type{\string} language variant of url (for sr, zh..) + * @return \type{\string} the URL */ public function getFullURL( $query = '', $variant = false ) { global $wgContLang, $wgServer, $wgRequest; - if ( '' == $this->mInterwiki ) { + if( is_array( $query ) ) { + $query = wfArrayToCGI( $query ); + } + + $interwiki = Interwiki::fetch( $this->mInterwiki ); + if ( !$interwiki ) { $url = $this->getLocalUrl( $query, $variant ); // Ugly quick hack to avoid duplicate prefixes (bug 4571 etc) @@ -780,7 +685,7 @@ class Title { $url = $wgServer . $url; } } else { - $baseUrl = $this->getInterwikiLink( $this->mInterwiki ); + $baseUrl = $interwiki->getURL( ); $namespace = wfUrlencode( $this->getNsText() ); if ( '' != $namespace ) { @@ -802,15 +707,21 @@ class Title { /** * Get a URL with no fragment or server name. If this page is generated * with action=render, $wgServer is prepended. - * @param string $query an optional query string; if not specified, - * $wgArticlePath will be used. - * @param string $variant language variant of url (for sr, zh..) - * @return string the URL + * @param mixed $query an optional query string; if not specified, + * $wgArticlePath will be used. Can be specified as an associative array + * as well, e.g., array( 'action' => 'edit' ) (keys and values will be + * URL-escaped). + * @param $variant \type{\string} language variant of url (for sr, zh..) + * @return \type{\string} the URL */ public function getLocalURL( $query = '', $variant = false ) { global $wgArticlePath, $wgScript, $wgServer, $wgRequest; global $wgVariantArticlePath, $wgContLang, $wgUser; + if( is_array( $query ) ) { + $query = wfArrayToCGI( $query ); + } + // internal links should point to same variant as current page (only anonymous users) if($variant == false && $wgContLang->hasVariants() && !$wgUser->isLoggedIn()){ $pref = $wgContLang->getPreferredVariant(false); @@ -853,7 +764,9 @@ class Title { $query = $matches[1]; if( isset( $matches[4] ) ) $query .= $matches[4]; $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); - if( $query != '' ) $url .= '?' . $query; + if( $query != '' ) { + $url = wfAppendQuery( $url, $query ); + } } } if ( $url === false ) { @@ -875,10 +788,40 @@ class Title { } /** + * Get a URL that's the simplest URL that will be valid to link, locally, + * to the current Title. It includes the fragment, but does not include + * the server unless action=render is used (or the link is external). If + * there's a fragment but the prefixed text is empty, we just return a link + * to the fragment. + * + * @param $query \type{\arrayof{\string}} An associative array of key => value pairs for the + * query string. Keys and values will be escaped. + * @param $variant \type{\string} Language variant of URL (for sr, zh..). Ignored + * for external links. Default is "false" (same variant as current page, + * for anonymous users). + * @return \type{\string} the URL + */ + public function getLinkUrl( $query = array(), $variant = false ) { + if( !is_array( $query ) ) { + throw new MWException( 'Title::getLinkUrl passed a non-array for '. + '$query' ); + } + if( $this->isExternal() ) { + return $this->getFullURL( $query ); + } elseif( $this->getPrefixedText() === '' + and $this->getFragment() !== '' ) { + return $this->getFragmentForURL(); + } else { + return $this->getLocalURL( $query, $variant ) + . $this->getFragmentForURL(); + } + } + + /** * Get an HTML-escaped version of the URL form, suitable for * using in a link, without a server name or fragment - * @param string $query an optional query string - * @return string the URL + * @param $query \type{\string} an optional query string + * @return \type{\string} the URL */ public function escapeLocalURL( $query = '' ) { return htmlspecialchars( $this->getLocalURL( $query ) ); @@ -888,8 +831,8 @@ class Title { * Get an HTML-escaped version of the URL form, suitable for * using in a link, including the server name and fragment * - * @return string the URL - * @param string $query an optional query string + * @param $query \type{\string} an optional query string + * @return \type{\string} the URL */ public function escapeFullURL( $query = '' ) { return htmlspecialchars( $this->getFullURL( $query ) ); @@ -900,9 +843,9 @@ class Title { * - Used in various Squid-related code, in case we have a different * internal hostname for the server from the exposed one. * - * @param string $query an optional query string - * @param string $variant language variant of url (for sr, zh..) - * @return string the URL + * @param $query \type{\string} an optional query string + * @param $variant \type{\string} language variant of url (for sr, zh..) + * @return \type{\string} the URL */ public function getInternalURL( $query = '', $variant = false ) { global $wgInternalServer; @@ -913,7 +856,7 @@ class Title { /** * Get the edit URL for this Title - * @return string the URL, or a null string if this is an + * @return \type{\string} the URL, or a null string if this is an * interwiki link */ public function getEditURL() { @@ -926,7 +869,7 @@ class Title { /** * Get the HTML-escaped displayable text form. * Used for the title field in <a> tags. - * @return string the text, including any prefixes + * @return \type{\string} the text, including any prefixes */ public function getEscapedText() { return htmlspecialchars( $this->getPrefixedText() ); @@ -934,15 +877,15 @@ class Title { /** * Is this Title interwiki? - * @return boolean + * @return \type{\bool} */ public function isExternal() { return ( '' != $this->mInterwiki ); } /** * Is this page "semi-protected" - the *only* protection is autoconfirm? * - * @param string Action to check (default: edit) - * @return bool + * @param @action \type{\string} Action to check (default: edit) + * @return \type{\bool} */ public function isSemiProtected( $action = 'edit' ) { if( $this->exists() ) { @@ -965,9 +908,9 @@ class Title { /** * Does the title correspond to a protected article? - * @param string $what the action the page is protected from, + * @param $what \type{\string} the action the page is protected from, * by default checks move and edit - * @return boolean + * @return \type{\bool} */ public function isProtected( $action = '' ) { global $wgRestrictionLevels, $wgRestrictionTypes; @@ -993,7 +936,7 @@ class Title { /** * Is $wgUser watching this page? - * @return boolean + * @return \type{\bool} */ public function userIsWatching() { global $wgUser; @@ -1017,8 +960,8 @@ class Title { * * May provide false positives, but should never provide a false negative. * - * @param string $action action that permission needs to be checked for - * @return boolean + * @param $action \type{\string} action that permission needs to be checked for + * @return \type{\bool} */ public function quickUserCan( $action ) { return $this->userCan( $action, false ); @@ -1028,7 +971,7 @@ class Title { * Determines if $wgUser is unable to edit this page because it has been protected * by $wgNamespaceProtection. * - * @return boolean + * @return \type{\bool} */ public function isNamespaceProtected() { global $wgNamespaceProtection, $wgUser; @@ -1043,9 +986,9 @@ class Title { /** * Can $wgUser perform $action on this page? - * @param string $action action that permission needs to be checked for - * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. - * @return boolean + * @param $action \type{\string} action that permission needs to be checked for + * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries. + * @return \type{\bool} */ public function userCan( $action, $doExpensiveQueries = true ) { global $wgUser; @@ -1057,11 +1000,11 @@ class Title { * * FIXME: This *does not* check throttles (User::pingLimiter()). * - * @param string $action action that permission needs to be checked for - * @param User $user user to check - * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. - * @param array $ignoreErrors Set this to a list of message keys whose corresponding errors may be ignored. - * @return array Array of arrays of the arguments to wfMsg to explain permissions problems. + * @param $action \type{\string}action that permission needs to be checked for + * @param $user \type{User} user to check + * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries. + * @param $ignoreErrors \type{\arrayof{\string}} Set this to a list of message keys whose corresponding errors may be ignored. + * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems. */ public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { if( !StubObject::isRealObject( $user ) ) { @@ -1080,7 +1023,8 @@ class Title { $errors[] = array( 'confirmedittext' ); } - if ( $user->isBlockedFrom( $this ) && $action != 'createaccount' ) { + // Edit blocks should not affect reading. Account creation blocks handled at userlogin. + if ( $user->isBlockedFrom( $this ) && $action != 'read' && $action != 'createaccount' ) { $block = $user->mBlock; // This is from OutputPage::blockedPage @@ -1147,10 +1091,10 @@ class Title { * which checks ONLY that previously checked by userCan (i.e. it leaves out * checks on wfReadOnly() and blocks) * - * @param string $action action that permission needs to be checked for - * @param User $user user to check - * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. - * @return array Array of arrays of the arguments to wfMsg to explain permissions problems. + * @param $action \type{\string} action that permission needs to be checked for + * @param $user \type{User} user to check + * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries. + * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems. */ private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) { wfProfileIn( __METHOD__ ); @@ -1158,61 +1102,55 @@ class Title { $errors = array(); // Use getUserPermissionsErrors instead - if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { + if( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { wfProfileOut( __METHOD__ ); return $result ? array() : array( array( 'badaccess-group0' ) ); } - if (!wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { - if ($result != array() && is_array($result) && !is_array($result[0])) + if( !wfRunHooks( 'getUserPermissionsErrors', array(&$this,&$user,$action,&$result) ) ) { + if( is_array($result) && count($result) && !is_array($result[0]) ) $errors[] = $result; # A single array representing an error - else if (is_array($result) && is_array($result[0])) + else if( is_array($result) && is_array($result[0]) ) $errors = array_merge( $errors, $result ); # A nested array representing multiple errors - else if ($result != '' && $result != null && $result !== true && $result !== false) + else if( $result !== '' && is_string($result) ) $errors[] = array($result); # A string representing a message-id - else if ($result === false ) + else if( $result === false ) $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that" } - if ($doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) { - if ($result != array() && is_array($result) && !is_array($result[0])) + if( $doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array(&$this,&$user,$action,&$result) ) ) { + if( is_array($result) && count($result) && !is_array($result[0]) ) $errors[] = $result; # A single array representing an error - else if (is_array($result) && is_array($result[0])) + else if( is_array($result) && is_array($result[0]) ) $errors = array_merge( $errors, $result ); # A nested array representing multiple errors - else if ($result != '' && $result != null && $result !== true && $result !== false) + else if( $result !== '' && is_string($result) ) $errors[] = array($result); # A string representing a message-id - else if ($result === false ) + else if( $result === false ) $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that" } + // TODO: document $specialOKActions = array( 'createaccount', 'execute' ); if( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions) ) { $errors[] = array('ns-specialprotected'); } - if ( $this->isNamespaceProtected() ) { - $ns = $this->getNamespace() == NS_MAIN - ? wfMsg( 'nstab-main' ) - : $this->getNsText(); - $errors[] = (NS_MEDIAWIKI == $this->mNamespace - ? array('protectedinterface') - : array( 'namespaceprotected', $ns ) ); - } - - if( $this->mDbkeyform == '_' ) { - # FIXME: Is this necessary? Shouldn't be allowed anyway... - $errors[] = array('badaccess-group0'); + if( $this->isNamespaceProtected() ) { + $ns = $this->getNamespace() == NS_MAIN ? + wfMsg( 'nstab-main' ) : $this->getNsText(); + $errors[] = NS_MEDIAWIKI == $this->mNamespace ? + array('protectedinterface') : array( 'namespaceprotected', $ns ); } # protect css/js subpages of user pages # XXX: this might be better using restrictions # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working - if( $this->isCssJsSubpage() - && !$user->isAllowed('editusercssjs') - && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) { + if( $this->isCssJsSubpage() && !$user->isAllowed('editusercssjs') + && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) + { $errors[] = array('customcssjsprotected'); } - if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { + if( $doExpensiveQueries && !$this->isCssJsSubpage() ) { # We /could/ use the protection level on the source page, but it's fairly ugly # as we have to establish a precedence hierarchy for pages included by multiple # cascade-protected pages. So just restrict it to people with 'protect' permission, @@ -1237,18 +1175,18 @@ class Title { foreach( $this->getRestrictions($action) as $right ) { // Backwards compatibility, rewrite sysop -> protect - if ( $right == 'sysop' ) { + if( $right == 'sysop' ) { $right = 'protect'; } if( '' != $right && !$user->isAllowed( $right ) ) { - //Users with 'editprotected' permission can edit protected pages + // Users with 'editprotected' permission can edit protected pages if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) { - //Users with 'editprotected' permission cannot edit protected pages - //with cascading option turned on. - if($this->mCascadeRestriction) { + // Users with 'editprotected' permission cannot edit protected pages + // with cascading option turned on. + if( $this->mCascadeRestriction ) { $errors[] = array( 'protectedpagetext', $right ); } else { - //Nothing, user can edit! + // Nothing, user can edit! } } else { $errors[] = array( 'protectedpagetext', $right ); @@ -1256,57 +1194,76 @@ class Title { } } - if ($action == 'protect') { - if ($this->getUserPermissionsErrors('edit', $user) != array()) { + if( $action == 'protect' ) { + if( $this->getUserPermissionsErrors('edit', $user) != array() ) { $errors[] = array( 'protect-cantedit' ); // If they can't edit, they shouldn't protect. } } - if ($action == 'create') { + if( $action == 'create' ) { $title_protection = $this->getTitleProtection(); + if( is_array($title_protection) ) { + extract($title_protection); // is this extract() really needed? - if (is_array($title_protection)) { - extract($title_protection); - - if ($pt_create_perm == 'sysop') - $pt_create_perm = 'protect'; - - if ($pt_create_perm == '' || !$user->isAllowed($pt_create_perm)) { - $errors[] = array ( 'titleprotected', User::whoIs($pt_user), $pt_reason ); + if( $pt_create_perm == 'sysop' ) { + $pt_create_perm = 'protect'; // B/C + } + if( $pt_create_perm == '' || !$user->isAllowed($pt_create_perm) ) { + $errors[] = array( 'titleprotected', User::whoIs($pt_user), $pt_reason ); } } - if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || - ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) { + if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || + ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) + { $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin'); } - } elseif( $action == 'move' && !( $this->isMovable() && $user->isAllowed( 'move' ) ) ) { - $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); - } elseif ( !$user->isAllowed( $action ) ) { - $return = null; - $groups = array(); - global $wgGroupPermissions; - foreach( $wgGroupPermissions as $key => $value ) { - if( isset( $value[$action] ) && $value[$action] == true ) { - $groupName = User::getGroupName( $key ); - $groupPage = User::getGroupPage( $key ); - if( $groupPage ) { - $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]'; - } else { - $groups[] = $groupName; - } - } + } elseif( $action == 'move' ) { + if( !$user->isAllowed( 'move' ) ) { + // User can't move anything + $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); + } elseif( !$user->isAllowed( 'move-rootuserpages' ) + && $this->getNamespace() == NS_USER && !$this->isSubpage() ) + { + // Show user page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-user-page' ); + } + // Check if user is allowed to move files if it's a file + if( $this->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) { + $errors[] = array( 'movenotallowedfile' ); + } + // Check for immobile pages + if( !MWNamespace::isMovable( $this->getNamespace() ) ) { + // Specific message for this case + $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); + } elseif( !$this->isMovable() ) { + // Less specific message for rarer cases + $errors[] = array( 'immobile-page' ); } - $n = count( $groups ); - $groups = implode( ', ', $groups ); - switch( $n ) { - case 0: - case 1: - case 2: - $return = array( "badaccess-group$n", $groups ); - break; - default: - $return = array( 'badaccess-groups', $groups ); + } elseif( $action == 'move-target' ) { + if( !$user->isAllowed( 'move' ) ) { + // User can't move anything + $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); + } elseif( !$user->isAllowed( 'move-rootuserpages' ) + && $this->getNamespace() == NS_USER && !$this->isSubpage() ) + { + // Show user page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-to-user-page' ); + } + if( !MWNamespace::isMovable( $this->getNamespace() ) ) { + $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); + } elseif( !$this->isMovable() ) { + $errors[] = array( 'immobile-target-page' ); + } + } elseif( !$user->isAllowed( $action ) ) { + $return = null; + $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), + User::getGroupsWithPermission( $action ) ); + if( $groups ) { + $return = array( 'badaccess-groups', + array( implode( ', ', $groups ), count( $groups ) ) ); + } else { + $return = array( "badaccess-group0" ); } $errors[] = $return; } @@ -1317,7 +1274,7 @@ class Title { /** * Is this title subject to title protection? - * @return mixed An associative array representing any existent title + * @return \type{\mixed} An associative array representing any existent title * protection, or false if there's none. */ private function getTitleProtection() { @@ -1328,7 +1285,8 @@ class Title { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'protected_titles', '*', - array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()) ); + array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), + __METHOD__ ); if ($row = $dbr->fetchRow( $res )) { return $row; @@ -1337,11 +1295,17 @@ class Title { } } + /** + * Update the title protection status + * @param $create_perm \type{\string} Permission required for creation + * @param $reason \type{\string} Reason for protection + * @param $expiry \type{\string} Expiry timestamp + */ public function updateTitleProtection( $create_perm, $reason, $expiry ) { - global $wgGroupPermissions,$wgUser,$wgContLang; + global $wgUser,$wgContLang; if ($create_perm == implode(',',$this->getRestrictions('create')) - && $expiry == $this->mRestrictionsExpiry) { + && $expiry == $this->mRestrictionsExpiry['create']) { // No change return true; } @@ -1354,9 +1318,12 @@ class Title { $expiry_description = ''; if ( $encodedExpiry != 'infinity' ) { - $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')'; + $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) , $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ).')'; } - + else { + $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ).')'; + } + # Update protection table if ($create_perm != '' ) { $dbw->replace( 'protected_titles', array(array('pt_namespace', 'pt_title')), @@ -1373,7 +1340,8 @@ class Title { $log = new LogPage( 'protect' ); if( $create_perm ) { - $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason . " [create=$create_perm] $expiry_description" ) ); + $params = array("[create=$create_perm] $expiry_description",''); + $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason ), $params ); } else { $log->addEntry( 'unprotect', $this, $reason ); } @@ -1382,18 +1350,19 @@ class Title { } /** - * Remove any title protection (due to page existing + * Remove any title protection due to page existing */ public function deleteTitleProtection() { $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'protected_titles', - array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()), __METHOD__ ); + array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), + __METHOD__ ); } /** * Can $wgUser edit this page? - * @return boolean + * @return \type{\bool} TRUE or FALSE * @deprecated use userCan('edit') */ public function userCanEdit( $doExpensiveQueries = true ) { @@ -1402,7 +1371,7 @@ class Title { /** * Can $wgUser create this page? - * @return boolean + * @return \type{\bool} TRUE or FALSE * @deprecated use userCan('create') */ public function userCanCreate( $doExpensiveQueries = true ) { @@ -1411,7 +1380,7 @@ class Title { /** * Can $wgUser move this page? - * @return boolean + * @return \type{\bool} TRUE or FALSE * @deprecated use userCan('move') */ public function userCanMove( $doExpensiveQueries = true ) { @@ -1422,16 +1391,15 @@ class Title { * Would anybody with sufficient privileges be able to move this page? * Some pages just aren't movable. * - * @return boolean + * @return \type{\bool} TRUE or FALSE */ public function isMovable() { - return MWNamespace::isMovable( $this->getNamespace() ) - && $this->getInterwiki() == ''; + return MWNamespace::isMovable( $this->getNamespace() ) && $this->getInterwiki() == ''; } /** * Can $wgUser read this page? - * @return boolean + * @return \type{\bool} TRUE or FALSE * @todo fold these checks into userCan() */ public function userCanRead() { @@ -1508,7 +1476,7 @@ class Title { /** * Is this a talk page of some sort? - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function isTalkPage() { return MWNamespace::isTalk( $this->getNamespace() ); @@ -1516,7 +1484,7 @@ class Title { /** * Is this a subpage? - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function isSubpage() { return MWNamespace::hasSubpages( $this->mNamespace ) @@ -1526,7 +1494,7 @@ class Title { /** * Does this have subpages? (Warning, usually requires an extra DB query.) - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function hasSubpages() { if( !MWNamespace::hasSubpages( $this->mNamespace ) ) { @@ -1554,7 +1522,7 @@ class Title { * Could this page contain custom CSS or JavaScript, based * on the title? * - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function isCssOrJsPage() { return $this->mNamespace == NS_MEDIAWIKI @@ -1563,7 +1531,7 @@ class Title { /** * Is this a .css or .js subpage of a user page? - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function isCssJsSubpage() { return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.(?:css|js)$/", $this->mTextform ) ); @@ -1571,6 +1539,7 @@ class Title { /** * Is this a *valid* .css or .js subpage of a user page? * Check that the corresponding skin exists + * @return \type{\bool} TRUE or FALSE */ public function isValidCssJsSubpage() { if ( $this->isCssJsSubpage() ) { @@ -1590,14 +1559,14 @@ class Title { } /** * Is this a .css subpage of a user page? - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function isCssSubpage() { return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.css$/", $this->mTextform ) ); } /** * Is this a .js subpage of a user page? - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function isJsSubpage() { return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.js$/", $this->mTextform ) ); @@ -1606,7 +1575,7 @@ class Title { * Protect css/js subpages of user pages: can $wgUser edit * this page? * - * @return boolean + * @return \type{\bool} TRUE or FALSE * @todo XXX: this might be better using restrictions */ public function userCanEditCssJsSubpage() { @@ -1617,7 +1586,7 @@ class Title { /** * Cascading protection: Return true if cascading restrictions apply to this page, false if not. * - * @return bool If the page is subject to cascading restrictions. + * @return \type{\bool} If the page is subject to cascading restrictions. */ public function isCascadeProtected() { list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); @@ -1627,10 +1596,10 @@ class Title { /** * Cascading protection: Get the source of any cascading restrictions on this page. * - * @param $get_pages bool Whether or not to retrieve the actual pages that the restrictions have come from. - * @return array( mixed title array, restriction array) - * Array of the Title objects of the pages from which cascading restrictions have come, false for none, or true if such restrictions exist, but $get_pages was not set. - * The restriction array is an array of each type, each of which contains an array of unique groups + * @param $get_pages \type{\bool} Whether or not to retrieve the actual pages that the restrictions have come from. + * @return \type{\arrayof{mixed title array, restriction array}} Array of the Title objects of the pages from + * which cascading restrictions have come, false for none, or true if such restrictions exist, but $get_pages was not set. + * The restriction array is an array of each type, each of which contains an array of unique groups. */ public function getCascadeProtectionSources( $get_pages = true ) { global $wgRestrictionTypes; @@ -1648,9 +1617,9 @@ class Title { wfProfileIn( __METHOD__ ); - $dbr = wfGetDb( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); - if ( $this->getNamespace() == NS_IMAGE ) { + if ( $this->getNamespace() == NS_FILE ) { $tables = array ('imagelinks', 'page_restrictions'); $where_clauses = array( 'il_to' => $this->getDBkey(), @@ -1679,7 +1648,7 @@ class Title { $now = wfTimestampNow(); $purgeExpired = false; - while( $row = $dbr->fetchObject( $res ) ) { + foreach( $res as $row ) { $expiry = Block::decodeExpiry( $row->pr_expiry ); if( $expiry > $now ) { if ($get_pages) { @@ -1712,7 +1681,6 @@ class Title { } else { $this->mHasCascadingRestrictions = $sources; } - return array( $sources, $pagerestrictions ); } @@ -1726,7 +1694,7 @@ class Title { /** * Loads a string into mRestrictions array - * @param resource $res restrictions as an SQL result. + * @param $res \type{Resource} restrictions as an SQL result. */ private function loadRestrictionsFromRow( $res, $oldFashionedRestrictions = NULL ) { global $wgRestrictionTypes; @@ -1734,10 +1702,10 @@ class Title { foreach( $wgRestrictionTypes as $type ){ $this->mRestrictions[$type] = array(); + $this->mRestrictionsExpiry[$type] = Block::decodeExpiry(''); } $this->mCascadeRestriction = false; - $this->mRestrictionsExpiry = Block::decodeExpiry(''); # Backwards-compatibility: also load the restrictions from the page record (old format). @@ -1768,7 +1736,7 @@ class Title { $now = wfTimestampNow(); $purgeExpired = false; - while ($row = $dbr->fetchObject( $res ) ) { + foreach( $res as $row ) { # Cycle through all the restrictions. // Don't take care of restrictions types that aren't in $wgRestrictionTypes @@ -1781,7 +1749,7 @@ class Title { // Only apply the restrictions if they haven't expired! if ( !$expiry || $expiry > $now ) { - $this->mRestrictionsExpiry = $expiry; + $this->mRestrictionsExpiry[$row->pr_type] = $expiry; $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); $this->mCascadeRestriction |= $row->pr_cascade; @@ -1799,6 +1767,9 @@ class Title { $this->mRestrictionsLoaded = true; } + /** + * Load restrictions from the page_restrictions table + */ public function loadRestrictions( $oldFashionedRestrictions = NULL ) { if( !$this->mRestrictionsLoaded ) { if ($this->exists()) { @@ -1819,13 +1790,13 @@ class Title { if (!$expiry || $expiry > $now) { // Apply the restrictions - $this->mRestrictionsExpiry = $expiry; + $this->mRestrictionsExpiry['create'] = $expiry; $this->mRestrictions['create'] = explode(',', trim($pt_create_perm) ); } else { // Get rid of the old restrictions Title::purgeExpiredRestrictions(); } } else { - $this->mRestrictionsExpiry = Block::decodeExpiry(''); + $this->mRestrictionsExpiry['create'] = Block::decodeExpiry(''); } $this->mRestrictionsLoaded = true; } @@ -1849,8 +1820,8 @@ class Title { /** * Accessor/initialisation for mRestrictions * - * @param string $action action that permission needs to be checked for - * @return array the array of groups allowed to edit this article + * @param $action \type{\string} action that permission needs to be checked for + * @return \type{\arrayof{\string}} the array of groups allowed to edit this article */ public function getRestrictions( $action ) { if( !$this->mRestrictionsLoaded ) { @@ -1862,8 +1833,20 @@ class Title { } /** + * Get the expiry time for the restriction against a given action + * @return 14-char timestamp, or 'infinity' if the page is protected forever + * or not protected at all, or false if the action is not recognised. + */ + public function getRestrictionExpiry( $action ) { + if( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); + } + return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; + } + + /** * Is there a version of this page in the deletion archive? - * @return int the number of archived revisions + * @return \type{\int} the number of archived revisions */ public function isDeleted() { $fname = 'Title::isDeleted'; @@ -1873,7 +1856,7 @@ class Title { $dbr = wfGetDB( DB_SLAVE ); $n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), $fname ); - if( $this->getNamespace() == NS_IMAGE ) { + if( $this->getNamespace() == NS_FILE ) { $n += $dbr->selectField( 'filearchive', 'COUNT(*)', array( 'fa_name' => $this->getDBkey() ), $fname ); } @@ -1884,18 +1867,22 @@ class Title { /** * Get the article ID for this Title from the link cache, * adding it if necessary - * @param int $flags a bit field; may be GAID_FOR_UPDATE to select + * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select * for update - * @return int the ID + * @return \type{\int} the ID */ public function getArticleID( $flags = 0 ) { + if( $this->getNamespace() < 0 ) { + return $this->mArticleID = 0; + } $linkCache = LinkCache::singleton(); - if ( $flags & GAID_FOR_UPDATE ) { + if( $flags & GAID_FOR_UPDATE ) { $oldUpdate = $linkCache->forUpdate( true ); + $linkCache->clearLink( $this ); $this->mArticleID = $linkCache->addLinkObj( $this ); $linkCache->forUpdate( $oldUpdate ); } else { - if ( -1 == $this->mArticleID ) { + if( -1 == $this->mArticleID ) { $this->mArticleID = $linkCache->addLinkObj( $this ); } } @@ -1905,16 +1892,15 @@ class Title { /** * Is this an article that is a redirect page? * Uses link cache, adding it if necessary - * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update - * @return bool + * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update + * @return \type{\bool} */ public function isRedirect( $flags = 0 ) { if( !is_null($this->mRedirect) ) return $this->mRedirect; - # Zero for special pages. - # Also, calling getArticleID() loads the field from cache! - if( !$this->getArticleID($flags) || $this->getNamespace() == NS_SPECIAL ) { - return false; + # Calling getArticleID() loads the field from cache as needed + if( !$this->getArticleID($flags) ) { + return $this->mRedirect = false; } $linkCache = LinkCache::singleton(); $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' ); @@ -1925,16 +1911,15 @@ class Title { /** * What is the length of this page? * Uses link cache, adding it if necessary - * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update - * @return bool + * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update + * @return \type{\bool} */ public function getLength( $flags = 0 ) { if( $this->mLength != -1 ) return $this->mLength; - # Zero for special pages. - # Also, calling getArticleID() loads the field from cache! - if( !$this->getArticleID($flags) || $this->getNamespace() == NS_SPECIAL ) { - return 0; + # Calling getArticleID() loads the field from cache as needed + if( !$this->getArticleID($flags) ) { + return $this->mLength = 0; } $linkCache = LinkCache::singleton(); $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) ); @@ -1944,18 +1929,16 @@ class Title { /** * What is the page_latest field for this page? - * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update - * @return int + * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update + * @return \type{\int} */ public function getLatestRevID( $flags = 0 ) { - if ($this->mLatestID !== false) + if( $this->mLatestID !== false ) return $this->mLatestID; $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB(DB_MASTER) : wfGetDB(DB_SLAVE); - return $this->mLatestID = $db->selectField( 'revision', - "max(rev_id)", - array('rev_page' => $this->getArticleID($flags)), - 'Title::getLatestRevID' ); + $this->mLatestID = $db->selectField( 'page', 'page_latest', $this->pageCond(), __METHOD__ ); + return $this->mLatestID; } /** @@ -1966,7 +1949,7 @@ class Title { * loading of the new page_id. It's also called from * Article::doDeleteArticle() * - * @param int $newid the new Article ID + * @param $newid \type{\int} the new Article ID */ public function resetArticleID( $newid ) { $linkCache = LinkCache::singleton(); @@ -1980,30 +1963,19 @@ class Title { /** * Updates page_touched for this page; called from LinksUpdate.php - * @return bool true if the update succeded + * @return \type{\bool} true if the update succeded */ public function invalidateCache() { - global $wgUseFileCache; - - if ( wfReadOnly() ) { + if( wfReadOnly() ) { return; } - $dbw = wfGetDB( DB_MASTER ); $success = $dbw->update( 'page', - array( /* SET */ - 'page_touched' => $dbw->timestamp() - ), array( /* WHERE */ - 'page_namespace' => $this->getNamespace() , - 'page_title' => $this->getDBkey() - ), 'Title::invalidateCache' + array( 'page_touched' => $dbw->timestamp() ), + $this->pageCond(), + __METHOD__ ); - - if ($wgUseFileCache) { - $cache = new HTMLFileCache($this); - @unlink($cache->fileCacheName()); - } - + HTMLFileCache::clearFileCache( $this ); return $success; } @@ -2011,8 +1983,8 @@ class Title { * Prefix some arbitrary text with the namespace or interwiki prefix * of this object * - * @param string $name the text - * @return string the prefixed text + * @param $name \type{\string} the text + * @return \type{\string} the prefixed text * @private */ /* private */ function prefix( $name ) { @@ -2034,7 +2006,7 @@ class Title { * removes illegal characters, splits off the interwiki and * namespace prefixes, sets the other forms, and canonicalizes * everything. - * @return bool true on success + * @return \type{\bool} true on success */ private function secureAndSplit() { global $wgContLang, $wgLocalInterwiki, $wgCapitalLinks; @@ -2064,8 +2036,7 @@ class Title { # Strip Unicode bidi override characters. # Sometimes they slip into cut-n-pasted page titles, where the # override chars get included in list displays. - $dbkey = str_replace( "\xE2\x80\x8E", '', $dbkey ); // 200E LEFT-TO-RIGHT MARK - $dbkey = str_replace( "\xE2\x80\x8F", '', $dbkey ); // 200F RIGHT-TO-LEFT MARK + $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey ); # Clean up whitespace # @@ -2101,7 +2072,7 @@ class Title { # Ordinary namespace $dbkey = $m[2]; $this->mNamespace = $ns; - } elseif( $this->getInterwikiLink( $p ) ) { + } elseif( Interwiki::isValidInterwiki( $p ) ) { if( !$firstPass ) { # Can't make a local interwiki link to an interwiki link. # That's just crazy! @@ -2158,9 +2129,9 @@ class Title { } /** - * Pages with "/./" or "/../" appearing in the URLs will - * often be unreachable due to the way web browsers deal - * with 'relative' URLs. Forbid them explicitly. + * Pages with "/./" or "/../" appearing in the URLs will often be un- + * reachable due to the way web browsers deal with 'relative' URLs. + * Also, they conflict with subpage syntax. Forbid them explicitly. */ if ( strpos( $dbkey, '.' ) !== false && ( $dbkey === '.' || $dbkey === '..' || @@ -2240,13 +2211,14 @@ class Title { } /** - * Set the fragment for this title - * This is kind of bad, since except for this rarely-used function, Title objects - * are immutable. The reason this is here is because it's better than setting the - * members directly, which is what Linker::formatComment was doing previously. + * Set the fragment for this title. Removes the first character from the + * specified fragment before setting, so it assumes you're passing it with + * an initial "#". + * + * Deprecated for public use, use Title::makeTitle() with fragment parameter. + * Still in active use privately. * - * @param string $fragment text - * @todo clarify whether access is supposed to be public (was marked as "kind of public") + * @param $fragment \type{\string} text */ public function setFragment( $fragment ) { $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); @@ -2254,7 +2226,7 @@ class Title { /** * Get a Title object associated with the talk page of this article - * @return Title the object for the talk page + * @return \type{Title} the object for the talk page */ public function getTalkPage() { return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); @@ -2264,10 +2236,15 @@ class Title { * Get a title object associated with the subject page of this * talk page * - * @return Title the object for the subject page + * @return \type{Title} the object for the subject page */ public function getSubjectPage() { - return Title::makeTitle( MWNamespace::getSubject( $this->getNamespace() ), $this->getDBkey() ); + // Is this the same title? + $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); + if( $this->getNamespace() == $subjectNS ) { + return $this; + } + return Title::makeTitle( $subjectNS, $this->getDBkey() ); } /** @@ -2277,8 +2254,8 @@ class Title { * WARNING: do not use this function on arbitrary user-supplied titles! * On heavily-used templates it will max out the memory. * - * @param string $options may be FOR UPDATE - * @return array the Title objects linking here + * @param $options \type{\string} may be FOR UPDATE + * @return \type{\arrayof{Title}} the Title objects linking here */ public function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) { $linkCache = LinkCache::singleton(); @@ -2295,12 +2272,12 @@ class Title { "{$prefix}_from=page_id", "{$prefix}_namespace" => $this->getNamespace(), "{$prefix}_title" => $this->getDBkey() ), - 'Title::getLinksTo', + __METHOD__, $options ); $retVal = array(); if ( $db->numRows( $res ) ) { - while ( $row = $db->fetchObject( $res ) ) { + foreach( $res as $row ) { if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) { $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect ); $retVal[] = $titleObj; @@ -2318,8 +2295,8 @@ class Title { * WARNING: do not use this function on arbitrary user-supplied titles! * On heavily-used templates it will max out the memory. * - * @param string $options may be FOR UPDATE - * @return array the Title objects linking here + * @param $options \type{\string} may be FOR UPDATE + * @return \type{\arrayof{Title}} the Title objects linking here */ public function getTemplateLinksTo( $options = '' ) { return $this->getLinksTo( $options, 'templatelinks', 'tl' ); @@ -2329,8 +2306,8 @@ class Title { * Get an array of Title objects referring to non-existent articles linked from this page * * @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case) - * @param string $options may be FOR UPDATE - * @return array the Title objects + * @param $options \type{\string} may be FOR UPDATE + * @return \type{\arrayof{Title}} the Title objects */ public function getBrokenLinksFrom( $options = '' ) { if ( $this->getArticleId() == 0 ) { @@ -2360,7 +2337,7 @@ class Title { $retVal = array(); if ( $db->numRows( $res ) ) { - while ( $row = $db->fetchObject( $res ) ) { + foreach( $res as $row ) { $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); } } @@ -2373,7 +2350,7 @@ class Title { * Get a list of URLs to purge from the Squid cache when this * page changes * - * @return array the URLs + * @return \type{\arrayof{\string}} the URLs */ public function getSquidURLs() { global $wgContLang; @@ -2395,6 +2372,9 @@ class Title { return $urls; } + /** + * Purge all applicable Squid URLs + */ public function purgeSquid() { global $wgUseSquid; if ( $wgUseSquid ) { @@ -2406,7 +2386,7 @@ class Title { /** * Move this page without authentication - * @param Title &$nt the new page Title + * @param &$nt \type{Title} the new page Title */ public function moveNoAuth( &$nt ) { return $this->moveTo( $nt, false ); @@ -2415,13 +2395,15 @@ class Title { /** * Check whether a given move operation would be valid. * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise - * @param Title &$nt the new title - * @param bool $auth indicates whether $wgUser's permissions + * @param &$nt \type{Title} the new title + * @param $auth \type{\bool} indicates whether $wgUser's permissions * should be checked - * @param string $reason is the log summary of the move, used for spam checking - * @return mixed True on success, getUserPermissionsErrors()-like array on failure + * @param $reason \type{\string} is the log summary of the move, used for spam checking + * @return \type{\mixed} True on success, getUserPermissionsErrors()-like array on failure */ public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { + global $wgUser; + $errors = array(); if( !$nt ) { // Normally we'd add this to $errors, but we'll get @@ -2431,8 +2413,14 @@ class Title { if( $this->equals( $nt ) ) { $errors[] = array('selfmove'); } - if( !$this->isMovable() || !$nt->isMovable() ) { - $errors[] = array('immobile_namespace'); + if( !$this->isMovable() ) { + $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); + } + if ( $nt->getInterwiki() != '' ) { + $errors[] = array( 'immobile-target-namespace-iw' ); + } + if ( !$nt->isMovable() ) { + $errors[] = array('immobile-target-namespace', $nt->getNsText() ); } $oldid = $this->getArticleID(); @@ -2448,31 +2436,35 @@ class Title { } // Image-specific checks - if( $this->getNamespace() == NS_IMAGE ) { + if( $this->getNamespace() == NS_FILE ) { $file = wfLocalFile( $this ); if( $file->exists() ) { - if( $nt->getNamespace() != NS_IMAGE ) { + if( $nt->getNamespace() != NS_FILE ) { $errors[] = array('imagenocrossnamespace'); } if( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) { $errors[] = array('imageinvalidfilename'); } - if( !File::checkExtensionCompatibility( $file, $nt->getDbKey() ) ) { + if( !File::checkExtensionCompatibility( $file, $nt->getDBKey() ) ) { $errors[] = array('imagetypemismatch'); } } } if ( $auth ) { - global $wgUser; - $errors = array_merge($errors, - $this->getUserPermissionsErrors('move', $wgUser), - $this->getUserPermissionsErrors('edit', $wgUser), - $nt->getUserPermissionsErrors('move', $wgUser), - $nt->getUserPermissionsErrors('edit', $wgUser)); + $errors = wfMergeErrorArrays( $errors, + $this->getUserPermissionsErrors('move', $wgUser), + $this->getUserPermissionsErrors('edit', $wgUser), + $nt->getUserPermissionsErrors('move-target', $wgUser), + $nt->getUserPermissionsErrors('edit', $wgUser) ); } - global $wgUser; + $match = EditPage::matchSpamRegex( $reason ); + if( $match !== false ) { + // This is kind of lame, won't display nice + $errors[] = array('spamprotectiontext'); + } + $err = null; if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) { $errors[] = array('hookaborted', $err); @@ -2500,13 +2492,13 @@ class Title { /** * Move a title to a new location - * @param Title &$nt the new title - * @param bool $auth indicates whether $wgUser's permissions + * @param &$nt \type{Title} the new title + * @param $auth \type{\bool} indicates whether $wgUser's permissions * should be checked - * @param string $reason The reason for the move - * @param bool $createRedirect Whether to create a redirect from the old title to the new title. + * @param $reason \type{\string} The reason for the move + * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title. * Ignored if the user doesn't have the suppressredirect right. - * @return mixed true on success, getUserPermissionsErrors()-like array on failure + * @return \type{\mixed} true on success, getUserPermissionsErrors()-like array on failure */ public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { $err = $this->isValidMoveOperation( $nt, $auth, $reason ); @@ -2515,6 +2507,7 @@ class Title { } $pageid = $this->getArticleID(); + $protected = $this->isProtected(); if( $nt->exists() ) { $err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect ); $pageCountChange = ($createRedirect ? 0 : -1); @@ -2549,8 +2542,29 @@ class Title { 'cl_sortkey' => $this->getPrefixedText() ), __METHOD__ ); - # Update watchlists + if( $protected ) { + # Protect the redirect title as the title used to be... + $dbw->insertSelect( 'page_restrictions', 'page_restrictions', + array( + 'pr_page' => $redirid, + 'pr_type' => 'pr_type', + 'pr_level' => 'pr_level', + 'pr_cascade' => 'pr_cascade', + 'pr_user' => 'pr_user', + 'pr_expiry' => 'pr_expiry' + ), + array( 'pr_page' => $pageid ), + __METHOD__, + array( 'IGNORE' ) + ); + # Update the protection log + $log = new LogPage( 'protect' ); + $comment = wfMsgForContent('prot_1movedto2',$this->getPrefixedText(), $nt->getPrefixedText() ); + if( $reason ) $comment .= ': ' . $reason; + $log->addEntry( 'move_prot', $nt, $comment, array($this->getPrefixedText()) ); // FIXME: $params? + } + # Update watchlists $oldnamespace = $this->getNamespace() & ~1; $newnamespace = $nt->getNamespace() & ~1; $oldtitle = $this->getDBkey(); @@ -2602,10 +2616,10 @@ class Title { * Move page to a title which is at present a redirect to the * source page * - * @param Title &$nt the page to move to, which should currently + * @param &$nt \type{Title} the page to move to, which should currently * be a redirect - * @param string $reason The reason for the move - * @param bool $createRedirect Whether to leave a redirect at the old title. + * @param $reason \type{\string} The reason for the move + * @param $createRedirect \type{\bool} Whether to leave a redirect at the old title. * Ignored if the user doesn't have the suppressredirect right */ private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) { @@ -2620,9 +2634,9 @@ class Title { $now = wfTimestampNow(); $newid = $nt->getArticleID(); $oldid = $this->getArticleID(); + $latest = $this->getLatestRevID(); $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); # Delete the old redirect. We don't save it to history since # by definition if we've got here it's rather uninteresting. @@ -2648,7 +2662,7 @@ class Title { $nullRevId = $nullRevision->insertOn( $dbw ); $article = new Article( $this ); - wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) ); + wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) ); # Change the name of the target page: $dbw->update( 'page', @@ -2676,7 +2690,7 @@ class Title { $redirectRevision->insertOn( $dbw ); $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); - wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) ); + wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false, $wgUser) ); # Now, we record the link from the redirect to the new title. # It should have no other outgoing links... @@ -2687,12 +2701,14 @@ class Title { 'pl_namespace' => $nt->getNamespace(), 'pl_title' => $nt->getDBkey() ), $fname ); + $redirectSuppressed = false; } else { $this->resetArticleID( 0 ); + $redirectSuppressed = true; } - + # Move an image if this is a file - if( $this->getNamespace() == NS_IMAGE ) { + if( $this->getNamespace() == NS_FILE ) { $file = wfLocalFile( $this ); if( $file->exists() ) { $status = $file->move( $nt ); @@ -2702,11 +2718,10 @@ class Title { } } } - $dbw->commit(); # Log the move $log = new LogPage( 'move' ); - $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) ); + $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) ); # Purge squid if ( $wgUseSquid ) { @@ -2719,9 +2734,9 @@ class Title { /** * Move page to non-existing title. - * @param Title &$nt the new Title - * @param string $reason The reason for the move - * @param bool $createRedirect Whether to create a redirect from the old title to the new title + * @param &$nt \type{Title} the new Title + * @param $reason \type{\string} The reason for the move + * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title * Ignored if the user doesn't have the suppressredirect right */ private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) { @@ -2729,14 +2744,16 @@ class Title { $fname = 'MovePageForm::moveToNewTitle'; $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() ); if ( $reason ) { - $comment .= ": $reason"; + $comment .= wfMsgExt( 'colon-separator', + array( 'escapenoentities', 'content' ) ); + $comment .= $reason; } $newid = $nt->getArticleID(); $oldid = $this->getArticleID(); + $latest = $this->getLatestRevId(); $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); $now = $dbw->timestamp(); # Save a null revision in the page's history notifying of the move @@ -2744,7 +2761,7 @@ class Title { $nullRevId = $nullRevision->insertOn( $dbw ); $article = new Article( $this ); - wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) ); + wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) ); # Rename page entry $dbw->update( 'page', @@ -2772,7 +2789,7 @@ class Title { $redirectRevision->insertOn( $dbw ); $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); - wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) ); + wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false, $wgUser) ); # Record the just-created redirect's linking to the page $dbw->insert( 'pagelinks', @@ -2781,12 +2798,14 @@ class Title { 'pl_namespace' => $nt->getNamespace(), 'pl_title' => $nt->getDBkey() ), $fname ); + $redirectSuppressed = false; } else { $this->resetArticleID( 0 ); + $redirectSuppressed = true; } - + # Move an image if this is a file - if( $this->getNamespace() == NS_IMAGE ) { + if( $this->getNamespace() == NS_FILE ) { $file = wfLocalFile( $this ); if( $file->exists() ) { $status = $file->move( $nt ); @@ -2796,11 +2815,10 @@ class Title { } } } - $dbw->commit(); # Log the move $log = new LogPage( 'move' ); - $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText()) ); + $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) ); # Purge caches as per article creation Article::onArticleCreate( $nt ); @@ -2810,41 +2828,69 @@ class Title { $this->purgeSquid(); } + + /** + * Checks if this page is just a one-rev redirect. + * Adds lock, so don't use just for light purposes. + * + * @return \type{\bool} TRUE or FALSE + */ + public function isSingleRevRedirect() { + $dbw = wfGetDB( DB_MASTER ); + # Is it a redirect? + $row = $dbw->selectRow( 'page', + array( 'page_is_redirect', 'page_latest', 'page_id' ), + $this->pageCond(), + __METHOD__, + 'FOR UPDATE' + ); + # Cache some fields we may want + $this->mArticleID = $row ? intval($row->page_id) : 0; + $this->mRedirect = $row ? (bool)$row->page_is_redirect : false; + $this->mLatestID = $row ? intval($row->page_latest) : false; + if( !$this->mRedirect ) { + return false; + } + # Does the article have a history? + $row = $dbw->selectField( array( 'page', 'revision'), + 'rev_id', + array( 'page_namespace' => $this->getNamespace(), + 'page_title' => $this->getDBkey(), + 'page_id=rev_page', + 'page_latest != rev_id' + ), + __METHOD__, + 'FOR UPDATE' + ); + # Return true if there was no history + return ($row === false); + } /** * Checks if $this can be moved to a given Title * - Selects for update, so don't call it unless you mean business * - * @param Title &$nt the new title to check + * @param &$nt \type{Title} the new title to check + * @return \type{\bool} TRUE or FALSE */ public function isValidMoveTarget( $nt ) { - - $fname = 'Title::isValidMoveTarget'; $dbw = wfGetDB( DB_MASTER ); - # Is it an existsing file? - if( $nt->getNamespace() == NS_IMAGE ) { + if( $nt->getNamespace() == NS_FILE ) { $file = wfLocalFile( $nt ); if( $file->exists() ) { wfDebug( __METHOD__ . ": file exists\n" ); return false; } } - - # Is it a redirect? - $id = $nt->getArticleID(); - $obj = $dbw->selectRow( array( 'page', 'revision', 'text'), - array( 'page_is_redirect','old_text','old_flags' ), - array( 'page_id' => $id, 'page_latest=rev_id', 'rev_text_id=old_id' ), - $fname, 'FOR UPDATE' ); - - if ( !$obj || 0 == $obj->page_is_redirect ) { - # Not a redirect - wfDebug( __METHOD__ . ": not a redirect\n" ); + # Is it a redirect with no history? + if( !$nt->isSingleRevRedirect() ) { + wfDebug( __METHOD__ . ": not a one-rev redirect\n" ); return false; } - $text = Revision::getRevisionText( $obj ); - + # Get the article text + $rev = Revision::newFromTitle( $nt ); + $text = $rev->getText(); # Does the redirect point to the source? # Or is it a broken self-redirect, usually caused by namespace collisions? $m = array(); @@ -2861,35 +2907,23 @@ class Title { wfDebug( __METHOD__ . ": failsafe\n" ); return false; } - - # Does the article have a history? - $row = $dbw->selectRow( array( 'page', 'revision'), - array( 'rev_id' ), - array( 'page_namespace' => $nt->getNamespace(), - 'page_title' => $nt->getDBkey(), - 'page_id=rev_page AND page_latest != rev_id' - ), $fname, 'FOR UPDATE' - ); - - # Return true if there was no history - return $row === false; + return true; } /** * Can this title be added to a user's watchlist? * - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function isWatchable() { - return !$this->isExternal() - && MWNamespace::isWatchable( $this->getNamespace() ); + return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); } /** * Get categories to which this Title belongs and return an array of * categories' names. * - * @return array an array of parents in the form: + * @return \type{\array} array an array of parents in the form: * $parent => $currentarticle */ public function getParentCategories() { @@ -2908,9 +2942,9 @@ class Title { $res = $dbr->query( $sql ); if( $dbr->numRows( $res ) > 0 ) { - while( $x = $dbr->fetchObject( $res ) ) - //$data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to); - $data[$wgContLang->getNSText( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText(); + foreach( $res as $row ) + //$data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to); + $data[$wgContLang->getNSText( NS_CATEGORY ).':'.$row->cl_to] = $this->getFullText(); $dbr->freeResult( $res ); } else { $data = array(); @@ -2920,8 +2954,8 @@ class Title { /** * Get a tree of parent categories - * @param array $children an array with the children in the keys, to check for circular refs - * @return array + * @param $children \type{\array} an array with the children in the keys, to check for circular refs + * @return \type{\array} Tree of parent categories */ public function getParentCategoryTree( $children = array() ) { $stack = array(); @@ -2950,25 +2984,30 @@ class Title { * Get an associative array for selecting this title from * the "page" table * - * @return array + * @return \type{\array} Selection array */ public function pageCond() { - return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); + if( $this->mArticleID > 0 ) { + // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs + return array( 'page_id' => $this->mArticleID ); + } else { + return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); + } } /** * Get the revision ID of the previous revision * - * @param integer $revision Revision ID. Get the revision that was before this one. - * @param integer $flags, GAID_FOR_UPDATE - * @return integer $oldrevision|false + * @param $revId \type{\int} Revision ID. Get the revision that was before this one. + * @param $flags \type{\int} GAID_FOR_UPDATE + * @return \twotypes{\int,\bool} Old revision ID, or FALSE if none exists */ - public function getPreviousRevisionID( $revision, $flags=0 ) { + public function getPreviousRevisionID( $revId, $flags=0 ) { $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); return $db->selectField( 'revision', 'rev_id', array( 'rev_page' => $this->getArticleId($flags), - 'rev_id < ' . intval( $revision ) + 'rev_id < ' . intval( $revId ) ), __METHOD__, array( 'ORDER BY' => 'rev_id DESC' ) @@ -2978,29 +3017,56 @@ class Title { /** * Get the revision ID of the next revision * - * @param integer $revision Revision ID. Get the revision that was after this one. - * @param integer $flags, GAID_FOR_UPDATE - * @return integer $oldrevision|false + * @param $revId \type{\int} Revision ID. Get the revision that was after this one. + * @param $flags \type{\int} GAID_FOR_UPDATE + * @return \twotypes{\int,\bool} Next revision ID, or FALSE if none exists */ - public function getNextRevisionID( $revision, $flags=0 ) { + public function getNextRevisionID( $revId, $flags=0 ) { $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); return $db->selectField( 'revision', 'rev_id', array( 'rev_page' => $this->getArticleId($flags), - 'rev_id > ' . intval( $revision ) + 'rev_id > ' . intval( $revId ) ), __METHOD__, array( 'ORDER BY' => 'rev_id' ) ); } + + /** + * Check if this is a new page + * + * @return bool + */ + public function isNewPage() { + $dbr = wfGetDB( DB_SLAVE ); + return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); + } + + /** + * Get the oldest revision timestamp of this page + * + * @return string, MW timestamp + */ + public function getEarliestRevTime() { + $dbr = wfGetDB( DB_SLAVE ); + if( $this->exists() ) { + $min = $dbr->selectField( 'revision', + 'MIN(rev_timestamp)', + array( 'rev_page' => $this->getArticleId() ), + __METHOD__ ); + return wfTimestampOrNull( TS_MW, $min ); + } + return null; + } /** * Get the number of revisions between the given revision IDs. * Used for diffs and other things that really need it. * - * @param integer $old Revision ID. - * @param integer $new Revision ID. - * @return integer Number of revisions between these IDs. + * @param $old \type{\int} Revision ID. + * @param $new \type{\int} Revision ID. + * @return \type{\int} Number of revisions between these IDs. */ public function countRevisionsBetween( $old, $new ) { $dbr = wfGetDB( DB_SLAVE ); @@ -3015,10 +3081,10 @@ class Title { /** * Compare with another title. * - * @param Title $title - * @return bool + * @param \type{Title} $title + * @return \type{\bool} TRUE or FALSE */ - public function equals( $title ) { + public function equals( Title $title ) { // Note: === is necessary for proper matching of number-like titles. return $this->getInterwiki() === $title->getInterwiki() && $this->getNamespace() == $title->getNamespace() @@ -3039,36 +3105,85 @@ class Title { /** * Return a string representation of this title * - * @return string + * @return \type{\string} String representation of this title */ public function __toString() { return $this->getPrefixedText(); } /** - * Check if page exists - * @return bool + * Check if page exists. For historical reasons, this function simply + * checks for the existence of the title in the page table, and will + * thus return false for interwiki links, special pages and the like. + * If you want to know if a title can be meaningfully viewed, you should + * probably call the isKnown() method instead. + * + * @return \type{\bool} TRUE or FALSE */ public function exists() { return $this->getArticleId() != 0; } /** - * Do we know that this title definitely exists, or should we otherwise - * consider that it exists? + * Should links to this title be shown as potentially viewable (i.e. as + * "bluelinks"), even if there's no record by this title in the page + * table? * - * @return bool + * This function is semi-deprecated for public use, as well as somewhat + * misleadingly named. You probably just want to call isKnown(), which + * calls this function internally. + * + * (ISSUE: Most of these checks are cheap, but the file existence check + * can potentially be quite expensive. Including it here fixes a lot of + * existing code, but we might want to add an optional parameter to skip + * it and any other expensive checks.) + * + * @return \type{\bool} TRUE or FALSE */ public function isAlwaysKnown() { - // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes - // the full l10n of that language to be loaded. That takes much memory and - // isn't needed. So we strip the language part away. - // Also, extension messages which are not loaded, are shown as red, because - // we don't call MessageCache::loadAllMessages. - list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 ); - return $this->isExternal() - || ( $this->mNamespace == NS_MAIN && $this->mDbkeyform == '' ) - || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $basename ) ); + if( $this->mInterwiki != '' ) { + return true; // any interwiki link might be viewable, for all we know + } + switch( $this->mNamespace ) { + case NS_MEDIA: + case NS_FILE: + return wfFindFile( $this ); // file exists, possibly in a foreign repo + case NS_SPECIAL: + return SpecialPage::exists( $this->getDBKey() ); // valid special page + case NS_MAIN: + return $this->mDbkeyform == ''; // selflink, possibly with fragment + case NS_MEDIAWIKI: + // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes + // the full l10n of that language to be loaded. That takes much memory and + // isn't needed. So we strip the language part away. + // Also, extension messages which are not loaded, are shown as red, because + // we don't call MessageCache::loadAllMessages. + list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 ); + return wfMsgWeirdKey( $basename ); // known system message + default: + return false; + } + } + + /** + * Does this title refer to a page that can (or might) be meaningfully + * viewed? In particular, this function may be used to determine if + * links to the title should be rendered as "bluelinks" (as opposed to + * "redlinks" to non-existent pages). + * + * @return \type{\bool} TRUE or FALSE + */ + public function isKnown() { + return $this->exists() || $this->isAlwaysKnown(); + } + + /** + * Is this in a namespace that allows actual pages? + * + * @return \type{\bool} TRUE or FALSE + */ + public function canExist() { + return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA; } /** @@ -3088,25 +3203,63 @@ class Title { /** * Get the last touched timestamp + * @param Database $db, optional db + * @return \type{\string} Last touched timestamp */ - public function getTouched() { + public function getTouched( $db = NULL ) { + $db = isset($db) ? $db : wfGetDB( DB_SLAVE ); + $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ ); + return $touched; + } + + /** + * Get the timestamp when this page was updated since the user last saw it. + * @param User $user + * @return mixed string/NULL + */ + public function getNotificationTimestamp( $user = NULL ) { + global $wgUser, $wgShowUpdatedMarker; + // Assume current user if none given + if( !$user ) $user = $wgUser; + // Check cache first + $uid = $user->getId(); + if( isset($this->mNotificationTimestamp[$uid]) ) { + return $this->mNotificationTimestamp[$uid]; + } + if( !$uid || !$wgShowUpdatedMarker ) { + return $this->mNotificationTimestamp[$uid] = false; + } + // Don't cache too much! + if( count($this->mNotificationTimestamp) >= self::CACHE_MAX ) { + $this->mNotificationTimestamp = array(); + } $dbr = wfGetDB( DB_SLAVE ); - $touched = $dbr->selectField( 'page', 'page_touched', - array( - 'page_namespace' => $this->getNamespace(), - 'page_title' => $this->getDBkey() - ), __METHOD__ + $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', + 'wl_notificationtimestamp', + array( 'wl_namespace' => $this->getNamespace(), + 'wl_title' => $this->getDBkey(), + 'wl_user' => $user->getId() + ), + __METHOD__ ); - return $touched; + return $this->mNotificationTimestamp[$uid]; } + /** + * Get the trackback URL for this page + * @return \type{\string} Trackback URL + */ public function trackbackURL() { - global $wgTitle, $wgScriptPath, $wgServer; + global $wgScriptPath, $wgServer; return "$wgServer$wgScriptPath/trackback.php?article=" - . htmlspecialchars(urlencode($wgTitle->getPrefixedDBkey())); + . htmlspecialchars(urlencode($this->getPrefixedDBkey())); } + /** + * Get the trackback RDF for this page + * @return \type{\string} Trackback RDF + */ public function trackbackRDF() { $url = htmlspecialchars($this->getFullURL()); $title = htmlspecialchars($this->getText()); @@ -3132,7 +3285,7 @@ class Title { /** * Generate strings used for xml 'id' names in monobook tabs - * @return string + * @return \type{\string} XML 'id' name */ public function getNamespaceKey() { global $wgContLang; @@ -3150,8 +3303,8 @@ class Title { case NS_PROJECT: case NS_PROJECT_TALK: return 'nstab-project'; - case NS_IMAGE: - case NS_IMAGE_TALK: + case NS_FILE: + case NS_FILE_TALK: return 'nstab-image'; case NS_MEDIAWIKI: case NS_MEDIAWIKI_TALK: @@ -3172,7 +3325,7 @@ class Title { /** * Returns true if this title resolves to the named special page - * @param string $name The special page name + * @param $name \type{\string} The special page name */ public function isSpecial( $name ) { if ( $this->getNamespace() == NS_SPECIAL ) { @@ -3186,7 +3339,7 @@ class Title { /** * If the Title refers to a special page alias which is not the local default, - * returns a new Title which points to the local default. Otherwise, returns $this. + * @return \type{Title} A new Title which points to the local default. Otherwise, returns $this. */ public function fixSpecialName() { if ( $this->getNamespace() == NS_SPECIAL ) { @@ -3206,12 +3359,19 @@ class Title { * In other words, is this a content page, for the purposes of calculating * statistics, etc? * - * @return bool + * @return \type{\bool} TRUE or FALSE */ public function isContentPage() { return MWNamespace::isContent( $this->getNamespace() ); } + /** + * Get all extant redirects to this Title + * + * @param $ns \twotypes{\int,\null} Single namespace to consider; + * NULL to consider all namespaces + * @return \type{\arrayof{Title}} Redirects to this title + */ public function getRedirectsHere( $ns = null ) { $redirs = array(); @@ -3223,7 +3383,7 @@ class Title { ); if ( !is_null($ns) ) $where['page_namespace'] = $ns; - $result = $dbr->select( + $res = $dbr->select( array( 'redirect', 'page' ), array( 'page_namespace', 'page_title' ), $where, @@ -3231,7 +3391,7 @@ class Title { ); - while( $row = $dbr->fetchObject( $result ) ) { + foreach( $res as $row ) { $redirs[] = self::newFromRow( $row ); } return $redirs; diff --git a/includes/TitleArray.php b/includes/TitleArray.php new file mode 100644 index 00000000..f7a9e1dc --- /dev/null +++ b/includes/TitleArray.php @@ -0,0 +1,81 @@ +<?php +/** + * Note: this entire file is a byte-for-byte copy of UserArray.php with + * s/User/Title/. If anyone can figure out how to do this nicely with inheri- + * tance or something, please do so. + */ + +/** + * The TitleArray class only exists to provide the newFromResult method at pre- + * sent. + */ +abstract class TitleArray implements Iterator { + /** + * @param $res result A MySQL result including at least page_namespace and + * page_title -- also can have page_id, page_len, page_is_redirect, + * page_latest (if those will be used). See Title::newFromRow. + * @return TitleArray + */ + static function newFromResult( $res ) { + $array = null; + if ( !wfRunHooks( 'TitleArrayFromResult', array( &$array, $res ) ) ) { + return null; + } + if ( $array === null ) { + $array = self::newFromResult_internal( $res ); + } + return $array; + } + + protected static function newFromResult_internal( $res ) { + $array = new TitleArrayFromResult( $res ); + return $array; + } +} + +class TitleArrayFromResult extends TitleArray { + var $res; + var $key, $current; + + function __construct( $res ) { + $this->res = $res; + $this->key = 0; + $this->setCurrent( $this->res->current() ); + } + + protected function setCurrent( $row ) { + if ( $row === false ) { + $this->current = false; + } else { + $this->current = Title::newFromRow( $row ); + } + } + + public function count() { + return $this->res->numRows(); + } + + function current() { + return $this->current; + } + + function key() { + return $this->key; + } + + function next() { + $row = $this->res->next(); + $this->setCurrent( $row ); + $this->key++; + } + + function rewind() { + $this->res->rewind(); + $this->key = 0; + $this->setCurrent( $this->res->current() ); + } + + function valid() { + return $this->current !== false; + } +} diff --git a/includes/UploadBase.php b/includes/UploadBase.php new file mode 100644 index 00000000..91155a1b --- /dev/null +++ b/includes/UploadBase.php @@ -0,0 +1,867 @@ +<?php + +class UploadBase { + var $mTempPath; + var $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType; + var $mTitle = false, $mTitleError = 0; + var $mFilteredName, $mFinalExtension; + + const SUCCESS = 0; + const OK = 0; + const BEFORE_PROCESSING = 1; + const LARGE_FILE_SERVER = 2; + const EMPTY_FILE = 3; + const MIN_LENGTH_PARTNAME = 4; + const ILLEGAL_FILENAME = 5; + const PROTECTED_PAGE = 6; + const OVERWRITE_EXISTING_FILE = 7; + const FILETYPE_MISSING = 8; + const FILETYPE_BADTYPE = 9; + const VERIFICATION_ERROR = 10; + const UPLOAD_VERIFICATION_ERROR = 11; + const UPLOAD_WARNING = 12; + const INTERNAL_ERROR = 13; + + const SESSION_VERSION = 2; + + /** + * Returns true if uploads are enabled. + * Can be overriden by subclasses. + */ + static function isEnabled() { + global $wgEnableUploads; + return $wgEnableUploads; + } + /** + * Returns true if the user can use this upload module or else a string + * identifying the missing permission. + * Can be overriden by subclasses. + */ + static function isAllowed( $user ) { + if( !$user->isAllowed( 'upload' ) ) + return 'upload'; + return true; + } + + // Upload handlers. Should probably just be a global + static $uploadHandlers = array( 'Stash', 'Upload', 'Url' ); + /** + * Create a form of UploadBase depending on wpSourceType and initializes it + */ + static function createFromRequest( &$request, $type = null ) { + $type = $type ? $type : $request->getVal( 'wpSourceType' ); + if( !$type ) + return null; + $type = ucfirst($type); + $className = 'UploadFrom'.$type; + if( !in_array( $type, self::$uploadHandlers ) ) + return null; + if( !call_user_func( array( $className, 'isEnabled' ) ) ) + return null; + if( !call_user_func( array( $className, 'isValidRequest' ), $request ) ) + return null; + + $handler = new $className; + $handler->initializeFromRequest( $request ); + return $handler; + } + + /** + * Check whether a request if valid for this handler + */ + static function isValidRequest( $request ) { + return false; + } + + function __construct() {} + + /** + * Do the real variable initialization + */ + function initialize( $name, $tempPath, $fileSize, $removeTempFile = false ) { + $this->mDesiredDestName = $name; + $this->mTempPath = $tempPath; + $this->mFileSize = $fileSize; + $this->mRemoveTempFile = $removeTempFile; + } + + /** + * Fetch the file. Usually a no-op + */ + function fetchFile() { + return self::OK; + } + + /** + * Verify whether the upload is sane. + * Returns self::OK or else an array with error information + */ + function verifyUpload() { + global $wgUser; + + /** + * If there was no filename or a zero size given, give up quick. + */ + if( empty( $this->mFileSize ) ) + return array( 'status' => self::EMPTY_FILE ); + + $nt = $this->getTitle(); + if( is_null( $nt ) ) { + $result = array( 'status' => $this->mTitleError ); + if( $this->mTitleError == self::ILLEGAL_FILENAME ) + $resul['filtered'] = $this->mFilteredName; + if ( $this->mTitleError == self::FILETYPE_BADTYPE ) + $result['finalExt'] = $this->mFinalExtension; + return $result; + } + $this->mLocalFile = wfLocalFile( $nt ); + $this->mDestName = $this->mLocalFile->getName(); + + /** + * In some cases we may forbid overwriting of existing files. + */ + $overwrite = $this->checkOverwrite( $this->mDestName ); + if( $overwrite !== true ) + return array( 'status' => self::OVERWRITE_EXISTING_FILE, 'overwrite' => $overwrite ); + + /** + * Look at the contents of the file; if we can recognize the + * type but it's corrupt or data of the wrong type, we should + * probably not accept it. + */ + $verification = $this->verifyFile( $this->mTempPath ); + + if( $verification !== true ) { + if( !is_array( $verification ) ) + $verification = array( $verification ); + $verification['status'] = self::VERIFICATION_ERROR; + return $verification; + } + + $error = ''; + if( !wfRunHooks( 'UploadVerification', + array( $this->mDestName, $this->mTempPath, &$error ) ) ) { + return array( 'status' => self::UPLOAD_VERIFICATION_ERROR, 'error' => $error ); + } + + return self::OK; + } + + /** + * Verifies that it's ok to include the uploaded file + * + * @param string $tmpfile the full path of the temporary file to verify + * @return mixed true of the file is verified, a string or array otherwise. + */ + protected function verifyFile( $tmpfile ) { + $this->mFileProps = File::getPropsFromPath( $this->mTempPath, + $this->mFinalExtension ); + $this->checkMacBinary(); + + #magically determine mime type + $magic = MimeMagic::singleton(); + $mime = $magic->guessMimeType( $tmpfile, false ); + + #check mime type, if desired + global $wgVerifyMimeType; + if ( $wgVerifyMimeType ) { + + wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n"); + #check mime type against file extension + if( !self::verifyExtension( $mime, $this->mFinalExtension ) ) { + return 'uploadcorrupt'; + } + + #check mime type blacklist + global $wgMimeTypeBlacklist; + if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist) + && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { + return array( 'filetype-badmime', $mime ); + } + } + + #check for htmlish code and javascript + if( $this->detectScript ( $tmpfile, $mime, $this->mFinalExtension ) ) { + return 'uploadscripted'; + } + + /** + * Scan the uploaded file for viruses + */ + $virus = $this->detectVirus($tmpfile); + if ( $virus ) { + return array( 'uploadvirus', $virus ); + } + + wfDebug( __METHOD__.": all clear; passing.\n" ); + return true; + } + + /** + * Check whether the user can edit, upload and create the image + */ + function verifyPermissions( $user ) { + /** + * If the image is protected, non-sysop users won't be able + * to modify it by uploading a new revision. + */ + $nt = $this->getTitle(); + if( is_null( $nt ) ) + return true; + $permErrors = $nt->getUserPermissionsErrors( 'edit', $user ); + $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user ); + $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $user ) ); + if( $permErrors || $permErrorsUpload || $permErrorsCreate ) { + $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) ); + $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) ); + return $permErrors; + } + return true; + } + + /** + * Check for non fatal problems with the file + */ + function checkWarnings() { + $warning = array(); + + $filename = $this->mLocalFile->getName(); + $n = strrpos( $filename, '.' ); + $partname = $n ? substr( $filename, 0, $n ) : $filename; + + // Check whether the resulting filename is different from the desired one + if( $this->mDesiredDestName != $filename ) + $warning['badfilename'] = $filename; + + // Check whether the file extension is on the unwanted list + global $wgCheckFileExtensions, $wgFileExtensions; + if ( $wgCheckFileExtensions ) { + if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) + $warning['filetype-unwanted-type'] = $this->mFinalExtension; + } + + global $wgUploadSizeWarning; + if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) + $warning['large-file'] = $wgUploadSizeWarning; + + if ( $this->mFileSize == 0 ) + $warning['emptyfile'] = true; + + $exists = self::getExistsWarning( $this->mLocalFile ); + if( $exists !== false ) + $warning['exists'] = $exists; + + // Check whether this may be a thumbnail + if( $exists !== false && $exists[0] != 'thumb' + && self::isThumbName( $this->mLocalFile->getName() ) ) + $warning['file-thumbnail-no'] = substr( $filename , 0, + strpos( $nt->getText() , '-' ) +1 ); + + $hash = File::sha1Base36( $this->mTempPath ); + $dupes = RepoGroup::singleton()->findBySha1( $hash ); + if( $dupes ) + $warning['duplicate'] = $dupes; + + $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist(); + foreach( $filenamePrefixBlacklist as $prefix ) { + if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) { + $warning['filename-bad-prefix'] = $prefix; + break; + } + } + + # If the file existed before and was deleted, warn the user of this + # Don't bother doing so if the file exists now, however + if( $this->mLocalFile->wasDeleted() && !$this->mLocalFile->exists() ) + $warning['filewasdeleted'] = $this->mLocalFile->getTitle(); + + return $warning; + } + + /** + * Really perform the upload. + */ + function performUpload( $comment, $pageText, $watch, $user ) { + $status = $this->mLocalFile->upload( $this->mTempPath, $comment, $pageText, + File::DELETE_SOURCE, $this->mFileProps, false, $user ); + + if( $status->isGood() && $watch ) { + $user->addWatch( $this->mLocalFile->getTitle() ); + } + + if( $status->isGood() ) + wfRunHooks( 'UploadComplete', array( &$this ) ); + + return $status; + } + + /** + * Returns a title or null + */ + function getTitle() { + if ( $this->mTitle !== false ) + return $this->mTitle; + + /** + * Chop off any directories in the given filename. Then + * filter out illegal characters, and try to make a legible name + * out of it. We'll strip some silently that Title would die on. + */ + + $basename = $this->mDesiredDestName; + + $this->mFilteredName = wfStripIllegalFilenameChars( $basename ); + + /** + * We'll want to blacklist against *any* 'extension', and use + * only the final one for the whitelist. + */ + list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName ); + + if( count( $ext ) ) { + $this->mFinalExtension = $ext[count( $ext ) - 1]; + } else { + $this->mFinalExtension = ''; + } + + /* Don't allow users to override the blacklist (check file extension) */ + global $wgCheckFileExtensions, $wgStrictFileExtensions; + global $wgFileExtensions, $wgFileBlacklist; + if ( $this->mFinalExtension == '' ) { + $this->mTitleError = self::FILETYPE_MISSING; + return $this->mTitle = null; + } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || + ( $wgCheckFileExtensions && $wgStrictFileExtensions && + !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) { + $this->mTitleError = self::FILETYPE_BADTYPE; + return $this->mTitle = null; + } + + # If there was more than one "extension", reassemble the base + # filename to prevent bogus complaints about length + if( count( $ext ) > 1 ) { + for( $i = 0; $i < count( $ext ) - 1; $i++ ) + $partname .= '.' . $ext[$i]; + } + + if( strlen( $partname ) < 1 ) { + $this->mTitleError = self::MIN_LENGTH_PARTNAME; + return $this->mTitle = null; + } + + $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); + if( is_null( $nt ) ) { + $this->mTitleError = self::ILLEGAL_FILENAME; + return $this->mTitle = null; + } + return $this->mTitle = $nt; + } + + function getLocalFile() { + if( is_null( $this->mLocalFile ) ) { + $nt = $this->getTitle(); + $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt ); + } + return $this->mLocalFile; + } + + /** + * Stash a file in a temporary directory for later processing + * after the user has confirmed it. + * + * If the user doesn't explicitly cancel or accept, these files + * can accumulate in the temp directory. + * + * @param string $saveName - the destination filename + * @param string $tempName - the source temporary file to save + * @return string - full path the stashed file, or false on failure + * @access private + */ + function saveTempUploadedFile( $saveName, $tempName ) { + global $wgOut; + $repo = RepoGroup::singleton()->getLocalRepo(); + $status = $repo->storeTemp( $saveName, $tempName ); + return $status; + } + + /** + * Stash a file in a temporary directory for later processing, + * and save the necessary descriptive info into the session. + * Returns a key value which will be passed through a form + * to pick up the path info on a later invocation. + * + * @return int + * @access private + */ + function stashSession() { + $status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath ); + + if( !$status->isGood() ) { + # Couldn't save the file. + return false; + } + + return array( + 'mTempPath' => $status->value, + 'mFileSize' => $this->mFileSize, + 'mFileProps' => $this->mFileProps, + 'version' => self::SESSION_VERSION, + ); + } + + /** + * Remove a temporarily kept file stashed by saveTempUploadedFile(). + * @return success + */ + function unsaveUploadedFile() { + $repo = RepoGroup::singleton()->getLocalRepo(); + $success = $repo->freeTemp( $this->mTempPath ); + return $success; + } + + /** + * If we've modified the upload file we need to manually remove it + * on exit to clean up. + * @access private + */ + function cleanupTempFile() { + if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) { + wfDebug( __METHOD__.": Removing temporary file {$this->mTempPath}\n" ); + unlink( $this->mTempPath ); + } + } + + function getTempPath() { + return $this->mTempPath; + } + + + /** + * Split a file into a base name and all dot-delimited 'extensions' + * on the end. Some web server configurations will fall back to + * earlier pseudo-'extensions' to determine type and execute + * scripts, so the blacklist needs to check them all. + * + * @return array + */ + function splitExtensions( $filename ) { + $bits = explode( '.', $filename ); + $basename = array_shift( $bits ); + return array( $basename, $bits ); + } + + /** + * Perform case-insensitive match against a list of file extensions. + * Returns true if the extension is in the list. + * + * @param string $ext + * @param array $list + * @return bool + */ + function checkFileExtension( $ext, $list ) { + return in_array( strtolower( $ext ), $list ); + } + + /** + * Perform case-insensitive match against a list of file extensions. + * Returns true if any of the extensions are in the list. + * + * @param array $ext + * @param array $list + * @return bool + */ + function checkFileExtensionList( $ext, $list ) { + foreach( $ext as $e ) { + if( in_array( strtolower( $e ), $list ) ) { + return true; + } + } + return false; + } + + + /** + * Checks if the mime type of the uploaded file matches the file extension. + * + * @param string $mime the mime type of the uploaded file + * @param string $extension The filename extension that the file is to be served with + * @return bool + */ + public static function verifyExtension( $mime, $extension ) { + $magic = MimeMagic::singleton(); + + if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) + if ( ! $magic->isRecognizableExtension( $extension ) ) { + wfDebug( __METHOD__.": passing file with unknown detected mime type; " . + "unrecognized extension '$extension', can't verify\n" ); + return true; + } else { + wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ". + "recognized extension '$extension', so probably invalid file\n" ); + return false; + } + + $match= $magic->isMatchingExtension($extension,$mime); + + if ($match===NULL) { + wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" ); + return true; + } elseif ($match===true) { + wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" ); + + #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! + return true; + + } else { + wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" ); + return false; + } + } + + /** + * Heuristic for detecting files that *could* contain JavaScript instructions or + * things that may look like HTML to a browser and are thus + * potentially harmful. The present implementation will produce false positives in some situations. + * + * @param string $file Pathname to the temporary upload file + * @param string $mime The mime type of the file + * @param string $extension The extension of the file + * @return bool true if the file contains something looking like embedded scripts + */ + function detectScript($file, $mime, $extension) { + global $wgAllowTitlesInSVG; + + #ugly hack: for text files, always look at the entire file. + #For binary field, just check the first K. + + if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file ); + else { + $fp = fopen( $file, 'rb' ); + $chunk = fread( $fp, 1024 ); + fclose( $fp ); + } + + $chunk= strtolower( $chunk ); + + if (!$chunk) return false; + + #decode from UTF-16 if needed (could be used for obfuscation). + if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE"; + elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE"; + else $enc= NULL; + + if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk); + + $chunk= trim($chunk); + + #FIXME: convert from UTF-16 if necessarry! + + wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n"); + + #check for HTML doctype + if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true; + + /** + * Internet Explorer for Windows performs some really stupid file type + * autodetection which can cause it to interpret valid image files as HTML + * and potentially execute JavaScript, creating a cross-site scripting + * attack vectors. + * + * Apple's Safari browser also performs some unsafe file type autodetection + * which can cause legitimate files to be interpreted as HTML if the + * web server is not correctly configured to send the right content-type + * (or if you're really uploading plain text and octet streams!) + * + * Returns true if IE is likely to mistake the given file for HTML. + * Also returns true if Safari would mistake the given file for HTML + * when served with a generic content-type. + */ + + $tags = array( + '<body', + '<head', + '<html', #also in safari + '<img', + '<pre', + '<script', #also in safari + '<table' + ); + if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) { + $tags[] = '<title'; + } + + foreach( $tags as $tag ) { + if( false !== strpos( $chunk, $tag ) ) { + return true; + } + } + + /* + * look for javascript + */ + + #resolve entity-refs to look at attributes. may be harsh on big files... cache result? + $chunk = Sanitizer::decodeCharReferences( $chunk ); + + #look for script-types + if (preg_match('!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim',$chunk)) return true; + + #look for html-style script-urls + if (preg_match('!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true; + + #look for css-style script-urls + if (preg_match('!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true; + + wfDebug("SpecialUpload::detectScript: no scripts found\n"); + return false; + } + + /** + * Generic wrapper function for a virus scanner program. + * This relies on the $wgAntivirus and $wgAntivirusSetup variables. + * $wgAntivirusRequired may be used to deny upload if the scan fails. + * + * @param string $file Pathname to the temporary upload file + * @return mixed false if not virus is found, NULL if the scan fails or is disabled, + * or a string containing feedback from the virus scanner if a virus was found. + * If textual feedback is missing but a virus was found, this function returns true. + */ + function detectVirus($file) { + global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; + + if ( !$wgAntivirus ) { + wfDebug( __METHOD__.": virus scanner disabled\n"); + return NULL; + } + + if ( !$wgAntivirusSetup[$wgAntivirus] ) { + wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" ); + $wgOut->wrapWikiMsg( '<div class="error">$1</div>', array( 'virus-badscanner', $wgAntivirus ) ); + return wfMsg('virus-unknownscanner') . " $wgAntivirus"; + } + + # look up scanner configuration + $command = $wgAntivirusSetup[$wgAntivirus]["command"]; + $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"]; + $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ? + $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null; + + if ( strpos( $command,"%f" ) === false ) { + # simple pattern: append file to scan + $command .= " " . wfEscapeShellArg( $file ); + } else { + # complex pattern: replace "%f" with file to scan + $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); + } + + wfDebug( __METHOD__.": running virus scan: $command \n" ); + + # execute virus scanner + $exitCode = false; + + #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. + # that does not seem to be worth the pain. + # Ask me (Duesentrieb) about it if it's ever needed. + $output = array(); + if ( wfIsWindows() ) { + exec( "$command", $output, $exitCode ); + } else { + exec( "$command 2>&1", $output, $exitCode ); + } + + # map exit code to AV_xxx constants. + $mappedCode = $exitCode; + if ( $exitCodeMap ) { + if ( isset( $exitCodeMap[$exitCode] ) ) { + $mappedCode = $exitCodeMap[$exitCode]; + } elseif ( isset( $exitCodeMap["*"] ) ) { + $mappedCode = $exitCodeMap["*"]; + } + } + + if ( $mappedCode === AV_SCAN_FAILED ) { + # scan failed (code was mapped to false by $exitCodeMap) + wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" ); + + if ( $wgAntivirusRequired ) { + return wfMsg('virus-scanfailed', array( $exitCode ) ); + } else { + return NULL; + } + } else if ( $mappedCode === AV_SCAN_ABORTED ) { + # scan failed because filetype is unknown (probably imune) + wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" ); + return NULL; + } else if ( $mappedCode === AV_NO_VIRUS ) { + # no virus found + wfDebug( __METHOD__.": file passed virus scan.\n" ); + return false; + } else { + $output = join( "\n", $output ); + $output = trim( $output ); + + if ( !$output ) { + $output = true; #if there's no output, return true + } elseif ( $msgPattern ) { + $groups = array(); + if ( preg_match( $msgPattern, $output, $groups ) ) { + if ( $groups[1] ) { + $output = $groups[1]; + } + } + } + + wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" ); + return $output; + } + } + + /** + * Check if the temporary file is MacBinary-encoded, as some uploads + * from Internet Explorer on Mac OS Classic and Mac OS X will be. + * If so, the data fork will be extracted to a second temporary file, + * which will then be checked for validity and either kept or discarded. + * + * @access private + */ + function checkMacBinary() { + $macbin = new MacBinary( $this->mTempPath ); + if( $macbin->isValid() ) { + $dataFile = tempnam( wfTempDir(), "WikiMacBinary" ); + $dataHandle = fopen( $dataFile, 'wb' ); + + wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" ); + $macbin->extractData( $dataHandle ); + + $this->mTempPath = $dataFile; + $this->mFileSize = $macbin->dataForkLength(); + + // We'll have to manually remove the new file if it's not kept. + $this->mRemoveTempFile = true; + } + $macbin->close(); + } + + /** + * Check if there's an overwrite conflict and, if so, if restrictions + * forbid this user from performing the upload. + * + * @return mixed true on success, WikiError on failure + * @access private + */ + function checkOverwrite() { + global $wgUser; + // First check whether the local file can be overwritten + if( $this->mLocalFile->exists() ) + if( !self::userCanReUpload( $wgUser, $this->mLocalFile ) ) + return 'fileexists-forbidden'; + + // Check shared conflicts + $file = wfFindFile( $this->mLocalFile->getName() ); + if ( $file && ( !$wgUser->isAllowed( 'reupload' ) || + !$wgUser->isAllowed( 'reupload-shared' ) ) ) + return 'fileexists-shared-forbidden'; + + return true; + + } + + /** + * Check if a user is the last uploader + * + * @param User $user + * @param string $img, image name + * @return bool + */ + public static function userCanReUpload( User $user, $img ) { + if( $user->isAllowed( 'reupload' ) ) + return true; // non-conditional + if( !$user->isAllowed( 'reupload-own' ) ) + return false; + if( is_string( $img ) ) + $img = wfLocalFile( $img ); + if ( !( $img instanceof LocalFile ) ) + return false; + + return $user->getId() == $img->getUser( 'id' ); + } + + public static function getExistsWarning( $file ) { + if( $file->exists() ) + return array( 'exists', $file ); + + if( $file->getTitle()->getArticleID() ) + return array( 'page-exists', $file ); + + if( strpos( $file->getName(), '.' ) == false ) { + $partname = $file->getName(); + $rawExtension = ''; + } else { + $n = strrpos( $file->getName(), '.' ); + $rawExtension = substr( $file->getName(), $n + 1 ); + $partname = substr( $file->getName(), 0, $n ); + } + + if ( $rawExtension != $file->getExtension() ) { + // We're not using the normalized form of the extension. + // Normal form is lowercase, using most common of alternate + // extensions (eg 'jpg' rather than 'JPEG'). + // + // Check for another file using the normalized form... + $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() ); + $file_lc = wfLocalFile( $nt_lc ); + + if( $file_lc->exists() ) + return array( 'exists-normalized', $file_lc ); + } + + if ( self::isThumbName( $file->getName() ) ) { + # Check for filenames like 50px- or 180px-, these are mostly thumbnails + $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension ); + $file_thb = wfLocalFile( $nt_thb ); + if( $file_thb->exists() ) + return array( 'thumb', $file_thb ); + } + + return false; + } + + public static function isThumbName( $filename ) { + $n = strrpos( $filename, '.' ); + $partname = $n ? substr( $filename, 0, $n ) : $filename; + return ( + substr( $partname , 3, 3 ) == 'px-' || + substr( $partname , 2, 3 ) == 'px-' + ) && + ereg( "[0-9]{2}" , substr( $partname , 0, 2) ); + } + + /** + * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]] + * + * @return array list of prefixes + */ + public static function getFilenamePrefixBlacklist() { + $blacklist = array(); + $message = wfMsgForContent( 'filename-prefix-blacklist' ); + if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) { + $lines = explode( "\n", $message ); + foreach( $lines as $line ) { + // Remove comment lines + $comment = substr( trim( $line ), 0, 1 ); + if ( $comment == '#' || $comment == '' ) { + continue; + } + // Remove additional comments after a prefix + $comment = strpos( $line, '#' ); + if ( $comment > 0 ) { + $line = substr( $line, 0, $comment-1 ); + } + $blacklist[] = trim( $line ); + } + } + return $blacklist; + } + + +} diff --git a/includes/UploadFromStash.php b/includes/UploadFromStash.php new file mode 100644 index 00000000..8bff3b49 --- /dev/null +++ b/includes/UploadFromStash.php @@ -0,0 +1,58 @@ +<?php + +class UploadFromStash extends UploadBase { + static function isValidSessionKey( $key, $sessionData ) { + return !empty( $key ) && + is_array( $sessionData ) && + isset( $sessionData[$key] ) && + isset( $sessionData[$key]['version'] ) && + $sessionData[$key]['version'] == self::SESSION_VERSION + ; + } + static function isValidRequest( $request ) { + $sessionData = $request->getSessionData('wsUploadData'); + return self::isValidSessionKey( + $request->getInt( 'wpSessionKey' ), + $sessionData + ); + } + + function initialize( $name, $sessionData ) { + /** + * Confirming a temporarily stashed upload. + * We don't want path names to be forged, so we keep + * them in the session on the server and just give + * an opaque key to the user agent. + */ + $this->initialize( $name, + $sessionData['mTempPath'], + $sessionData['mFileSize'], + false + ); + + $this->mFileProps = $sessionData['mFileProps']; + } + function initializeFromRequest( &$request ) { + $sessionKey = $request->getInt( 'wpSessionKey' ); + $sessionData = $request->getSessionData('wsUploadData'); + + $desiredDestName = $request->getText( 'wpDestFile' ); + if( !$desiredDestName ) + $desiredDestName = $request->getText( 'wpUploadFile' ); + + return $this->initialize( $desiredDestName, $sessionData[$sessionKey] ); + } + + /** + * File has been previously verified so no need to do so again. + */ + protected function verifyFile( $tmpfile ) { + return true; + } + /** + * We're here from "ignore warnings anyway" so return just OK + */ + function checkWarnings() { + return array(); + } +} diff --git a/includes/UploadFromUpload.php b/includes/UploadFromUpload.php new file mode 100644 index 00000000..1b6762c6 --- /dev/null +++ b/includes/UploadFromUpload.php @@ -0,0 +1,20 @@ +<?php + +class UploadFromUpload extends UploadBase { + + function initializeFromRequest( &$request ) { + $desiredDestName = $request->getText( 'wpDestFile' ); + if( !$desiredDestName ) + $desiredDestName = $request->getText( 'wpUploadFile' ); + + return $this->initialize( + $desiredDestName, + $request->getFileTempName( 'wpUploadFile' ), + $request->getFileSize( 'wpUploadFile' ) + ); + } + + static function isValidRequest( $request ) { + return (bool)$request->getFileTempName( 'wpUploadFile' ); + } +} diff --git a/includes/UploadFromUrl.php b/includes/UploadFromUrl.php new file mode 100644 index 00000000..7e23b8cd --- /dev/null +++ b/includes/UploadFromUrl.php @@ -0,0 +1,92 @@ +<?php + + +class UploadFromUrl extends UploadBase { + static function isAllowed( $user ) { + if( !$user->isAllowed( 'upload_by_url' ) ) + return 'upload_by_url'; + return parent::isAllowed( $user ); + } + static function isEnabled() { + global $wgAllowCopyUploads; + return $wgAllowCopyUploads && parent::isEnabled(); + } + + function initialize( $name, $url ) { + global $wgTmpDirectory; + $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' ); + $this-initialize( $name, $local_file, 0, true ); + + $this->mUrl = trim( $url ); + } + + /** + * Do the real fetching stuff + */ + function fetchFile() { + if( stripos($this->mUrl, 'http://') !== 0 && stripos($this->mUrl, 'ftp://') !== 0 ) { + return array( + 'status' => self::BEFORE_PROCESSING, + 'error' => 'upload-proto-error', + ); + } + $res = $this->curlCopy(); + if( $res !== true ) { + return array( + 'status' => self::BEFORE_PROCESSING, + 'error' => $res, + ); + } + return self::OK; + } + + /** + * Safe copy from URL + * Returns true if there was an error, false otherwise + */ + private function curlCopy() { + global $wgUser, $wgOut; + + # Open temporary file + $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" ); + if( $this->mCurlDestHandle === false ) { + # Could not open temporary file to write in + return 'upload-file-error'; + } + + $ch = curl_init(); + curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug + curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout + curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed + curl_setopt( $ch, CURLOPT_URL, $this->mUrl); + curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) ); + curl_exec( $ch ); + $error = curl_errno( $ch ); + curl_close( $ch ); + + fclose( $this->mCurlDestHandle ); + unset( $this->mCurlDestHandle ); + + if( $error ) + return "upload-curl-error$errornum"; + + return true; + } + + /** + * Callback function for CURL-based web transfer + * Write data to file unless we've passed the length limit; + * if so, abort immediately. + * @access private + */ + function uploadCurlCallback( $ch, $data ) { + global $wgMaxUploadSize; + $length = strlen( $data ); + $this->mFileSize += $length; + if( $this->mFileSize > $wgMaxUploadSize ) { + return 0; + } + fwrite( $this->mCurlDestHandle, $data ); + return $length; + } +} diff --git a/includes/User.php b/includes/User.php index 4e39d678..9fee089c 100644 --- a/includes/User.php +++ b/includes/User.php @@ -1,20 +1,29 @@ <?php /** - * See user.txt + * Implements the User class for the %MediaWiki software. * @file */ -# Number of characters in user_token field +/** + * \int Number of characters in user_token field. + * @ingroup Constants + */ define( 'USER_TOKEN_LENGTH', 32 ); -# Serialized record version +/** + * \int Serialized record version. + * @ingroup Constants + */ define( 'MW_USER_VERSION', 6 ); -# Some punctuation to prevent editing from broken text-mangling proxies. +/** + * \string Some punctuation to prevent editing from broken text-mangling proxies. + * @ingroup Constants + */ define( 'EDIT_TOKEN_SUFFIX', '+\\' ); /** - * Thrown by User::setPassword() on error + * Thrown by User::setPassword() on error. * @ingroup Exception */ class PasswordError extends MWException { @@ -34,11 +43,13 @@ class PasswordError extends MWException { class User { /** - * A list of default user toggles, i.e. boolean user preferences that are - * displayed by Special:Preferences as checkboxes. This list can be - * extended via the UserToggles hook or $wgContLang->getExtraUserToggles(). + * \type{\arrayof{\string}} A list of default user toggles, i.e., boolean user + * preferences that are displayed by Special:Preferences as checkboxes. + * This list can be extended via the UserToggles hook or by + * $wgContLang::getExtraUserToggles(). + * @showinitializer */ - static public $mToggles = array( + public static $mToggles = array( 'highlightbroken', 'justify', 'hideminor', @@ -71,21 +82,26 @@ class User { 'showjumplinks', 'uselivepreview', 'forceeditsummary', - 'watchlisthideown', - 'watchlisthidebots', 'watchlisthideminor', + 'watchlisthidebots', + 'watchlisthideown', + 'watchlisthideanons', + 'watchlisthideliu', 'ccmeonemails', 'diffonly', 'showhiddencats', + 'noconvertlink', + 'norollbackdiff', ); /** - * List of member variables which are saved to the shared cache (memcached). - * Any operation which changes the corresponding database fields must - * call a cache-clearing function. + * \type{\arrayof{\string}} List of member variables which are saved to the + * shared cache (memcached). Any operation which changes the + * corresponding database fields must call a cache-clearing function. + * @showinitializer */ static $mCacheVars = array( - # user table + // user table 'mId', 'mName', 'mRealName', @@ -101,13 +117,15 @@ class User { 'mEmailTokenExpires', 'mRegistration', 'mEditCount', - # user_group table + // user_group table 'mGroups', ); /** - * Core rights - * Each of these should have a corresponding message of the form "right-$right" + * \type{\arrayof{\string}} Core rights. + * Each of these should have a corresponding message of the form + * "right-$right". + * @showinitializer */ static $mCoreRights = array( 'apihighlimits', @@ -132,6 +150,9 @@ class User { 'markbotedits', 'minoredit', 'move', + 'movefile', + 'move-rootuserpages', + 'move-subpages', 'nominornewtalk', 'noratelimit', 'patrol', @@ -142,6 +163,7 @@ class User { 'reupload', 'reupload-shared', 'rollback', + 'siteadmin', 'suppressredirect', 'trackback', 'undelete', @@ -150,47 +172,57 @@ class User { 'upload_by_url', 'userrights', ); - static $mAllRights = false; - /** - * The cache variable declarations + * \string Cached results of getAllRights() */ + static $mAllRights = false; + + /** @name Cache variables */ + //@{ var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups; + //@} /** - * Whether the cache variables have been loaded + * \bool Whether the cache variables have been loaded. */ - var $mDataLoaded; + var $mDataLoaded, $mAuthLoaded; /** - * Initialisation data source if mDataLoaded==false. May be one of: - * defaults anonymous user initialised from class defaults - * name initialise from mName - * id initialise from mId - * session log in from cookies or session if possible + * \string Initialization data source if mDataLoaded==false. May be one of: + * - 'defaults' anonymous user initialised from class defaults + * - 'name' initialise from mName + * - 'id' initialise from mId + * - 'session' log in from cookies or session if possible * * Use the User::newFrom*() family of functions to set this. */ var $mFrom; - /** - * Lazy-initialised variables, invalidated with clearInstanceCache - */ + /** @name Lazy-initialized variables, invalidated with clearInstanceCache */ + //@{ var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights, - $mBlockreason, $mBlock, $mEffectiveGroups; + $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally, + $mLocked, $mHideName; + //@} /** - * Lightweight constructor for anonymous user - * Use the User::newFrom* factory functions for other kinds of users + * Lightweight constructor for an anonymous user. + * Use the User::newFrom* factory functions for other kinds of users. + * + * @see newFromName() + * @see newFromId() + * @see newFromConfirmationCode() + * @see newFromSession() + * @see newFromRow() */ function User() { $this->clearInstanceCache( 'defaults' ); } /** - * Load the user table data for this object from the source given by mFrom + * Load the user table data for this object from the source given by mFrom. */ function load() { if ( $this->mDataLoaded ) { @@ -219,6 +251,7 @@ class User { break; case 'session': $this->loadFromSession(); + wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) ); break; default: throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" ); @@ -227,8 +260,8 @@ class User { } /** - * Load user table data given mId - * @return false if the ID does not exist, true otherwise + * Load user table data, given mId has already been set. + * @return \bool false if the ID does not exist, true otherwise * @private */ function loadFromId() { @@ -283,6 +316,10 @@ class User { global $wgMemc; $wgMemc->set( $key, $data ); } + + + /** @name newFrom*() static factory methods */ + //@{ /** * Static factory method for creation from username. @@ -290,14 +327,14 @@ class User { * This is slightly less efficient than newFromId(), so use newFromId() if * you have both an ID and a name handy. * - * @param $name String: username, validated by Title:newFromText() - * @param $validate Mixed: validate username. Takes the same parameters as + * @param $name \string Username, validated by Title::newFromText() + * @param $validate \mixed Validate username. Takes the same parameters as * User::getCanonicalName(), except that true is accepted as an alias * for 'valid', for BC. * - * @return User object, or null if the username is invalid. If the username - * is not present in the database, the result will be a user object with - * a name, zero user ID and default settings. + * @return \type{User} The User object, or null if the username is invalid. If the + * username is not present in the database, the result will be a user object + * with a name, zero user ID and default settings. */ static function newFromName( $name, $validate = 'valid' ) { if ( $validate === true ) { @@ -315,6 +352,12 @@ class User { } } + /** + * Static factory method for creation from a given user ID. + * + * @param $id \int Valid user ID + * @return \type{User} The corresponding User object + */ static function newFromId( $id ) { $u = new User; $u->mId = $id; @@ -329,8 +372,8 @@ class User { * * If the code is invalid or has expired, returns NULL. * - * @param $code string - * @return User + * @param $code \string Confirmation code + * @return \type{User} */ static function newFromConfirmationCode( $code ) { $dbr = wfGetDB( DB_SLAVE ); @@ -349,7 +392,7 @@ class User { * Create a new user object using data from session or cookies. If the * login credentials are invalid, the result is an anonymous user. * - * @return User + * @return \type{User} */ static function newFromSession() { $user = new User; @@ -360,17 +403,22 @@ class User { /** * Create a new user object from a user row. * The row should have all fields from the user table in it. + * @param $row array A row from the user table + * @return \type{User} */ static function newFromRow( $row ) { $user = new User; $user->loadFromRow( $row ); return $user; } + + //@} + /** - * Get username given an id. - * @param $id Integer: database user id - * @return string Nickname of a user + * Get the username corresponding to a given user ID + * @param $id \int User ID + * @return \string The corresponding username */ static function whoIs( $id ) { $dbr = wfGetDB( DB_SLAVE ); @@ -378,10 +426,10 @@ class User { } /** - * Get the real name of a user given their identifier + * Get the real name of a user given their user ID * - * @param $id Int: database user id - * @return string Real name of a user + * @param $id \int User ID + * @return \string The corresponding user's real name */ static function whoIsReal( $id ) { $dbr = wfGetDB( DB_SLAVE ); @@ -390,12 +438,11 @@ class User { /** * Get database id given a user name - * @param $name String: nickname of a user - * @return integer|null Database user id (null: if non existent - * @static + * @param $name \string Username + * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent */ static function idFromName( $name ) { - $nt = Title::newFromText( $name ); + $nt = Title::makeTitleSafe( NS_USER, $name ); if( is_null( $nt ) ) { # Illegal name return null; @@ -423,8 +470,8 @@ class User { * addresses like this, if we allowed accounts like this to be created * new users could get the old edits of these anonymous users. * - * @param $name String: nickname of a user - * @return bool + * @param $name \string String to match + * @return \bool True or false */ static function isIP( $name ) { return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name); @@ -438,8 +485,8 @@ class User { * is longer than the maximum allowed username size or doesn't begin with * a capital letter. * - * @param $name string - * @return bool + * @param $name \string String to match + * @return \bool True or false */ static function isValidUserName( $name ) { global $wgContLang, $wgMaxNameChars; @@ -492,8 +539,8 @@ class User { * If an account already exists in this form, login will be blocked * by a failure to pass this function. * - * @param $name string - * @return bool + * @param $name \string String to match + * @return \bool True or false */ static function isUsableName( $name ) { global $wgReservedUsernames; @@ -502,8 +549,14 @@ class User { return false; } + static $reservedUsernames = false; + if ( !$reservedUsernames ) { + $reservedUsernames = $wgReservedUsernames; + wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) ); + } + // Certain names may be reserved for batch processes. - foreach ( $wgReservedUsernames as $reserved ) { + foreach ( $reservedUsernames as $reserved ) { if ( substr( $reserved, 0, 4 ) == 'msg:' ) { $reserved = wfMsgForContent( substr( $reserved, 4 ) ); } @@ -524,8 +577,8 @@ class User { * rather than in isValidUserName() to avoid disrupting * existing accounts. * - * @param $name string - * @return bool + * @param $name \string String to match + * @return \bool True or false */ static function isCreatableName( $name ) { return @@ -538,8 +591,8 @@ class User { /** * Is the input a valid password for this user? * - * @param $password String: desired password - * @return bool + * @param $password \string Desired password + * @return \bool True or false */ function isValidPassword( $password ) { global $wgMinimalPasswordLength, $wgContLang; @@ -556,7 +609,7 @@ class User { } /** - * Does a string look like an email address? + * Does a string look like an e-mail address? * * There used to be a regular expression here, it got removed because it * rejected valid addresses. Actually just check if there is '@' somewhere @@ -564,8 +617,8 @@ class User { * * @todo Check for RFC 2822 compilance (bug 959) * - * @param $addr String: email address - * @return bool + * @param $addr \string E-mail address + * @return \bool True or false */ public static function isValidEmailAddr( $addr ) { $result = null; @@ -579,12 +632,12 @@ class User { /** * Given unvalidated user input, return a canonical username, or false if * the username is invalid. - * @param $name string - * @param $validate Mixed: type of validation to use: - * false No validation - * 'valid' Valid for batch processes - * 'usable' Valid for batch processes and login - * 'creatable' Valid for batch processes, login and account creation + * @param $name \string User input + * @param $validate \types{\string,\bool} Type of validation to use: + * - false No validation + * - 'valid' Valid for batch processes + * - 'usable' Valid for batch processes and login + * - 'creatable' Valid for batch processes, login and account creation */ static function getCanonicalName( $name, $validate = 'valid' ) { # Force usernames to capital @@ -598,7 +651,9 @@ class User { return false; # Clean up name according to title rules - $t = Title::newFromText( $name ); + $t = ($validate === 'valid') ? + Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name ); + # Check for invalid titles if( is_null( $t ) ) { return false; } @@ -634,11 +689,10 @@ class User { /** * Count the number of edits of a user + * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas * - * It should not be static and some day should be merged as proper member function / deprecated -- domas - * - * @param $uid Int: the user ID to check - * @return int + * @param $uid \int User ID to check + * @return \int The user's edit count */ static function edits( $uid ) { wfProfileIn( __METHOD__ ); @@ -674,7 +728,7 @@ class User { * Return a random password. Sourced from mt_rand, so it's not particularly secure. * @todo hash random numbers to improve security, like generateToken() * - * @return string + * @return \string New random password */ static function randomPassword() { global $wgMinimalPasswordLength; @@ -691,9 +745,10 @@ class User { } /** - * Set cached properties to default. Note: this no longer clears - * uncached lazy-initialised properties. The constructor does that instead. + * Set cached properties to default. * + * @note This no longer clears uncached lazy-initialised properties; + * the constructor does that instead. * @private */ function loadDefaults( $name = false ) { @@ -728,8 +783,7 @@ class User { } /** - * Initialise php session - * @deprecated use wfSetupSession() + * @deprecated Use wfSetupSession(). */ function SetupSession() { wfDeprecated( __METHOD__ ); @@ -738,8 +792,8 @@ class User { /** * Load user data from the session or login cookie. If there are no valid - * credentials, initialises the user as an anon. - * @return true if the user is logged in, false otherwise + * credentials, initialises the user as an anonymous user. + * @return \bool True if the user is logged in, false otherwise. */ private function loadFromSession() { global $wgMemc, $wgCookiePrefix; @@ -750,20 +804,27 @@ class User { return $result; } - if ( isset( $_SESSION['wsUserID'] ) ) { - if ( 0 != $_SESSION['wsUserID'] ) { + if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) { + $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] ); + if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) { + $this->loadDefaults(); // Possible collision! + wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and + cookie user ID ($sId) don't match!" ); + return false; + } + $_SESSION['wsUserID'] = $sId; + } else if ( isset( $_SESSION['wsUserID'] ) ) { + if ( $_SESSION['wsUserID'] != 0 ) { $sId = $_SESSION['wsUserID']; } else { $this->loadDefaults(); return false; } - } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) { - $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] ); - $_SESSION['wsUserID'] = $sId; } else { $this->loadDefaults(); return false; } + if ( isset( $_SESSION['wsUserName'] ) ) { $sName = $_SESSION['wsUserName']; } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) { @@ -806,10 +867,10 @@ class User { } /** - * Load user and user_group data from the database - * $this->mId must be set, this is how the user is identified. + * Load user and user_group data from the database. + * $this::mId must be set, this is how the user is identified. * - * @return true if the user exists, false if the user is anonymous + * @return \bool True if the user exists, false if the user is anonymous * @private */ function loadFromDatabase() { @@ -840,7 +901,9 @@ class User { } /** - * Initialise the user object from a row from the user table + * Initialize this object from a row from the user table. + * + * @param $row \type{\arrayof{\mixed}} Row from the user table to load. */ function loadFromRow( $row ) { $this->mDataLoaded = true; @@ -865,7 +928,7 @@ class User { } /** - * Load the groups from the database if they aren't already loaded + * Load the groups from the database if they aren't already loaded. * @private */ function loadGroups() { @@ -884,8 +947,8 @@ class User { /** * Clear various cached data stored in this object. - * @param $reloadFrom String: reload user and user_groups table data from a - * given source. May be "name", "id", "defaults", "session" or false for + * @param $reloadFrom \string Reload user and user_groups table data from a + * given source. May be "name", "id", "defaults", "session", or false for * no reload. */ function clearInstanceCache( $reloadFrom = false ) { @@ -906,9 +969,8 @@ class User { /** * Combine the language default options with any site-specific options * and add the default language variants. - * Not really private cause it's called by Language class - * @return array - * @private + * + * @return \type{\arrayof{\string}} Array of options */ static function getDefaultOptions() { global $wgNamespacesToBeSearchedDefault; @@ -934,8 +996,8 @@ class User { /** * Get a given default option value. * - * @param $opt string - * @return string + * @param $opt \string Name of option to retrieve + * @return \string Default option value */ public static function getDefaultOption( $opt ) { $defOpts = self::getDefaultOptions(); @@ -948,7 +1010,7 @@ class User { /** * Get a list of user toggle names - * @return array + * @return \type{\arrayof{\string}} Array of user toggle names */ static function getToggles() { global $wgContLang; @@ -961,7 +1023,7 @@ class User { /** * Get blocking information * @private - * @param $bFromSlave Bool: specify whether to check slave or master. To + * @param $bFromSlave \bool Whether to check the slave database first. To * improve performance, non-critical checks are done * against slaves. Check when actually saving should be * done against master. @@ -986,6 +1048,7 @@ class User { $this->mBlockedby = 0; $this->mHideName = 0; + $this->mAllowUsertalk = 0; $ip = wfGetIP(); if ($this->isAllowed( 'ipblock-exempt' ) ) { @@ -1001,12 +1064,14 @@ class User { $this->mBlockedby = $this->mBlock->mBy; $this->mBlockreason = $this->mBlock->mReason; $this->mHideName = $this->mBlock->mHideName; + $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk; if ( $this->isLoggedIn() ) { $this->spreadBlock(); } } else { - $this->mBlock = null; - wfDebug( __METHOD__.": No block.\n" ); + // Bug 13611: don't remove mBlock here, to allow account creation blocks to + // apply to users. Note that the existence of $this->mBlock is not used to + // check for edit blocks, $this->mBlockedby is instead. } # Proxy blocking @@ -1032,6 +1097,12 @@ class User { wfProfileOut( __METHOD__ ); } + /** + * Whether the given IP is in the SORBS blacklist. + * + * @param $ip \string IP to check + * @return \bool True if blacklisted. + */ function inSorbsBlacklist( $ip ) { global $wgEnableSorbs, $wgSorbsUrl; @@ -1039,24 +1110,27 @@ class User { $this->inDnsBlacklist( $ip, $wgSorbsUrl ); } + /** + * Whether the given IP is in a given DNS blacklist. + * + * @param $ip \string IP to check + * @param $base \string URL of the DNS blacklist + * @return \bool True if blacklisted. + */ function inDnsBlacklist( $ip, $base ) { wfProfileIn( __METHOD__ ); $found = false; $host = ''; - // FIXME: IPv6 ??? - $m = array(); - if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) { + // FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170) + if( IP::isIPv4($ip) ) { # Make hostname - for ( $i=4; $i>=1; $i-- ) { - $host .= $m[$i] . '.'; - } - $host .= $base; + $host = "$ip.$base"; # Send query $ipList = gethostbynamel( $host ); - if ( $ipList ) { + if( $ipList ) { wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" ); $found = true; } else { @@ -1071,7 +1145,7 @@ class User { /** * Is this user subject to rate limiting? * - * @return bool + * @return \bool True if rate limited */ public function isPingLimitable() { global $wgRateLimitsExcludedGroups; @@ -1086,10 +1160,11 @@ class User { * Primitive rate limits: enforce maximum actions per time period * to put a brake on flooding. * - * Note: when using a shared cache like memcached, IP-address + * @note When using a shared cache like memcached, IP-address * last-hit counters will be shared across wikis. * - * @return bool true if a rate limiter was tripped + * @param $action \string Action to enforce; 'edit' if unspecified + * @return \bool True if a rate limiter was tripped */ function pingLimiter( $action='edit' ) { @@ -1180,7 +1255,9 @@ class User { /** * Check if user is blocked - * @return bool True if blocked, false otherwise + * + * @param $bFromSlave \bool Whether to check the slave database instead of the master + * @return \bool True if blocked, false otherwise */ function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site wfDebug( "User::isBlocked: enter\n" ); @@ -1190,6 +1267,10 @@ class User { /** * Check if user is blocked from editing a particular article + * + * @param $title \string Title to check + * @param $bFromSlave \bool Whether to check the slave database instead of the master + * @return \bool True if blocked, false otherwise */ function isBlockedFrom( $title, $bFromSlave = false ) { global $wgBlockAllowsUTEdit; @@ -1198,8 +1279,9 @@ class User { wfDebug( __METHOD__.": asking isBlocked()\n" ); $blocked = $this->isBlocked( $bFromSlave ); + $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false); # If a user's name is suppressed, they cannot make edits anywhere - if ( !$this->mHideName && $wgBlockAllowsUTEdit && $title->getText() === $this->getName() && + if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() && $title->getNamespace() == NS_USER_TALK ) { $blocked = false; wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" ); @@ -1209,8 +1291,8 @@ class User { } /** - * Get name of blocker - * @return string name of blocker + * If user is blocked, return the name of the user who placed the block + * @return \string name of blocker */ function blockedBy() { $this->getBlockedStatus(); @@ -1218,16 +1300,74 @@ class User { } /** - * Get blocking reason - * @return string Blocking reason + * If user is blocked, return the specified reason for the block + * @return \string Blocking reason */ function blockedFor() { $this->getBlockedStatus(); return $this->mBlockreason; } + + /** + * Check if user is blocked on all wikis. + * Do not use for actual edit permission checks! + * This is intented for quick UI checks. + * + * @param $ip \type{\string} IP address, uses current client if none given + * @return \type{\bool} True if blocked, false otherwise + */ + function isBlockedGlobally( $ip = '' ) { + if( $this->mBlockedGlobally !== null ) { + return $this->mBlockedGlobally; + } + // User is already an IP? + if( IP::isIPAddress( $this->getName() ) ) { + $ip = $this->getName(); + } else if( !$ip ) { + $ip = wfGetIP(); + } + $blocked = false; + wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) ); + $this->mBlockedGlobally = (bool)$blocked; + return $this->mBlockedGlobally; + } + + /** + * Check if user account is locked + * + * @return \type{\bool} True if locked, false otherwise + */ + function isLocked() { + if( $this->mLocked !== null ) { + return $this->mLocked; + } + global $wgAuth; + $authUser = $wgAuth->getUserInstance( $this ); + $this->mLocked = (bool)$authUser->isLocked(); + return $this->mLocked; + } + + /** + * Check if user account is hidden + * + * @return \type{\bool} True if hidden, false otherwise + */ + function isHidden() { + if( $this->mHideName !== null ) { + return $this->mHideName; + } + $this->getBlockedStatus(); + if( !$this->mHideName ) { + global $wgAuth; + $authUser = $wgAuth->getUserInstance( $this ); + $this->mHideName = (bool)$authUser->isHidden(); + } + return $this->mHideName; + } /** - * Get the user ID. Returns 0 if the user is anonymous or nonexistent. + * Get the user's ID. + * @return \int The user's ID; 0 if the user is anonymous or nonexistent */ function getId() { if( $this->mId === null and $this->mName !== null @@ -1242,7 +1382,8 @@ class User { } /** - * Set the user and reload all fields according to that ID + * Set the user and reload all fields according to a given ID + * @param $v \int User ID to reload */ function setId( $v ) { $this->mId = $v; @@ -1250,7 +1391,8 @@ class User { } /** - * Get the user name, or the IP for anons + * Get the user name, or the IP of an anonymous user + * @return \string User's name or IP address */ function getName() { if ( !$this->mDataLoaded && $this->mFrom == 'name' ) { @@ -1275,8 +1417,9 @@ class User { * address for an anonymous user to something other than the current * remote IP. * - * User::newFromName() has rougly the same function, when the named user + * @note User::newFromName() has rougly the same function, when the named user * does not exist. + * @param $str \string New user name to set */ function setName( $str ) { $this->load(); @@ -1284,13 +1427,17 @@ class User { } /** - * Return the title dbkey form of the name, for eg user pages. - * @return string + * Get the user's name escaped by underscores. + * @return \string Username escaped by underscores. */ function getTitleKey() { return str_replace( ' ', '_', $this->getName() ); } + /** + * Check if the user has new messages. + * @return \bool True if the user has new messages + */ function getNewtalk() { $this->load(); @@ -1322,6 +1469,7 @@ class User { /** * Return the talk page(s) this user has new messages on. + * @return \type{\arrayof{\string}} Array of page URLs */ function getNewMessageLinks() { $talks = array(); @@ -1337,13 +1485,13 @@ class User { /** - * Perform a user_newtalk check, uncached. - * Use getNewtalk for a cached check. + * Internal uncached check for new messages * - * @param $field string - * @param $id mixed - * @param $fromMaster Bool: true to fetch from the master, false for a slave - * @return bool + * @see getNewtalk() + * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise + * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise + * @param $fromMaster \bool true to fetch from the master, false for a slave + * @return \bool True if the user has new messages * @private */ function checkNewtalk( $field, $id, $fromMaster = false ) { @@ -1358,9 +1506,10 @@ class User { } /** - * Add or update the - * @param $field string - * @param $id mixed + * Add or update the new messages flag + * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise + * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise + * @return \bool True if successful, false otherwise * @private */ function updateNewtalk( $field, $id ) { @@ -1380,8 +1529,9 @@ class User { /** * Clear the new messages flag for the given user - * @param $field string - * @param $id mixed + * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise + * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise + * @return \bool True if successful, false otherwise * @private */ function deleteNewtalk( $field, $id ) { @@ -1400,7 +1550,7 @@ class User { /** * Update the 'You have new messages!' status. - * @param $val bool + * @param $val \bool Whether the user has new messages */ function setNewtalk( $val ) { if( wfReadOnly() ) { @@ -1439,6 +1589,7 @@ class User { /** * Generate a current or new-future timestamp to be stored in the * user_touched field when we update things. + * @return \string Timestamp in TS_MW format */ private static function newTouchedTimestamp() { global $wgClockSkewFudge; @@ -1453,6 +1604,7 @@ class User { * Called implicitly from invalidateCache() and saveSettings(). */ private function clearSharedCache() { + $this->load(); if( $this->mId ) { global $wgMemc; $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) ); @@ -1479,13 +1631,25 @@ class User { } } + /** + * Validate the cache for this account. + * @param $timestamp \string A timestamp in TS_MW format + */ function validateCache( $timestamp ) { $this->load(); return ($timestamp >= $this->mTouched); } /** - * Set the password and reset the random token + * Get the user touched timestamp + */ + function getTouched() { + $this->load(); + return $this->mTouched; + } + + /** + * Set the password and reset the random token. * Calls through to authentication plugin if necessary; * will have no effect if the auth plugin refuses to * pass the change through or if the legal password @@ -1495,7 +1659,7 @@ class User { * wipes it, so the account cannot be logged in until * a new password is set, for instance via e-mail. * - * @param $str string + * @param $str \string New password to set * @throws PasswordError on failure */ function setPassword( $str ) { @@ -1523,10 +1687,9 @@ class User { } /** - * Set the password and reset the random token no matter - * what. + * Set the password and reset the random token unconditionally. * - * @param $str string + * @param $str \string New password to set */ function setInternalPassword( $str ) { $this->load(); @@ -1542,6 +1705,10 @@ class User { $this->mNewpassTime = null; } + /** + * Get the user's current token. + * @return \string Token + */ function getToken() { $this->load(); return $this->mToken; @@ -1550,6 +1717,8 @@ class User { /** * Set the random token (used for persistent authentication) * Called from loadDefaults() among other places. + * + * @param $token \string If specified, set the token to this value * @private */ function setToken( $token = false ) { @@ -1568,7 +1737,13 @@ class User { $this->mToken = $token; } } - + + /** + * Set the cookie password + * + * @param $str \string New cookie password + * @private + */ function setCookiePassword( $str ) { $this->load(); $this->mCookiePassword = md5( $str ); @@ -1576,7 +1751,9 @@ class User { /** * Set the password for a password reminder or new account email - * Sets the user_newpass_time field if $throttle is true + * + * @param $str \string New password to set + * @param $throttle \bool If true, reset the throttle timestamp to the present */ function setNewpassword( $str, $throttle = true ) { $this->load(); @@ -1587,8 +1764,9 @@ class User { } /** - * Returns true if a password reminder email has already been sent within - * the last $wgPasswordReminderResendTime hours + * Has password reminder email been sent within the last + * $wgPasswordReminderResendTime hours? + * @return \bool True or false */ function isPasswordReminderThrottled() { global $wgPasswordReminderResendTime; @@ -1600,38 +1778,62 @@ class User { return time() < $expiry; } + /** + * Get the user's e-mail address + * @return \string User's email address + */ function getEmail() { $this->load(); wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) ); return $this->mEmail; } + /** + * Get the timestamp of the user's e-mail authentication + * @return \string TS_MW timestamp + */ function getEmailAuthenticationTimestamp() { $this->load(); wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) ); return $this->mEmailAuthenticated; } + /** + * Set the user's e-mail address + * @param $str \string New e-mail address + */ function setEmail( $str ) { $this->load(); $this->mEmail = $str; wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) ); } + /** + * Get the user's real name + * @return \string User's real name + */ function getRealName() { $this->load(); return $this->mRealName; } + /** + * Set the user's real name + * @param $str \string New real name + */ function setRealName( $str ) { $this->load(); $this->mRealName = $str; } /** - * @param $oname String: the option to check - * @param $defaultOverride String: A default value returned if the option does not exist - * @return string + * Get the user's current setting for a given option. + * + * @param $oname \string The option to check + * @param $defaultOverride \string A default value returned if the option does not exist + * @return \string User's current value for the option + * @see getBoolOption() + * @see getIntOption() */ function getOption( $oname, $defaultOverride = '' ) { $this->load(); @@ -1649,46 +1851,41 @@ class User { return $defaultOverride; } } - - /** - * Get the user's date preference, including some important migration for - * old user rows. - */ - function getDatePreference() { - if ( is_null( $this->mDatePreference ) ) { - global $wgLang; - $value = $this->getOption( 'date' ); - $map = $wgLang->getDatePreferenceMigrationMap(); - if ( isset( $map[$value] ) ) { - $value = $map[$value]; - } - $this->mDatePreference = $value; - } - return $this->mDatePreference; - } - + /** - * @param $oname String: the option to check - * @return bool False if the option is not selected, true if it is + * Get the user's current setting for a given option, as a boolean value. + * + * @param $oname \string The option to check + * @return \bool User's current value for the option + * @see getOption() */ function getBoolOption( $oname ) { return (bool)$this->getOption( $oname ); } + /** - * Get an option as an integer value from the source string. - * @param $oname String: the option to check - * @param $default Int: optional value to return if option is unset/blank. - * @return int + * Get the user's current setting for a given option, as a boolean value. + * + * @param $oname \string The option to check + * @param $defaultOverride \int A default value returned if the option does not exist + * @return \int User's current value for the option + * @see getOption() */ - function getIntOption( $oname, $default=0 ) { + function getIntOption( $oname, $defaultOverride=0 ) { $val = $this->getOption( $oname ); if( $val == '' ) { - $val = $default; + $val = $defaultOverride; } return intval( $val ); } + /** + * Set the given option for a user. + * + * @param $oname \string The option to set + * @param $val \mixed New value to set + */ function setOption( $oname, $val ) { $this->load(); if ( is_null( $this->mOptions ) ) { @@ -1713,6 +1910,28 @@ class User { $this->mOptions[$oname] = $val; } + /** + * Get the user's preferred date format. + * @return \string User's preferred date format + */ + function getDatePreference() { + // Important migration for old data rows + if ( is_null( $this->mDatePreference ) ) { + global $wgLang; + $value = $this->getOption( 'date' ); + $map = $wgLang->getDatePreferenceMigrationMap(); + if ( isset( $map[$value] ) ) { + $value = $map[$value]; + } + $this->mDatePreference = $value; + } + return $this->mDatePreference; + } + + /** + * Get the permissions this user has. + * @return \type{\arrayof{\string}} Array of permission names + */ function getRights() { if ( is_null( $this->mRights ) ) { $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); @@ -1726,7 +1945,7 @@ class User { /** * Get the list of explicit group memberships this user has. * The implicit * and user groups are not included. - * @return array of strings + * @return \type{\arrayof{\string}} Array of internal group names */ function getGroups() { $this->load(); @@ -1737,8 +1956,8 @@ class User { * Get the list of implicit group memberships this user has. * This includes all explicit groups, plus 'user' if logged in, * '*' for all accounts and autopromoted groups - * @param $recache Boolean: don't use the cache - * @return array of strings + * @param $recache \bool Whether to avoid the cache + * @return \type{\arrayof{\string}} Array of internal group names */ function getEffectiveGroups( $recache = false ) { if ( $recache || is_null( $this->mEffectiveGroups ) ) { @@ -1759,7 +1978,10 @@ class User { return $this->mEffectiveGroups; } - /* Return the edit count for the user. This is where User::edits should have been */ + /** + * Get the user's edit count. + * @return \int User'e edit count + */ function getEditCount() { if ($this->mId) { if ( !isset( $this->mEditCount ) ) { @@ -1776,7 +1998,7 @@ class User { /** * Add the user to the given group. * This takes immediate effect. - * @param $group string + * @param $group \string Name of the group to add */ function addGroup( $group ) { $dbw = wfGetDB( DB_MASTER ); @@ -1800,7 +2022,7 @@ class User { /** * Remove the user from the given group. * This takes immediate effect. - * @param $group string + * @param $group \string Name of the group to remove */ function removeGroup( $group ) { $this->load(); @@ -1821,27 +2043,24 @@ class User { /** - * A more legible check for non-anonymousness. - * Returns true if the user is not an anonymous visitor. - * - * @return bool + * Get whether the user is logged in + * @return \bool True or false */ function isLoggedIn() { return $this->getID() != 0; } /** - * A more legible check for anonymousness. - * Returns true if the user is an anonymous visitor. - * - * @return bool + * Get whether the user is anonymous + * @return \bool True or false */ function isAnon() { return !$this->isLoggedIn(); } /** - * Whether the user is a bot + * Get whether the user is a bot + * @return \bool True or false * @deprecated */ function isBot() { @@ -1851,8 +2070,8 @@ class User { /** * Check if user is allowed to access a feature / make an action - * @param $action String: action to be checked - * @return boolean True: action is allowed, False: action should not be allowed + * @param $action \string action to be checked + * @return \bool True if action is allowed, else false */ function isAllowed($action='') { if ( $action === '' ) @@ -1866,7 +2085,7 @@ class User { /** * Check whether to enable recent changes patrol features for this user - * @return bool + * @return \bool True or false */ public function useRCPatrol() { global $wgUseRCPatrol; @@ -1874,8 +2093,8 @@ class User { } /** - * Check whether to enable recent changes patrol features for this user - * @return bool + * Check whether to enable new pages patrol features for this user + * @return \bool True or false */ public function useNPPatrol() { global $wgUseRCPatrol, $wgUseNPPatrol; @@ -1883,31 +2102,34 @@ class User { } /** - * Load a skin if it doesn't exist or return it + * Get the current skin, loading it if required + * @return \type{Skin} Current skin * @todo FIXME : need to check the old failback system [AV] */ function &getSkin() { - global $wgRequest; + global $wgRequest, $wgAllowUserSkin, $wgDefaultSkin; if ( ! isset( $this->mSkin ) ) { wfProfileIn( __METHOD__ ); - # get the user skin - $userSkin = $this->getOption( 'skin' ); - $userSkin = $wgRequest->getVal('useskin', $userSkin); - + if( $wgAllowUserSkin ) { + # get the user skin + $userSkin = $this->getOption( 'skin' ); + $userSkin = $wgRequest->getVal('useskin', $userSkin); + } else { + # if we're not allowing users to override, then use the default + $userSkin = $wgDefaultSkin; + } + $this->mSkin =& Skin::newFromKey( $userSkin ); wfProfileOut( __METHOD__ ); } return $this->mSkin; } - /**#@+ - * @param $title Title: article title to look at - */ - /** - * Check watched status of an article - * @return bool True if article is watched + * Check the watched status of an article. + * @param $title \type{Title} Title of the article to look at + * @return \bool True if article is watched */ function isWatched( $title ) { $wl = WatchedItem::fromUserTitle( $this, $title ); @@ -1915,7 +2137,8 @@ class User { } /** - * Watch an article + * Watch an article. + * @param $title \type{Title} Title of the article to look at */ function addWatch( $title ) { $wl = WatchedItem::fromUserTitle( $this, $title ); @@ -1924,7 +2147,8 @@ class User { } /** - * Stop watching an article + * Stop watching an article. + * @param $title \type{Title} Title of the article to look at */ function removeWatch( $title ) { $wl = WatchedItem::fromUserTitle( $this, $title ); @@ -1936,6 +2160,7 @@ class User { * Clear the user's notification timestamp for the given title. * If e-notif e-mails are on, they will receive notification mails on * the next change of the page if it's watched etc. + * @param $title \type{Title} Title of the article to look at */ function clearNotification( &$title ) { global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker; @@ -1991,14 +2216,12 @@ class User { } } - /**#@-*/ - /** * Resets all of the given user's page-change notification timestamps. * If e-notif e-mails are on, they will receive notification mails on * the next change of any watched page. * - * @param $currentUser Int: user ID number + * @param $currentUser \int User ID */ function clearAllNotifications( $currentUser ) { global $wgUseEnotif, $wgShowUpdatedMarker; @@ -2021,8 +2244,9 @@ class User { } /** + * Encode this user's options as a string + * @return \string Encoded options * @private - * @return string Encoding options */ function encodeOptions() { $this->load(); @@ -2038,6 +2262,8 @@ class User { } /** + * Set this user's options from an encoded string + * @param $str \string Encoded options to import * @private */ function decodeOptions( $str ) { @@ -2051,46 +2277,30 @@ class User { } } + /** + * Set a cookie on the user's client. Wrapper for + * WebResponse::setCookie + * @param $name \string Name of the cookie to set + * @param $value \string Value to set + * @param $exp \int Expiration time, as a UNIX time value; + * if 0 or not specified, use the default $wgCookieExpiration + */ protected function setCookie( $name, $value, $exp=0 ) { - global $wgCookiePrefix,$wgCookieDomain,$wgCookieSecure,$wgCookieExpiration, $wgCookieHttpOnly; - if( $exp == 0 ) { - $exp = time() + $wgCookieExpiration; - } - $httpOnlySafe = wfHttpOnlySafe(); - wfDebugLog( 'cookie', - 'setcookie: "' . implode( '", "', - array( - $wgCookiePrefix . $name, - $value, - $exp, - '/', - $wgCookieDomain, - $wgCookieSecure, - $httpOnlySafe && $wgCookieHttpOnly ) ) . '"' ); - if( $httpOnlySafe && isset( $wgCookieHttpOnly ) ) { - setcookie( $wgCookiePrefix . $name, - $value, - $exp, - '/', - $wgCookieDomain, - $wgCookieSecure, - $wgCookieHttpOnly ); - } else { - // setcookie() fails on PHP 5.1 if you give it future-compat paramters. - // stab stab! - setcookie( $wgCookiePrefix . $name, - $value, - $exp, - '/', - $wgCookieDomain, - $wgCookieSecure ); - } + global $wgRequest; + $wgRequest->response()->setcookie( $name, $value, $exp ); } + /** + * Clear a cookie on the user's client + * @param $name \string Name of the cookie to clear + */ protected function clearCookie( $name ) { $this->setCookie( $name, '', time() - 86400 ); } + /** + * Set the default cookies for this session on the user's client. + */ function setCookies() { $this->load(); if ( 0 == $this->mId ) return; @@ -2110,7 +2320,10 @@ class User { } wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) ); - $_SESSION = $session + $_SESSION; + #check for null, since the hook could cause a null value + if ( !is_null( $session ) && isset( $_SESSION ) ){ + $_SESSION = $session + $_SESSION; + } foreach ( $cookies as $name => $value ) { if ( $value === false ) { $this->clearCookie( $name ); @@ -2121,7 +2334,7 @@ class User { } /** - * Logout user. + * Log this user out. */ function logout() { global $wgUser; @@ -2131,8 +2344,9 @@ class User { } /** - * Really logout user - * Clears the cookies and session, resets the instance cache + * Clear the user's cookies and session, and reset the instance cache. + * @private + * @see logout() */ function doLogout() { $this->clearInstanceCache( 'defaults' ); @@ -2147,7 +2361,7 @@ class User { } /** - * Save object settings into database + * Save this user's settings into the database. * @todo Only rarely do all these fields need to be set! */ function saveSettings() { @@ -2178,10 +2392,11 @@ class User { ); wfRunHooks( 'UserSaveSettings', array( $this ) ); $this->clearSharedCache(); + $this->getUserPage()->invalidateCache(); } /** - * Checks if a user with the given name exists, returns the ID. + * If only this user's username is known, and it exists, return the user ID. */ function idForName() { $s = trim( $this->getName() ); @@ -2198,18 +2413,18 @@ class User { /** * Add a user to the database, return the user object * - * @param $name String: the user's name - * @param $params Associative array of non-default parameters to save to the database: - * password The user's password. Password logins will be disabled if this is omitted. - * newpassword A temporary password mailed to the user - * email The user's email address - * email_authenticated The email authentication timestamp - * real_name The user's real name - * options An associative array of non-default options - * token Random authentication token. Do not set. - * registration Registration timestamp. Do not set. + * @param $name \string Username to add + * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database: + * - password The user's password. Password logins will be disabled if this is omitted. + * - newpassword A temporary password mailed to the user + * - email The user's email address + * - email_authenticated The email authentication timestamp + * - real_name The user's real name + * - options An associative array of non-default options + * - token Random authentication token. Do not set. + * - registration Registration timestamp. Do not set. * - * @return User object, or null if the username already exists + * @return \type{User} A new User object, or null if the username already exists */ static function createNew( $name, $params = array() ) { $user = new User; @@ -2247,7 +2462,7 @@ class User { } /** - * Add an existing user object to the database + * Add this existing user object to the database */ function addToDatabase() { $this->load(); @@ -2271,13 +2486,13 @@ class User { ); $this->mId = $dbw->insertId(); - # Clear instance cache other than user table data, which is already accurate + // Clear instance cache other than user table data, which is already accurate $this->clearInstanceCache(); } /** - * If the (non-anonymous) user is blocked, this function will block any IP address - * that they successfully log on from. + * If this (non-anonymous) user is blocked, block any IP address + * they've successfully logged in from. */ function spreadBlock() { wfDebug( __METHOD__."()\n" ); @@ -2306,10 +2521,10 @@ class User { * which will give them a chance to modify this key based on their own * settings. * - * @return string + * @return \string Page rendering hash */ function getPageRenderingHash() { - global $wgContLang, $wgUseDynamicDates, $wgLang; + global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang; if( $this->mHash ){ return $this->mHash; } @@ -2329,6 +2544,8 @@ class User { $extra = $wgContLang->getExtraHashOptions(); $confstr .= $extra; + $confstr .= $wgRenderHashAppend; + // Give a chance for extensions to modify the hash, if they have // extra options or other effects on the parser cache. wfRunHooks( 'PageRenderingHash', array( &$confstr ) ); @@ -2339,21 +2556,28 @@ class User { return $confstr; } + /** + * Get whether the user is explicitly blocked from account creation. + * @return \bool True if blocked + */ function isBlockedFromCreateAccount() { $this->getBlockedStatus(); return $this->mBlock && $this->mBlock->mCreateAccount; } /** - * Determine if the user is blocked from using Special:Emailuser. - * - * @return boolean + * Get whether the user is blocked from using Special:Emailuser. + * @return \bool True if blocked */ function isBlockedFromEmailuser() { $this->getBlockedStatus(); return $this->mBlock && $this->mBlock->mBlockEmail; } + /** + * Get whether the user is allowed to create an account. + * @return \bool True if allowed + */ function isAllowedToCreateAccount() { return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); } @@ -2368,7 +2592,7 @@ class User { /** * Get this user's personal page title. * - * @return Title + * @return \type{Title} User's personal page title */ function getUserPage() { return Title::makeTitle( NS_USER, $this->getName() ); @@ -2377,7 +2601,7 @@ class User { /** * Get this user's talk page title. * - * @return Title + * @return \type{Title} User's talk page title */ function getTalkPage() { $title = $this->getUserPage(); @@ -2385,6 +2609,8 @@ class User { } /** + * Get the maximum valid user ID. + * @return \int User ID * @static */ function getMaxID() { @@ -2401,7 +2627,7 @@ class User { /** * Determine whether the user is a newbie. Newbies are either * anonymous IPs, or the most recently created accounts. - * @return bool True if it is a newbie. + * @return \bool True if the user is a newbie */ function isNewbie() { return !$this->isAllowed( 'autoconfirmed' ); @@ -2411,7 +2637,7 @@ class User { * Is the user active? We check to see if they've made at least * X number of edits in the last Y days. * - * @return bool true if the user is active, false if not + * @return \bool True if the user is active, false if not. */ public function isActiveEditor() { global $wgActiveUserEditCount, $wgActiveUserDays; @@ -2435,8 +2661,8 @@ class User { /** * Check to see if the given clear-text password is one of the accepted passwords - * @param $password String: user password. - * @return bool True if the given password is correct otherwise False. + * @param $password \string user password. + * @return \bool True if the given password is correct, otherwise False. */ function checkPassword( $password ) { global $wgAuth; @@ -2476,7 +2702,7 @@ class User { /** * Check if the given clear-text password matches the temporary password * sent by e-mail for password reset operations. - * @return bool + * @return \bool True if matches, false otherwise */ function checkTemporaryPassword( $plaintext ) { return self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ); @@ -2488,9 +2714,8 @@ class User { * login credentials aren't being hijacked with a foreign form * submission. * - * @param $salt Mixed: optional function-specific data for hash. - * Use a string or an array of strings. - * @return string + * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing + * @return \string The new edit token */ function editToken( $salt = '' ) { if ( $this->isAnon() ) { @@ -2510,9 +2735,10 @@ class User { } /** - * Generate a hex-y looking random token for various uses. - * Could be made more cryptographically sure if someone cares. - * @return string + * Generate a looking random token for various uses. + * + * @param $salt \string Optional salt value + * @return \string The new random token */ function generateToken( $salt = '' ) { $token = dechex( mt_rand() ) . dechex( mt_rand() ); @@ -2525,9 +2751,9 @@ class User { * user's own login session, not a form submission from a third-party * site. * - * @param $val String: the input value to compare - * @param $salt String: optional function-specific data for hash - * @return bool + * @param $val \string Input value to compare + * @param $salt \string Optional function-specific data for hashing + * @return \bool Whether the token matches */ function matchEditToken( $val, $salt = '' ) { $sessionToken = $this->editToken( $salt ); @@ -2538,7 +2764,12 @@ class User { } /** - * Check whether the edit token is fine except for the suffix + * Check given value against the token value stored in the session, + * ignoring the suffix. + * + * @param $val \string Input value to compare + * @param $salt \string Optional function-specific data for hashing + * @return \bool Whether the token matches */ function matchEditTokenNoSuffix( $val, $salt = '' ) { $sessionToken = $this->editToken( $salt ); @@ -2549,10 +2780,7 @@ class User { * Generate a new e-mail confirmation token and send a confirmation/invalidation * mail to the user's given address. * - * Calls saveSettings() internally; as it has side effects, not committing changes - * would be pretty silly. - * - * @return mixed True on success, a WikiError object on failure. + * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure. */ function sendConfirmationMail() { global $wgLang; @@ -2575,11 +2803,11 @@ class User { * Send an e-mail to this user's account. Does not check for * confirmed status or validity. * - * @param $subject string - * @param $body string - * @param $from string: optional from address; default $wgPasswordSender will be used otherwise. - * @param $replyto string - * @return mixed True on success, a WikiError object on failure. + * @param $subject \string Message subject + * @param $body \string Message body + * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used + * @param $replyto \string Reply-To address + * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure */ function sendMail( $subject, $body, $from = null, $replyto = null ) { if( is_null( $from ) ) { @@ -2594,13 +2822,13 @@ class User { /** * Generate, store, and return a new e-mail confirmation code. - * A hash (unsalted since it's used as a key) is stored. + * A hash (unsalted, since it's used as a key) is stored. * - * Call saveSettings() after calling this function to commit + * @note Call saveSettings() after calling this function to commit * this change to the database. * - * @param &$expiration mixed output: accepts the expiration time - * @return string + * @param[out] &$expiration \mixed Accepts the expiration time + * @return \string New token * @private */ function confirmationToken( &$expiration ) { @@ -2617,8 +2845,8 @@ class User { /** * Return a URL the user can use to confirm their email address. - * @param $token accepts the email confirmation token - * @return string + * @param $token \string Accepts the email confirmation token + * @return \string New token URL * @private */ function confirmationTokenUrl( $token ) { @@ -2626,8 +2854,8 @@ class User { } /** * Return a URL the user can use to invalidate their email address. - * @param $token accepts the email confirmation token - * @return string + * @param $token \string Accepts the email confirmation token + * @return \string New token URL * @private */ function invalidationTokenUrl( $token ) { @@ -2639,10 +2867,14 @@ class User { * This uses $wgArticlePath directly as a quickie hack to use the * hardcoded English names of the Special: pages, for ASCII safety. * - * Since these URLs get dropped directly into emails, using the + * @note Since these URLs get dropped directly into emails, using the * short English names avoids insanely long URL-encoded links, which * also sometimes can get corrupted in some browsers/mailers * (bug 6957 with Gmail and Internet Explorer). + * + * @param $page \string Special page + * @param $token \string Token + * @return \string Formatted URL */ protected function getTokenUrl( $page, $token ) { global $wgArticlePath; @@ -2656,7 +2888,7 @@ class User { /** * Mark the e-mail address confirmed. * - * Call saveSettings() after calling this function to commit the change. + * @note Call saveSettings() after calling this function to commit the change. */ function confirmEmail() { $this->setEmailAuthenticationTimestamp( wfTimestampNow() ); @@ -2664,10 +2896,10 @@ class User { } /** - * Invalidate the user's email confirmation, unauthenticate the email - * if it was already confirmed. + * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail + * address if it was already confirmed. * - * Call saveSettings() after calling this function to commit the change. + * @note Call saveSettings() after calling this function to commit the change. */ function invalidateEmail() { $this->load(); @@ -2677,6 +2909,10 @@ class User { return true; } + /** + * Set the e-mail authentication timestamp. + * @param $timestamp \string TS_MW timestamp + */ function setEmailAuthenticationTimestamp( $timestamp ) { $this->load(); $this->mEmailAuthenticated = $timestamp; @@ -2686,9 +2922,13 @@ class User { /** * Is this user allowed to send e-mails within limits of current * site configuration? - * @return bool + * @return \bool True if allowed */ function canSendEmail() { + global $wgEnableEmail, $wgEnableUserEmail; + if( !$wgEnableEmail || !$wgEnableUserEmail ) { + return false; + } $canSend = $this->isEmailConfirmed(); wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) ); return $canSend; @@ -2697,7 +2937,7 @@ class User { /** * Is this user allowed to receive e-mails within limits of current * site configuration? - * @return bool + * @return \bool True if allowed */ function canReceiveEmail() { return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' ); @@ -2707,11 +2947,11 @@ class User { * Is this user's e-mail address valid-looking and confirmed within * limits of the current site configuration? * - * If $wgEmailAuthentication is on, this may require the user to have + * @note If $wgEmailAuthentication is on, this may require the user to have * confirmed their address by returning a code or using a password * sent to the address from the wiki. * - * @return bool + * @return \bool True if confirmed */ function isEmailConfirmed() { global $wgEmailAuthentication; @@ -2731,8 +2971,8 @@ class User { } /** - * Return true if there is an outstanding request for e-mail confirmation. - * @return bool + * Check whether there is an outstanding request for e-mail confirmation. + * @return \bool True if pending */ function isEmailConfirmationPending() { global $wgEmailAuthentication; @@ -2743,20 +2983,40 @@ class User { } /** - * Get the timestamp of account creation, or false for - * non-existent/anonymous user accounts + * Get the timestamp of account creation. * - * @return mixed + * @return \types{\string,\bool} string Timestamp of account creation, or false for + * non-existent/anonymous user accounts. */ public function getRegistration() { - return $this->mId > 0 + return $this->getId() > 0 ? $this->mRegistration : false; } + + /** + * Get the timestamp of the first edit + * + * @return \types{\string,\bool} string Timestamp of first edit, or false for + * non-existent/anonymous user accounts. + */ + public function getFirstEditTimestamp() { + if( $this->getId() == 0 ) return false; // anons + $dbr = wfGetDB( DB_SLAVE ); + $time = $dbr->selectField( 'revision', 'rev_timestamp', + array( 'rev_user' => $this->getId() ), + __METHOD__, + array( 'ORDER BY' => 'rev_timestamp ASC' ) + ); + if( !$time ) return false; // no edits + return wfTimestamp( TS_MW, $time ); + } /** - * @param $groups Array: list of groups - * @return array list of permission key names for given groups combined + * Get the permissions associated with a given list of groups + * + * @param $groups \type{\arrayof{\string}} List of internal group names + * @return \type{\arrayof{\string}} List of permission key names for given groups combined */ static function getGroupPermissions( $groups ) { global $wgGroupPermissions; @@ -2764,15 +3024,35 @@ class User { foreach( $groups as $group ) { if( isset( $wgGroupPermissions[$group] ) ) { $rights = array_merge( $rights, + // array_filter removes empty items array_keys( array_filter( $wgGroupPermissions[$group] ) ) ); } } - return $rights; + return array_unique($rights); + } + + /** + * Get all the groups who have a given permission + * + * @param $role \string Role to check + * @return \type{\arrayof{\string}} List of internal group names with the given permission + */ + static function getGroupsWithPermission( $role ) { + global $wgGroupPermissions; + $allowedGroups = array(); + foreach ( $wgGroupPermissions as $group => $rights ) { + if ( isset( $rights[$role] ) && $rights[$role] ) { + $allowedGroups[] = $group; + } + } + return $allowedGroups; } /** - * @param $group String: key name - * @return string localized descriptive name for group, if provided + * Get the localized descriptive name for a group, if it exists + * + * @param $group \string Internal group name + * @return \string Localized descriptive group name */ static function getGroupName( $group ) { global $wgMessageCache; @@ -2785,8 +3065,10 @@ class User { } /** - * @param $group String: key name - * @return string localized descriptive name for member of a group, if provided + * Get the localized descriptive name for a member of a group, if it exists + * + * @param $group \string Internal group name + * @return \string Localized name for group member */ static function getGroupMember( $group ) { global $wgMessageCache; @@ -2801,9 +3083,8 @@ class User { /** * Return the set of defined explicit groups. * The implicit groups (by default *, 'user' and 'autoconfirmed') - * are not included, as they are defined automatically, - * not in the database. - * @return array + * are not included, as they are defined automatically, not in the database. + * @return \type{\arrayof{\string}} Array of internal group names */ static function getAllGroups() { global $wgGroupPermissions; @@ -2814,7 +3095,8 @@ class User { } /** - * Get a list of all available permissions + * Get a list of all available permissions. + * @return \type{\arrayof{\string}} Array of permission names */ static function getAllRights() { if ( self::$mAllRights === false ) { @@ -2831,8 +3113,7 @@ class User { /** * Get a list of implicit groups - * - * @return array + * @return \type{\arrayof{\string}} Array of internal group names */ public static function getImplicitGroups() { global $wgImplicitGroups; @@ -2844,8 +3125,8 @@ class User { /** * Get the title of a page describing a particular group * - * @param $group Name of the group - * @return mixed + * @param $group \string Internal group name + * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise */ static function getGroupPage( $group ) { global $wgMessageCache; @@ -2860,11 +3141,12 @@ class User { } /** - * Create a link to the group in HTML, if available + * Create a link to the group in HTML, if available; + * else return the group name. * - * @param $group Name of the group - * @param $text The text of the link - * @return mixed + * @param $group \string Internal name of the group + * @param $text \string The text of the link + * @return \string HTML link to the group */ static function makeGroupLinkHTML( $group, $text = '' ) { if( $text == '' ) { @@ -2881,11 +3163,12 @@ class User { } /** - * Create a link to the group in Wikitext, if available + * Create a link to the group in Wikitext, if available; + * else return the group name. * - * @param $group Name of the group - * @param $text The text of the link (by default, the name of the group) - * @return mixed + * @param $group \string Internal name of the group + * @param $text \string The text of the link + * @return \string Wikilink to the group */ static function makeGroupLinkWiki( $group, $text = '' ) { if( $text == '' ) { @@ -2944,6 +3227,12 @@ class User { $this->invalidateCache(); } + /** + * Get the description of a given right + * + * @param $right \string Right to query + * @return \string Localized description of the right + */ static function getRightDescription( $right ) { global $wgMessageCache; $wgMessageCache->loadAllMessages(); @@ -2957,8 +3246,9 @@ class User { /** * Make an old-style password hash * - * @param $password String: plain-text password - * @param $userId String: user ID + * @param $password \string Plain-text password + * @param $userId \string User ID + * @return \string Password hash */ static function oldCrypt( $password, $userId ) { global $wgPasswordSalt; @@ -2972,19 +3262,26 @@ class User { /** * Make a new-style password hash * - * @param $password String: plain-text password - * @param $salt String: salt, may be random or the user ID. False to generate a salt. + * @param $password \string Plain-text password + * @param $salt \string Optional salt, may be random or the user ID. + * If unspecified or false, will generate one automatically + * @return \string Password hash */ static function crypt( $password, $salt = false ) { global $wgPasswordSalt; - if($wgPasswordSalt) { + $hash = ''; + if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) { + return $hash; + } + + if( $wgPasswordSalt ) { if ( $salt === false ) { $salt = substr( wfGenerateToken(), 0, 8 ); } return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); } else { - return ':A:' . md5( $password); + return ':A:' . md5( $password ); } } @@ -2992,13 +3289,20 @@ class User { * Compare a password hash with a plain-text password. Requires the user * ID if there's a chance that the hash is an old-style hash. * - * @param $hash String: password hash - * @param $password String: plain-text password to compare - * @param $userId String: user ID for old-style password salt + * @param $hash \string Password hash + * @param $password \string Plain-text password to compare + * @param $userId \string User ID for old-style password salt + * @return \bool */ static function comparePasswords( $hash, $password, $userId = false ) { $m = false; $type = substr( $hash, 0, 3 ); + + $result = false; + if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) { + return $result; + } + if ( $type == ':A:' ) { # Unsalted return md5( $password ) === substr( $hash, 3 ); @@ -3011,4 +3315,41 @@ class User { return self::oldCrypt( $password, $userId ) === $hash; } } + + /** + * Add a newuser log entry for this user + * @param $byEmail Boolean: account made by email? + */ + public function addNewUserLogEntry( $byEmail = false ) { + global $wgUser, $wgContLang, $wgNewUserLog; + if( empty($wgNewUserLog) ) { + return true; // disabled + } + $talk = $wgContLang->getFormattedNsText( NS_TALK ); + if( $this->getName() == $wgUser->getName() ) { + $action = 'create'; + $message = ''; + } else { + $action = 'create2'; + $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : ''; + } + $log = new LogPage( 'newusers' ); + $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) ); + return true; + } + + /** + * Add an autocreate newuser log entry for this user + * Used by things like CentralAuth and perhaps other authplugins. + */ + public function addNewUserLogEntryAutoCreate() { + global $wgNewUserLog; + if( empty($wgNewUserLog) ) { + return true; // disabled + } + $log = new LogPage( 'newusers', false ); + $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) ); + return true; + } + } diff --git a/includes/UserArray.php b/includes/UserArray.php index 27847e6f..a2f54b7f 100644 --- a/includes/UserArray.php +++ b/includes/UserArray.php @@ -36,6 +36,10 @@ class UserArrayFromResult extends UserArray { } } + public function count() { + return $this->res->numRows(); + } + function current() { return $this->current; } diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 0bc4268f..ab1a740b 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -32,13 +32,15 @@ class MailAddress { * @param $address Mixed: string with an email address, or a User object * @param $name String: human-readable name if a string address is given */ - function __construct( $address, $name=null ) { + function __construct( $address, $name = null, $realName = null ) { if( is_object( $address ) && $address instanceof User ) { $this->address = $address->getEmail(); $this->name = $address->getName(); + $this->realName = $address->getRealName(); } else { $this->address = strval( $address ); $this->name = strval( $name ); + $this->reaName = strval( $realName ); } } @@ -51,7 +53,9 @@ class MailAddress { # can't handle "Joe Bloggs <joe@bloggs.com>" format email addresses, # so don't bother generating them if( $this->name != '' && !wfIsWindows() ) { - $quoted = wfQuotedPrintable( $this->name ); + global $wgEnotifUseRealName; + $name = ( $wgEnotifUseRealName && $this->realName ) ? $this->realName : $this->name; + $quoted = wfQuotedPrintable( $name ); if( strpos( $quoted, '.' ) !== false || strpos( $quoted, ',' ) !== false ) { $quoted = '"' . $quoted . '"'; } @@ -98,9 +102,10 @@ class UserMailer { * @param $subject String: email's subject. * @param $body String: email's text. * @param $replyto String: optional reply-to email (default: null). + * @param $contentType String: optional custom Content-Type * @return mixed True on success, a WikiError object on failure. */ - static function send( $to, $from, $subject, $body, $replyto=null ) { + static function send( $to, $from, $subject, $body, $replyto=null, $contentType=null ) { global $wgSMTP, $wgOutputEncoding, $wgErrorString, $wgEnotifImpersonal; global $wgEnotifMaxRecips; @@ -139,7 +144,8 @@ class UserMailer { $headers['Subject'] = wfQuotedPrintable( $subject ); $headers['Date'] = date( 'r' ); $headers['MIME-Version'] = '1.0'; - $headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding; + $headers['Content-type'] = (is_null($contentType) ? + 'text/plain; charset='.$wgOutputEncoding : $contentType); $headers['Content-transfer-encoding'] = '8bit'; $headers['Message-ID'] = "<$msgid@" . $wgSMTP['IDHost'] . '>'; // FIXME $headers['X-Mailer'] = 'MediaWiki mailer'; @@ -170,9 +176,11 @@ class UserMailer { } else { $endl = "\n"; } + $ctype = (is_null($contentType) ? + 'text/plain; charset='.$wgOutputEncoding : $contentType); $headers = "MIME-Version: 1.0$endl" . - "Content-type: text/plain; charset={$wgOutputEncoding}$endl" . + "Content-type: $ctype$endl" . "Content-Transfer-Encoding: 8bit$endl" . "X-Mailer: MediaWiki mailer$endl". 'From: ' . $from->toString(); @@ -255,14 +263,9 @@ class UserMailer { * */ class EmailNotification { - /**@{{ - * @private - */ - var $to, $subject, $body, $replyto, $from; - var $user, $title, $timestamp, $summary, $minorEdit, $oldid, $composed_common, $editor; - var $mailTargets = array(); - - /**@}}*/ + private $to, $subject, $body, $replyto, $from; + private $user, $title, $timestamp, $summary, $minorEdit, $oldid, $composed_common, $editor; + private $mailTargets = array(); /** * Send emails corresponding to the user $editor editing the page $title. @@ -339,7 +342,7 @@ class EmailNotification { $userTalkId = false; - if ( (!$minorEdit || $wgEnotifMinorEdits) ) { + if ( !$minorEdit || ($wgEnotifMinorEdits && !$editor->isAllowed('nominornewtalk') ) ) { if ( $wgEnotifUserTalk && $isUserTalkPage ) { $targetUser = User::newFromName( $title->getText() ); if ( !$targetUser || $targetUser->isAnon() ) { @@ -347,9 +350,13 @@ class EmailNotification { } elseif ( $targetUser->getId() == $editor->getId() ) { wfDebug( __METHOD__.": user edited their own talk page, no notification sent\n" ); } elseif( $targetUser->getOption( 'enotifusertalkpages' ) ) { - wfDebug( __METHOD__.": sending talk page update notification\n" ); - $this->compose( $targetUser ); - $userTalkId = $targetUser->getId(); + if( $targetUser->isEmailConfirmed() ) { + wfDebug( __METHOD__.": sending talk page update notification\n" ); + $this->compose( $targetUser ); + $userTalkId = $targetUser->getId(); + } else { + wfDebug( __METHOD__.": talk page owner doesn't have validated email\n" ); + } } else { wfDebug( __METHOD__.": talk page owner doesn't want notifications\n" ); } @@ -396,7 +403,9 @@ class EmailNotification { $this->sendMails(); - if ( $wgShowUpdatedMarker || $wgEnotifWatchlist ) { + $latestTimestamp = Revision::getTimestampFromId( $title, $title->getLatestRevID() ); + // Do not update watchlists if something else already did. + if ( $timestamp >= $latestTimestamp && ($wgShowUpdatedMarker || $wgEnotifWatchlist) ) { # Mark the changed watch-listed page with a timestamp, so that the page is # listed with an "updated since your last visit" icon in the watch list. Do # not do this to users for their own edits. @@ -422,7 +431,7 @@ class EmailNotification { function composeCommonMailtext() { global $wgPasswordSender, $wgNoReplyAddress; global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress; - global $wgEnotifImpersonal; + global $wgEnotifImpersonal, $wgEnotifUseRealName; $this->composed_common = true; @@ -439,9 +448,6 @@ class EmailNotification { $replyto = ''; /* fail safe */ $keys = array(); - # regarding the use of oldid as an indicator for the last visited version, see also - # http://bugzilla.wikipeda.org/show_bug.cgi?id=603 "Delete + undelete cycle doesn't preserve old_id" - # However, in the case of a new page which is already watched, we have no previous version to compare if( $this->oldid ) { $difflink = $this->title->getFullUrl( 'diff=0&oldid=' . $this->oldid ); $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_lastvisited', $difflink ); @@ -476,7 +482,7 @@ class EmailNotification { # the user has not opted-out and the option is enabled at the # global configuration level. $editor = $this->editor; - $name = $editor->getName(); + $name = $wgEnotifUseRealName ? $editor->getRealName() : $editor->getName(); $adminAddress = new MailAddress( $wgPasswordSender, 'WikiAdmin' ); $editorAddress = new MailAddress( $editor ); if( $wgEnotifRevealEditorAddress @@ -557,12 +563,13 @@ class EmailNotification { * @private */ function sendPersonalised( $watchingUser ) { - global $wgLang; + global $wgLang, $wgEnotifUseRealName; // From the PHP manual: // Note: The to parameter cannot be an address in the form of "Something <someone@example.com>". // The mail command will not parse this properly while talking with the MTA. $to = new MailAddress( $watchingUser ); - $body = str_replace( '$WATCHINGUSERNAME', $watchingUser->getName() , $this->body ); + $name = $wgEnotifUseRealName ? $watchingUser->getRealName() : $watchingUser->getName(); + $body = str_replace( '$WATCHINGUSERNAME', $name , $this->body ); $timecorrection = $watchingUser->getOption( 'timecorrection' ); diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php index 23fc6a74..2d2d34f1 100644 --- a/includes/WatchedItem.php +++ b/includes/WatchedItem.php @@ -8,22 +8,23 @@ * @ingroup Watchlist */ class WatchedItem { - var $mTitle, $mUser; + var $mTitle, $mUser, $id, $ns, $ti; /** * Create a WatchedItem object with the given user and title - * @todo document - * @access private + * @param $user User: the user to use for (un)watching + * @param $title Title: the title we're going to (un)watch + * @return WatchedItem object */ - static function fromUserTitle( $user, $title ) { + public static function fromUserTitle( $user, $title ) { $wl = new WatchedItem; $wl->mUser = $user; $wl->mTitle = $title; $wl->id = $user->getId(); -# Patch (also) for email notification on page changes T.Gries/M.Arndt 11.09.2004 -# TG patch: here we do not consider pages and their talk pages equivalent - why should we ? -# The change results in talk-pages not automatically included in watchlists, when their parent page is included -# $wl->ns = $title->getNamespace() & ~1; + # Patch (also) for email notification on page changes T.Gries/M.Arndt 11.09.2004 + # TG patch: here we do not consider pages and their talk pages equivalent - why should we ? + # The change results in talk-pages not automatically included in watchlists, when their parent page is included + # $wl->ns = $title->getNamespace() & ~1; $wl->ns = $title->getNamespace(); $wl->ti = $title->getDBkey(); @@ -32,8 +33,9 @@ class WatchedItem { /** * Is mTitle being watched by mUser? + * @return bool */ - function isWatched() { + public function isWatched() { # Pages and their talk pages are considered equivalent for watching; # remember that talk namespaces are numbered as page namespace+1. $fname = 'WatchedItem::isWatched'; @@ -46,9 +48,11 @@ class WatchedItem { } /** - * @todo document + * Given a title and user (assumes the object is setup), add the watch to the + * database. + * @return bool (always true) */ - function addWatch() { + public function addWatch() { $fname = 'WatchedItem::addWatch'; wfProfileIn( $fname ); @@ -77,7 +81,11 @@ class WatchedItem { return true; } - function removeWatch() { + /** + * Same as addWatch, only the opposite. + * @return bool + */ + public function removeWatch() { $fname = 'WatchedItem::removeWatch'; $success = false; @@ -118,11 +126,14 @@ class WatchedItem { * @param $ot Title: page title to duplicate entries from, if present * @param $nt Title: page title to add watches on */ - static function duplicateEntries( $ot, $nt ) { + public static function duplicateEntries( $ot, $nt ) { WatchedItem::doDuplicateEntries( $ot->getSubjectPage(), $nt->getSubjectPage() ); WatchedItem::doDuplicateEntries( $ot->getTalkPage(), $nt->getTalkPage() ); } + /** + * Handle duplicate entries. Backend for duplicateEntries(). + */ private static function doDuplicateEntries( $ot, $nt ) { $fname = "WatchedItem::duplicateEntries"; $oldnamespace = $ot->getNamespace(); diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php index fcfdb782..e49851bd 100644 --- a/includes/WatchlistEditor.php +++ b/includes/WatchlistEditor.php @@ -46,19 +46,19 @@ class WatchlistEditor { $this->unwatchTitles( $toUnwatch, $user ); $user->invalidateCache(); if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) - $output->addHtml( wfMsgExt( 'watchlistedit-raw-done', 'parse' ) ); + $output->addHTML( wfMsgExt( 'watchlistedit-raw-done', 'parse' ) ); if( ( $count = count( $toWatch ) ) > 0 ) { - $output->addHtml( wfMsgExt( 'watchlistedit-raw-added', 'parse', $count ) ); + $output->addHTML( wfMsgExt( 'watchlistedit-raw-added', 'parse', $count ) ); $this->showTitles( $toWatch, $output, $wgUser->getSkin() ); } if( ( $count = count( $toUnwatch ) ) > 0 ) { - $output->addHtml( wfMsgExt( 'watchlistedit-raw-removed', 'parse', $count ) ); + $output->addHTML( wfMsgExt( 'watchlistedit-raw-removed', 'parse', $count ) ); $this->showTitles( $toUnwatch, $output, $wgUser->getSkin() ); } } else { $this->clearWatchlist( $user ); $user->invalidateCache(); - $output->addHtml( wfMsgExt( 'watchlistedit-raw-removed', 'parse', count( $current ) ) ); + $output->addHTML( wfMsgExt( 'watchlistedit-raw-removed', 'parse', count( $current ) ) ); $this->showTitles( $current, $output, $wgUser->getSkin() ); } } @@ -70,7 +70,7 @@ class WatchlistEditor { $titles = $this->extractTitles( $request->getArray( 'titles' ) ); $this->unwatchTitles( $titles, $user ); $user->invalidateCache(); - $output->addHtml( wfMsgExt( 'watchlistedit-normal-done', 'parse', + $output->addHTML( wfMsgExt( 'watchlistedit-normal-done', 'parse', $GLOBALS['wgLang']->formatNum( count( $titles ) ) ) ); $this->showTitles( $titles, $output, $wgUser->getSkin() ); } @@ -138,16 +138,16 @@ class WatchlistEditor { } $batch->execute(); // Print out the list - $output->addHtml( "<ul>\n" ); + $output->addHTML( "<ul>\n" ); foreach( $titles as $title ) { if( !$title instanceof Title ) $title = Title::newFromText( $title ); if( $title instanceof Title ) { - $output->addHtml( "<li>" . $skin->makeLinkObj( $title ) + $output->addHTML( "<li>" . $skin->makeLinkObj( $title ) . ' (' . $skin->makeLinkObj( $title->getTalkPage(), $talk ) . ")</li>\n" ); } } - $output->addHtml( "</ul>\n" ); + $output->addHTML( "</ul>\n" ); } /** @@ -239,10 +239,10 @@ class WatchlistEditor { */ private function showItemCount( $output, $user ) { if( ( $count = $this->countWatchlist( $user ) ) > 0 ) { - $output->addHtml( wfMsgExt( 'watchlistedit-numitems', 'parse', + $output->addHTML( wfMsgExt( 'watchlistedit-numitems', 'parse', $GLOBALS['wgLang']->formatNum( $count ) ) ); } else { - $output->addHtml( wfMsgExt( 'watchlistedit-noitems', 'parse' ) ); + $output->addHTML( wfMsgExt( 'watchlistedit-noitems', 'parse' ) ); } return $count; } @@ -323,6 +323,8 @@ class WatchlistEditor { ), __METHOD__ ); + $article = new Article($title); + wfRunHooks('UnwatchArticleComplete',array(&$user,&$article)); } } } @@ -340,21 +342,47 @@ class WatchlistEditor { $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl( 'action=edit' ) ) ); $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) ); - $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-normal-legend' ) . '</legend>'; + $form .= "<fieldset>\n<legend>" . wfMsgHtml( 'watchlistedit-normal-legend' ) . "</legend>"; $form .= wfMsgExt( 'watchlistedit-normal-explain', 'parse' ); - foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ) { - $form .= '<h2>' . $this->getNamespaceHeading( $namespace ) . '</h2>'; - $form .= '<ul>'; - foreach( $pages as $dbkey => $redirect ) { - $title = Title::makeTitleSafe( $namespace, $dbkey ); - $form .= $this->buildRemoveLine( $title, $redirect, $wgUser->getSkin() ); - } - $form .= '</ul>'; - } + $form .= $this->buildRemoveList( $user, $wgUser->getSkin() ); $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-normal-submit' ) ) . '</p>'; $form .= '</fieldset></form>'; - $output->addHtml( $form ); + $output->addHTML( $form ); + } + } + + /** + * Build the part of the standard watchlist editing form with the actual + * title selection checkboxes and stuff. Also generates a table of + * contents if there's more than one heading. + * + * @param $user User + * @param $skin Skin (really, Linker) + */ + private function buildRemoveList( $user, $skin ) { + $list = ""; + $toc = $skin->tocIndent(); + $tocLength = 0; + foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ) { + $tocLength++; + $heading = htmlspecialchars( $this->getNamespaceHeading( $namespace ) ); + $anchor = "editwatchlist-ns" . $namespace; + + $list .= $skin->makeHeadLine( 2, ">", $anchor, $heading, "" ); + $toc .= $skin->tocLine( $anchor, $heading, $tocLength, 1 ) . $skin->tocLineEnd(); + + $list .= "<ul>\n"; + foreach( $pages as $dbkey => $redirect ) { + $title = Title::makeTitleSafe( $namespace, $dbkey ); + $list .= $this->buildRemoveLine( $title, $redirect, $skin ); + } + $list .= "</ul>\n"; + } + // ISSUE: omit the TOC if the total number of titles is low? + if( $tocLength > 1 ) { + $list = $skin->tocList( $toc ) . $list; } + return $list; } /** @@ -389,9 +417,9 @@ class WatchlistEditor { if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) { $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getText() ), wfMsgHtml( 'contributions' ) ); } - return '<li>' + return "<li>" . Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) ) - . $link . ' (' . implode( ' | ', $tools ) . ')' . '</li>'; + . $link . " (" . implode( ' | ', $tools ) . ")" . "</li>\n"; } /** @@ -419,7 +447,7 @@ class WatchlistEditor { $form .= '</textarea>'; $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '</p>'; $form .= '</fieldset></form>'; - $output->addHtml( $form ); + $output->addHTML( $form ); } /** diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 3fce5845..46747125 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -39,7 +39,8 @@ if ( !function_exists( '__autoload' ) ) { * not create a second WebRequest object; make a FauxRequest object if * you want to pass arbitrary data to some function in place of the web * input. - * + * + * @ingroup HTTP */ class WebRequest { var $data = array(); @@ -54,7 +55,7 @@ class WebRequest { // POST overrides GET data // We don't use $_REQUEST here to avoid interference from cookies... - $this->data = wfArrayMerge( $_GET, $_POST ); + $this->data = $_POST + $_GET; } /** @@ -255,6 +256,18 @@ class WebRequest { return (string)$val; } } + + /** + * Set an aribtrary value into our get/post data. + * @param $key string Key name to use + * @param $value mixed Value to set + * @return mixed old value if one was present, null otherwise + */ + function setVal( $key, $value ) { + $ret = isset( $this->data[$key] ) ? $this->data[$key] : null; + $this->data[$key] = $value; + return $ret; + } /** * Fetch an array from the input or return $default if it's not set. @@ -507,7 +520,7 @@ class WebRequest { unset( $newquery['title'] ); $newquery = array_merge( $newquery, $array ); $query = wfArrayToCGI( $newquery ); - return $onlyquery ? $query : $wgTitle->getLocalURL( $basequery ); + return $onlyquery ? $query : $wgTitle->getLocalURL( $query ); } /** @@ -636,11 +649,24 @@ class WebRequest { } } } + + /* + * Get data from $_SESSION + */ + function getSessionData( $key ) { + if( !isset( $_SESSION[$key] ) ) + return null; + return $_SESSION[$key]; + } + function setSessionData( $key, $data ) { + $_SESSION[$key] = $data; + } } /** * WebRequest clone which takes values from a provided array. * + * @ingroup HTTP */ class FauxRequest extends WebRequest { var $wasPosted = false; @@ -650,7 +676,7 @@ class FauxRequest extends WebRequest { * fake GET/POST values * @param $wasPosted Bool: whether to treat the data as POST */ - function FauxRequest( $data, $wasPosted = false ) { + function FauxRequest( $data, $wasPosted = false, $session = null ) { if( is_array( $data ) ) { $this->data = $data; } else { @@ -658,6 +684,11 @@ class FauxRequest extends WebRequest { } $this->wasPosted = $wasPosted; $this->headers = array(); + $this->session = $session ? $session : array(); + } + + function notImplemented( $method ) { + throw new MWException( "{$method}() not implemented" ); } function getText( $name, $default = '' ) { @@ -678,15 +709,24 @@ class FauxRequest extends WebRequest { } function getRequestURL() { - throw new MWException( 'FauxRequest::getRequestURL() not implemented' ); + $this->notImplemented( __METHOD__ ); } function appendQuery( $query ) { - throw new MWException( 'FauxRequest::appendQuery() not implemented' ); + $this->notImplemented( __METHOD__ ); } function getHeader( $name ) { return isset( $this->headers[$name] ) ? $this->headers[$name] : false; } + function getSessionData( $key ) { + if( !isset( $this->session[$key] ) ) + return null; + return $this->session[$key]; + } + function setSessionData( $key, $data ) { + $this->notImplemented( __METHOD__ ); + } + } diff --git a/includes/WebResponse.php b/includes/WebResponse.php index 05023e15..09d37385 100644 --- a/includes/WebResponse.php +++ b/includes/WebResponse.php @@ -2,17 +2,59 @@ /** * Allow programs to request this object from WebRequest::response() * and handle all outputting (or lack of outputting) via it. + * @ingroup HTTP */ class WebResponse { - /** Output a HTTP header */ - function header($string, $replace=true) { + /** + * Output a HTTP header, wrapper for PHP's + * header() + * @param $string String: header to output + * @param $replace Bool: replace current similar header + */ + public function header($string, $replace=true) { header($string,$replace); } - /** Set the browser cookie */ - function setcookie($name, $value, $expire) { - global $wgCookiePath, $wgCookieDomain, $wgCookieSecure; - setcookie($name,$value,$expire, $wgCookiePath, $wgCookieDomain, $wgCookieSecure); + /** Set the browser cookie + * @param $name String: name of cookie + * @param $value String: value to give cookie + * @param $expire Int: number of seconds til cookie expires + */ + public function setcookie( $name, $value, $expire = 0 ) { + global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain; + global $wgCookieSecure,$wgCookieExpiration, $wgCookieHttpOnly; + if ( $expire == 0 ) { + $expire = time() + $wgCookieExpiration; + } + $httpOnlySafe = wfHttpOnlySafe(); + wfDebugLog( 'cookie', + 'setcookie: "' . implode( '", "', + array( + $wgCookiePrefix . $name, + $value, + $expire, + $wgCookiePath, + $wgCookieDomain, + $wgCookieSecure, + $httpOnlySafe && $wgCookieHttpOnly ) ) . '"' ); + if( $httpOnlySafe && isset( $wgCookieHttpOnly ) ) { + setcookie( $wgCookiePrefix . $name, + $value, + $expire, + $wgCookiePath, + $wgCookieDomain, + $wgCookieSecure, + $wgCookieHttpOnly ); + } else { + // setcookie() fails on PHP 5.1 if you give it future-compat paramters. + // stab stab! + setcookie( $wgCookiePrefix . $name, + $value, + $expire, + $wgCookiePath, + $wgCookieDomain, + $wgCookieSecure ); + } } } diff --git a/includes/WebStart.php b/includes/WebStart.php index 411c211c..edc58cb3 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -4,16 +4,6 @@ # starts the profiler and loads the configuration, and optionally loads # Setup.php depending on whether MW_NO_SETUP is defined. -# Test for PHP bug which breaks PHP 5.0.x on 64-bit... -# As of 1.8 this breaks lots of common operations instead -# of just some rare ones like export. -$borked = str_replace( 'a', 'b', array( -1 => -1 ) ); -if( !isset( $borked[-1] ) ) { - echo "PHP 5.0.x is buggy on your 64-bit system; you must upgrade to PHP 5.1.x\n" . - "or higher. ABORTING. (http://bugs.php.net/bug.php?id=34879 for details)\n"; - die( -1 ); -} - # Protect against register_globals # This must be done before any globals are set by the code if ( ini_get( 'register_globals' ) ) { @@ -74,6 +64,7 @@ if ( $IP === false ) { $IP = realpath( '.' ); } + # Start profiler require_once( "$IP/StartProfiler.php" ); wfProfileIn( 'WebStart.php-conf' ); @@ -81,20 +72,46 @@ wfProfileIn( 'WebStart.php-conf' ); # Load up some global defines. require_once( "$IP/includes/Defines.php" ); -# LocalSettings.php is the per site customization file. If it does not exit -# the wiki installer need to be launched or the generated file moved from -# ./config/ to ./ -if( !file_exists( "$IP/LocalSettings.php" ) ) { - require_once( "$IP/includes/DefaultSettings.php" ); # used for printing the version - require_once( "$IP/includes/templates/NoLocalSettings.php" ); - die(); +# Check for PHP 5 +if ( !function_exists( 'version_compare' ) + || version_compare( phpversion(), '5.0.0' ) < 0 +) { + define( 'MW_PHP4', '1' ); + require( "$IP/includes/DefaultSettings.php" ); + require( "$IP/includes/templates/PHP4.php" ); + exit; +} + +# Test for PHP bug which breaks PHP 5.0.x on 64-bit... +# As of 1.8 this breaks lots of common operations instead +# of just some rare ones like export. +$borked = str_replace( 'a', 'b', array( -1 => -1 ) ); +if( !isset( $borked[-1] ) ) { + echo "PHP 5.0.x is buggy on your 64-bit system; you must upgrade to PHP 5.1.x\n" . + "or higher. ABORTING. (http://bugs.php.net/bug.php?id=34879 for details)\n"; + exit; } # Start the autoloader, so that extensions can derive classes from core files require_once( "$IP/includes/AutoLoader.php" ); -# Include site settings. $IP may be changed (hopefully before the AutoLoader is invoked) -require_once( "$IP/LocalSettings.php" ); +if ( defined( 'MW_CONFIG_CALLBACK' ) ) { + # Use a callback function to configure MediaWiki + require_once( "$IP/includes/DefaultSettings.php" ); + call_user_func( MW_CONFIG_CALLBACK ); +} else { + # LocalSettings.php is the per site customization file. If it does not exit + # the wiki installer need to be launched or the generated file moved from + # ./config/ to ./ + if( !file_exists( "$IP/LocalSettings.php" ) ) { + require_once( "$IP/includes/DefaultSettings.php" ); # used for printing the version + require_once( "$IP/includes/templates/NoLocalSettings.php" ); + die(); + } + + # Include site settings. $IP may be changed (hopefully before the AutoLoader is invoked) + require_once( "$IP/LocalSettings.php" ); +} wfProfileOut( 'WebStart.php-conf' ); wfProfileIn( 'WebStart.php-ob_start' ); diff --git a/includes/Wiki.php b/includes/Wiki.php index fa49290a..ce4ce67e 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -42,6 +42,7 @@ class MediaWiki { /** * Initialization of ... everything * Performs the request too + * FIXME: why is this crap called "initialize" when it performs everything? * * @param $title Title ($wgTitle) * @param $article Article @@ -51,8 +52,11 @@ class MediaWiki { */ function initialize( &$title, &$article, &$output, &$user, $request ) { wfProfileIn( __METHOD__ ); - $this->preliminaryChecks( $title, $output, $request ) ; - if ( !$this->initializeSpecialCases( $title, $output, $request ) ) { + if( !$this->preliminaryChecks( $title, $output, $request ) ) { + wfProfileOut( __METHOD__ ); + return; + } + if( !$this->initializeSpecialCases( $title, $output, $request ) ) { $new_article = $this->initializeArticle( $title, $request ); if( is_object( $new_article ) ) { $article = $new_article; @@ -60,6 +64,7 @@ class MediaWiki { } elseif( is_string( $new_article ) ) { $output->redirect( $new_article ); } else { + wfProfileOut( __METHOD__ ); throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle() returned neither an object nor a URL" ); } } @@ -76,7 +81,7 @@ class MediaWiki { */ function checkMaxLag( $maxLag ) { list( $host, $lag ) = wfGetLB()->getMaxLag(); - if ( $lag > $maxLag ) { + if( $lag > $maxLag ) { wfMaxlagError( $host, $lag, $maxLag ); return false; } else { @@ -84,7 +89,6 @@ class MediaWiki { } } - /** * Checks some initial queries * Note that $title here is *not* a Title object, but a string! @@ -95,26 +99,23 @@ class MediaWiki { */ function checkInitialQueries( $title, $action ) { global $wgOut, $wgRequest, $wgContLang; - if( $wgRequest->getVal( 'printable' ) == 'yes' ){ + if( $wgRequest->getVal( 'printable' ) === 'yes' ) { $wgOut->setPrintable(); } - $ret = NULL; - - if ( '' == $title && 'delete' != $action ) { - $ret = Title::newMainPage(); - } elseif ( $curid = $wgRequest->getInt( 'curid' ) ) { + if( $curid = $wgRequest->getInt( 'curid' ) ) { # URLs like this are generated by RC, because rc_title isn't always accurate $ret = Title::newFromID( $curid ); + } elseif( '' == $title && 'delete' != $action ) { + $ret = Title::newMainPage(); } else { $ret = Title::newFromURL( $title ); // check variant links so that interwiki links don't have to worry // about the possible different language variants if( count( $wgContLang->getVariants() ) > 1 && !is_null( $ret ) && $ret->getArticleID() == 0 ) $wgContLang->findVariantLink( $title, $ret ); - } - if ( ( $oldid = $wgRequest->getInt( 'oldid' ) ) + if( ( $oldid = $wgRequest->getInt( 'oldid' ) ) && ( is_null( $ret ) || $ret->getNamespace() != NS_SPECIAL ) ) { // Allow oldid to override a changed or missing title. $rev = Revision::newFromId( $oldid ); @@ -133,7 +134,6 @@ class MediaWiki { * @param $request WebRequest */ function preliminaryChecks( &$title, &$output, $request ) { - if( $request->getCheck( 'search' ) ) { // Compatibility with old search URLs which didn't use Special:Search // Just check for presence here, so blank requests still @@ -142,16 +142,16 @@ class MediaWiki { // Do this above the read whitelist check for security... $title = SpecialPage::getTitleFor( 'Search' ); } - # If the user is not logged in, the Namespace:title of the article must be in # the Read array in order for the user to see it. (We have to check here to # catch special pages etc. We check again in Article::view()) - if ( !is_null( $title ) && !$title->userCanRead() ) { + if( !is_null( $title ) && !$title->userCanRead() ) { $output->loginToUse(); $output->output(); - exit; + $output->disable(); + return false; } - + return true; } /** @@ -161,6 +161,8 @@ class MediaWiki { * - redirect loop * - special pages * + * FIXME: why is this crap called "initialize" when it performs everything? + * * @param $title Title * @param $output OutputPage * @param $request WebRequest @@ -170,25 +172,25 @@ class MediaWiki { wfProfileIn( __METHOD__ ); $action = $this->getVal( 'Action' ); - if( !$title || $title->getDBkey() == '' ) { + if( is_null($title) || $title->getDBkey() == '' ) { $title = SpecialPage::getTitleFor( 'Badtitle' ); # Die now before we mess up $wgArticle and the skin stops working throw new ErrorPageError( 'badtitle', 'badtitletext' ); - } else if ( $title->getInterwiki() != '' ) { + } else if( $title->getInterwiki() != '' ) { if( $rdfrom = $request->getVal( 'rdfrom' ) ) { $url = $title->getFullURL( 'rdfrom=' . urlencode( $rdfrom ) ); } else { $url = $title->getFullURL(); } /* Check for a redirect loop */ - if ( !preg_match( '/^' . preg_quote( $this->getVal('Server'), '/' ) . '/', $url ) && $title->isLocal() ) { + if( !preg_match( '/^' . preg_quote( $this->getVal('Server'), '/' ) . '/', $url ) && $title->isLocal() ) { $output->redirect( $url ); } else { $title = SpecialPage::getTitleFor( 'Badtitle' ); throw new ErrorPageError( 'badtitle', 'badtitletext' ); } - } else if ( ( $action == 'view' ) && !$request->wasPosted() && - (!isset( $this->GET['title'] ) || $title->getPrefixedDBKey() != $this->GET['title'] ) && + } else if( $action == 'view' && !$request->wasPosted() && + ( !isset($this->GET['title']) || $title->getPrefixedDBKey() != $this->GET['title'] ) && !count( array_diff( array_keys( $this->GET ), array( 'action', 'title' ) ) ) ) { $targetUrl = $title->getFullURL(); @@ -219,7 +221,7 @@ class MediaWiki { $output->setSquidMaxage( 1200 ); $output->redirect( $targetUrl, '301' ); } - } else if ( NS_SPECIAL == $title->getNamespace() ) { + } else if( NS_SPECIAL == $title->getNamespace() ) { /* actions that need to be made when we have a special pages */ SpecialPage::executePath( $title ); } else { @@ -241,7 +243,7 @@ class MediaWiki { static function articleFromTitle( &$title ) { if( NS_MEDIA == $title->getNamespace() ) { // FIXME: where should this go? - $title = Title::makeTitle( NS_IMAGE, $title->getDBkey() ); + $title = Title::makeTitle( NS_FILE, $title->getDBkey() ); } $article = null; @@ -251,12 +253,12 @@ class MediaWiki { } switch( $title->getNamespace() ) { - case NS_IMAGE: - return new ImagePage( $title ); - case NS_CATEGORY: - return new CategoryPage( $title ); - default: - return new Article( $title ); + case NS_FILE: + return new ImagePage( $title ); + case NS_CATEGORY: + return new CategoryPage( $title ); + default: + return new Article( $title ); } } @@ -271,27 +273,32 @@ class MediaWiki { function initializeArticle( &$title, $request ) { wfProfileIn( __METHOD__ ); - $action = $this->getVal( 'action' ); + $action = $this->getVal( 'action', 'view' ); $article = self::articleFromTitle( $title ); - - wfDebug("Article: ".$title->getPrefixedText()."\n"); - + # NS_MEDIAWIKI has no redirects. + # It is also used for CSS/JS, so performance matters here... + if( $title->getNamespace() == NS_MEDIAWIKI ) { + wfProfileOut( __METHOD__ ); + return $article; + } // Namespace might change when using redirects // Check for redirects ... - $file = $title->getNamespace() == NS_IMAGE ? $article->getFile() : null; + $file = ($title->getNamespace() == NS_FILE) ? $article->getFile() : null; if( ( $action == 'view' || $action == 'render' ) // ... for actions that show content - && !$request->getVal( 'oldid' ) && // ... and are not old revisions - $request->getVal( 'redirect' ) != 'no' && // ... unless explicitly told not to - // ... and the article is not a non-redirect image page with associated file - !( is_object( $file ) && $file->exists() && !$file->getRedirected() ) ) { - + && !$request->getVal( 'oldid' ) && // ... and are not old revisions + $request->getVal( 'redirect' ) != 'no' && // ... unless explicitly told not to + // ... and the article is not a non-redirect image page with associated file + !( is_object( $file ) && $file->exists() && !$file->getRedirected() ) ) + { # Give extensions a change to ignore/handle redirects as needed $ignoreRedirect = $target = false; - wfRunHooks( 'InitializeArticleMaybeRedirect', array( &$title, &$request, &$ignoreRedirect, &$target ) ); - + $dbr = wfGetDB( DB_SLAVE ); $article->loadPageData( $article->pageDataFromTitle( $dbr, $title ) ); + wfRunHooks( 'InitializeArticleMaybeRedirect', + array(&$title,&$request,&$ignoreRedirect,&$target,&$article) ); + // Follow redirects only for... redirects if( !$ignoreRedirect && $article->isRedirect() ) { # Is the target already set by an extension? @@ -302,12 +309,11 @@ class MediaWiki { return $target; } } - - if( is_object( $target ) ) { + if( is_object($target) ) { // Rewrite environment to redirected article $rarticle = self::articleFromTitle( $target ); $rarticle->loadPageData( $rarticle->pageDataFromTitle( $dbr, $target ) ); - if ( $rarticle->exists() || ( is_object( $file ) && !$file->isLocal() ) ) { + if( $rarticle->exists() || ( is_object( $file ) && !$file->isLocal() ) ) { $rarticle->setRedirectedFrom( $title ); $article = $rarticle; $title = $target; @@ -327,14 +333,18 @@ class MediaWiki { * @param $deferredUpdates array of updates to do * @param $output OutputPage */ - function finalCleanup ( &$deferredUpdates, &$output ) { + function finalCleanup( &$deferredUpdates, &$output ) { wfProfileIn( __METHOD__ ); - $this->doUpdates( $deferredUpdates ); - $this->doJobs(); # Now commit any transactions, so that unreported errors after output() don't roll back the whole thing $factory = wfGetLBFactory(); - $factory->shutdown(); + $factory->commitMasterChanges(); + # Output everything! $output->output(); + # Do any deferred jobs + $this->doUpdates( $deferredUpdates ); + $this->doJobs(); + # Commit and close up! + $factory->shutdown(); wfProfileOut( __METHOD__ ); } @@ -359,7 +369,7 @@ class MediaWiki { $up->doUpdate(); # Commit after every update to prevent lock contention - if ( $dbw->trxLevel() ) { + if( $dbw->trxLevel() ) { $dbw->commit(); } } @@ -372,12 +382,12 @@ class MediaWiki { function doJobs() { $jobRunRate = $this->getVal( 'JobRunRate' ); - if ( $jobRunRate <= 0 || wfReadOnly() ) { + if( $jobRunRate <= 0 || wfReadOnly() ) { return; } - if ( $jobRunRate < 1 ) { + if( $jobRunRate < 1 ) { $max = mt_getrandmax(); - if ( mt_rand( 0, $max ) > $max * $jobRunRate ) { + if( mt_rand( 0, $max ) > $max * $jobRunRate ) { return; } $n = 1; @@ -391,7 +401,7 @@ class MediaWiki { $success = $job->run(); $t += wfTime(); $t = round( $t*1000 ); - if ( !$success ) { + if( !$success ) { $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n"; } else { $output .= "Success, Time: $t ms\n"; @@ -420,7 +430,7 @@ class MediaWiki { function performAction( &$output, &$article, &$title, &$user, &$request ) { wfProfileIn( __METHOD__ ); - if ( !wfRunHooks( 'MediaWikiPerformAction', array( $output, $article, $title, $user, $request, $this ) ) ) { + if( !wfRunHooks( 'MediaWikiPerformAction', array( $output, $article, $title, $user, $request, $this ) ) ) { wfProfileOut( __METHOD__ ); return; } @@ -436,6 +446,10 @@ class MediaWiki { $output->setSquidMaxage( $this->getVal( 'SquidMaxage' ) ); $article->view(); break; + case 'raw': // includes JS/CSS + $raw = new RawPage( $article ); + $raw->view(); + break; case 'watch': case 'unwatch': case 'delete': @@ -457,21 +471,20 @@ class MediaWiki { if( !$this->getVal( 'EnableDublinCoreRdf' ) ) { wfHttpError( 403, 'Forbidden', wfMsg( 'nodublincore' ) ); } else { - require_once( 'includes/Metadata.php' ); - wfDublinCoreRdf( $article ); + $rdf = new DublinCoreRdf( $article ); + $rdf->show(); } break; case 'creativecommons': if( !$this->getVal( 'EnableCreativeCommonsRdf' ) ) { wfHttpError( 403, 'Forbidden', wfMsg( 'nocreativecommons' ) ); } else { - require_once( 'includes/Metadata.php' ); - wfCreativeCommonsRdf( $article ); + $rdf = new CreativeCommonsRdf( $article ); + $rdf->show(); } break; case 'credits': - require_once( 'includes/Credits.php' ); - showCreditsPage( $article ); + Credits::showPage( $article ); break; case 'submit': if( session_id() == '' ) { @@ -504,10 +517,6 @@ class MediaWiki { $history = new PageHistory( $article ); $history->history(); break; - case 'raw': - $raw = new RawPage( $article ); - $raw->view(); - break; default: if( wfRunHooks( 'UnknownAction', array( $action, $article ) ) ) { $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); diff --git a/includes/WikiError.php b/includes/WikiError.php index c5082004..41edb2f3 100644 --- a/includes/WikiError.php +++ b/includes/WikiError.php @@ -79,7 +79,8 @@ class WikiErrorMsg extends WikiError { } /** - * @todo document + * Error class designed to handle errors involved with + * XML parsing * @ingroup Exception */ class WikiXmlError extends WikiError { diff --git a/includes/Xml.php b/includes/Xml.php index 32a68251..68990d86 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -112,11 +112,11 @@ class Xml { * * @param $selected Mixed: Namespace which should be pre-selected * @param $all Mixed: Value of an item denoting all namespaces, or null to omit - * @param $hidden Mixed: Include hidden namespaces? [WTF? --RC] * @param $element_name String: value of the "name" attribute of the select tag + * @param $label String: optional label to add to the field * @return string */ - public static function namespaceSelector( $selected = '', $all = null, $hidden = false, $element_name = 'namespace' ) { + public static function namespaceSelector( $selected = '', $all = null, $element_name = 'namespace', $label = null ) { global $wgContLang; $namespaces = $wgContLang->getFormattedNamespaces(); $options = array(); @@ -139,12 +139,16 @@ class Xml { $options[] = self::option( $name, $index, $index === $selected ); } - return Xml::openElement( 'select', array( 'id' => 'namespace', 'name' => $element_name, + $ret = Xml::openElement( 'select', array( 'id' => 'namespace', 'name' => $element_name, 'class' => 'namespaceselector' ) ) . "\n" . implode( "\n", $options ) . "\n" . Xml::closeElement( 'select' ); + if ( !is_null( $label ) ) { + $ret = Xml::label( $label, $element_name ) . ' ' . $ret; + } + return $ret; } /** @@ -640,18 +644,63 @@ class Xml { $form .= Xml::openElement( 'tr', array( 'id' => $id ) ); $form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMsgExt( $labelmsg, array('parseinline') ) ); - $form .= Xml::openElement( 'td' ) . $input . Xml::closeElement( 'td' ); + $form .= Xml::openElement( 'td', array( 'class' => 'mw-input' ) ) . $input . Xml::closeElement( 'td' ); + $form .= Xml::closeElement( 'tr' ); + } + + if( $submitLabel ) { + $form .= Xml::openElement( 'tr', array( 'id' => $id ) ); + $form .= Xml::tags( 'td', array(), '' ); + $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMsg( $submitLabel ) ) . Xml::closeElement( 'td' ); $form .= Xml::closeElement( 'tr' ); } $form .= "</tbody></table>"; - - if ($submitLabel) { - $form .= Xml::submitButton( wfMsg($submitLabel) ); - } + return $form; } + + /** + * Build a table of data + * @param array $rows An array of arrays of strings, each to be a row in a table + * @param array $attribs Attributes to apply to the table tag [optional] + * @param array $headers An array of strings to use as table headers [optional] + * @return string + */ + public static function buildTable( $rows, $attribs = array(), $headers = null ) { + $s = Xml::openElement( 'table', $attribs ); + if ( is_array( $headers ) ) { + foreach( $headers as $id => $header ) { + $attribs = array(); + if ( is_string( $id ) ) $attribs['id'] = $id; + $s .= Xml::element( 'th', $attribs, $header ); + } + } + foreach( $rows as $id => $row ) { + $attribs = array(); + if ( is_string( $id ) ) $attribs['id'] = $id; + $s .= Xml::buildTableRow( $attribs, $row ); + } + $s .= Xml::closeElement( 'table' ); + return $s; + } + + /** + * Build a row for a table + * @param array $cells An array of strings to put in <td> + * @return string + */ + public static function buildTableRow( $attribs, $cells ) { + $s = Xml::openElement( 'tr', $attribs ); + foreach( $cells as $id => $cell ) { + $attribs = array(); + if ( is_string( $id ) ) $attribs['id'] = $id; + $s .= Xml::element( 'td', $attribs, $cell ); + } + $s .= Xml::closeElement( 'tr' ); + return $s; + } } class XmlSelect { @@ -674,7 +723,8 @@ class XmlSelect { } public function addOption( $name, $value = false ) { - $value = $value ? $value : $name; + // Stab stab stab + $value = ($value !== false) ? $value : $name; $this->options[] = Xml::option( $name, $value, $value === $this->default ); } @@ -682,4 +732,4 @@ class XmlSelect { return Xml::tags( 'select', $this->attributes, implode( "\n", $this->options ) ); } -}
\ No newline at end of file +} diff --git a/includes/XmlFunctions.php b/includes/XmlFunctions.php index bc18a2cd..8cb8f3f5 100644 --- a/includes/XmlFunctions.php +++ b/includes/XmlFunctions.php @@ -4,63 +4,83 @@ * Look at the Xml class (Xml.php) for the implementations. */ function wfElement( $element, $attribs = null, $contents = '') { + wfDeprecated(__FUNCTION__); return Xml::element( $element, $attribs, $contents ); } function wfElementClean( $element, $attribs = array(), $contents = '') { + wfDeprecated(__FUNCTION__); return Xml::elementClean( $element, $attribs, $contents ); } function wfOpenElement( $element, $attribs = null ) { + wfDeprecated(__FUNCTION__); return Xml::openElement( $element, $attribs ); } function wfCloseElement( $element ) { + wfDeprecated(__FUNCTION__); return "</$element>"; } -function HTMLnamespaceselector($selected = '', $allnamespaces = null, $includehidden=false) { - return Xml::namespaceSelector( $selected, $allnamespaces, $includehidden ); +function HTMLnamespaceselector($selected = '', $allnamespaces = null ) { + wfDeprecated(__FUNCTION__); + return Xml::namespaceSelector( $selected, $allnamespaces ); } function wfSpan( $text, $class, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::span( $text, $class, $attribs ); } function wfInput( $name, $size=false, $value=false, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::input( $name, $size, $value, $attribs ); } function wfAttrib( $name, $present = true ) { + wfDeprecated(__FUNCTION__); return Xml::attrib( $name, $present ); } function wfCheck( $name, $checked=false, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::check( $name, $checked, $attribs ); } function wfRadio( $name, $value, $checked=false, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::radio( $name, $value, $checked, $attribs ); } function wfLabel( $label, $id ) { + wfDeprecated(__FUNCTION__); return Xml::label( $label, $id ); } function wfInputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::inputLabel( $label, $name, $id, $size, $value, $attribs ); } function wfCheckLabel( $label, $name, $id, $checked=false, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::checkLabel( $label, $name, $id, $checked, $attribs ); } function wfRadioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::radioLabel( $label, $name, $value, $id, $checked, $attribs ); } function wfSubmitButton( $value, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::submitButton( $value, $attribs ); } function wfHidden( $name, $value, $attribs=array() ) { + wfDeprecated(__FUNCTION__); return Xml::hidden( $name, $value, $attribs ); } function wfEscapeJsString( $string ) { + wfDeprecated(__FUNCTION__); return Xml::escapeJsString( $string ); } function wfIsWellFormedXml( $text ) { + wfDeprecated(__FUNCTION__); return Xml::isWellFormed( $text ); } function wfIsWellFormedXmlFragment( $text ) { + wfDeprecated(__FUNCTION__); return Xml::isWellFormedXmlFragment( $text ); } function wfBuildForm( $fields, $submitLabel ) { + wfDeprecated(__FUNCTION__); return Xml::buildForm( $fields, $submitLabel ); } diff --git a/includes/XmlTypeCheck.php b/includes/XmlTypeCheck.php index 8ee211e1..a004ef4d 100644 --- a/includes/XmlTypeCheck.php +++ b/includes/XmlTypeCheck.php @@ -31,6 +31,13 @@ class XmlTypeCheck { $this->filterCallback = $filterCallback; $this->run( $file ); } + + /** + * Get the root element. Simple accessor to $rootElement + */ + public function getRootElement() { + return $this->rootElement; + } private function run( $fname ) { $parser = xml_parser_create_ns( 'UTF-8' ); diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php index 1cae8463..4c1e0ae8 100644 --- a/includes/ZhConversion.php +++ b/includes/ZhConversion.php @@ -2574,6 +2574,10 @@ $zh2Hant = array( "三只" => "三隻", "三余" => "三餘", "上梁" => "上樑", +"上签名" => "上簽名", +"上签字" => "上簽字", +"上签写" => "上簽寫", +"上签收" => "上簽收", "上签" => "上籤", "上药" => "上藥", "下于" => "下於", @@ -3482,6 +3486,7 @@ $zh2Hant = array( "外强中干" => "外強中乾", "外制" => "外製", "多划" => "多劃", +"多只是" => "多只是", "多天后" => "多天後", "多于" => "多於", "多冲" => "多衝", @@ -4776,7 +4781,6 @@ $zh2Hant = array( "水准" => "水準", "水里" => "水裡", "水里乡" => "水里鄉", -"水表" => "水錶", "水硷" => "水鹼", "永历" => "永曆", "求助于" => "求助於", @@ -6228,7 +6232,6 @@ $zh2Hant = array( "退烧药" => "退燒藥", "逋发" => "逋髮", "透辟" => "透闢", -"这么着" => "這么著", "这里" => "這裏", "这里" => "這裡", "这只" => "這隻", @@ -6922,7 +6925,6 @@ $zh2Hant = array( "斗斗" => "鬥鬥", "斗鱼" => "鬥魚", "斗鹌鹑" => "鬥鵪鶉", -"闹着玩儿" => "鬧著玩儿", "闹着玩儿" => "鬧著玩兒", "闹钟" => "鬧鐘", "哄动" => "鬨動", @@ -9910,22 +9912,22 @@ $zh2Hans = array( "乘著" => "乘着", "書畫" => "书画", "乾乾" => "乾乾", -"乾元;" => "乾元;", -"乾卦;" => "乾卦;", +"乾元" => "乾元", +"乾卦" => "乾卦", "乾縣" => "乾县", "乾嘉" => "乾嘉", "乾圖" => "乾图", "乾坤 " => "乾坤 ", -"乾宅;" => "乾宅;", +"乾宅" => "乾宅", "乾斷" => "乾断", "乾旦" => "乾旦", -"乾曜;" => "乾曜;", +"乾曜" => "乾曜", "乾清宮" => "乾清宫", "乾盛世" => "乾盛世", "乾紅" => "乾红", "乾綱" => "乾纲", -"乾象;" => "乾象;", -"乾造;" => "乾造;", +"乾象" => "乾象", +"乾造" => "乾造", "乾陵" => "乾陵", "乾隆" => "乾隆", "爭著" => "争着", @@ -10220,8 +10222,12 @@ $zh2Hans = array( "本著名" => "本著名", "本著者" => "本著者", "機械畫" => "机械画", +"殺著" => "杀着", +"殺著作" => "杀著作", +"殺著名" => "杀著名", +"殺著者" => "杀著者", "雜著" => "杂着", -"李乾德;" => "李乾德;", +"李乾德" => "李乾德", "來著" => "来着", "板著臉" => "板着脸", "枕著" => "枕着", @@ -10631,6 +10637,7 @@ $zh2TW = array( "復蘇" => "復甦", "缺省" => "預設", "串行" => "串列", +"串列加速器" => "串列加速器", "以太网" => "乙太網", "位图" => "點陣圖", "例程" => "常式", @@ -10695,7 +10702,6 @@ $zh2TW = array( "服务器" => "伺服器", "等于" => "等於", "局域网" => "區域網", -"计算机" => "電腦", "扫瞄仪" => "掃瞄器", "宽带" => "寬頻", "数据库" => "資料庫", @@ -10753,7 +10759,6 @@ $zh2TW = array( "伯利兹" => "貝里斯", "伯利茲" => "貝里斯", "佛得角" => "維德角", -"佛得角" => "維德角", "克罗地亚" => "克羅埃西亞", "克羅地亞" => "克羅埃西亞", "冈比亚" => "甘比亞", @@ -10811,10 +10816,11 @@ $zh2TW = array( "塞浦路斯" => "塞普勒斯", "塞舌尔" => "塞席爾", "塞舌爾" => "塞席爾", -"多米尼加" => "多明尼加", +"多米尼加共和国" => "多明尼加", +"多米尼加共和國" => "多明尼加", "多明尼加共和國" => "多明尼加", -"多米尼加联邦" => "多米尼克", -"多明尼加聯邦" => "多米尼克", +"多米尼加国" => "多米尼克", +"多明尼加國" => "多米尼克", "安提瓜和巴布达" => "安地卡及巴布達", "安提瓜和巴布達" => "安地卡及巴布達", "尼日利亚" => "奈及利亞", @@ -10822,17 +10828,14 @@ $zh2TW = array( "尼日尔" => "尼日", "尼日爾" => "尼日", "巴巴多斯" => "巴貝多", -"巴巴多斯" => "巴貝多", "巴布亚新几内亚" => "巴布亞紐幾內亞", "巴布亞新畿內亞" => "巴布亞紐幾內亞", "布基纳法索" => "布吉納法索", "布基納法索" => "布吉納法索", "布隆迪" => "蒲隆地", "布隆迪" => "蒲隆地", -"希腊" => "希臘", "帕劳" => "帛琉", "意大利" => "義大利", -"意大利" => "義大利", "所罗门群岛" => "索羅門群島", "所羅門群島" => "索羅門群島", "文莱" => "汶萊", @@ -10886,7 +10889,6 @@ $zh2TW = array( "赞比亚" => "尚比亞", "贊比亞" => "尚比亞", "阿塞拜疆" => "亞塞拜然", -"阿塞拜疆" => "亞塞拜然", "阿拉伯联合酋长国" => "阿拉伯聯合大公國", "阿拉伯聯合酋長國" => "阿拉伯聯合大公國", "马尔代夫" => "馬爾地夫", @@ -10917,7 +10919,6 @@ $zh2TW = array( "積架" => "捷豹", "福士" => "福斯", "雪铁龙" => "雪鐵龍", -"马自达" => "馬自達", "萬事得" => "馬自達", "拿破仑" => "拿破崙", "拿破侖" => "拿破崙", @@ -10941,25 +10942,17 @@ $zh2HK = array( "凶殘" => "兇殘", "緝凶" => "緝兇", "買凶" => "買兇", -"打印机" => "打印機", "印表機" => "打印機", "字节" => "位元組", "字節" => "位元組", -"打印" => "打印", "列印" => "打印", "硬件" => "硬件", "硬體" => "硬件", -"二极管" => "二極管", "二極體" => "二極管", -"三极管" => "三極管", "三極體" => "三極管", -"数码" => "數碼", "數位" => "數碼", -"软件" => "軟件", "軟體" => "軟件", -"网络" => "網絡", "網路" => "網絡", -"人工智能" => "人工智能", "人工智慧" => "人工智能", "航天飞机" => "穿梭機", "太空梭" => "穿梭機", @@ -10969,141 +10962,85 @@ $zh2HK = array( "機器人" => "機械人", "移动电话" => "流動電話", "行動電話" => "流動電話", -"调制解调器" => "調制解調器", "數據機" => "調制解調器", "短信" => "短訊", "簡訊" => "短訊", -"乍得" => "乍得", "查德" => "乍得", -"也门" => "也門", "葉門" => "也門", -"伯利兹" => "伯利茲", "貝里斯" => "伯利茲", -"佛得角" => "佛得角", "維德角" => "佛得角", -"克罗地亚" => "克羅地亞", "克羅埃西亞" => "克羅地亞", -"冈比亚" => "岡比亞", "甘比亞" => "岡比亞", -"几内亚比绍" => "幾內亞比紹", "幾內亞比索" => "幾內亞比紹", -"列支敦士登" => "列支敦士登", "列支敦斯登" => "列支敦士登", -"利比里亚" => "利比里亞", "賴比瑞亞" => "利比里亞", -"加纳" => "加納", "迦納" => "加納", -"加蓬" => "加蓬", "加彭" => "加蓬", -"博茨瓦纳" => "博茨瓦納", "波札那" => "博茨瓦納", -"卡塔尔" => "卡塔爾", "卡達" => "卡塔爾", -"卢旺达" => "盧旺達", "盧安達" => "盧旺達", -"危地马拉" => "危地馬拉", "瓜地馬拉" => "危地馬拉", "厄瓜多尔" => "厄瓜多爾", +"厄瓜多爾" => "厄瓜多爾", "厄瓜多" => "厄瓜多爾", -"厄立特里亚" => "厄立特里亞", "厄利垂亞" => "厄立特里亞", -"吉布提" => "吉布堤", "吉布地" => "吉布堤", -"哥斯达黎加" => "哥斯達黎加", "哥斯大黎加" => "哥斯達黎加", -"图瓦卢" => "圖瓦盧", "吐瓦魯" => "圖瓦盧", -"圣卢西亚" => "聖盧西亞", "聖露西亞" => "聖盧西亞", "圣基茨和尼维斯" => "聖吉斯納域斯", "聖克里斯多福及尼維斯" => "聖吉斯納域斯", -"圣文森特和格林纳丁斯" => "聖文森特和格林納丁斯", "聖文森及格瑞那丁" => "聖文森特和格林納丁斯", -"圣马力诺" => "聖馬力諾", "聖馬利諾" => "聖馬力諾", -"圭亚那" => "圭亞那", "蓋亞那" => "圭亞那", -"坦桑尼亚" => "坦桑尼亞", "坦尚尼亞" => "坦桑尼亞", -"埃塞俄比亚" => "埃塞俄比亞", "衣索匹亞" => "埃塞俄比亞", "衣索比亞" => "埃塞俄比亞", -"基里巴斯" => "基里巴斯", "吉里巴斯" => "基里巴斯", -"狮子山" => "獅子山", "塞普勒斯" => "塞浦路斯", -"塞舌尔" => "塞舌爾", "塞席爾" => "塞舌爾", -"多米尼加" => "多明尼加共和國", -"多明尼加" => "多明尼加共和國", -"多米尼加联邦" => "多明尼加聯邦", -"多米尼克" => "多明尼加聯邦", -"安提瓜和巴布达" => "安提瓜和巴布達", +"多米尼克" => "多明尼加國", "安地卡及巴布達" => "安提瓜和巴布達", "尼日利亚" => "尼日利亞", +"尼日利亞" => "尼日利亞", "奈及利亞" => "尼日利亞", "尼日尔" => "尼日爾", +"尼日爾" => "尼日爾", "尼日" => "尼日爾", -"巴巴多斯" => "巴巴多斯", "巴貝多" => "巴巴多斯", -"巴布亚新几内亚" => "巴布亞新畿內亞", "巴布亞紐幾內亞" => "巴布亞新畿內亞", -"布基纳法索" => "布基納法索", "布吉納法索" => "布基納法索", -"布隆迪" => "布隆迪", "蒲隆地" => "布隆迪", +"帕劳" => "帛琉", "義大利" => "意大利", -"所罗门群岛" => "所羅門群島", "索羅門群島" => "所羅門群島", -"斯威士兰" => "斯威士蘭", +"文莱" => "汶萊", "史瓦濟蘭" => "斯威士蘭", -"斯洛文尼亚" => "斯洛文尼亞", "斯洛維尼亞" => "斯洛文尼亞", -"新西兰" => "新西蘭", "紐西蘭" => "新西蘭", -"格林纳达" => "格林納達", "格瑞那達" => "格林納達", -"格鲁吉亚" => "喬治亞", -"格魯吉亞" => "喬治亞", -"梵蒂冈" => "梵蒂岡", -"毛里塔尼亚" => "毛里塔尼亞", "茅利塔尼亞" => "毛里塔尼亞", "毛里求斯" => "毛里裘斯", "模里西斯" => "毛里裘斯", +"沙地阿拉伯" => "沙特阿拉伯", "沙烏地阿拉伯" => "沙特阿拉伯", -"波斯尼亚和黑塞哥维那" => "波斯尼亞黑塞哥維那", "波士尼亞赫塞哥維納" => "波斯尼亞黑塞哥維那", -"津巴布韦" => "津巴布韋", "辛巴威" => "津巴布韋", -"洪都拉斯" => "洪都拉斯", "宏都拉斯" => "洪都拉斯", -"特立尼达和托巴哥" => "特立尼達和多巴哥", "千里達托貝哥" => "特立尼達和多巴哥", -"瑙鲁" => "瑙魯", "諾魯" => "瑙魯", -"瓦努阿图" => "瓦努阿圖", "萬那杜" => "瓦努阿圖", -"科摩罗" => "科摩羅", "葛摩" => "科摩羅", -"索马里" => "索馬里", "索馬利亞" => "索馬里", -"老挝" => "老撾", "寮國" => "老撾", "肯尼亚" => "肯雅", "肯亞" => "肯雅", -"莫桑比克" => "莫桑比克", "莫三比克" => "莫桑比克", -"莱索托" => "萊索托", "賴索托" => "萊索托", -"贝宁" => "貝寧", "貝南" => "貝寧", -"赞比亚" => "贊比亞", "尚比亞" => "贊比亞", -"阿塞拜疆" => "阿塞拜疆", "亞塞拜然" => "阿塞拜疆", -"阿拉伯联合酋长国" => "阿拉伯聯合酋長國", "阿拉伯聯合大公國" => "阿拉伯聯合酋長國", -"马尔代夫" => "馬爾代夫", "馬爾地夫" => "馬爾代夫", "馬利共和國" => "馬里共和國", "方便面" => "即食麵", @@ -11135,11 +11072,9 @@ $zh2HK = array( "拿破崙" => "拿破侖", "布什" => "布殊", "布希" => "布殊", -"克林顿" => "克林頓", "柯林頓" => "克林頓", "萨达姆" => "薩達姆", "海珊" => "侯賽因", -"侯赛因" => "侯賽因", "大卫·贝克汉姆" => "大衛碧咸", "迈克尔·欧文" => "米高奧雲", "珍妮弗·卡普里亚蒂" => "卡佩雅蒂", @@ -11158,6 +11093,7 @@ $zh2CN = array( "記憶體" => "内存", "預設" => "默认", "串列" => "串行", +"串列加速器" => "串列加速器", "乙太網" => "以太网", "點陣圖" => "位图", "常式" => "例程", @@ -11262,150 +11198,92 @@ $zh2CN = array( "簡訊" => "短信", "烏茲別克" => "乌兹别克斯坦", "查德" => "乍得", -"乍得" => "乍得", -"也門" => "", "葉門" => "也门", "伯利茲" => "伯利兹", "貝里斯" => "伯利兹", "維德角" => "佛得角", -"佛得角" => "佛得角", -"克羅地亞" => "克罗地亚", "克羅埃西亞" => "克罗地亚", -"岡比亞" => "冈比亚", "甘比亞" => "冈比亚", -"幾內亞比紹" => "几内亚比绍", "幾內亞比索" => "几内亚比绍", "列支敦斯登" => "列支敦士登", -"列支敦士登" => "列支敦士登", -"利比里亞" => "利比里亚", "賴比瑞亞" => "利比里亚", -"加納" => "加纳", "迦納" => "加纳", "加彭" => "加蓬", -"加蓬" => "加蓬", -"博茨瓦納" => "博茨瓦纳", "波札那" => "博茨瓦纳", -"卡塔爾" => "卡塔尔", "卡達" => "卡塔尔", -"盧旺達" => "卢旺达", "盧安達" => "卢旺达", -"危地馬拉" => "危地马拉", "瓜地馬拉" => "危地马拉", "厄瓜多爾" => "厄瓜多尔", +"厄瓜多尔" => "厄瓜多尔", "厄瓜多" => "厄瓜多尔", -"厄立特里亞" => "厄立特里亚", "厄利垂亞" => "厄立特里亚", -"吉布堤" => "吉布提", "吉布地" => "吉布提", "哈薩克" => "哈萨克斯坦", -"哥斯達黎加" => "哥斯达黎加", "哥斯大黎加" => "哥斯达黎加", -"圖瓦盧" => "图瓦卢", "吐瓦魯" => "图瓦卢", "土庫曼" => "土库曼斯坦", -"聖盧西亞" => "圣卢西亚", "聖露西亞" => "圣卢西亚", "聖吉斯納域斯" => "圣基茨和尼维斯", "聖克里斯多福及尼維斯" => "圣基茨和尼维斯", -"聖文森特和格林納丁斯" => "圣文森特和格林纳丁斯", "聖文森及格瑞那丁" => "圣文森特和格林纳丁斯", -"聖馬力諾" => "圣马力诺", "聖馬利諾" => "圣马力诺", -"圭亞那" => "圭亚那", "蓋亞那" => "圭亚那", -"坦桑尼亞" => "坦桑尼亚", "坦尚尼亞" => "坦桑尼亚", -"埃塞俄比亞" => "埃塞俄比亚", "衣索匹亞" => "埃塞俄比亚", "衣索比亞" => "埃塞俄比亚", "吉里巴斯" => "基里巴斯", -"基里巴斯" => "基里巴斯", "塔吉克" => "塔吉克斯坦", "塞拉利昂" => "塞拉利昂", "塞普勒斯" => "塞浦路斯", -"塞浦路斯" => "塞浦路斯", -"塞舌爾" => "塞舌尔", "塞席爾" => "塞舌尔", -"多明尼加共和國" => "多米尼加", -"多明尼加" => "多米尼加", -"多明尼加聯邦" => "多米尼加联邦", -"多米尼克" => "多米尼加联邦", -"安提瓜和巴布達" => "安提瓜和巴布达", +"多米尼克" => "多米尼加国", "安地卡及巴布達" => "安提瓜和巴布达", "尼日利亞" => "尼日利亚", +"尼日利亚" => "尼日利亚", "奈及利亞" => "尼日利亚", "尼日爾" => "尼日尔", +"尼日尔" => "尼日尔", "尼日" => "尼日尔", "巴貝多" => "巴巴多斯", -"巴巴多斯" => "巴巴多斯", -"巴布亞新畿內亞" => "巴布亚新几内亚", "巴布亞紐幾內亞" => "巴布亚新几内亚", "布基納法索" => "布基纳法索", "布吉納法索" => "布基纳法索", "蒲隆地" => "布隆迪", -"布隆迪" => "布隆迪", -"希臘" => "希腊", "帛琉" => "帕劳", "義大利" => "意大利", -"意大利" => "意大利", -"所羅門群島" => "所罗门群岛", "索羅門群島" => "所罗门群岛", "汶萊" => "文莱", -"斯威士蘭" => "斯威士兰", "史瓦濟蘭" => "斯威士兰", -"斯洛文尼亞" => "斯洛文尼亚", "斯洛維尼亞" => "斯洛文尼亚", -"新西蘭" => "新西兰", "紐西蘭" => "新西兰", -"格林納達" => "格林纳达", "格瑞那達" => "格林纳达", -"格魯吉亞" => "乔治亚", -"喬治亞" => "乔治亚", -"梵蒂岡" => "梵蒂冈", -"毛里塔尼亞" => "毛里塔尼亚", "茅利塔尼亞" => "毛里塔尼亚", "毛里裘斯" => "毛里求斯", "模里西斯" => "毛里求斯", "沙地阿拉伯" => "沙特阿拉伯", "沙烏地阿拉伯" => "沙特阿拉伯", -"波斯尼亞黑塞哥維那" => "波斯尼亚和黑塞哥维那", "波士尼亞赫塞哥維納" => "波斯尼亚和黑塞哥维那", -"津巴布韋" => "津巴布韦", "辛巴威" => "津巴布韦", "宏都拉斯" => "洪都拉斯", -"洪都拉斯" => "洪都拉斯", -"特立尼達和多巴哥" => "特立尼达和托巴哥", "千里達托貝哥" => "特立尼达和托巴哥", -"瑙魯" => "瑙鲁", "諾魯" => "瑙鲁", -"瓦努阿圖" => "瓦努阿图", "萬那杜" => "瓦努阿图", "溫納圖" => "瓦努阿图", -"科摩羅" => "科摩罗", "葛摩" => "科摩罗", "象牙海岸" => "科特迪瓦", "突尼西亞" => "突尼斯", -"索馬里" => "索马里", "索馬利亞" => "索马里", -"老撾" => "老挝", "寮國" => "老挝", "肯雅" => "肯尼亚", "肯亞" => "肯尼亚", "蘇利南" => "苏里南", "莫三比克" => "莫桑比克", -"莫桑比克" => "莫桑比克", -"萊索托" => "莱索托", "賴索托" => "莱索托", -"貝寧" => "贝宁", "貝南" => "贝宁", -"贊比亞" => "赞比亚", "尚比亞" => "赞比亚", "亞塞拜然" => "阿塞拜疆", -"阿塞拜疆" => "阿塞拜疆", -"阿拉伯聯合酋長國" => "阿拉伯联合酋长国", "阿拉伯聯合大公國" => "阿拉伯联合酋长国", "南韓" => "韩国", -"馬爾代夫" => "马尔代夫", "馬爾地夫" => "马尔代夫", "馬爾他" => "马耳他", "馬利共和國" => "马里共和国", @@ -11415,7 +11293,7 @@ $zh2CN = array( "泡麵" => "方便面", "笨豬跳" => "蹦极跳", "绑紧跳" => "蹦极跳", -"冷盤 " => "凉菜", +"冷盤" => "凉菜", "冷菜" => "凉菜", "散钱" => "零钱", "谐星" => "笑星", @@ -11443,16 +11321,12 @@ $zh2CN = array( "積架" => "捷豹", "福斯" => "大众", "福士" => "大众", -"雪鐵龍" => "雪铁龙", "萬事得" => "马自达", -"馬自達" => "马自达", "寶獅" => "标志", "拿破崙" => "拿破仑", "布殊" => "布什", "布希" => "布什", "柯林頓" => "克林顿", -"克林頓" => "克林顿", -"薩達姆" => "萨达姆", "海珊" => "萨达姆", "梵谷" => "凡高", "大衛碧咸" => "大卫·贝克汉姆", @@ -11472,6 +11346,7 @@ $zh2SG = array( "方便面" => "快速面", "速食麵" => "快速面", "即食麵" => "快速面", +"泡麵" => "快速面", "蹦极跳" => "绑紧跳", "笨豬跳" => "绑紧跳", "凉菜" => "冷菜", @@ -11483,6 +11358,5 @@ $zh2SG = array( "民乐" => "华乐", "住房" => "住屋", "房价" => "屋价", -"泡麵" => "快速面", );
\ No newline at end of file diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 732adae1..22144333 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -38,14 +38,15 @@ */ abstract class ApiBase { - // These constants allow modules to specify exactly how to treat incomming parameters. + // These constants allow modules to specify exactly how to treat incoming parameters. - const PARAM_DFLT = 0; - const PARAM_ISMULTI = 1; - const PARAM_TYPE = 2; - const PARAM_MAX = 3; - const PARAM_MAX2 = 4; - const PARAM_MIN = 5; + const PARAM_DFLT = 0; // Default value of the parameter + const PARAM_ISMULTI = 1; // Boolean, do we accept more than one item for this parameter (e.g.: titles)? + const PARAM_TYPE = 2; // Can be either a string type (e.g.: 'integer') or an array of allowed values + const PARAM_MAX = 3; // Max value allowed for a parameter. Only applies if TYPE='integer' + const PARAM_MAX2 = 4; // Max value allowed for a parameter for bots and sysops. Only applies if TYPE='integer' + const PARAM_MIN = 5; // Lowest value allowed for a parameter. Only applies if TYPE='integer' + const PARAM_ALLOW_DUPLICATES = 6; // Boolean, do we allow the same value to be set more than once when ISMULTI=true const LIMIT_BIG1 = 500; // Fast query, std user limit const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit @@ -159,6 +160,10 @@ abstract class ApiBase { $data =& $this->getResult()->getData(); if(isset($data['warnings'][$this->getModuleName()])) { + # Don't add duplicate warnings + $warn_regex = preg_quote($warning, '/'); + if(preg_match("/{$warn_regex}(\\n|$)/", $data['warnings'][$this->getModuleName()]['*'])) + return; $warning = "{$data['warnings'][$this->getModuleName()]['*']}\n$warning"; unset($data['warnings'][$this->getModuleName()]); } @@ -238,10 +243,10 @@ abstract class ApiBase { * module's help. */ public function makeHelpMsgParameters() { - $params = $this->getAllowedParams(); + $params = $this->getFinalParams(); if ($params !== false) { - $paramsDescription = $this->getParamDescription(); + $paramsDescription = $this->getFinalParamDescription(); $msg = ''; $paramPrefix = "\n" . str_repeat(' ', 19); foreach ($params as $paramName => $paramSettings) { @@ -260,7 +265,7 @@ abstract class ApiBase { $choices = array(); $nothingPrompt = false; foreach ($type as $t) - if ($t=='') + if ($t === '') $nothingPrompt = 'Can be empty, or '; else $choices[] = $t; @@ -319,18 +324,39 @@ abstract class ApiBase { } /** - * Returns an array of allowed parameters (keys) => default value for that parameter + * Returns an array of allowed parameters (keys) => default value for that parameter. + * Don't call this function directly: use getFinalParams() to allow hooks + * to modify parameters as needed. */ protected function getAllowedParams() { return false; } /** - * Returns the description string for the given parameter. + * Returns an array of parameter descriptions. + * Don't call this functon directly: use getFinalParamDescription() to allow + * hooks to modify descriptions as needed. */ protected function getParamDescription() { return false; } + + /** + * Get final list of parameters, after hooks have had + * a chance to tweak it as needed. + */ + public function getFinalParams() { + $params = $this->getAllowedParams(); + wfRunHooks('APIGetAllowedParams', array(&$this, &$params)); + return $params; + } + + + public function getFinalParamDescription() { + $desc = $this->getParamDescription(); + wfRunHooks('APIGetParamDescription', array(&$this, &$desc)); + return $desc; + } /** * This method mangles parameter name based on the prefix supplied to the constructor. @@ -343,12 +369,11 @@ abstract class ApiBase { /** * Using getAllowedParams(), makes an array of the values provided by the user, * with key being the name of the variable, and value - validated value from user or default. - * This method can be used to generate local variables using extract(). * limit=max will not be parsed if $parseMaxLimit is set to false; use this * when the max limit is not definite, e.g. when getting revisions. */ public function extractRequestParams($parseMaxLimit = true) { - $params = $this->getAllowedParams(); + $params = $this->getFinalParams(); $results = array (); foreach ($params as $paramName => $paramSettings) @@ -361,10 +386,27 @@ abstract class ApiBase { * Get a value for the given parameter */ protected function getParameter($paramName, $parseMaxLimit = true) { - $params = $this->getAllowedParams(); + $params = $this->getFinalParams(); $paramSettings = $params[$paramName]; return $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit); } + + /** + * Die if none or more than one of a certain set of parameters is set + */ + public function requireOnlyOneParameter($params) { + $required = func_get_args(); + array_shift($required); + + $intersection = array_intersect(array_keys(array_filter($params, + create_function('$x', 'return !is_null($x);') + )), $required); + if (count($intersection) > 1) { + $this->dieUsage('The parameters '.implode(', ', $intersection).' can not be used together', 'invalidparammix'); + } elseif (count($intersection) == 0) { + $this->dieUsage('One of the parameters '.implode(', ', $required).' is required', 'missingparam'); + } + } /** * Returns an array of the namespaces (by integer id) that exist on the @@ -400,10 +442,12 @@ abstract class ApiBase { $default = $paramSettings; $multi = false; $type = gettype($paramSettings); + $dupes = false; } else { $default = isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null; $multi = isset ($paramSettings[self :: PARAM_ISMULTI]) ? $paramSettings[self :: PARAM_ISMULTI] : false; $type = isset ($paramSettings[self :: PARAM_TYPE]) ? $paramSettings[self :: PARAM_TYPE] : null; + $dupes = isset ($paramSettings[self:: PARAM_ALLOW_DUPLICATES]) ? $paramSettings[self :: PARAM_ALLOW_DUPLICATES] : false; // When type is not given, and no choices, the type is the same as $default if (!isset ($type)) { @@ -494,8 +538,8 @@ abstract class ApiBase { } } - // There should never be any duplicate values in a list - if (is_array($value)) + // Throw out duplicates if requested + if (is_array($value) && !$dupes) $value = array_unique($value); } @@ -515,10 +559,10 @@ abstract class ApiBase { protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) { if( trim($value) === "" ) return array(); - $sizeLimit = $this->mMainModule->canApiHighLimits() ? 501 : 51; - $valuesList = explode('|', $value,$sizeLimit); - if( count($valuesList) == $sizeLimit ) { - $junk = array_pop($valuesList); // kill last jumbled param + $sizeLimit = $this->mMainModule->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1; + $valuesList = explode('|', $value, $sizeLimit + 1); + if( self::truncateArray($valuesList, $sizeLimit) ) { + $this->setWarning("Too many values supplied for parameter '$valueName': the limit is $sizeLimit"); } if (!$allowMultiple && count($valuesList) != 1) { $possibleValues = is_array($allowedValues) ? "of '" . implode("', '", $allowedValues) . "'" : ''; @@ -527,7 +571,7 @@ abstract class ApiBase { if (is_array($allowedValues)) { # Check for unknown values $unknown = array_diff($valuesList, $allowedValues); - if(!empty($unknown)) + if(count($unknown)) { if($allowMultiple) { @@ -569,6 +613,23 @@ abstract class ApiBase { } } } + + /** + * Truncate an array to a certain length. + * @param $arr array Array to truncate + * @param $limit int Maximum length + * @return bool True if the array was truncated, false otherwise + */ + public static function truncateArray(&$arr, $limit) + { + $modified = false; + while(count($arr) > $limit) + { + $junk = array_pop($arr); + $modified = true; + } + return $modified; + } /** * Call main module's error handler @@ -594,8 +655,6 @@ abstract class ApiBase { 'protectedpagetext' => array('code' => 'protectedpage', 'info' => "The ``\$1'' right is required to edit this page"), 'protect-cantedit' => array('code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it"), 'badaccess-group0' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Generic permission denied message - 'badaccess-group1' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Can't use the parameter 'cause it's wikilinked - 'badaccess-group2' => array('code' => 'permissiondenied', 'info' => "Permission denied"), 'badaccess-groups' => array('code' => 'permissiondenied', 'info' => "Permission denied"), 'titleprotected' => array('code' => 'protectedtitle', 'info' => "This title has been protected from creation"), 'nocreate-loggedin' => array('code' => 'cantcreate', 'info' => "You don't have permission to create new pages"), @@ -632,13 +691,21 @@ abstract class ApiBase { 'ipb_already_blocked' => array('code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked"), 'ipb_blocked_as_range' => array('code' => 'blockedasrange', 'info' => "IP address ``\$1'' was blocked as part of range ``\$2''. You can't unblock the IP invidually, but you can unblock the range as a whole."), 'ipb_cant_unblock' => array('code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already"), + 'mailnologin' => array('code' => 'cantsend', 'info' => "You're not logged in or you don't have a confirmed e-mail address, so you can't send e-mail"), + 'usermaildisabled' => array('code' => 'usermaildisabled', 'info' => "User email has been disabled"), + 'blockedemailuser' => array('code' => 'blockedfrommail', 'info' => "You have been blocked from sending e-mail"), + 'notarget' => array('code' => 'notarget', 'info' => "You have not specified a valid target for this action"), + 'noemail' => array('code' => 'noemail', 'info' => "The user has not specified a valid e-mail address, or has chosen not to receive e-mail from other users"), + 'rcpatroldisabled' => array('code' => 'patroldisabled', 'info' => "Patrolling is disabled on this wiki"), + 'markedaspatrollederror-noautopatrol' => array('code' => 'noautopatrol', 'info' => "You don't have permission to patrol your own changes"), // API-specific messages 'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"), 'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"), + 'nosuchpageid' => array('code' => 'nosuchpageid', 'info' => "There is no page with ID \$1"), 'invaliduser' => array('code' => 'invaliduser', 'info' => "Invalid username ``\$1''"), - 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time"), - 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time is in the past"), + 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time ``\$1''"), + 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time ``\$1'' is in the past"), 'create-titleexists' => array('code' => 'create-titleexists', 'info' => "Existing titles can't be protected with 'create'"), 'missingtitle-createonly' => array('code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'"), 'cantblock' => array('code' => 'cantblock', 'info' => "You don't have permission to block users"), @@ -651,6 +718,12 @@ abstract class ApiBase { 'permdenied-undelete' => array('code' => 'permissiondenied', 'info' => "You don't have permission to restore deleted revisions"), 'createonly-exists' => array('code' => 'articleexists', 'info' => "The article you tried to create has been created already"), 'nocreate-missing' => array('code' => 'missingtitle', 'info' => "The article you tried to edit doesn't exist"), + 'nosuchrcid' => array('code' => 'nosuchrcid', 'info' => "There is no change with rcid ``\$1''"), + 'cantpurge' => array('code' => 'cantpurge', 'info' => "Only users with the 'purge' right can purge pages via the API"), + 'protect-invalidaction' => array('code' => 'protect-invalidaction', 'info' => "Invalid protection type ``\$1''"), + 'protect-invalidlevel' => array('code' => 'protect-invalidlevel', 'info' => "Invalid protection level ``\$1''"), + 'toofewexpiries' => array('code' => 'toofewexpiries', 'info' => "\$1 expiry timestamps were provided where \$2 were needed"), + // ApiEditPage messages 'noimageredirect-anon' => array('code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects"), @@ -665,18 +738,33 @@ abstract class ApiBase { 'editconflict' => array('code' => 'editconflict', 'info' => "Edit conflict detected"), 'hashcheckfailed' => array('code' => 'badmd5', 'info' => "The supplied MD5 hash was incorrect"), 'missingtext' => array('code' => 'notext', 'info' => "One of the text, appendtext and prependtext parameters must be set"), + 'emptynewsection' => array('code' => 'emptynewsection', 'info' => 'Creating empty new sections is not possible.'), ); /** * Output the error message related to a certain array - * @param array $error Element of a getUserPermissionsErrors() + * @param array $error Element of a getUserPermissionsErrors()-style array */ public function dieUsageMsg($error) { + $parsed = $this->parseMsg($error); + $this->dieUsage($parsed['code'], $parsed['info']); + } + + /** + * Return the error message related to a certain array + * @param array $error Element of a getUserPermissionsErrors()-style array + * @return array('code' => code, 'info' => info) + */ + public function parseMsg($error) { $key = array_shift($error); if(isset(self::$messageMap[$key])) - $this->dieUsage(wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error), wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error)); + return array( 'code' => + wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error), + 'info' => + wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error) + ); // If the key isn't present, throw an "unknown error" - $this->dieUsageMsg(array('unknownerror', $key)); + return $this->parseMsg(array('unknownerror', $key)); } /** @@ -814,6 +902,6 @@ abstract class ApiBase { * Returns a String that identifies the version of this class. */ public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiBase.php 36309 2008-06-15 20:37:28Z catrope $'; + return __CLASS__ . ': $Id: ApiBase.php 47041 2009-02-09 14:39:41Z catrope $'; } } diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 34813bf7..dfb11061 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -49,7 +49,7 @@ class ApiBlock extends ApiBase { * of success. If it fails, the result will specify the nature of the error. */ public function execute() { - global $wgUser; + global $wgUser, $wgBlockAllowsUTEdit; $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); @@ -72,8 +72,6 @@ class ApiBlock extends ApiBase { $this->dieUsageMsg(array('canthide')); if($params['noemail'] && !$wgUser->isAllowed('blockemail')) $this->dieUsageMsg(array('cantblock-email')); - if(wfReadOnly()) - $this->dieUsageMsg(array('readonlytext')); $form = new IPBlockForm(''); $form->BlockAddress = $params['user']; @@ -83,13 +81,15 @@ class ApiBlock extends ApiBase { $form->BlockOther = ''; $form->BlockAnonOnly = $params['anononly']; $form->BlockCreateAccount = $params['nocreate']; - $form->BlockEnableAutoBlock = $params['autoblock']; + $form->BlockEnableAutoblock = $params['autoblock']; $form->BlockEmail = $params['noemail']; $form->BlockHideName = $params['hidename']; + $form->BlockAllowUsertalk = $params['allowusertalk'] && $wgBlockAllowsUTEdit; + $form->BlockReblock = $params['reblock']; $userID = $expiry = null; $retval = $form->doBlock($userID, $expiry); - if(!empty($retval)) + if(count($retval)) // We don't care about multiple errors, just report one of them $this->dieUsageMsg($retval); @@ -107,6 +107,8 @@ class ApiBlock extends ApiBase { $res['noemail'] = ''; if($params['hidename']) $res['hidename'] = ''; + if($params['allowusertalk']) + $res['allowusertalk'] = ''; $this->getResult()->addValue(null, $this->getModuleName(), $res); } @@ -125,13 +127,15 @@ class ApiBlock extends ApiBase { 'autoblock' => false, 'noemail' => false, 'hidename' => false, + 'allowusertalk' => false, + 'reblock' => false, ); } public function getParamDescription() { return array ( 'user' => 'Username, IP address or IP range you want to block', - 'token' => 'A block token previously obtained through the gettoken parameter', + 'token' => 'A block token previously obtained through the gettoken parameter or prop=info', 'gettoken' => 'If set, a block token will be returned, and no other action will be taken', 'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.', 'reason' => 'Reason for block (optional)', @@ -139,7 +143,9 @@ class ApiBlock extends ApiBase { 'nocreate' => 'Prevent account creation', 'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from', 'noemail' => 'Prevent user from sending e-mail through the wiki. (Requires the "blockemail" right.)', - 'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)' + 'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)', + 'allowusertalk' => 'Allow the user to edit their own talk page (depends on $wgBlockAllowsUTEdit)', + 'reblock' => 'If the user is already blocked, overwrite the existing block', ); } @@ -157,6 +163,6 @@ class ApiBlock extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiBlock.php 35388 2008-05-27 10:18:28Z catrope $'; + return __CLASS__ . ': $Id: ApiBlock.php 43677 2008-11-18 15:21:04Z catrope $'; } } diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 06592d46..c0212924 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -52,29 +52,36 @@ class ApiDelete extends ApiBase { $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); - $titleObj = NULL; - if(!isset($params['title'])) - $this->dieUsageMsg(array('missingparam', 'title')); + $this->requireOnlyOneParameter($params, 'title', 'pageid'); if(!isset($params['token'])) $this->dieUsageMsg(array('missingparam', 'token')); - $titleObj = Title::newFromText($params['title']); - if(!$titleObj) - $this->dieUsageMsg(array('invalidtitle', $params['title'])); + if(isset($params['title'])) + { + $titleObj = Title::newFromText($params['title']); + if(!$titleObj) + $this->dieUsageMsg(array('invalidtitle', $params['title'])); + } + else if(isset($params['pageid'])) + { + $titleObj = Title::newFromID($params['pageid']); + if(!$titleObj) + $this->dieUsageMsg(array('nosuchpageid', $params['pageid'])); + } if(!$titleObj->exists()) $this->dieUsageMsg(array('notanarticle')); $reason = (isset($params['reason']) ? $params['reason'] : NULL); - if ($titleObj->getNamespace() == NS_IMAGE) { - $retval = self::deletefile($params['token'], $titleObj, $params['oldimage'], $reason, false); - if(!empty($retval)) + if ($titleObj->getNamespace() == NS_FILE) { + $retval = self::deleteFile($params['token'], $titleObj, $params['oldimage'], $reason, false); + if(count($retval)) // We don't care about multiple errors, just report one of them $this->dieUsageMsg(current($retval)); } else { $articleObj = new Article($titleObj); $retval = self::delete($articleObj, $params['token'], $reason); - if(!empty($retval)) + if(count($retval)) // We don't care about multiple errors, just report one of them $this->dieUsageMsg(current($retval)); @@ -90,8 +97,6 @@ class ApiDelete extends ApiBase { private static function getPermissionsError(&$title, $token) { global $wgUser; - // Check wiki readonly - if (wfReadOnly()) return array(array('readonlytext')); // Check permissions $errors = $title->getUserPermissionsErrors('delete', $wgUser); @@ -114,8 +119,8 @@ class ApiDelete extends ApiBase { public static function delete(&$article, $token, &$reason = NULL) { global $wgUser; - - $errors = self::getPermissionsError($article->getTitle(), $token); + $title = $article->getTitle(); + $errors = self::getPermissionsError($title, $token); if (count($errors)) return $errors; // Auto-generate a summary, if necessary @@ -156,7 +161,8 @@ class ApiDelete extends ApiBase { if( !FileDeleteForm::haveDeletableFile($file, $oldfile, $oldimage) ) return array(array('nofile')); - + if (is_null($reason)) # Log and RC don't like null reasons + $reason = ''; $status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress ); if( !$status->isGood() ) @@ -170,6 +176,9 @@ class ApiDelete extends ApiBase { public function getAllowedParams() { return array ( 'title' => null, + 'pageid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), 'token' => null, 'reason' => null, 'watch' => false, @@ -180,7 +189,8 @@ class ApiDelete extends ApiBase { public function getParamDescription() { return array ( - 'title' => 'Title of the page you want to delete.', + 'title' => 'Title of the page you want to delete. Cannot be used together with pageid', + 'pageid' => 'Page ID of the page you want to delete. Cannot be used together with title', 'token' => 'A delete token previously retrieved through prop=info', 'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used.', 'watch' => 'Add the page to your watchlist', @@ -191,7 +201,7 @@ class ApiDelete extends ApiBase { public function getDescription() { return array( - 'Deletes a page. You need to be logged in as a sysop to use this function, see also action=login.' + 'Delete a page.' ); } @@ -203,6 +213,6 @@ class ApiDelete extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiDelete.php 35350 2008-05-26 12:15:21Z simetrical $'; + return __CLASS__ . ': $Id: ApiDelete.php 44541 2008-12-13 21:07:18Z mrzman $'; } } diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php new file mode 100644 index 00000000..40e38a0f --- /dev/null +++ b/includes/api/ApiDisabled.php @@ -0,0 +1,72 @@ +<?php + +/* + * Created on Sep 25, 2008 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + + +/** + * API module that dies with an error immediately. + * + * Use this to disable core modules with + * $wgAPIModules['modulename'] = 'ApiDisabled'; + * + * To disable submodules of action=query, use ApiQueryDisabled instead + * + * @ingroup API + */ +class ApiDisabled extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + $this->dieUsage("The ``{$this->getModuleName()}'' module has been disabled.", 'moduledisabled'); + } + + public function getAllowedParams() { + return array (); + } + + public function getParamDescription() { + return array (); + } + + public function getDescription() { + return array( + 'This module has been disabled.' + ); + } + + protected function getExamples() { + return array (); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiDisabled.php 41268 2008-09-25 20:50:50Z catrope $'; + } +} diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index d10432f3..bc5dfa87 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -29,8 +29,10 @@ if (!defined('MEDIAWIKI')) { } /** - * A query module to list all external URLs found on a given set of pages. + * A module that allows for editing and creating pages. * + * Currently, this wraps around the EditPage class in an ugly way, + * EditPage.php should be rewritten to provide a cleaner interface * @ingroup API */ class ApiEditPage extends ApiBase { @@ -66,7 +68,7 @@ class ApiEditPage extends ApiBase { $errors = $titleObj->getUserPermissionsErrors('edit', $wgUser); if(!$titleObj->exists()) $errors = array_merge($errors, $titleObj->getUserPermissionsErrors('create', $wgUser)); - if(!empty($errors)) + if(count($errors)) $this->dieUsageMsg($errors[0]); $articleObj = new Article($titleObj); @@ -98,8 +100,11 @@ class ApiEditPage extends ApiBase { $reqArr['wpEdittime'] = wfTimestamp(TS_MW, $params['basetimestamp']); else $reqArr['wpEdittime'] = $articleObj->getTimestamp(); - # Fake wpStartime - $reqArr['wpStarttime'] = $reqArr['wpEdittime']; + if(!is_null($params['starttimestamp']) && $params['starttimestamp'] != '') + $reqArr['wpStarttime'] = wfTimestamp(TS_MW, $params['starttimestamp']); + else + # Fake wpStartime + $reqArr['wpStarttime'] = $reqArr['wpEdittime']; if($params['minor'] || (!$params['notminor'] && $wgUser->getOption('minordefault'))) $reqArr['wpMinoredit'] = ''; if($params['recreate']) @@ -111,6 +116,8 @@ class ApiEditPage extends ApiBase { $this->dieUsage("The section parameter must be set to an integer or 'new'", "invalidsection"); $reqArr['wpSection'] = $params['section']; } + else + $reqArr['wpSection'] = ''; if($params['watch']) $watch = true; @@ -134,13 +141,13 @@ class ApiEditPage extends ApiBase { # Handle CAPTCHA parameters global $wgRequest; if(isset($params['captchaid'])) - $wgRequest->data['wpCaptchaId'] = $params['captchaid']; + $wgRequest->setVal( 'wpCaptchaId', $params['captchaid'] ); if(isset($params['captchaword'])) - $wgRequest->data['wpCaptchaWord'] = $params['captchaword']; + $wgRequest->setVal( 'wpCaptchaWord', $params['captchaword'] ); $r = array(); if(!wfRunHooks('APIEditBeforeSave', array(&$ep, $ep->textbox1, &$r))) { - if(!empty($r)) + if(count($r)) { $r['result'] = "Failure"; $this->getResult()->addValue(null, $this->getModuleName(), $r); @@ -200,18 +207,24 @@ class ApiEditPage extends ApiBase { case EditPage::AS_CONFLICT_DETECTED: $this->dieUsageMsg(array('editconflict')); #case EditPage::AS_SUMMARY_NEEDED: Can't happen since we set wpIgnoreBlankSummary - #case EditPage::AS_TEXTBOX_EMPTY: Can't happen since we don't do sections + case EditPage::AS_TEXTBOX_EMPTY: + $this->dieUsageMsg(array('emptynewsection')); case EditPage::AS_END: # This usually means some kind of race condition # or DB weirdness occurred. Throw an unknown error here. - $this->dieUsageMsg(array('unknownerror', 'AS_END')); + $this->dieUsageMsg(array('unknownerror')); case EditPage::AS_SUCCESS_NEW_ARTICLE: $r['new'] = ''; case EditPage::AS_SUCCESS_UPDATE: $r['result'] = "Success"; $r['pageid'] = $titleObj->getArticleID(); $r['title'] = $titleObj->getPrefixedText(); - $newRevId = $titleObj->getLatestRevId(); + # HACK: We create a new Article object here because getRevIdFetched() + # refuses to be run twice, and because Title::getLatestRevId() + # won't fetch from the master unless we select for update, which we + # don't want to do. + $newArticle = new Article($titleObj); + $newRevId = $newArticle->getRevIdFetched(); if($newRevId == $oldRevId) $r['nochange'] = ''; else @@ -245,6 +258,7 @@ class ApiEditPage extends ApiBase { 'notminor' => false, 'bot' => false, 'basetimestamp' => null, + 'starttimestamp' => null, 'recreate' => false, 'createonly' => false, 'nocreate' => false, @@ -271,6 +285,9 @@ class ApiEditPage extends ApiBase { 'basetimestamp' => array('Timestamp of the base revision (gotten through prop=revisions&rvprop=timestamp).', 'Used to detect edit conflicts; leave unset to ignore conflicts.' ), + 'starttimestamp' => array('Timestamp when you obtained the edit token.', + 'Used to detect edit conflicts; leave unset to ignore conflicts.' + ), 'recreate' => 'Override any errors about the article having been deleted in the meantime', 'createonly' => 'Don\'t edit the page if it exists already', 'nocreate' => 'Throw an error if the page doesn\'t exist', @@ -294,6 +311,6 @@ class ApiEditPage extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiEditPage.php 36309 2008-06-15 20:37:28Z catrope $'; + return __CLASS__ . ': $Id: ApiEditPage.php 44394 2008-12-10 14:12:54Z catrope $'; } } diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index 7e083536..fbdf495f 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -39,6 +39,11 @@ class ApiEmailUser extends ApiBase { public function execute() { global $wgUser; + + // Check whether email is enabled + if ( !EmailUserForm::userEmailEnabled() ) + $this->dieUsageMsg( array( 'usermaildisabled' ) ); + $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); @@ -53,12 +58,12 @@ class ApiEmailUser extends ApiBase { // Validate target $targetUser = EmailUserForm::validateEmailTarget( $params['target'] ); if ( !( $targetUser instanceof User ) ) - $this->dieUsageMsg( array( $targetUser[0] ) ); + $this->dieUsageMsg( array( $targetUser ) ); // Check permissions $error = EmailUserForm::getPermissionsError( $wgUser, $params['token'] ); if ( $error ) - $this->dieUsageMsg( array( $error[0] ) ); + $this->dieUsageMsg( array( $error ) ); $form = new EmailUserForm( $targetUser, $params['text'], $params['subject'], $params['ccme'] ); @@ -89,7 +94,6 @@ class ApiEmailUser extends ApiBase { 'target' => 'User to send email to', 'subject' => 'Subject header', 'text' => 'Mail body', - // FIXME: How to properly get a token? 'token' => 'A token previously acquired via prop=info', 'ccme' => 'Send a copy of this mail to me', ); @@ -97,7 +101,7 @@ class ApiEmailUser extends ApiBase { public function getDescription() { return array( - 'Emails a user.' + 'Email a user.' ); } @@ -108,7 +112,7 @@ class ApiEmailUser extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: $'; + return __CLASS__ . ': $Id: ApiEmailUser.php 41269 2008-09-25 21:39:36Z catrope $'; } }
\ No newline at end of file diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index 397aece3..f4e6212a 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -43,23 +43,22 @@ class ApiExpandTemplates extends ApiBase { public function execute() { // Get parameters - extract( $this->extractRequestParams() ); - $retval = ''; + $params = $this->extractRequestParams(); //Create title for parser - $title_obj = Title :: newFromText( $title ); + $title_obj = Title :: newFromText( $params['title'] ); if(!$title_obj) - $title_obj = Title :: newFromText( "API" ); // Default title is "API". For example, ExpandTemplates uses "ExpendTemplates" for it + $title_obj = Title :: newFromText( "API" ); // default $result = $this->getResult(); // Parse text global $wgParser; $options = new ParserOptions(); - if ( $generatexml ) + if ( $params['generatexml'] ) { $wgParser->startExternalParse( $title_obj, $options, OT_PREPROCESS ); - $dom = $wgParser->preprocessToDom( $text ); + $dom = $wgParser->preprocessToDom( $params['text'] ); if ( is_callable( array( $dom, 'saveXML' ) ) ) { $xml = $dom->saveXML(); } else { @@ -67,9 +66,9 @@ class ApiExpandTemplates extends ApiBase { } $xml_result = array(); $result->setContent( $xml_result, $xml ); - $result->addValue( null, 'parsetree', $xml_result); + $result->addValue( null, 'parsetree', $xml_result); } - $retval = $wgParser->preprocess( $text, $title_obj, $options ); + $retval = $wgParser->preprocess( $params['text'], $title_obj, $options ); // Return result $retval_array = array(); @@ -106,6 +105,6 @@ class ApiExpandTemplates extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiExpandTemplates.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiExpandTemplates.php 44719 2008-12-17 16:34:01Z catrope $'; } } diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 8f08f4db..9efbbbe0 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -199,15 +199,17 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or * This method also replaces any '<' with < */ protected function formatHTML($text) { + global $wgUrlProtocols; + // Escape everything first for full coverage $text = htmlspecialchars($text); // encode all comments or tags as safe blue strings $text = preg_replace('/\<(!--.*?--|.*?)\>/', '<span style="color:blue;"><\1></span>', $text); // identify URLs - $protos = "http|https|ftp|gopher"; + $protos = implode("|", $wgUrlProtocols); # This regex hacks around bug 13218 (" included in the URL) - $text = preg_replace("#(($protos)://.*?)(")?([ \\'\"()<\n])#", '<a href="\\1">\\1</a>\\3\\4', $text); + $text = preg_replace("#(($protos).*?)(")?([ \\'\"()<\n])#", '<a href="\\1">\\1</a>\\3\\4', $text); // identify requests to api.php $text = preg_replace("#api\\.php\\?[^ \\()<\n\t]+#", '<a href="\\0">\\0</a>', $text); if( $this->mHelp ) { @@ -239,7 +241,7 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or } public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 44569 2008-12-14 08:31:04Z tstarling $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 43470 2008-11-14 00:30:34Z tstarling $'; } } @@ -300,6 +302,6 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 44569 2008-12-14 08:31:04Z tstarling $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 43470 2008-11-14 00:30:34Z tstarling $'; } } diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 42156849..1d89eb18 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -58,7 +58,10 @@ class ApiFormatJson extends ApiFormatBase { $suffix = ")"; } - if (!function_exists('json_encode') || $this->getIsHtml()) { + // Some versions of PHP have a broken json_encode, see PHP bug + // 46944. Test encoding an affected character (U+20000) to + // avoid this. + if (!function_exists('json_encode') || $this->getIsHtml() || strtolower(json_encode("\xf0\xa0\x80\x80")) != '\ud840\udc00') { $json = new Services_JSON(); $this->printText($prefix . $json->encode($this->getResultData(), $this->getIsHtml()) . $suffix); } else { @@ -86,6 +89,6 @@ class ApiFormatJson extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatJson.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiFormatJson.php 45682 2009-01-12 19:06:33Z raymond $'; } } diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php index 87d7086e..4b29ff56 100644 --- a/includes/api/ApiFormatJson_json.php +++ b/includes/api/ApiFormatJson_json.php @@ -50,7 +50,7 @@ * @author Matt Knapp <mdknapp[at]gmail[dot]com> * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com> * @copyright 2005 Michal Migurski -* @version CVS: $Id: ApiFormatJson_json.php 35098 2008-05-20 17:13:28Z ialex $ +* @version CVS: $Id: ApiFormatJson_json.php 45682 2009-01-12 19:06:33Z raymond $ * @license http://www.opensource.org/licenses/bsd-license.php * @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198 */ @@ -168,6 +168,17 @@ class Services_JSON return chr(0xC0 | (($bytes >> 6) & 0x1F)) . chr(0x80 | ($bytes & 0x3F)); + case (0xFC00 & $bytes) == 0xD800 && strlen($utf16) >= 4 && (0xFC & ord($utf16{2})) == 0xDC: + // return a 4-byte UTF-8 character + $char = ((($bytes & 0x03FF) << 10) + | ((ord($utf16{2}) & 0x03) << 8) + | ord($utf16{3})); + $char += 0x10000; + return chr(0xF0 | (($char >> 18) & 0x07)) + . chr(0x80 | (($char >> 12) & 0x3F)) + . chr(0x80 | (($char >> 6) & 0x3F)) + . chr(0x80 | ($char & 0x3F)); + case (0xFFFF & $bytes) == $bytes: // return a 3-byte UTF-8 character // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 @@ -218,6 +229,20 @@ class Services_JSON | (0x0F & (ord($utf8{1}) >> 2))) . chr((0xC0 & (ord($utf8{1}) << 6)) | (0x7F & ord($utf8{2}))); + + case 4: + // return a UTF-16 surrogate pair from a 4-byte UTF-8 char + if(ord($utf8{0}) > 0xF4) return ''; # invalid + $char = ((0x1C0000 & (ord($utf8{0}) << 18)) + | (0x03F000 & (ord($utf8{1}) << 12)) + | (0x000FC0 & (ord($utf8{2}) << 6)) + | (0x00003F & ord($utf8{3}))); + if($char > 0x10FFFF) return ''; # invalid + $char -= 0x10000; + return chr(0xD8 | (($char >> 18) & 0x03)) + . chr(($char >> 10) & 0xFF) + . chr(0xDC | (($char >> 8) & 0x03)) + . chr($char & 0xFF); } // ignoring UTF-32 for now, sorry @@ -346,40 +371,19 @@ class Services_JSON case (($ord_var_c & 0xF8) == 0xF0): // characters U-00010000 - U-001FFFFF, mask 11110XXX // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + // These will always return a surrogate pair $char = pack('C*', $ord_var_c, ord($var{$c + 1}), ord($var{$c + 2}), ord($var{$c + 3})); $c += 3; $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - - case (($ord_var_c & 0xFC) == 0xF8): - // characters U-00200000 - U-03FFFFFF, mask 111110XX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2}), - ord($var{$c + 3}), - ord($var{$c + 4})); - $c += 4; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - - case (($ord_var_c & 0xFE) == 0xFC): - // characters U-04000000 - U-7FFFFFFF, mask 1111110X - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2}), - ord($var{$c + 3}), - ord($var{$c + 4}), - ord($var{$c + 5})); - $c += 5; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); + if($utf16 == '') { + $ascii .= '\ufffd'; + } else { + $utf16 = str_split($utf16, 2); + $ascii .= sprintf('\u%04s\u%04s', bin2hex($utf16[0]), bin2hex($utf16[1])); + } break; } } @@ -591,6 +595,16 @@ class Services_JSON } break; + case preg_match('/\\\uD[89AB][0-9A-F]{2}\\\uD[C-F][0-9A-F]{2}/i', substr($chrs, $c, 12)): + // escaped unicode surrogate pair + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))) + . chr(hexdec(substr($chrs, ($c + 8), 2))) + . chr(hexdec(substr($chrs, ($c + 10), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 11; + break; + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): // single, escaped unicode character $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) @@ -812,6 +826,9 @@ class Services_JSON } } + +// Hide the PEAR_Error variant from Doxygen +/// @cond if (class_exists('PEAR_Error')) { /** @@ -827,6 +844,7 @@ if (class_exists('PEAR_Error')) { } } else { +/// @endcond /** * @todo Ultimately, this class shall be descended from PEAR_Error diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index 0909539e..e741c16d 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -42,38 +42,62 @@ class ApiFormatWddx extends ApiFormatBase { } public function execute() { - if (function_exists('wddx_serialize_value')) { + if (function_exists('wddx_serialize_value') && !$this->getIsHtml()) { $this->printText(wddx_serialize_value($this->getResultData())); } else { - $this->printText('<?xml version="1.0" encoding="utf-8"?>'); - $this->printText('<wddxPacket version="1.0"><header/><data>'); - $this->slowWddxPrinter($this->getResultData()); - $this->printText('</data></wddxPacket>'); + // Don't do newlines and indentation if we weren't asked + // for pretty output + $nl = ($this->getIsHtml() ? "" : "\n"); + $indstr = " "; + $this->printText("<?xml version=\"1.0\"?>$nl"); + $this->printText("<wddxPacket version=\"1.0\">$nl"); + $this->printText("$indstr<header/>$nl"); + $this->printText("$indstr<data>$nl"); + $this->slowWddxPrinter($this->getResultData(), 4); + $this->printText("$indstr</data>$nl"); + $this->printText("</wddxPacket>$nl"); } } /** * Recursivelly go through the object and output its data in WDDX format. */ - function slowWddxPrinter($elemValue) { + function slowWddxPrinter($elemValue, $indent = 0) { + $indstr = ($this->getIsHtml() ? "" : str_repeat(' ', $indent)); + $indstr2 = ($this->getIsHtml() ? "" : str_repeat(' ', $indent + 2)); + $nl = ($this->getIsHtml() ? "" : "\n"); switch (gettype($elemValue)) { case 'array' : - $this->printText('<struct>'); - foreach ($elemValue as $subElemName => $subElemValue) { - $this->printText(wfElement('var', array ( - 'name' => $subElemName - ), null)); - $this->slowWddxPrinter($subElemValue); - $this->printText('</var>'); + // Check whether we've got an associative array (<struct>) + // or a regular array (<array>) + $cnt = count($elemValue); + if($cnt == 0 || array_keys($elemValue) === range(0, $cnt - 1)) { + // Regular array + $this->printText($indstr . Xml::element('array', array( + 'length' => $cnt + ), null) . $nl); + foreach($elemValue as $subElemValue) + $this->slowWddxPrinter($subElemValue, $indent + 2); + $this->printText("$indstr</array>$nl"); + } else { + // Associative array (<struct>) + $this->printText("$indstr<struct>$nl"); + foreach($elemValue as $subElemName => $subElemValue) { + $this->printText($indstr2 . Xml::element('var', array( + 'name' => $subElemName + ), null) . $nl); + $this->slowWddxPrinter($subElemValue, $indent + 4); + $this->printText("$indstr2</var>$nl"); + } + $this->printText("$indstr</struct>$nl"); } - $this->printText('</struct>'); break; case 'integer' : case 'double' : - $this->printText(wfElement('number', null, $elemValue)); + $this->printText($indstr . Xml::element('number', null, $elemValue) . $nl); break; case 'string' : - $this->printText(wfElement('string', null, $elemValue)); + $this->printText($indstr . Xml::element('string', null, $elemValue) . $nl); break; default : ApiBase :: dieDebug(__METHOD__, 'Unknown type ' . gettype($elemValue)); @@ -85,6 +109,6 @@ class ApiFormatWddx extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatWddx.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiFormatWddx.php 44588 2008-12-14 19:14:21Z demon $'; } } diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index d35eb3e9..7ff57324 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -56,12 +56,12 @@ class ApiFormatXml extends ApiFormatBase { $params = $this->extractRequestParams(); $this->mDoubleQuote = $params['xmldoublequote']; - $this->printText('<?xml version="1.0" encoding="utf-8"?>'); + $this->printText('<?xml version="1.0"?>'); $this->recXmlPrint($this->mRootElemName, $this->getResultData(), $this->getIsHtml() ? -2 : null); } /** - * This method takes an array and converts it into an xml. + * This method takes an array and converts it to XML. * There are several noteworthy cases: * * If array contains a key '_element', then the code assumes that ALL other keys are not important and replaces them with the value['_element']. @@ -80,6 +80,7 @@ class ApiFormatXml extends ApiFormatBase { } else { $indstr = ''; } + $elemName = str_replace(' ', '_', $elemName); switch (gettype($elemValue)) { case 'array' : @@ -104,6 +105,14 @@ class ApiFormatXml extends ApiFormatBase { foreach ($elemValue as $subElemId => & $subElemValue) { if (is_string($subElemValue) && $this->mDoubleQuote) $subElemValue = $this->doubleQuote($subElemValue); + + // Replace spaces with underscores + $newSubElemId = str_replace(' ', '_', $subElemId); + if($newSubElemId != $subElemId) { + $elemValue[$newSubElemId] = $subElemValue; + unset($elemValue[$subElemId]); + $subElemId = $newSubElemId; + } if (gettype($subElemId) === 'integer') { $indElements[] = $subElemValue; @@ -114,18 +123,18 @@ class ApiFormatXml extends ApiFormatBase { } } - if (is_null($subElemIndName) && !empty ($indElements)) + if (is_null($subElemIndName) && count($indElements)) ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName()."); - if (!empty ($subElements) && !empty ($indElements) && !is_null($subElemContent)) + if (count($subElements) && count($indElements) && !is_null($subElemContent)) ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has content and subelements"); if (!is_null($subElemContent)) { - $this->printText($indstr . wfElement($elemName, $elemValue, $subElemContent)); - } elseif (empty ($indElements) && empty ($subElements)) { - $this->printText($indstr . wfElement($elemName, $elemValue)); + $this->printText($indstr . Xml::element($elemName, $elemValue, $subElemContent)); + } elseif (!count($indElements) && !count($subElements)) { + $this->printText($indstr . Xml::element($elemName, $elemValue)); } else { - $this->printText($indstr . wfElement($elemName, $elemValue, null)); + $this->printText($indstr . Xml::element($elemName, $elemValue, null)); foreach ($subElements as $subElemId => & $subElemValue) $this->recXmlPrint($subElemId, $subElemValue, $indent); @@ -133,14 +142,14 @@ class ApiFormatXml extends ApiFormatBase { foreach ($indElements as $subElemId => & $subElemValue) $this->recXmlPrint($subElemIndName, $subElemValue, $indent); - $this->printText($indstr . wfCloseElement($elemName)); + $this->printText($indstr . Xml::closeElement($elemName)); } break; case 'object' : // ignore break; default : - $this->printText($indstr . wfElement($elemName, null, $elemValue)); + $this->printText($indstr . Xml::element($elemName, null, $elemValue)); break; } } @@ -166,6 +175,6 @@ class ApiFormatXml extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatXml.php 37075 2008-07-04 22:44:57Z brion $'; + return __CLASS__ . ': $Id: ApiFormatXml.php 44588 2008-12-14 19:14:21Z demon $'; } } diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php index c0d4093e..f16b2c8a 100644 --- a/includes/api/ApiFormatYaml_spyc.php +++ b/includes/api/ApiFormatYaml_spyc.php @@ -1,883 +1,234 @@ <?php - /** - * Spyc -- A Simple PHP YAML Class - * @version 0.2.3 -- 2006-02-04 - * @author Chris Wanstrath <chris@ozmm.org> - * @see http://spyc.sourceforge.net/ - * @copyright Copyright 2005-2006 Chris Wanstrath - * @license http://www.opensource.org/licenses/mit-license.php MIT License - */ - - /** - * A node, used by Spyc for parsing YAML. - * @ingroup API - */ - class YAMLNode { - /**#@+ - * @access public - * @var string - */ - var $parent; - var $id; - /**#@-*/ - /** - * @access public - * @var mixed - */ - var $data; - /** - * @access public - * @var int - */ - var $indent; - /** - * @access public - * @var bool - */ - var $children = false; - - /** - * The constructor assigns the node a unique ID. - * @access public - * @return void - */ - function YAMLNode() { - $this->id = uniqid(''); - } - } - - /** - * The Simple PHP YAML Class. - * - * This class can be used to read a YAML file and convert its contents - * into a PHP array. It currently supports a very limited subsection of - * the YAML spec. - * - * Usage: - * <code> - * $parser = new Spyc; - * $array = $parser->load($file); - * </code> - * @ingroup API - */ - class Spyc { - - /** - * Load YAML into a PHP array statically - * - * The load method, when supplied with a YAML stream (string or file), - * will do its best to convert YAML in a file into a PHP array. Pretty - * simple. - * Usage: - * <code> - * $array = Spyc::YAMLLoad('lucky.yml'); - * print_r($array); - * </code> - * @access public - * @return array - * @param string $input Path of YAML file or string containing YAML - */ - function YAMLLoad($input) { - $spyc = new Spyc; - return $spyc->load($input); - } - - /** - * Dump YAML from PHP array statically - * - * The dump method, when supplied with an array, will do its best - * to convert the array into friendly YAML. Pretty simple. Feel free to - * save the returned string as nothing.yml and pass it around. - * - * Oh, and you can decide how big the indent is and what the wordwrap - * for folding is. Pretty cool -- just pass in 'false' for either if - * you want to use the default. - * - * Indent's default is 2 spaces, wordwrap's default is 40 characters. And - * you can turn off wordwrap by passing in 0. - * - * @access public - * @static - * @return string - * @param array $array PHP array - * @param int $indent Pass in false to use the default, which is 2 - * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) - */ - public static function YAMLDump($array,$indent = false,$wordwrap = false) { - $spyc = new Spyc; - return $spyc->dump($array,$indent,$wordwrap); - } - - /** - * Load YAML into a PHP array from an instantiated object - * - * The load method, when supplied with a YAML stream (string or file path), - * will do its best to convert the YAML into a PHP array. Pretty simple. - * Usage: - * <code> - * $parser = new Spyc; - * $array = $parser->load('lucky.yml'); - * print_r($array); - * </code> - * @access public - * @return array - * @param string $input Path of YAML file or string containing YAML - */ - function load($input) { - // See what type of input we're talking about - // If it's not a file, assume it's a string - if (!empty($input) && (strpos($input, "\n") === false) - && file_exists($input)) { - $yaml = file($input); - } else { - $yaml = explode("\n",$input); - } - // Initiate some objects and values - $base = new YAMLNode; - $base->indent = 0; - $this->_lastIndent = 0; - $this->_lastNode = $base->id; - $this->_inBlock = false; - $this->_isInline = false; - - foreach ($yaml as $linenum => $line) { - $ifchk = trim($line); - - // If the line starts with a tab (instead of a space), throw a fit. - if (preg_match('/^(\t)+(\w+)/', $line)) { - $err = 'ERROR: Line '. ($linenum + 1) .' in your input YAML begins'. - ' with a tab. YAML only recognizes spaces. Please reformat.'; - die($err); - } - - if ($this->_inBlock === false && empty($ifchk)) { - continue; - } elseif ($this->_inBlock == true && empty($ifchk)) { - $last =& $this->_allNodes[$this->_lastNode]; - $last->data[key($last->data)] .= "\n"; - } elseif ($ifchk{0} != '#' && substr($ifchk,0,3) != '---') { - // Create a new node and get its indent - $node = new YAMLNode; - $node->indent = $this->_getIndent($line); - - // Check where the node lies in the hierarchy - if ($this->_lastIndent == $node->indent) { - // If we're in a block, add the text to the parent's data - if ($this->_inBlock === true) { - $parent =& $this->_allNodes[$this->_lastNode]; - $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; - } else { - // The current node's parent is the same as the previous node's - if (isset($this->_allNodes[$this->_lastNode])) { - $node->parent = $this->_allNodes[$this->_lastNode]->parent; - } - } - } elseif ($this->_lastIndent < $node->indent) { - if ($this->_inBlock === true) { - $parent =& $this->_allNodes[$this->_lastNode]; - $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; - } elseif ($this->_inBlock === false) { - // The current node's parent is the previous node - $node->parent = $this->_lastNode; - - // If the value of the last node's data was > or | we need to - // start blocking i.e. taking in all lines as a text value until - // we drop our indent. - $parent =& $this->_allNodes[$node->parent]; - $this->_allNodes[$node->parent]->children = true; - if (is_array($parent->data)) { - $chk = $parent->data[key($parent->data)]; - if ($chk === '>') { - $this->_inBlock = true; - $this->_blockEnd = ' '; - $parent->data[key($parent->data)] = - str_replace('>','',$parent->data[key($parent->data)]); - $parent->data[key($parent->data)] .= trim($line).' '; - $this->_allNodes[$node->parent]->children = false; - $this->_lastIndent = $node->indent; - } elseif ($chk === '|') { - $this->_inBlock = true; - $this->_blockEnd = "\n"; - $parent->data[key($parent->data)] = - str_replace('|','',$parent->data[key($parent->data)]); - $parent->data[key($parent->data)] .= trim($line)."\n"; - $this->_allNodes[$node->parent]->children = false; - $this->_lastIndent = $node->indent; - } - } - } - } elseif ($this->_lastIndent > $node->indent) { - // Any block we had going is dead now - if ($this->_inBlock === true) { - $this->_inBlock = false; - if ($this->_blockEnd = "\n") { - $last =& $this->_allNodes[$this->_lastNode]; - $last->data[key($last->data)] = - trim($last->data[key($last->data)]); - } - } - - // We don't know the parent of the node so we have to find it - // foreach ($this->_allNodes as $n) { - foreach ($this->_indentSort[$node->indent] as $n) { - if ($n->indent == $node->indent) { - $node->parent = $n->parent; - } - } - } - - if ($this->_inBlock === false) { - // Set these properties with information from our current node - $this->_lastIndent = $node->indent; - // Set the last node - $this->_lastNode = $node->id; - // Parse the YAML line and return its data - $node->data = $this->_parseLine($line); - // Add the node to the master list - $this->_allNodes[$node->id] = $node; - // Add a reference to the node in an indent array - $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id]; - // Add a reference to the node in a References array if this node - // has a YAML reference in it. - if ( - ( (is_array($node->data)) && - isset($node->data[key($node->data)]) && - (!is_array($node->data[key($node->data)])) ) - && - ( (preg_match('/^&([^ ]+)/',$node->data[key($node->data)])) - || - (preg_match('/^\*([^ ]+)/',$node->data[key($node->data)])) ) - ) { - $this->_haveRefs[] =& $this->_allNodes[$node->id]; - } elseif ( - ( (is_array($node->data)) && - isset($node->data[key($node->data)]) && - (is_array($node->data[key($node->data)])) ) - ) { - // Incomplete reference making code. Ugly, needs cleaned up. - foreach ($node->data[key($node->data)] as $d) { - if ( !is_array($d) && - ( (preg_match('/^&([^ ]+)/',$d)) - || - (preg_match('/^\*([^ ]+)/',$d)) ) - ) { - $this->_haveRefs[] =& $this->_allNodes[$node->id]; - } - } - } - } - } - } - unset($node); - - // Here we travel through node-space and pick out references (& and *) - $this->_linkReferences(); - - // Build the PHP array out of node-space - $trunk = $this->_buildArray(); - return $trunk; - } - - /** - * Dump PHP array to YAML - * - * The dump method, when supplied with an array, will do its best - * to convert the array into friendly YAML. Pretty simple. Feel free to - * save the returned string as tasteful.yml and pass it around. - * - * Oh, and you can decide how big the indent is and what the wordwrap - * for folding is. Pretty cool -- just pass in 'false' for either if - * you want to use the default. - * - * Indent's default is 2 spaces, wordwrap's default is 40 characters. And - * you can turn off wordwrap by passing in 0. - * - * @access public - * @return string - * @param array $array PHP array - * @param int $indent Pass in false to use the default, which is 2 - * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) - */ - function dump($array,$indent = false,$wordwrap = false) { - // Dumps to some very clean YAML. We'll have to add some more features - // and options soon. And better support for folding. - - // New features and options. - if ($indent === false or !is_numeric($indent)) { - $this->_dumpIndent = 2; - } else { - $this->_dumpIndent = $indent; - } - - if ($wordwrap === false or !is_numeric($wordwrap)) { - $this->_dumpWordWrap = 40; - } else { - $this->_dumpWordWrap = $wordwrap; - } - - // New YAML document - $string = "---\n"; - - // Start at the base of the array and move through it. - foreach ($array as $key => $value) { - $string .= $this->_yamlize($key,$value,0); - } - return $string; - } - - /**** Private Properties ****/ - - /**#@+ - * @access private - * @var mixed - */ - var $_haveRefs; - var $_allNodes; - var $_lastIndent; - var $_lastNode; - var $_inBlock; - var $_isInline; - var $_dumpIndent; - var $_dumpWordWrap; - /**#@-*/ - - /**** Private Methods ****/ - - /** - * Attempts to convert a key / value array item to YAML - * @access private - * @return string - * @param $key The name of the key - * @param $value The value of the item - * @param $indent The indent of the current node - */ - function _yamlize($key,$value,$indent) { - if (is_array($value)) { - // It has children. What to do? - // Make it the right kind of item - $string = $this->_dumpNode($key,NULL,$indent); - // Add the indent - $indent += $this->_dumpIndent; - // Yamlize the array - $string .= $this->_yamlizeArray($value,$indent); - } elseif (!is_array($value)) { - // It doesn't have children. Yip. - $string = $this->_dumpNode($key,$value,$indent); - } - return $string; - } - - /** - * Attempts to convert an array to YAML - * @access private - * @return string - * @param $array The array you want to convert - * @param $indent The indent of the current level - */ - function _yamlizeArray($array,$indent) { - if (is_array($array)) { - $string = ''; - foreach ($array as $key => $value) { - $string .= $this->_yamlize($key,$value,$indent); - } - return $string; - } else { - return false; - } - } - - /** - * Find out whether a string needs to be output as a literal rather than in plain style. - * Added by Roan Kattouw 13-03-2008 - * @param $value The string to check - * @return bool - */ - function _needLiteral($value) { - # Check whether the string contains # or : or begins with any of: - # [ - ? , [ ] { } ! * & | > ' " % @ ` ] - # or is a number or contains newlines - return (bool)(gettype($value) == "string" && - (is_numeric($value) || - strpos($value, "\n") || - preg_match("/[#:]/", $value) || - preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value))); - - } - - /** - * Returns YAML from a key and a value - * @access private - * @return string - * @param $key The name of the key - * @param $value The value of the item - * @param $indent The indent of the current node - */ - function _dumpNode($key,$value,$indent) { - // do some folding here, for blocks - if ($this->_needLiteral($value)) { - $value = $this->_doLiteralBlock($value,$indent); - } else { - $value = $this->_doFolding($value,$indent); - } - - $spaces = str_repeat(' ',$indent); - - if (is_int($key)) { - // It's a sequence - if ($value) - $string = $spaces.'- '.$value."\n"; - else - $string = $spaces . "-\n"; - } else { - // It's mapped - if ($value) - $string = $spaces.$key.': '.$value."\n"; - else - $string = $spaces . $key . ":\n"; - } - return $string; - } - - /** - * Creates a literal block for dumping - * @access private - * @return string - * @param $value - * @param $indent int The value of the indent - */ - function _doLiteralBlock($value,$indent) { - $exploded = explode("\n",$value); - $newValue = '|'; - $indent += $this->_dumpIndent; - $spaces = str_repeat(' ',$indent); - foreach ($exploded as $line) { - $newValue .= "\n" . $spaces . trim($line); - } - return $newValue; - } - - /** - * Folds a string of text, if necessary - * @access private - * @return string - * @param $value The string you wish to fold - */ - function _doFolding($value,$indent) { - // Don't do anything if wordwrap is set to 0 - if ($this->_dumpWordWrap === 0) { - return $value; - } - - if (strlen($value) > $this->_dumpWordWrap) { - $indent += $this->_dumpIndent; - $indent = str_repeat(' ',$indent); - $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); - $value = ">\n".$indent.$wrapped; - } - return $value; - } - - /* Methods used in loading */ - - /** - * Finds and returns the indentation of a YAML line - * @access private - * @return int - * @param string $line A line from the YAML file - */ - function _getIndent($line) { - $match = array(); - preg_match('/^\s{1,}/',$line,$match); - if (!empty($match[0])) { - $indent = substr_count($match[0],' '); - } else { - $indent = 0; - } - return $indent; - } - - /** - * Parses YAML code and returns an array for a node - * @access private - * @return array - * @param string $line A line from the YAML file - */ - function _parseLine($line) { - $line = trim($line); - - $array = array(); - - if (preg_match('/^-(.*):$/',$line)) { - // It's a mapped sequence - $key = trim(substr(substr($line,1),0,-1)); - $array[$key] = ''; - } elseif ($line[0] == '-' && substr($line,0,3) != '---') { - // It's a list item but not a new stream - if (strlen($line) > 1) { - $value = trim(substr($line,1)); - // Set the type of the value. Int, string, etc - $value = $this->_toType($value); - $array[] = $value; - } else { - $array[] = array(); - } - } elseif (preg_match('/^(.+):/',$line,$key)) { - // It's a key/value pair most likely - // If the key is in double quotes pull it out - $matches = array(); - if (preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { - $value = trim(str_replace($matches[1],'',$line)); - $key = $matches[2]; - } else { - // Do some guesswork as to the key and the value - $explode = explode(':',$line); - $key = trim($explode[0]); - array_shift($explode); - $value = trim(implode(':',$explode)); - } - - // Set the type of the value. Int, string, etc - $value = $this->_toType($value); - if (empty($key)) { - $array[] = $value; - } else { - $array[$key] = $value; - } - } - return $array; - } - - /** - * Finds the type of the passed value, returns the value as the new type. - * @access private - * @param string $value - * @return mixed - */ - function _toType($value) { - $matches = array(); - if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) { - $value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches)); - $value = preg_replace('/\\\\"/','"',$value); - } elseif (preg_match('/^\\[(.+)\\]$/',$value,$matches)) { - // Inline Sequence - - // Take out strings sequences and mappings - $explode = $this->_inlineEscape($matches[1]); - - // Propogate value array - $value = array(); - foreach ($explode as $v) { - $value[] = $this->_toType($v); - } - } elseif (strpos($value,': ')!==false && !preg_match('/^{(.+)/',$value)) { - // It's a map - $array = explode(': ',$value); - $key = trim($array[0]); - array_shift($array); - $value = trim(implode(': ',$array)); - $value = $this->_toType($value); - $value = array($key => $value); - } elseif (preg_match("/{(.+)}$/",$value,$matches)) { - // Inline Mapping - - // Take out strings sequences and mappings - $explode = $this->_inlineEscape($matches[1]); - - // Propogate value array - $array = array(); - foreach ($explode as $v) { - $array = $array + $this->_toType($v); - } - $value = $array; - } elseif (strtolower($value) == 'null' or $value == '' or $value == '~') { - $value = NULL; - } elseif (ctype_digit($value)) { - $value = (int)$value; - } elseif (in_array(strtolower($value), - array('true', 'on', '+', 'yes', 'y'))) { - $value = TRUE; - } elseif (in_array(strtolower($value), - array('false', 'off', '-', 'no', 'n'))) { - $value = FALSE; - } elseif (is_numeric($value)) { - $value = (float)$value; - } else { - // Just a normal string, right? - $value = trim(preg_replace('/#(.+)$/','',$value)); - } - - return $value; - } - - /** - * Used in inlines to check for more inlines or quoted strings - * @access private - * @return array - */ - function _inlineEscape($inline) { - // There's gotta be a cleaner way to do this... - // While pure sequences seem to be nesting just fine, - // pure mappings and mappings with sequences inside can't go very - // deep. This needs to be fixed. - - // Check for strings - $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; - $strings = array(); - if (preg_match_all($regex,$inline,$strings)) { - $saved_strings[] = $strings[0][0]; - $inline = preg_replace($regex,'YAMLString',$inline); - } - unset($regex); - - // Check for sequences - $seqs = array(); - if (preg_match_all('/\[(.+)\]/U',$inline,$seqs)) { - $inline = preg_replace('/\[(.+)\]/U','YAMLSeq',$inline); - $seqs = $seqs[0]; - } - - // Check for mappings - $maps = array(); - if (preg_match_all('/{(.+)}/U',$inline,$maps)) { - $inline = preg_replace('/{(.+)}/U','YAMLMap',$inline); - $maps = $maps[0]; - } - - $explode = explode(', ',$inline); - - // Re-add the strings - if (!empty($saved_strings)) { - $i = 0; - foreach ($explode as $key => $value) { - if (strpos($value,'YAMLString')) { - $explode[$key] = str_replace('YAMLString',$saved_strings[$i],$value); - ++$i; - } - } - } - - // Re-add the sequences - if (!empty($seqs)) { - $i = 0; - foreach ($explode as $key => $value) { - if (strpos($value,'YAMLSeq') !== false) { - $explode[$key] = str_replace('YAMLSeq',$seqs[$i],$value); - ++$i; - } - } - } - - // Re-add the mappings - if (!empty($maps)) { - $i = 0; - foreach ($explode as $key => $value) { - if (strpos($value,'YAMLMap') !== false) { - $explode[$key] = str_replace('YAMLMap',$maps[$i],$value); - ++$i; - } - } - } - - return $explode; - } - - /** - * Builds the PHP array from all the YAML nodes we've gathered - * @access private - * @return array - */ - function _buildArray() { - $trunk = array(); - - if (!isset($this->_indentSort[0])) { - return $trunk; - } - - foreach ($this->_indentSort[0] as $n) { - if (empty($n->parent)) { - $this->_nodeArrayizeData($n); - // Check for references and copy the needed data to complete them. - $this->_makeReferences($n); - // Merge our data with the big array we're building - $trunk = $this->_array_kmerge($trunk,$n->data); - } - } - - return $trunk; - } - - /** - * Traverses node-space and sets references (& and *) accordingly - * @access private - * @return bool - */ - function _linkReferences() { - if (is_array($this->_haveRefs)) { - foreach ($this->_haveRefs as $node) { - if (!empty($node->data)) { - $key = key($node->data); - // If it's an array, don't check. - if (is_array($node->data[$key])) { - foreach ($node->data[$key] as $k => $v) { - $this->_linkRef($node,$key,$k,$v); - } - } else { - $this->_linkRef($node,$key); - } - } - } - } - return true; - } - - function _linkRef(&$n,$key,$k = NULL,$v = NULL) { - if (empty($k) && empty($v)) { - // Look for &refs - $matches = array(); - if (preg_match('/^&([^ ]+)/',$n->data[$key],$matches)) { - // Flag the node so we know it's a reference - $this->_allNodes[$n->id]->ref = substr($matches[0],1); - $this->_allNodes[$n->id]->data[$key] = - substr($n->data[$key],strlen($matches[0])+1); - // Look for *refs - } elseif (preg_match('/^\*([^ ]+)/',$n->data[$key],$matches)) { - $ref = substr($matches[0],1); - // Flag the node as having a reference - $this->_allNodes[$n->id]->refKey = $ref; - } - } elseif (!empty($k) && !empty($v)) { - if (preg_match('/^&([^ ]+)/',$v,$matches)) { - // Flag the node so we know it's a reference - $this->_allNodes[$n->id]->ref = substr($matches[0],1); - $this->_allNodes[$n->id]->data[$key][$k] = - substr($v,strlen($matches[0])+1); - // Look for *refs - } elseif (preg_match('/^\*([^ ]+)/',$v,$matches)) { - $ref = substr($matches[0],1); - // Flag the node as having a reference - $this->_allNodes[$n->id]->refKey = $ref; - } - } - } - - /** - * Finds the children of a node and aids in the building of the PHP array - * @access private - * @param int $nid The id of the node whose children we're gathering - * @return array - */ - function _gatherChildren($nid) { - $return = array(); - $node =& $this->_allNodes[$nid]; - foreach ($this->_allNodes as $z) { - if ($z->parent == $node->id) { - // We found a child - $this->_nodeArrayizeData($z); - // Check for references - $this->_makeReferences($z); - // Merge with the big array we're returning - // The big array being all the data of the children of our parent node - $return = $this->_array_kmerge($return,$z->data); - } - } - return $return; - } - - /** - * Turns a node's data and its children's data into a PHP array - * - * @access private - * @param array $node The node which you want to arrayize - * @return boolean - */ - function _nodeArrayizeData(&$node) { - if (is_array($node->data) && $node->children == true) { - // This node has children, so we need to find them - $childs = $this->_gatherChildren($node->id); - // We've gathered all our children's data and are ready to use it - $key = key($node->data); - $key = empty($key) ? 0 : $key; - // If it's an array, add to it of course - if (is_array($node->data[$key])) { - $node->data[$key] = $this->_array_kmerge($node->data[$key],$childs); - } else { - $node->data[$key] = $childs; - } - } elseif (!is_array($node->data) && $node->children == true) { - // Same as above, find the children of this node - $childs = $this->_gatherChildren($node->id); - $node->data = array(); - $node->data[] = $childs; - } - - // We edited $node by reference, so just return true - return true; - } - - /** - * Traverses node-space and copies references to / from this object. - * @access private - * @param object $z A node whose references we wish to make real - * @return bool - */ - function _makeReferences(&$z) { - // It is a reference - if (isset($z->ref)) { - $key = key($z->data); - // Copy the data to this object for easy retrieval later - $this->ref[$z->ref] =& $z->data[$key]; - // It has a reference - } elseif (isset($z->refKey)) { - if (isset($this->ref[$z->refKey])) { - $key = key($z->data); - // Copy the data from this object to make the node a real reference - $z->data[$key] =& $this->ref[$z->refKey]; - } - } - return true; - } - - - /** - * Merges arrays and maintains numeric keys. - * - * An ever-so-slightly modified version of the array_kmerge() function posted - * to php.net by mail at nospam dot iaindooley dot com on 2004-04-08. - * - * http://www.php.net/manual/en/function.array-merge.php#41394 - * - * @access private - * @param array $arr1 - * @param array $arr2 - * @return array - */ - function _array_kmerge($arr1,$arr2) { - if(!is_array($arr1)) - $arr1 = array(); - - if(!is_array($arr2)) - $arr2 = array(); - - $keys1 = array_keys($arr1); - $keys2 = array_keys($arr2); - $keys = array_merge($keys1,$keys2); - $vals1 = array_values($arr1); - $vals2 = array_values($arr2); - $vals = array_merge($vals1,$vals2); - $ret = array(); - - foreach($keys as $key) { - list( /* unused */ ,$val) = each($vals); - // This is the good part! If a key already exists, but it's part of a - // sequence (an int), just keep addin numbers until we find a fresh one. - if (isset($ret[$key]) and is_int($key)) { - while (array_key_exists($key, $ret)) { - $key++; - } - } - $ret[$key] = $val; - } - - return $ret; - } - } +/** + * Spyc -- A Simple PHP YAML Class + * @version 0.2.3 -- 2006-02-04 + * @author Chris Wanstrath <chris@ozmm.org> + * @see http://spyc.sourceforge.net/ + * @copyright Copyright 2005-2006 Chris Wanstrath + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * The Simple PHP YAML Class. + * + * This class can be used to read a YAML file and convert its contents + * into a PHP array. It currently supports a very limited subsection of + * the YAML spec. + * + * @ingroup API + */ +class Spyc { + + /** + * Dump YAML from PHP array statically + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as nothing.yml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @return string + * @param $array Array: PHP array + * @param $indent Integer: Pass in false to use the default, which is 2 + * @param $wordwrap Integer: Pass in 0 for no wordwrap, false for default (40) + */ + public static function YAMLDump($array,$indent = false,$wordwrap = false) { + $spyc = new Spyc; + return $spyc->dump($array,$indent,$wordwrap); + } + + /** + * Dump PHP array to YAML + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as tasteful.yml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @public + * @return string + * @param $array Array: PHP array + * @param $indent Integer: Pass in false to use the default, which is 2 + * @param $wordwrap Integer: Pass in 0 for no wordwrap, false for default (40) + */ + function dump($array,$indent = false,$wordwrap = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = "---\n"; + + // Start at the base of the array and move through it. + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,0); + } + return $string; + } + + /**** Private Properties ****/ + + private $_haveRefs; + private $_allNodes; + private $_lastIndent; + private $_lastNode; + private $_inBlock; + private $_isInline; + private $_dumpIndent; + private $_dumpWordWrap; + + /**** Private Methods ****/ + + /** + * Attempts to convert a key / value array item to YAML + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + private function _yamlize($key,$value,$indent) { + if (is_array($value)) { + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key,NULL,$indent); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif (!is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key,$value,$indent); + } + return $string; + } + + /** + * Attempts to convert an array to YAML + * @return string + * @param $array The array you want to convert + * @param $indent The indent of the current level + */ + private function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,$indent); + } + return $string; + } else { + return false; + } + } + + /** + * Find out whether a string needs to be output as a literal rather than in plain style. + * Added by Roan Kattouw 13-03-2008 + * @param $value The string to check + * @return bool + */ + function _needLiteral($value) { + # Check whether the string contains # or : or begins with any of: + # [ - ? , [ ] { } ! * & | > ' " % @ ` ] + # or is a number or contains newlines + return (bool)(gettype($value) == "string" && + (is_numeric($value) || + strpos($value, "\n") || + preg_match("/[#:]/", $value) || + preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value))); + + } + + /** + * Returns YAML from a key and a value + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + private function _dumpNode($key,$value,$indent) { + // do some folding here, for blocks + if ($this->_needLiteral($value)) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + } + + $spaces = str_repeat(' ',$indent); + + if (is_int($key)) { + // It's a sequence + if ($value !== '' && !is_null($value)) + $string = $spaces.'- '.$value."\n"; + else + $string = $spaces . "-\n"; + } else { + // It's mapped + if ($value !== '' && !is_null($value)) + $string = $spaces . $key . ': ' . $value . "\n"; + else + $string = $spaces . $key . ":\n"; + } + return $string; + } + + /** + * Creates a literal block for dumping + * @return string + * @param $value + * @param $indent int The value of the indent + */ + private function _doLiteralBlock($value,$indent) { + $exploded = explode("\n",$value); + $newValue = '|'; + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $newValue .= "\n" . $spaces . trim($line); + } + return $newValue; + } + + /** + * Folds a string of text, if necessary + * @return string + * @param $value The string you wish to fold + */ + private function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + if ($this->_dumpWordWrap === 0) { + return $value; + } + + if (strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } + return $value; + } +} diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index a45390c4..43b30f7c 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -36,23 +36,6 @@ if (!defined('MEDIAWIKI')) { */ class ApiLogin extends ApiBase { - /** - * Time (in seconds) a user must wait after submitting - * a bad login (will be multiplied by the THROTTLE_FACTOR for each bad attempt) - */ - const THROTTLE_TIME = 5; - - /** - * The factor by which the wait-time in between authentication - * attempts is increased every failed attempt. - */ - const THROTTLE_FACTOR = 2; - - /** - * The maximum number of failed logins after which the wait increase stops. - */ - const THOTTLE_MAX_COUNT = 10; - public function __construct($main, $action) { parent :: __construct($main, $action, 'lg'); } @@ -61,7 +44,7 @@ class ApiLogin extends ApiBase { * Executes the log-in attempt using the parameters passed. If * the log-in succeeeds, it attaches a cookie to the session * and outputs the user id, username, and session token. If a - * log-in fails, as the result of a bad password, a nonexistant + * log-in fails, as the result of a bad password, a nonexistent * user, or any other reason, the host is cached with an expiry * and no log-in attempts will be accepted until that expiry * is reached. The expiry is $this->mLoginThrottle. @@ -69,25 +52,14 @@ class ApiLogin extends ApiBase { * @access public */ public function execute() { - $name = $password = $domain = null; - extract($this->extractRequestParams()); + $params = $this->extractRequestParams(); $result = array (); - // Make sure noone is trying to guess the password brut-force - $nextLoginIn = $this->getNextLoginTimeout(); - if ($nextLoginIn > 0) { - $result['result'] = 'NeedToWait'; - $result['details'] = "Please wait $nextLoginIn seconds before next log-in attempt"; - $result['wait'] = $nextLoginIn; - $this->getResult()->addValue(null, 'login', $result); - return; - } - - $params = new FauxRequest(array ( - 'wpName' => $name, - 'wpPassword' => $password, - 'wpDomain' => $domain, + $req = new FauxRequest(array ( + 'wpName' => $params['name'], + 'wpPassword' => $params['password'], + 'wpDomain' => $params['domain'], 'wpRemember' => '' )); @@ -96,8 +68,8 @@ class ApiLogin extends ApiBase { wfSetupSession(); } - $loginForm = new LoginForm($params); - switch ($loginForm->authenticateUserData()) { + $loginForm = new LoginForm($req); + switch ($authRes = $loginForm->authenticateUserData()) { case LoginForm :: SUCCESS : global $wgUser, $wgCookiePrefix; @@ -139,95 +111,18 @@ class ApiLogin extends ApiBase { $result['result'] = 'CreateBlocked'; $result['details'] = 'Your IP address is blocked from account creation'; break; + case LoginForm :: THROTTLED : + global $wgPasswordAttemptThrottle; + $result['result'] = 'Throttled'; + $result['wait'] = $wgPasswordAttemptThrottle['seconds']; + break; default : - ApiBase :: dieDebug(__METHOD__, 'Unhandled case value'); - } - - if ($result['result'] != 'Success' && !isset( $result['details'] ) ) { - $delay = $this->cacheBadLogin(); - $result['wait'] = $delay; - $result['details'] = "Please wait " . $delay . " seconds before next log-in attempt"; + ApiBase :: dieDebug(__METHOD__, "Unhandled case value: {$authRes}"); } - // if we were allowed to try to login, memcache is fine $this->getResult()->addValue(null, 'login', $result); } - - /** - * Caches a bad-login attempt associated with the host and with an - * expiry of $this->mLoginThrottle. These are cached by a key - * separate from that used by the captcha system--as such, logging - * in through the standard interface will get you a legal session - * and cookies to prove it, but will not remove this entry. - * - * Returns the number of seconds until next login attempt will be allowed. - * - * @access private - */ - private function cacheBadLogin() { - global $wgMemc; - - $key = $this->getMemCacheKey(); - $val = $wgMemc->get( $key ); - - $val['lastReqTime'] = time(); - if (!isset($val['count'])) { - $val['count'] = 1; - } else { - $val['count'] = 1 + $val['count']; - } - - $delay = ApiLogin::calculateDelay($val['count']); - - $wgMemc->delete($key); - // Cache expiration should be the maximum timeout - to prevent a "try and wait" attack - $wgMemc->add( $key, $val, ApiLogin::calculateDelay(ApiLogin::THOTTLE_MAX_COUNT) ); - - return $delay; - } - - /** - * How much time the client must wait before it will be - * allowed to try to log-in next. - * The return value is 0 if no wait is required. - */ - private function getNextLoginTimeout() { - global $wgMemc; - - $val = $wgMemc->get($this->getMemCacheKey()); - - $elapse = (time() - $val['lastReqTime']); // in seconds - $canRetryIn = ApiLogin::calculateDelay($val['count']) - $elapse; - - return $canRetryIn < 0 ? 0 : $canRetryIn; - } - - /** - * Based on the number of previously attempted logins, returns - * the delay (in seconds) when the next login attempt will be allowed. - */ - private static function calculateDelay($count) { - // Defensive programming - $count = intval($count); - $count = $count < 1 ? 1 : $count; - $count = $count > self::THOTTLE_MAX_COUNT ? self::THOTTLE_MAX_COUNT : $count; - - return self::THROTTLE_TIME + self::THROTTLE_TIME * ($count - 1) * self::THROTTLE_FACTOR; - } - - /** - * Internal cache key for badlogin checks. Robbed from the - * ConfirmEdit extension and modified to use a key unique to the - * API login.3 - * - * @return string - * @access private - */ - private function getMemCacheKey() { - return wfMemcKey( 'apilogin', 'badlogin', 'ip', wfGetIP() ); - } - public function mustBePosted() { return true; } public function getAllowedParams() { @@ -263,6 +158,6 @@ class ApiLogin extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiLogin.php 35565 2008-05-29 19:23:37Z btongminh $'; + return __CLASS__ . ': $Id: ApiLogin.php 45275 2009-01-01 02:02:03Z simetrical $'; } } diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index 694c9e3c..8b178f6a 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -42,11 +42,12 @@ class ApiLogout extends ApiBase { public function execute() { global $wgUser; + $oldName = $wgUser->getName(); $wgUser->logout(); // Give extensions to do something after user logout $injected_html = ''; - wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html) ); + wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html, $oldName) ); } public function getAllowedParams() { @@ -70,6 +71,6 @@ class ApiLogout extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiLogout.php 35294 2008-05-24 20:44:49Z btongminh $'; + return __CLASS__ . ': $Id: ApiLogout.php 43522 2008-11-15 01:23:39Z brion $'; } } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 2d0e278c..60d932be 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -65,6 +65,7 @@ class ApiMain extends ApiBase { 'feedwatchlist' => 'ApiFeedWatchlist', 'help' => 'ApiHelp', 'paraminfo' => 'ApiParamInfo', + 'purge' => 'ApiPurge', ); private static $WriteModules = array ( @@ -77,6 +78,8 @@ class ApiMain extends ApiBase { 'move' => 'ApiMove', 'edit' => 'ApiEditPage', 'emailuser' => 'ApiEmailUser', + 'watch' => 'ApiWatch', + 'patrol' => 'ApiPatrol', ); /** @@ -99,6 +102,23 @@ class ApiMain extends ApiBase { 'dbg' => 'ApiFormatDbg', 'dbgfm' => 'ApiFormatDbg' ); + + /** + * List of user roles that are specifically relevant to the API. + * array( 'right' => array ( 'msg' => 'Some message with a $1', + * 'params' => array ( $someVarToSubst ) ), + * ); + */ + private static $mRights = array('writeapi' => array( + 'msg' => 'Use of the write API', + 'params' => array() + ), + 'apihighlimits' => array( + 'msg' => 'Use higher limits in API queries (Slow queries: $1 results; Fast queries: $2 results). The limits for slow queries also apply to multivalue parameters.', + 'params' => array (ApiMain::LIMIT_SML2, ApiMain::LIMIT_BIG2) + ) + ); + private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames; private $mResult, $mAction, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode, $mSquidMaxage; @@ -144,9 +164,9 @@ class ApiMain extends ApiBase { if($wgEnableWriteAPI) $this->mModules += self::$WriteModules; - $this->mModuleNames = array_keys($this->mModules); // todo: optimize + $this->mModuleNames = array_keys($this->mModules); $this->mFormats = self :: $Formats; - $this->mFormatNames = array_keys($this->mFormats); // todo: optimize + $this->mFormatNames = array_keys($this->mFormats); $this->mResult = new ApiResult($this); $this->mShowVersions = false; @@ -193,6 +213,8 @@ class ApiMain extends ApiBase { if (!$wgUser->isAllowed('writeapi')) $this->dieUsage('You\'re not allowed to edit this ' . 'wiki through the API', 'writeapidenied'); + if (wfReadOnly()) + $this->dieUsageMsg(array('readonlytext')); } /** @@ -206,6 +228,8 @@ class ApiMain extends ApiBase { * Create an instance of an output formatter by its name */ public function createPrinterByName($format) { + if( !isset( $this->mFormats[$format] ) ) + $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' ); return new $this->mFormats[$format] ($this, $format); } @@ -235,6 +259,11 @@ class ApiMain extends ApiBase { try { $this->executeAction(); } catch (Exception $e) { + // Log it + if ( $e instanceof MWException ) { + wfDebugLog( 'exception', $e->getLogMessage() ); + } + // // Handle any kind of exception by outputing properly formatted error message. // If this fails, an unhandled exception should be thrown so that global error @@ -248,7 +277,7 @@ class ApiMain extends ApiBase { $headerStr = 'MediaWiki-API-Error: ' . $errCode; if ($e->getCode() === 0) - header($headerStr, true); + header($headerStr); else header($headerStr, true, $e->getCode()); @@ -260,12 +289,11 @@ class ApiMain extends ApiBase { $this->printResult(true); } - global $wgRequest; if($this->mSquidMaxage == -1) { # Nobody called setCacheMaxAge(), use the (s)maxage parameters - $smaxage = $wgRequest->getVal('smaxage', 0); - $maxage = $wgRequest->getVal('maxage', 0); + $smaxage = $this->getParameter('smaxage'); + $maxage = $this->getParameter('maxage'); } else $smaxage = $maxage = $this->mSquidMaxage; @@ -332,6 +360,9 @@ class ApiMain extends ApiBase { } $this->getResult()->reset(); + // Re-add the id + if($this->mRequest->getCheck('requestid')) + $this->getResult()->addValue(null, 'requestid', $this->mRequest->getVal('requestid')); $this->getResult()->addValue(null, 'error', $errMessage); return $errMessage['code']; @@ -341,12 +372,19 @@ class ApiMain extends ApiBase { * Execute the actual module, without any error handling */ protected function executeAction() { + // First add the id to the top element + if($this->mRequest->getCheck('requestid')) + $this->getResult()->addValue(null, 'requestid', $this->mRequest->getVal('requestid')); $params = $this->extractRequestParams(); $this->mShowVersions = $params['version']; $this->mAction = $params['action']; + if( !is_string( $this->mAction ) ) { + $this->dieUsage( "The API requires a valid action parameter", 'unknown_action' ); + } + // Instantiate the module requested by the user $module = new $this->mModules[$this->mAction] ($this, $this->mAction); @@ -356,6 +394,9 @@ class ApiMain extends ApiBase { $maxLag = $params['maxlag']; list( $host, $lag ) = wfGetLB()->getMaxLag(); if ( $lag > $maxLag ) { + header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); + header( 'X-Database-Lag: ' . intval( $lag ) ); + // XXX: should we return a 503 HTTP error code like wfMaxlagError() does? if( $wgShowHostnames ) { ApiBase :: dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); } else { @@ -384,6 +425,7 @@ class ApiMain extends ApiBase { // Execute $module->profileIn(); $module->execute(); + wfRunHooks('APIAfterExecute', array(&$module)); $module->profileOut(); if (!$this->mInternalMode) { @@ -396,6 +438,7 @@ class ApiMain extends ApiBase { * Print results using the current printer */ protected function printResult($isError) { + $this->getResult()->cleanupUTF8(); $printer = $this->mPrinter; $printer->profileIn(); @@ -437,6 +480,7 @@ class ApiMain extends ApiBase { ApiBase :: PARAM_TYPE => 'integer', ApiBase :: PARAM_DFLT => 0 ), + 'requestid' => null, ); } @@ -451,6 +495,7 @@ class ApiMain extends ApiBase { 'maxlag' => 'Maximum lag', 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached', 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached', + 'requestid' => 'Request ID to distinguish requests. This will just be output back to you', ); } @@ -493,6 +538,7 @@ class ApiMain extends ApiBase { 'API developers:', ' Roan Kattouw <Firstname>.<Lastname>@home.nl (lead developer Sep 2007-present)', ' Victor Vasiliev - vasilvv at gee mail dot com', + ' Bryan Tong Minh - bryan . tongminh @ gmail . com', ' Yuri Astrakhan <Firstname><Lastname>@gmail.com (creator, lead developer Sep 2006-Sep 2007)', '', 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org', @@ -521,6 +567,14 @@ class ApiMain extends ApiBase { $msg .= "\n"; } + $msg .= "\n$astriks Permissions $astriks\n\n"; + foreach ( self :: $mRights as $right => $rightMsg ) { + $groups = User::getGroupsWithPermission( $right ); + $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg[ 'msg' ], $rightMsg[ 'params' ] ) . + "\nGranted to:\n " . str_replace( "*", "all", implode( ", ", $groups ) ) . "\n"; + + } + $msg .= "\n$astriks Formats $astriks\n\n"; foreach( $this->mFormats as $formatName => $unused ) { $module = $this->createPrinterByName($formatName); @@ -539,7 +593,7 @@ class ApiMain extends ApiBase { public static function makeHelpMsgHeader($module, $paramName) { $modulePrefix = $module->getModulePrefix(); - if (!empty($modulePrefix)) + if (strval($modulePrefix) !== '') $modulePrefix = "($modulePrefix) "; return "* $paramName={$module->getModuleName()} $modulePrefix*"; @@ -602,8 +656,8 @@ class ApiMain extends ApiBase { */ public function getVersion() { $vers = array (); - $vers[] = 'MediaWiki ' . SpecialVersion::getVersion(); - $vers[] = __CLASS__ . ': $Id: ApiMain.php 44569 2008-12-14 08:31:04Z tstarling $'; + $vers[] = 'MediaWiki: ' . SpecialVersion::getVersion() . "\n http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/"; + $vers[] = __CLASS__ . ': $Id: ApiMain.php 45752 2009-01-14 21:36:57Z catrope $'; $vers[] = ApiBase :: getBaseVersion(); $vers[] = ApiFormatBase :: getBaseVersion(); $vers[] = ApiQueryBase :: getBaseVersion(); diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index 8687bdcd..13b058c9 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -44,9 +44,7 @@ class ApiMove extends ApiBase { if(is_null($params['reason'])) $params['reason'] = ''; - $titleObj = NULL; - if(!isset($params['from'])) - $this->dieUsageMsg(array('missingparam', 'from')); + $this->requireOnlyOneParameter($params, 'from', 'fromid'); if(!isset($params['to'])) $this->dieUsageMsg(array('missingparam', 'to')); if(!isset($params['token'])) @@ -54,9 +52,18 @@ class ApiMove extends ApiBase { if(!$wgUser->matchEditToken($params['token'])) $this->dieUsageMsg(array('sessionfailure')); - $fromTitle = Title::newFromText($params['from']); - if(!$fromTitle) - $this->dieUsageMsg(array('invalidtitle', $params['from'])); + if(isset($params['from'])) + { + $fromTitle = Title::newFromText($params['from']); + if(!$fromTitle) + $this->dieUsageMsg(array('invalidtitle', $params['from'])); + } + else if(isset($params['fromid'])) + { + $fromTitle = Title::newFromID($params['fromid']); + if(!$fromTitle) + $this->dieUsageMsg(array('nosuchpageid', $params['fromid'])); + } if(!$fromTitle->exists()) $this->dieUsageMsg(array('notanarticle')); $fromTalk = $fromTitle->getTalkPage(); @@ -66,27 +73,10 @@ class ApiMove extends ApiBase { $this->dieUsageMsg(array('invalidtitle', $params['to'])); $toTalk = $toTitle->getTalkPage(); - // Run getUserPermissionsErrors() here so we get message arguments too, - // rather than just a message key. The latter is troublesome for messages - // that use arguments. - // FIXME: moveTo() should really return an array, requires some - // refactoring of other code, though (mainly SpecialMovepage.php) - $errors = array_merge($fromTitle->getUserPermissionsErrors('move', $wgUser), - $fromTitle->getUserPermissionsErrors('edit', $wgUser), - $toTitle->getUserPermissionsErrors('move', $wgUser), - $toTitle->getUserPermissionsErrors('edit', $wgUser)); - if(!empty($errors)) - // We don't care about multiple errors, just report one of them - $this->dieUsageMsg(current($errors)); - $hookErr = null; - $retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']); if($retval !== true) - { - # FIXME: Title::moveTo() sometimes returns a string $this->dieUsageMsg(reset($retval)); - } $r = array('from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason']); if(!$params['noredirect'] || !$wgUser->isAllowed('suppressredirect')) @@ -105,8 +95,9 @@ class ApiMove extends ApiBase { // We're not gonna dieUsage() on failure, since we already changed something else { - $r['talkmove-error-code'] = ApiBase::$messageMap[$retval]['code']; - $r['talkmove-error-info'] = ApiBase::$messageMap[$retval]['info']; + $parsed = $this->parseMsg(reset($retval)); + $r['talkmove-error-code'] = $parsed['code']; + $r['talkmove-error-info'] = $parsed['info']; } } @@ -129,6 +120,9 @@ class ApiMove extends ApiBase { public function getAllowedParams() { return array ( 'from' => null, + 'fromid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), 'to' => null, 'token' => null, 'reason' => null, @@ -141,7 +135,8 @@ class ApiMove extends ApiBase { public function getParamDescription() { return array ( - 'from' => 'Title of the page you want to move.', + 'from' => 'Title of the page you want to move. Cannot be used together with fromid.', + 'fromid' => 'Page ID of the page you want to move. Cannot be used together with from.', 'to' => 'Title you want to rename the page to.', 'token' => 'A move token previously retrieved through prop=info', 'reason' => 'Reason for the move (optional).', @@ -154,7 +149,7 @@ class ApiMove extends ApiBase { public function getDescription() { return array( - 'Moves a page.' + 'Move a page.' ); } @@ -165,6 +160,6 @@ class ApiMove extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiMove.php 35619 2008-05-30 19:59:47Z btongminh $'; + return __CLASS__ . ': $Id: ApiMove.php 47041 2009-02-09 14:39:41Z catrope $'; } } diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index e09cb285..54482e4b 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -53,7 +53,7 @@ class ApiPageSet extends ApiQueryBase { private $mRequestedPageFields; public function __construct($query, $resolveRedirects = false) { - parent :: __construct($query, __CLASS__); + parent :: __construct($query, 'query'); $this->mAllPages = array (); $this->mTitles = array(); @@ -92,10 +92,11 @@ class ApiPageSet extends ApiQueryBase { */ public function getPageTableFields() { // Ensure we get minimum required fields + // DON'T change this order $pageFlds = array ( - 'page_id' => null, 'page_namespace' => null, - 'page_title' => null + 'page_title' => null, + 'page_id' => null, ); // only store non-default fields @@ -227,19 +228,18 @@ class ApiPageSet extends ApiQueryBase { */ public function execute() { $this->profileIn(); - $titles = $pageids = $revids = null; - extract($this->extractRequestParams()); + $params = $this->extractRequestParams(); // Only one of the titles/pageids/revids is allowed at the same time $dataSource = null; - if (isset ($titles)) + if (isset ($params['titles'])) $dataSource = 'titles'; - if (isset ($pageids)) { + if (isset ($params['pageids'])) { if (isset ($dataSource)) $this->dieUsage("Cannot use 'pageids' at the same time as '$dataSource'", 'multisource'); $dataSource = 'pageids'; } - if (isset ($revids)) { + if (isset ($params['revids'])) { if (isset ($dataSource)) $this->dieUsage("Cannot use 'revids' at the same time as '$dataSource'", 'multisource'); $dataSource = 'revids'; @@ -247,15 +247,17 @@ class ApiPageSet extends ApiQueryBase { switch ($dataSource) { case 'titles' : - $this->initFromTitles($titles); + $this->initFromTitles($params['titles']); break; case 'pageids' : - $this->initFromPageIds($pageids); + $this->initFromPageIds($params['pageids']); break; case 'revids' : if($this->mResolveRedirects) - $this->dieUsage('revids may not be used with redirect resolution', 'params'); - $this->initFromRevIDs($revids); + $this->setWarning('Redirect resolution cannot be used together with the revids= parameter. '. + 'Any redirects the revids= point to have not been resolved.'); + $this->mResolveRedirects = false; + $this->initFromRevIDs($params['revids']); break; default : // Do nothing - some queries do not need any of the data sources. @@ -366,7 +368,7 @@ class ApiPageSet extends ApiQueryBase { } private function initFromPageIds($pageids) { - if(empty($pageids)) + if(!count($pageids)) return; $pageids = array_map('intval', $pageids); // paranoia @@ -424,7 +426,7 @@ class ApiPageSet extends ApiQueryBase { if(isset($remaining)) { // Any items left in the $remaining list are added as missing if($processTitles) { - // The remaining titles in $remaining are non-existant pages + // The remaining titles in $remaining are non-existent pages foreach ($remaining as $ns => $dbkeys) { foreach ( $dbkeys as $dbkey => $unused ) { $title = Title :: makeTitle($ns, $dbkey); @@ -438,7 +440,7 @@ class ApiPageSet extends ApiQueryBase { else { // The remaining pageids do not exist - if(empty($this->mMissingPageIDs)) + if(!$this->mMissingPageIDs) $this->mMissingPageIDs = array_keys($remaining); else $this->mMissingPageIDs = array_merge($this->mMissingPageIDs, array_keys($remaining)); @@ -448,16 +450,16 @@ class ApiPageSet extends ApiQueryBase { private function initFromRevIDs($revids) { - if(empty($revids)) + if(!count($revids)) return; $db = $this->getDB(); $pageids = array(); $remaining = array_flip($revids); - $tables = array('revision'); + $tables = array('revision','page'); $fields = array('rev_id','rev_page'); - $where = array('rev_deleted' => 0, 'rev_id' => $revids); + $where = array('rev_deleted' => 0, 'rev_id' => $revids,'rev_page = page_id'); // Get pageIDs data from the `page` table $this->profileDBIn(); @@ -475,8 +477,6 @@ class ApiPageSet extends ApiQueryBase { $this->mMissingRevIDs = array_keys($remaining); // Populate all the page information - if($this->mResolveRedirects) - ApiBase :: dieDebug(__METHOD__, 'revids may not be used with redirect resolution'); $this->initFromPageIds(array_keys($pageids)); } @@ -488,7 +488,7 @@ class ApiPageSet extends ApiQueryBase { // Repeat until all redirects have been resolved // The infinite loop is prevented by keeping all known pages in $this->mAllPages - while (!empty ($this->mPendingRedirectIDs)) { + while ($this->mPendingRedirectIDs) { // Resolve redirects by querying the pagelinks table, and repeat the process // Create a new linkBatch object for the next pass @@ -537,7 +537,7 @@ class ApiPageSet extends ApiQueryBase { $this->mRedirectTitles[$from] = $to; } $db->freeResult($res); - if(!empty($this->mPendingRedirectIDs)) + if($this->mPendingRedirectIDs) { # We found pages that aren't in the redirect table # Add them @@ -580,16 +580,16 @@ class ApiPageSet extends ApiQueryBase { continue; // There's nothing else we can do } $iw = $titleObj->getInterwiki(); - if (!empty($iw)) { + if (strval($iw) !== '') { // This title is an interwiki link. $this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw; } else { // Validation if ($titleObj->getNamespace() < 0) - $this->dieUsage("No support for special pages has been implemented", 'unsupportednamespace'); - - $linkBatch->addObj($titleObj); + $this->setWarning("No support for special pages has been implemented"); + else + $linkBatch->addObj($titleObj); } // Make sure we remember the original title that was given to us @@ -628,6 +628,6 @@ class ApiPageSet extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiPageSet.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiPageSet.php 45275 2009-01-01 02:02:03Z simetrical $'; } } diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 77ce514f..2cf044cf 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -86,12 +86,12 @@ class ApiParamInfo extends ApiBase { $retval['classname'] = get_class($obj); $retval['description'] = (is_array($obj->getDescription()) ? implode("\n", $obj->getDescription()) : $obj->getDescription()); $retval['prefix'] = $obj->getModulePrefix(); - $allowedParams = $obj->getAllowedParams(); + $allowedParams = $obj->getFinalParams(); if(!is_array($allowedParams)) return $retval; $retval['parameters'] = array(); - $paramDesc = $obj->getParamDescription(); - foreach($obj->getAllowedParams() as $n => $p) + $paramDesc = $obj->getFinalParamDescription(); + foreach($allowedParams as $n => $p) { $a = array('name' => $n); if(!is_array($p)) @@ -111,7 +111,15 @@ class ApiParamInfo extends ApiBase { $a['default'] = $p[ApiBase::PARAM_DFLT]; if(isset($p[ApiBase::PARAM_ISMULTI])) if($p[ApiBase::PARAM_ISMULTI]) + { $a['multi'] = ''; + $a['limit'] = $this->getMain()->canApiHighLimits() ? + ApiBase::LIMIT_SML2 : + ApiBase::LIMIT_SML1; + } + if(isset($p[ApiBase::PARAM_ALLOW_DUPLICATES])) + if($p[ApiBase::PARAM_ALLOW_DUPLICATES]) + $a['allowsduplicates'] = ''; if(isset($p[ApiBase::PARAM_TYPE])) { $a['type'] = $p[ApiBase::PARAM_TYPE]; @@ -161,6 +169,6 @@ class ApiParamInfo extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiParamInfo.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiParamInfo.php 41653 2008-10-04 15:03:03Z catrope $'; } } diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 4dcc94b6..e221fb1d 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -49,10 +49,13 @@ class ApiParse extends ApiBase { $prop = array_flip($params['prop']); $revid = false; - global $wgParser, $wgUser; + // The parser needs $wgTitle to be set, apparently the + // $title parameter in Parser::parse isn't enough *sigh* + global $wgParser, $wgUser, $wgTitle; $popts = new ParserOptions(); $popts->setTidy(true); $popts->enableLimitReport(); + $redirValues = null; if(!is_null($oldid) || !is_null($page)) { if(!is_null($oldid)) @@ -63,23 +66,42 @@ class ApiParse extends ApiBase { $this->dieUsage("There is no revision ID $oldid", 'missingrev'); if(!$rev->userCan(Revision::DELETED_TEXT)) $this->dieUsage("You don't have permission to view deleted revisions", 'permissiondenied'); - $text = $rev->getRawText(); + $text = $rev->getText( Revision::FOR_THIS_USER ); $titleObj = $rev->getTitle(); + $wgTitle = $titleObj; $p_result = $wgParser->parse($text, $titleObj, $popts); } else { - $titleObj = Title::newFromText($page); + if($params['redirects']) + { + $req = new FauxRequest(array( + 'action' => 'query', + 'redirects' => '', + 'titles' => $page + )); + $main = new ApiMain($req); + $main->execute(); + $data = $main->getResultData(); + $redirValues = @$data['query']['redirects']; + $to = $page; + foreach((array)$redirValues as $r) + $to = $r['to']; + } + else + $to = $page; + $titleObj = Title::newFromText($to); if(!$titleObj) $this->dieUsage("The page you specified doesn't exist", 'missingtitle'); - // Try the parser cache first $articleObj = new Article($titleObj); if(isset($prop['revid'])) $oldid = $articleObj->getRevIdFetched(); + // Try the parser cache first $pcache = ParserCache::singleton(); $p_result = $pcache->get($articleObj, $wgUser); - if(!$p_result) { + if(!$p_result) + { $p_result = $wgParser->parse($articleObj->getContent(), $titleObj, $popts); global $wgUseParserCache; if($wgUseParserCache) @@ -92,12 +114,25 @@ class ApiParse extends ApiBase { $titleObj = Title::newFromText($title); if(!$titleObj) $titleObj = Title::newFromText("API"); + $wgTitle = $titleObj; + if($params['pst'] || $params['onlypst']) + $text = $wgParser->preSaveTransform($text, $titleObj, $wgUser, $popts); + if($params['onlypst']) + { + // Build a result and bail out + $result_array['text'] = array(); + $this->getResult()->setContent($result_array['text'], $text); + $this->getResult()->addValue(null, $this->getModuleName(), $result_array); + return; + } $p_result = $wgParser->parse($text, $titleObj, $popts); } // Return result $result = $this->getResult(); $result_array = array(); + if($params['redirects'] && !is_null($redirValues)) + $result_array['redirects'] = $redirValues; if(isset($prop['text'])) { $result_array['text'] = array(); $result->setContent($result_array['text'], $p_result->getText()); @@ -120,6 +155,7 @@ class ApiParse extends ApiBase { $result_array['revid'] = $oldid; $result_mapping = array( + 'redirects' => 'r', 'langlinks' => 'll', 'categories' => 'cl', 'links' => 'pl', @@ -184,6 +220,7 @@ class ApiParse extends ApiBase { ), 'text' => null, 'page' => null, + 'redirects' => false, 'oldid' => null, 'prop' => array( ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid', @@ -199,19 +236,28 @@ class ApiParse extends ApiBase { 'sections', 'revid' ) - ) + ), + 'pst' => false, + 'onlypst' => false, ); } public function getParamDescription() { return array ( 'text' => 'Wikitext to parse', + 'redirects' => 'If the page parameter is set to a redirect, resolve it', 'title' => 'Title of page the text belongs to', 'page' => 'Parse the content of this page. Cannot be used together with text and title', 'oldid' => 'Parse the content of this revision. Overrides page', 'prop' => array('Which pieces of information to get.', 'NOTE: Section tree is only generated if there are more than 4 sections, or if the __TOC__ keyword is present' ), + 'pst' => array( 'Do a pre-save transform on the input before parsing it.', + 'Ignored if page or oldid is used.' + ), + 'onlypst' => array('Do a PST on the input, but don\'t parse it.', + 'Returns PSTed wikitext. Ignored if page or oldid is used.' + ), ); } @@ -226,6 +272,6 @@ class ApiParse extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiParse.php 36983 2008-07-03 15:01:50Z catrope $'; + return __CLASS__ . ': $Id: ApiParse.php 44858 2008-12-20 20:00:07Z catrope $'; } } diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php new file mode 100644 index 00000000..08de87b0 --- /dev/null +++ b/includes/api/ApiPatrol.php @@ -0,0 +1,99 @@ +<?php + +/* + * Created on Sep 2, 2008 + * + * API for MediaWiki 1.14+ + * + * Copyright (C) 2008 Soxred93 soxred93@gmail.com, + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + require_once ('ApiBase.php'); +} + +/** + * Allows user to patrol pages + * @ingroup API + */ +class ApiPatrol extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + /** + * Patrols the article or provides the reason the patrol failed. + */ + public function execute() { + global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol; + $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); + + if(!isset($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + if(!isset($params['rcid'])) + $this->dieUsageMsg(array('missingparam', 'rcid')); + if(!$wgUser->matchEditToken($params['token'])) + $this->dieUsageMsg(array('sessionfailure')); + + $rc = RecentChange::newFromID($params['rcid']); + if(!$rc instanceof RecentChange) + $this->dieUsageMsg(array('nosuchrcid', $params['rcid'])); + $retval = RecentChange::markPatrolled($params['rcid']); + + if($retval) + $this->dieUsageMsg(current($retval)); + + $result = array('rcid' => $rc->getAttribute('rc_id')); + ApiQueryBase::addTitleInfo($result, $rc->getTitle()); + $this->getResult()->addValue(null, $this->getModuleName(), $result); + } + + public function getAllowedParams() { + return array ( + 'token' => null, + 'rcid' => array( + ApiBase :: PARAM_TYPE => 'integer' + ), + ); + } + + public function getParamDescription() { + return array ( + 'token' => 'Patrol token obtained from list=recentchanges', + 'rcid' => 'Recentchanges ID to patrol', + ); + } + + public function getDescription() { + return array ( + 'Patrol a page or revision. ' + ); + } + + protected function getExamples() { + return array( + 'api.php?action=patrol&token=123abc&rcid=230672766' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiPatrol.php 42548 2008-10-25 14:04:43Z tstarling $'; + } +} diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index 30bcfdbc..522d02b2 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -37,7 +37,7 @@ class ApiProtect extends ApiBase { } public function execute() { - global $wgUser; + global $wgUser, $wgRestrictionTypes, $wgRestrictionLevels; $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); @@ -46,7 +46,7 @@ class ApiProtect extends ApiBase { $this->dieUsageMsg(array('missingparam', 'title')); if(!isset($params['token'])) $this->dieUsageMsg(array('missingparam', 'token')); - if(!isset($params['protections']) || empty($params['protections'])) + if(empty($params['protections'])) $this->dieUsageMsg(array('missingparam', 'protections')); if(!$wgUser->matchEditToken($params['token'])) @@ -57,25 +57,23 @@ class ApiProtect extends ApiBase { $this->dieUsageMsg(array('invalidtitle', $params['title'])); $errors = $titleObj->getUserPermissionsErrors('protect', $wgUser); - if(!empty($errors)) + if($errors) // We don't care about multiple errors, just report one of them $this->dieUsageMsg(current($errors)); - if(in_array($params['expiry'], array('infinite', 'indefinite', 'never'))) - $expiry = Block::infinity(); - else + $expiry = (array)$params['expiry']; + if(count($expiry) != count($params['protections'])) { - $expiry = strtotime($params['expiry']); - if($expiry < 0 || $expiry == false) - $this->dieUsageMsg(array('invalidexpiry')); - - $expiry = wfTimestamp(TS_MW, $expiry); - if($expiry < wfTimestampNow()) - $this->dieUsageMsg(array('pastexpiry')); + if(count($expiry) == 1) + $expiry = array_fill(0, count($params['protections']), $expiry[0]); + else + $this->dieUsageMsg(array('toofewexpiries', count($expiry), count($params['protections']))); } - + $protections = array(); - foreach($params['protections'] as $prot) + $expiryarray = array(); + $resultProtections = array(); + foreach($params['protections'] as $i => $prot) { $p = explode('=', $prot); $protections[$p[0]] = ($p[1] == 'all' ? '' : $p[1]); @@ -83,26 +81,45 @@ class ApiProtect extends ApiBase { $this->dieUsageMsg(array('create-titleexists')); if(!$titleObj->exists() && $p[0] != 'create') $this->dieUsageMsg(array('missingtitles-createonly')); + if(!in_array($p[0], $wgRestrictionTypes) && $p[0] != 'create') + $this->dieUsageMsg(array('protect-invalidaction', $p[0])); + if(!in_array($p[1], $wgRestrictionLevels) && $p[1] != 'all') + $this->dieUsageMsg(array('protect-invalidlevel', $p[1])); + + if(in_array($expiry[$i], array('infinite', 'indefinite', 'never'))) + $expiryarray[$p[0]] = Block::infinity(); + else + { + $exp = strtotime($expiry[$i]); + if($exp < 0 || $exp == false) + $this->dieUsageMsg(array('invalidexpiry', $expiry[$i])); + + $exp = wfTimestamp(TS_MW, $exp); + if($exp < wfTimestampNow()) + $this->dieUsageMsg(array('pastexpiry', $expiry[$i])); + $expiryarray[$p[0]] = $exp; + } + $resultProtections[] = array($p[0] => $protections[$p[0]], + 'expiry' => ($expiryarray[$p[0]] == Block::infinity() ? + 'infinite' : + wfTimestamp(TS_ISO_8601, $expiryarray[$p[0]]))); } + $cascade = $params['cascade']; if($titleObj->exists()) { $articleObj = new Article($titleObj); - $ok = $articleObj->updateRestrictions($protections, $params['reason'], $params['cascade'], $expiry); + $ok = $articleObj->updateRestrictions($protections, $params['reason'], $cascade, $expiryarray); } else - $ok = $titleObj->updateTitleProtection($protections['create'], $params['reason'], $expiry); + $ok = $titleObj->updateTitleProtection($protections['create'], $params['reason'], $expiryarray['create']); if(!$ok) // This is very weird. Maybe the article was deleted or the user was blocked/desysopped in the meantime? // Just throw an unknown error in this case, as it's very likely to be a race condition $this->dieUsageMsg(array()); $res = array('title' => $titleObj->getPrefixedText(), 'reason' => $params['reason']); - if($expiry == Block::infinity()) - $res['expiry'] = 'infinity'; - else - $res['expiry'] = wfTimestamp(TS_ISO_8601, $expiry); - - if($params['cascade']) + if($cascade) $res['cascade'] = ''; - $res['protections'] = $protections; + $res['protections'] = $resultProtections; + $this->getResult()->setIndexedTagName($res['protections'], 'protection'); $this->getResult()->addValue(null, $this->getModuleName(), $res); } @@ -115,7 +132,11 @@ class ApiProtect extends ApiBase { 'protections' => array( ApiBase :: PARAM_ISMULTI => true ), - 'expiry' => 'infinite', + 'expiry' => array( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_ALLOW_DUPLICATES => true, + ApiBase :: PARAM_DFLT => 'infinite', + ), 'reason' => '', 'cascade' => false ); @@ -123,12 +144,14 @@ class ApiProtect extends ApiBase { public function getParamDescription() { return array ( - 'title' => 'Title of the page you want to restore.', + 'title' => 'Title of the page you want to (un)protect.', 'token' => 'A protect token previously retrieved through prop=info', 'protections' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)', - 'expiry' => 'Expiry timestamp. If set to \'infinite\', \'indefinite\' or \'never\', the protection will never expire.', + 'expiry' => array('Expiry timestamps. If only one timestamp is set, it\'ll be used for all protections.', + 'Use \'infinite\', \'indefinite\' or \'never\', for a neverexpiring protection.'), 'reason' => 'Reason for (un)protecting (optional)', - 'cascade' => 'Enable cascading protection (i.e. protect pages included in this page)' + 'cascade' => array('Enable cascading protection (i.e. protect pages included in this page)', + 'Ignored if not all protection levels are \'sysop\' or \'protect\''), ); } @@ -140,12 +163,12 @@ class ApiProtect extends ApiBase { protected function getExamples() { return array ( - 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=sysop|move=sysop&cascade&expiry=20070901163000', + 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=sysop|move=sysop&cascade&expiry=20070901163000|never', 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=all|move=all&reason=Lifting%20restrictions' ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiProtect.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiProtect.php 44426 2008-12-10 22:39:41Z catrope $'; } } diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php new file mode 100644 index 00000000..d7202a46 --- /dev/null +++ b/includes/api/ApiPurge.php @@ -0,0 +1,106 @@ +<?php + +/* + * Created on Sep 2, 2008 + * + * API for MediaWiki 1.14+ + * + * Copyright (C) 2008 Chad Horohoe + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + require_once ('ApiBase.php'); +} + +/** + * API interface for page purging + * @ingroup API + */ +class ApiPurge extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + /** + * Purges the cache of a page + */ + public function execute() { + global $wgUser; + $params = $this->extractRequestParams(); + if(!$wgUser->isAllowed('purge')) + $this->dieUsageMsg(array('cantpurge')); + if(!isset($params['titles'])) + $this->dieUsageMsg(array('missingparam', 'titles')); + $result = array(); + foreach($params['titles'] as $t) { + $r = array(); + $title = Title::newFromText($t); + if(!$title instanceof Title) + { + $r['title'] = $t; + $r['invalid'] = ''; + $result[] = $r; + continue; + } + ApiQueryBase::addTitleInfo($r, $title); + if(!$title->exists()) + { + $r['missing'] = ''; + $result[] = $r; + continue; + } + $article = new Article($title); + $article->doPurge(); // Directly purge and skip the UI part of purge(). + $r['purged'] = ''; + $result[] = $r; + } + $this->getResult()->setIndexedTagName($result, 'page'); + $this->getResult()->addValue(null, $this->getModuleName(), $result); + } + + public function getAllowedParams() { + return array ( + 'titles' => array( + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + public function getParamDescription() { + return array ( + 'titles' => 'A list of titles', + ); + } + + public function getDescription() { + return array ( + 'Purge the cache for the given titles.' + ); + } + + protected function getExamples() { + return array( + 'api.php?action=purge&titles=Main_Page|API' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiPurge.php 41020 2008-09-19 00:21:03Z demon $'; + } +} diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index f4a2402f..45a5667a 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -56,6 +56,7 @@ class ApiQuery extends ApiBase { 'categories' => 'ApiQueryCategories', 'extlinks' => 'ApiQueryExternalLinks', 'categoryinfo' => 'ApiQueryCategoryInfo', + 'duplicatefiles' => 'ApiQueryDuplicateFiles', ); private $mQueryListModules = array ( @@ -75,6 +76,7 @@ class ApiQuery extends ApiBase { 'search' => 'ApiQuerySearch', 'usercontribs' => 'ApiQueryContributions', 'watchlist' => 'ApiQueryWatchlist', + 'watchlistraw' => 'ApiQueryWatchlistRaw', 'exturlusage' => 'ApiQueryExtLinksUsage', 'users' => 'ApiQueryUsers', 'random' => 'ApiQueryRandom', @@ -93,10 +95,10 @@ class ApiQuery extends ApiBase { parent :: __construct($main, $action); // Allow custom modules to be added in LocalSettings.php - global $wgApiQueryPropModules, $wgApiQueryListModules, $wgApiQueryMetaModules; - self :: appendUserModules($this->mQueryPropModules, $wgApiQueryPropModules); - self :: appendUserModules($this->mQueryListModules, $wgApiQueryListModules); - self :: appendUserModules($this->mQueryMetaModules, $wgApiQueryMetaModules); + global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules; + self :: appendUserModules($this->mQueryPropModules, $wgAPIPropModules); + self :: appendUserModules($this->mQueryListModules, $wgAPIListModules); + self :: appendUserModules($this->mQueryMetaModules, $wgAPIMetaModules); $this->mPropModuleNames = array_keys($this->mQueryPropModules); $this->mListModuleNames = array_keys($this->mQueryListModules); @@ -209,6 +211,7 @@ class ApiQuery extends ApiBase { foreach ($modules as $module) { $module->profileIn(); $module->execute(); + wfRunHooks('APIQueryAfterExecute', array(&$module)); $module->profileOut(); } } @@ -229,8 +232,8 @@ class ApiQuery extends ApiBase { * Create instances of all modules requested by the client */ private function InstantiateModules(&$modules, $param, $moduleList) { - $list = $this->params[$param]; - if (isset ($list)) + $list = @$this->params[$param]; + if (!is_null ($list)) foreach ($list as $moduleName) $modules[] = new $moduleList[$moduleName] ($this, $moduleName); } @@ -253,7 +256,7 @@ class ApiQuery extends ApiBase { ); } - if (!empty ($normValues)) { + if (count($normValues)) { $result->setIndexedTagName($normValues, 'n'); $result->addValue('query', 'normalized', $normValues); } @@ -267,7 +270,7 @@ class ApiQuery extends ApiBase { ); } - if (!empty ($intrwValues)) { + if (count($intrwValues)) { $result->setIndexedTagName($intrwValues, 'i'); $result->addValue('query', 'interwiki', $intrwValues); } @@ -281,7 +284,7 @@ class ApiQuery extends ApiBase { ); } - if (!empty ($redirValues)) { + if (count($redirValues)) { $result->setIndexedTagName($redirValues, 'r'); $result->addValue('query', 'redirects', $redirValues); } @@ -290,7 +293,7 @@ class ApiQuery extends ApiBase { // Missing revision elements // $missingRevIDs = $pageSet->getMissingRevisionIDs(); - if (!empty ($missingRevIDs)) { + if (count($missingRevIDs)) { $revids = array (); foreach ($missingRevIDs as $revid) { $revids[$revid] = array ( @@ -332,7 +335,7 @@ class ApiQuery extends ApiBase { $pages[$pageid] = $vals; } - if (!empty ($pages)) { + if (count($pages)) { if ($this->params['indexpageids']) { $pageIDs = array_keys($pages); @@ -381,6 +384,7 @@ class ApiQuery extends ApiBase { // populate resultPageSet with the generator output $generator->profileIn(); $generator->executeGenerator($resultPageSet); + wfRunHooks('APIQueryGeneratorAfterExecute', array(&$generator, &$resultPageSet)); $resultPageSet->finishPageSetGeneration(); $generator->profileOut(); @@ -476,7 +480,6 @@ class ApiQuery extends ApiBase { return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters(); } - // @todo should work correctly public function shouldCheckMaxlag() { return true; } @@ -509,7 +512,7 @@ class ApiQuery extends ApiBase { public function getVersion() { $psModule = new ApiPageSet($this); $vers = array (); - $vers[] = __CLASS__ . ': $Id: ApiQuery.php 35098 2008-05-20 17:13:28Z ialex $'; + $vers[] = __CLASS__ . ': $Id: ApiQuery.php 42548 2008-10-25 14:04:43Z tstarling $'; $vers[] = $psModule->getVersion(); return $vers; } diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 3ff42c88..e6287eea 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -56,17 +56,30 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $this->addTables('category'); $this->addFields('cat_title'); - if (!is_null($params['from'])) - $this->addWhere('cat_title>=' . $db->addQuotes($this->titleToKey($params['from']))); + $dir = ($params['dir'] == 'descending' ? 'older' : 'newer'); + $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from'])); + $this->addWhereRange('cat_title', $dir, $from, null); if (isset ($params['prefix'])) - $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'"); + $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'"); $this->addOption('LIMIT', $params['limit']+1); $this->addOption('ORDER BY', 'cat_title' . ($params['dir'] == 'descending' ? ' DESC' : '')); $prop = array_flip($params['prop']); $this->addFieldsIf( array( 'cat_pages', 'cat_subcats', 'cat_files' ), isset($prop['size']) ); - $this->addFieldsIf( 'cat_hidden', isset($prop['hidden']) ); + if(isset($prop['hidden'])) + { + $this->addTables(array('page', 'page_props')); + $this->addJoinConds(array( + 'page' => array('LEFT JOIN', array( + 'page_namespace' => NS_CATEGORY, + 'page_title=cat_title')), + 'page_props' => array('LEFT JOIN', array( + 'pp_page=page_id', + 'pp_propname' => 'hiddencat')), + )); + $this->addFields('pp_propname AS cat_hidden'); + } $res = $this->select(__METHOD__); @@ -158,6 +171,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllCategories.php 36790 2008-06-29 22:26:23Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllCategories.php 44590 2008-12-14 20:24:23Z catrope $'; } } diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index aefbb725..9ad34aa2 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -74,30 +74,30 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $arr = explode('|', $params['continue']); if(count($arr) != 2) $this->dieUsage("Invalid continue parameter", 'badcontinue'); - $params['from'] = $arr[0]; // Handled later + $from = $this->getDB()->strencode($this->titleToKey($arr[0])); $id = intval($arr[1]); - $this->addWhere("pl_from >= $id"); + $this->addWhere("pl_title > '$from' OR " . + "(pl_title = '$from' AND " . + "pl_from > $id)"); } if (!is_null($params['from'])) - $this->addWhere('pl_title>=' . $db->addQuotes($this->titleToKey($params['from']))); + $this->addWhere('pl_title>=' . $db->addQuotes($this->titlePartToKey($params['from']))); if (isset ($params['prefix'])) - $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'"); + $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'"); $this->addFields(array ( - 'pl_namespace', 'pl_title', - 'pl_from' )); + $this->addFieldsIf('pl_from', !$params['unique']); $this->addOption('USE INDEX', 'pl_namespace'); $limit = $params['limit']; $this->addOption('LIMIT', $limit+1); - # Only order by pl_namespace if it isn't constant in the WHERE clause - if(count($params['namespace']) != 1) - $this->addOption('ORDER BY', 'pl_namespace, pl_title'); - else + if($params['unique']) $this->addOption('ORDER BY', 'pl_title'); + else + $this->addOption('ORDER BY', 'pl_title, pl_from'); $res = $this->select(__METHOD__); @@ -107,7 +107,10 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter('continue', $this->keyToTitle($row->pl_title) . "|" . $row->pl_from); + if($params['unique']) + $this->setContinueEnumParameter('from', $this->keyToTitle($row->pl_title)); + else + $this->setContinueEnumParameter('continue', $this->keyToTitle($row->pl_title) . "|" . $row->pl_from); break; } @@ -116,7 +119,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { if ($fld_ids) $vals['fromid'] = intval($row->pl_from); if ($fld_title) { - $title = Title :: makeTitle($row->pl_namespace, $row->pl_title); + $title = Title :: makeTitle($params['namespace'], $row->pl_title); $vals['ns'] = intval($title->getNamespace()); $vals['title'] = $title->getPrefixedText(); } @@ -187,6 +190,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllLinks.php 37258 2008-07-07 14:48:40Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllLinks.php 45850 2009-01-17 20:03:25Z catrope $'; } } diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index dd0e98a8..8395808b 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -121,7 +121,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $row = $db->fetchObject($res); $count++; - if (!$row || $lastUser != $row->user_name) { + if (!$row || $lastUser !== $row->user_name) { // Save the last pass's user data if (is_array($lastUserData)) $data[] = $lastUserData; @@ -219,6 +219,6 @@ class ApiQueryAllUsers extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllUsers.php 36790 2008-06-29 22:26:23Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllUsers.php 44472 2008-12-11 21:51:01Z catrope $'; } } diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php index 26cbc368..ea83c667 100644 --- a/includes/api/ApiQueryAllimages.php +++ b/includes/api/ApiQueryAllimages.php @@ -61,10 +61,11 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); // Image filters - if (!is_null($params['from'])) - $this->addWhere('img_name>=' . $db->addQuotes($this->titleToKey($params['from']))); + $dir = ($params['dir'] == 'descending' ? 'older' : 'newer'); + $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from'])); + $this->addWhereRange('img_name', $dir, $from, null); if (isset ($params['prefix'])) - $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'"); + $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'"); if (isset ($params['minsize'])) { $this->addWhere('img_size>=' . intval($params['minsize'])); @@ -109,10 +110,10 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase { if (is_null($resultPageSet)) { $file = $repo->newFileFromRow( $row ); - - $data[] = ApiQueryImageInfo::getInfo( $file, $prop, $result ); + $data[] = array_merge(array('name' => $row->img_name), + ApiQueryImageInfo::getInfo($file, $prop, $result)); } else { - $data[] = Title::makeTitle( NS_IMAGE, $row->img_name ); + $data[] = Title::makeTitle(NS_FILE, $row->img_name); } } $db->freeResult($res); @@ -162,7 +163,8 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase { 'dimensions', // Obsolete 'mime', 'sha1', - 'metadata' + 'metadata', + 'bitdepth', ), ApiBase :: PARAM_DFLT => 'timestamp|url', ApiBase :: PARAM_ISMULTI => true @@ -200,6 +202,6 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllimages.php 37909 2008-07-22 13:26:15Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllimages.php 44121 2008-12-01 17:14:30Z vyznev $'; } } diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index 39490fe7..531fa02a 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -62,11 +62,21 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $this->addWhereIf('page_is_redirect = 0', $params['filterredir'] === 'nonredirects'); $this->addWhereFld('page_namespace', $params['namespace']); $dir = ($params['dir'] == 'descending' ? 'older' : 'newer'); - $from = (is_null($params['from']) ? null : $this->titleToKey($params['from'])); + $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from'])); $this->addWhereRange('page_title', $dir, $from, null); if (isset ($params['prefix'])) - $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'"); + $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'"); + if (is_null($resultPageSet)) { + $selectFields = array ( + 'page_namespace', + 'page_title', + 'page_id' + ); + } else { + $selectFields = $resultPageSet->getPageTableFields(); + } + $this->addFields($selectFields); $forceNameTitleIndex = true; if (isset ($params['minsize'])) { $this->addWhere('page_len>=' . intval($params['minsize'])); @@ -79,15 +89,20 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } // Page protection filtering - if (isset ($params['prtype'])) { + if (!empty ($params['prtype'])) { $this->addTables('page_restrictions'); $this->addWhere('page_id=pr_page'); $this->addWhere('pr_expiry>' . $db->addQuotes($db->timestamp())); $this->addWhereFld('pr_type', $params['prtype']); - $prlevel = $params['prlevel']; - if (!is_null($prlevel) && $prlevel != '' && $prlevel != '*') + // Remove the empty string and '*' from the prlevel array + $prlevel = array_diff($params['prlevel'], array('', '*')); + if (!empty($prlevel)) $this->addWhereFld('pr_level', $prlevel); + if ($params['prfiltercascade'] == 'cascading') + $this->addWhereFld('pr_cascade', 1); + if ($params['prfiltercascade'] == 'noncascading') + $this->addWhereFld('pr_cascade', 0); $this->addOption('DISTINCT'); @@ -105,20 +120,16 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } else if($params['filterlanglinks'] == 'withlanglinks') { $this->addTables('langlinks'); $this->addWhere('page_id=ll_from'); + $this->addOption('STRAIGHT_JOIN'); + // We have to GROUP BY all selected fields to stop + // PostgreSQL from whining + $this->addOption('GROUP BY', implode(', ', $selectFields)); $forceNameTitleIndex = false; } if ($forceNameTitleIndex) $this->addOption('USE INDEX', 'name_title'); - if (is_null($resultPageSet)) { - $this->addFields(array ( - 'page_id', - 'page_namespace', - 'page_title' - )); - } else { - $this->addFields($resultPageSet->getPageTableFields()); - } + $limit = $params['limit']; $this->addOption('LIMIT', $limit+1); @@ -185,6 +196,14 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { ApiBase :: PARAM_TYPE => $wgRestrictionLevels, ApiBase :: PARAM_ISMULTI => true ), + 'prfiltercascade' => array ( + ApiBase :: PARAM_DFLT => 'all', + ApiBase :: PARAM_TYPE => array ( + 'cascading', + 'noncascading', + 'all' + ), + ), 'limit' => array ( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', @@ -221,6 +240,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { 'maxsize' => 'Limit to pages with at most this many bytes', 'prtype' => 'Limit to protected pages only', 'prlevel' => 'The protection level (must be used with apprtype= parameter)', + 'prfiltercascade' => 'Filter protections based on cascadingness (ignored when apprtype isn\'t set)', 'filterlanglinks' => 'Filter based on whether a page has langlinks', 'limit' => 'How many total pages to return.' ); @@ -244,6 +264,6 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllpages.php 37775 2008-07-17 09:26:01Z brion $'; + return __CLASS__ . ': $Id: ApiQueryAllpages.php 44863 2008-12-20 23:54:04Z catrope $'; } } diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index fea058f3..f67e0044 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -60,7 +60,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ); public function __construct($query, $moduleName) { - $code = $prefix = $linktbl = null; extract($this->backlinksSettings[$moduleName]); parent :: __construct($query, $moduleName, $code); @@ -100,7 +99,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { * AND pl_title='Foo' AND pl_namespace=0 * LIMIT 11 ORDER BY pl_from */ - $db = $this->getDb(); + $db = $this->getDB(); $this->addTables(array('page', $this->bl_table)); $this->addWhere("{$this->bl_from}=page_id"); if(is_null($resultPageSet)) @@ -108,12 +107,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { else $this->addFields($resultPageSet->getPageTableFields()); $this->addFields('page_is_redirect'); - $this->addWhereFld($this->bl_title, $this->rootTitle->getDbKey()); + $this->addWhereFld($this->bl_title, $this->rootTitle->getDBKey()); if($this->hasNS) $this->addWhereFld($this->bl_ns, $this->rootTitle->getNamespace()); $this->addWhereFld('page_namespace', $this->params['namespace']); if(!is_null($this->contID)) - $this->addWhere("page_id>={$this->contID}"); + $this->addWhere("{$this->bl_from}>={$this->contID}"); if($this->params['filterredir'] == 'redirects') $this->addWhereFld('page_is_redirect', 1); if($this->params['filterredir'] == 'nonredirects') @@ -124,11 +123,11 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { private function prepareSecondQuery($resultPageSet = null) { /* SELECT page_id, page_title, page_namespace, page_is_redirect, pl_title, pl_namespace - * FROM pagelinks, page WHERE pl_from=page_id - * AND (pl_title='Foo' AND pl_namespace=0) OR (pl_title='Bar' AND pl_namespace=1) - * LIMIT 11 ORDER BY pl_namespace, pl_title, pl_from + FROM pagelinks, page WHERE pl_from=page_id + AND (pl_title='Foo' AND pl_namespace=0) OR (pl_title='Bar' AND pl_namespace=1) + ORDER BY pl_namespace, pl_title, pl_from LIMIT 11 */ - $db = $this->getDb(); + $db = $this->getDB(); $this->addTables(array('page', $this->bl_table)); $this->addWhere("{$this->bl_from}=page_id"); if(is_null($resultPageSet)) @@ -138,16 +137,31 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->addFields($this->bl_title); if($this->hasNS) $this->addFields($this->bl_ns); - $titleWhere = ''; + $titleWhere = array(); foreach($this->redirTitles as $t) - $titleWhere .= ($titleWhere != '' ? " OR " : '') . - "({$this->bl_title} = ".$db->addQuotes($t->getDBKey()). + $titleWhere[] = "({$this->bl_title} = ".$db->addQuotes($t->getDBKey()). ($this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : "") . ")"; - $this->addWhere($titleWhere); + $this->addWhere($db->makeList($titleWhere, LIST_OR)); $this->addWhereFld('page_namespace', $this->params['namespace']); if(!is_null($this->redirID)) - $this->addWhere("page_id>={$this->redirID}"); + { + $first = $this->redirTitles[0]; + $title = $db->strencode($first->getDBKey()); + $ns = $first->getNamespace(); + $from = $this->redirID; + if($this->hasNS) + $this->addWhere("{$this->bl_ns} > $ns OR ". + "({$this->bl_ns} = $ns AND ". + "({$this->bl_title} > '$title' OR ". + "({$this->bl_title} = '$title' AND ". + "{$this->bl_from} >= $from)))"); + else + $this->addWhere("{$this->bl_title} > '$title' OR ". + "({$this->bl_title} = '$title' AND ". + "{$this->bl_from} >= $from)"); + + } if($this->params['filterredir'] == 'redirects') $this->addWhereFld('page_is_redirect', 1); if($this->params['filterredir'] == 'nonredirects') @@ -170,7 +184,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->prepareFirstQuery($resultPageSet); $db = $this->getDB(); - $res = $this->select(__METHOD__); + $res = $this->select(__METHOD__.'::firstQuery'); $count = 0; $this->data = array (); @@ -195,11 +209,11 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } $db->freeResult($res); - if($this->redirect && !empty($this->redirTitles)) + if($this->redirect && count($this->redirTitles)) { $this->resetQueryParams(); $this->prepareSecondQuery($resultPageSet); - $res = $this->select(__METHOD__); + $res = $this->select(__METHOD__.'::secondQuery'); $count = 0; while($row = $db->fetchObject($res)) { @@ -210,7 +224,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if($this->hasNS) $contTitle = Title::makeTitle($row->{$this->bl_ns}, $row->{$this->bl_title}); else - $contTitle = Title::makeTitle(NS_IMAGE, $row->{$this->bl_title}); + $contTitle = Title::makeTitle(NS_FILE, $row->{$this->bl_title}); $this->continueStr = $this->getContinueRedirStr($contTitle->getArticleID(), $row->page_id); break; } @@ -229,7 +243,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $resultData = array(); foreach($this->data as $ns => $a) foreach($a as $title => $arr) - $resultData[$arr['pageid']] = $arr; + $resultData[] = $arr; $result = $this->getResult(); $result->setIndexedTagName($resultData, $this->bl_code); $result->addValue('query', $this->getModuleName(), $resultData); @@ -254,7 +268,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ApiQueryBase::addTitleInfo($a, Title::makeTitle($row->page_namespace, $row->page_title)); if($row->page_is_redirect) $a['redirect'] = ''; - $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_IMAGE; + $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_FILE; $this->data[$ns][$row->{$this->bl_title}]['redirlinks'][] = $a; $this->getResult()->setIndexedTagName($this->data[$ns][$row->{$this->bl_title}]['redirlinks'], $this->bl_code); } @@ -276,7 +290,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } // only image titles are allowed for the root in imageinfo mode - if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_IMAGE) + if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE) $this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title'); } @@ -399,6 +413,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryBacklinks.php 37504 2008-07-10 14:28:09Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryBacklinks.php 46135 2009-01-24 13:03:40Z catrope $'; } } diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index f392186b..896dd00c 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -126,13 +126,19 @@ abstract class ApiQueryBase extends ApiBase { * Clauses can be formatted as 'foo=bar' or array('foo' => 'bar'), * the latter only works if the value is a constant (i.e. not another field) * + * If $value is an empty array, this function does nothing. + * * For example, array('foo=bar', 'baz' => 3, 'bla' => 'foo') translates * to "foo=bar AND baz='3' AND bla='foo'" * @param mixed $value String or array */ protected function addWhere($value) { - if (is_array($value)) - $this->where = array_merge($this->where, $value); + if (is_array($value)) { + // Sanity check: don't insert empty arrays, + // Database::makeList() chokes on them + if ( count( $value ) ) + $this->where = array_merge($this->where, $value); + } else $this->where[] = $value; } @@ -154,10 +160,12 @@ abstract class ApiQueryBase extends ApiBase { /** * Equivalent to addWhere(array($field => $value)) * @param string $field Field name - * @param string $value Value; ignored if nul; + * @param string $value Value; ignored if null or empty array; */ protected function addWhereFld($field, $value) { - if (!is_null($value)) + // Use count() to its full documented capabilities to simultaneously + // test for null, empty array or empty countable object + if ( count( $value ) ) $this->where[$field] = $value; } @@ -236,7 +244,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Add information (title and namespace) about a Title object to a result array - * @param array $arr Result array la ApiResult + * @param array $arr Result array à la ApiResult * @param Title $title Title object * @param string $prefix Module prefix */ @@ -264,7 +272,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a sub-element under the page element with the given page ID * @param int $pageId Page ID - * @param array $data Data array la ApiResult + * @param array $data Data array à la ApiResult */ protected function addPageSubItems($pageId, $data) { $result = $this->getResult(); @@ -324,10 +332,13 @@ abstract class ApiQueryBase extends ApiBase { * @return string Page title with underscores */ public function titleToKey($title) { + # Don't throw an error if we got an empty string + if(trim($title) == '') + return ''; $t = Title::newFromText($title); if(!$t) $this->dieUsageMsg(array('invalidtitle', $title)); - return $t->getDbKey(); + return $t->getPrefixedDbKey(); } /** @@ -336,19 +347,40 @@ abstract class ApiQueryBase extends ApiBase { * @return string Page title with spaces */ public function keyToTitle($key) { + # Don't throw an error if we got an empty string + if(trim($key) == '') + return ''; $t = Title::newFromDbKey($key); # This really shouldn't happen but we gotta check anyway if(!$t) $this->dieUsageMsg(array('invalidtitle', $key)); return $t->getPrefixedText(); } + + /** + * An alternative to titleToKey() that doesn't trim trailing spaces + * @param string $titlePart Title part with spaces + * @return string Title part with underscores + */ + public function titlePartToKey($titlePart) { + return substr($this->titleToKey($titlePart . 'x'), 0, -1); + } + + /** + * An alternative to keyToTitle() that doesn't trim trailing spaces + * @param string $keyPart Key part with spaces + * @return string Key part with underscores + */ + public function keyPartToTitle($keyPart) { + return substr($this->keyToTitle($keyPart . 'x'), 0, -1); + } /** * Get version string for use in the API help output * @return string */ public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiQueryBase.php 37083 2008-07-05 11:18:50Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryBase.php 44461 2008-12-11 19:11:11Z ialex $'; } } diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index ebe87908..6f356cea 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -42,10 +42,6 @@ class ApiQueryBlocks extends ApiQueryBase { } public function execute() { - $this->run(); - } - - private function run() { global $wgUser; $params = $this->extractRequestParams(); @@ -87,17 +83,17 @@ class ApiQueryBlocks extends ApiQueryBase { if($fld_range) $this->addFields(array('ipb_range_start', 'ipb_range_end')); if($fld_flags) - $this->addFields(array('ipb_auto', 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', 'ipb_block_email', 'ipb_deleted')); + $this->addFields(array('ipb_auto', 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk')); $this->addOption('LIMIT', $params['limit'] + 1); $this->addWhereRange('ipb_timestamp', $params['dir'], $params['start'], $params['end']); if(isset($params['ids'])) - $this->addWhere(array('ipb_id' => $params['ids'])); + $this->addWhereFld('ipb_id', $params['ids']); if(isset($params['users'])) { foreach((array)$params['users'] as $u) $this->prepareUsername($u); - $this->addWhere(array('ipb_address' => $this->usernames)); + $this->addWhereFld('ipb_address', $this->usernames); } if(isset($params['ip'])) { @@ -120,19 +116,18 @@ class ApiQueryBlocks extends ApiQueryBase { )); } if(!$wgUser->isAllowed('suppress')) - $this->addWhere(array('ipb_deleted' => 0)); + $this->addWhereFld('ipb_deleted', 0); // Purge expired entries on one in every 10 queries if(!mt_rand(0, 10)) Block::purgeExpired(); $res = $this->select(__METHOD__); - $db = wfGetDB(); $count = 0; - while($row = $db->fetchObject($res)) + while($row = $res->fetchObject()) { - if($count++ == $params['limit']) + if(++$count > $params['limit']) { // We've had enough $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ipb_timestamp)); @@ -142,13 +137,9 @@ class ApiQueryBlocks extends ApiQueryBase { if($fld_id) $block['id'] = $row->ipb_id; if($fld_user && !$row->ipb_auto) - { $block['user'] = $row->ipb_address; - } if($fld_by) - { $block['by'] = $row->user_name; - } if($fld_timestamp) $block['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ipb_timestamp); if($fld_expiry) @@ -157,8 +148,8 @@ class ApiQueryBlocks extends ApiQueryBase { $block['reason'] = $row->ipb_reason; if($fld_range) { - $block['rangestart'] = $this->convertHexIP($row->ipb_range_start); - $block['rangeend'] = $this->convertHexIP($row->ipb_range_end); + $block['rangestart'] = IP::hexToQuad($row->ipb_range_start); + $block['rangeend'] = IP::hexToQuad($row->ipb_range_end); } if($fld_flags) { @@ -175,6 +166,8 @@ class ApiQueryBlocks extends ApiQueryBase { $block['noemail'] = ''; if($row->ipb_deleted) $block['hidden'] = ''; + if($row->ipb_allow_usertalk) + $block['allowusertalk'] = ''; } $data[] = $block; } @@ -194,19 +187,6 @@ class ApiQueryBlocks extends ApiQueryBase { $this->usernames[] = $name; } - protected function convertHexIP($ip) - { - // Converts a hexadecimal IP to nnn.nnn.nnn.nnn format - $dec = wfBaseConvert($ip, 16, 10); - $parts[0] = (int)($dec / (256*256*256)); - $dec %= 256*256*256; - $parts[1] = (int)($dec / (256*256)); - $dec %= 256*256; - $parts[2] = (int)($dec / 256); - $parts[3] = $dec % 256; - return implode('.', $parts); - } - public function getAllowedParams() { return array ( 'start' => array( @@ -279,6 +259,6 @@ class ApiQueryBlocks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryBlocks.php 37892 2008-07-21 21:37:11Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryBlocks.php 43676 2008-11-18 15:11:11Z catrope $'; } } diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 51492d63..9c4e9b41 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -54,6 +54,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $prop = $params['prop']; + $show = array_flip((array)$params['show']); $this->addFields(array ( 'cl_from', @@ -86,11 +87,31 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->dieUsage("Invalid continue param. You should pass the " . "original value returned by the previous query", "_badcontinue"); $clfrom = intval($cont[0]); - $clto = $this->getDb()->strencode($this->titleToKey($cont[1])); + $clto = $this->getDB()->strencode($this->titleToKey($cont[1])); $this->addWhere("cl_from > $clfrom OR ". "(cl_from = $clfrom AND ". "cl_to >= '$clto')"); } + if(isset($show['hidden']) && isset($show['!hidden'])) + $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); + if(isset($show['hidden']) || isset($show['!hidden'])) + { + $this->addOption('STRAIGHT_JOIN'); + $this->addTables(array('page', 'page_props')); + $this->addJoinConds(array( + 'page' => array('LEFT JOIN', array( + 'page_namespace' => NS_CATEGORY, + 'page_title = cl_to')), + 'page_props' => array('LEFT JOIN', array( + 'pp_page=page_id', + 'pp_propname' => 'hiddencat')) + )); + if(isset($show['hidden'])) + $this->addWhere(array('pp_propname IS NOT NULL')); + else + $this->addWhere(array('pp_propname IS NULL')); + } + # Don't order by cl_from if it's constant in the WHERE clause if(count($this->getPageSet()->getGoodTitles()) == 1) $this->addOption('ORDER BY', 'cl_to'); @@ -128,7 +149,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if ($fld_sortkey) $vals['sortkey'] = $row->cl_sortkey; if ($fld_timestamp) - $vals['timestamp'] = $row->cl_timestamp; + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp); $data[] = $vals; } @@ -166,6 +187,13 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { 'timestamp', ) ), + 'show' => array( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array( + 'hidden', + '!hidden', + ) + ), 'limit' => array( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', @@ -181,6 +209,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { return array ( 'prop' => 'Which additional properties to get for each category.', 'limit' => 'How many categories to return', + 'show' => 'Which kind of categories to show', 'continue' => 'When more results are available, use this to continue', ); } @@ -199,6 +228,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategories.php 37909 2008-07-22 13:26:15Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryCategories.php 44585 2008-12-14 17:39:50Z catrope $'; } } diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php index f809bb15..f83c4a5b 100644 --- a/includes/api/ApiQueryCategoryInfo.php +++ b/includes/api/ApiQueryCategoryInfo.php @@ -41,9 +41,10 @@ class ApiQueryCategoryInfo extends ApiQueryBase { public function execute() { $alltitles = $this->getPageSet()->getAllTitlesByNamespace(); - $categories = $alltitles[NS_CATEGORY]; - if(empty($categories)) + if ( empty( $alltitles[NS_CATEGORY] ) ) { return; + } + $categories = $alltitles[NS_CATEGORY]; $titles = $this->getPageSet()->getGoodTitles() + $this->getPageSet()->getMissingTitles(); @@ -51,11 +52,19 @@ class ApiQueryCategoryInfo extends ApiQueryBase { foreach($categories as $c) { $t = $titles[$c]; - $cattitles[$c] = $t->getDbKey(); + $cattitles[$c] = $t->getDBKey(); } - $this->addTables('category'); - $this->addFields(array('cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'cat_hidden')); + $this->addTables(array('category', 'page', 'page_props')); + $this->addJoinConds(array( + 'page' => array('LEFT JOIN', array( + 'page_namespace' => NS_CATEGORY, + 'page_title=cat_title')), + 'page_props' => array('LEFT JOIN', array( + 'pp_page=page_id', + 'pp_propname' => 'hiddencat')), + )); + $this->addFields(array('cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'pp_propname AS cat_hidden')); $this->addWhere(array('cat_title' => $cattitles)); $db = $this->getDB(); @@ -86,6 +95,6 @@ class ApiQueryCategoryInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 37504 2008-07-10 14:28:09Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 44590 2008-12-14 20:24:23Z catrope $'; } } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 3909b213..e2f577a2 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -76,17 +76,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addTables(array('page','categorylinks')); // must be in this order for 'USE INDEX' // Not needed after bug 10280 is applied to servers if($params['sort'] == 'timestamp') - { $this->addOption('USE INDEX', 'cl_timestamp'); - // cl_timestamp will be added by addWhereRange() later - $this->addOption('ORDER BY', 'cl_to'); - } else - { - $dir = ($params['dir'] == 'desc' ? ' DESC' : ''); $this->addOption('USE INDEX', 'cl_sortkey'); - $this->addOption('ORDER BY', 'cl_to, cl_sortkey' . $dir . ', cl_from' . $dir); - } $this->addWhere('cl_from=page_id'); $this->setContinuation($params['continue'], $params['dir']); @@ -94,6 +86,11 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addWhereFld('page_namespace', $params['namespace']); if($params['sort'] == 'timestamp') $this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']); + else + { + $this->addWhereRange('cl_sortkey', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['startsortkey'], $params['endsortkey']); + $this->addWhereRange('cl_from', ($params['dir'] == 'asc' ? 'newer' : 'older'), null, null); + } $limit = $params['limit']; $this->addOption('LIMIT', $limit +1); @@ -157,18 +154,15 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { if (is_null($continue)) return; // This is not a continuation request - $continueList = explode('|', $continue); - $hasError = count($continueList) != 2; - $from = 0; - if (!$hasError && strlen($continueList[1]) > 0) { - $from = intval($continueList[1]); - $hasError = ($from == 0); - } + $pos = strrpos($continue, '|'); + $sortkey = substr($continue, 0, $pos); + $fromstr = substr($continue, $pos + 1); + $from = intval($fromstr); - if ($hasError) + if ($from == 0 && strlen($fromstr) > 0) $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "badcontinue"); - $encSortKey = $this->getDB()->addQuotes($continueList[0]); + $encSortKey = $this->getDB()->addQuotes($sortkey); $encFrom = $this->getDB()->addQuotes($from); $op = ($dir == 'desc' ? '<' : '>'); @@ -225,7 +219,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { ), 'end' => array( ApiBase :: PARAM_TYPE => 'timestamp' - ) + ), + 'startsortkey' => null, + 'endsortkey' => null, ); } @@ -238,6 +234,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'dir' => 'In which direction to sort', 'start' => 'Timestamp to start listing from. Can only be used with cmsort=timestamp', 'end' => 'Timestamp to end listing at. Can only be used with cmsort=timestamp', + 'startsortkey' => 'Sortkey to start listing from. Can only be used with cmsort=sortkey', + 'endsortkey' => 'Sortkey to end listing at. Can only be used with cmsort=sortkey', 'continue' => 'For large categories, give the value retured from previous query', 'limit' => 'The maximum number of pages to return.', ); @@ -257,6 +255,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 42197 2008-10-18 10:09:19Z ialex $'; } } diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 8368896d..408421c4 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -107,6 +107,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $lb = new LinkBatch($titles); $where = $lb->constructSet('ar', $db); $this->addWhere($where); + } else { + $this->dieUsage('You have to specify a page title or titles'); } $this->addOption('LIMIT', $limit + 1); @@ -228,6 +230,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 37502 2008-07-10 14:13:11Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 40798 2008-09-13 20:41:58Z aaron $'; } } diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php new file mode 100644 index 00000000..50825464 --- /dev/null +++ b/includes/api/ApiQueryDisabled.php @@ -0,0 +1,72 @@ +<?php + +/* + * Created on Sep 25, 2008 + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + + +/** + * API module that does nothing + * + * Use this to disable core modules with e.g. + * $wgAPIPropModules['modulename'] = 'ApiQueryDisabled'; + * + * To disable top-level modules, use ApiDisabled instead + * + * @ingroup API + */ +class ApiQueryDisabled extends ApiQueryBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + $this->setWarning("The ``{$this->getModuleName()}'' module has been disabled."); + } + + public function getAllowedParams() { + return array (); + } + + public function getParamDescription() { + return array (); + } + + public function getDescription() { + return array( + 'This module has been disabled.' + ); + } + + protected function getExamples() { + return array (); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryDisabled.php 41268 2008-09-25 20:50:50Z catrope $'; + } +} diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php new file mode 100644 index 00000000..5f7d7ee0 --- /dev/null +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -0,0 +1,164 @@ +<?php + +/* + * Created on Sep 27, 2008 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Roan Kattow <Firstname>,<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiQueryBase.php"); +} + +/** + * A query module to list duplicates of the given file(s) + * + * @ingroup API + */ +class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'df'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + $params = $this->extractRequestParams(); + $namespaces = $this->getPageSet()->getAllTitlesByNamespace(); + if ( empty( $namespaces[NS_FILE] ) ) { + return; + } + $images = $namespaces[NS_FILE]; + + $this->addTables('image', 'i1'); + $this->addTables('image', 'i2'); + $this->addFields(array( + 'i1.img_name AS orig_name', + 'i2.img_name AS dup_name', + 'i2.img_user_text AS dup_user_text', + 'i2.img_timestamp AS dup_timestamp' + )); + $this->addWhere(array( + 'i1.img_name' => array_keys($images), + 'i1.img_sha1 = i2.img_sha1', + 'i1.img_name != i2.img_name', + )); + if(isset($params['continue'])) + { + $cont = explode('|', $params['continue']); + if(count($cont) != 2) + $this->dieUsage("Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue"); + $orig = $this->getDB()->strencode($this->titleTokey($cont[0])); + $dup = $this->getDB()->strencode($this->titleToKey($cont[1])); + $this->addWhere("i1.img_name > '$orig' OR ". + "(i1.img_name = '$orig' AND ". + "i2.img_name >= '$dup')"); + } + $this->addOption('ORDER BY', 'i1.img_name'); + $this->addOption('LIMIT', $params['limit'] + 1); + + $res = $this->select(__METHOD__); + $db = $this->getDB(); + $count = 0; + $data = array(); + $titles = array(); + $lastName = ''; + while($row = $db->fetchObject($res)) + { + if(++$count > $params['limit']) + { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', + $this->keyToTitle($row->orig_name) . '|' . + $this->keyToTitle($row->dup_name)); + break; + } + if(!is_null($resultPageSet)) + $titles[] = Title::makeTitle(NS_FILE, $row->dup_name); + else + { + if($row->orig_name != $lastName) + { + if($lastName != '') + { + $this->addPageSubItems($images[$lastName], $data); + $data = array(); + } + $lastName = $row->orig_name; + } + + $data[] = array( + 'name' => $row->dup_name, + 'user' => $row->dup_user_text, + 'timestamp' => wfTimestamp(TS_ISO_8601, $row->dup_timestamp) + ); + } + } + if(!is_null($resultPageSet)) + $resultPageSet->populateFromTitles($titles); + else if($lastName != '') + $this->addPageSubItems($images[$lastName], $data); + $db->freeResult($res); + } + + public function getAllowedParams() { + return array ( + 'limit' => array( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'continue' => null, + ); + } + + public function getParamDescription() { + return array ( + 'limit' => 'How many files to return', + 'continue' => 'When more results are available, use this to continue', + ); + } + + public function getDescription() { + return 'List all files that are duplicates of the given file(s).'; + } + + protected function getExamples() { + return array ( 'api.php?action=query&titles=Image:Albert_Einstein_Head.jpg&prop=duplicatefiles', + 'api.php?action=query&generator=allimages&prop=duplicatefiles', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryDuplicateFiles.php 44121 2008-12-01 17:14:30Z vyznev $'; + } +} diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 8ffb7246..85e21f42 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -54,7 +54,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { // Find the right prefix global $wgUrlProtocols; - if(!is_null($protocol) && !empty($protocol) && !in_array($protocol, $wgUrlProtocols)) + if($protocol && !in_array($protocol, $wgUrlProtocols)) { foreach ($wgUrlProtocols as $p) { if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) { @@ -66,7 +66,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { else $protocol = null; - $db = $this->getDb(); + $db = $this->getDB(); $this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX' $this->addOption('USE INDEX', 'el_index'); $this->addWhere('page_id=el_from'); @@ -206,6 +206,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 37909 2008-07-22 13:26:15Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 43271 2008-11-06 22:38:42Z siebrand $'; } } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 33ff1d3f..612d5cc9 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -56,10 +56,10 @@ class ApiQueryImageInfo extends ApiQueryBase { } $pageIds = $this->getPageSet()->getAllTitlesByNamespace(); - if (!empty($pageIds[NS_IMAGE])) { + if (!empty($pageIds[NS_FILE])) { $result = $this->getResult(); - $images = RepoGroup::singleton()->findFiles( array_keys( $pageIds[NS_IMAGE] ) ); + $images = RepoGroup::singleton()->findFiles( array_keys( $pageIds[NS_FILE] ) ); foreach ( $images as $img ) { $data = array(); @@ -78,14 +78,14 @@ class ApiQueryImageInfo extends ApiQueryBase { if(++$count > $params['limit']) { // We've reached the extra one which shows that there are additional pages to be had. Stop here... // Only set a query-continue if there was only one title - if(count($pageIds[NS_IMAGE]) == 1) + if(count($pageIds[NS_FILE]) == 1) $this->setContinueEnumParameter('start', $oldie->getTimestamp()); break; } $data[] = self::getInfo( $oldie, $prop, $result ); } - $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ]; + $pageId = $pageIds[NS_FILE][ $img->getOriginalTitle()->getDBkey() ]; $result->addValue( array( 'query', 'pages', intval( $pageId ) ), 'imagerepository', $img->getRepoName() @@ -93,10 +93,10 @@ class ApiQueryImageInfo extends ApiQueryBase { $this->addPageSubItems($pageId, $data); } - $missing = array_diff( array_keys( $pageIds[NS_IMAGE] ), array_keys( $images ) ); + $missing = array_diff( array_keys( $pageIds[NS_FILE] ), array_keys( $images ) ); foreach ( $missing as $title ) $result->addValue( - array( 'query', 'pages', intval( $pageIds[NS_IMAGE][$title] ) ), + array( 'query', 'pages', intval( $pageIds[NS_FILE][$title] ) ), 'imagerepository', '' ); } @@ -123,12 +123,12 @@ class ApiQueryImageInfo extends ApiQueryBase { } if( isset( $prop['url'] ) ) { if( !is_null( $scale ) && !$file->isOld() ) { - $thumb = $file->getThumbnail( $scale['width'], $scale['height'] ); - if( $thumb ) + $mto = $file->transform( array( 'width' => $scale['width'], 'height' => $scale['height'] ) ); + if( $mto && !$mto->isError() ) { - $vals['thumburl'] = wfExpandUrl( $thumb->getURL() ); - $vals['thumbwidth'] = $thumb->getWidth(); - $vals['thumbheight'] = $thumb->getHeight(); + $vals['thumburl'] = $mto->getUrl(); + $vals['thumbwidth'] = $mto->getWidth(); + $vals['thumbheight'] = $mto->getHeight(); } } $vals['url'] = $file->getFullURL(); @@ -148,6 +148,9 @@ class ApiQueryImageInfo extends ApiQueryBase { if( isset( $prop['archivename'] ) && $file->isOld() ) $vals['archivename'] = $file->getArchiveName(); + + if( isset( $prop['bitdepth'] ) ) + $vals['bitdepth'] = $file->getBitDepth(); return $vals; } @@ -166,7 +169,8 @@ class ApiQueryImageInfo extends ApiQueryBase { 'sha1', 'mime', 'metadata', - 'archivename' + 'archivename', + 'bitdepth', ) ), 'limit' => array( @@ -219,6 +223,6 @@ class ApiQueryImageInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryImageInfo.php 37504 2008-07-10 14:28:09Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryImageInfo.php 44121 2008-12-01 17:14:30Z vyznev $'; } } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index 32c4e1b0..02fe24f1 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -66,7 +66,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $this->dieUsage("Invalid continue param. You should pass the " . "original value returned by the previous query", "_badcontinue"); $ilfrom = intval($cont[0]); - $ilto = $this->getDb()->strencode($this->titleToKey($cont[1])); + $ilto = $this->getDB()->strencode($this->titleToKey($cont[1])); $this->addWhere("il_from > $ilfrom OR ". "(il_from = $ilfrom AND ". "il_to >= '$ilto')"); @@ -103,7 +103,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } $vals = array(); - ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_IMAGE, $row->il_to)); + ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_FILE, $row->il_to)); $data[] = $vals; } @@ -123,7 +123,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { '|' . $this->keyToTitle($row->il_to)); break; } - $titles[] = Title :: makeTitle(NS_IMAGE, $row->il_to); + $titles[] = Title :: makeTitle(NS_FILE, $row->il_to); } $resultPageSet->populateFromTitles($titles); } @@ -165,6 +165,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryImages.php 37535 2008-07-10 21:20:43Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryImages.php 44121 2008-12-01 17:14:30Z vyznev $'; } } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 9c6487b3..0c5c72fc 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -68,7 +68,8 @@ class ApiQueryInfo extends ApiQueryBase { 'protect' => array( 'ApiQueryInfo', 'getProtectToken' ), 'move' => array( 'ApiQueryInfo', 'getMoveToken' ), 'block' => array( 'ApiQueryInfo', 'getBlockToken' ), - 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' ) + 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' ), + 'email' => array( 'ApiQueryInfo', 'getEmailToken' ), ); wfRunHooks('APIQueryInfoTokens', array(&$this->tokenFunctions)); return $this->tokenFunctions; @@ -153,17 +154,33 @@ class ApiQueryInfo extends ApiQueryBase { return self::getBlockToken($pageid, $title); } + public static function getEmailToken($pageid, $title) + { + global $wgUser; + if(!$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailUser()) + return false; + + static $cachedEmailToken = null; + if(!is_null($cachedEmailToken)) + return $cachedEmailToken; + + $cachedEmailToken = $wgUser->editToken(); + return $cachedEmailToken; + } + public function execute() { global $wgUser; $params = $this->extractRequestParams(); - $fld_protection = $fld_talkid = $fld_subjectid = false; + $fld_protection = $fld_talkid = $fld_subjectid = $fld_url = $fld_readable = false; if(!is_null($params['prop'])) { $prop = array_flip($params['prop']); $fld_protection = isset($prop['protection']); $fld_talkid = isset($prop['talkid']); $fld_subjectid = isset($prop['subjectid']); + $fld_url = isset($prop['url']); + $fld_readable = isset($prop['readable']); } $pageSet = $this->getPageSet(); @@ -180,7 +197,7 @@ class ApiQueryInfo extends ApiQueryBase { $pageLength = $pageSet->getCustomField('page_len'); $db = $this->getDB(); - if ($fld_protection && !empty($titles)) { + if ($fld_protection && count($titles)) { $this->addTables('page_restrictions'); $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade')); $this->addWhereFld('pr_page', array_keys($titles)); @@ -195,12 +212,44 @@ class ApiQueryInfo extends ApiQueryBase { if($row->pr_cascade) $a['cascade'] = ''; $protections[$row->pr_page][] = $a; + + # Also check old restrictions + if($pageRestrictions[$row->pr_page]) { + foreach(explode(':', trim($pageRestrictions[$pageid])) as $restrict) { + $temp = explode('=', trim($restrict)); + if(count($temp) == 1) { + // old old format should be treated as edit/move restriction + $restriction = trim( $temp[0] ); + if($restriction == '') + continue; + $protections[$row->pr_page][] = array( + 'type' => 'edit', + 'level' => $restriction, + 'expiry' => 'infinity', + ); + $protections[$row->pr_page][] = array( + 'type' => 'move', + 'level' => $restriction, + 'expiry' => 'infinity', + ); + } else { + $restriction = trim( $temp[1] ); + if($restriction == '') + continue; + $protections[$row->pr_page][] = array( + 'type' => $temp[0], + 'level' => $restriction, + 'expiry' => 'infinity', + ); + } + } + } } $db->freeResult($res); $imageIds = array(); foreach ($titles as $id => $title) - if ($title->getNamespace() == NS_IMAGE) + if ($title->getNamespace() == NS_FILE) $imageIds[] = $id; // To avoid code duplication $cascadeTypes = array( @@ -214,7 +263,7 @@ class ApiQueryInfo extends ApiQueryBase { array( 'prefix' => 'il', 'table' => 'imagelinks', - 'ns' => NS_IMAGE, + 'ns' => NS_FILE, 'title' => 'il_to', 'ids' => $imageIds ) @@ -256,7 +305,7 @@ class ApiQueryInfo extends ApiQueryBase { } // We don't need to check for pt stuff if there are no nonexistent titles - if($fld_protection && !empty($missing)) + if($fld_protection && count($missing)) { $this->resetQueryParams(); // Construct a custom WHERE clause that matches all titles in $missing @@ -278,8 +327,8 @@ class ApiQueryInfo extends ApiQueryBase { $images = array(); $others = array(); foreach ($missing as $title) - if ($title->getNamespace() == NS_IMAGE) - $images[] = $title->getDbKey(); + if ($title->getNamespace() == NS_FILE) + $images[] = $title->getDBKey(); else $others[] = $title; @@ -328,7 +377,7 @@ class ApiQueryInfo extends ApiQueryBase { 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ), 'source' => $source->getPrefixedText() ); - $prottitles[NS_IMAGE][$row->il_to][] = $a; + $prottitles[NS_FILE][$row->il_to][] = $a; } $db->freeResult($res); } @@ -350,7 +399,7 @@ class ApiQueryInfo extends ApiQueryBase { else if($fld_talkid) $talktitles[] = $t->getTalkPage(); } - if(!empty($talktitles) || !empty($subjecttitles)) + if(count($talktitles) || count($subjecttitles)) { // Construct a custom WHERE clause that matches // all titles in $talktitles and $subjecttitles @@ -386,6 +435,7 @@ class ApiQueryInfo extends ApiQueryBase { if (!is_null($params['token'])) { $tokenFunctions = $this->getTokenFunctions(); + $pageInfo['starttimestamp'] = wfTimestamp(TS_ISO_8601, time()); foreach($params['token'] as $t) { $val = call_user_func($tokenFunctions[$t], $pageid, $title); @@ -397,46 +447,23 @@ class ApiQueryInfo extends ApiQueryBase { } if($fld_protection) { + $pageInfo['protection'] = array(); if (isset($protections[$pageid])) { $pageInfo['protection'] = $protections[$pageid]; $result->setIndexedTagName($pageInfo['protection'], 'pr'); - } else { - # Also check old restrictions - if( $pageRestrictions[$pageid] ) { - foreach( explode( ':', trim( $pageRestrictions[$pageid] ) ) as $restrict ) { - $temp = explode( '=', trim( $restrict ) ); - if(count($temp) == 1) { - // old old format should be treated as edit/move restriction - $restriction = trim( $temp[0] ); - $pageInfo['protection'][] = array( - 'type' => 'edit', - 'level' => $restriction, - 'expiry' => 'infinity', - ); - $pageInfo['protection'][] = array( - 'type' => 'move', - 'level' => $restriction, - 'expiry' => 'infinity', - ); - } else { - $restriction = trim( $temp[1] ); - $pageInfo['protection'][] = array( - 'type' => $temp[0], - 'level' => $restriction, - 'expiry' => 'infinity', - ); - } - } - $result->setIndexedTagName($pageInfo['protection'], 'pr'); - } else { - $pageInfo['protection'] = array(); - } } } - if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDbKey()])) - $pageInfo['talkid'] = $talkids[$title->getNamespace()][$title->getDbKey()]; - if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDbKey()])) - $pageInfo['subjectid'] = $subjectids[$title->getNamespace()][$title->getDbKey()]; + if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDBKey()])) + $pageInfo['talkid'] = $talkids[$title->getNamespace()][$title->getDBKey()]; + if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDBKey()])) + $pageInfo['subjectid'] = $subjectids[$title->getNamespace()][$title->getDBKey()]; + if($fld_url) { + $pageInfo['fullurl'] = $title->getFullURL(); + $pageInfo['editurl'] = $title->getFullURL('action=edit'); + } + if($fld_readable) + if($title->userCanRead()) + $pageInfo['readable'] = ''; $result->addValue(array ( 'query', @@ -444,19 +471,22 @@ class ApiQueryInfo extends ApiQueryBase { ), $pageid, $pageInfo); } - // Get edit/protect tokens and protection data for missing titles if requested - // Delete and move tokens are N/A for missing titles anyway - if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid) + // Get properties for missing titles if requested + if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid || + $fld_url || $fld_readable) { $res = &$result->getData(); foreach($missing as $pageid => $title) { if(!is_null($params['token'])) { $tokenFunctions = $this->getTokenFunctions(); + $res['query']['pages'][$pageid]['starttimestamp'] = wfTimestamp(TS_ISO_8601, time()); foreach($params['token'] as $t) { $val = call_user_func($tokenFunctions[$t], $pageid, $title); - if($val !== false) + if($val === false) + $this->setWarning("Action '$t' is not allowed for the current user"); + else $res['query']['pages'][$pageid][$t . 'token'] = $val; } } @@ -470,10 +500,17 @@ class ApiQueryInfo extends ApiQueryBase { $res['query']['pages'][$pageid]['protection'] = array(); $result->setIndexedTagName($res['query']['pages'][$pageid]['protection'], 'pr'); } - if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDbKey()])) - $res['query']['pages'][$pageid]['talkid'] = $talkids[$title->getNamespace()][$title->getDbKey()]; - if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDbKey()])) - $res['query']['pages'][$pageid]['subjectid'] = $subjectids[$title->getNamespace()][$title->getDbKey()]; + if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDBKey()])) + $res['query']['pages'][$pageid]['talkid'] = $talkids[$title->getNamespace()][$title->getDBKey()]; + if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDBKey()])) + $res['query']['pages'][$pageid]['subjectid'] = $subjectids[$title->getNamespace()][$title->getDBKey()]; + if($fld_url) { + $res['query']['pages'][$pageid]['fullurl'] = $title->getFullURL(); + $res['query']['pages'][$pageid]['editurl'] = $title->getFullURL('action=edit'); + } + if($fld_readable) + if($title->userCanRead()) + $res['query']['pages'][$pageid]['readable'] = ''; } } } @@ -486,7 +523,9 @@ class ApiQueryInfo extends ApiQueryBase { ApiBase :: PARAM_TYPE => array ( 'protection', 'talkid', - 'subjectid' + 'subjectid', + 'url', + 'readable', )), 'token' => array ( ApiBase :: PARAM_DFLT => NULL, @@ -521,6 +560,6 @@ class ApiQueryInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryInfo.php 37191 2008-07-06 18:43:06Z brion $'; + return __CLASS__ . ': $Id: ApiQueryInfo.php 45683 2009-01-12 19:10:42Z raymond $'; } } diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index e7d84fc3..8eaf8d02 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -58,7 +58,7 @@ class ApiQueryLangLinks extends ApiQueryBase { $this->dieUsage("Invalid continue param. You should pass the " . "original value returned by the previous query", "_badcontinue"); $llfrom = intval($cont[0]); - $lllang = $this->getDb()->strencode($cont[1]); + $lllang = $this->getDB()->strencode($cont[1]); $this->addWhere("ll_from > $llfrom OR ". "(ll_from = $llfrom AND ". "ll_lang >= '$lllang')"); @@ -134,6 +134,6 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLangLinks.php 37534 2008-07-10 21:08:37Z brion $'; + return __CLASS__ . ': $Id: ApiQueryLangLinks.php 43271 2008-11-06 22:38:42Z siebrand $'; } } diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 546a599d..91b5b529 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -76,9 +76,9 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $this->addFields(array ( - $this->prefix . '_from pl_from', - $this->prefix . '_namespace pl_namespace', - $this->prefix . '_title pl_title' + $this->prefix . '_from AS pl_from', + $this->prefix . '_namespace AS pl_namespace', + $this->prefix . '_title AS pl_title' )); $this->addTables($this->table); @@ -92,7 +92,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { "original value returned by the previous query", "_badcontinue"); $plfrom = intval($cont[0]); $plns = intval($cont[1]); - $pltitle = $this->getDb()->strencode($this->titleToKey($cont[2])); + $pltitle = $this->getDB()->strencode($this->titleToKey($cont[2])); $this->addWhere("{$this->prefix}_from > $plfrom OR ". "({$this->prefix}_from = $plfrom AND ". "({$this->prefix}_namespace > $plns OR ". @@ -213,6 +213,6 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLinks.php 37909 2008-07-22 13:26:15Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryLinks.php 43271 2008-11-06 22:38:42Z siebrand $'; } } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 47a526bb..83c73b83 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -93,16 +93,15 @@ class ApiQueryLogEvents extends ApiQueryBase { $limit = $params['limit']; $this->addOption('LIMIT', $limit +1); - + + $index = false; $user = $params['user']; if (!is_null($user)) { - $userid = $db->selectField('user', 'user_id', array ( - 'user_name' => $user - )); + $userid = User::idFromName($user); if (!$userid) $this->dieUsage("User name $user not found", 'param_user'); $this->addWhereFld('log_user', $userid); - $this->addOption('USE INDEX', array('logging' => array('user_time','page_time'))); + $index = 'user_time'; } $title = $params['title']; @@ -112,8 +111,14 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->dieUsage("Bad title value '$title'", 'param_title'); $this->addWhereFld('log_namespace', $titleObj->getNamespace()); $this->addWhereFld('log_title', $titleObj->getDBkey()); - $this->addOption('USE INDEX', array('logging' => array('user_time','page_time'))); + + // Use the title index in preference to the user index if there is a conflict + $index = 'page_time'; } + if ( $index ) { + $this->addOption( 'USE INDEX', array( 'logging' => $index ) ); + } + $data = array (); $count = 0; @@ -134,6 +139,48 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->getResult()->setIndexedTagName($data, 'item'); $this->getResult()->addValue('query', $this->getModuleName(), $data); } + + public static function addLogParams($result, &$vals, $params, $type, $ts) { + $params = explode("\n", $params); + switch ($type) { + case 'move': + if (isset ($params[0])) { + $title = Title :: newFromText($params[0]); + if ($title) { + $vals2 = array(); + ApiQueryBase :: addTitleInfo($vals2, $title, "new_"); + $vals[$type] = $vals2; + $params = null; + } + } + break; + case 'patrol': + $vals2 = array(); + list( $vals2['cur'], $vals2['prev'], $vals2['auto'] ) = $params; + $vals[$type] = $vals2; + $params = null; + break; + case 'rights': + $vals2 = array(); + list( $vals2['old'], $vals2['new'] ) = $params; + $vals[$type] = $vals2; + $params = null; + break; + case 'block': + $vals2 = array(); + list( $vals2['duration'], $vals2['flags'] ) = $params; + $vals2['expiry'] = wfTimestamp(TS_ISO_8601, + strtotime($params[0], wfTimestamp(TS_UNIX, $ts))); + $vals[$type] = $vals2; + $params = null; + break; + } + if (!is_null($params)) { + $result->setIndexedTagName($params, 'param'); + $vals = array_merge($vals, $params); + } + return $vals; + } private function extractRowInfo($row) { $vals = array(); @@ -154,43 +201,9 @@ class ApiQueryLogEvents extends ApiQueryBase { } if ($this->fld_details && $row->log_params !== '') { - $params = explode("\n", $row->log_params); - switch ($row->log_type) { - case 'move': - if (isset ($params[0])) { - $title = Title :: newFromText($params[0]); - if ($title) { - $vals2 = array(); - ApiQueryBase :: addTitleInfo($vals2, $title, "new_"); - $vals[$row->log_type] = $vals2; - $params = null; - } - } - break; - case 'patrol': - $vals2 = array(); - list( $vals2['cur'], $vals2['prev'], $vals2['auto'] ) = $params; - $vals[$row->log_type] = $vals2; - $params = null; - break; - case 'rights': - $vals2 = array(); - list( $vals2['old'], $vals2['new'] ) = $params; - $vals[$row->log_type] = $vals2; - $params = null; - break; - case 'block': - $vals2 = array(); - list( $vals2['duration'], $vals2['flags'] ) = $params; - $vals[$row->log_type] = $vals2; - $params = null; - break; - } - - if (isset($params)) { - $this->getResult()->setIndexedTagName($params, 'param'); - $vals = array_merge($vals, $params); - } + self::addLogParams($this->getResult(), $vals, + $row->log_params, $row->log_type, + $row->log_timestamp); } if ($this->fld_user) { @@ -201,7 +214,7 @@ class ApiQueryLogEvents extends ApiQueryBase { if ($this->fld_timestamp) { $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->log_timestamp); } - if ($this->fld_comment && !empty ($row->log_comment)) { + if ($this->fld_comment && isset($row->log_comment)) { $vals['comment'] = $row->log_comment; } @@ -277,6 +290,6 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLogEvents.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiQueryLogEvents.php 44234 2008-12-04 15:59:26Z catrope $'; } } diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index 046157a6..e7b8bf46 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -48,13 +48,13 @@ if (!defined('MEDIAWIKI')) { $this->run($resultPageSet); } - protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet) { + protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet, $redirect) { $this->resetQueryParams(); $this->addTables('page'); $this->addOption('LIMIT', $limit); $this->addWhereFld('page_namespace', $namespace); $this->addWhereRange('page_random', 'newer', $randstr, null); - $this->addWhere(array('page_is_redirect' => 0)); + $this->addWhereFld('page_is_redirect', $redirect); $this->addOption('USE INDEX', 'page_random'); if(is_null($resultPageSet)) $this->addFields(array('page_id', 'page_title', 'page_namespace')); @@ -89,7 +89,8 @@ if (!defined('MEDIAWIKI')) { $result = $this->getResult(); $data = array(); $this->pageIDs = array(); - $this->prepareQuery(wfRandom(), $params['limit'], $params['namespace'], $resultPageSet); + + $this->prepareQuery(wfRandom(), $params['limit'], $params['namespace'], $resultPageSet, $params['redirect']); $count = $this->runQuery($data, $resultPageSet); if($count < $params['limit']) { @@ -97,7 +98,7 @@ if (!defined('MEDIAWIKI')) { * for page_random. We'll just take the lowest ones, see * also the comment in Title::getRandomTitle() */ - $this->prepareQuery(0, $params['limit'] - $count, $params['namespace'], $resultPageSet); + $this->prepareQuery(0, $params['limit'] - $count, $params['namespace'], $resultPageSet, $params['redirect']); $this->runQuery($data, $resultPageSet); } @@ -129,13 +130,15 @@ if (!defined('MEDIAWIKI')) { ApiBase :: PARAM_MAX => 10, ApiBase :: PARAM_MAX2 => 20 ), + 'redirect' => false, ); } public function getParamDescription() { return array ( 'namespace' => 'Return pages in these namespaces only', - 'limit' => 'Limit how many random pages will be returned' + 'limit' => 'Limit how many random pages will be returned', + 'redirect' => 'Load a random redirect instead of a random page' ); } diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 2b8c6a92..04eb910f 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -43,16 +43,48 @@ class ApiQueryRecentChanges extends ApiQueryBase { private $fld_comment = false, $fld_user = false, $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false, $fld_sizes = false; + + protected function getTokenFunctions() { + // tokenname => function + // function prototype is func($pageid, $title, $rev) + // should return token or false + + // Don't call the hooks twice + if(isset($this->tokenFunctions)) + return $this->tokenFunctions; + + // If we're in JSON callback mode, no tokens can be obtained + if(!is_null($this->getMain()->getRequest()->getVal('callback'))) + return array(); + + $this->tokenFunctions = array( + 'patrol' => array( 'ApiQueryRecentChanges', 'getPatrolToken' ) + ); + wfRunHooks('APIQueryRecentChangesTokens', array(&$this->tokenFunctions)); + return $this->tokenFunctions; + } + + public static function getPatrolToken($pageid, $title, $rc) + { + global $wgUser; + if(!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol()) + return false; + + // The patrol token is always the same, let's exploit that + static $cachedPatrolToken = null; + if(!is_null($cachedPatrolToken)) + return $cachedPatrolToken; + + $cachedPatrolToken = $wgUser->editToken(); + return $cachedPatrolToken; + } /** * Generates and outputs the result of this query based upon the provided parameters. */ public function execute() { - /* Initialize vars */ - $limit = $prop = $namespace = $titles = $show = $type = $dir = $start = $end = null; - /* Get the parameters of the request. */ - extract($this->extractRequestParams()); + $params = $this->extractRequestParams(); /* Build our basic query. Namely, something along the lines of: * SELECT * FROM recentchanges WHERE rc_timestamp > $start @@ -62,13 +94,13 @@ class ApiQueryRecentChanges extends ApiQueryBase { $db = $this->getDB(); $this->addTables('recentchanges'); $this->addOption('USE INDEX', array('recentchanges' => 'rc_timestamp')); - $this->addWhereRange('rc_timestamp', $dir, $start, $end); - $this->addWhereFld('rc_namespace', $namespace); + $this->addWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']); + $this->addWhereFld('rc_namespace', $params['namespace']); $this->addWhereFld('rc_deleted', 0); - if(!empty($titles)) + if($params['titles']) { $lb = new LinkBatch; - foreach($titles as $t) + foreach($params['titles'] as $t) { $obj = Title::newFromText($t); $lb->addObj($obj); @@ -77,19 +109,19 @@ class ApiQueryRecentChanges extends ApiQueryBase { // LinkBatch refuses these, but we need them anyway if(!array_key_exists($obj->getNamespace(), $lb->data)) $lb->data[$obj->getNamespace()] = array(); - $lb->data[$obj->getNamespace()][$obj->getDbKey()] = 1; + $lb->data[$obj->getNamespace()][$obj->getDBKey()] = 1; } } - $where = $lb->constructSet('rc', $this->getDb()); + $where = $lb->constructSet('rc', $this->getDB()); if($where != '') $this->addWhere($where); } - if(!is_null($type)) - $this->addWhereFld('rc_type', $this->parseRCType($type)); + if(!is_null($params['type'])) + $this->addWhereFld('rc_type', $this->parseRCType($params['type'])); - if (!is_null($show)) { - $show = array_flip($show); + if (!is_null($params['show'])) { + $show = array_flip($params['show']); /* Check for conflicting parameters. */ if ((isset ($show['minor']) && isset ($show['!minor'])) @@ -103,7 +135,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { // Check permissions global $wgUser; - if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->isAllowed('patrol')) + if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol()) $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied'); /* Add additional conditions to query depending upon parameters. */ @@ -125,14 +157,15 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'rc_timestamp', 'rc_namespace', 'rc_title', + 'rc_cur_id', 'rc_type', 'rc_moved_to_ns', 'rc_moved_to_title' )); /* Determine what properties we need to display. */ - if (!is_null($prop)) { - $prop = array_flip($prop); + if (!is_null($params['prop'])) { + $prop = array_flip($params['prop']); /* Set up internal members based upon params. */ $this->fld_comment = isset ($prop['comment']); @@ -144,14 +177,14 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->fld_sizes = isset ($prop['sizes']); $this->fld_redirect = isset($prop['redirect']); $this->fld_patrolled = isset($prop['patrolled']); + $this->fld_loginfo = isset($prop['loginfo']); global $wgUser; - if($this->fld_patrolled && !$wgUser->isAllowed('patrol')) + if($this->fld_patrolled && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol()) $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied'); /* Add fields to our query if they are specified as a needed parameter. */ $this->addFieldsIf('rc_id', $this->fld_ids); - $this->addFieldsIf('rc_cur_id', $this->fld_ids); $this->addFieldsIf('rc_this_oldid', $this->fld_ids); $this->addFieldsIf('rc_last_oldid', $this->fld_ids); $this->addFieldsIf('rc_comment', $this->fld_comment); @@ -163,6 +196,10 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addFieldsIf('rc_old_len', $this->fld_sizes); $this->addFieldsIf('rc_new_len', $this->fld_sizes); $this->addFieldsIf('rc_patrolled', $this->fld_patrolled); + $this->addFieldsIf('rc_logid', $this->fld_loginfo); + $this->addFieldsIf('rc_log_type', $this->fld_loginfo); + $this->addFieldsIf('rc_log_action', $this->fld_loginfo); + $this->addFieldsIf('rc_params', $this->fld_loginfo); if($this->fld_redirect || isset($show['redirect']) || isset($show['!redirect'])) { $this->addTables('page'); @@ -170,9 +207,8 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addFields('page_is_redirect'); } } - /* Specify the limit for our query. It's $limit+1 because we (possibly) need to - * generate a "continue" parameter, to allow paging. */ - $this->addOption('LIMIT', $limit +1); + $this->token = $params['token']; + $this->addOption('LIMIT', $params['limit'] +1); $data = array (); $count = 0; @@ -183,7 +219,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { /* Iterate through the rows, adding data extracted from them to our query result. */ while ($row = $db->fetchObject($res)) { - if (++ $count > $limit) { + if (++ $count > $params['limit']) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp)); break; @@ -215,7 +251,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { private function extractRowInfo($row) { /* If page was moved somewhere, get the title of the move target. */ $movedToTitle = false; - if (!empty($row->rc_moved_to_title)) + if (isset($row->rc_moved_to_title) && $row->rc_moved_to_title !== '') $movedToTitle = Title :: makeTitle($row->rc_moved_to_ns, $row->rc_moved_to_title); /* Determine the title of the page that has been changed. */ @@ -228,11 +264,11 @@ class ApiQueryRecentChanges extends ApiQueryBase { /* Determine what kind of change this was. */ switch ( $type ) { - case RC_EDIT: $vals['type'] = 'edit'; break; - case RC_NEW: $vals['type'] = 'new'; break; - case RC_MOVE: $vals['type'] = 'move'; break; - case RC_LOG: $vals['type'] = 'log'; break; - case RC_MOVE_OVER_REDIRECT: $vals['type'] = 'move over redirect'; break; + case RC_EDIT: $vals['type'] = 'edit'; break; + case RC_NEW: $vals['type'] = 'new'; break; + case RC_MOVE: $vals['type'] = 'move'; break; + case RC_LOG: $vals['type'] = 'log'; break; + case RC_MOVE_OVER_REDIRECT: $vals['type'] = 'move over redirect'; break; default: $vals['type'] = $type; } @@ -279,7 +315,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp); /* Add edit summary / log summary. */ - if ($this->fld_comment && !empty ($row->rc_comment)) { + if ($this->fld_comment && isset($row->rc_comment)) { $vals['comment'] = $row->rc_comment; } @@ -290,6 +326,29 @@ class ApiQueryRecentChanges extends ApiQueryBase { /* Add the patrolled flag */ if ($this->fld_patrolled && $row->rc_patrolled == 1) $vals['patrolled'] = ''; + + if ($this->fld_loginfo && $row->rc_type == RC_LOG) { + $vals['logid'] = $row->rc_logid; + $vals['logtype'] = $row->rc_log_type; + $vals['logaction'] = $row->rc_log_action; + ApiQueryLogEvents::addLogParams($this->getResult(), + $vals, $row->rc_params, + $row->rc_log_type, $row->rc_timestamp); + } + + if(!is_null($this->token)) + { + $tokenFunctions = $this->getTokenFunctions(); + foreach($this->token as $t) + { + $val = call_user_func($tokenFunctions[$t], $row->rc_cur_id, + $title, RecentChange::newFromRow($row)); + if($val === false) + $this->setWarning("Action '$t' is not allowed for the current user"); + else + $vals[$t . 'token'] = $val; + } + } return $vals; } @@ -345,9 +404,14 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'ids', 'sizes', 'redirect', - 'patrolled' + 'patrolled', + 'loginfo', ) ), + 'token' => array( + ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()), + ApiBase :: PARAM_ISMULTI => true + ), 'show' => array ( ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array ( @@ -389,6 +453,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'namespace' => 'Filter log entries to only this namespace(s)', 'titles' => 'Filter log entries to only these page titles', 'prop' => 'Include additional pieces of information', + 'token' => 'Which tokens to obtain for each change', 'show' => array ( 'Show only items that meet this criteria.', 'For example, to see only minor edits done by logged-in users, set show=minor|!anon' @@ -409,6 +474,6 @@ class ApiQueryRecentChanges extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 37909 2008-07-22 13:26:15Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 44719 2008-12-17 16:34:01Z catrope $'; } } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 1fd2d7c6..977e792b 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -58,7 +58,7 @@ class ApiQueryRevisions extends ApiQueryBase { return array(); $this->tokenFunctions = array( - 'rollback' => array( 'ApiQueryRevisions','getRollbackToken' ) + 'rollback' => array( 'ApiQueryRevisions', 'getRollbackToken' ) ); wfRunHooks('APIQueryRevisionsTokens', array(&$this->tokenFunctions)); return $this->tokenFunctions; @@ -74,14 +74,16 @@ class ApiQueryRevisions extends ApiQueryBase { } public function execute() { - $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = $expandtemplates = $section = $token = null; - extract($this->extractRequestParams(false)); + $params = $this->extractRequestParams(false); // If any of those parameters are used, work in 'enumeration' mode. // Enum mode can only be used when exactly one page is provided. // Enumerating revisions on multiple pages make it extremely // difficult to manage continuations and require additional SQL indexes - $enumRevMode = (!is_null($user) || !is_null($excludeuser) || !is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end)); + $enumRevMode = (!is_null($params['user']) || !is_null($params['excludeuser']) || + !is_null($params['limit']) || !is_null($params['startid']) || + !is_null($params['endid']) || $params['dir'] === 'newer' || + !is_null($params['start']) || !is_null($params['end'])); $pageSet = $this->getPageSet(); @@ -100,8 +102,10 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addTables('revision'); $this->addFields( Revision::selectFields() ); + $this->addTables( 'page' ); + $this->addWhere('page_id = rev_page'); - $prop = array_flip($prop); + $prop = array_flip($params['prop']); // Optional fields $this->fld_ids = isset ($prop['ids']); @@ -111,11 +115,9 @@ class ApiQueryRevisions extends ApiQueryBase { $this->fld_comment = isset ($prop['comment']); $this->fld_size = isset ($prop['size']); $this->fld_user = isset ($prop['user']); - $this->token = $token; + $this->token = $params['token']; - if ( !is_null($this->token) || ( $this->fld_content && $this->expandTemplates ) || $pageCount > 0) { - $this->addTables( 'page' ); - $this->addWhere('page_id=rev_page'); + if ( !is_null($this->token) || $pageCount > 0) { $this->addFields( Revision::selectPageFields() ); } @@ -136,15 +138,17 @@ class ApiQueryRevisions extends ApiQueryBase { $this->fld_content = true; - $this->expandTemplates = $expandtemplates; - if(isset($section)) - $this->section = $section; + $this->expandTemplates = $params['expandtemplates']; + $this->generateXML = $params['generatexml']; + if(isset($params['section'])) + $this->section = $params['section']; else $this->section = false; } $userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 ); $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 ); + $limit = $params['limit']; if( $limit == 'max' ) { $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; $this->getResult()->addValue( 'limits', $this->getModuleName(), $limit ); @@ -153,13 +157,13 @@ class ApiQueryRevisions extends ApiQueryBase { if ($enumRevMode) { // This is mostly to prevent parameter errors (and optimize SQL?) - if (!is_null($startid) && !is_null($start)) + if (!is_null($params['startid']) && !is_null($params['start'])) $this->dieUsage('start and startid cannot be used together', 'badparams'); - if (!is_null($endid) && !is_null($end)) + if (!is_null($params['endid']) && !is_null($params['end'])) $this->dieUsage('end and endid cannot be used together', 'badparams'); - if(!is_null($user) && !is_null( $excludeuser)) + if(!is_null($params['user']) && !is_null($params['excludeuser'])) $this->dieUsage('user and excludeuser cannot be used together', 'badparams'); // This code makes an assumption that sorting by rev_id and rev_timestamp produces @@ -169,10 +173,12 @@ class ApiQueryRevisions extends ApiQueryBase { // one row with the same timestamp for the same page. // The order needs to be the same as start parameter to avoid SQL filesort. - if (is_null($startid) && is_null($endid)) - $this->addWhereRange('rev_timestamp', $dir, $start, $end); + if (is_null($params['startid']) && is_null($params['endid'])) + $this->addWhereRange('rev_timestamp', $params['dir'], + $params['start'], $params['end']); else - $this->addWhereRange('rev_id', $dir, $startid, $endid); + $this->addWhereRange('rev_id', $params['dir'], + $params['startid'], $params['endid']); // must manually initialize unset limit if (is_null($limit)) @@ -182,30 +188,38 @@ class ApiQueryRevisions extends ApiQueryBase { // There is only one ID, use it $this->addWhereFld('rev_page', current(array_keys($pageSet->getGoodTitles()))); - if(!is_null($user)) { - $this->addWhereFld('rev_user_text', $user); - } elseif (!is_null( $excludeuser)) { - $this->addWhere('rev_user_text != ' . $this->getDB()->addQuotes($excludeuser)); + if(!is_null($params['user'])) { + $this->addWhereFld('rev_user_text', $params['user']); + } elseif (!is_null( $params['excludeuser'])) { + $this->addWhere('rev_user_text != ' . + $this->getDB()->addQuotes($params['excludeuser'])); } } elseif ($revCount > 0) { - $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax); + $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; + $revs = $pageSet->getRevisionIDs(); + if(self::truncateArray($revs, $max)) + $this->setWarning("Too many values supplied for parameter 'revids': the limit is $max"); // Get all revision IDs - $this->addWhereFld('rev_id', array_keys($pageSet->getRevisionIDs())); + $this->addWhereFld('rev_id', array_keys($revs)); // assumption testing -- we should never get more then $revCount rows. $limit = $revCount; } elseif ($pageCount > 0) { + $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; + $titles = $pageSet->getGoodTitles(); + if(self::truncateArray($titles, $max)) + $this->setWarning("Too many values supplied for parameter 'titles': the limit is $max"); + // When working in multi-page non-enumeration mode, // limit to the latest revision only $this->addWhere('page_id=rev_page'); $this->addWhere('page_latest=rev_id'); - $this->validateLimit('page_count', $pageCount, 1, $userMax, $botMax); - + // Get all page IDs - $this->addWhereFld('page_id', array_keys($pageSet->getGoodTitles())); + $this->addWhereFld('page_id', array_keys($titles)); // assumption testing -- we should never get more then $pageCount rows. $limit = $pageCount; @@ -281,7 +295,7 @@ class ApiQueryRevisions extends ApiQueryBase { if ($this->fld_comment) { $comment = $revision->getComment(); - if (!empty($comment)) + if (strval($comment) !== '') $vals['comment'] = $comment; } @@ -312,6 +326,17 @@ class ApiQueryRevisions extends ApiQueryBase { if($text === false) $this->dieUsage("There is no section {$this->section} in r".$revision->getId(), 'nosuchsection'); } + if ($this->generateXML) { + $wgParser->startExternalParse( $title, new ParserOptions(), OT_PREPROCESS ); + $dom = $wgParser->preprocessToDom( $text ); + if ( is_callable( array( $dom, 'saveXML' ) ) ) { + $xml = $dom->saveXML(); + } else { + $xml = $dom->__toString(); + } + $vals['parsetree'] = $xml; + + } if ($this->expandTemplates) { $text = $wgParser->preprocess( $text, $title, new ParserOptions() ); } @@ -366,11 +391,9 @@ class ApiQueryRevisions extends ApiQueryBase { 'excludeuser' => array( ApiBase :: PARAM_TYPE => 'user' ), - 'expandtemplates' => false, - 'section' => array( - ApiBase :: PARAM_TYPE => 'integer' - ), + 'generatexml' => false, + 'section' => null, 'token' => array( ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()), ApiBase :: PARAM_ISMULTI => true @@ -390,6 +413,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'user' => 'only include revisions made by user', 'excludeuser' => 'exclude revisions made by user', 'expandtemplates' => 'expand templates in revision content', + 'generatexml' => 'generate XML parse tree for revision content', 'section' => 'only retrieve the content of this section', 'token' => 'Which tokens to obtain for each revision', ); @@ -424,6 +448,6 @@ class ApiQueryRevisions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRevisions.php 37300 2008-07-08 08:42:27Z btongminh $'; + return __CLASS__ . ': $Id: ApiQueryRevisions.php 44719 2008-12-17 16:34:01Z catrope $'; } } diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 84a2ec63..cb020fff 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -53,7 +53,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $limit = $params['limit']; $query = $params['search']; - if (is_null($query) || empty($query)) + $what = $params['what']; + if (strval($query) === '') $this->dieUsage("empty search string is not allowed", 'param-search'); $search = SearchEngine::create(); @@ -61,13 +62,30 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $search->setNamespaces( $params['namespace'] ); $search->showRedirects = $params['redirects']; - if ($params['what'] == 'text') + if ($what == 'text') { $matches = $search->searchText( $query ); - else + } elseif( $what == 'title' ) { $matches = $search->searchTitle( $query ); + } else { + // We default to title searches; this is a terrible legacy + // of the way we initially set up the MySQL fulltext-based + // search engine with separate title and text fields. + // In the future, the default should be for a combined index. + $what = 'title'; + $matches = $search->searchTitle( $query ); + + // Not all search engines support a separate title search, + // for instance the Lucene-based engine we use on Wikipedia. + // In this case, fall back to full-text search (which will + // include titles in it!) + if( is_null( $matches ) ) { + $what = 'text'; + $matches = $search->searchText( $query ); + } + } if (is_null($matches)) - $this->dieUsage("{$params['what']} search is disabled", - "search-{$params['what']}-disabled"); + $this->dieUsage("{$what} search is disabled", + "search-{$what}-disabled"); $data = array (); $count = 0; @@ -78,8 +96,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { break; } - // Silently skip broken titles - if ($result->isBrokenTitle()) continue; + // Silently skip broken and missing titles + if ($result->isBrokenTitle() || $result->isMissingRevision()) + continue; $title = $result->getTitle(); if (is_null($resultPageSet)) { @@ -109,7 +128,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ApiBase :: PARAM_ISMULTI => true, ), 'what' => array ( - ApiBase :: PARAM_DFLT => 'title', + ApiBase :: PARAM_DFLT => null, ApiBase :: PARAM_TYPE => array ( 'title', 'text', @@ -151,6 +170,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySearch.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiQuerySearch.php 44186 2008-12-03 19:33:57Z catrope $'; } } diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 1fd3b888..84757f7f 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -57,6 +57,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'specialpagealiases': $this->appendSpecialPageAliases( $p ); break; + case 'magicwords': + $this->appendMagicWords( $p ); + break; case 'interwikimap': $filteriw = isset( $params['filteriw'] ) ? $params['filteriw'] : false; $this->appendInterwikiMap( $p, $filteriw ); @@ -70,6 +73,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'usergroups': $this->appendUserGroups( $p ); break; + case 'extensions': + $this->appendExtensions( $p ); + break; default : ApiBase :: dieDebug( __METHOD__, "Unknown prop=$p" ); } @@ -129,8 +135,13 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'id' => $ns ); ApiResult :: setContent( $data[$ns], $title ); - if( MWNamespace::hasSubpages($ns) ) + $canonical = MWNamespace::getCanonicalName( $ns ); + + if( MWNamespace::hasSubpages( $ns ) ) $data[$ns]['subpages'] = ''; + + if( $canonical ) + $data[$ns]['canonical'] = strtr($canonical, '_', ' '); } $this->getResult()->setIndexedTagName( $data, 'ns' ); @@ -138,9 +149,11 @@ class ApiQuerySiteinfo extends ApiQueryBase { } protected function appendNamespaceAliases( $property ) { - global $wgNamespaceAliases; + global $wgNamespaceAliases, $wgContLang; + $wgContLang->load(); + $aliases = array_merge($wgNamespaceAliases, $wgContLang->namespaceAliases); $data = array(); - foreach( $wgNamespaceAliases as $title => $ns ) { + foreach( $aliases as $title => $ns ) { $item = array( 'id' => $ns ); @@ -164,6 +177,22 @@ class ApiQuerySiteinfo extends ApiQueryBase { $this->getResult()->setIndexedTagName( $data, 'specialpage' ); $this->getResult()->addValue( 'query', $property, $data ); } + + protected function appendMagicWords( $property ) { + global $wgContLang; + $data = array(); + foreach($wgContLang->getMagicWords() as $magicword => $aliases) + { + $caseSensitive = array_shift($aliases); + $arr = array('name' => $magicword, 'aliases' => $aliases); + if($caseSensitive) + $arr['case-sensitive'] = ''; + $this->getResult()->setIndexedTagName($arr['aliases'], 'alias'); + $data[] = $arr; + } + $this->getResult()->setIndexedTagName($data, 'magicword'); + $this->getResult()->addValue('query', $property, $data); + } protected function appendInterwikiMap( $property, $filter ) { $this->resetQueryParams(); @@ -174,7 +203,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $this->addWhere( 'iw_local = 1' ); elseif( $filter === '!local' ) $this->addWhere( 'iw_local = 0' ); - elseif( $filter !== false ) + elseif( $filter ) ApiBase :: dieDebug( __METHOD__, "Unknown filter=$filter" ); $this->addOption( 'ORDER BY', 'iw_prefix' ); @@ -239,7 +268,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['edits'] = intval( SiteStats::edits() ); $data['images'] = intval( SiteStats::images() ); $data['users'] = intval( SiteStats::users() ); - $data['admins'] = intval( SiteStats::admins() ); + $data['activeusers'] = intval( SiteStats::activeUsers() ); + $data['admins'] = intval( SiteStats::numberingroup('sysop') ); $data['jobs'] = intval( SiteStats::jobs() ); $this->getResult()->addValue( 'query', $property, $data ); } @@ -257,6 +287,40 @@ class ApiQuerySiteinfo extends ApiQueryBase { $this->getResult()->addValue( 'query', $property, $data ); } + protected function appendExtensions( $property ) { + global $wgExtensionCredits; + $data = array(); + foreach ( $wgExtensionCredits as $type => $extensions ) { + foreach ( $extensions as $ext ) { + $ret = array(); + $ret['type'] = $type; + if ( isset( $ext['name'] ) ) + $ret['name'] = $ext['name']; + if ( isset( $ext['description'] ) ) + $ret['description'] = $ext['description']; + if ( isset( $ext['descriptionmsg'] ) ) + $ret['descriptionmsg'] = $ext['descriptionmsg']; + if ( isset( $ext['author'] ) ) { + $ret['author'] = is_array( $ext['author'] ) ? + implode( ', ', $ext['author' ] ) : $ext['author']; + } + if ( isset( $ext['version'] ) ) { + $ret['version'] = $ext['version']; + } elseif ( isset( $ext['svn-revision'] ) && + preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/', + $ext['svn-revision'], $m ) ) + { + $ret['version'] = 'r' . $m[1]; + } + $data[] = $ret; + } + } + + $this->getResult()->setIndexedTagName( $data, 'ext' ); + $this->getResult()->addValue( 'query', $property, $data ); + } + + public function getAllowedParams() { return array( 'prop' => array( @@ -267,10 +331,12 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'namespaces', 'namespacealiases', 'specialpagealiases', + 'magicwords', 'interwikimap', 'dbrepllag', 'statistics', 'usergroups', + 'extensions', ) ), 'filteriw' => array( @@ -288,13 +354,15 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'prop' => array( 'Which sysinfo properties to get:', ' "general" - Overall system information', - ' "namespaces" - List of registered namespaces (localized)', + ' "namespaces" - List of registered namespaces and their canonical names', ' "namespacealiases" - List of registered namespace aliases', ' "specialpagealiases" - List of special page aliases', + ' "magicwords" - List of magic words and their aliases', ' "statistics" - Returns site statistics', ' "interwikimap" - Returns interwiki map (optionally filtered)', ' "dbrepllag" - Returns database server with the highest replication lag', ' "usergroups" - Returns user groups and the associated permissions', + ' "extensions" - Returns extensions installed on the wiki', ), 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', 'showalldb' => 'List all database servers, not just the one lagging the most', @@ -314,6 +382,6 @@ class ApiQuerySiteinfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 37034 2008-07-04 09:21:11Z vasilievvv $'; + return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 44862 2008-12-20 23:49:16Z catrope $'; } } diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index c477acdb..be6c8bc4 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -62,6 +62,7 @@ class ApiQueryContributions extends ApiQueryBase { if(isset($this->params['userprefix'])) { $this->prefixMode = true; + $this->multiUserMode = true; $this->userprefix = $this->params['userprefix']; } else @@ -72,6 +73,7 @@ class ApiQueryContributions extends ApiQueryBase { foreach($this->params['user'] as $u) $this->prepareUsername($u); $this->prefixMode = false; + $this->multiUserMode = (count($this->params['user']) > 1); } $this->prepareQuery(); @@ -87,7 +89,10 @@ class ApiQueryContributions extends ApiQueryBase { while ( $row = $db->fetchObject( $res ) ) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp)); + if($this->multiUserMode) + $this->setContinueEnumParameter('continue', $this->continueStr($row)); + else + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp)); break; } @@ -132,13 +137,28 @@ class ApiQueryContributions extends ApiQueryBase { //anything we retrieve. $this->addTables(array('revision', 'page')); $this->addWhere('page_id=rev_page'); + + // Handle continue parameter + if($this->multiUserMode && !is_null($this->params['continue'])) + { + $continue = explode('|', $this->params['continue']); + if(count($continue) != 2) + $this->dieUsage("Invalid continue param. You should pass the original " . + "value returned by the previous query", "_badcontinue"); + $encUser = $this->getDB()->strencode($continue[0]); + $encTS = wfTimestamp(TS_MW, $continue[1]); + $op = ($this->params['dir'] == 'older' ? '<' : '>'); + $this->addWhere("rev_user_text $op '$encUser' OR " . + "(rev_user_text = '$encUser' AND " . + "rev_timestamp $op= '$encTS')"); + } $this->addWhereFld('rev_deleted', 0); // We only want pages by the specified users. if($this->prefixMode) - $this->addWhere("rev_user_text LIKE '" . $this->getDb()->escapeLike($this->userprefix) . "%'"); + $this->addWhere("rev_user_text LIKE '" . $this->getDB()->escapeLike($this->userprefix) . "%'"); else - $this->addWhereFld( 'rev_user_text', $this->usernames ); + $this->addWhereFld('rev_user_text', $this->usernames); // ... and in the specified timeframe. // Ensure the same sort order for rev_user_text and rev_timestamp // so our query is indexed @@ -157,6 +177,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->addWhereIf('rev_minor_edit != 0', isset ($show['minor'])); } $this->addOption('LIMIT', $this->params['limit'] + 1); + $this->addOption( 'USE INDEX', array( 'revision' => 'usertext_timestamp' ) ); // Mandatory fields: timestamp allows request continuation // ns+title checks if the user has access rights for this page @@ -207,11 +228,17 @@ class ApiQueryContributions extends ApiQueryBase { $vals['top'] = ''; } - if ($this->fld_comment && !empty ($row->rev_comment)) + if ($this->fld_comment && isset( $row->rev_comment ) ) $vals['comment'] = $row->rev_comment; return $vals; } + + private function continueStr($row) + { + return $row->rev_user_text . '|' . + wfTimestamp(TS_ISO_8601, $row->rev_timestamp); + } public function getAllowedParams() { return array ( @@ -228,6 +255,7 @@ class ApiQueryContributions extends ApiQueryBase { 'end' => array ( ApiBase :: PARAM_TYPE => 'timestamp' ), + 'continue' => null, 'user' => array ( ApiBase :: PARAM_ISMULTI => true ), @@ -269,6 +297,7 @@ class ApiQueryContributions extends ApiQueryBase { 'limit' => 'The maximum number of contributions to return.', 'start' => 'The start timestamp to return from.', 'end' => 'The end timestamp to return to.', + 'continue' => 'When more results are available, use this to continue.', 'user' => 'The user to retrieve contributions for.', 'userprefix' => 'Retrieve contibutions for all users whose names begin with this value. Overrides ucuser.', 'dir' => 'The direction to search (older or newer).', @@ -290,6 +319,6 @@ class ApiQueryContributions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserContributions.php 37383 2008-07-09 11:44:49Z btongminh $'; + return __CLASS__ . ': $Id: ApiQueryUserContributions.php 43271 2008-11-06 22:38:42Z siebrand $'; } } diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index 2d55a352..203b7e25 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -76,12 +76,16 @@ class ApiQueryUserInfo extends ApiQueryBase { $result->setIndexedTagName($vals['groups'], 'g'); // even if empty } if (isset($this->prop['rights'])) { - $vals['rights'] = $wgUser->getRights(); + // User::getRights() may return duplicate values, strip them + $vals['rights'] = array_values(array_unique($wgUser->getRights())); $result->setIndexedTagName($vals['rights'], 'r'); // even if empty } if (isset($this->prop['options'])) { $vals['options'] = (is_null($wgUser->mOptions) ? User::getDefaultOptions() : $wgUser->mOptions); } + if (isset($this->prop['preferencestoken']) && is_null($this->getMain()->getRequest()->getVal('callback'))) { + $vals['preferencestoken'] = $wgUser->editToken(); + } if (isset($this->prop['editcount'])) { $vals['editcount'] = $wgUser->getEditCount(); } @@ -110,6 +114,7 @@ class ApiQueryUserInfo extends ApiQueryBase { if(!$wgUser->isAnon()) $categories[] = 'newbie'; } + $categories = array_merge($categories, $wgUser->getGroups()); // Now get the actual limits $retval = array(); @@ -134,6 +139,7 @@ class ApiQueryUserInfo extends ApiQueryBase { 'groups', 'rights', 'options', + 'preferencestoken', 'editcount', 'ratelimits' ) @@ -168,6 +174,6 @@ class ApiQueryUserInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserInfo.php 35186 2008-05-22 16:39:43Z brion $'; + return __CLASS__ . ': $Id: ApiQueryUserInfo.php 43764 2008-11-20 15:15:00Z catrope $'; } } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index a8147567..e50d8d82 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -68,15 +68,13 @@ if (!defined('MEDIAWIKI')) { else $goodNames[] = $n; } - if(empty($goodNames)) + if(!count($goodNames)) return $retval; - $db = $this->getDb(); + $db = $this->getDB(); $this->addTables('user', 'u1'); - $this->addFields('u1.user_name'); + $this->addFields('u1.*'); $this->addWhereFld('u1.user_name', $goodNames); - $this->addFieldsIf('u1.user_editcount', isset($this->prop['editcount'])); - $this->addFieldsIf('u1.user_registration', isset($this->prop['registration'])); if(isset($this->prop['groups'])) { $this->addTables('user_groups'); @@ -96,20 +94,26 @@ if (!defined('MEDIAWIKI')) { $data = array(); $res = $this->select(__METHOD__); while(($r = $db->fetchObject($res))) { - $data[$r->user_name]['name'] = $r->user_name; + $user = User::newFromRow($r); + $name = $user->getName(); + $data[$name]['name'] = $name; if(isset($this->prop['editcount'])) - $data[$r->user_name]['editcount'] = $r->user_editcount; + // No proper member function in User class for this + $data[$name]['editcount'] = $r->user_editcount; if(isset($this->prop['registration'])) - $data[$r->user_name]['registration'] = wfTimestampOrNull(TS_ISO_8601, $r->user_registration); + // Nor for this one + $data[$name]['registration'] = wfTimestampOrNull(TS_ISO_8601, $r->user_registration); if(isset($this->prop['groups'])) // This row contains only one group, others will be added from other rows if(!is_null($r->ug_group)) - $data[$r->user_name]['groups'][] = $r->ug_group; + $data[$name]['groups'][] = $r->ug_group; if(isset($this->prop['blockinfo'])) if(!is_null($r->blocker_name)) { - $data[$r->user_name]['blockedby'] = $r->blocker_name; - $data[$r->user_name]['blockreason'] = $r->ipb_reason; + $data[$name]['blockedby'] = $r->blocker_name; + $data[$name]['blockreason'] = $r->ipb_reason; } + if(isset($this->prop['emailable']) && $user->canReceiveEmail()) + $data[$name]['emailable'] = ''; } // Second pass: add result data to $retval @@ -134,7 +138,8 @@ if (!defined('MEDIAWIKI')) { 'blockinfo', 'groups', 'editcount', - 'registration' + 'registration', + 'emailable', ) ), 'users' => array( @@ -147,9 +152,11 @@ if (!defined('MEDIAWIKI')) { return array ( 'prop' => array( 'What pieces of information to include', - ' blockinfo - tags if the user is blocked, by whom, and for what reason', - ' groups - lists all the groups the user belongs to', - ' editcount - adds the user\'s edit count' + ' blockinfo - tags if the user is blocked, by whom, and for what reason', + ' groups - lists all the groups the user belongs to', + ' editcount - adds the user\'s edit count', + ' registration - adds the user\'s registration timestamp', + ' emailable - tags if the user can and wants to receive e-mail through [[Special:Emailuser]]', ), 'users' => 'A list of users to obtain the same information for' ); @@ -164,6 +171,6 @@ if (!defined('MEDIAWIKI')) { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUsers.php 38183 2008-07-29 12:58:04Z rotem $'; + return __CLASS__ . ': $Id: ApiQueryUsers.php 44231 2008-12-04 14:42:30Z catrope $'; } } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index d17e83f6..ed3482fb 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -59,12 +59,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if (!$wgUser->isLoggedIn()) $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin'); - $allrev = $start = $end = $namespace = $dir = $limit = $prop = $show = null; - extract($this->extractRequestParams()); + $params = $this->extractRequestParams(); - if (!is_null($prop) && is_null($resultPageSet)) { + if (!is_null($params['prop']) && is_null($resultPageSet)) { - $prop = array_flip($prop); + $prop = array_flip($params['prop']); $this->fld_ids = isset($prop['ids']); $this->fld_title = isset($prop['title']); @@ -76,8 +75,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->fld_patrol = isset($prop['patrol']); if ($this->fld_patrol) { - global $wgUseRCPatrol, $wgUser; - if (!$wgUseRCPatrol || !$wgUser->isAllowed('patrol')) + global $wgUser; + if (!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol()) $this->dieUsage('patrol property is not available', 'patrol'); } } @@ -100,7 +99,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addFieldsIf('rc_old_len', $this->fld_sizes); $this->addFieldsIf('rc_new_len', $this->fld_sizes); } - elseif ($allrev) { + elseif ($params['allrev']) { $this->addFields(array ( 'rc_this_oldid', 'rc_namespace', @@ -131,20 +130,26 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'rc_deleted' => 0, )); - $this->addWhereRange('rc_timestamp', $dir, $start, $end); - $this->addWhereFld('wl_namespace', $namespace); - $this->addWhereIf('rc_this_oldid=page_latest', !$allrev); + $this->addWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']); + $this->addWhereFld('wl_namespace', $params['namespace']); + $this->addWhereIf('rc_this_oldid=page_latest', !$params['allrev']); - if (!is_null($show)) { - $show = array_flip($show); + if (!is_null($params['show'])) { + $show = array_flip($params['show']); /* Check for conflicting parameters. */ if ((isset ($show['minor']) && isset ($show['!minor'])) || (isset ($show['bot']) && isset ($show['!bot'])) - || (isset ($show['anon']) && isset ($show['!anon']))) { + || (isset ($show['anon']) && isset ($show['!anon'])) + || (isset ($show['patrolled']) && isset ($show['!patrolled']))) { $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); } + + // Check permissions + global $wgUser; + if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol()) + $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied'); /* Add additional conditions to query depending upon parameters. */ $this->addWhereIf('rc_minor = 0', isset ($show['!minor'])); @@ -153,13 +158,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addWhereIf('rc_bot != 0', isset ($show['bot'])); $this->addWhereIf('rc_user = 0', isset ($show['anon'])); $this->addWhereIf('rc_user != 0', isset ($show['!anon'])); + $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled'])); + $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled'])); } # This is an index optimization for mysql, as done in the Special:Watchlist page - $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end) && $wgDBtype == 'mysql'); + $this->addWhereIf("rc_timestamp > ''", !isset ($params['start']) && !isset ($params['end']) && $wgDBtype == 'mysql'); - $this->addOption('LIMIT', $limit +1); + $this->addOption('LIMIT', $params['limit'] +1); $data = array (); $count = 0; @@ -167,7 +174,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $db = $this->getDB(); while ($row = $db->fetchObject($res)) { - if (++ $count > $limit) { + if (++ $count > $params['limit']) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp)); break; @@ -178,7 +185,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if ($vals) $data[] = $vals; } else { - if ($allrev) { + if ($params['allrev']) { $data[] = intval($row->rc_this_oldid); } else { $data[] = intval($row->rc_cur_id); @@ -192,7 +199,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->getResult()->setIndexedTagName($data, 'item'); $this->getResult()->addValue('query', $this->getModuleName(), $data); } - elseif ($allrev) { + elseif ($params['allrev']) { $resultPageSet->populateFromRevisionIDs($data); } else { $resultPageSet->populateFromPageIDs($data); @@ -237,7 +244,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $vals['newlen'] = intval($row->rc_new_len); } - if ($this->fld_comment && !empty ($row->rc_comment)) + if ($this->fld_comment && isset( $row->rc_comment )) $vals['comment'] = $row->rc_comment; return $vals; @@ -292,7 +299,9 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'bot', '!bot', 'anon', - '!anon' + '!anon', + 'patrolled', + '!patrolled', ) ) ); @@ -329,6 +338,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryWatchlist.php 37909 2008-07-22 13:26:15Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryWatchlist.php 44719 2008-12-17 16:34:01Z catrope $'; } } diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php new file mode 100644 index 00000000..e9951b42 --- /dev/null +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -0,0 +1,179 @@ +<?php + +/* + * Created on Oct 4, 2008 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * This query action allows clients to retrieve a list of pages + * on the logged-in user's watchlist. + * + * @ingroup API + */ +class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'wr'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + global $wgUser; + + $this->selectNamedDB('watchlist', DB_SLAVE, 'watchlist'); + + if (!$wgUser->isLoggedIn()) + $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin'); + $params = $this->extractRequestParams(); + $prop = array_flip((array)$params['prop']); + $show = array_flip((array)$params['show']); + if(isset($show['changed']) && isset($show['!changed'])) + $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); + + $this->addTables('watchlist'); + $this->addFields(array('wl_namespace', 'wl_title')); + $this->addFieldsIf('wl_notificationtimestamp', isset($prop['changed'])); + $this->addWhereFld('wl_user', $wgUser->getId()); + $this->addWhereFld('wl_namespace', $params['namespace']); + $this->addWhereIf('wl_notificationtimestamp IS NOT NULL', isset($show['changed'])); + $this->addWhereIf('wl_notificationtimestamp IS NULL', isset($show['!changed'])); + if(isset($params['continue'])) + { + $cont = explode('|', $params['continue']); + if(count($cont) != 2) + $this->dieUsage("Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue"); + $ns = intval($cont[0]); + $title = $this->getDB()->strencode($this->titleToKey($cont[1])); + $this->addWhere("wl_namespace > '$ns' OR ". + "(wl_namespace = '$ns' AND ". + "wl_title >= '$title')"); + } + // Don't ORDER BY wl_namespace if it's constant in the WHERE clause + if(count($params['namespace']) == 1) + $this->addOption('ORDER BY', 'wl_title'); + else + $this->addOption('ORDER BY', 'wl_namespace, wl_title'); + $this->addOption('LIMIT', $params['limit'] + 1); + $res = $this->select(__METHOD__); + + $db = $this->getDB(); + $data = array(); + $titles = array(); + $count = 0; + while($row = $db->fetchObject($res)) + { + if(++$count > $params['limit']) + { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', $row->wl_namespace . '|' . + $this->keyToTitle($row->wl_title)); + break; + } + $t = Title::makeTitle($row->wl_namespace, $row->wl_title); + if(is_null($resultPageSet)) + { + $vals = array(); + ApiQueryBase::addTitleInfo($vals, $t); + if(isset($prop['changed']) && !is_null($row->wl_notificationtimestamp)) + $vals['changed'] = wfTimestamp(TS_ISO_8601, $row->wl_notificationtimestamp); + $data[] = $vals; + } + else + $titles[] = $t; + } + if(is_null($resultPageSet)) + { + $this->getResult()->setIndexedTagName($data, 'wr'); + $this->getResult()->addValue(null, $this->getModuleName(), $data); + } + else + $resultPageSet->populateFromTitles($titles); + } + + public function getAllowedParams() { + return array ( + 'continue' => null, + 'namespace' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace' + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'changed', + ) + ), + 'show' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'changed', + '!changed', + ) + ) + ); + } + + public function getParamDescription() { + return array ( + 'continue' => 'When more results are available, use this to continue', + 'namespace' => 'Only list pages in the given namespace(s).', + 'limit' => 'How many total results to return per request.', + 'prop' => 'Which additional properties to get (non-generator mode only).', + 'show' => 'Only list items that meet these criteria.', + ); + } + + public function getDescription() { + return "Get all pages on the logged in user's watchlist"; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=watchlistraw', + 'api.php?action=query&generator=watchlistraw&gwrshow=changed&prop=revisions', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryWatchlistRaw.php 41651 2008-10-04 14:30:33Z catrope $'; + } +} diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 9e798d35..900953e0 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -100,7 +100,7 @@ class ApiResult extends ApiBase { } elseif (is_array($arr[$name]) && is_array($value)) { $merged = array_intersect_key($arr[$name], $value); - if (empty ($merged)) + if (!count($merged)) $arr[$name] += $value; else ApiBase :: dieDebug(__METHOD__, "Attempting to merge element $name"); @@ -180,18 +180,27 @@ class ApiResult extends ApiBase { } } - if (empty($name)) + if (!$name) $data[] = $value; // Add list element else ApiResult :: setElement($data, $name, $value); // Add named element } + /** + * Ensure all values in this result are valid UTF-8. + */ + public function cleanUpUTF8() + { + $data = & $this->getData(); + array_walk_recursive($data, array('UtfNormal', 'cleanUp')); + } + public function execute() { ApiBase :: dieDebug(__METHOD__, 'execute() is not supported on Result object'); } public function getVersion() { - return __CLASS__ . ': $Id: ApiResult.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiResult.php 45752 2009-01-14 21:36:57Z catrope $'; } } @@ -201,7 +210,7 @@ if (!function_exists('array_intersect_key')) { $argc = func_num_args(); if ($argc > 2) { - for ($i = 1; !empty($isec) && $i < $argc; $i++) { + for ($i = 1; $isec && $i < $argc; $i++) { $arr = func_get_arg($i); foreach (array_keys($isec) as $key) { diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 3739f694..653dca9e 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -37,7 +37,6 @@ class ApiRollback extends ApiBase { } public function execute() { - global $wgUser; $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); @@ -55,7 +54,10 @@ class ApiRollback extends ApiBase { if(!$titleObj->exists()) $this->dieUsageMsg(array('notanarticle')); - $username = User::getCanonicalName($params['user']); + #We need to be able to revert IPs, but getCanonicalName rejects them + $username = User::isIP($params['user']) + ? $params['user'] + : User::getCanonicalName($params['user']); if(!$username) $this->dieUsageMsg(array('invaliduser', $params['user'])); @@ -64,20 +66,17 @@ class ApiRollback extends ApiBase { $details = null; $retval = $articleObj->doRollback($username, $summary, $params['token'], $params['markbot'], $details); - if(!empty($retval)) + if($retval) // We don't care about multiple errors, just report one of them $this->dieUsageMsg(current($retval)); - $current = $target = $summary = NULL; - extract($details); - $info = array( 'title' => $titleObj->getPrefixedText(), - 'pageid' => $current->getPage(), - 'summary' => $summary, + 'pageid' => $details['current']->getPage(), + 'summary' => $details['summary'], 'revid' => $titleObj->getLatestRevID(), - 'old_revid' => $current->getID(), - 'last_revid' => $target->getID() + 'old_revid' => $details['current']->getID(), + 'last_revid' => $details['target']->getID() ); $this->getResult()->addValue(null, $this->getModuleName(), $info); @@ -99,7 +98,7 @@ class ApiRollback extends ApiBase { return array ( 'title' => 'Title of the page you want to rollback.', 'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.', - 'token' => 'A rollback token previously retrieved through prop=info', + 'token' => 'A rollback token previously retrieved through prop=revisions', 'summary' => 'Custom edit summary. If not set, default summary will be used.', 'markbot' => 'Mark the reverted edits and the revert as bot edits' ); @@ -107,8 +106,8 @@ class ApiRollback extends ApiBase { public function getDescription() { return array( - 'Undoes the last edit to the page. If the last user who edited the page made multiple edits in a row,', - 'they will all be rolled back. You need to be logged in as a sysop to use this function, see also action=login.' + 'Undo the last edit to the page. If the last user who edited the page made multiple edits in a row,', + 'they will all be rolled back.' ); } @@ -120,6 +119,6 @@ class ApiRollback extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiRollback.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiRollback.php 45043 2008-12-26 04:13:47Z mrzman $'; } } diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index d6a02a2a..cd52c518 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -64,14 +64,12 @@ class ApiUnblock extends ApiBase { $this->dieUsageMsg(array('sessionfailure')); if(!$wgUser->isAllowed('block')) $this->dieUsageMsg(array('cantunblock')); - if(wfReadOnly()) - $this->dieUsageMsg(array('readonlytext')); $id = $params['id']; $user = $params['user']; $reason = (is_null($params['reason']) ? '' : $params['reason']); $retval = IPUnblockForm::doUnblock($id, $user, $reason, $range); - if(!empty($retval)) + if($retval) $this->dieUsageMsg($retval); $res['id'] = $id; @@ -96,7 +94,7 @@ class ApiUnblock extends ApiBase { return array ( 'id' => 'ID of the block you want to unblock (obtained through list=blocks). Cannot be used together with user', 'user' => 'Username, IP address or IP range you want to unblock. Cannot be used together with id', - 'token' => 'An unblock token previously obtained through the gettoken parameter', + 'token' => 'An unblock token previously obtained through the gettoken parameter or prop=info', 'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken', 'reason' => 'Reason for unblock (optional)', ); @@ -116,6 +114,6 @@ class ApiUnblock extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiUnblock.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiUnblock.php 42651 2008-10-27 12:06:49Z catrope $'; } } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index e054a70e..7ae9a3c0 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -51,8 +51,6 @@ class ApiUndelete extends ApiBase { $this->dieUsageMsg(array('permdenied-undelete')); if($wgUser->isBlocked()) $this->dieUsageMsg(array('blockedtext')); - if(wfReadOnly()) - $this->dieUsageMsg(array('readonlytext')); if(!$wgUser->matchEditToken($params['token'])) $this->dieUsageMsg(array('sessionfailure')); @@ -69,7 +67,7 @@ class ApiUndelete extends ApiBase { $params['timestamps'][$i] = wfTimestamp(TS_MW, $ts); $pa = new PageArchive($titleObj); - $dbw = wfGetDb(DB_MASTER); + $dbw = wfGetDB(DB_MASTER); $dbw->begin(); $retval = $pa->undelete((isset($params['timestamps']) ? $params['timestamps'] : array()), $params['reason']); if(!is_array($retval)) @@ -123,6 +121,6 @@ class ApiUndelete extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiUndelete.php 35348 2008-05-26 10:51:31Z catrope $'; + return __CLASS__ . ': $Id: ApiUndelete.php 43270 2008-11-06 22:30:55Z siebrand $'; } } diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php new file mode 100644 index 00000000..ab122fea --- /dev/null +++ b/includes/api/ApiWatch.php @@ -0,0 +1,99 @@ +<?php + +/* + * Created on Jan 4, 2008 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com, + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +/** + * API module to allow users to log out of the wiki. API equivalent of + * Special:Userlogout. + * + * @ingroup API + */ +class ApiWatch extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + $this->getMain()->requestWriteMode(); + if(!$wgUser->isLoggedIn()) + $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin'); + $params = $this->extractRequestParams(); + $title = Title::newFromText($params['title']); + if(!$title) + $this->dieUsageMsg(array('invalidtitle', $params['title'])); + $article = new Article($title); + $res = array('title' => $title->getPrefixedText()); + if($params['unwatch']) + { + $res['unwatched'] = ''; + $success = $article->doUnwatch(); + } + else + { + $res['watched'] = ''; + $success = $article->doWatch(); + } + if(!$success) + $this->dieUsageMsg(array('hookaborted')); + $this->getResult()->addValue(null, $this->getModuleName(), $res); + } + + public function getAllowedParams() { + return array ( + 'title' => null, + 'unwatch' => false, + ); + } + + public function getParamDescription() { + return array ( + 'title' => 'The page to (un)watch', + 'unwatch' => 'If set the page will be unwatched rather than watched', + ); + } + + public function getDescription() { + return array ( + 'Add or remove a page from/to the current user\'s watchlist' + ); + } + + protected function getExamples() { + return array( + 'api.php?action=watch&title=Main_Page', + 'api.php?action=watch&title=Main_Page&unwatch', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiWatch.php 40460 2008-09-04 22:20:32Z ialex $'; + } +} diff --git a/includes/db/Database.php b/includes/db/Database.php index 885ede54..84b88643 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -205,12 +205,17 @@ class Database { return false; } - /**#@+ - * Get function + /** + * Return the last query that went through Database::query() + * @return String */ function lastQuery() { return $this->mLastQuery; } + + /** + * Is a connection to the database open? + * @return Boolean + */ function isOpen() { return $this->mOpened; } - /**#@-*/ function setFlag( $flag ) { $this->mFlags |= $flag; @@ -243,13 +248,13 @@ class Database { # Other functions #------------------------------------------------------------------------------ - /**@{{ + /** * Constructor. - * @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 $server String: database server host + * @param $user String: database user name + * @param $password String: database user password + * @param $dbName String: database name + * @param $failFunction * @param $flags * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php */ @@ -293,7 +298,11 @@ class Database { } /** - * @static + * Same as new Database( ... ), kept for backward compatibility + * @param $server String: database server host + * @param $user String: database user name + * @param $password String: database user password + * @param $dbName String: database name * @param failFunction * @param $flags */ @@ -305,9 +314,13 @@ class Database { /** * Usually aborts on failure * If the failFunction is set to a non-zero integer, returns success + * @param $server String: database server host + * @param $user String: database user name + * @param $password String: database user password + * @param $dbName String: database name */ function open( $server, $user, $password, $dbName ) { - global $wguname, $wgAllDBsAreLocalhost; + global $wgAllDBsAreLocalhost; wfProfileIn( __METHOD__ ); # Test for missing mysql.so @@ -338,13 +351,17 @@ class Database { wfProfileIn("dbconnect-$server"); - # Try to connect up to three times # The kernel's default SYN retransmission period is far too slow for us, - # so we use a short timeout plus a manual retry. + # so we use a short timeout plus a manual retry. Retrying means that a small + # but finite rate of SYN packet loss won't cause user-visible errors. $this->mConn = false; - $max = 3; + if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) { + $numAttempts = 2; + } else { + $numAttempts = 1; + } $this->installErrorHandler(); - for ( $i = 0; $i < $max && !$this->mConn; $i++ ) { + for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) { if ( $i > 1 ) { usleep( 1000 ); } @@ -360,23 +377,28 @@ class Database { } } $phpError = $this->restoreErrorHandler(); + # Always log connection errors + if ( !$this->mConn ) { + $error = $this->lastError(); + if ( !$error ) { + $error = $phpError; + } + wfLogDBError( "Error connecting to {$this->mServer}: $error\n" ); + wfDebug( "DB connection error\n" ); + wfDebug( "Server: $server, User: $user, Password: " . + substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" ); + $success = false; + } wfProfileOut("dbconnect-$server"); - 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"; - wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \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; + if ( $dbName != '' && $this->mConn !== false ) { + $success = @/**/mysql_select_db( $dbName, $this->mConn ); + if ( !$success ) { + $error = "Error selecting database $dbName on server {$this->mServer} " . + "from client host " . wfHostname() . "\n"; + wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n"); + wfDebug( $error ); } } else { # Delay USE query @@ -405,16 +427,25 @@ class Database { wfProfileOut( __METHOD__ ); return $success; } - /**@}}*/ protected function installErrorHandler() { $this->mPHPError = false; + $this->htmlErrors = ini_set( 'html_errors', '0' ); set_error_handler( array( $this, 'connectionErrorHandler' ) ); } protected function restoreErrorHandler() { restore_error_handler(); - return $this->mPHPError; + if ( $this->htmlErrors !== false ) { + ini_set( 'html_errors', $this->htmlErrors ); + } + if ( $this->mPHPError ) { + $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError ); + $error = preg_replace( '!^.*?:(.*)$!', '$1', $error ); + return $error; + } else { + return false; + } } protected function connectionErrorHandler( $errno, $errstr ) { @@ -425,7 +456,7 @@ class Database { * Closes a database connection. * if it is open : commits any open transactions * - * @return bool operation success. true if already closed. + * @return Bool operation success. true if already closed. */ function close() { @@ -441,7 +472,7 @@ class Database { } /** - * @param string $error fallback error message, used if none is given by MySQL + * @param $error String: fallback error message, used if none is given by MySQL */ function reportConnectionError( $error = 'Unknown error' ) { $myError = $this->lastError(); @@ -457,7 +488,6 @@ class Database { } } else { # New method - wfLogDBError( "Connection error: $error\n" ); throw new DBConnectionError( $this, $error ); } } @@ -468,7 +498,7 @@ class Database { * @param $sql String: SQL query * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST * comment (you can use __METHOD__ or add some extra info) - * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors... + * @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors... * maybe best to catch the exception instead? * @return true for a successful write query, ResultWrapper object for a successful read query, * or false on failure if $tempIgnore set @@ -572,7 +602,7 @@ class Database { * The DBMS-dependent part of query() * @param $sql String: SQL query. * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure - * @access private + * @private */ /*private*/ function doQuery( $sql ) { if( $this->bufferResults() ) { @@ -584,11 +614,11 @@ class Database { } /** - * @param $error - * @param $errno - * @param $sql - * @param string $fname - * @param bool $tempIgnore + * @param $error String + * @param $errno Integer + * @param $sql String + * @param $fname String + * @param $tempIgnore Boolean */ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { global $wgCommandLineMode; @@ -630,8 +660,8 @@ class Database { /** * 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 + * @param $prepared String: the prepared sql + * @param $args Mixed: Either an array here, or put scalars as varargs */ function execute( $prepared, $args = null ) { if( !is_array( $args ) ) { @@ -646,8 +676,8 @@ class Database { /** * Prepare & execute an SQL statement, quoting and inserting arguments * in the appropriate places. - * @param string $query - * @param string $args ... + * @param $query String + * @param $args ... */ function safeQuery( $query, $args = null ) { $prepared = $this->prepare( $query, 'Database::safeQuery' ); @@ -664,8 +694,8 @@ class Database { /** * 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 + * @param $preparedQuery String: a 'preparable' SQL statement + * @param $args Array of arguments to fill it with * @return string executable SQL */ function fillPrepared( $preparedQuery, $args ) { @@ -680,8 +710,8 @@ class Database { * The arguments should be in $this->preparedArgs and must not be touched * while we're doing this. * - * @param array $matches - * @return string + * @param $matches Array + * @return String * @private */ function fillPreparedArg( $matches ) { @@ -702,11 +732,9 @@ class Database { } } - /**#@+ - * @param mixed $res A SQL result - */ /** * Free a result object + * @param $res Mixed: A SQL result */ function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { @@ -758,6 +786,7 @@ class Database { /** * Get the number of rows in a result object + * @param $res Mixed: A SQL result */ function numRows( $res ) { if ( $res instanceof ResultWrapper ) { @@ -773,6 +802,7 @@ class Database { /** * Get the number of fields in a result object * See documentation for mysql_num_fields() + * @param $res Mixed: A SQL result */ function numFields( $res ) { if ( $res instanceof ResultWrapper ) { @@ -785,6 +815,8 @@ class Database { * Get a field name in a result object * See documentation for mysql_field_name(): * http://www.php.net/mysql_field_name + * @param $res Mixed: A SQL result + * @param $n Integer */ function fieldName( $res, $n ) { if ( $res instanceof ResultWrapper ) { @@ -808,6 +840,8 @@ class Database { /** * Change the position of the cursor in a result object * See mysql_data_seek() + * @param $res Mixed: A SQL result + * @param $row Mixed: Either MySQL row or ResultWrapper */ function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { @@ -854,7 +888,6 @@ class Database { * See mysql_affected_rows() for more details */ function affectedRows() { return mysql_affected_rows( $this->mConn ); } - /**#@-*/ // end of template : @param $result /** * Simple UPDATE wrapper @@ -864,8 +897,7 @@ class Database { * 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' ) - { + function set( $table, $var, $value, $cond, $fname = 'Database::set' ) { $table = $this->tableName( $table ); $sql = "UPDATE $table SET $var = '" . $this->strencode( $value ) . "' WHERE ($cond)"; @@ -890,7 +922,7 @@ class Database { $row = $this->fetchRow( $res ); if ( $row !== false ) { $this->freeResult( $res ); - return $row[0]; + return reset( $row ); } else { return false; } @@ -902,9 +934,9 @@ class Database { * * @private * - * @param array $options an associative array of options to be turned into + * @param $options Array: associative array of options to be turned into * an SQL query, valid keys are listed in the function. - * @return array + * @return Array */ function makeSelectOptions( $options ) { $preLimitTail = $postLimitTail = ''; @@ -953,14 +985,14 @@ class Database { /** * SELECT wrapper * - * @param mixed $table Array or string, table name(s) (prefix auto-added) - * @param mixed $vars Array or string, field name(s) to be retrieved - * @param mixed $conds Array or string, condition(s) for WHERE - * @param string $fname Calling function name (use __METHOD__) for logs/profiling - * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see Database::makeSelectOptions code for list of supported stuff - * @param array $join_conds Associative array of table join conditions (optional) - * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * @param $table Mixed: Array or string, table name(s) (prefix auto-added) + * @param $vars Mixed: Array or string, field name(s) to be retrieved + * @param $conds Mixed: Array or string, condition(s) for WHERE + * @param $fname String: Calling function name (use __METHOD__) for logs/profiling + * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * see Database::makeSelectOptions code for list of supported stuff + * @param $join_conds Array: Associative array of table join conditions (optional) + * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure */ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) @@ -972,14 +1004,14 @@ class Database { /** * SELECT wrapper * - * @param mixed $table Array or string, table name(s) (prefix auto-added) - * @param mixed $vars Array or string, field name(s) to be retrieved - * @param mixed $conds Array or string, condition(s) for WHERE - * @param string $fname Calling function name (use __METHOD__) for logs/profiling - * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see Database::makeSelectOptions code for list of supported stuff - * @param array $join_conds Associative array of table join conditions (optional) - * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * @param $table Mixed: Array or string, table name(s) (prefix auto-added) + * @param $vars Mixed: Array or string, field name(s) to be retrieved + * @param $conds Mixed: Array or string, condition(s) for WHERE + * @param $fname String: Calling function name (use __METHOD__) for logs/profiling + * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * see Database::makeSelectOptions code for list of supported stuff + * @param $join_conds Array: Associative array of table join conditions (optional) + * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) * @return string, the SQL text */ function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) { @@ -1030,13 +1062,17 @@ class Database { * Single row SELECT wrapper * Aborts or returns FALSE on error * - * $vars: the selected variables - * $conds: a condition map, terms are ANDed together. + * @param $table String: table name + * @param $vars String: the selected variables + * @param $conds Array: 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 + * @param $fname String: Calling functio name + * @param $options Array + * @param $join_conds Array * * @todo migrate documentation to phpdocumentor format */ @@ -1086,8 +1122,7 @@ class Database { * 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 + * @param $sql String: A SQL Query */ static function generalizeSQL( $sql ) { # This does the same as the regexp below would do, but in such a way @@ -1280,7 +1315,7 @@ class Database { * Make UPDATE options for the Database::update function * * @private - * @param array $options The options passed to Database::update + * @param $options Array: The options passed to Database::update * @return string */ function makeUpdateOptions( $options ) { @@ -1298,14 +1333,14 @@ class Database { /** * 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 + * @param $table String: The table to UPDATE + * @param $values Array: An array of values to SET + * @param $conds Array: An array of conditions (WHERE). Use '*' to update all rows. + * @param $fname String: The Class::Function calling this function + * (for the log) + * @param $options Array: An array of UPDATE options, can be one or * more of IGNORE, LOW_PRIORITY - * @return bool + * @return Boolean */ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) { $table = $this->tableName( $table ); @@ -1410,8 +1445,8 @@ class Database { * themselves. Pass the canonical name to such functions. This is only needed * when calling query() directly. * - * @param string $name database table name - * @return string full database name + * @param $name String: database table name + * @return String: full database name */ function tableName( $name ) { global $wgSharedDB, $wgSharedPrefix, $wgSharedTables; @@ -1540,8 +1575,8 @@ class Database { /** * Wrapper for addslashes() - * @param string $s String to be slashed. - * @return string slashed string. + * @param $s String: to be slashed. + * @return String: slashed string. */ function strencode( $s ) { return mysql_real_escape_string( $s, $this->mConn ); @@ -1632,11 +1667,12 @@ class Database { * * 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 + * @param $delTable String: The table to delete from. + * @param $joinTable String: The other table. + * @param $delVar String: The variable to join on, in the first table. + * @param $joinVar String: The variable to join on, in the second table. + * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause + * @param $fname String: Calling function name (use __METHOD__) for logs/profiling */ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) { if ( !$conds ) { @@ -1732,9 +1768,9 @@ class Database { /** * 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) + * @param $sql String: SQL query we will append the limit too + * @param $limit Integer: the SQL limit + * @param $offset Integer the SQL offset (default false) */ function limitResult($sql, $limit, $offset=false) { if( !is_numeric($limit) ) { @@ -1752,10 +1788,10 @@ class Database { * 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 + * @param $cond String: SQL expression which will result in a boolean value + * @param $trueVal String: SQL expression to return if true + * @param $falseVal String: SQL expression to return if false + * @return String: SQL fragment */ function conditional( $cond, $trueVal, $falseVal ) { return " IF($cond, $trueVal, $falseVal) "; @@ -1765,9 +1801,9 @@ class Database { * Returns a comand for str_replace function in SQL query. * Uses REPLACE() in MySQL * - * @param string $orig String or column to modify - * @param string $old String or column to seek - * @param string $new String or column to replace with + * @param $orig String: column to modify + * @param $old String: column to seek + * @param $new String: column to replace with */ function strreplace( $orig, $old, $new ) { return "REPLACE({$orig}, {$old}, {$new})"; @@ -1838,9 +1874,8 @@ class Database { /** * 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 + * @param $pos MySQLMasterPos object + * @param $timeout Integer: the maximum number of seconds to wait for synchronisation */ function masterPosWait( MySQLMasterPos $pos, $timeout ) { $fname = 'Database::masterPosWait'; @@ -1896,7 +1931,8 @@ class Database { $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' ); $row = $this->fetchObject( $res ); if ( $row ) { - return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos ); + $pos = isset($row->Exec_master_log_pos) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos; + return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos ); } else { return false; } @@ -2001,14 +2037,14 @@ class Database { } /** - * @return string wikitext of a link to the server software's web site + * @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 + * @return String: Version information from the database */ function getServerVersion() { return mysql_get_server_info( $this->mConn ); @@ -2106,7 +2142,7 @@ class Database { * May be useful for very long batch queries such as * full-wiki dumps, where a single query reads out * over hours or days. - * @param int $timeout in seconds + * @param $timeout Integer in seconds */ public function setTimeout( $timeout ) { $this->query( "SET net_read_timeout=$timeout" ); @@ -2116,9 +2152,9 @@ class Database { /** * Read and execute SQL commands from a file. * Returns true on success, error string or exception on failure (depending on object's error ignore settings) - * @param string $filename File name to open - * @param callback $lineCallback Optional function called before reading each line - * @param callback $resultCallback Optional function called for each MySQL result + * @param $filename String: File name to open + * @param $lineCallback Callback: Optional function called before reading each line + * @param $resultCallback Callback: Optional function called for each MySQL result */ function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) { $fp = fopen( $filename, 'r' ); @@ -2133,9 +2169,9 @@ class Database { /** * Read and execute commands from an open file handle * Returns true on success, error string or exception on failure (depending on object's error ignore settings) - * @param string $fp File handle - * @param callback $lineCallback Optional function called before reading each line - * @param callback $resultCallback Optional function called for each MySQL result + * @param $fp String: File handle + * @param $lineCallback Callback: Optional function called before reading each line + * @param $resultCallback Callback: Optional function called for each MySQL result */ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) { $cmd = ""; @@ -2177,7 +2213,7 @@ class Database { $cmd = $this->replaceVars( $cmd ); $res = $this->query( $cmd, __METHOD__ ); if ( $resultCallback ) { - call_user_func( $resultCallback, $res ); + call_user_func( $resultCallback, $res, $this ); } if ( false === $res ) { @@ -2240,8 +2276,8 @@ class Database { * Abstracted from Filestore::lock() so child classes can implement for * their own needs. * - * @param string $lockName Name of lock to aquire - * @param string $method Name of method calling us + * @param $lockName String: Name of lock to aquire + * @param $method String: Name of method calling us * @return bool */ public function lock( $lockName, $method ) { @@ -2263,14 +2299,24 @@ class Database { * @todo fixme - Figure out a way to return a bool * based on successful lock release. * - * @param string $lockName Name of lock to release - * @param string $method Name of method calling us + * @param $lockName String: Name of lock to release + * @param $method String: Name of method calling us */ public function unlock( $lockName, $method ) { $lockName = $this->addQuotes( $lockName ); $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method ); $this->freeResult( $result ); } + + /** + * Get search engine class. All subclasses of this + * need to implement this if they wish to use searching. + * + * @return String + */ + public function getSearchEngine() { + return "SearchMySQL"; + } } /** @@ -2390,8 +2436,8 @@ class DBError extends MWException { /** * 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 + * @param $db Database object which threw the error + * @param $error A simple error message to be used for debugging */ function __construct( Database &$db, $error ) { $this->db =& $db; @@ -2483,7 +2529,13 @@ border=\"0\" ALT=\"Google\"></A> } $text = str_replace( '$1', $this->error, $noconnect ); - $text .= wfGetSiteNotice(); + + /* + if ( $GLOBALS['wgShowExceptionDetails'] ) { + $text .= '</p><p>Backtrace:</p><p>' . + nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . + "</p>\n"; + }*/ if($wgUseFileCache) { if($wgTitle) { @@ -2504,13 +2556,13 @@ border=\"0\" ALT=\"Google\"></A> $cache = new HTMLFileCache( $t ); if( $cache->isFileCached() ) { // @todo, FIXME: $msg is not defined on the next line. - $msg = '<p style="color: red"><b>'.$msg."<br />\n" . + $msg = '<p style="color: red"><b>'.$text."<br />\n" . $cachederror . "</b></p>\n"; $tag = '<div id="article">'; $text = str_replace( $tag, - $tag . $msg, + $tag . $text, $cache->fetchPageText() ); } } diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 32fe28b1..28ccab2d 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -105,7 +105,7 @@ class DatabaseMssql extends Database { $success = @/**/mssql_select_db($dbName, $this->mConn); if (!$success) { $error = "Error selecting database $dbName on server {$this->mServer} " . - "from client host {$wguname['nodename']}\n"; + "from client host " . wfHostname() . "\n"; wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n"); wfDebug( $error ); } @@ -154,9 +154,6 @@ class DatabaseMssql extends Database { return $ret; } - /**#@+ - * @param mixed $res A SQL result - */ /** * Free a result object */ @@ -225,6 +222,7 @@ class DatabaseMssql extends Database { /** * Get the number of fields in a result object * See documentation for mysql_num_fields() + * @param $res SQL result object as returned from Database::query(), etc. */ function numFields( $res ) { if ( $res instanceof ResultWrapper ) { @@ -237,6 +235,8 @@ class DatabaseMssql extends Database { * Get a field name in a result object * See documentation for mysql_field_name(): * http://www.php.net/mysql_field_name + * @param $res SQL result object as returned from Database::query(), etc. + * @param $n Int */ function fieldName( $res, $n ) { if ( $res instanceof ResultWrapper ) { @@ -263,6 +263,8 @@ class DatabaseMssql extends Database { /** * Change the position of the cursor in a result object * See mysql_data_seek() + * @param $res SQL result object as returned from Database::query(), etc. + * @param $row Database row */ function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { @@ -339,7 +341,7 @@ class DatabaseMssql extends Database { * * @private * - * @param array $options an associative array of options to be turned into + * @param $options Array: an associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return array */ @@ -390,11 +392,11 @@ class DatabaseMssql extends Database { /** * SELECT wrapper * - * @param mixed $table Array or string, table name(s) (prefix auto-added) - * @param mixed $vars Array or string, field name(s) to be retrieved - * @param mixed $conds Array or string, condition(s) for WHERE - * @param string $fname Calling function name (use __METHOD__) for logs/profiling - * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * @param $table Mixed: Array or string, table name(s) (prefix auto-added) + * @param $vars Mixed: Array or string, field name(s) to be retrieved + * @param $conds Mixed: Array or string, condition(s) for WHERE + * @param $fname String: Calling function name (use __METHOD__) for logs/profiling + * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), * see Database::makeSelectOptions code for list of supported stuff * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure */ @@ -643,12 +645,12 @@ class DatabaseMssql extends Database { /** * 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 + * @param $table String: The table to UPDATE + * @param $values Array: An array of values to SET + * @param $conds Array: An array of conditions (WHERE). Use '*' to update all rows. + * @param $fname String: The Class::Function calling this function + * (for the log) + * @param $options Array: An array of UPDATE options, can be one or * more of IGNORE, LOW_PRIORITY * @return bool */ @@ -666,7 +668,7 @@ class DatabaseMssql extends Database { * Make UPDATE options for the Database::update function * * @private - * @param array $options The options passed to Database::update + * @param $options Array: The options passed to Database::update * @return string */ function makeUpdateOptions( $options ) { @@ -698,7 +700,7 @@ class DatabaseMssql extends Database { /** * MSSQL doubles quotes instead of escaping them - * @param string $s String to be slashed. + * @param $s String to be slashed. * @return string slashed string. */ function strencode($s) { @@ -755,11 +757,12 @@ class DatabaseMssql extends Database { * * 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 + * @param $delTable String: The table to delete from. + * @param $joinTable String: The other table. + * @param $delVar String: The variable to join on, in the first table. + * @param $joinVar String: The variable to join on, in the second table. + * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause + * @param $fname String: Calling function name */ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) { if ( !$conds ) { @@ -857,9 +860,9 @@ class DatabaseMssql extends Database { /** * Returns an SQL expression for a simple conditional. * - * @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 + * @param $cond String: SQL expression which will result in a boolean value + * @param $trueVal String: SQL expression to return if true + * @param $falseVal String: SQL expression to return if false * @return string SQL fragment */ function conditional( $cond, $trueVal, $falseVal ) { @@ -1007,6 +1010,10 @@ class DatabaseMssql extends Database { public function unlock( $lockName, $method ) { return true; } + + public function getSearchEngine() { + return "SearchEngineDummy"; + } } diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index f4dbac71..4c37a507 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -509,10 +509,10 @@ class DatabaseOracle extends Database { * Returns an SQL expression for a simple conditional. * Uses CASE on Oracle * - * @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 + * @param $cond String: SQL expression which will result in a boolean value + * @param $trueVal String: SQL expression to return if true + * @param $falseVal String: SQL expression to return if false + * @return String: SQL fragment */ function conditional( $cond, $trueVal, $falseVal ) { return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; @@ -640,7 +640,7 @@ echo "error!\n"; * * @private * - * @param array $options an associative array of options to be turned into + * @param $options Array: an associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return array */ @@ -716,5 +716,9 @@ echo "error!\n"; public function unlock( $lockName, $method ) { return true; } + + public function getSearchEngine() { + return "SearchOracle"; + } } // end DatabaseOracle class diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 8fd04cb6..16a74b53 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -78,12 +78,6 @@ class DatabasePostgres extends Database { $failFunction = false, $flags = 0 ) { - global $wgOut; - # 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); @@ -138,10 +132,9 @@ class DatabasePostgres extends Database { global $wgDBport; - if (!strlen($user)) { ## e.g. the class is being loaded - return; + if (!strlen($user)) { ## e.g. the class is being loaded + return; } - $this->close(); $this->mServer = $server; $this->mPort = $port = $wgDBport; @@ -149,22 +142,31 @@ class DatabasePostgres extends Database { $this->mPassword = $password; $this->mDBname = $dbName; - $hstring=""; + $connectVars = array( + 'dbname' => $dbName, + 'user' => $user, + 'password' => $password ); if ($server!=false && $server!="") { - $hstring="host=$server "; + $connectVars['host'] = $server; } if ($port!=false && $port!="") { - $hstring .= "port=$port "; + $connectVars['port'] = $port; } + $connectString = $this->makeConnectionString( $connectVars ); - error_reporting( E_ALL ); - @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password"); + $this->installErrorHandler(); + $this->mConn = pg_connect( $connectString ); + $phpError = $this->restoreErrorHandler(); 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; + if ( !$this->mFailFunction ) { + throw new DBConnectionError( $this, $phpError ); + } else { + return false; + } } $this->mOpened = true; @@ -189,6 +191,14 @@ class DatabasePostgres extends Database { return $this->mConn; } + function makeConnectionString( $vars ) { + $s = ''; + foreach ( $vars as $name => $value ) { + $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' "; + } + return $s; + } + function initial_setup($password, $dbName) { // If this is the initial connection, setup the schema stuff and possibly create the user @@ -197,9 +207,8 @@ class DatabasePostgres extends Database { print "<li>Checking the version of Postgres..."; $version = $this->getServerVersion(); $PGMINVER = '8.1'; - if ($this->numeric_version < $PGMINVER) { - print "<b>FAILED</b>. Required version is $PGMINVER. You have " . - htmlspecialchars( $this->numeric_version ) . " (" . htmlspecialchars( $version ) . ")</li>\n"; + if ($version < $PGMINVER) { + print "<b>FAILED</b>. Required version is $PGMINVER. You have " . htmlspecialchars( $version ) . "</li>\n"; dieout("</ul>"); } print "version " . htmlspecialchars( $this->numeric_version ) . " is OK.</li>\n"; @@ -730,10 +739,10 @@ class DatabasePostgres extends Database { * $args may be a single associative array, or an array of these with numeric keys, * for multi-row insert (Postgres version 8.2 and above only). * - * @param array $table String: Name of the table to insert to. - * @param array $args Array: Items to insert into the table. - * @param array $fname String: Name of the function, for profiling - * @param mixed $options String or Array. Valid options: IGNORE + * @param $table String: Name of the table to insert to. + * @param $args Array: Items to insert into the table. + * @param $fname String: Name of the function, for profiling + * @param $options String or Array. Valid options: IGNORE * * @return bool Success of insert operation. IGNORE always returns true. */ @@ -746,8 +755,7 @@ class DatabasePostgres extends Database { $table = $this->tableName( $table ); if (! isset( $wgDBversion ) ) { - $this->getServerVersion(); - $wgDBversion = $this->numeric_version; + $wgDBversion = $this->getServerVersion(); } if ( !is_array( $options ) ) @@ -1009,10 +1017,10 @@ class DatabasePostgres extends Database { * Returns an SQL expression for a simple conditional. * Uses CASE on Postgres * - * @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 + * @param $cond String: SQL expression which will result in a boolean value + * @param $trueVal String: SQL expression to return if true + * @param $falseVal String: SQL expression to return if false + * @return String: SQL fragment */ function conditional( $cond, $trueVal, $falseVal ) { return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; @@ -1055,7 +1063,7 @@ class DatabasePostgres extends Database { /** * @return string wikitext of a link to the server software's web site */ - function getSoftwareLink() { + function getSoftwareLink() { return "[http://www.postgresql.org/ PostgreSQL]"; } @@ -1063,16 +1071,17 @@ class DatabasePostgres extends Database { * @return string Version information from the database */ function getServerVersion() { - $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0); - $thisver = array(); - if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) { - die("Could not determine the numeric version from $version!"); + $versionInfo = pg_version( $this->mConn ); + if ( isset( $versionInfo['server'] ) ) { + $this->numeric_version = $versionInfo['server']; + } else { + // There's no way to identify the precise version before 7.4, but + // it doesn't matter anyway since we're just going to give an error. + $this->numeric_version = '7.3 or earlier'; } - $this->numeric_version = $thisver[1]; - return $version; + return $this->numeric_version; } - /** * Query whether a given relation exists (in the given schema, or the * default mw one if not given) @@ -1319,7 +1328,7 @@ END; * * @private * - * @param string $com SQL string, read from a stream (usually tables.sql) + * @param $ins String: SQL string, read from a stream (usually tables.sql) * * @return string SQL string */ @@ -1344,7 +1353,7 @@ END; * * @private * - * @param array $options an associative array of options to be turned into + * @param $options Array: an associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return array */ @@ -1417,5 +1426,9 @@ END; public function unlock( $lockName, $method ) { return true; } + + public function getSearchEngine() { + return "SearchPostgres"; + } } // end DatabasePostgres class diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index 112c417b..dfc506cc 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -20,11 +20,9 @@ class DatabaseSqlite extends Database { * Constructor */ function __construct($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0) { - global $wgOut,$wgSQLiteDataDir; + global $wgOut,$wgSQLiteDataDir, $wgSQLiteDataDirMode; if ("$wgSQLiteDataDir" == '') $wgSQLiteDataDir = dirname($_SERVER['DOCUMENT_ROOT']).'/data'; - if (!is_dir($wgSQLiteDataDir)) mkdir($wgSQLiteDataDir,0700); - if (!isset($wgOut)) $wgOut = NULL; # Can't get a reference if it hasn't been set yet - $this->mOut =& $wgOut; + if (!is_dir($wgSQLiteDataDir)) wfMkdirParents( $wgSQLiteDataDir, $wgSQLiteDataDirMode ); $this->mFailFunction = $failFunction; $this->mFlags = $flags; $this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite"; @@ -48,11 +46,28 @@ class DatabaseSqlite extends Database { $this->mConn = false; if ($dbName) { $file = $this->mDatabaseFile; - if ($this->mFlags & DBO_PERSISTENT) $this->mConn = new PDO("sqlite:$file",$user,$pass,array(PDO::ATTR_PERSISTENT => true)); - else $this->mConn = new PDO("sqlite:$file",$user,$pass); - if ($this->mConn === false) wfDebug("DB connection error: $err\n");; + try { + if ( $this->mFlags & DBO_PERSISTENT ) { + $this->mConn = new PDO( "sqlite:$file", $user, $pass, + array( PDO::ATTR_PERSISTENT => true ) ); + } else { + $this->mConn = new PDO( "sqlite:$file", $user, $pass ); + } + } catch ( PDOException $e ) { + $err = $e->getMessage(); + } + if ( $this->mConn === false ) { + wfDebug( "DB connection error: $err\n" ); + if ( !$this->mFailFunction ) { + throw new DBConnectionError( $this, $err ); + } else { + return false; + } + + } $this->mOpened = $this->mConn; - $this->mConn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT); # set error codes only, dont raise exceptions + # set error codes only, don't raise exceptions + $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); } return $this->mConn; } @@ -390,7 +405,19 @@ class DatabaseSqlite extends Database { public function unlock( $lockName, $method ) { return true; } + + public function getSearchEngine() { + return "SearchEngineDummy"; + } + /** + * No-op version of deadlockLoop + */ + public function deadlockLoop( /*...*/ ) { + $args = func_get_args(); + $function = array_shift( $args ); + return call_user_func_array( $function, $args ); + } } /** diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php index 48c2d99b..820aa2ea 100644 --- a/includes/db/LBFactory_Multi.php +++ b/includes/db/LBFactory_Multi.php @@ -36,6 +36,8 @@ * * masterTemplateOverrides An override array for all master servers. * + * readOnlyBySection A map of section name to read-only message. Missing or false for read/write. + * * @ingroup Database */ class LBFactory_Multi extends LBFactory { @@ -44,7 +46,7 @@ class LBFactory_Multi extends LBFactory { // Optional settings var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array(); var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer; - var $templateOverridesByCluster, $masterTemplateOverrides; + var $templateOverridesByCluster, $masterTemplateOverrides, $readOnlyBySection = array(); // Other stuff var $conf, $mainLBs = array(), $extLBs = array(); var $lastWiki, $lastSection; @@ -55,7 +57,8 @@ class LBFactory_Multi extends LBFactory { $required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' ); $optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName', 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer', - 'templateOverridesByCluster', 'masterTemplateOverrides' ); + 'templateOverridesByCluster', 'masterTemplateOverrides', + 'readOnlyBySection' ); foreach ( $required as $key ) { if ( !isset( $conf[$key] ) ) { @@ -69,6 +72,13 @@ class LBFactory_Multi extends LBFactory { $this->$key = $conf[$key]; } } + + // Check for read-only mode + $section = $this->getSectionForWiki(); + if ( !empty( $this->readOnlyBySection[$section] ) ) { + global $wgReadOnly; + $wgReadOnly = $this->readOnlyBySection[$section]; + } } function getSectionForWiki( $wiki = false ) { diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index 42c4044d..f847fe22 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -13,7 +13,7 @@ class LoadBalancer { /* private */ var $mServers, $mConns, $mLoads, $mGroupLoads; /* private */ var $mFailFunction, $mErrorConnection; - /* private */ var $mReadIndex, $mLastIndex, $mAllowLagged; + /* private */ var $mReadIndex, $mAllowLagged; /* private */ var $mWaitForPos, $mWaitTimeout; /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error'; /* private */ var $mParentInfo, $mLagTimes; @@ -50,7 +50,6 @@ class LoadBalancer { 'local' => array(), 'foreignUsed' => array(), 'foreignFree' => array() ); - $this->mLastIndex = -1; $this->mLoads = array(); $this->mWaitForPos = false; $this->mLaggedSlaveMode = false; @@ -399,11 +398,20 @@ class LoadBalancer { /** * Get a connection by index * This is the main entry point for this class. + * @param int $i Database + * @param array $groups Query groups + * @param string $wiki Wiki ID */ public function &getConnection( $i, $groups = array(), $wiki = false ) { global $wgDBtype; wfProfileIn( __METHOD__ ); + if ( $i == DB_LAST ) { + throw new MWException( 'Attempt to call ' . __METHOD__ . ' with deprecated server index DB_LAST' ); + } elseif ( $i === null || $i === false ) { + throw new MWException( 'Attempt to call ' . __METHOD__ . ' with invalid server index' ); + } + if ( $wiki === wfWikiID() ) { $wiki = false; } @@ -433,19 +441,13 @@ class LoadBalancer { # Operation-based index if ( $i == DB_SLAVE ) { $i = $this->getReaderIndex( false, $wiki ); - } elseif ( $i == DB_LAST ) { - # Just use $this->mLastIndex, which should already be set - $i = $this->mLastIndex; - if ( $i === -1 ) { - # Oh dear, not set, best to use the writer for safety - wfDebug( "Warning: DB_LAST used when there was no previous index\n" ); - $i = $this->getWriterIndex(); + # Couldn't find a working server in getReaderIndex()? + if ( $i === false ) { + $this->mLastError = 'No working slave server: ' . $this->mLastError; + $this->reportConnectionError( $this->mErrorConnection ); + return false; } } - # Couldn't find a working server in getReaderIndex()? - if ( $i === false ) { - $this->reportConnectionError( $this->mErrorConnection ); - } # Now we have an explicit index into the servers array $conn = $this->openConnection( $i, $wiki ); @@ -525,7 +527,7 @@ class LoadBalancer { } else { $server = $this->mServers[$i]; $server['serverIndex'] = $i; - $conn = $this->reallyOpenConnection( $server ); + $conn = $this->reallyOpenConnection( $server, false ); if ( $conn->isOpen() ) { $this->mConns['local'][$i][0] = $conn; } else { @@ -534,7 +536,6 @@ class LoadBalancer { $conn = false; } } - $this->mLastIndex = $i; wfProfileOut( __METHOD__ ); return $conn; } @@ -576,9 +577,8 @@ class LoadBalancer { $oldWiki = key( $this->mConns['foreignFree'][$i] ); if ( !$conn->selectDB( $dbName ) ) { - global $wguname; $this->mLastError = "Error selecting database $dbName on server " . - $conn->getServer() . " from client host {$wguname['nodename']}\n"; + $conn->getServer() . " from client host " . wfHostname() . "\n"; $this->mErrorConnection = $conn; $conn = false; } else { @@ -598,6 +598,7 @@ class LoadBalancer { $this->mErrorConnection = $conn; $conn = false; } else { + $conn->tablePrefix( $prefix ); $this->mConns['foreignUsed'][$i][$wiki] = $conn; wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" ); } @@ -661,31 +662,27 @@ class LoadBalancer { function reportConnectionError( &$conn ) { wfProfileIn( __METHOD__ ); - # Prevent infinite recursion - - static $reporting = false; - if ( !$reporting ) { - $reporting = true; - if ( !is_object( $conn ) ) { - // No last connection, probably due to all servers being too busy - $conn = new Database; - if ( $this->mFailFunction ) { - $conn->failFunction( $this->mFailFunction ); - $conn->reportConnectionError( $this->mLastError ); - } else { - // If all servers were busy, mLastError will contain something sensible - throw new DBConnectionError( $conn, $this->mLastError ); - } + + if ( !is_object( $conn ) ) { + // No last connection, probably due to all servers being too busy + wfLogDBError( "LB failure with no last connection\n" ); + $conn = new Database; + if ( $this->mFailFunction ) { + $conn->failFunction( $this->mFailFunction ); + $conn->reportConnectionError( $this->mLastError ); } else { - if ( $this->mFailFunction ) { - $conn->failFunction( $this->mFailFunction ); - } else { - $conn->failFunction( false ); - } - $server = $conn->getProperty( 'mServer' ); - $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); + // If all servers were busy, mLastError will contain something sensible + throw new DBConnectionError( $conn, $this->mLastError ); + } + } else { + if ( $this->mFailFunction ) { + $conn->failFunction( $this->mFailFunction ); + } else { + $conn->failFunction( false ); } - $reporting = false; + $server = $conn->getProperty( 'mServer' ); + wfLogDBError( "Connection error: {$this->mLastError} ({$server})\n" ); + $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); } wfProfileOut( __METHOD__ ); } diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 8e16f1a1..929ab2b9 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -64,6 +64,9 @@ class LoadMonitor_MySQL implements LoadMonitor { $requestRate = 10; global $wgMemc; + if ( empty( $wgMemc ) ) + $wgMemc = wfGetMainCache(); + $masterName = $this->parent->getServerName( 0 ); $memcKey = wfMemcKey( 'lag_times', $masterName ); $times = $wgMemc->get( $memcKey ); diff --git a/includes/diff/Diff.php b/includes/diff/Diff.php new file mode 100644 index 00000000..538c2d83 --- /dev/null +++ b/includes/diff/Diff.php @@ -0,0 +1,580 @@ +<?php +/* Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu> + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * or see http://www.gnu.org/ + */ + +/** + * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which + * in turn is based on Myers' "An O(ND) difference algorithm and its variations" + * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s + * "An O(NP) Sequence Comparison Algorithm"). + * + * This implementation supports an upper bound on the excution time. + * + * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space + * + * @author Guy Van den Broeck + * @ingroup DifferenceEngine + */ +class WikiDiff3 { + + //Input variables + private $from; + private $to; + private $m; + private $n; + + private $tooLong; + private $powLimit; + + //State variables + private $maxDifferences; + private $lcsLengthCorrectedForHeuristic = false; + + //Output variables + public $length; + public $removed; + public $added; + public $heuristicUsed; + + function __construct($tooLong = 2000000, $powLimit = 1.45){ + $this->tooLong = $tooLong; + $this->powLimit = $powLimit; + } + + public function diff(/*array*/ $from, /*array*/ $to){ + //remember initial lengths + $m = sizeof($from); + $n = count($to); + + $this->heuristicUsed = false; + + //output + $removed = $m > 0 ? array_fill(0, $m, true) : array(); + $added = $n > 0 ? array_fill(0, $n, true) : array(); + + //reduce the complexity for the next step (intentionally done twice) + //remove common tokens at the start + $i = 0; + while($i < $m && $i < $n && $from[$i] === $to[$i]) { + $removed[$i] = $added[$i] = false; + unset($from[$i], $to[$i]); + ++$i; + } + + //remove common tokens at the end + $j = 1; + while($i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j]) { + $removed[$m - $j] = $added[$n - $j] = false; + unset($from[$m - $j], $to[$n - $j]); + ++$j; + } + + $this->from = $newFromIndex = $this->to = $newToIndex = array(); + + //remove tokens not in both sequences + $shared = array(); + foreach( $from as $key ) { + $shared[$key] = false; + } + + foreach($to as $index => &$el) { + if(array_key_exists($el, $shared)) { + //keep it + $this->to[] = $el; + $shared[$el] = true; + $newToIndex[] = $index; + } + } + foreach($from as $index => &$el) { + if($shared[$el]) { + //keep it + $this->from[] = $el; + $newFromIndex[] = $index; + } + } + + unset($shared, $from, $to); + + $this->m = count($this->from); + $this->n = count($this->to); + + $this->removed = $this->m > 0 ? array_fill(0, $this->m, true) : array(); + $this->added = $this->n > 0 ? array_fill(0, $this->n, true) : array(); + + if ($this->m == 0 || $this->n == 0) { + $this->length = 0; + } else { + $this->maxDifferences = ceil(($this->m + $this->n) / 2.0); + if ($this->m * $this->n > $this->tooLong) { + // limit complexity to D^POW_LIMIT for long sequences + $this->maxDifferences = floor(pow($this->maxDifferences, $this->powLimit - 1.0)); + wfDebug("Limiting max number of differences to $this->maxDifferences\n"); + } + + /* + * The common prefixes and suffixes are always part of some LCS, include + * them now to reduce our search space + */ + $max = min($this->m, $this->n); + for ($forwardBound = 0; $forwardBound < $max + && $this->from[$forwardBound] === $this->to[$forwardBound]; + ++$forwardBound) { + $this->removed[$forwardBound] = $this->added[$forwardBound] = false; + } + + $backBoundL1 = $this->m - 1; + $backBoundL2 = $this->n - 1; + + while ($backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound + && $this->from[$backBoundL1] === $this->to[$backBoundL2]) { + $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false; + } + + $temp = array_fill(0, $this->m + $this->n + 1, 0); + $V = array($temp, $temp); + $snake = array(0, 0, 0); + + $this->length = $forwardBound + $this->m - $backBoundL1 - 1 + + $this->lcs_rec($forwardBound, $backBoundL1, + $forwardBound, $backBoundL2, $V, $snake); + } + + $this->m = $m; + $this->n = $n; + + $this->length += $i + $j - 1; + + foreach($this->removed as $key => &$removed_elem) { + if(!$removed_elem) { + $removed[$newFromIndex[$key]] = false; + } + } + foreach($this->added as $key => &$added_elem) { + if(!$added_elem) { + $added[$newToIndex[$key]] = false; + } + } + $this->removed = $removed; + $this->added = $added; + } + + function diff_range($from_lines, $to_lines) { + // Diff and store locally + $this->diff($from_lines, $to_lines); + unset($from_lines, $to_lines); + + $ranges = array(); + $xi = $yi = 0; + while ($xi < $this->m || $yi < $this->n) { + // Matching "snake". + while ($xi < $this->m && $yi < $this->n + && !$this->removed[$xi] + && !$this->added[$yi]) { + ++$xi; + ++$yi; + } + // Find deletes & adds. + $xstart = $xi; + while ($xi < $this->m && $this->removed[$xi]) { + ++$xi; + } + + $ystart = $yi; + while ($yi < $this->n && $this->added[$yi]) { + ++$yi; + } + + if ($xi > $xstart || $yi > $ystart) { + $ranges[] = new RangeDifference($xstart, $xi, + $ystart, $yi); + } + } + return $ranges; + } + + private function lcs_rec($bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake) { + // check that both sequences are non-empty + if ($bottoml1 > $topl1 || $bottoml2 > $topl2) { + return 0; + } + + $d = $this->find_middle_snake($bottoml1, $topl1, $bottoml2, + $topl2, $V, $snake); + + // need to store these so we don't lose them when they're + // overwritten by the recursion + $len = $snake[2]; + $startx = $snake[0]; + $starty = $snake[1]; + + // the middle snake is part of the LCS, store it + for ($i = 0; $i < $len; ++$i) { + $this->removed[$startx + $i] = $this->added[$starty + $i] = false; + } + + if ($d > 1) { + return $len + + $this->lcs_rec($bottoml1, $startx - 1, $bottoml2, + $starty - 1, $V, $snake) + + $this->lcs_rec($startx + $len, $topl1, $starty + $len, + $topl2, $V, $snake); + } else if ($d == 1) { + /* + * In this case the sequences differ by exactly 1 line. We have + * already saved all the lines after the difference in the for loop + * above, now we need to save all the lines before the difference. + */ + $max = min($startx - $bottoml1, $starty - $bottoml2); + for ($i = 0; $i < $max; ++$i) { + $this->removed[$bottoml1 + $i] = + $this->added[$bottoml2 + $i] = false; + } + return $max + $len; + } + return $len; + } + + private function find_middle_snake($bottoml1, $topl1, $bottoml2,$topl2, &$V, &$snake) { + $from = &$this->from; + $to = &$this->to; + $V0 = &$V[0]; + $V1 = &$V[1]; + $snake0 = &$snake[0]; + $snake1 = &$snake[1]; + $snake2 = &$snake[2]; + $bottoml1_min_1 = $bottoml1-1; + $bottoml2_min_1 = $bottoml2-1; + $N = $topl1 - $bottoml1_min_1; + $M = $topl2 - $bottoml2_min_1; + $delta = $N - $M; + $maxabsx = $N+$bottoml1; + $maxabsy = $M+$bottoml2; + $limit = min($this->maxDifferences, ceil(($N + $M ) / 2)); + + //value_to_add_forward: a 0 or 1 that we add to the start + // offset to make it odd/even + if (($M & 1) == 1) { + $value_to_add_forward = 1; + } else { + $value_to_add_forward = 0; + } + + if (($N & 1) == 1) { + $value_to_add_backward = 1; + } else { + $value_to_add_backward = 0; + } + + $start_forward = -$M; + $end_forward = $N; + $start_backward = -$N; + $end_backward = $M; + + $limit_min_1 = $limit - 1; + $limit_plus_1 = $limit + 1; + + $V0[$limit_plus_1] = 0; + $V1[$limit_min_1] = $N; + $limit = min($this->maxDifferences, ceil(($N + $M ) / 2)); + + if (($delta & 1) == 1) { + for ($d = 0; $d <= $limit; ++$d) { + $start_diag = max($value_to_add_forward + $start_forward, -$d); + $end_diag = min($end_forward, $d); + $value_to_add_forward = 1 - $value_to_add_forward; + + // compute forward furthest reaching paths + for ($k = $start_diag; $k <= $end_diag; $k += 2) { + if ($k == -$d || ($k < $d + && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k])) { + $x = $V0[$limit_plus_1 + $k]; + } else { + $x = $V0[$limit_min_1 + $k] + 1; + } + + $absx = $snake0 = $x + $bottoml1; + $absy = $snake1 = $x - $k + $bottoml2; + + while ($absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy]) { + ++$absx; + ++$absy; + } + $x = $absx-$bottoml1; + + $snake2 = $absx -$snake0; + $V0[$limit + $k] = $x; + if ($k >= $delta - $d + 1 && $k <= $delta + $d - 1 + && $x >= $V1[$limit + $k - $delta]) { + return 2 * $d - 1; + } + + // check to see if we can cut down the diagonal range + if ($x >= $N && $end_forward > $k - 1) { + $end_forward = $k - 1; + } else if ($absy - $bottoml2 >= $M) { + $start_forward = $k + 1; + $value_to_add_forward = 0; + } + } + + $start_diag = max($value_to_add_backward + $start_backward, -$d); + $end_diag = min($end_backward, $d); + $value_to_add_backward = 1 - $value_to_add_backward; + + // compute backward furthest reaching paths + for ($k = $start_diag; $k <= $end_diag; $k += 2) { + if ($k == $d + || ($k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k])) { + $x = $V1[$limit_min_1 + $k]; + } else { + $x = $V1[$limit_plus_1 + $k] - 1; + } + + $y = $x - $k - $delta; + + $snake2 = 0; + while ($x > 0 && $y > 0 + && $from[$x +$bottoml1_min_1] === $to[$y + $bottoml2_min_1]) { + --$x; + --$y; + ++$snake2; + } + $V1[$limit + $k] = $x; + + // check to see if we can cut down our diagonal range + if ($x <= 0) { + $start_backward = $k + 1; + $value_to_add_backward = 0; + } else if ($y <= 0 && $end_backward > $k - 1) { + $end_backward = $k - 1; + } + } + } + } else { + for ($d = 0; $d <= $limit; ++$d) { + $start_diag = max($value_to_add_forward + $start_forward, -$d); + $end_diag = min($end_forward, $d); + $value_to_add_forward = 1 - $value_to_add_forward; + + // compute forward furthest reaching paths + for ($k = $start_diag; $k <= $end_diag; $k += 2) { + if ($k == -$d + || ($k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k])) { + $x = $V0[$limit_plus_1 + $k]; + } else { + $x = $V0[$limit_min_1 + $k] + 1; + } + + $absx = $snake0 = $x + $bottoml1; + $absy = $snake1 = $x - $k + $bottoml2; + + while ($absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy]) { + ++$absx; + ++$absy; + } + $x = $absx-$bottoml1; + $snake2 = $absx -$snake0; + $V0[$limit + $k] = $x; + + // check to see if we can cut down the diagonal range + if ($x >= $N && $end_forward > $k - 1) { + $end_forward = $k - 1; + } else if ($absy-$bottoml2 >= $M) { + $start_forward = $k + 1; + $value_to_add_forward = 0; + } + } + + $start_diag = max($value_to_add_backward + $start_backward, -$d); + $end_diag = min($end_backward, $d); + $value_to_add_backward = 1 - $value_to_add_backward; + + // compute backward furthest reaching paths + for ($k = $start_diag; $k <= $end_diag; $k += 2) { + if ($k == $d + || ($k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k])) { + $x = $V1[$limit_min_1 + $k]; + } else { + $x = $V1[$limit_plus_1 + $k] - 1; + } + + $y = $x - $k - $delta; + + $snake2 = 0; + while ($x > 0 && $y > 0 + && $from[$x +$bottoml1_min_1] === $to[$y + $bottoml2_min_1]) { + --$x; + --$y; + ++$snake2; + } + $V1[$limit + $k] = $x; + + if ($k >= -$delta - $d && $k <= $d - $delta + && $x <= $V0[$limit + $k + $delta]) { + $snake0 = $bottoml1 + $x; + $snake1 = $bottoml2 + $y; + return 2 * $d; + } + + // check to see if we can cut down our diagonal range + if ($x <= 0) { + $start_backward = $k + 1; + $value_to_add_backward = 0; + } else if ($y <= 0 && $end_backward > $k - 1) { + $end_backward = $k - 1; + } + } + } + } + /* + * computing the true LCS is too expensive, instead find the diagonal + * with the most progress and pretend a midle snake of length 0 occurs + * there. + */ + + $most_progress = self::findMostProgress($M, $N, $limit, $V); + + $snake0 = $bottoml1 + $most_progress[0]; + $snake1 = $bottoml2 + $most_progress[1]; + $snake2 = 0; + wfDebug("Computing the LCS is too expensive. Using a heuristic.\n"); + $this->heuristicUsed = true; + return 5; /* + * HACK: since we didn't really finish the LCS computation + * we don't really know the length of the SES. We don't do + * anything with the result anyway, unless it's <=1. We know + * for a fact SES > 1 so 5 is as good a number as any to + * return here + */ + } + + private static function findMostProgress($M, $N, $limit, $V) { + $delta = $N - $M; + + if (($M & 1) == ($limit & 1)) { + $forward_start_diag = max(-$M, -$limit); + } else { + $forward_start_diag = max(1 - $M, -$limit); + } + + $forward_end_diag = min($N, $limit); + + if (($N & 1) == ($limit & 1)) { + $backward_start_diag = max(-$N, -$limit); + } else { + $backward_start_diag = max(1 - $N, -$limit); + } + + $backward_end_diag = -min($M, $limit); + + $temp = array(0, 0, 0); + + + $max_progress = array_fill(0, ceil(max($forward_end_diag - $forward_start_diag, + $backward_end_diag - $backward_start_diag) / 2), $temp); + $num_progress = 0; // the 1st entry is current, it is initialized + // with 0s + + // first search the forward diagonals + for ($k = $forward_start_diag; $k <= $forward_end_diag; $k += 2) { + $x = $V[0][$limit + $k]; + $y = $x - $k; + if ($x > $N || $y > $M) { + continue; + } + + $progress = $x + $y; + if ($progress > $max_progress[0][2]) { + $num_progress = 0; + $max_progress[0][0] = $x; + $max_progress[0][1] = $y; + $max_progress[0][2] = $progress; + } else if ($progress == $max_progress[0][2]) { + ++$num_progress; + $max_progress[$num_progress][0] = $x; + $max_progress[$num_progress][1] = $y; + $max_progress[$num_progress][2] = $progress; + } + } + + $max_progress_forward = true; // initially the maximum + // progress is in the forward + // direction + + // now search the backward diagonals + for ($k = $backward_start_diag; $k <= $backward_end_diag; $k += 2) { + $x = $V[1][$limit + $k]; + $y = $x - $k - $delta; + if ($x < 0 || $y < 0) { + continue; + } + + $progress = $N - $x + $M - $y; + if ($progress > $max_progress[0][2]) { + $num_progress = 0; + $max_progress_forward = false; + $max_progress[0][0] = $x; + $max_progress[0][1] = $y; + $max_progress[0][2] = $progress; + } else if ($progress == $max_progress[0][2] && !$max_progress_forward) { + ++$num_progress; + $max_progress[$num_progress][0] = $x; + $max_progress[$num_progress][1] = $y; + $max_progress[$num_progress][2] = $progress; + } + } + + // return the middle diagonal with maximal progress. + return $max_progress[floor($num_progress / 2)]; + } + + public function getLcsLength(){ + if($this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic){ + $this->lcsLengthCorrectedForHeuristic = true; + $this->length = $this->m-array_sum($this->added); + } + return $this->length; + } + +} + +/** + * Alternative representation of a set of changes, by the index + * ranges that are changed. + * + * @ingroup DifferenceEngine + */ +class RangeDifference { + + public $leftstart; + public $leftend; + public $leftlength; + + public $rightstart; + public $rightend; + public $rightlength; + + function __construct($leftstart, $leftend, $rightstart, $rightend){ + $this->leftstart = $leftstart; + $this->leftend = $leftend; + $this->leftlength = $leftend - $leftstart; + $this->rightstart = $rightstart; + $this->rightend = $rightend; + $this->rightlength = $rightend - $rightstart; + } +} diff --git a/includes/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 0b4028cb..b30ff190 100644 --- a/includes/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -27,6 +27,7 @@ class DifferenceEngine { 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? + var $htmldiff; /**#@-*/ /** @@ -36,8 +37,9 @@ class DifferenceEngine { * @param $new String: either 'prev' or 'next'. * @param $rcid Integer: ??? FIXME (default 0) * @param $refreshCache boolean If set, refreshes the diff cache + * @param $htmldiff boolean If set, output using HTMLDiff instead of raw wikicode diff */ - function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) { + function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false) { $this->mTitle = $titleObj; wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n"); @@ -67,6 +69,7 @@ class DifferenceEngine { } $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer $this->mRefreshCache = $refreshCache; + $this->htmldiff = $htmldiff; } function getTitle() { @@ -74,10 +77,11 @@ class DifferenceEngine { } function showDiffPage( $diffOnly = false ) { - global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol; + global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff; wfProfileIn( __METHOD__ ); - # If external diffs are enabled both globally and for the user, + + # 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')) { @@ -88,19 +92,19 @@ class DifferenceEngine { $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 + [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; @@ -141,14 +145,15 @@ CONTROL; } else { $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle ); } - $wgOut->setSubtitle( wfMsg( 'difference' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); - if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) { + if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) { $wgOut->loginToUse(); $wgOut->output(); + $wgOut->disable(); wfProfileOut( __METHOD__ ); - exit; + return; } $sk = $wgUser->getSkin(); @@ -162,7 +167,7 @@ CONTROL; } // Prepare a change patrol link, if applicable - if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) { + if( $wgUseRCPatrol && $this->mTitle->userCan('patrol') ) { // If we've been given an explicit change identifier, use it; saves time if( $this->mRcidMarkPatrolled ) { $rcid = $this->mRcidMarkPatrolled; @@ -170,15 +175,15 @@ CONTROL; // Look for an unpatrolled change corresponding to this diff $db = wfGetDB( DB_SLAVE ); $change = RecentChange::newFromConds( - array( - // Add redundant user,timestamp condition so we can use the existing index - 'rc_user_text' => $this->mNewRev->getRawUserText(), - 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), + array( + // Add redundant user,timestamp condition so we can use the existing index + 'rc_user_text' => $this->mNewRev->getUserText( Revision::FOR_THIS_USER ), + 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), 'rc_this_oldid' => $this->mNewid, 'rc_last_oldid' => $this->mOldid, - 'rc_patrolled' => 0 - ), - __METHOD__ + 'rc_patrolled' => 0 + ), + __METHOD__ ); if( $change instanceof RecentChange ) { $rcid = $change->mAttribs['rc_id']; @@ -192,8 +197,8 @@ CONTROL; $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'markaspatrolleddiff' ), - "action=markpatrolled&rcid={$rcid}" - ) . ']</span>'; + "action=markpatrolled&rcid={$rcid}" + ) . ']</span>'; } else { $patrol = ''; } @@ -201,57 +206,58 @@ CONTROL; $patrol = ''; } + $htmldiffarg = $this->htmlDiffArgument(); $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ), - 'diff=prev&oldid='.$this->mOldid, '', '', 'id="differences-prevlink"' ); + 'diff=prev&oldid='.$this->mOldid.$htmldiffarg, '', '', 'id="differences-prevlink"' ); if ( $this->mNewRev->isCurrent() ) { $nextlink = ' '; } else { $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), - 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); + 'diff=next&oldid='.$this->mNewid.$htmldiffarg, '', '', 'id="differences-nextlink"' ); } $oldminor = ''; $newminor = ''; if ($this->mOldRev->mMinorEdit == 1) { - $oldminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' '; + $oldminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' '; } if ($this->mNewRev->mMinorEdit == 1) { - $newminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' '; + $newminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' '; } $rdel = ''; $ldel = ''; if( $wgUser->isAllowed( 'deleterevision' ) ) { $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) { - // If revision was hidden from sysops - $ldel = wfMsgHtml('rev-delundel'); + // If revision was hidden from sysops + $ldel = wfMsgHtml( 'rev-delundel' ); } else { $ldel = $sk->makeKnownLinkObj( $revdel, - wfMsgHtml('rev-delundel'), + wfMsgHtml( 'rev-delundel' ), 'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) . '&oldid=' . urlencode( $this->mOldRev->getId() ) ); // Bolden oversighted content if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) - $ldel = "<strong>$ldel</strong>"; + $ldel = "<strong>$ldel</strong>"; } $ldel = " <tt>(<small>$ldel</small>)</tt> "; // We don't currently handle well changing the top revision's settings if( $this->mNewRev->isCurrent() ) { - // If revision was hidden from sysops - $rdel = wfMsgHtml('rev-delundel'); + // If revision was hidden from sysops + $rdel = wfMsgHtml( 'rev-delundel' ); } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) { - // If revision was hidden from sysops - $rdel = wfMsgHtml('rev-delundel'); + // If revision was hidden from sysops + $rdel = wfMsgHtml( 'rev-delundel' ); } else { $rdel = $sk->makeKnownLinkObj( $revdel, - wfMsgHtml('rev-delundel'), + wfMsgHtml( 'rev-delundel' ), 'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) . '&oldid=' . urlencode( $this->mNewRev->getId() ) ); // Bolden oversighted content if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) - $rdel = "<strong>$rdel</strong>"; + $rdel = "<strong>$rdel</strong>"; } $rdel = " <tt>(<small>$rdel</small>)</tt> "; } @@ -265,11 +271,22 @@ CONTROL; '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" . '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>'; - $this->showDiff( $oldHeader, $newHeader ); - - if ( !$diffOnly ) - $this->renderNewRevision(); - + if( $wgEnableHtmlDiff && $this->htmldiff) { + $multi = $this->getMultiNotice(); + $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ), + 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'</div>'); + $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); + $this->renderHtmlDiff(); + } else { + if($wgEnableHtmlDiff){ + $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ), + 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'</div>'); + } + $this->showDiff( $oldHeader, $newHeader ); + if( !$diffOnly ) { + $this->renderNewRevision(); + } + } wfProfileOut( __METHOD__ ); } @@ -283,9 +300,9 @@ CONTROL; $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); #add deleted rev tag if needed if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); + $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $wgOut->addWikiMsg( 'rev-deleted-text-view' ); + $wgOut->addWikiMsg( 'rev-deleted-text-view' ); } if( !$this->mNewRev->isCurrent() ) { @@ -305,12 +322,12 @@ CONTROL; // Wrap the whole lot in a <pre> and don't parse $m = array(); preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); - $wgOut->addHtml( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); - $wgOut->addHtml( htmlspecialchars( $this->mNewtext ) ); - $wgOut->addHtml( "\n</pre>\n" ); + $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); + $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) ); + $wgOut->addHTML( "\n</pre>\n" ); } } else - $wgOut->addWikiTextTidy( $this->mNewtext ); + $wgOut->addWikiTextTidy( $this->mNewtext ); if( !$this->mNewRev->isCurrent() ) { $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); @@ -319,6 +336,70 @@ CONTROL; wfProfileOut( __METHOD__ ); } + + function renderHtmlDiff() { + global $wgOut, $wgTitle, $wgParser, $wgDebugComments; + wfProfileIn( __METHOD__ ); + + $this->showDiffStyle(); + + $wgOut->addHTML( '<h2>'.wfMsgHtml( 'visual-comparison' )."</h2>\n" ); + #add deleted rev tag if needed + if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); + } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $wgOut->addWikiMsg( 'rev-deleted-text-view' ); + } + + if( !$this->mNewRev->isCurrent() ) { + $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); + } + + $this->loadText(); + + // Old revision + if( is_object( $this->mOldRev ) ) { + $wgOut->setRevisionId( $this->mOldRev->getId() ); + } + + $popts = $wgOut->parserOptions(); + $oldTidy = $popts->setTidy( true ); + $popts->setEditSection( false ); + + $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() ); + $popts->setTidy( $oldTidy ); + + //only for new? + //$wgOut->addParserOutputNoText( $parserOutput ); + $oldHtml = $parserOutput->getText(); + wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) ); + + // New revision + if( is_object( $this->mNewRev ) ) { + $wgOut->setRevisionId( $this->mNewRev->getId() ); + } + + $popts = $wgOut->parserOptions(); + $oldTidy = $popts->setTidy( true ); + + $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() ); + $popts->setTidy( $oldTidy ); + + $wgOut->addParserOutputNoText( $parserOutput ); + $newHtml = $parserOutput->getText(); + wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) ); + + unset($parserOutput, $popts); + + $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut)); + $differ->htmlDiff($oldHtml, $newHtml); + if ( $wgDebugComments ) { + $wgOut->addHTML( "\n<!-- HtmlDiff Debug Output:\n" . HTMLDiffer::getDebugOutput() . " End Debug -->" ); + } + + wfProfileOut( __METHOD__ ); + } + /** * Show the first revision of an article. Uses normal diff headers in * contrast to normal "old revision" display style. @@ -343,31 +424,44 @@ CONTROL; # Check if user is allowed to look at this page. If not, bail out. # - if ( !( $this->mTitle->userCanRead() ) ) { + if ( !$this->mTitle->userCanRead() ) { $wgOut->loginToUse(); $wgOut->output(); wfProfileOut( __METHOD__ ); - exit; + throw new MWException("Permission Error: you do not have access to view this page"); } # Prepare the header box # $sk = $wgUser->getSkin(); - $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); + $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid.$this->htmlDiffArgument(), '', '', '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"; + $sk->revUserTools( $this->mNewRev ) . "<br />" . + $sk->revComment( $this->mNewRev ) . "<br />" . + $nextlink . "</div>\n"; $wgOut->addHTML( $header ); - $wgOut->setSubtitle( wfMsg( 'difference' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); wfProfileOut( __METHOD__ ); } + function htmlDiffArgument(){ + global $wgEnableHtmlDiff; + if($wgEnableHtmlDiff){ + if($this->htmldiff){ + return '&htmldiff=1'; + }else{ + return '&htmldiff=0'; + } + }else{ + return ''; + } + } + /** * Get the diff text, send it to $wgOut * Returns false if the diff could not be generated, otherwise returns true @@ -423,9 +517,9 @@ CONTROL; wfProfileIn( __METHOD__ ); // Check if the diff should be hidden from this user if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { - return ''; + return ''; } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - return ''; + return ''; } // Cacheable? $key = false; @@ -453,7 +547,9 @@ CONTROL; $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); // Save to cache for 7 days - if ( $key !== false && $difftext !== false ) { + if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { + wfIncrStats( 'diff_uncacheable' ); + } else if ( $key !== false && $difftext !== false ) { wfIncrStats( 'diff_cache_miss' ); $wgMemc->set( $key, $difftext, 7*86400 ); } else { @@ -486,7 +582,7 @@ CONTROL; dl('php_wikidiff.so'); } return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . - $this->debug( 'wikidiff1' ); + $this->debug( 'wikidiff1' ); } if ( $wgExternalDiffEngine == 'wikidiff2' ) { @@ -505,7 +601,7 @@ CONTROL; return $text; } } - if ( $wgExternalDiffEngine !== false ) { + if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) { # Diff via the shell global $wgTmpDirectory; $tempName1 = tempnam( $wgTmpDirectory, 'diff_' ); @@ -541,25 +637,25 @@ CONTROL; $diffs = new Diff( $ota, $nta ); $formatter = new TableDiffFormatter(); return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . - $this->debug(); + $this->debug(); } - + /** * Generate a debug comment indicating diff generating time, * server node, and generator backend. */ protected function debug( $generator="internal" ) { - global $wgShowHostnames, $wgNodeName; + global $wgShowHostnames; $data = array( $generator ); if( $wgShowHostnames ) { - $data[] = $wgNodeName; + $data[] = wfHostname(); } $data[] = wfTimestamp( TS_DB ); return "<!-- diff generator: " . - implode( " ", - array_map( + implode( " ", + array_map( "htmlspecialchars", - $data ) ) . + $data ) ) . " -->\n"; } @@ -568,12 +664,12 @@ CONTROL; */ function localiseLineNumbers( $text ) { return preg_replace_callback( '/<!--LINE (\d+)-->/', - array( &$this, 'localiseLineNumbersCb' ), $text ); + array( &$this, 'localiseLineNumbersCb' ), $text ); } function localiseLineNumbersCb( $matches ) { global $wgLang; - return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) ); + return wfMsgExt( 'lineno', array( 'parseinline' ), $wgLang->formatNum( $matches[1] ) ); } @@ -582,7 +678,7 @@ CONTROL; */ function getMultiNotice() { if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) ) - return ''; + return ''; if( !$this->mOldPage->equals( $this->mNewPage ) ) { // Comparing two different pages? Count would be meaningless. @@ -597,7 +693,7 @@ CONTROL; $n = $this->mTitle->countRevisionsBetween( $oldid, $newid ); if ( !$n ) - return ''; + return ''; return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n ); } @@ -607,22 +703,20 @@ CONTROL; * Add the header to a diff body */ static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) { - global $wgOut; - $header = " - <table class='diff'> - <col class='diff-marker' /> - <col class='diff-content' /> - <col class='diff-marker' /> - <col class='diff-content' /> - <tr valign='top'> - <td colspan='2' class='diff-otitle'>{$otitle}</td> - <td colspan='2' class='diff-ntitle'>{$ntitle}</td> - </tr> + <table class='diff'> + <col class='diff-marker' /> + <col class='diff-content' /> + <col class='diff-marker' /> + <col class='diff-content' /> + <tr valign='top'> + <td colspan='2' class='diff-otitle'>{$otitle}</td> + <td colspan='2' class='diff-ntitle'>{$ntitle}</td> + </tr> "; if ( $multi != '' ) - $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>"; + $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>"; return $header . $diff . "</table>"; } @@ -647,7 +741,7 @@ CONTROL; * API convenience. */ function loadRevisionData() { - global $wgLang; + global $wgLang, $wgUser; if ( $this->mRevisionsLoaded ) { return true; } else { @@ -657,10 +751,10 @@ CONTROL; // Load the new revision object $this->mNewRev = $this->mNewid - ? Revision::newFromId( $this->mNewid ) - : Revision::newFromTitle( $this->mTitle ); + ? Revision::newFromId( $this->mNewid ) + : Revision::newFromTitle( $this->mTitle ); if( !$this->mNewRev instanceof Revision ) - return false; + return false; // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) $this->mNewid = $this->mNewRev->getId(); @@ -673,10 +767,10 @@ CONTROL; $this->mNewPage = $this->mNewRev->getTitle(); if( $this->mNewRev->isCurrent() ) { $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); - $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) ); + $this->mPagetitle = wfMsgHTML( 'currentrev-asof', $timestamp ); $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' ); - $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)"; + $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"; $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)"; } else { @@ -688,9 +782,9 @@ CONTROL; $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)"; } if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>"; + $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>"; } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>'; + $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>'; } // Load the old revision object @@ -722,17 +816,19 @@ CONTROL; $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) ); $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>" - . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)"; + . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)"; // Add an "undo" link $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid); + $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) ); + $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' ); if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { - $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; + $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)"; } if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) { - $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>'; + $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>'; } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { - $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>'; + $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>'; } } @@ -754,13 +850,13 @@ CONTROL; return false; } if ( $this->mOldRev ) { - $this->mOldtext = $this->mOldRev->revText(); + $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER ); if ( $this->mOldtext === false ) { return false; } } if ( $this->mNewRev ) { - $this->mNewtext = $this->mNewRev->revText(); + $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); if ( $this->mNewtext === false ) { return false; } @@ -828,7 +924,7 @@ class _DiffOp_Copy extends _DiffOp { function _DiffOp_Copy ($orig, $closing = false) { if (!is_array($closing)) - $closing = $orig; + $closing = $orig; $this->orig = $orig; $this->closing = $closing; } @@ -892,7 +988,6 @@ class _DiffOp_Change extends _DiffOp { } } - /** * Class used internally by Diff to actually compute the diffs. * @@ -911,70 +1006,30 @@ class _DiffOp_Change extends _DiffOp { * are my own. * * Line length limits for robustness added by Tim Starling, 2005-08-31 + * Alternative implementation added by Guy Van den Broeck, 2008-07-30 * - * @author Geoffrey T. Dairiki, Tim Starling + * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck * @private * @ingroup DifferenceEngine */ class _DiffEngine { + const MAX_XREF_LENGTH = 10000; - function diff ($from_lines, $to_lines) { + function diff ($from_lines, $to_lines){ wfProfileIn( __METHOD__ ); - $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)); + // Diff and store locally + $this->diff_local($from_lines, $to_lines); // 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. + $n_from = sizeof($from_lines); + $n_to = sizeof($to_lines); + $edits = array(); $xi = $yi = 0; while ($xi < $n_from || $yi < $n_to) { @@ -984,33 +1039,96 @@ class _DiffEngine { // Skip matching "snake". $copy = array(); while ( $xi < $n_from && $yi < $n_to - && !$this->xchanged[$xi] && !$this->ychanged[$yi]) { + && !$this->xchanged[$xi] && !$this->ychanged[$yi]) { $copy[] = $from_lines[$xi++]; ++$yi; } if ($copy) - $edits[] = new _DiffOp_Copy($copy); + $edits[] = new _DiffOp_Copy($copy); // Find deletes & adds. $delete = array(); while ($xi < $n_from && $this->xchanged[$xi]) - $delete[] = $from_lines[$xi++]; + $delete[] = $from_lines[$xi++]; $add = array(); while ($yi < $n_to && $this->ychanged[$yi]) - $add[] = $to_lines[$yi++]; + $add[] = $to_lines[$yi++]; if ($delete && $add) - $edits[] = new _DiffOp_Change($delete, $add); + $edits[] = new _DiffOp_Change($delete, $add); elseif ($delete) - $edits[] = new _DiffOp_Delete($delete); + $edits[] = new _DiffOp_Delete($delete); elseif ($add) - $edits[] = new _DiffOp_Add($add); + $edits[] = new _DiffOp_Add($add); } wfProfileOut( __METHOD__ ); return $edits; } + function diff_local ($from_lines, $to_lines) { + global $wgExternalDiffEngine; + wfProfileIn( __METHOD__); + + if($wgExternalDiffEngine == 'wikidiff3'){ + // wikidiff3 + $wikidiff3 = new WikiDiff3(); + $wikidiff3->diff($from_lines, $to_lines); + $this->xchanged = $wikidiff3->removed; + $this->ychanged = $wikidiff3->added; + unset($wikidiff3); + }else{ + // old diff + $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)); + } + wfProfileOut( __METHOD__ ); + } + /** * Returns the whole line if it's small enough, or the MD5 hash otherwise */ @@ -1022,7 +1140,6 @@ class _DiffEngine { } } - /* Divide the Largest Common Subsequence (LCS) of the sequences * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally * sized segments. @@ -1040,23 +1157,22 @@ class _DiffEngine { * of the portions it is going to specify. */ function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) { - wfProfileIn( __METHOD__ ); $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; + // 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; + 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; + for ($i = $ylim - 1; $i >= $yoff; $i--) + $ymatches[$this->yv[$i]][] = $i; $this->lcs = 0; $this->seq[0]= $yoff - 1; @@ -1066,25 +1182,24 @@ class _DiffEngine { $numer = $xlim - $xoff + $nchunks - 1; $x = $xoff; for ($chunk = 0; $chunk < $nchunks; $chunk++) { - wfProfileIn( __METHOD__ . "-chunk" ); if ($chunk > 0) - for ($i = 0; $i <= $this->lcs; $i++) - $ymids[$i][$chunk-1] = $this->seq[$i]; + 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; + 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; - } + 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]); @@ -1100,7 +1215,6 @@ class _DiffEngine { } } } - wfProfileOut( __METHOD__ . "-chunk" ); } $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); @@ -1112,18 +1226,14 @@ class _DiffEngine { } $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); - wfProfileOut( __METHOD__ ); return array($this->lcs, $seps); } function _lcs_pos ($ypos) { - wfProfileIn( __METHOD__ ); - $end = $this->lcs; if ($end == 0 || $ypos > $this->seq[$end]) { $this->seq[++$this->lcs] = $ypos; $this->in_seq[$ypos] = 1; - wfProfileOut( __METHOD__ ); return $this->lcs; } @@ -1131,9 +1241,9 @@ class _DiffEngine { while ($beg < $end) { $mid = (int)(($beg + $end) / 2); if ( $ypos > $this->seq[$mid] ) - $beg = $mid + 1; + $beg = $mid + 1; else - $end = $mid; + $end = $mid; } USE_ASSERTS && assert($ypos != $this->seq[$end]); @@ -1141,7 +1251,6 @@ class _DiffEngine { $this->in_seq[$this->seq[$end]] = false; $this->seq[$end] = $ypos; $this->in_seq[$ypos] = 1; - wfProfileOut( __METHOD__ ); return $end; } @@ -1157,24 +1266,22 @@ class _DiffEngine { * All line numbers are origin-0 and discarded lines are not counted. */ function _compareseq ($xoff, $xlim, $yoff, $ylim) { - wfProfileIn( __METHOD__ ); - // Slide down the bottom initial diagonal. while ($xoff < $xlim && $yoff < $ylim - && $this->xv[$xoff] == $this->yv[$yoff]) { + && $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]) { + && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) { --$xlim; --$ylim; } if ($xoff == $xlim || $yoff == $ylim) - $lcs = 0; + $lcs = 0; else { // This is ad hoc but seems to work well. //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); @@ -1188,9 +1295,9 @@ class _DiffEngine { // X and Y sequences have no common subsequence: // mark all changed. while ($yoff < $ylim) - $this->ychanged[$this->yind[$yoff++]] = 1; + $this->ychanged[$this->yind[$yoff++]] = 1; while ($xoff < $xlim) - $this->xchanged[$this->xind[$xoff++]] = 1; + $this->xchanged[$this->xind[$xoff++]] = 1; } else { // Use the partitions to split this problem into subproblems. reset($seps); @@ -1200,7 +1307,6 @@ class _DiffEngine { $pt1 = $pt2; } } - wfProfileOut( __METHOD__ ); } /* Adjust inserts/deletes of identical lines to join changes @@ -1237,23 +1343,23 @@ class _DiffEngine { * $other_changed[$j] == false. */ while ($j < $other_len && $other_changed[$j]) - $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++; + $j++; } if ($i == $len) - break; + break; $start = $i; // Find the end of this run of changes. while (++$i < $len && $changed[$i]) - continue; + continue; do { /* @@ -1271,10 +1377,10 @@ class _DiffEngine { $changed[--$start] = 1; $changed[--$i] = false; while ($start > 0 && $changed[$start - 1]) - $start--; + $start--; USE_ASSERTS && assert('$j > 0'); while ($other_changed[--$j]) - continue; + continue; USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); } @@ -1296,14 +1402,14 @@ class _DiffEngine { $changed[$start++] = false; $changed[$i++] = 1; while ($i < $len && $changed[$i]) - $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++; + $j++; } } } while ($runlength != $i - $start); @@ -1317,7 +1423,7 @@ class _DiffEngine { $changed[--$i] = 0; USE_ASSERTS && assert('$j > 0'); while ($other_changed[--$j]) - continue; + continue; USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); } } @@ -1376,7 +1482,7 @@ class Diff function isEmpty () { foreach ($this->edits as $edit) { if ($edit->type != 'copy') - return false; + return false; } return true; } @@ -1392,7 +1498,7 @@ class Diff $lcs = 0; foreach ($this->edits as $edit) { if ($edit->type == 'copy') - $lcs += sizeof($edit->orig); + $lcs += sizeof($edit->orig); } return $lcs; } @@ -1410,7 +1516,7 @@ class Diff foreach ($this->edits as $edit) { if ($edit->orig) - array_splice($lines, sizeof($lines), 0, $edit->orig); + array_splice($lines, sizeof($lines), 0, $edit->orig); } return $lines; } @@ -1428,7 +1534,7 @@ class Diff foreach ($this->edits as $edit) { if ($edit->closing) - array_splice($lines, sizeof($lines), 0, $edit->closing); + array_splice($lines, sizeof($lines), 0, $edit->closing); } return $lines; } @@ -1441,21 +1547,21 @@ class Diff function _check ($from_lines, $to_lines) { wfProfileIn( __METHOD__ ); if (serialize($from_lines) != serialize($this->orig())) - trigger_error("Reconstructed original doesn't match", E_USER_ERROR); + 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); + 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); + 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); + 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); + trigger_error("Edit sequence is non-optimal", E_USER_ERROR); $prevtype = $edit->type; } @@ -1496,7 +1602,7 @@ class MappedDiff extends Diff * have the same number of elements as $to_lines. */ function MappedDiff($from_lines, $to_lines, - $mapped_from_lines, $mapped_to_lines) { + $mapped_from_lines, $mapped_to_lines) { wfProfileIn( __METHOD__ ); assert(sizeof($from_lines) == sizeof($mapped_from_lines)); @@ -1579,8 +1685,8 @@ class DiffFormatter { $block[] = new _DiffOp_Copy($context); } $this->_block($x0, $ntrail + $xi - $x0, - $y0, $ntrail + $yi - $y0, - $block); + $y0, $ntrail + $yi - $y0, + $block); $block = false; } } @@ -1593,21 +1699,21 @@ class DiffFormatter { $y0 = $yi - sizeof($context); $block = array(); if ($context) - $block[] = new _DiffOp_Copy($context); + $block[] = new _DiffOp_Copy($context); } $block[] = $edit; } if ($edit->orig) - $xi += sizeof($edit->orig); + $xi += sizeof($edit->orig); if ($edit->closing) - $yi += sizeof($edit->closing); + $yi += sizeof($edit->closing); } if (is_array($block)) - $this->_block($x0, $xi - $x0, - $y0, $yi - $y0, - $block); + $this->_block($x0, $xi - $x0, + $y0, $yi - $y0, + $block); $end = $this->_end_diff(); wfProfileOut( __METHOD__ ); @@ -1619,15 +1725,15 @@ class DiffFormatter { $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen)); foreach ($edits as $edit) { if ($edit->type == 'copy') - $this->_context($edit->orig); + $this->_context($edit->orig); elseif ($edit->type == 'add') - $this->_added($edit->closing); + $this->_added($edit->closing); elseif ($edit->type == 'delete') - $this->_deleted($edit->orig); + $this->_deleted($edit->orig); elseif ($edit->type == 'change') - $this->_changed($edit->orig, $edit->closing); + $this->_changed($edit->orig, $edit->closing); else - trigger_error('Unknown edit type', E_USER_ERROR); + trigger_error('Unknown edit type', E_USER_ERROR); } $this->_end_block(); wfProfileOut( __METHOD__ ); @@ -1645,9 +1751,9 @@ class DiffFormatter { function _block_header($xbeg, $xlen, $ybeg, $ylen) { if ($xlen > 1) - $xbeg .= "," . ($xbeg + $xlen - 1); + $xbeg .= "," . ($xbeg + $xlen - 1); if ($ylen > 1) - $ybeg .= "," . ($ybeg + $ylen - 1); + $ybeg .= "," . ($ybeg + $ylen - 1); return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg; } @@ -1661,7 +1767,7 @@ class DiffFormatter { function _lines($lines, $prefix = ' ') { foreach ($lines as $line) - echo "$prefix $line\n"; + echo "$prefix $line\n"; } function _context($lines) { @@ -1716,40 +1822,40 @@ class ArrayDiffFormatter extends DiffFormatter { $newline = 1; $retval = array(); foreach($diff->edits as $edit) - switch($edit->type) { - case 'add': - foreach($edit->closing as $l) { - $retval[] = array( + switch($edit->type) { + case 'add': + foreach($edit->closing as $l) { + $retval[] = array( 'action' => 'add', 'new'=> $l, 'newline' => $newline++ - ); - } - break; - case 'delete': - foreach($edit->orig as $l) { - $retval[] = array( + ); + } + break; + case 'delete': + foreach($edit->orig as $l) { + $retval[] = array( 'action' => 'delete', 'old' => $l, 'oldline' => $oldline++, - ); - } - break; - case 'change': - foreach($edit->orig as $i => $l) { - $retval[] = array( + ); + } + break; + case 'change': + foreach($edit->orig as $i => $l) { + $retval[] = array( 'action' => 'change', 'old' => $l, 'new' => @$edit->closing[$i], 'oldline' => $oldline++, 'newline' => $newline++, - ); - } - break; - case 'copy': - $oldline += count($edit->orig); - $newline += count($edit->orig); - } + ); + } + break; + case 'copy': + $oldline += count($edit->orig); + $newline += count($edit->orig); + } return $retval; } } @@ -1777,13 +1883,13 @@ class _HWLDF_WordAccumulator { function _flushGroup ($new_tag) { if ($this->_group !== '') { if ($this->_tag == 'ins') - $this->_line .= '<ins class="diffchange diffchange-inline">' . - htmlspecialchars ( $this->_group ) . '</ins>'; + $this->_line .= '<ins class="diffchange diffchange-inline">' . + htmlspecialchars ( $this->_group ) . '</ins>'; elseif ($this->_tag == 'del') - $this->_line .= '<del class="diffchange diffchange-inline">' . - htmlspecialchars ( $this->_group ) . '</del>'; + $this->_line .= '<del class="diffchange diffchange-inline">' . + htmlspecialchars ( $this->_group ) . '</del>'; else - $this->_line .= htmlspecialchars ( $this->_group ); + $this->_line .= htmlspecialchars ( $this->_group ); } $this->_group = ''; $this->_tag = $new_tag; @@ -1792,21 +1898,21 @@ class _HWLDF_WordAccumulator { function _flushLine ($new_tag) { $this->_flushGroup($new_tag); if ($this->_line != '') - array_push ( $this->_lines, $this->_line ); + array_push ( $this->_lines, $this->_line ); else - # make empty lines visible by inserting an NBSP - array_push ( $this->_lines, NBSP ); + # 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); + $this->_flushGroup($tag); foreach ($words as $word) { // new-line should only come as first char of word. if ($word == '') - continue; + continue; if ($word[0] == "\n") { $this->_flushLine($tag); $word = substr($word, 1); @@ -1837,7 +1943,7 @@ class WordLevelDiff extends MappedDiff { list ($closing_words, $closing_stripped) = $this->_split($closing_lines); $this->MappedDiff($orig_words, $closing_words, - $orig_stripped, $closing_stripped); + $orig_stripped, $closing_stripped); wfProfileOut( __METHOD__ ); } @@ -1862,7 +1968,7 @@ class WordLevelDiff extends MappedDiff { } else { $m = array(); if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', - $line, $m)) + $line, $m)) { $words = array_merge( $words, $m[0] ); $stripped = array_merge( $stripped, $m[1] ); @@ -1879,9 +1985,9 @@ class WordLevelDiff extends MappedDiff { foreach ($this->edits as $edit) { if ($edit->type == 'copy') - $orig->addWords($edit->orig); + $orig->addWords($edit->orig); elseif ($edit->orig) - $orig->addWords($edit->orig, 'del'); + $orig->addWords($edit->orig, 'del'); } $lines = $orig->getLines(); wfProfileOut( __METHOD__ ); @@ -1894,9 +2000,9 @@ class WordLevelDiff extends MappedDiff { foreach ($this->edits as $edit) { if ($edit->type == 'copy') - $closing->addWords($edit->closing); + $closing->addWords($edit->closing); elseif ($edit->closing) - $closing->addWords($edit->closing, 'ins'); + $closing->addWords($edit->closing, 'ins'); } $lines = $closing->getLines(); wfProfileOut( __METHOD__ ); @@ -1969,24 +2075,24 @@ class TableDiffFormatter extends DiffFormatter { function _added( $lines ) { foreach ($lines as $line) { echo '<tr>' . $this->emptyLine() . - $this->addedLine( '<ins class="diffchange">' . - htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n"; + $this->addedLine( '<ins class="diffchange">' . + htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n"; } } function _deleted($lines) { foreach ($lines as $line) { echo '<tr>' . $this->deletedLine( '<del class="diffchange">' . - htmlspecialchars ( $line ) . '</del>' ) . - $this->emptyLine() . "</tr>\n"; + htmlspecialchars ( $line ) . '</del>' ) . + $this->emptyLine() . "</tr>\n"; } } function _context( $lines ) { foreach ($lines as $line) { echo '<tr>' . - $this->contextLine( htmlspecialchars ( $line ) ) . - $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n"; + $this->contextLine( htmlspecialchars ( $line ) ) . + $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n"; } } @@ -2003,11 +2109,11 @@ class TableDiffFormatter extends DiffFormatter { while ( $line = array_shift( $del ) ) { $aline = array_shift( $add ); echo '<tr>' . $this->deletedLine( $line ) . - $this->addedLine( $aline ) . "</tr>\n"; + $this->addedLine( $aline ) . "</tr>\n"; } foreach ($add as $line) { # If any leftovers echo '<tr>' . $this->emptyLine() . - $this->addedLine( $line ) . "</tr>\n"; + $this->addedLine( $line ) . "</tr>\n"; } wfProfileOut( __METHOD__ ); } diff --git a/includes/diff/HTMLDiff.php b/includes/diff/HTMLDiff.php new file mode 100644 index 00000000..0698059f --- /dev/null +++ b/includes/diff/HTMLDiff.php @@ -0,0 +1,1005 @@ +<?php + +/** Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu> + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * or see http://www.gnu.org/ + * + * @ingroup DifferenceEngine + */ + +/** + * When detecting the last common parent of two nodes, all results are stored as + * a LastCommonParentResult. + */ +class LastCommonParentResult { + + // Parent + public $parent; + + // Splitting + public $splittingNeeded = false; + + // Depth + public $lastCommonParentDepth = -1; + + // Index + public $indexInLastCommonParent = -1; +} + +class Modification{ + + const NONE = 1; + const REMOVED = 2; + const ADDED = 4; + const CHANGED = 8; + + public $type; + + public $id = -1; + + public $firstOfID = false; + + public $changes; + + function __construct($type) { + $this->type = $type; + } + + public static function typeToString($type) { + switch($type) { + case self::NONE: return 'none'; + case self::REMOVED: return 'removed'; + case self::ADDED: return 'added'; + case self::CHANGED: return 'changed'; + } + } +} + +class DomTreeBuilder { + + public $textNodes = array(); + + public $bodyNode; + + private $currentParent; + + private $newWord = ''; + + protected $bodyStarted = false; + + protected $bodyEnded = false; + + private $whiteSpaceBeforeThis = false; + + private $lastSibling; + + private $notInPre = true; + + function __construct() { + $this->bodyNode = $this->currentParent = new BodyNode(); + $this->lastSibling = new DummyNode(); + } + + /** + * Must be called manually + */ + public function endDocument() { + $this->endWord(); + HTMLDiffer::diffDebug( count($this->textNodes) . " text nodes in document.\n" ); + } + + public function startElement($parser, $name, /*array*/ $attributes) { + if (strcasecmp($name, 'body') != 0) { + HTMLDiffer::diffDebug( "Starting $name node.\n" ); + $this->endWord(); + + $newNode = new TagNode($this->currentParent, $name, $attributes); + $this->currentParent->children[] = $newNode; + $this->currentParent = $newNode; + $this->lastSibling = new DummyNode(); + if ($this->whiteSpaceBeforeThis && !in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) { + $this->currentParent->whiteBefore = true; + } + $this->whiteSpaceBeforeThis = false; + if(strcasecmp($name, 'pre') == 0) { + $this->notInPre = false; + } + } + } + + public function endElement($parser, $name) { + if(strcasecmp($name, 'body') != 0) { + HTMLDiffer::diffDebug( "Ending $name node.\n"); + if (0 == strcasecmp($name,'img')) { + // Insert a dummy leaf for the image + $img = new ImageNode($this->currentParent, $this->currentParent->attributes); + $this->currentParent->children[] = $img; + $img->whiteBefore = $this->whiteSpaceBeforeThis; + $this->lastSibling = $img; + $this->textNodes[] = $img; + } + $this->endWord(); + if (!in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) { + $this->lastSibling = $this->currentParent; + } else { + $this->lastSibling = new DummyNode(); + } + $this->currentParent = $this->currentParent->parent; + $this->whiteSpaceBeforeThis = false; + if (!$this->notInPre && strcasecmp($name, 'pre') == 0) { + $this->notInPre = true; + } + } else { + $this->endDocument(); + } + } + + const regex = '/([\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1})/'; + const whitespace = '/^[\s]{1}$/'; + const delimiter = '/^[\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1}$/'; + + public function characters($parser, $data) { + $matches = preg_split(self::regex, $data, -1, PREG_SPLIT_DELIM_CAPTURE); + + foreach($matches as &$word) { + if (preg_match(self::whitespace, $word) && $this->notInPre) { + $this->endWord(); + $this->lastSibling->whiteAfter = true; + $this->whiteSpaceBeforeThis = true; + } else if (preg_match(self::delimiter, $word)) { + $this->endWord(); + $textNode = new TextNode($this->currentParent, $word); + $this->currentParent->children[] = $textNode; + $textNode->whiteBefore = $this->whiteSpaceBeforeThis; + $this->whiteSpaceBeforeThis = false; + $this->lastSibling = $textNode; + $this->textNodes[] = $textNode; + } else { + $this->newWord .= $word; + } + } + } + + private function endWord() { + if ($this->newWord !== '') { + $node = new TextNode($this->currentParent, $this->newWord); + $this->currentParent->children[] = $node; + $node->whiteBefore = $this->whiteSpaceBeforeThis; + $this->whiteSpaceBeforeThis = false; + $this->lastSibling = $node; + $this->textNodes[] = $node; + $this->newWord = ""; + } + } + + public function getDiffLines() { + return array_map(array('TextNode','toDiffLine'), $this->textNodes); + } +} + +class TextNodeDiffer { + + private $textNodes; + public $bodyNode; + + private $oldTextNodes; + private $oldBodyNode; + + private $newID = 0; + + private $changedID = 0; + + private $changedIDUsed = false; + + // used to remove the whitespace between a red and green block + private $whiteAfterLastChangedPart = false; + + private $deletedID = 0; + + function __construct(DomTreeBuilder $tree, DomTreeBuilder $oldTree) { + $this->textNodes = $tree->textNodes; + $this->bodyNode = $tree->bodyNode; + $this->oldTextNodes = $oldTree->textNodes; + $this->oldBodyNode = $oldTree->bodyNode; + } + + public function markAsNew($start, $end) { + if ($end <= $start) { + return; + } + + if ($this->whiteAfterLastChangedPart) { + $this->textNodes[$start]->whiteBefore = false; + } + + for ($i = $start; $i < $end; ++$i) { + $mod = new Modification(Modification::ADDED); + $mod->id = $this->newID; + $this->textNodes[$i]->modification = $mod; + } + if ($start < $end) { + $this->textNodes[$start]->modification->firstOfID = true; + } + ++$this->newID; + } + + public function handlePossibleChangedPart($leftstart, $leftend, $rightstart, $rightend) { + $i = $rightstart; + $j = $leftstart; + + if ($this->changedIDUsed) { + ++$this->changedID; + $this->changedIDUsed = false; + } + + $changes; + while ($i < $rightend) { + $acthis = new AncestorComparator($this->textNodes[$i]->getParentTree()); + $acother = new AncestorComparator($this->oldTextNodes[$j]->getParentTree()); + $result = $acthis->getResult($acother); + unset($acthis, $acother); + + if ( $result ) { + $mod = new Modification(Modification::CHANGED); + + if (!$this->changedIDUsed) { + $mod->firstOfID = true; + } else if (!is_null( $result ) && $result !== $this->changes) { + ++$this->changedID; + $mod->firstOfID = true; + } + + $mod->changes = $result; + $mod->id = $this->changedID; + + $this->textNodes[$i]->modification = $mod; + $this->changes = $result; + $this->changedIDUsed = true; + } else if ($this->changedIDUsed) { + ++$this->changedID; + $this->changedIDUsed = false; + } + ++$i; + ++$j; + } + } + + public function markAsDeleted($start, $end, $before) { + + if ($end <= $start) { + return; + } + + if ($before > 0 && $this->textNodes[$before - 1]->whiteAfter) { + $this->whiteAfterLastChangedPart = true; + } else { + $this->whiteAfterLastChangedPart = false; + } + + for ($i = $start; $i < $end; ++$i) { + $mod = new Modification(Modification::REMOVED); + $mod->id = $this->deletedID; + + // oldTextNodes is used here because we're going to move its deleted + // elements to this tree! + $this->oldTextNodes[$i]->modification = $mod; + } + $this->oldTextNodes[$start]->modification->firstOfID = true; + + $root = $this->oldTextNodes[$start]->getLastCommonParent($this->oldTextNodes[$end-1])->parent; + + $junk1 = $junk2 = null; + $deletedNodes = $root->getMinimalDeletedSet($this->deletedID, $junk1, $junk2); + + HTMLDiffer::diffDebug( "Minimal set of deleted nodes of size " . count($deletedNodes) . "\n" ); + + // Set prevLeaf to the leaf after which the old HTML needs to be + // inserted + if ($before > 0) { + $prevLeaf = $this->textNodes[$before - 1]; + } + // Set nextLeaf to the leaf before which the old HTML needs to be + // inserted + if ($before < count($this->textNodes)) { + $nextLeaf = $this->textNodes[$before]; + } + + while (count($deletedNodes) > 0) { + if (isset($prevLeaf)) { + $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]); + } else { + $prevResult = new LastCommonParentResult(); + $prevResult->parent = $this->bodyNode; + $prevResult->indexInLastCommonParent = -1; + } + if (isset($nextleaf)) { + $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[count($deletedNodes) - 1]); + } else { + $nextResult = new LastCommonParentResult(); + $nextResult->parent = $this->bodyNode; + $nextResult->indexInLastCommonParent = $this->bodyNode->getNbChildren(); + } + + if ($prevResult->lastCommonParentDepth == $nextResult->lastCommonParentDepth) { + // We need some metric to choose which way to add-... + if ($deletedNodes[0]->parent === $deletedNodes[count($deletedNodes) - 1]->parent + && $prevResult->parent === $nextResult->parent) { + // The difference is not in the parent + $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1; + } else { + // The difference is in the parent, so compare them + // now THIS is tricky + $distancePrev = $deletedNodes[0]->parent->getMatchRatio($prevResult->parent); + $distanceNext = $deletedNodes[count($deletedNodes) - 1]->parent->getMatchRatio($nextResult->parent); + + if ($distancePrev <= $distanceNext) { + $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1; + } else { + $nextResult->lastCommonParentDepth = $nextResult->lastCommonParentDepth + 1; + } + } + + } + + if ($prevResult->lastCommonParentDepth > $nextResult->lastCommonParentDepth) { + // Inserting at the front + if ($prevResult->splittingNeeded) { + $prevLeaf->parent->splitUntil($prevResult->parent, $prevLeaf, true); + } + $prevLeaf = $deletedNodes[0]->copyTree(); + unset($deletedNodes[0]); + $deletedNodes = array_values($deletedNodes); + $prevLeaf->setParent($prevResult->parent); + $prevResult->parent->addChildAbsolute($prevLeaf,$prevResult->indexInLastCommonParent + 1); + } else if ($prevResult->lastCommonParentDepth < $nextResult->lastCommonParentDepth) { + // Inserting at the back + if ($nextResult->splittingNeeded) { + $splitOccured = $nextLeaf->parent->splitUntil($nextResult->parent, $nextLeaf, false); + if ($splitOccured) { + // The place where to insert is shifted one place to the + // right + $nextResult->indexInLastCommonParent = $nextResult->indexInLastCommonParent + 1; + } + } + $nextLeaf = $deletedNodes[count(deletedNodes) - 1]->copyTree(); + unset($deletedNodes[count(deletedNodes) - 1]); + $deletedNodes = array_values($deletedNodes); + $nextLeaf->setParent($nextResult->parent); + $nextResult->parent->addChildAbsolute($nextLeaf,$nextResult->indexInLastCommonParent); + } + } + ++$this->deletedID; + } + + public function expandWhiteSpace() { + $this->bodyNode->expandWhiteSpace(); + } + + public function lengthNew(){ + return count($this->textNodes); + } + + public function lengthOld(){ + return count($this->oldTextNodes); + } +} + +class HTMLDiffer { + + private $output; + private static $debug = ''; + + function __construct($output) { + $this->output = $output; + } + + function htmlDiff($from, $to) { + wfProfileIn( __METHOD__ ); + // Create an XML parser + $xml_parser = xml_parser_create(''); + + $domfrom = new DomTreeBuilder(); + + // Set the functions to handle opening and closing tags + xml_set_element_handler($xml_parser, array($domfrom, "startElement"), array($domfrom, "endElement")); + + // Set the function to handle blocks of character data + xml_set_character_data_handler($xml_parser, array($domfrom, "characters")); + + HTMLDiffer::diffDebug( "Parsing " . strlen($from) . " characters worth of HTML\n" ); + if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false) + || !xml_parse($xml_parser, $from, false) + || !xml_parse($xml_parser, '</body>', true)){ + $error = xml_error_string(xml_get_error_code($xml_parser)); + $line = xml_get_current_line_number($xml_parser); + HTMLDiffer::diffDebug( "XML error: $error at line $line\n" ); + } + xml_parser_free($xml_parser); + unset($from); + + $xml_parser = xml_parser_create(''); + + $domto = new DomTreeBuilder(); + + // Set the functions to handle opening and closing tags + xml_set_element_handler($xml_parser, array($domto, "startElement"), array($domto, "endElement")); + + // Set the function to handle blocks of character data + xml_set_character_data_handler($xml_parser, array($domto, "characters")); + + HTMLDiffer::diffDebug( "Parsing " . strlen($to) . " characters worth of HTML\n" ); + if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false) + || !xml_parse($xml_parser, $to, false) + || !xml_parse($xml_parser, '</body>', true)){ + $error = xml_error_string(xml_get_error_code($xml_parser)); + $line = xml_get_current_line_number($xml_parser); + HTMLDiffer::diffDebug( "XML error: $error at line $line\n" ); + } + xml_parser_free($xml_parser); + unset($to); + + $diffengine = new WikiDiff3(); + $differences = $this->preProcess($diffengine->diff_range($domfrom->getDiffLines(), $domto->getDiffLines())); + unset($xml_parser, $diffengine); + + $domdiffer = new TextNodeDiffer($domto, $domfrom); + + $currentIndexLeft = 0; + $currentIndexRight = 0; + foreach ($differences as &$d) { + if ($d->leftstart > $currentIndexLeft) { + $domdiffer->handlePossibleChangedPart($currentIndexLeft, $d->leftstart, + $currentIndexRight, $d->rightstart); + } + if ($d->leftlength > 0) { + $domdiffer->markAsDeleted($d->leftstart, $d->leftend, $d->rightstart); + } + $domdiffer->markAsNew($d->rightstart, $d->rightend); + + $currentIndexLeft = $d->leftend; + $currentIndexRight = $d->rightend; + } + $oldLength = $domdiffer->lengthOld(); + if ($currentIndexLeft < $oldLength) { + $domdiffer->handlePossibleChangedPart($currentIndexLeft, $oldLength, $currentIndexRight, $domdiffer->lengthNew()); + } + $domdiffer->expandWhiteSpace(); + $output = new HTMLOutput('htmldiff', $this->output); + $output->parse($domdiffer->bodyNode); + wfProfileOut( __METHOD__ ); + } + + private function preProcess(/*array*/ $differences) { + $newRanges = array(); + + $nbDifferences = count($differences); + for ($i = 0; $i < $nbDifferences; ++$i) { + $leftStart = $differences[$i]->leftstart; + $leftEnd = $differences[$i]->leftend; + $rightStart = $differences[$i]->rightstart; + $rightEnd = $differences[$i]->rightend; + + $leftLength = $leftEnd - $leftStart; + $rightLength = $rightEnd - $rightStart; + + while ($i + 1 < $nbDifferences && self::score($leftLength, + $differences[$i + 1]->leftlength, + $rightLength, + $differences[$i + 1]->rightlength) + > ($differences[$i + 1]->leftstart - $leftEnd)) { + $leftEnd = $differences[$i + 1]->leftend; + $rightEnd = $differences[$i + 1]->rightend; + $leftLength = $leftEnd - $leftStart; + $rightLength = $rightEnd - $rightStart; + ++$i; + } + $newRanges[] = new RangeDifference($leftStart, $leftEnd, $rightStart, $rightEnd); + } + return $newRanges; + } + + /** + * Heuristic to merge differences for readability. + */ + public static function score($ll, $nll, $rl, $nrl) { + if (($ll == 0 && $nll == 0) + || ($rl == 0 && $nrl == 0)) { + return 0; + } + $numbers = array($ll, $nll, $rl, $nrl); + $d = 0; + foreach ($numbers as &$number) { + while ($number > 3) { + $d += 3; + $number -= 3; + $number *= 0.5; + } + $d += $number; + + } + return $d / (1.5 * count($numbers)); + } + + /** + * Add to debug output + * @param string $str Debug output + */ + public static function diffDebug( $str ) { + self :: $debug .= $str; + } + + /** + * Get debug output + * @return string + */ + public static function getDebugOutput() { + return self :: $debug; + } + +} + +class TextOnlyComparator { + + public $leafs = array(); + + function _construct(TagNode $tree) { + $this->addRecursive($tree); + $this->leafs = array_map(array('TextNode','toDiffLine'), $this->leafs); + } + + private function addRecursive(TagNode $tree) { + foreach ($tree->children as &$child) { + if ($child instanceof TagNode) { + $this->addRecursive($child); + } else if ($child instanceof TextNode) { + $this->leafs[] = $node; + } + } + } + + public function getMatchRatio(TextOnlyComparator $other) { + $nbOthers = count($other->leafs); + $nbThis = count($this->leafs); + if($nbOthers == 0 || $nbThis == 0){ + return -log(0); + } + + $diffengine = new WikiDiff3(25000, 1.35); + $diffengine->diff($this->leafs, $other->leafs); + + $lcsLength = $diffengine->getLcsLength(); + + $distanceThis = $nbThis-$lcsLength; + + return (2.0 - $lcsLength/$nbOthers - $lcsLength/$nbThis) / 2.0; + } +} + +/** + * A comparator used when calculating the difference in ancestry of two Nodes. + */ +class AncestorComparator { + + public $ancestors; + public $ancestorsText; + + function __construct(/*array*/ $ancestors) { + $this->ancestors = $ancestors; + $this->ancestorsText = array_map(array('TagNode','toDiffLine'), $ancestors); + } + + public $compareTxt = ""; + + public function getResult(AncestorComparator $other) { + + $diffengine = new WikiDiff3(10000, 1.35); + $differences = $diffengine->diff_range($other->ancestorsText,$this->ancestorsText); + + if (count($differences) == 0){ + return null; + } + $changeTxt = new ChangeTextGenerator($this, $other); + + return $changeTxt->getChanged($differences)->toString();; + } +} + +class ChangeTextGenerator { + + private $ancestorComparator; + private $other; + + private $factory; + + function __construct(AncestorComparator $ancestorComparator, AncestorComparator $other) { + $this->ancestorComparator = $ancestorComparator; + $this->other = $other; + $this->factory = new TagToStringFactory(); + } + + public function getChanged(/*array*/ $differences) { + $txt = new ChangeText; + $rootlistopened = false; + if (count($differences) > 1) { + $txt->addHtml('<ul class="changelist">'); + $rootlistopened = true; + } + $nbDifferences = count($differences); + for ($j = 0; $j < $nbDifferences; ++$j) { + $d = $differences[$j]; + $lvl1listopened = false; + if ($rootlistopened) { + $txt->addHtml('<li>'); + } + if ($d->leftlength + $d->rightlength > 1) { + $txt->addHtml('<ul class="changelist">'); + $lvl1listopened = true; + } + // left are the old ones + for ($i = $d->leftstart; $i < $d->leftend; ++$i) { + if ($lvl1listopened){ + $txt->addHtml('<li>'); + } + // add a bullet for a old tag + $this->addTagOld($txt, $this->other->ancestors[$i]); + if ($lvl1listopened){ + $txt->addHtml('</li>'); + } + } + // right are the new ones + for ($i = $d->rightstart; $i < $d->rightend; ++$i) { + if ($lvl1listopened){ + $txt->addHtml('<li>'); + } + // add a bullet for a new tag + $this->addTagNew($txt, $this->ancestorComparator->ancestors[$i]); + + if ($lvl1listopened){ + $txt->addHtml('</li>'); + } + } + if ($lvl1listopened) { + $txt->addHtml('</ul>'); + } + if ($rootlistopened) { + $txt->addHtml('</li>'); + } + } + if ($rootlistopened) { + $txt->addHtml('</ul>'); + } + return $txt; + } + + private function addTagOld(ChangeText $txt, TagNode $ancestor) { + $this->factory->create($ancestor)->getRemovedDescription($txt); + } + + private function addTagNew(ChangeText $txt, TagNode $ancestor) { + $this->factory->create($ancestor)->getAddedDescription($txt); + } +} + +class ChangeText { + + private $txt = ""; + + public function addHtml($s) { + $this->txt .= $s; + } + + public function toString() { + return $this->txt; + } +} + +class TagToStringFactory { + + private static $containerTags = array('html', 'body', 'p', 'blockquote', + 'h1', 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', + 'table', 'tbody', 'tr', 'td', 'th', 'br', 'hr', 'code', 'dl', + 'dt', 'dd', 'input', 'form', 'img', 'span', 'a'); + + private static $styleTags = array('i', 'b', 'strong', 'em', 'font', + 'big', 'del', 'tt', 'sub', 'sup', 'strike'); + + const MOVED = 1; + const STYLE = 2; + const UNKNOWN = 4; + + public function create(TagNode $node) { + $sem = $this->getChangeSemantic($node->qName); + if (strcasecmp($node->qName,'a') == 0) { + return new AnchorToString($node, $sem); + } + if (strcasecmp($node->qName,'img') == 0) { + return new NoContentTagToString($node, $sem); + } + return new TagToString($node, $sem); + } + + protected function getChangeSemantic($qname) { + if (in_array(strtolower($qname),self::$containerTags)) { + return self::MOVED; + } + if (in_array(strtolower($qname),self::$styleTags)) { + return self::STYLE; + } + return self::UNKNOWN; + } +} + +class TagToString { + + protected $node; + + protected $sem; + + function __construct(TagNode $node, $sem) { + $this->node = $node; + $this->sem = $sem; + } + + public function getRemovedDescription(ChangeText $txt) { + $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' ); + if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){ + $tagDescription = "<" . $this->node->qName . ">"; + } + if ($this->sem == TagToStringFactory::MOVED) { + $txt->addHtml( wfMsgExt( 'diff-movedoutof', 'parseinline', $tagDescription ) ); + } else if ($this->sem == TagToStringFactory::STYLE) { + $txt->addHtml( wfMsgExt( 'diff-styleremoved' , 'parseinline', $tagDescription ) ); + } else { + $txt->addHtml( wfMsgExt( 'diff-removed' , 'parseinline', $tagDescription ) ); + } + $this->addAttributes($txt, $this->node->attributes); + $txt->addHtml('.'); + } + + public function getAddedDescription(ChangeText $txt) { + $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' ); + if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){ + $tagDescription = "<" . $this->node->qName . ">"; + } + if ($this->sem == TagToStringFactory::MOVED) { + $txt->addHtml( wfMsgExt( 'diff-movedto' , 'parseinline', $tagDescription) ); + } else if ($this->sem == TagToStringFactory::STYLE) { + $txt->addHtml( wfMsgExt( 'diff-styleadded', 'parseinline', $tagDescription ) ); + } else { + $txt->addHtml( wfMsgExt( 'diff-added', 'parseinline', $tagDescription ) ); + } + $this->addAttributes($txt, $this->node->attributes); + $txt->addHtml('.'); + } + + protected function addAttributes(ChangeText $txt, array $attributes) { + if (count($attributes) < 1) { + return; + } + $firstOne = true; + $nbAttributes_min_1 = count($attributes)-1; + $keys = array_keys($attributes); + for ($i=0;$i<$nbAttributes_min_1;$i++) { + $key = $keys[$i]; + $attr = $attributes[$key]; + if($firstOne) { + $firstOne = false; + $txt->addHtml( wfMsgExt('diff-with', 'escapenoentities', $this->translateArgument($key), htmlspecialchars($attr) ) ); + continue; + } + $txt->addHtml( wfMsgExt( 'comma-separator', 'escapenoentities' ) . + wfMsgExt( 'diff-with-additional', 'escapenoentities', + $this->translateArgument( $key ), htmlspecialchars( $attr ) ) + ); + } + + if ($nbAttributes_min_1 > 0) { + $txt->addHtml( wfMsgExt( 'diff-with-final', 'escapenoentities', + $this->translateArgument($keys[$nbAttributes_min_1]), + htmlspecialchars($attributes[$keys[$nbAttributes_min_1]]) ) ); + } + } + + protected function translateArgument($name) { + $translation = wfMsgExt('diff-' . $name, 'parseinline' ); + if ( wfEmptyMsg( 'diff-' . $name, $translation ) ) { + $translation = "<" . $name . ">";; + } + return htmlspecialchars( $translation ); + } +} + +class NoContentTagToString extends TagToString { + + function __construct(TagNode $node, $sem) { + parent::__construct($node, $sem); + } + + public function getAddedDescription(ChangeText $txt) { + $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' ); + if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){ + $tagDescription = "<" . $this->node->qName . ">"; + } + $txt->addHtml( wfMsgExt('diff-changedto', 'parseinline', $tagDescription ) ); + $this->addAttributes($txt, $this->node->attributes); + $txt->addHtml('.'); + } + + public function getRemovedDescription(ChangeText $txt) { + $txt->addHtml( wfMsgExt('diff-changedfrom', 'parseinline', $tagDescription ) ); + $this->addAttributes($txt, $this->node->attributes); + $txt->addHtml('.'); + } +} + +class AnchorToString extends TagToString { + + function __construct(TagNode $node, $sem) { + parent::__construct($node, $sem); + } + + protected function addAttributes(ChangeText $txt, array $attributes) { + if (array_key_exists('href', $attributes)) { + $txt->addHtml(' ' . wfMsgExt( 'diff-withdestination', 'parseinline', htmlspecialchars($attributes['href']) ) ); + unset($attributes['href']); + } + parent::addAttributes($txt, $attributes); + } +} + +/** + * Takes a branch root and creates an HTML file for it. + */ +class HTMLOutput{ + + private $prefix; + private $handler; + + function __construct($prefix, $handler) { + $this->prefix = $prefix; + $this->handler = $handler; + } + + public function parse(TagNode $node) { + $handler = &$this->handler; + + if (strcasecmp($node->qName, 'img') != 0 && strcasecmp($node->qName, 'body') != 0) { + $handler->startElement($node->qName, $node->attributes); + } + + $newStarted = false; + $remStarted = false; + $changeStarted = false; + $changeTXT = ''; + + foreach ($node->children as &$child) { + if ($child instanceof TagNode) { + if ($newStarted) { + $handler->endElement('span'); + $newStarted = false; + } else if ($changeStarted) { + $handler->endElement('span'); + $changeStarted = false; + } else if ($remStarted) { + $handler->endElement('span'); + $remStarted = false; + } + $this->parse($child); + } else if ($child instanceof TextNode) { + $mod = $child->modification; + + if ($newStarted && ($mod->type != Modification::ADDED || $mod->firstOfID)) { + $handler->endElement('span'); + $newStarted = false; + } else if ($changeStarted && ($mod->type != Modification::CHANGED + || $mod->changes != $changeTXT || $mod->firstOfID)) { + $handler->endElement('span'); + $changeStarted = false; + } else if ($remStarted && ($mod->type != Modification::REMOVED || $mod ->firstOfID)) { + $handler->endElement('span'); + $remStarted = false; + } + + // no else because a removed part can just be closed and a new + // part can start + if (!$newStarted && $mod->type == Modification::ADDED) { + $attrs = array('class' => 'diff-html-added'); + if ($mod->firstOfID) { + $attrs['id'] = "added-{$this->prefix}-{$mod->id}"; + } + $handler->startElement('span', $attrs); + $newStarted = true; + } else if (!$changeStarted && $mod->type == Modification::CHANGED) { + $attrs = array('class' => 'diff-html-changed'); + if ($mod->firstOfID) { + $attrs['id'] = "changed-{$this->prefix}-{$mod->id}"; + } + $handler->startElement('span', $attrs); + + //tooltip + $handler->startElement('span', array('class' => 'tip')); + $handler->html($mod->changes); + $handler->endElement('span'); + + $changeStarted = true; + $changeTXT = $mod->changes; + } else if (!$remStarted && $mod->type == Modification::REMOVED) { + $attrs = array('class'=>'diff-html-removed'); + if ($mod->firstOfID) { + $attrs['id'] = "removed-{$this->prefix}-{$mod->id}"; + } + $handler->startElement('span', $attrs); + $remStarted = true; + } + + $chars = $child->text; + + if ($child instanceof ImageNode) { + $this->writeImage($child); + } else { + $handler->characters($chars); + } + } + } + + if ($newStarted) { + $handler->endElement('span'); + $newStarted = false; + } else if ($changeStarted) { + $handler->endElement('span'); + $changeStarted = false; + } else if ($remStarted) { + $handler->endElement('span'); + $remStarted = false; + } + + if (strcasecmp($node->qName, 'img') != 0 + && strcasecmp($node->qName, 'body') != 0) { + $handler->endElement($node->qName); + } + } + + private function writeImage(ImageNode $imgNode) { + $attrs = $imgNode->attributes; + $this->handler->startElement('img', $attrs); + $this->handler->endElement('img'); + } +} + +class DelegatingContentHandler { + + private $delegate; + + function __construct($delegate) { + $this->delegate = $delegate; + } + + function startElement($qname, /*array*/ $arguments) { + $this->delegate->addHtml(Xml::openElement($qname, $arguments)); + } + + function endElement($qname){ + $this->delegate->addHtml(Xml::closeElement($qname)); + } + + function characters($chars){ + $this->delegate->addHtml(htmlspecialchars($chars)); + } + + function html($html){ + $this->delegate->addHtml($html); + } +} diff --git a/includes/diff/Nodes.php b/includes/diff/Nodes.php new file mode 100644 index 00000000..1b1363d4 --- /dev/null +++ b/includes/diff/Nodes.php @@ -0,0 +1,439 @@ +<?php + +/** Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu> + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * or see http://www.gnu.org/ + * + */ + +/** + * Any element in the DOM tree of an HTML document. + * @ingroup DifferenceEngine + */ +class Node { + + public $parent; + + protected $parentTree; + + public $whiteBefore = false; + + public $whiteAfter = false; + + function __construct($parent) { + $this->parent = $parent; + } + + public function getParentTree() { + if (!isset($this->parentTree)) { + if (!is_null($this->parent)) { + $this->parentTree = $this->parent->getParentTree(); + $this->parentTree[] = $this->parent; + } else { + $this->parentTree = array(); + } + } + return $this->parentTree; + } + + public function getLastCommonParent(Node $other) { + $result = new LastCommonParentResult(); + + $myParents = $this->getParentTree(); + $otherParents = $other->getParentTree(); + + $i = 1; + $isSame = true; + $nbMyParents = count($myParents); + $nbOtherParents = count($otherParents); + while ($isSame && $i < $nbMyParents && $i < $nbOtherParents) { + if (!$myParents[$i]->openingTag === $otherParents[$i]->openingTag) { + $isSame = false; + } else { + // After a while, the index i-1 must be the last common parent + $i++; + } + } + + $result->lastCommonParentDepth = $i - 1; + $result->parent = $myParents[$i - 1]; + + if (!$isSame || $nbMyParents > $nbOtherParents) { + // Not all tags matched, or all tags matched but + // there are tags left in this tree + $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($myParents[$i]); + $result->splittingNeeded = true; + } else if ($nbMyParents <= $nbOtherParents) { + $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($this); + } + return $result; + } + + public function setParent($parent) { + $this->parent = $parent; + unset($this->parentTree); + } + + public function inPre() { + $tree = $this->getParentTree(); + foreach ($tree as &$ancestor) { + if ($ancestor->isPre()) { + return true; + } + } + return false; + } +} + +/** + * Node that can contain other nodes. Represents an HTML tag. + * @ingroup DifferenceEngine + */ +class TagNode extends Node { + + public $children = array(); + + public $qName; + + public $attributes = array(); + + public $openingTag; + + function __construct($parent, $qName, /*array*/ $attributes) { + parent::__construct($parent); + $this->qName = strtolower($qName); + foreach($attributes as $key => &$value){ + $this->attributes[strtolower($key)] = $value; + } + return $this->openingTag = Xml::openElement($this->qName, $this->attributes); + } + + public function addChildAbsolute(Node $node, $index) { + array_splice($this->children, $index, 0, array($node)); + } + + public function getIndexOf(Node $child) { + // don't trust array_search with objects + foreach ($this->children as $key => &$value){ + if ($value === $child) { + return $key; + } + } + return null; + } + + public function getNbChildren() { + return count($this->children); + } + + public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) { + $nodes = array(); + + $allDeleted = false; + $somethingDeleted = false; + $hasNonDeletedDescendant = false; + + if (empty($this->children)) { + return $nodes; + } + + foreach ($this->children as &$child) { + $allDeleted_local = false; + $somethingDeleted_local = false; + $childrenChildren = $child->getMinimalDeletedSet($id, $allDeleted_local, $somethingDeleted_local); + if ($somethingDeleted_local) { + $nodes = array_merge($nodes, $childrenChildren); + $somethingDeleted = true; + } + if (!$allDeleted_local) { + $hasNonDeletedDescendant = true; + } + } + if (!$hasNonDeletedDescendant) { + $nodes = array($this); + $allDeleted = true; + } + return $nodes; + } + + public function splitUntil(TagNode $parent, Node $split, $includeLeft) { + $splitOccured = false; + if ($parent !== $this) { + $part1 = new TagNode(null, $this->qName, $this->attributes); + $part2 = new TagNode(null, $this->qName, $this->attributes); + $part1->setParent($this->parent); + $part2->setParent($this->parent); + + $onSplit = false; + $pastSplit = false; + foreach ($this->children as &$child) + { + if ($child === $split) { + $onSplit = true; + } + if(!$pastSplit || ($onSplit && $includeLeft)) { + $child->setParent($part1); + $part1->children[] = $child; + } else { + $child->setParent($part2); + $part2->children[] = $child; + } + if ($onSplit) { + $onSplit = false; + $pastSplit = true; + } + } + $myindexinparent = $this->parent->getIndexOf($this); + if (!empty($part1->children)) { + $this->parent->addChildAbsolute($part1, $myindexinparent); + } + if (!empty($part2->children)) { + $this->parent->addChildAbsolute($part2, $myindexinparent); + } + if (!empty($part1->children) && !empty($part2->children)) { + $splitOccured = true; + } + + $this->parent->removeChild($myindexinparent); + + if ($includeLeft) { + $this->parent->splitUntil($parent, $part1, $includeLeft); + } else { + $this->parent->splitUntil($parent, $part2, $includeLeft); + } + } + return $splitOccured; + + } + + private function removeChild($index) { + unset($this->children[$index]); + $this->children = array_values($this->children); + } + + public static $blocks = array('html', 'body','p','blockquote', 'h1', + 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', 'table', + 'tbody', 'tr', 'td', 'th', 'br'); + + public function copyTree() { + $newThis = new TagNode(null, $this->qName, $this->attributes); + $newThis->whiteBefore = $this->whiteBefore; + $newThis->whiteAfter = $this->whiteAfter; + foreach ($this->children as &$child) { + $newChild = $child->copyTree(); + $newChild->setParent($newThis); + $newThis->children[] = $newChild; + } + return $newThis; + } + + public function getMatchRatio(TagNode $other) { + $txtComp = new TextOnlyComparator($other); + return $txtComp->getMatchRatio(new TextOnlyComparator($this)); + } + + public function expandWhiteSpace() { + $shift = 0; + $spaceAdded = false; + + $nbOriginalChildren = $this->getNbChildren(); + for ($i = 0; $i < $nbOriginalChildren; ++$i) { + $child = $this->children[$i + $shift]; + + if ($child instanceof TagNode) { + if (!$child->isPre()) { + $child->expandWhiteSpace(); + } + } + if (!$spaceAdded && $child->whiteBefore) { + $ws = new WhiteSpaceNode(null, ' ', $child->getLeftMostChild()); + $ws->setParent($this); + $this->addChildAbsolute($ws,$i + ($shift++)); + } + if ($child->whiteAfter) { + $ws = new WhiteSpaceNode(null, ' ', $child->getRightMostChild()); + $ws->setParent($this); + $this->addChildAbsolute($ws,$i + 1 + ($shift++)); + $spaceAdded = true; + } else { + $spaceAdded = false; + } + + } + } + + public function getLeftMostChild() { + if (empty($this->children)) { + return $this; + } + return $this->children[0]->getLeftMostChild(); + } + + public function getRightMostChild() { + if (empty($this->children)) { + return $this; + } + return $this->children[$this->getNbChildren() - 1]->getRightMostChild(); + } + + public function isPre() { + return 0 == strcasecmp($this->qName,'pre'); + } + + public static function toDiffLine(TagNode $node) { + return $node->openingTag; + } +} + +/** + * Represents a piece of text in the HTML file. + * @ingroup DifferenceEngine + */ +class TextNode extends Node { + + public $text; + + public $modification; + + function __construct($parent, $text) { + parent::__construct($parent); + $this->modification = new Modification(Modification::NONE); + $this->text = $text; + } + + public function copyTree() { + $clone = clone $this; + $clone->setParent(null); + return $clone; + } + + public function getLeftMostChild() { + return $this; + } + + public function getRightMostChild() { + return $this; + } + + public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) { + if ($this->modification->type == Modification::REMOVED + && $this->modification->id == $id){ + $somethingDeleted = true; + $allDeleted = true; + return array($this); + } + return array(); + } + + public function isSameText($other) { + if (is_null($other) || ! $other instanceof TextNode) { + return false; + } + return str_replace('\n', ' ',$this->text) === str_replace('\n', ' ',$other->text); + } + + public static function toDiffLine(TextNode $node) { + return str_replace('\n', ' ',$node->text); + } +} + +/** + * @todo Document + * @ingroup DifferenceEngine + */ +class WhiteSpaceNode extends TextNode { + + function __construct($parent, $s, Node $like = null) { + parent::__construct($parent, $s); + if(!is_null($like) && $like instanceof TextNode) { + $newModification = clone $like->modification; + $newModification->firstOfID = false; + $this->modification = $newModification; + } + } +} + +/** + * Represents the root of a HTML document. + * @ingroup DifferenceEngine + */ +class BodyNode extends TagNode { + + function __construct() { + parent::__construct(null, 'body', array()); + } + + public function copyTree() { + $newThis = new BodyNode(); + foreach ($this->children as &$child) { + $newChild = $child->copyTree(); + $newChild->setParent($newThis); + $newThis->children[] = $newChild; + } + return $newThis; + } + + public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) { + $nodes = array(); + foreach ($this->children as &$child) { + $childrenChildren = $child->getMinimalDeletedSet($id, + $allDeleted, $somethingDeleted); + $nodes = array_merge($nodes, $childrenChildren); + } + return $nodes; + } + +} + +/** + * Represents an image in HTML. Even though images do not contain any text they + * are independent visible objects on the page. They are logically a TextNode. + * @ingroup DifferenceEngine + */ +class ImageNode extends TextNode { + + public $attributes; + + function __construct(TagNode $parent, /*array*/ $attrs) { + if(!array_key_exists('src', $attrs)) { + HTMLDiffer::diffDebug( "Image without a source\n" ); + parent::__construct($parent, '<img></img>'); + }else{ + parent::__construct($parent, '<img>' . strtolower($attrs['src']) . '</img>'); + } + $this->attributes = $attrs; + } + + public function isSameText($other) { + if (is_null($other) || ! $other instanceof ImageNode) { + return false; + } + return $this->text === $other->text; + } + +} + +/** + * No-op node + * @ingroup DifferenceEngine + */ +class DummyNode extends Node { + + function __construct() { + // no op + } + +} diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php index 646256bb..3919cfbc 100644 --- a/includes/filerepo/ArchivedFile.php +++ b/includes/filerepo/ArchivedFile.php @@ -30,12 +30,9 @@ class ArchivedFile /**#@-*/ function ArchivedFile( $title, $id=0, $key='' ) { - if( !is_object($title) ) { - throw new MWException( 'ArchivedFile constructor given bogus title.' ); - } $this->id = -1; - $this->title = $title; - $this->name = $title->getDBkey(); + $this->title = false; + $this->name = false; $this->group = ''; $this->key = ''; $this->size = 0; @@ -51,6 +48,20 @@ class ArchivedFile $this->timestamp = NULL; $this->deleted = 0; $this->dataLoaded = false; + + if( is_object($title) ) { + $this->title = $title; + $this->name = $title->getDBkey(); + } + + if ($id) + $this->id = $id; + + if ($key) + $this->key = $key; + + if (!$id && !$key && !is_object($title)) + throw new MWException( "No specifications provided to ArchivedFile constructor." ); } /** @@ -61,8 +72,19 @@ class ArchivedFile if ( $this->dataLoaded ) { return true; } - $conds = ($this->id) ? "fa_id = {$this->id}" : "fa_storage_key = '{$this->key}'"; - if( $this->title->getNamespace() == NS_IMAGE ) { + $conds = array(); + + if ($this->id>0) + $conds['fa_id'] = $this->id; + if ($this->key) + $conds['fa_storage_key'] = $this->key; + if ($this->title) + $conds['fa_name'] = $this->title->getDBkey(); + + if (!count($conds)) + throw new MWException( "No specific information for retrieving archived file" ); + + if( !$this->title || $this->title->getNamespace() == NS_FILE ) { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'filearchive', array( @@ -84,9 +106,7 @@ class ArchivedFile 'fa_user_text', 'fa_timestamp', 'fa_deleted' ), - array( - 'fa_name' => $this->title->getDBkey(), - $conds ), + $conds, __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); @@ -129,7 +149,7 @@ class ArchivedFile * @return ResultWrapper */ public static function newFromRow( $row ) { - $file = new ArchivedFile( Title::makeTitle( NS_IMAGE, $row->fa_name ) ); + $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) ); $file->id = intval($row->fa_id); $file->name = $row->fa_name; @@ -251,7 +271,7 @@ class ArchivedFile */ public function getTimestamp() { $this->load(); - return $this->timestamp; + return wfTimestamp( TS_MW, $this->timestamp ); } /** diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index eb8df0f5..d561e61b 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -6,7 +6,7 @@ * @ingroup FileRepo */ class FSRepo extends FileRepo { - var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels; + var $directory, $deletedDir, $url, $deletedHashLevels; var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); var $oldFileFactory = false; var $pathDisclosureProtection = 'simple'; @@ -452,14 +452,6 @@ class FSRepo extends FileRepo { } /** - * Get a relative path including trailing slash, e.g. f/fa/ - * If the repo is not hashed, returns an empty string - */ - function getHashPath( $name ) { - return FileRepo::getHashPathForLevel( $name, $this->hashLevels ); - } - - /** * Get a relative path for a deletion archive key, * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg */ diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index 64b48e0a..4f0990af 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -264,7 +264,14 @@ abstract class File { * Overridden by LocalFile, UnregisteredLocalFile * STUB */ - function getMetadata() { return false; } + public function getMetadata() { return false; } + + /** + * Return the bit depth of the file + * Overridden by LocalFile + * STUB + */ + public function getBitDepth() { return 0; } /** * Return the size of the image file, in bytes @@ -499,8 +506,7 @@ abstract class File { * * @param integer $width maximum width of the generated thumbnail * @param integer $height maximum height of the image (optional) - * @param boolean $render True to render the thumbnail if it doesn't exist, - * false to just return the URL + * @param boolean $render Deprecated * * @return ThumbnailImage or null on failure * @@ -511,8 +517,7 @@ abstract class File { if ( $height != -1 ) { $params['height'] = $height; } - $flags = $render ? self::RENDER_NOW : 0; - return $this->transform( $params, $flags ); + return $this->transform( $params, 0 ); } /** @@ -575,7 +580,7 @@ abstract class File { // Purge. Useful in the event of Core -> Squid connection failure or squid // purge collisions from elsewhere during failure. Don't keep triggering for // "thumbs" which have the main image URL though (bug 13776) - if ( $wgUseSquid && ($thumb->isError() || $thumb->getUrl() != $this->getURL()) ) { + if ( $wgUseSquid && ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL()) ) { SquidUpdate::purge( array( $thumbUrl ) ); } } while (false); @@ -678,8 +683,9 @@ abstract class File { * @param $limit integer Limit of rows to return * @param $start timestamp Only revisions older than $start will be returned * @param $end timestamp Only revisions newer than $end will be returned + * @param $inc bool Include the endpoints of the time range */ - function getHistory($limit = null, $start = null, $end = null) { + function getHistory($limit = null, $start = null, $end = null, $inc=true) { return array(); } @@ -1212,7 +1218,7 @@ abstract class File { if ( $handler ) { return $handler->getLongDesc( $this ); } else { - return MediaHandler::getLongDesc( $this ); + return MediaHandler::getGeneralLongDesc( $this ); } } @@ -1221,7 +1227,7 @@ abstract class File { if ( $handler ) { return $handler->getShortDesc( $this ); } else { - return MediaHandler::getShortDesc( $this ); + return MediaHandler::getGeneralShortDesc( $this ); } } @@ -1241,7 +1247,7 @@ abstract class File { function getRedirectedTitle() { if ( $this->redirected ) { if ( !$this->redirectTitle ) - $this->redirectTitle = Title::makeTitle( NS_IMAGE, $this->redirected ); + $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected ); return $this->redirectTitle; } } diff --git a/includes/filerepo/FileCache.php b/includes/filerepo/FileCache.php new file mode 100644 index 00000000..7840d1a3 --- /dev/null +++ b/includes/filerepo/FileCache.php @@ -0,0 +1,156 @@ +<?php +/** + * Cache of file objects, wrapping some RepoGroup functions to avoid redundant + * queries. Loosely inspired by the LinkCache / LinkBatch classes for titles. + * + * ISSUE: Merge with RepoGroup? + * + * @ingroup FileRepo + */ +class FileCache { + var $repoGroup; + var $cache = array(), $notFound = array(); + + protected static $instance; + + /** + * Get a FileCache instance. Typically, only one instance of FileCache + * is needed in a MediaWiki invocation. + */ + static function singleton() { + if ( self::$instance ) { + return self::$instance; + } + self::$instance = new FileCache( RepoGroup::singleton() ); + return self::$instance; + } + + /** + * Destroy the singleton instance, so that a new one will be created next + * time singleton() is called. + */ + static function destroySingleton() { + self::$instance = null; + } + + /** + * Set the singleton instance to a given object + */ + static function setSingleton( $instance ) { + self::$instance = $instance; + } + + /** + * Construct a group of file repositories. + * @param RepoGroup $repoGroup + */ + function __construct( $repoGroup ) { + $this->repoGroup = $repoGroup; + } + + + /** + * Add some files to the cache. This is a fairly low-level function, + * which most users should not need to call. Note that any existing + * entries for the same keys will not be replaced. Call clearFiles() + * first if you need that. + * @param array $files array of File objects, indexed by DB key + */ + function addFiles( $files ) { + wfDebug( "FileCache adding ".count( $files )." files\n" ); + $this->cache += $files; + } + + /** + * Remove some files from the cache, so that their existence will be + * rechecked. This is a fairly low-level function, which most users + * should not need to call. + * @param array $remove array indexed by DB keys to remove (the values are ignored) + */ + function clearFiles( $remove ) { + wfDebug( "FileCache clearing data for ".count( $remove )." files\n" ); + $this->cache = array_diff_keys( $this->cache, $remove ); + $this->notFound = array_diff_keys( $this->notFound, $remove ); + } + + /** + * Mark some DB keys as nonexistent. This is a fairly low-level + * function, which most users should not need to call. + * @param array $dbkeys array of DB keys + */ + function markNotFound( $dbkeys ) { + wfDebug( "FileCache marking ".count( $dbkeys )." files as not found\n" ); + $this->notFound += array_fill_keys( $dbkeys, true ); + } + + + /** + * Search the cache for a file. + * @param mixed $title Title object or string + * @return File object or false if it is not found + * @todo Implement searching for old file versions(?) + */ + function findFile( $title ) { + if( !( $title instanceof Title ) ) { + $title = Title::makeTitleSafe( NS_FILE, $title ); + } + if( !$title ) { + return false; // invalid title? + } + + $dbkey = $title->getDBkey(); + if( array_key_exists( $dbkey, $this->cache ) ) { + wfDebug( "FileCache HIT for $dbkey\n" ); + return $this->cache[$dbkey]; + } + if( array_key_exists( $dbkey, $this->notFound ) ) { + wfDebug( "FileCache negative HIT for $dbkey\n" ); + return false; + } + + // Not in cache, fall back to a direct query + $file = $this->repoGroup->findFile( $title ); + if( $file ) { + wfDebug( "FileCache MISS for $dbkey\n" ); + $this->cache[$dbkey] = $file; + } else { + wfDebug( "FileCache negative MISS for $dbkey\n" ); + $this->notFound[$dbkey] = true; + } + return $file; + } + + /** + * Search the cache for multiple files. + * @param array $titles Title objects or strings to search for + * @return array of File objects, indexed by DB key + */ + function findFiles( $titles ) { + $titleObjs = array(); + foreach ( $titles as $title ) { + if ( !( $title instanceof Title ) ) { + $title = Title::makeTitleSafe( NS_FILE, $title ); + } + if ( $title ) { + $titleObjs[$title->getDBkey()] = $title; + } + } + + $result = array_intersect_key( $this->cache, $titleObjs ); + + $unsure = array_diff_key( $titleObjs, $result, $this->notFound ); + if( $unsure ) { + wfDebug( "FileCache MISS for ".count( $unsure )." files out of ".count( $titleObjs )."...\n" ); + // XXX: We assume the array returned by findFiles() is + // indexed by DBkey; this appears to be true, but should + // be explicitly documented. + $found = $this->repoGroup->findFiles( $unsure ); + $result += $found; + $this->addFiles( $found ); + $this->markNotFound( array_keys( array_diff_key( $unsure, $found ) ) ); + } + + wfDebug( "FileCache found ".count( $result )." files out of ".count( $titleObjs )."\n" ); + return $result; + } +} diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index edfc2a99..5beac732 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -15,6 +15,7 @@ abstract class FileRepo { var $thumbScriptUrl, $transformVia404; var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital; var $pathDisclosureProtection = 'paranoid'; + var $descriptionCacheExpiry, $apiThumbCacheExpiry, $hashLevels; /** * Factory functions for creating new files @@ -30,7 +31,8 @@ abstract class FileRepo { // Optional settings $this->initialCapital = true; // by default foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', - 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', 'descriptionCacheExpiry' ) as $var ) + 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', + 'descriptionCacheExpiry', 'apiThumbCacheExpiry', 'hashLevels' ) as $var ) { if ( isset( $info[$var] ) ) { $this->$var = $info[$var]; @@ -57,7 +59,7 @@ abstract class FileRepo { */ function newFile( $title, $time = false ) { if ( !($title instanceof Title) ) { - $title = Title::makeTitleSafe( NS_IMAGE, $title ); + $title = Title::makeTitleSafe( NS_FILE, $title ); if ( !is_object( $title ) ) { return null; } @@ -83,7 +85,7 @@ abstract class FileRepo { */ function findFile( $title, $time = false, $flags = 0 ) { if ( !($title instanceof Title) ) { - $title = Title::makeTitleSafe( NS_IMAGE, $title ); + $title = Title::makeTitleSafe( NS_FILE, $title ); if ( !is_object( $title ) ) { return false; } @@ -99,7 +101,7 @@ abstract class FileRepo { # Now try an old version of the file if ( $time !== false ) { $img = $this->newFile( $title, $time ); - if ( $img->exists() ) { + if ( $img && $img->exists() ) { if ( !$img->isDeleted(File::DELETED_FILE) ) { return $img; } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) { @@ -113,7 +115,7 @@ abstract class FileRepo { return false; } $redir = $this->checkRedirect( $title ); - if( $redir && $redir->getNamespace() == NS_IMAGE) { + if( $redir && $redir->getNamespace() == NS_FILE) { $img = $this->newFile( $redir ); if( !$img ) { return false; @@ -129,12 +131,12 @@ abstract class FileRepo { /* * Find many files at once. * @param array $titles, an array of titles - * @param int $flags + * @todo Think of a good way to optionally pass timestamps to this function. */ - function findFiles( $titles, $flags ) { + function findFiles( $titles ) { $result = array(); foreach ( $titles as $index => $title ) { - $file = $this->findFile( $title, $flags ); + $file = $this->findFile( $title ); if ( $file ) $result[$file->getTitle()->getDBkey()] = $file; } @@ -236,6 +238,14 @@ abstract class FileRepo { return $path; } } + + /** + * Get a relative path including trailing slash, e.g. f/fa/ + * If the repo is not hashed, returns an empty string + */ + function getHashPath( $name ) { + return self::getHashPathForLevel( $name, $this->hashLevels ); + } /** * Get the name of this repository, as specified by $info['name]' to the constructor @@ -245,25 +255,6 @@ abstract class FileRepo { } /** - * Get the file description page base URL, or false if there isn't one. - * @private - */ - function getDescBaseUrl() { - if ( is_null( $this->descBaseUrl ) ) { - if ( !is_null( $this->articleUrl ) ) { - $this->descBaseUrl = str_replace( '$1', - wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl ); - } elseif ( !is_null( $this->scriptDirUrl ) ) { - $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' . - wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) ) . ':'; - } else { - $this->descBaseUrl = false; - } - } - return $this->descBaseUrl; - } - - /** * Get the URL of an image description page. May return false if it is * unknown or not applicable. In general this should only be called by the * File class, since it may return invalid results for certain kinds of @@ -273,12 +264,29 @@ abstract class FileRepo { * constructor, whereas local repositories use the local Title functions. */ function getDescriptionUrl( $name ) { - $base = $this->getDescBaseUrl(); - if ( $base ) { - return $base . wfUrlencode( $name ); - } else { - return false; + $encName = wfUrlencode( $name ); + if ( !is_null( $this->descBaseUrl ) ) { + # "http://example.com/wiki/Image:" + return $this->descBaseUrl . $encName; + } + if ( !is_null( $this->articleUrl ) ) { + # "http://example.com/wiki/$1" + # + # We use "Image:" as the canonical namespace for + # compatibility across all MediaWiki versions. + return str_replace( '$1', + "Image:$encName", $this->articleUrl ); } + if ( !is_null( $this->scriptDirUrl ) ) { + # "http://example.com/w" + # + # We use "Image:" as the canonical namespace for + # compatibility across all MediaWiki versions, + # and just sort of hope index.php is right. ;) + return $this->scriptDirUrl . + "/index.php?title=Image:$encName"; + } + return false; } /** @@ -290,12 +298,12 @@ abstract class FileRepo { function getDescriptionRenderUrl( $name ) { if ( isset( $this->scriptDirUrl ) ) { return $this->scriptDirUrl . '/index.php?title=' . - wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) . + wfUrlencode( 'Image:' . $name ) . '&action=render'; } else { - $descBase = $this->getDescBaseUrl(); - if ( $descBase ) { - return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' ); + $descUrl = $this->getDescriptionUrl( $name ); + if ( $descUrl ) { + return wfAppendQuery( $descUrl, 'action=render' ); } else { return false; } diff --git a/includes/filerepo/ForeignAPIFile.php b/includes/filerepo/ForeignAPIFile.php index aaf92204..d9fb85d0 100644 --- a/includes/filerepo/ForeignAPIFile.php +++ b/includes/filerepo/ForeignAPIFile.php @@ -7,15 +7,19 @@ * @ingroup FileRepo */ class ForeignAPIFile extends File { - function __construct( $title, $repo, $info ) { + + private $mExists; + + function __construct( $title, $repo, $info, $exists = false ) { parent::__construct( $title, $repo ); $this->mInfo = $info; + $this->mExists = $exists; } static function newFromTitle( $title, $repo ) { $info = $repo->getImageInfo( $title ); if( $info ) { - return new ForeignAPIFile( $title, $repo, $info ); + return new ForeignAPIFile( $title, $repo, $info, true ); } else { return null; } @@ -23,7 +27,7 @@ class ForeignAPIFile extends File { // Dummy functions... public function exists() { - return true; + return $this->mExists; } public function getPath() { @@ -31,12 +35,15 @@ class ForeignAPIFile extends File { } function transform( $params, $flags = 0 ) { - $thumbUrl = $this->repo->getThumbUrl( - $this->getName(), - isset( $params['width'] ) ? $params['width'] : -1, - isset( $params['height'] ) ? $params['height'] : -1 ); + if( !$this->canRender() ) { + // show icon + return parent::transform( $params, $flags ); + } + $thumbUrl = $this->repo->getThumbUrlFromCache( + $this->getName(), + isset( $params['width'] ) ? $params['width'] : -1, + isset( $params['height'] ) ? $params['height'] : -1 ); if( $thumbUrl ) { - wfDebug( __METHOD__ . " got remote thumb $thumbUrl\n" ); return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );; } return false; @@ -98,4 +105,64 @@ class ForeignAPIFile extends File { ? $this->mInfo['descriptionurl'] : false; } + + /** + * Only useful if we're locally caching thumbs anyway... + */ + function getThumbPath( $suffix = '' ) { + if ( $this->repo->canCacheThumbs() ) { + global $wgUploadDirectory; + $path = $wgUploadDirectory . '/thumb/' . $this->getHashPath( $this->getName() ); + if ( $suffix ) { + $path = $path . $suffix . '/'; + } + return $path; + } + else { + return null; + } + } + + function getThumbnails() { + $files = array(); + $dir = $this->getThumbPath( $this->getName() ); + if ( is_dir( $dir ) ) { + $handle = opendir( $dir ); + if ( $handle ) { + while ( false !== ( $file = readdir($handle) ) ) { + if ( $file{0} != '.' ) { + $files[] = $file; + } + } + closedir( $handle ); + } + } + return $files; + } + + function purgeCache() { + $this->purgeThumbnails(); + $this->purgeDescriptionPage(); + } + + function purgeDescriptionPage() { + global $wgMemc; + $url = $this->repo->getDescriptionRenderUrl( $this->getName() ); + $key = wfMemcKey( 'RemoteFileDescription', 'url', md5($url) ); + $wgMemc->delete( $key ); + } + + function purgeThumbnails() { + global $wgMemc; + $key = wfMemcKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() ); + $wgMemc->delete( $key ); + $files = $this->getThumbnails(); + $dir = $this->getThumbPath( $this->getName() ); + foreach ( $files as $file ) { + unlink( $dir . $file ); + } + if ( is_dir( $dir ) ) { + rmdir( $dir ); // Might have already gone away, spews errors if we don't. + } + } } diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index 0dee699f..6fc9c465 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -19,6 +19,7 @@ */ class ForeignAPIRepo extends FileRepo { var $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' ); + var $apiThumbCacheExpiry = 0; protected $mQueryCache = array(); function __construct( $info ) { @@ -30,10 +31,12 @@ class ForeignAPIRepo extends FileRepo { } } +/** + * No-ops + */ function storeBatch( $triplets, $flags = 0 ) { return false; } - function storeTemp( $originalName, $srcPath ) { return false; } @@ -69,14 +72,16 @@ class ForeignAPIRepo extends FileRepo { array_merge( $query, array( 'format' => 'json', - 'action' => 'query', - 'prop' => 'imageinfo' ) ) ); + 'action' => 'query' ) ) ); if( !isset( $this->mQueryCache[$url] ) ) { - $key = wfMemcKey( 'ForeignAPIRepo', $url ); + $key = wfMemcKey( 'ForeignAPIRepo', 'Metadata', md5( $url ) ); $data = $wgMemc->get( $key ); if( !$data ) { $data = Http::get( $url ); + if ( !$data ) { + return null; + } $wgMemc->set( $key, $data, 3600 ); } @@ -92,7 +97,22 @@ class ForeignAPIRepo extends FileRepo { function getImageInfo( $title, $time = false ) { return $this->queryImage( array( 'titles' => 'Image:' . $title->getText(), - 'iiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime' ) ); + 'iiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime', + 'prop' => 'imageinfo' ) ); + } + + function findBySha1( $hash ) { + $results = $this->fetchImageQuery( array( + 'aisha1base36' => $hash, + 'aiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime', + 'list' => 'allimages', ) ); + $ret = array(); + if ( isset( $results['query']['allimages'] ) ) { + foreach ( $results['query']['allimages'] as $img ) { + $ret[] = new ForeignAPIFile( Title::makeTitle( NS_IMAGE, $img['name'] ), $this, $img ); + } + } + return $ret; } function getThumbUrl( $name, $width=-1, $height=-1 ) { @@ -100,11 +120,56 @@ class ForeignAPIRepo extends FileRepo { 'titles' => 'Image:' . $name, 'iiprop' => 'url', 'iiurlwidth' => $width, - 'iiurlheight' => $height ) ); + 'iiurlheight' => $height, + 'prop' => 'imageinfo' ) ); if( $info ) { + wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] . "\n" ); return $info['thumburl']; } else { return false; } } + + function getThumbUrlFromCache( $name, $width, $height ) { + global $wgMemc, $wgUploadPath, $wgServer, $wgUploadDirectory; + + if ( !$this->canCacheThumbs() ) { + return $this->getThumbUrl( $name, $width, $height ); + } + + $key = wfMemcKey( 'ForeignAPIRepo', 'ThumbUrl', $name ); + if ( $thumbUrl = $wgMemc->get($key) ) { + wfDebug("Got thumb from local cache. $thumbUrl \n"); + return $thumbUrl; + } + else { + $foreignUrl = $this->getThumbUrl( $name, $width, $height ); + + // We need the same filename as the remote one :) + $fileName = ltrim( substr( $foreignUrl, strrpos( $foreignUrl, '/' ) ), '/' ); + $path = 'thumb/' . $this->getHashPath( $name ) . $name . "/"; + if ( !is_dir($wgUploadDirectory . '/' . $path) ) { + wfMkdirParents($wgUploadDirectory . '/' . $path); + } + if ( !is_writable( $wgUploadDirectory . '/' . $path . $fileName ) ) { + wfDebug( __METHOD__ . " could not write to thumb path\n" ); + return $foreignUrl; + } + $localUrl = $wgServer . $wgUploadPath . '/' . $path . $fileName; + $thumb = Http::get( $foreignUrl ); + # FIXME: Delete old thumbs that aren't being used. Maintenance script? + file_put_contents($wgUploadDirectory . '/' . $path . $fileName, $thumb ); + $wgMemc->set( $key, $localUrl, $this->apiThumbCacheExpiry ); + wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" ); + return $localUrl; + } + } + + /** + * Are we locally caching the thumbnails? + * @return bool + */ + public function canCacheThumbs() { + return ( $this->apiThumbCacheExpiry > 0 ); + } } diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php index eed26048..5fb432c8 100644 --- a/includes/filerepo/ForeignDBFile.php +++ b/includes/filerepo/ForeignDBFile.php @@ -13,7 +13,7 @@ class ForeignDBFile extends LocalFile { * Do not call this except from inside a repo class. */ static function newFromRow( $row, $repo ) { - $title = Title::makeTitle( NS_IMAGE, $row->img_name ); + $title = Title::makeTitle( NS_FILE, $row->img_name ); $file = new self( $title, $repo ); $file->loadFromRow( $row ); return $file; diff --git a/includes/filerepo/Image.php b/includes/filerepo/Image.php index 665dd4bf..5207bb4b 100644 --- a/includes/filerepo/Image.php +++ b/includes/filerepo/Image.php @@ -36,7 +36,7 @@ class Image extends LocalFile { */ static function newFromName( $name ) { wfDeprecated( __METHOD__ ); - $title = Title::makeTitleSafe( NS_IMAGE, $name ); + $title = Title::makeTitleSafe( NS_FILE, $name ); if ( is_object( $title ) ) { $img = wfFindFile( $title ); if ( !$img ) { diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php index 57c0703d..6fd6de72 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -68,7 +68,7 @@ class LocalFile extends File * Do not call this except from inside a repo class. */ static function newFromRow( $row, $repo ) { - $title = Title::makeTitle( NS_IMAGE, $row->img_name ); + $title = Title::makeTitle( NS_FILE, $row->img_name ); $file = new self( $title, $repo ); $file->loadFromRow( $row ); return $file; @@ -453,6 +453,11 @@ class LocalFile extends File return $this->metadata; } + function getBitDepth() { + $this->load(); + return $this->bits; + } + /** * Return the size of the image file, in bytes * @public @@ -619,31 +624,38 @@ class LocalFile extends File /** purgeDescription inherited */ /** purgeEverything inherited */ - function getHistory($limit = null, $start = null, $end = null) { + function getHistory($limit = null, $start = null, $end = null, $inc = true) { $dbr = $this->repo->getSlaveDB(); $tables = array('oldimage'); - $join_conds = array(); $fields = OldLocalFile::selectFields(); - $conds = $opts = array(); + $conds = $opts = $join_conds = array(); + $eq = $inc ? "=" : ""; $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() ); - if( $start !== null ) { - $conds[] = "oi_timestamp <= " . $dbr->addQuotes( $dbr->timestamp( $start ) ); + if( $start ) { + $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) ); } - if( $end !== null ) { - $conds[] = "oi_timestamp >= " . $dbr->addQuotes( $dbr->timestamp( $end ) ); + if( $end ) { + $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) ); } if( $limit ) { $opts['LIMIT'] = $limit; } - $opts['ORDER BY'] = 'oi_timestamp DESC'; + // Search backwards for time > x queries + $order = (!$start && $end !== null) ? "ASC" : "DESC"; + $opts['ORDER BY'] = "oi_timestamp $order"; + $opts['USE INDEX'] = array('oldimage' => 'oi_name_timestamp'); - wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields, &$conds, &$opts, &$join_conds ) ); + wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields, + &$conds, &$opts, &$join_conds ) ); $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds ); $r = array(); while( $row = $dbr->fetchObject($res) ) { $r[] = OldLocalFile::newFromRow($row, $this->repo); } + if( $order == "ASC" ) { + $r = array_reverse( $r ); // make sure it ends up descending + } return $r; } @@ -732,11 +744,11 @@ class LocalFile extends File * @return FileRepoStatus object. On success, the value member contains the * archive name, or an empty string if it was a new file. */ - function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) { + function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) { $this->lock(); $status = $this->publish( $srcPath, $flags ); if ( $status->ok ) { - if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) { + if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) { $status->fatal( 'filenotfound', $srcPath ); } } @@ -766,18 +778,22 @@ class LocalFile extends File /** * Record a file upload in the upload log and the image table */ - function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false ) + function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null ) { - global $wgUser; + if( is_null( $user ) ) { + global $wgUser; + $user = $wgUser; + } $dbw = $this->repo->getMasterDB(); + $dbw->begin(); if ( !$props ) { $props = $this->repo->getFileProps( $this->getVirtualUrl() ); } $props['description'] = $comment; - $props['user'] = $wgUser->getId(); - $props['user_text'] = $wgUser->getName(); + $props['user'] = $user->getId(); + $props['user_text'] = $user->getName(); $props['timestamp'] = wfTimestamp( TS_MW ); $this->setProps( $props ); @@ -812,8 +828,8 @@ class LocalFile extends File 'img_minor_mime' => $this->minor_mime, 'img_timestamp' => $timestamp, 'img_description' => $comment, - 'img_user' => $wgUser->getId(), - 'img_user_text' => $wgUser->getName(), + 'img_user' => $user->getId(), + 'img_user_text' => $user->getName(), 'img_metadata' => $this->metadata, 'img_sha1' => $this->sha1 ), @@ -858,8 +874,8 @@ class LocalFile extends File 'img_minor_mime' => $this->minor_mime, 'img_timestamp' => $timestamp, 'img_description' => $comment, - 'img_user' => $wgUser->getId(), - 'img_user_text' => $wgUser->getName(), + 'img_user' => $user->getId(), + 'img_user_text' => $user->getName(), 'img_metadata' => $this->metadata, 'img_sha1' => $this->sha1 ), array( /* WHERE */ @@ -874,19 +890,22 @@ class LocalFile extends File } $descTitle = $this->getTitle(); - $article = new Article( $descTitle ); + $article = new ImagePage( $descTitle ); + $article->setFile( $this ); # Add the log entry $log = new LogPage( 'upload' ); $action = $reupload ? 'overwrite' : 'upload'; - $log->addEntry( $action, $descTitle, $comment ); + $log->addEntry( $action, $descTitle, $comment, array(), $user ); if( $descTitle->exists() ) { # Create a null revision - $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false ); + $latest = $descTitle->getLatestRevID(); + $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), + $log->getRcComment(), false ); $nullRevision->insertOn( $dbw ); - wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) ); + wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $user) ); $article->updateRevisionOn( $dbw, $nullRevision ); # Invalidate the cache for the description page @@ -1109,8 +1128,8 @@ class LocalFile extends File if ( !$revision ) return false; $text = $revision->getText(); if ( !$text ) return false; - $html = $wgParser->parse( $text, new ParserOptions ); - return $html; + $pout = $wgParser->parse( $text, $this->title, new ParserOptions() ); + return $pout->getText(); } function getDescription() { @@ -1128,7 +1147,7 @@ class LocalFile extends File // Initialise now if necessary if ( $this->sha1 == '' && $this->fileExists ) { $this->sha1 = File::sha1Base36( $this->getPath() ); - if ( strval( $this->sha1 ) != '' ) { + if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) { $dbw = $this->repo->getMasterDB(); $dbw->update( 'image', array( 'img_sha1' => $this->sha1 ), @@ -1355,7 +1374,7 @@ class LocalFileDeleteBatch { $dbw->delete( 'oldimage', array( 'oi_name' => $this->file->getName(), - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' + 'oi_archive_name' => array_keys( $oldRels ) ), __METHOD__ ); } if ( $deleteCurrent ) { @@ -1509,7 +1528,8 @@ class LocalFileRestoreBatch { $result = $dbw->select( 'filearchive', '*', $conditions, __METHOD__, - array( 'ORDER BY' => 'fa_timestamp DESC' ) ); + array( 'ORDER BY' => 'fa_timestamp DESC' ) + ); $idsPresent = array(); $storeBatch = array(); @@ -1554,15 +1574,11 @@ class LocalFileRestoreBatch { 'minor_mime' => $row->fa_minor_mime, 'major_mime' => $row->fa_major_mime, 'media_type' => $row->fa_media_type, - 'metadata' => $row->fa_metadata ); + 'metadata' => $row->fa_metadata + ); } if ( $first && !$exists ) { - // The live (current) version cannot be hidden! - if( !$this->unsuppress && $row->fa_deleted ) { - $this->file->unlock(); - return $status; - } // This revision will be published as the new current version $destRel = $this->file->getRel(); $insertCurrent = array( @@ -1579,7 +1595,13 @@ class LocalFileRestoreBatch { 'img_user' => $row->fa_user, 'img_user_text' => $row->fa_user_text, 'img_timestamp' => $row->fa_timestamp, - 'img_sha1' => $sha1); + 'img_sha1' => $sha1 + ); + // The live (current) version cannot be hidden! + if( !$this->unsuppress && $row->fa_deleted ) { + $storeBatch[] = array( $deletedUrl, 'public', $destRel ); + $this->cleanupBatch[] = $row->fa_storage_key; + } } else { $archiveName = $row->fa_archive_name; if( $archiveName == '' ) { @@ -1616,6 +1638,7 @@ class LocalFileRestoreBatch { $deleteIds[] = $row->fa_id; if( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) { // private files can stay where they are + $status->successCount++; } else { $storeBatch[] = array( $deletedUrl, 'public', $destRel ); $this->cleanupBatch[] = $row->fa_storage_key; @@ -1705,7 +1728,7 @@ class LocalFileMoveBatch { $this->file = $file; $this->target = $target; $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() ); - $this->newHash = $this->file->repo->getHashPath( $this->target->getDbKey() ); + $this->newHash = $this->file->repo->getHashPath( $this->target->getDBKey() ); $this->oldName = $this->file->getName(); $this->newName = $this->file->repo->getNameFromTitle( $this->target ); $this->oldRel = $this->oldHash . $this->oldName; @@ -1751,7 +1774,7 @@ class LocalFileMoveBatch { continue; } $this->olds[] = array( - "{$archiveBase}/{$this->oldHash}{$oldname}", + "{$archiveBase}/{$this->oldHash}{$oldName}", "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}" ); } diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 90b198c8..5eb1a11c 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -94,7 +94,7 @@ class LocalRepo extends FSRepo { 'page_id', //Field array( //Conditions 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbKey(), + 'page_title' => $title->getDBKey(), ), __METHOD__ //Function name ); @@ -108,7 +108,7 @@ class LocalRepo extends FSRepo { $title = Title::newFromTitle( $title ); } if( $title instanceof Title && $title->getNamespace() == NS_MEDIA ) { - $title = Title::makeTitle( NS_IMAGE, $title->getText() ); + $title = Title::makeTitle( NS_FILE, $title->getText() ); } $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) ); @@ -164,8 +164,7 @@ class LocalRepo extends FSRepo { /* * Find many files using one query */ - function findFiles( $titles, $flags ) { - // FIXME: Comply with $flags + function findFiles( $titles ) { // FIXME: Only accepts a $titles array where the keys are the sanitized // file names. diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php index 89e49c4c..46c35bd9 100644 --- a/includes/filerepo/OldLocalFile.php +++ b/includes/filerepo/OldLocalFile.php @@ -23,7 +23,7 @@ class OldLocalFile extends LocalFile { } static function newFromRow( $row, $repo ) { - $title = Title::makeTitle( NS_IMAGE, $row->oi_name ); + $title = Title::makeTitle( NS_FILE, $row->oi_name ); $file = new self( $title, $repo, null, $row->oi_archive_name ); $file->loadFromRow( $row, 'oi_' ); return $file; @@ -64,6 +64,7 @@ class OldLocalFile extends LocalFile { 'oi_user', 'oi_user_text', 'oi_timestamp', + 'oi_deleted', 'oi_sha1', ); } diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index 7cb837b3..2303f581 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -82,7 +82,7 @@ class RepoGroup { } return false; } - function findFiles( $titles, $flags = 0 ) { + function findFiles( $titles ) { if ( !$this->reposInitialised ) { $this->initialiseRepos(); } @@ -90,11 +90,12 @@ class RepoGroup { $titleObjs = array(); foreach ( $titles as $title ) { if ( !( $title instanceof Title ) ) - $title = Title::makeTitleSafe( NS_IMAGE, $title ); - $titleObjs[$title->getDBkey()] = $title; + $title = Title::makeTitleSafe( NS_FILE, $title ); + if ( $title ) + $titleObjs[$title->getDBkey()] = $title; } - $images = $this->localRepo->findFiles( $titleObjs, $flags ); + $images = $this->localRepo->findFiles( $titleObjs ); foreach ( $this->foreignRepos as $repo ) { // Remove found files from $titleObjs @@ -102,7 +103,7 @@ class RepoGroup { if ( isset( $titleObjs[$name] ) ) unset( $titleObjs[$name] ); - $images = array_merge( $images, $repo->findFiles( $titleObjs, $flags ) ); + $images = array_merge( $images, $repo->findFiles( $titleObjs ) ); } return $images; } @@ -176,6 +177,13 @@ class RepoGroup { return $this->getRepo( 'local' ); } + /** + * Call a function for each foreign repo, with the repo object as the + * first parameter. + * + * @param $callback callback The function to call + * @param $params array Optional additional parameters to pass to the function + */ function forEachForeignRepo( $callback, $params = array() ) { foreach( $this->foreignRepos as $repo ) { $args = array_merge( array( $repo ), $params ); @@ -186,8 +194,12 @@ class RepoGroup { return false; } + /** + * Does the installation have any foreign repos set up? + * @return bool + */ function hasForeignRepos() { - return !empty( $this->foreignRepos ); + return (bool)$this->foreignRepos; } /** diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php index c687ef6e..6f63cb0b 100644 --- a/includes/filerepo/UnregisteredLocalFile.php +++ b/includes/filerepo/UnregisteredLocalFile.php @@ -32,7 +32,7 @@ class UnregisteredLocalFile extends File { $this->name = $repo->getNameFromTitle( $title ); } else { $this->name = basename( $path ); - $this->title = Title::makeTitleSafe( NS_IMAGE, $this->name ); + $this->title = Title::makeTitleSafe( NS_FILE, $this->name ); } $this->repo = $repo; if ( $path ) { diff --git a/includes/media/BMP.php b/includes/media/BMP.php index ce1b0362..39b29744 100644 --- a/includes/media/BMP.php +++ b/includes/media/BMP.php @@ -11,6 +11,15 @@ * @ingroup Media */ class BmpHandler extends BitmapHandler { + // We never want to use .bmp in an <img/> tag + function mustRender( $file ) { + return true; + } + + // Render files as PNG + function getThumbType( $text, $mime ) { + return array( 'png', 'image/png' ); + } /* * Get width and height from the bmp header. diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index e01386e9..b949ae3d 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -41,9 +41,10 @@ class BitmapHandler extends ImageHandler { } function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { - global $wgUseImageMagick, $wgImageMagickConvertCommand; + global $wgUseImageMagick, $wgImageMagickConvertCommand, $wgImageMagickTempDir; global $wgCustomConvertCommand; global $wgSharpenParameter, $wgSharpenReductionThreshold; + global $wgMaxAnimatedGifArea; if ( !$this->normaliseParams( $image, $params ) ) { return new TransformParameterError( $params ); @@ -59,7 +60,7 @@ class BitmapHandler extends ImageHandler { $retval = 0; wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" ); - if ( $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) { + if ( !$image->mustRender() && $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) { # normaliseParams (or the user) wants us to return the unscaled image wfDebug( __METHOD__.": returning unscaled image\n" ); return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); @@ -77,6 +78,7 @@ class BitmapHandler extends ImageHandler { } else { $scaler = 'client'; } + wfDebug( __METHOD__.": scaler $scaler\n" ); if ( $scaler == 'client' ) { # Client-side image scaling, use the source URL @@ -85,18 +87,22 @@ class BitmapHandler extends ImageHandler { } if ( $flags & self::TRANSFORM_LATER ) { + wfDebug( __METHOD__.": Transforming later per flags.\n" ); return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); } if ( !wfMkdirParents( dirname( $dstPath ) ) ) { - wfDebug( "Unable to create thumbnail destination directory, falling back to client scaling\n" ); + wfDebug( __METHOD__.": Unable to create thumbnail destination directory, falling back to client scaling\n" ); return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); } if ( $scaler == 'im' ) { # use ImageMagick + $quality = ''; $sharpen = ''; + $frame = ''; + $animation = ''; if ( $mimeType == 'image/jpeg' ) { $quality = "-quality 80"; // 80% # Sharpening, see bug 6193 @@ -105,8 +111,21 @@ class BitmapHandler extends ImageHandler { } } elseif ( $mimeType == 'image/png' ) { $quality = "-quality 95"; // zlib 9, adaptive filtering + } elseif( $mimeType == 'image/gif' ) { + if( $srcWidth * $srcHeight > $wgMaxAnimatedGifArea ) { + // Extract initial frame only; we're so big it'll + // be a total drag. :P + $frame = '[0]'; + } else { + // Coalesce is needed to scale animated GIFs properly (bug 1017). + $animation = ' -coalesce '; + } + } + + if ( strval( $wgImageMagickTempDir ) !== '' ) { + $tempEnv = 'MAGICK_TMPDIR=' . wfEscapeShellArg( $wgImageMagickTempDir ) . ' '; } else { - $quality = ''; // default + $tempEnv = ''; } # Specify white background color, will be used for transparent images @@ -116,11 +135,12 @@ class BitmapHandler extends ImageHandler { # It seems that ImageMagick has a bug wherein it produces thumbnails of # the wrong size in the second case. - $cmd = wfEscapeShellArg($wgImageMagickConvertCommand) . + $cmd = + $tempEnv . + wfEscapeShellArg($wgImageMagickConvertCommand) . " {$quality} -background white -size {$physicalWidth} ". - wfEscapeShellArg($srcPath) . - // Coalesce is needed to scale animated GIFs properly (bug 1017). - ' -coalesce ' . + wfEscapeShellArg($srcPath . $frame) . + $animation . // For the -resize option a "!" is needed to force exact size, // or ImageMagick may decide your ratio is wrong and slice off // a pixel. diff --git a/includes/media/Bitmap_ClientOnly.php b/includes/media/Bitmap_ClientOnly.php new file mode 100644 index 00000000..9801f9be --- /dev/null +++ b/includes/media/Bitmap_ClientOnly.php @@ -0,0 +1,15 @@ +<?php + +class BitmapHandler_ClientOnly extends BitmapHandler { + function normaliseParams( $image, &$params ) { + return ImageHandler::normaliseParams( $image, $params ); + } + + function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { + if ( !$this->normaliseParams( $image, $params ) ) { + return new TransformParameterError( $params ); + } + return new ThumbnailImage( $image, $image->getURL(), $params['width'], + $params['height'], $image->getPath() ); + } +} diff --git a/includes/media/Generic.php b/includes/media/Generic.php index b2cb70f6..a9c681e1 100644 --- a/includes/media/Generic.php +++ b/includes/media/Generic.php @@ -239,6 +239,21 @@ abstract class MediaHandler { $sk->formatSize( $file->getSize() ), $file->getMimeType() ); } + + static function getGeneralShortDesc( $file ) { + global $wgLang; + $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $file->getSize() ) ) . ')'; + return "$nbytes"; + } + + static function getGeneralLongDesc( $file ) { + global $wgUser; + $sk = $wgUser->getSkin(); + return wfMsgExt( 'file-info', 'parseinline', + $sk->formatSize( $file->getSize() ), + $file->getMimeType() ); + } function getDimensionsString( $file ) { return ''; diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 2604e3b4..f0519e89 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -27,7 +27,6 @@ class SvgHandler extends ImageHandler { if ( !parent::normaliseParams( $image, $params ) ) { return false; } - # Don't make an image bigger than wgMaxSVGSize $params['physicalWidth'] = $params['width']; $params['physicalHeight'] = $params['height']; @@ -60,32 +59,49 @@ class SvgHandler extends ImageHandler { return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, wfMsg( 'thumbnail_dest_directory' ) ); } - + + $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight ); + if( $status === true ) { + return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); + } else { + return $status; // MediaTransformError + } + } + + /* + * Transform an SVG file to PNG + * This function can be called outside of thumbnail contexts + * @param string $srcPath + * @param string $dstPath + * @param string $width + * @param string $height + * @returns TRUE/MediaTransformError + */ + public function rasterize( $srcPath, $dstPath, $width, $height ) { + global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath; $err = false; - if( isset( $wgSVGConverters[$wgSVGConverter] ) ) { + if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) { $cmd = str_replace( array( '$path/', '$width', '$height', '$input', '$output' ), array( $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "", - intval( $physicalWidth ), - intval( $physicalHeight ), + intval( $width ), + intval( $height ), wfEscapeShellArg( $srcPath ), wfEscapeShellArg( $dstPath ) ), - $wgSVGConverters[$wgSVGConverter] ) . " 2>&1"; + $wgSVGConverters[$wgSVGConverter] + ) . " 2>&1"; wfProfileIn( 'rsvg' ); wfDebug( __METHOD__.": $cmd\n" ); $err = wfShellExec( $cmd, $retval ); wfProfileOut( 'rsvg' ); } - $removed = $this->removeBadFile( $dstPath, $retval ); if ( $retval != 0 || $removed ) { - wfDebugLog( 'thumbnail', - sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', + wfDebugLog( 'thumbnail', sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', wfHostname(), $retval, trim($err), $cmd ) ); - return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); - } else { - return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); + return new MediaTransformError( 'thumbnail_error', $width, $height, $err ); } + return true; } function getImageSize( $image, $path ) { diff --git a/includes/memcached-client.php b/includes/memcached-client.php index 6bd18387..79745309 100644 --- a/includes/memcached-client.php +++ b/includes/memcached-client.php @@ -454,7 +454,7 @@ class memcached if (!$this->_active) return false; - $this->stats['get_multi']++; + @$this->stats['get_multi']++; $sock_keys = array(); foreach ($keys as $key) @@ -800,8 +800,8 @@ class memcached if (is_resource($sock)) { $this->_flush_read_buffer($sock); return $sock; - } - $hv += $this->_hashfunc($tries . $realkey); + } + $hv = $this->_hashfunc( $hv . $realkey ); } return false; diff --git a/includes/mime.types b/includes/mime.types index 6021e926..2b8cb9ab 100644 --- a/includes/mime.types +++ b/includes/mime.types @@ -59,6 +59,7 @@ application/xslt+xml xslt application/xml xml xsl xsd application/xml-dtd dtd application/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw +application/x-rar rar audio/basic au snd audio/midi mid midi kar audio/mpeg mpga mp2 mp3 diff --git a/includes/parser/CoreLinkFunctions.php b/includes/parser/CoreLinkFunctions.php new file mode 100644 index 00000000..d6d11880 --- /dev/null +++ b/includes/parser/CoreLinkFunctions.php @@ -0,0 +1,47 @@ +<?php + +/** + * Various core link functions, registered in Parser::firstCallInit() + * @ingroup Parser + */ +class CoreLinkFunctions { + static function register( $parser ) { + $parser->setLinkHook( NS_CATEGORY, array( __CLASS__, 'categoryLinkHook' ) ); + return true; + } + + static function defaultLinkHook( $parser, $holders, $markers, + Title $title, $titleText, &$displayText = null, &$leadingColon = false ) { + if( isset($displayText) && $markers->findMarker( $displayText ) ) { + # There are links inside of the displayText + # For backwards compatibility the deepest links are dominant so this + # link should not be handled + $displayText = $markers->expand($displayText); + # Return false so that this link is reverted back to WikiText + return false; + } + return $holders->makeHolder( $title, isset($displayText) ? $displayText : $titleText, '', '', '' ); + } + + static function categoryLinkHook( $parser, $holders, $markers, + Title $title, $titleText, &$sortText = null, &$leadingColon = false ) { + global $wgContLang; + # When a category link starts with a : treat it as a normal link + if( $leadingColon ) return true; + if( isset($sortText) && $markers->findMarker( $sortText ) ) { + # There are links inside of the sortText + # For backwards compatibility the deepest links are dominant so this + # link should not be handled + $sortText = $markers->expand($sortText); + # Return false so that this link is reverted back to WikiText + return false; + } + if( !isset($sortText) ) $sortText = $parser->getDefaultSort(); + $sortText = Sanitizer::decodeCharReferences( $sortText ); + $sortText = str_replace( "\n", '', $sortText ); + $sortText = $wgContLang->convertCategoryKey( $sortText ); + $parser->mOutput->addCategory( $title->getDBkey(), $sortText ); + return ''; + } + +} diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php index d9072e93..a3b5189a 100644 --- a/includes/parser/CoreParserFunctions.php +++ b/includes/parser/CoreParserFunctions.php @@ -33,7 +33,9 @@ class CoreParserFunctions { $parser->setFunctionHook( 'numberofarticles', array( __CLASS__, 'numberofarticles' ), SFH_NO_HASH ); $parser->setFunctionHook( 'numberoffiles', array( __CLASS__, 'numberoffiles' ), SFH_NO_HASH ); $parser->setFunctionHook( 'numberofadmins', array( __CLASS__, 'numberofadmins' ), SFH_NO_HASH ); + $parser->setFunctionHook( 'numberingroup', array( __CLASS__, 'numberingroup' ), SFH_NO_HASH ); $parser->setFunctionHook( 'numberofedits', array( __CLASS__, 'numberofedits' ), SFH_NO_HASH ); + $parser->setFunctionHook( 'numberofviews', array( __CLASS__, 'numberofviews' ), SFH_NO_HASH ); $parser->setFunctionHook( 'language', array( __CLASS__, 'language' ), SFH_NO_HASH ); $parser->setFunctionHook( 'padleft', array( __CLASS__, 'padleft' ), SFH_NO_HASH ); $parser->setFunctionHook( 'padright', array( __CLASS__, 'padright' ), SFH_NO_HASH ); @@ -56,7 +58,10 @@ class CoreParserFunctions { static function intFunction( $parser, $part1 = '' /*, ... */ ) { if ( strval( $part1 ) !== '' ) { $args = array_slice( func_get_args(), 2 ); - return wfMsgReal( $part1, $args, true ); + $message = wfMsgGetKey( $part1, true, false, false ); + $message = wfMsgReplaceArgs( $message, $args ); + $message = $parser->replaceVariables( $message ); // like $wgMessageCache->transform() + return $message; } else { return array( 'found' => false ); } @@ -64,20 +69,13 @@ class CoreParserFunctions { static function ns( $parser, $part1 = '' ) { global $wgContLang; - $found = false; if ( intval( $part1 ) || $part1 == "0" ) { - $text = $wgContLang->getNsText( intval( $part1 ) ); - $found = true; + $index = intval( $part1 ); } else { - $param = str_replace( ' ', '_', strtolower( $part1 ) ); - $index = MWNamespace::getCanonicalIndex( strtolower( $param ) ); - if ( !is_null( $index ) ) { - $text = $wgContLang->getNsText( $index ); - $found = true; - } + $index = $wgContLang->getNsIndex( str_replace( ' ', '_', $part1 ) ); } - if ( $found ) { - return $text; + if ( $index !== false ) { + return $wgContLang->getFormattedNsText( $index ); } else { return array( 'found' => false ); } @@ -128,8 +126,12 @@ class CoreParserFunctions { # 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 ) ) { + if( !is_null( $title ) ) { + # Convert NS_MEDIA -> NS_FILE + if( $title->getNamespace() == NS_MEDIA ) { + $title = Title::makeTitle( NS_FILE, $title->getDBKey() ); + } + if( !is_null( $arg ) ) { $text = $title->$func( $arg ); } else { $text = $title->$func(); @@ -167,10 +169,16 @@ class CoreParserFunctions { * @return string */ static function displaytitle( $parser, $text = '' ) { + global $wgRestrictDisplayTitle; $text = trim( Sanitizer::decodeCharReferences( $text ) ); - $title = Title::newFromText( $text ); - if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) ) + + if ( !$wgRestrictDisplayTitle ) { $parser->mOutput->setDisplayTitle( $text ); + } else { + $title = Title::newFromText( $text ); + if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) ) + $parser->mOutput->setDisplayTitle( $text ); + } return ''; } @@ -207,14 +215,20 @@ class CoreParserFunctions { return self::formatRaw( SiteStats::images(), $raw ); } static function numberofadmins( $parser, $raw = null ) { - return self::formatRaw( SiteStats::admins(), $raw ); + return self::formatRaw( SiteStats::numberingroup('sysop'), $raw ); } static function numberofedits( $parser, $raw = null ) { return self::formatRaw( SiteStats::edits(), $raw ); } + static function numberofviews( $parser, $raw = null ) { + return self::formatRaw( SiteStats::views(), $raw ); + } static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) { return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw ); } + static function numberingroup( $parser, $name = '', $raw = null) { + return self::formatRaw( SiteStats::numberingroup( strtolower( $name ) ), $raw ); + } /** * Return the number of pages in the given category, or 0 if it's nonexis- @@ -269,12 +283,12 @@ class CoreParserFunctions { if( isset( $cache[$page] ) ) { $length = $cache[$page]; } elseif( $parser->incrementExpensiveFunctionCount() ) { - $length = $cache[$page] = $title->getLength(); + $rev = Revision::newFromTitle($title); + $id = $rev ? $rev->getPage() : 0; + $length = $cache[$page] = $rev ? $rev->getSize() : 0; // Register dependency in templatelinks - $id = $title->getArticleId(); - $revid = Revision::newFromTitle($title); - $parser->mOutput->addTemplate($title, $id, $revid); + $parser->mOutput->addTemplate( $title, $id, $rev ? $rev->getId() : 0 ); } return self::formatRaw( $length, $raw ); } @@ -320,9 +334,18 @@ class CoreParserFunctions { public static function defaultsort( $parser, $text ) { $text = trim( $text ); - if( strlen( $text ) > 0 ) - $parser->setDefaultSort( $text ); - return ''; + if( strlen( $text ) == 0 ) + return ''; + $old = $parser->getCustomDefaultSort(); + $parser->setDefaultSort( $text ); + if( $old === false || $old == $text ) + return ''; + else + return( '<span class="error">' . + wfMsg( 'duplicate-defaultsort', + htmlspecialchars( $old ), + htmlspecialchars( $text ) ) . + '</span>' ); } public static function filepath( $parser, $name='', $option='' ) { @@ -330,7 +353,7 @@ class CoreParserFunctions { if( $file ) { $url = $file->getFullUrl(); if( $option == 'nowiki' ) { - return "<nowiki>$url</nowiki>"; + return array( $url, 'nowiki' => true ); } return $url; } else { @@ -365,7 +388,7 @@ class CoreParserFunctions { foreach ( $args as $arg ) { $bits = $arg->splitArg(); if ( strval( $bits['index'] ) === '' ) { - $name = $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ); + $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); $value = trim( $frame->expand( $bits['value'] ) ); if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) { $value = isset( $m[1] ) ? $m[1] : ''; diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php new file mode 100644 index 00000000..35b672b9 --- /dev/null +++ b/includes/parser/LinkHolderArray.php @@ -0,0 +1,438 @@ +<?php + +class LinkHolderArray { + var $internals = array(), $interwikis = array(); + var $size = 0; + var $parent; + + function __construct( $parent ) { + $this->parent = $parent; + } + + /** + * Reduce memory usage to reduce the impact of circular references + */ + function __destruct() { + foreach ( $this as $name => $value ) { + unset( $this->$name ); + } + } + + /** + * Merge another LinkHolderArray into this one + */ + function merge( $other ) { + foreach ( $other->internals as $ns => $entries ) { + $this->size += count( $entries ); + if ( !isset( $this->internals[$ns] ) ) { + $this->internals[$ns] = $entries; + } else { + $this->internals[$ns] += $entries; + } + } + $this->interwikis += $other->interwikis; + } + + /** + * Returns true if the memory requirements of this object are getting large + */ + function isBig() { + global $wgLinkHolderBatchSize; + return $this->size > $wgLinkHolderBatchSize; + } + + /** + * Clear all stored link holders. + * Make sure you don't have any text left using these link holders, before you call this + */ + function clear() { + $this->internals = array(); + $this->interwikis = array(); + $this->size = 0; + } + + /** + * Make a link placeholder. The text returned can be later resolved to a real link with + * replaceLinkHolders(). This is done for two reasons: firstly to avoid further + * parsing of interwiki links, and secondly to allow all existence checks and + * article length checks (for stub links) to be bundled into a single query. + * + */ + function makeHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + wfProfileIn( __METHOD__ ); + if ( ! is_object($nt) ) { + # Fail gracefully + $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}"; + } else { + # Separate the link trail from the rest of the link + list( $inside, $trail ) = Linker::splitTrail( $trail ); + + $entry = array( + 'title' => $nt, + 'text' => $prefix.$text.$inside, + 'pdbk' => $nt->getPrefixedDBkey(), + ); + if ( $query !== '' ) { + $entry['query'] = $query; + } + + if ( $nt->isExternal() ) { + // Use a globally unique ID to keep the objects mergable + $key = $this->parent->nextLinkID(); + $this->interwikis[$key] = $entry; + $retVal = "<!--IWLINK $key-->{$trail}"; + } else { + $key = $this->parent->nextLinkID(); + $ns = $nt->getNamespace(); + $this->internals[$ns][$key] = $entry; + $retVal = "<!--LINK $ns:$key-->{$trail}"; + } + $this->size++; + } + wfProfileOut( __METHOD__ ); + return $retVal; + } + + /** + * Get the stub threshold + */ + function getStubThreshold() { + global $wgUser; + if ( !isset( $this->stubThreshold ) ) { + $this->stubThreshold = $wgUser->getOption('stubthreshold'); + } + return $this->stubThreshold; + } + + /** + * Replace <!--LINK--> link placeholders with actual links, in the buffer + * Placeholders created in Skin::makeLinkObj() + * Returns an array of link CSS classes, indexed by PDBK. + */ + function replace( &$text ) { + wfProfileIn( __METHOD__ ); + + $colours = $this->replaceInternal( $text ); + $this->replaceInterwiki( $text ); + + wfProfileOut( __METHOD__ ); + return $colours; + } + + /** + * Replace internal links + */ + protected function replaceInternal( &$text ) { + if ( !$this->internals ) { + return; + } + + wfProfileIn( __METHOD__ ); + global $wgContLang; + + $colours = array(); + $sk = $this->parent->getOptions()->getSkin(); + $linkCache = LinkCache::singleton(); + $output = $this->parent->getOutput(); + + wfProfileIn( __METHOD__.'-check' ); + $dbr = wfGetDB( DB_SLAVE ); + $page = $dbr->tableName( 'page' ); + $threshold = $this->getStubThreshold(); + + # Sort by namespace + ksort( $this->internals ); + + # Generate query + $query = false; + $current = null; + foreach ( $this->internals as $ns => $entries ) { + foreach ( $entries as $index => $entry ) { + $key = "$ns:$index"; + $title = $entry['title']; + $pdbk = $entry['pdbk']; + + # Skip invalid entries. + # Result will be ugly, but prevents crash. + if ( is_null( $title ) ) { + continue; + } + + # Check if it's a static known link, e.g. interwiki + if ( $title->isAlwaysKnown() ) { + $colours[$pdbk] = ''; + } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) { + $colours[$pdbk] = $sk->getLinkColour( $title, $threshold ); + $output->addLink( $title, $id ); + } elseif ( $linkCache->isBadLink( $pdbk ) ) { + $colours[$pdbk] = 'new'; + } else { + # Not in the link cache, add it to the query + if ( !isset( $current ) ) { + $current = $ns; + $query = "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len"; + $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN("; + } elseif ( $current != $ns ) { + $current = $ns; + $query .= ")) OR (page_namespace=$ns AND page_title IN("; + } else { + $query .= ', '; + } + + $query .= $dbr->addQuotes( $title->getDBkey() ); + } + } + } + if ( $query ) { + $query .= '))'; + + $res = $dbr->query( $query, __METHOD__ ); + + # Fetch data and form into an associative array + # non-existent = broken + $linkcolour_ids = array(); + while ( $s = $dbr->fetchObject($res) ) { + $title = Title::makeTitle( $s->page_namespace, $s->page_title ); + $pdbk = $title->getPrefixedDBkey(); + $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect ); + $output->addLink( $title, $s->page_id ); + # FIXME: convoluted data flow + # The redirect status and length is passed to getLinkColour via the LinkCache + # Use formal parameters instead + $colours[$pdbk] = $sk->getLinkColour( $title, $threshold ); + //add id to the extension todolist + $linkcolour_ids[$s->page_id] = $pdbk; + } + unset( $res ); + //pass an array of page_ids to an extension + wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) ); + } + wfProfileOut( __METHOD__.'-check' ); + + # Do a second query for different language variants of links and categories + if($wgContLang->hasVariants()) { + $this->doVariants( $colours ); + } + + # Construct search and replace arrays + wfProfileIn( __METHOD__.'-construct' ); + $replacePairs = array(); + foreach ( $this->internals as $ns => $entries ) { + foreach ( $entries as $index => $entry ) { + $pdbk = $entry['pdbk']; + $title = $entry['title']; + $query = isset( $entry['query'] ) ? $entry['query'] : ''; + $key = "$ns:$index"; + $searchkey = "<!--LINK $key-->"; + if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) { + $linkCache->addBadLinkObj( $title ); + $colours[$pdbk] = 'new'; + $output->addLink( $title, 0 ); + $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title, + $entry['text'], + $query ); + } else { + $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk], + $entry['text'], + $query ); + } + } + } + $replacer = new HashtableReplacer( $replacePairs, 1 ); + wfProfileOut( __METHOD__.'-construct' ); + + # Do the thing + wfProfileIn( __METHOD__.'-replace' ); + $text = preg_replace_callback( + '/(<!--LINK .*?-->)/', + $replacer->cb(), + $text); + + wfProfileOut( __METHOD__.'-replace' ); + wfProfileOut( __METHOD__ ); + } + + /** + * Replace interwiki links + */ + protected function replaceInterwiki( &$text ) { + if ( empty( $this->interwikis ) ) { + return; + } + + wfProfileIn( __METHOD__ ); + # Make interwiki link HTML + $sk = $this->parent->getOptions()->getSkin(); + $replacePairs = array(); + foreach( $this->interwikis as $key => $link ) { + $replacePairs[$key] = $sk->link( $link['title'], $link['text'] ); + } + $replacer = new HashtableReplacer( $replacePairs, 1 ); + + $text = preg_replace_callback( + '/<!--IWLINK (.*?)-->/', + $replacer->cb(), + $text ); + wfProfileOut( __METHOD__ ); + } + + /** + * Modify $this->internals and $colours according to language variant linking rules + */ + protected function doVariants( &$colours ) { + global $wgContLang; + $linkBatch = new LinkBatch(); + $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders) + $output = $this->parent->getOutput(); + $linkCache = LinkCache::singleton(); + $sk = $this->parent->getOptions()->getSkin(); + $threshold = $this->getStubThreshold(); + + // Add variants of links to link batch + foreach ( $this->internals as $ns => $entries ) { + foreach ( $entries as $index => $entry ) { + $key = "$ns:$index"; + $pdbk = $entry['pdbk']; + $title = $entry['title']; + $titleText = $title->getText(); + + // generate all variants of the link title text + $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText); + + // if link was not found (in first query), add all variants to query + if ( !isset($colours[$pdbk]) ){ + foreach($allTextVariants as $textVariant){ + if($textVariant != $titleText){ + $variantTitle = Title::makeTitle( $ns, $textVariant ); + if(is_null($variantTitle)) continue; + $linkBatch->addObj( $variantTitle ); + $variantMap[$variantTitle->getPrefixedDBkey()][] = $key; + } + } + } + } + } + + // process categories, check if a category exists in some variant + $categoryMap = array(); // maps $category_variant => $category (dbkeys) + $varCategories = array(); // category replacements oldDBkey => newDBkey + foreach( $output->getCategoryLinks() as $category ){ + $variants = $wgContLang->convertLinkToAllVariants($category); + foreach($variants as $variant){ + if($variant != $category){ + $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) ); + if(is_null($variantTitle)) continue; + $linkBatch->addObj( $variantTitle ); + $categoryMap[$variant] = $category; + } + } + } + + + if(!$linkBatch->isEmpty()){ + // construct query + $dbr = wfGetDB( DB_SLAVE ); + $page = $dbr->tableName( 'page' ); + $titleClause = $linkBatch->constructSet('page', $dbr); + $variantQuery = "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len"; + $variantQuery .= " FROM $page WHERE $titleClause"; + $varRes = $dbr->query( $variantQuery, __METHOD__ ); + $linkcolour_ids = array(); + + // for each found variants, figure out link holders and replace + while ( $s = $dbr->fetchObject($varRes) ) { + + $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title ); + $varPdbk = $variantTitle->getPrefixedDBkey(); + $vardbk = $variantTitle->getDBkey(); + + $holderKeys = array(); + if(isset($variantMap[$varPdbk])){ + $holderKeys = $variantMap[$varPdbk]; + $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect ); + $output->addLink( $variantTitle, $s->page_id ); + } + + // loop over link holders + foreach($holderKeys as $key){ + list( $ns, $index ) = explode( ':', $key, 2 ); + $entry =& $this->internals[$ns][$index]; + $pdbk = $entry['pdbk']; + + if(!isset($colours[$pdbk])){ + // found link in some of the variants, replace the link holder data + $entry['title'] = $variantTitle; + $entry['pdbk'] = $varPdbk; + + // set pdbk and colour + # FIXME: convoluted data flow + # The redirect status and length is passed to getLinkColour via the LinkCache + # Use formal parameters instead + $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold ); + $linkcolour_ids[$s->page_id] = $pdbk; + } + } + + // check if the object is a variant of a category + if(isset($categoryMap[$vardbk])){ + $oldkey = $categoryMap[$vardbk]; + if($oldkey != $vardbk) + $varCategories[$oldkey]=$vardbk; + } + } + wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) ); + + // rebuild the categories in original order (if there are replacements) + if(count($varCategories)>0){ + $newCats = array(); + $originalCats = $output->getCategories(); + foreach($originalCats as $cat => $sortkey){ + // make the replacement + if( array_key_exists($cat,$varCategories) ) + $newCats[$varCategories[$cat]] = $sortkey; + else $newCats[$cat] = $sortkey; + } + $output->setCategoryLinks($newCats); + } + } + } + + /** + * Replace <!--LINK--> link placeholders with plain text of links + * (not HTML-formatted). + * @param string $text + * @return string + */ + function replaceText( $text ) { + wfProfileIn( __METHOD__ ); + + $text = preg_replace_callback( + '/<!--(LINK|IWLINK) (.*?)-->/', + array( &$this, 'replaceTextCallback' ), + $text ); + + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * @param array $matches + * @return string + * @private + */ + function replaceTextCallback( $matches ) { + $type = $matches[1]; + $key = $matches[2]; + if( $type == 'LINK' ) { + list( $ns, $index ) = explode( ':', $key, 2 ); + if( isset( $this->internals[$ns][$index]['text'] ) ) { + return $this->internals[$ns][$index]['text']; + } + } elseif( $type == 'IWLINK' ) { + if( isset( $this->interwikis[$key]['text'] ) ) { + return $this->interwikis[$key]['text']; + } + } + return $matches[0]; + } +} diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 3ff56a2b..7fcfb90a 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -92,17 +92,18 @@ class Parser # Persistent: var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables, $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor, - $mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf; + $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf; # Cleared with clearState(): var $mOutput, $mAutonumber, $mDTopen, $mStripState; var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; - var $mInterwikiLinkHolders, $mLinkHolders; + var $mLinkHolders, $mLinkID; var $mIncludeSizes, $mPPNodeCount, $mDefaultSort; var $mTplExpandCache; // empty-frame expansion cache var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores; var $mExpensiveFunctionCount; // number of expensive parser function calls + var $mFileCache; # Temporary # These are variables reset at least once per parse regardless of $clearState @@ -128,6 +129,7 @@ class Parser $this->mFunctionHooks = array(); $this->mFunctionSynonyms = array( 0 => array(), 1 => array() ); $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' ); + $this->mUrlProtocols = wfUrlProtocols(); $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'. '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S'; $this->mVarCache = array(); @@ -147,6 +149,18 @@ class Parser } /** + * Reduce memory usage to reduce the impact of circular references + */ + function __destruct() { + if ( isset( $this->mLinkHolders ) ) { + $this->mLinkHolders->__destruct(); + } + foreach ( $this as $name => $value ) { + unset( $this->$name ); + } + } + + /** * Do various kinds of initialisation on the first call of the parser */ function firstCallInit() { @@ -183,17 +197,8 @@ class Parser $this->mStripState = new StripState; $this->mArgStack = false; $this->mInPre = false; - $this->mInterwikiLinkHolders = array( - 'texts' => array(), - 'titles' => array() - ); - $this->mLinkHolders = array( - 'namespaces' => array(), - 'dbkeys' => array(), - 'queries' => array(), - 'texts' => array(), - 'titles' => array() - ); + $this->mLinkHolders = new LinkHolderArray( $this ); + $this->mLinkID = 0; $this->mRevisionTimestamp = $this->mRevisionId = null; /** @@ -208,7 +213,7 @@ class Parser */ #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString(); # Changed to \x7f to allow XML double-parsing -- TS - $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString(); + $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString(); # Clear these on every parse, bug 4549 @@ -225,6 +230,7 @@ class Parser $this->mHeadings = array(); $this->mDoubleUnderscores = array(); $this->mExpensiveFunctionCount = 0; + $this->mFileCache = array(); # Fix cloning if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) { @@ -283,22 +289,22 @@ class Parser * Convert wikitext to HTML * Do not call this function recursively. * - * @param string $text Text we want to parse - * @param Title &$title A title object - * @param array $options - * @param boolean $linestart - * @param boolean $clearState - * @param int $revid number to pass in {{REVISIONID}} + * @param $text String: text we want to parse + * @param $title A title object + * @param $options ParserOptions + * @param $linestart boolean + * @param $clearState boolean + * @param $revid Int: number to pass in {{REVISIONID}} * @return ParserOutput a ParserOutput */ - public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) { + public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) { /** * First pass--just handle <nowiki> sections, pass the rest off * to internalParse() which does all the real work. */ global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang; - $fname = 'Parser::parse-' . wfGetCaller(); + $fname = __METHOD__.'-' . wfGetCaller(); wfProfileIn( __METHOD__ ); wfProfileIn( $fname ); @@ -332,7 +338,6 @@ class Parser ); $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text ); - # only once and last $text = $this->doBlockLevels( $text, $linestart ); $this->replaceLinkHolders( $text ); @@ -352,7 +357,7 @@ class Parser $uniq_prefix = $this->mUniqPrefix; $matches = array(); $elements = array_keys( $this->mTransparentTagHooks ); - $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); + $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); foreach( $matches as $marker => $data ) { list( $element, $content, $params, $tag ) = $data; @@ -370,7 +375,7 @@ class Parser $text = Sanitizer::normalizeCharReferences( $text ); if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) { - $text = Parser::tidy($text); + $text = self::tidy($text); } else { # attempt to sanitize at least some nesting problems # (bug #2702 and quite a few others) @@ -475,6 +480,8 @@ class Parser function &getTitle() { return $this->mTitle; } function getOptions() { return $this->mOptions; } function getRevisionId() { return $this->mRevisionId; } + function getOutput() { return $this->mOutput; } + function nextLinkID() { return $this->mLinkID++; } function getFunctionLang() { global $wgLang, $wgContLang; @@ -553,7 +560,7 @@ class Parser $text = $inside; $tail = null; } else { - if( $element == '!--' ) { + if( $element === '!--' ) { $end = '/(-->)/'; } else { $end = "/(<\\/$element\\s*>)/i"; @@ -658,18 +665,27 @@ class Parser */ function tidy( $text ) { global $wgTidyInternal; + $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'. ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'. '<head><title>test</title></head><body>'.$text.'</body></html>'; + + # Tidy is known to clobber tabs; convert 'em to entities + $wrappedtext = str_replace("\t", '	', $wrappedtext); + if( $wgTidyInternal ) { - $correctedtext = Parser::internalTidy( $wrappedtext ); + $correctedtext = self::internalTidy( $wrappedtext ); } else { - $correctedtext = Parser::externalTidy( $wrappedtext ); + $correctedtext = self::externalTidy( $wrappedtext ); } if( is_null( $correctedtext ) ) { wfDebug( "Tidy error detected!\n" ); return $text . "\n<!-- Tidy found serious XHTML errors -->\n"; } + + # Convert the tabs back from entities + $correctedtext = str_replace('	', "\t", $correctedtext); + return $correctedtext; } @@ -681,8 +697,7 @@ class Parser */ function externalTidy( $text ) { global $wgTidyConf, $wgTidyBin, $wgTidyOpts; - $fname = 'Parser::externalTidy'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $cleansource = ''; $opts = ' -utf8'; @@ -693,23 +708,25 @@ class Parser 2 => array('file', wfGetNull(), 'a') ); $pipes = array(); - $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes); - if (is_resource($process)) { - // Theoretically, this style of communication could cause a deadlock - // here. If the stdout buffer fills up, then writes to stdin could - // block. This doesn't appear to happen with tidy, because tidy only - // writes to stdout after it's finished reading from stdin. Search - // for tidyParseStdin and tidySaveStdout in console/tidy.c - fwrite($pipes[0], $text); - fclose($pipes[0]); - while (!feof($pipes[1])) { - $cleansource .= fgets($pipes[1], 1024); + if( function_exists('proc_open') ) { + $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes); + if (is_resource($process)) { + // Theoretically, this style of communication could cause a deadlock + // here. If the stdout buffer fills up, then writes to stdin could + // block. This doesn't appear to happen with tidy, because tidy only + // writes to stdout after it's finished reading from stdin. Search + // for tidyParseStdin and tidySaveStdout in console/tidy.c + fwrite($pipes[0], $text); + fclose($pipes[0]); + while (!feof($pipes[1])) { + $cleansource .= fgets($pipes[1], 1024); + } + fclose($pipes[1]); + proc_close($process); } - fclose($pipes[1]); - proc_close($process); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); if( $cleansource == '' && $text != '') { // Some kind of error happened, so we couldn't get the corrected text. @@ -731,8 +748,7 @@ class Parser */ function internalTidy( $text ) { global $wgTidyConf, $IP, $wgDebugTidy; - $fname = 'Parser::internalTidy'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $tidy = new tidy; $tidy->parseString( $text, $wgTidyConf, 'utf8' ); @@ -750,7 +766,7 @@ class Parser "\n-->"; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $cleansource; } @@ -760,34 +776,35 @@ class Parser * @private */ function doTableStuff ( $text ) { - $fname = 'Parser::doTableStuff'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); - $lines = explode ( "\n" , $text ); + $lines = StringUtils::explode( "\n", $text ); + $out = ''; $td_history = array (); // Is currently a td tag open? $last_tag_history = array (); // Save history of last lag activated (td, th or caption) $tr_history = array (); // Is currently a tr tag open? $tr_attributes = array (); // history of tr attributes $has_opened_tr = array(); // Did this table open a <tr> element? $indent_level = 0; // indent level of the table - foreach ( $lines as $key => $line ) - { - $line = trim ( $line ); + + foreach ( $lines as $outLine ) { + $line = trim( $outLine ); if( $line == '' ) { // empty line, go to next line + $out .= $outLine."\n"; continue; } - $first_character = $line{0}; + $first_character = $line[0]; $matches = array(); - if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) { + if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) { // First check if we are starting a new table $indent_level = strlen( $matches[1] ); $attributes = $this->mStripState->unstripBoth( $matches[2] ); $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' ); - $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>"; + $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>"; array_push ( $td_history , false ); array_push ( $last_tag_history , '' ); array_push ( $tr_history , false ); @@ -795,8 +812,9 @@ class Parser array_push ( $has_opened_tr , false ); } else if ( count ( $td_history ) == 0 ) { // Don't do any of the following + $out .= $outLine."\n"; continue; - } else if ( substr ( $line , 0 , 2 ) == '|}' ) { + } else if ( substr ( $line , 0 , 2 ) === '|}' ) { // We are ending a table $line = '</table>' . substr ( $line , 2 ); $last_tag = array_pop ( $last_tag_history ); @@ -813,8 +831,8 @@ class Parser $line = "</{$last_tag}>{$line}"; } array_pop ( $tr_attributes ); - $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level ); - } else if ( substr ( $line , 0 , 2 ) == '|-' ) { + $outLine = $line . str_repeat( '</dd></dl>' , $indent_level ); + } else if ( substr ( $line , 0 , 2 ) === '|-' ) { // Now we have a table row $line = preg_replace( '#^\|-+#', '', $line ); @@ -837,21 +855,21 @@ class Parser $line = "</{$last_tag}>{$line}"; } - $lines[$key] = $line; + $outLine = $line; array_push ( $tr_history , false ); array_push ( $td_history , false ); array_push ( $last_tag_history , '' ); } - else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) { + else if ( $first_character === '|' || $first_character === '!' || substr ( $line , 0 , 2 ) === '|+' ) { // This might be cell elements, td, th or captions - if ( substr ( $line , 0 , 2 ) == '|+' ) { + if ( substr ( $line , 0 , 2 ) === '|+' ) { $first_character = '+'; $line = substr ( $line , 1 ); } $line = substr ( $line , 1 ); - if ( $first_character == '!' ) { + if ( $first_character === '!' ) { $line = str_replace ( '!!' , '||' , $line ); } @@ -861,13 +879,13 @@ class Parser // attribute values containing literal "||". $cells = StringUtils::explodeMarkup( '||' , $line ); - $lines[$key] = ''; + $outLine = ''; // Loop through each table cell foreach ( $cells as $cell ) { $previous = ''; - if ( $first_character != '+' ) + if ( $first_character !== '+' ) { $tr_after = array_pop ( $tr_attributes ); if ( !array_pop ( $tr_history ) ) { @@ -885,11 +903,11 @@ class Parser $previous = "</{$last_tag}>{$previous}"; } - if ( $first_character == '|' ) { + if ( $first_character === '|' ) { $last_tag = 'td'; - } else if ( $first_character == '!' ) { + } else if ( $first_character === '!' ) { $last_tag = 'th'; - } else if ( $first_character == '+' ) { + } else if ( $first_character === '+' ) { $last_tag = 'caption'; } else { $last_tag = ''; @@ -912,38 +930,42 @@ class Parser $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}"; } - $lines[$key] .= $cell; + $outLine .= $cell; array_push ( $td_history , true ); } } + $out .= $outLine . "\n"; } // Closing open td, tr && table while ( count ( $td_history ) > 0 ) { if ( array_pop ( $td_history ) ) { - $lines[] = '</td>' ; + $out .= "</td>\n"; } if ( array_pop ( $tr_history ) ) { - $lines[] = '</tr>' ; + $out .= "</tr>\n"; } if ( !array_pop ( $has_opened_tr ) ) { - $lines[] = "<tr><td></td></tr>" ; + $out .= "<tr><td></td></tr>\n" ; } - $lines[] = '</table>' ; + $out .= "</table>\n"; } - $output = implode ( "\n" , $lines ) ; + // Remove trailing line-ending (b/c) + if ( substr( $out, -1 ) === "\n" ) { + $out = substr( $out, 0, -1 ); + } // special case: don't return empty table - if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) { - $output = ''; + if( $out === "<table>\n<tr><td></td></tr>\n</table>" ) { + $out = ''; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); - return $output; + return $out; } /** @@ -954,12 +976,11 @@ class Parser */ function internalParse( $text ) { $isMain = true; - $fname = 'Parser::internalParse'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); # Hook to suspend the parser in this state if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text ; } @@ -992,84 +1013,147 @@ class Parser $text = $this->doMagicLinks( $text ); $text = $this->formatHeadings( $text, $isMain ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } /** * Replace special strings like "ISBN xxx" and "RFC xxx" with * magic external links. - * + * + * DML * @private */ function doMagicLinks( $text ) { wfProfileIn( __METHOD__ ); + $prots = $this->mUrlProtocols; + $urlChar = self::EXT_LINK_URL_CLASS; $text = preg_replace_callback( '!(?: # Start cases - <a.*?</a> | # Skip link text - <.*?> | # Skip stuff inside HTML elements - (?:RFC|PMID)\s+([0-9]+) | # RFC or PMID, capture number as m[1] - ISBN\s+(\b # ISBN, capture number as m[2] - (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix - (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters - [0-9Xx] # check digit - \b) + (<a.*?</a>) | # m[1]: Skip link text + (<.*?>) | # m[2]: Skip stuff inside HTML elements' . " + (\\b(?:$prots)$urlChar+) | # m[3]: Free external links" . ' + (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number + ISBN\s+(\b # m[5]: ISBN, capture number + (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix + (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters + [0-9Xx] # check digit + \b) )!x', array( &$this, 'magicLinkCallback' ), $text ); wfProfileOut( __METHOD__ ); return $text; } function magicLinkCallback( $m ) { - if ( substr( $m[0], 0, 1 ) == '<' ) { + if ( isset( $m[1] ) && strval( $m[1] ) !== '' ) { + # Skip anchor + return $m[0]; + } elseif ( isset( $m[2] ) && strval( $m[2] ) !== '' ) { # Skip HTML element return $m[0]; - } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) { - $isbn = $m[2]; - $num = strtr( $isbn, array( - '-' => '', - ' ' => '', - 'x' => 'X', - )); - $titleObj = SpecialPage::getTitleFor( 'Booksources', $num ); - $text = '<a href="' . - $titleObj->escapeLocalUrl() . - "\" class=\"internal\">ISBN $isbn</a>"; - } else { - if ( substr( $m[0], 0, 3 ) == 'RFC' ) { + } elseif ( isset( $m[3] ) && strval( $m[3] ) !== '' ) { + # Free external link + return $this->makeFreeExternalLink( $m[0] ); + } elseif ( isset( $m[4] ) && strval( $m[4] ) !== '' ) { + # RFC or PMID + if ( substr( $m[0], 0, 3 ) === 'RFC' ) { $keyword = 'RFC'; $urlmsg = 'rfcurl'; - $id = $m[1]; - } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) { + $id = $m[4]; + } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) { $keyword = 'PMID'; $urlmsg = 'pubmedurl'; - $id = $m[1]; + $id = $m[4]; } else { throw new MWException( __METHOD__.': unrecognised match type "' . substr($m[0], 0, 20 ) . '"' ); } - $url = wfMsg( $urlmsg, $id); $sk = $this->mOptions->getSkin(); $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); - $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>"; + return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>"; + } elseif ( isset( $m[5] ) && strval( $m[5] ) !== '' ) { + # ISBN + $isbn = $m[5]; + $num = strtr( $isbn, array( + '-' => '', + ' ' => '', + 'x' => 'X', + )); + $titleObj = SpecialPage::getTitleFor( 'Booksources', $num ); + return'<a href="' . + $titleObj->escapeLocalUrl() . + "\" class=\"internal\">ISBN $isbn</a>"; + } else { + return $m[0]; } - return $text; } /** + * Make a free external link, given a user-supplied URL + * @return HTML + * @private + */ + function makeFreeExternalLink( $url ) { + global $wgContLang; + wfProfileIn( __METHOD__ ); + + $sk = $this->mOptions->getSkin(); + $trail = ''; + + # The characters '<' and '>' (which were escaped by + # removeHTMLtags()) should not be included in + # URLs, per RFC 2396. + $m2 = array(); + if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { + $trail = substr($url, $m2[0][1]) . $trail; + $url = substr($url, 0, $m2[0][1]); + } + + # Move trailing punctuation to $trail + $sep = ',;\.:!?'; + # If there is no left bracket, then consider right brackets fair game too + if ( strpos( $url, '(' ) === false ) { + $sep .= ')'; + } + + $numSepChars = strspn( strrev( $url ), $sep ); + if ( $numSepChars ) { + $trail = substr( $url, -$numSepChars ) . $trail; + $url = substr( $url, 0, -$numSepChars ); + } + + $url = Sanitizer::cleanUrl( $url ); + + # Is this an external image? + $text = $this->maybeMakeExternalImage( $url ); + if ( $text === false ) { + # Not an image, make a link + $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', + $this->getExternalLinkAttribs() ); + # Register it in the output object... + # Replace unnecessary URL escape codes with their equivalent characters + $pasteurized = self::replaceUnusualEscapes( $url ); + $this->mOutput->addExternalLink( $pasteurized ); + } + wfProfileOut( __METHOD__ ); + return $text . $trail; + } + + + /** * Parse headers and return html * * @private */ function doHeadings( $text ) { - $fname = 'Parser::doHeadings'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); for ( $i = 6; $i >= 1; --$i ) { $h = str_repeat( '=', $i ); $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } @@ -1079,15 +1163,14 @@ class Parser * @return string the altered text */ function doAllQuotes( $text ) { - $fname = 'Parser::doAllQuotes'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $outtext = ''; - $lines = explode( "\n", $text ); + $lines = StringUtils::explode( "\n", $text ); foreach ( $lines as $line ) { - $outtext .= $this->doQuotes ( $line ) . "\n"; + $outtext .= $this->doQuotes( $line ) . "\n"; } $outtext = substr($outtext, 0,-1); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $outtext; } @@ -1149,9 +1232,9 @@ class Parser { $x1 = substr ($arr[$i-1], -1); $x2 = substr ($arr[$i-1], -2, 1); - if ($x1 == ' ') { + if ($x1 === ' ') { if ($firstspace == -1) $firstspace = $i; - } else if ($x2 == ' ') { + } else if ($x2 === ' ') { if ($firstsingleletterword == -1) $firstsingleletterword = $i; } else { if ($firstmultiletterword == -1) $firstmultiletterword = $i; @@ -1191,7 +1274,7 @@ class Parser { if (($i % 2) == 0) { - if ($state == 'both') + if ($state === 'both') $buffer .= $r; else $output .= $r; @@ -1200,41 +1283,41 @@ class Parser { if (strlen ($r) == 2) { - if ($state == 'i') + if ($state === 'i') { $output .= '</i>'; $state = ''; } - else if ($state == 'bi') + else if ($state === 'bi') { $output .= '</i>'; $state = 'b'; } - else if ($state == 'ib') + else if ($state === 'ib') { $output .= '</b></i><b>'; $state = 'b'; } - else if ($state == 'both') + else if ($state === 'both') { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; } else # $state can be 'b' or '' { $output .= '<i>'; $state .= 'i'; } } else if (strlen ($r) == 3) { - if ($state == 'b') + if ($state === 'b') { $output .= '</b>'; $state = ''; } - else if ($state == 'bi') + else if ($state === 'bi') { $output .= '</i></b><i>'; $state = 'i'; } - else if ($state == 'ib') + else if ($state === 'ib') { $output .= '</b>'; $state = 'i'; } - else if ($state == 'both') + else if ($state === 'both') { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; } else # $state can be 'i' or '' { $output .= '<b>'; $state .= 'b'; } } else if (strlen ($r) == 5) { - if ($state == 'b') + if ($state === 'b') { $output .= '</b><i>'; $state = 'i'; } - else if ($state == 'i') + else if ($state === 'i') { $output .= '</i><b>'; $state = 'b'; } - else if ($state == 'bi') + else if ($state === 'bi') { $output .= '</i></b>'; $state = ''; } - else if ($state == 'ib') + else if ($state === 'ib') { $output .= '</b></i>'; $state = ''; } - else if ($state == 'both') + else if ($state === 'both') { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; } else # ($state == '') { $buffer = ''; $state = 'both'; } @@ -1243,21 +1326,21 @@ class Parser $i++; } # Now close all remaining tags. Notice that the order is important. - if ($state == 'b' || $state == 'ib') + if ($state === 'b' || $state === 'ib') $output .= '</b>'; - if ($state == 'i' || $state == 'bi' || $state == 'ib') + if ($state === 'i' || $state === 'bi' || $state === 'ib') $output .= '</i>'; - if ($state == 'bi') + if ($state === 'bi') $output .= '</b>'; # There might be lonely ''''', so make sure we have a buffer - if ($state == 'both' && $buffer) + if ($state === 'both' && $buffer) $output .= '<b><i>'.$buffer.'</i></b>'; return $output; } } /** - * Replace external links + * Replace external links (REL) * * Note: this is all very hackish and the order of execution matters a lot. * Make sure to run maintenance/parserTests.php if you change this code. @@ -1266,14 +1349,12 @@ class Parser */ function replaceExternalLinks( $text ) { global $wgContLang; - $fname = 'Parser::replaceExternalLinks'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $sk = $this->mOptions->getSkin(); $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); - - $s = $this->replaceFreeExternalLinks( array_shift( $bits ) ); + $s = array_shift( $bits ); $i = 0; while ( $i<count( $bits ) ) { @@ -1301,13 +1382,14 @@ class Parser $dtrail = ''; # Set linktype for CSS - if URL==text, link is essentially free - $linktype = ($text == $url) ? 'free' : 'text'; + $linktype = ($text === $url) ? 'free' : 'text'; # No link text, e.g. [http://domain.tld/some.link] if ( $text == '' ) { # Autonumber if allowed. See bug #5918 if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) { - $text = '[' . ++$this->mAutonumber . ']'; + $langObj = $this->getFunctionLang(); + $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']'; $linktype = 'autonumber'; } else { # Otherwise just use the URL @@ -1324,108 +1406,44 @@ class Parser $url = Sanitizer::cleanUrl( $url ); - # Process the trail (i.e. everything after this link up until start of the next link), - # replacing any non-bracketed links - $trail = $this->replaceFreeExternalLinks( $trail ); + if ( $this->mOptions->mExternalLinkTarget ) { + $attribs = array( 'target' => $this->mOptions->mExternalLinkTarget ); + } else { + $attribs = array(); + } # Use the encoded URL # This means that users can paste URLs directly into the text # Funny characters like ö aren't valid in URLs anyway # This was changed in August 2004 - $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail; + $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->getExternalLinkAttribs() ) + . $dtrail . $trail; # Register link in the output object. # Replace unnecessary URL escape codes with the referenced character # This prevents spammers from hiding links from the filters - $pasteurized = Parser::replaceUnusualEscapes( $url ); + $pasteurized = self::replaceUnusualEscapes( $url ); $this->mOutput->addExternalLink( $pasteurized ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $s; } - /** - * Replace anything that looks like a URL with a link - * @private - */ - function replaceFreeExternalLinks( $text ) { - global $wgContLang; - $fname = 'Parser::replaceFreeExternalLinks'; - wfProfileIn( $fname ); - - $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); - $s = array_shift( $bits ); - $i = 0; - - $sk = $this->mOptions->getSkin(); - - while ( $i < count( $bits ) ){ - $protocol = $bits[$i++]; - $remainder = $bits[$i++]; - - $m = array(); - if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) { - # Found some characters after the protocol that look promising - $url = $protocol . $m[1]; - $trail = $m[2]; - - # special case: handle urls as url args: - # http://www.example.com/foo?=http://www.example.com/bar - if(strlen($trail) == 0 && - isset($bits[$i]) && - preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) && - preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) - { - # add protocol, arg - $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link - $i += 2; - $trail = $m[2]; - } - - # The characters '<' and '>' (which were escaped by - # removeHTMLtags()) should not be included in - # URLs, per RFC 2396. - $m2 = array(); - if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { - $trail = substr($url, $m2[0][1]) . $trail; - $url = substr($url, 0, $m2[0][1]); - } - - # Move trailing punctuation to $trail - $sep = ',;\.:!?'; - # If there is no left bracket, then consider right brackets fair game too - if ( strpos( $url, '(' ) === false ) { - $sep .= ')'; - } - - $numSepChars = strspn( strrev( $url ), $sep ); - if ( $numSepChars ) { - $trail = substr( $url, -$numSepChars ) . $trail; - $url = substr( $url, 0, -$numSepChars ); - } - - $url = Sanitizer::cleanUrl( $url ); - - # Is this an external image? - $text = $this->maybeMakeExternalImage( $url ); - if ( $text === false ) { - # Not an image, make a link - $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() ); - # Register it in the output object... - # Replace unnecessary URL escape codes with their equivalent characters - $pasteurized = Parser::replaceUnusualEscapes( $url ); - $this->mOutput->addExternalLink( $pasteurized ); - } - $s .= $text . $trail; - } else { - $s .= $protocol . $remainder; - } + function getExternalLinkAttribs() { + $attribs = array(); + global $wgNoFollowLinks, $wgNoFollowNsExceptions; + $ns = $this->mTitle->getNamespace(); + if( $wgNoFollowLinks && !in_array($ns, $wgNoFollowNsExceptions) ) { + $attribs['rel'] = 'nofollow'; } - wfProfileOut( $fname ); - return $s; + if ( $this->mOptions->getExternalLinkTarget() ) { + $attribs['target'] = $this->mOptions->getExternalLinkTarget(); + } + return $attribs; } + /** * Replace unusual URL escape codes with their equivalent characters * @param string @@ -1438,7 +1456,7 @@ class Parser */ static function replaceUnusualEscapes( $url ) { return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', - array( 'Parser', 'replaceUnusualEscapesCallback' ), $url ); + array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url ); } /** @@ -1462,7 +1480,7 @@ class Parser /** * make an image if it's allowed, either through the global - * option or through the exception + * option, through the exception, or through the on-wiki whitelist * @private */ function maybeMakeExternalImage( $url ) { @@ -1470,47 +1488,88 @@ class Parser $imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); $imagesexception = !empty($imagesfrom); $text = false; + # $imagesfrom could be either a single string or an array of strings, parse out the latter + if( $imagesexception && is_array( $imagesfrom ) ) { + $imagematch = false; + foreach( $imagesfrom as $match ) { + if( strpos( $url, $match ) === 0 ) { + $imagematch = true; + break; + } + } + } elseif( $imagesexception ) { + $imagematch = (strpos( $url, $imagesfrom ) === 0); + } else { + $imagematch = false; + } if ( $this->mOptions->getAllowExternalImages() - || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) { + || ( $imagesexception && $imagematch ) ) { if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) { # Image found $text = $sk->makeExternalImage( $url ); } } + if( !$text && $this->mOptions->getEnableImageWhitelist() + && preg_match( self::EXT_IMAGE_REGEX, $url ) ) { + $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) ); + foreach( $whitelist as $entry ) { + # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments + if( strpos( $entry, '#' ) === 0 || $entry === '' ) + continue; + if( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) { + # Image matches a whitelist entry + $text = $sk->makeExternalImage( $url ); + break; + } + } + } return $text; } /** * Process [[ ]] wikilinks + * @return processed text * * @private */ function replaceInternalLinks( $s ) { + $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) ); + return $s; + } + + /** + * Process [[ ]] wikilinks (RIL) + * @return LinkHolderArray + * + * @private + */ + function replaceInternalLinks2( &$s ) { global $wgContLang; - static $fname = 'Parser::replaceInternalLinks' ; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); - wfProfileIn( $fname.'-setup' ); - static $tc = FALSE; + wfProfileIn( __METHOD__.'-setup' ); + static $tc = FALSE, $e1, $e1_img; # the % is needed to support urlencoded titles as well - if ( !$tc ) { $tc = Title::legalChars() . '#%'; } + if ( !$tc ) { + $tc = Title::legalChars() . '#%'; + # Match a link having the form [[namespace:link|alternate]]trail + $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; + # Match cases where there is no "]]", which might still be images + $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; + } $sk = $this->mOptions->getSkin(); + $holders = new LinkHolderArray( $this ); #split the entire text string on occurences of [[ - $a = explode( '[[', ' ' . $s ); + $a = StringUtils::explode( '[[', ' ' . $s ); #get the first element (all text up to first [[), and remove the space we added - $s = array_shift( $a ); + $s = $a->current(); + $a->next(); + $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void" $s = substr( $s, 1 ); - # Match a link having the form [[namespace:link|alternate]]trail - static $e1 = FALSE; - if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; } - # Match cases where there is no "]]", which might still be images - static $e1_img = FALSE; - if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; } - $useLinkPrefixExtension = $wgContLang->linkPrefixExtension(); $e2 = null; if ( $useLinkPrefixExtension ) { @@ -1520,8 +1579,8 @@ class Parser } if( is_null( $this->mTitle ) ) { - wfProfileOut( $fname ); - wfProfileOut( $fname.'-setup' ); + wfProfileOut( __METHOD__.'-setup' ); + wfProfileOut( __METHOD__ ); throw new MWException( __METHOD__.": \$this->mTitle is null\n" ); } $nottalk = !$this->mTitle->isTalkPage(); @@ -1543,13 +1602,20 @@ class Parser $selflink = array($this->mTitle->getPrefixedText()); } $useSubpages = $this->areSubpagesAllowed(); - wfProfileOut( $fname.'-setup' ); + wfProfileOut( __METHOD__.'-setup' ); # Loop for each link - for ($k = 0; isset( $a[$k] ); $k++) { - $line = $a[$k]; + for ( ; $line !== false && $line !== null ; $a->next(), $line = $a->current() ) { + # Check for excessive memory usage + if ( $holders->isBig() ) { + # Too big + # Do the existence check, replace the link holders and clear the array + $holders->replace( $s ); + $holders->clear(); + } + if ( $useLinkPrefixExtension ) { - wfProfileIn( $fname.'-prefixhandling' ); + wfProfileIn( __METHOD__.'-prefixhandling' ); if ( preg_match( $e2, $s, $m ) ) { $prefix = $m[2]; $s = $m[1]; @@ -1561,12 +1627,12 @@ class Parser $prefix = $first_prefix; $first_prefix = false; } - wfProfileOut( $fname.'-prefixhandling' ); + wfProfileOut( __METHOD__.'-prefixhandling' ); } $might_be_img = false; - wfProfileIn( "$fname-e1" ); + wfProfileIn( __METHOD__."-e1" ); if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt $text = $m[2]; # If we get a ] at the beginning of $m[3] that means we have a link that's something like: @@ -1600,18 +1666,18 @@ class Parser $trail = ""; } else { # Invalid form; output directly $s .= $prefix . '[[' . $line ; - wfProfileOut( "$fname-e1" ); + wfProfileOut( __METHOD__."-e1" ); continue; } - wfProfileOut( "$fname-e1" ); - wfProfileIn( "$fname-misc" ); + wfProfileOut( __METHOD__."-e1" ); + wfProfileIn( __METHOD__."-misc" ); # Don't allow internal links to pages containing # PROTO: where PROTO is a valid URL protocol; these # should be external links. if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) { $s .= $prefix . '[[' . $line ; - wfProfileOut( "$fname-misc" ); + wfProfileOut( __METHOD__."-misc" ); continue; } @@ -1622,33 +1688,36 @@ class Parser $link = $m[1]; } - $noforce = (substr($m[1], 0, 1) != ':'); + $noforce = (substr($m[1], 0, 1) !== ':'); if (!$noforce) { # Strip off leading ':' $link = substr($link, 1); } - wfProfileOut( "$fname-misc" ); - wfProfileIn( "$fname-title" ); + wfProfileOut( __METHOD__."-misc" ); + wfProfileIn( __METHOD__."-title" ); $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) ); if( !$nt ) { $s .= $prefix . '[[' . $line; - wfProfileOut( "$fname-title" ); + wfProfileOut( __METHOD__."-title" ); continue; } $ns = $nt->getNamespace(); $iw = $nt->getInterWiki(); - wfProfileOut( "$fname-title" ); + wfProfileOut( __METHOD__."-title" ); if ($might_be_img) { # if this is actually an invalid link - wfProfileIn( "$fname-might_be_img" ); - if ($ns == NS_IMAGE && $noforce) { #but might be an image + wfProfileIn( __METHOD__."-might_be_img" ); + if ($ns == NS_FILE && $noforce) { #but might be an image $found = false; - while (isset ($a[$k+1]) ) { + while ( true ) { #look at the next 'line' to see if we can close it there - $spliced = array_splice( $a, $k + 1, 1 ); - $next_line = array_shift( $spliced ); + $a->next(); + $next_line = $a->current(); + if ( $next_line === false || $next_line === null ) { + break; + } $m = explode( ']]', $next_line, 3 ); if ( count( $m ) == 3 ) { # the first ]] closes the inner link, the second the image @@ -1668,19 +1737,19 @@ class Parser if ( !$found ) { # we couldn't find the end of this imageLink, so output it raw #but don't ignore what might be perfectly normal links in the text we've examined - $text = $this->replaceInternalLinks($text); + $holders->merge( $this->replaceInternalLinks2( $text ) ); $s .= "{$prefix}[[$link|$text"; # note: no $trail, because without an end, there *is* no trail - wfProfileOut( "$fname-might_be_img" ); + wfProfileOut( __METHOD__."-might_be_img" ); continue; } } else { #it's not an image, so output it raw $s .= "{$prefix}[[$link|$text"; # note: no $trail, because without an end, there *is* no trail - wfProfileOut( "$fname-might_be_img" ); + wfProfileOut( __METHOD__."-might_be_img" ); continue; } - wfProfileOut( "$fname-might_be_img" ); + wfProfileOut( __METHOD__."-might_be_img" ); } $wasblank = ( '' == $text ); @@ -1690,41 +1759,36 @@ class Parser if( $noforce ) { # Interwikis - wfProfileIn( "$fname-interwiki" ); + wfProfileIn( __METHOD__."-interwiki" ); if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) { $this->mOutput->addLanguageLink( $nt->getFullText() ); $s = rtrim($s . $prefix); $s .= trim($trail, "\n") == '' ? '': $prefix . $trail; - wfProfileOut( "$fname-interwiki" ); + wfProfileOut( __METHOD__."-interwiki" ); continue; } - wfProfileOut( "$fname-interwiki" ); + wfProfileOut( __METHOD__."-interwiki" ); - if ( $ns == NS_IMAGE ) { - wfProfileIn( "$fname-image" ); + if ( $ns == NS_FILE ) { + wfProfileIn( __METHOD__."-image" ); if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) { # recursively parse links inside the image caption # actually, this will parse them in any other parameters, too, # but it might be hard to fix that, and it doesn't matter ATM $text = $this->replaceExternalLinks($text); - $text = $this->replaceInternalLinks($text); + $holders->merge( $this->replaceInternalLinks2( $text ) ); # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them - $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail; - $this->mOutput->addImage( $nt->getDBkey() ); - - wfProfileOut( "$fname-image" ); - continue; - } else { - # We still need to record the image's presence on the page - $this->mOutput->addImage( $nt->getDBkey() ); + $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail; } - wfProfileOut( "$fname-image" ); + $this->mOutput->addImage( $nt->getDBkey() ); + wfProfileOut( __METHOD__."-image" ); + continue; } if ( $ns == NS_CATEGORY ) { - wfProfileIn( "$fname-category" ); + wfProfileIn( __METHOD__."-category" ); $s = rtrim($s . "\n"); # bug 87 if ( $wasblank ) { @@ -1743,26 +1807,27 @@ class Parser */ $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail; - wfProfileOut( "$fname-category" ); + wfProfileOut( __METHOD__."-category" ); continue; } } # Self-link checking - if( $nt->getFragment() === '' ) { + if( $nt->getFragment() === '' && $ns != NS_SPECIAL ) { if( in_array( $nt->getPrefixedText(), $selflink, true ) ) { $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); continue; } } - # Special and Media are pseudo-namespaces; no pages actually exist in them + # NS_MEDIA is a pseudo-namespace for linking directly to a file + # FIXME: Should do batch file existence checks, see comment below if( $ns == NS_MEDIA ) { # Give extensions a chance to select the file revision for us $skip = $time = false; wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) ); if ( $skip ) { - $link = $sk->makeLinkObj( $nt ); + $link = $sk->link( $nt ); } else { $link = $sk->makeMediaLinkObj( $nt, $text, $time ); } @@ -1770,28 +1835,23 @@ class Parser $s .= $prefix . $this->armorLinks( $link ) . $trail; $this->mOutput->addImage( $nt->getDBkey() ); continue; - } elseif( $ns == NS_SPECIAL ) { - if( SpecialPage::exists( $nt->getDBkey() ) ) { - $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); - } else { - $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); - } - continue; - } elseif( $ns == NS_IMAGE ) { - $img = wfFindFile( $nt ); - if( $img ) { - // Force a blue link if the file exists; may be a remote - // upload on the shared repository, and we want to see its - // auto-generated page. - $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); - $this->mOutput->addLink( $nt ); - continue; - } } - $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); + + # Some titles, such as valid special pages or files in foreign repos, should + # be shown as bluelinks even though they're not included in the page table + # + # FIXME: isAlwaysKnown() can be expensive for file links; we should really do + # batch file existence checks for NS_FILE and NS_MEDIA + if( $iw == '' && $nt->isAlwaysKnown() ) { + $this->mOutput->addLink( $nt ); + $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + } else { + # Links will be added to the output link list after checking + $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix ); + } } - wfProfileOut( $fname ); - return $s; + wfProfileOut( __METHOD__ ); + return $holders; } /** @@ -1800,32 +1860,10 @@ class Parser * parsing of interwiki links, and secondly to allow all existence checks and * article length checks (for stub links) to be bundled into a single query. * + * @deprecated */ function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - wfProfileIn( __METHOD__ ); - if ( ! is_object($nt) ) { - # Fail gracefully - $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}"; - } else { - # Separate the link trail from the rest of the link - list( $inside, $trail ) = Linker::splitTrail( $trail ); - - if ( $nt->isExternal() ) { - $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside ); - $this->mInterwikiLinkHolders['titles'][] = $nt; - $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}"; - } else { - $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() ); - $this->mLinkHolders['dbkeys'][] = $nt->getDBkey(); - $this->mLinkHolders['queries'][] = $query; - $this->mLinkHolders['texts'][] = $prefix.$text.$inside; - $this->mLinkHolders['titles'][] = $nt; - - $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}"; - } - } - wfProfileOut( __METHOD__ ); - return $retVal; + return $this->mLinkHolders->makeHolder( $nt, $text, $query, $trail, $prefix ); } /** @@ -1853,10 +1891,8 @@ class Parser * Insert a NOPARSE hacky thing into any inline links in a chunk that's * going to go through further parsing steps before inline URL expansion. * - * In particular this is important when using action=render, which causes - * full URLs to be included. - * - * Oh man I hate our multi-layer parser! + * Not needed quite as much as it used to be since free links are a bit + * more sensible these days. But bracketed links are still an issue. * * @param string more-or-less HTML * @return string less-or-more HTML with NOPARSE bits @@ -1891,8 +1927,7 @@ class Parser # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage - $fname = 'Parser::maybeDoSubpageLink'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $ret = $target; # default return value is no change # Some namespaces don't allow subpages, @@ -1908,7 +1943,7 @@ class Parser # bug 7425 $target = trim( $target ); # Look at the first character - if( $target != '' && $target{0} == '/' ) { + if( $target != '' && $target{0} === '/' ) { # / at end means we don't want the slash to be shown $m = array(); $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m ); @@ -1935,7 +1970,7 @@ class Parser if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) ); # / at the end means don't show full path - if( substr( $nodotdot, -1, 1 ) == '/' ) { + if( substr( $nodotdot, -1, 1 ) === '/' ) { $nodotdot = substr( $nodotdot, 0, -1 ); if( '' === $text ) { $text = $nodotdot . $suffix; @@ -1951,7 +1986,7 @@ class Parser } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $ret; } @@ -1987,10 +2022,10 @@ class Parser /* private */ function openList( $char ) { $result = $this->closeParagraph(); - if ( '*' == $char ) { $result .= '<ul><li>'; } - else if ( '#' == $char ) { $result .= '<ol><li>'; } - else if ( ':' == $char ) { $result .= '<dl><dd>'; } - else if ( ';' == $char ) { + if ( '*' === $char ) { $result .= '<ul><li>'; } + else if ( '#' === $char ) { $result .= '<ol><li>'; } + else if ( ':' === $char ) { $result .= '<dl><dd>'; } + else if ( ';' === $char ) { $result .= '<dl><dt>'; $this->mDTopen = true; } @@ -2000,11 +2035,11 @@ class Parser } /* private */ function nextItem( $char ) { - if ( '*' == $char || '#' == $char ) { return '</li><li>'; } - else if ( ':' == $char || ';' == $char ) { + if ( '*' === $char || '#' === $char ) { return '</li><li>'; } + else if ( ':' === $char || ';' === $char ) { $close = '</dd>'; if ( $this->mDTopen ) { $close = '</dt>'; } - if ( ';' == $char ) { + if ( ';' === $char ) { $this->mDTopen = true; return $close . '<dt>'; } else { @@ -2016,9 +2051,9 @@ class Parser } /* private */ function closeList( $char ) { - if ( '*' == $char ) { $text = '</li></ul>'; } - else if ( '#' == $char ) { $text = '</li></ol>'; } - else if ( ':' == $char ) { + if ( '*' === $char ) { $text = '</li></ul>'; } + else if ( '#' === $char ) { $text = '</li></ol>'; } + else if ( ':' === $char ) { if ( $this->mDTopen ) { $this->mDTopen = false; $text = '</dt></dl>'; @@ -2032,56 +2067,59 @@ class Parser /**#@-*/ /** - * Make lists from lines starting with ':', '*', '#', etc. + * Make lists from lines starting with ':', '*', '#', etc. (DBL) * * @private * @return string the lists rendered as HTML */ function doBlockLevels( $text, $linestart ) { - $fname = 'Parser::doBlockLevels'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); # Parsing through the text line by line. The main thing # happening here is handling of block-level elements p, pre, # and making lists from lines starting with * # : etc. # - $textLines = explode( "\n", $text ); + $textLines = StringUtils::explode( "\n", $text ); $lastPrefix = $output = ''; $this->mDTopen = $inBlockElem = false; $prefixLength = 0; $paragraphStack = false; - if ( !$linestart ) { - $output .= array_shift( $textLines ); - } foreach ( $textLines as $oLine ) { + # Fix up $linestart + if ( !$linestart ) { + $output .= $oLine; + $linestart = true; + continue; + } + $lastPrefixLength = strlen( $lastPrefix ); $preCloseMatch = preg_match('/<\\/pre/i', $oLine ); $preOpenMatch = preg_match('/<pre/i', $oLine ); if ( !$this->mInPre ) { # Multiple prefixes may abut each other for nested lists. $prefixLength = strspn( $oLine, '*#:;' ); - $pref = substr( $oLine, 0, $prefixLength ); + $prefix = substr( $oLine, 0, $prefixLength ); # eh? - $pref2 = str_replace( ';', ':', $pref ); + $prefix2 = str_replace( ';', ':', $prefix ); $t = substr( $oLine, $prefixLength ); - $this->mInPre = !empty($preOpenMatch); + $this->mInPre = (bool)$preOpenMatch; } else { # Don't interpret any other prefixes in preformatted text $prefixLength = 0; - $pref = $pref2 = ''; + $prefix = $prefix2 = ''; $t = $oLine; } # List generation - if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) { + if( $prefixLength && $lastPrefix === $prefix2 ) { # Same as the last item, so no need to deal with nesting or opening stuff - $output .= $this->nextItem( substr( $pref, -1 ) ); + $output .= $this->nextItem( substr( $prefix, -1 ) ); $paragraphStack = false; - if ( substr( $pref, -1 ) == ';') { + if ( substr( $prefix, -1 ) === ';') { # The one nasty exception: definition lists work like this: # ; title : definition text # So we check for : in the remainder text to split up the @@ -2094,21 +2132,21 @@ class Parser } } elseif( $prefixLength || $lastPrefixLength ) { # Either open or close a level... - $commonPrefixLength = $this->getCommon( $pref, $lastPrefix ); + $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix ); $paragraphStack = false; while( $commonPrefixLength < $lastPrefixLength ) { - $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} ); + $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] ); --$lastPrefixLength; } if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) { - $output .= $this->nextItem( $pref{$commonPrefixLength-1} ); + $output .= $this->nextItem( $prefix[$commonPrefixLength-1] ); } while ( $prefixLength > $commonPrefixLength ) { - $char = substr( $pref, $commonPrefixLength, 1 ); + $char = substr( $prefix, $commonPrefixLength, 1 ); $output .= $this->openList( $char ); - if ( ';' == $char ) { + if ( ';' === $char ) { # FIXME: This is dupe of code above if ($this->findColonNoLinks($t, $term, $t2) !== false) { $t = $t2; @@ -2117,10 +2155,10 @@ class Parser } ++$commonPrefixLength; } - $lastPrefix = $pref2; + $lastPrefix = $prefix2; } if( 0 == $prefixLength ) { - wfProfileIn( "$fname-paragraph" ); + wfProfileIn( __METHOD__."-paragraph" ); # No prefix (not in list)--go to paragraph mode // XXX: use a stack for nestable elements like span, table and div $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t ); @@ -2140,9 +2178,9 @@ class Parser $inBlockElem = true; } } else if ( !$inBlockElem && !$this->mInPre ) { - if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) { + if ( ' ' == $t{0} and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) { // pre - if ($this->mLastSection != 'pre') { + if ($this->mLastSection !== 'pre') { $paragraphStack = false; $output .= $this->closeParagraph().'<pre>'; $this->mLastSection = 'pre'; @@ -2156,7 +2194,7 @@ class Parser $paragraphStack = false; $this->mLastSection = 'p'; } else { - if ($this->mLastSection != 'p' ) { + if ($this->mLastSection !== 'p' ) { $output .= $this->closeParagraph(); $this->mLastSection = ''; $paragraphStack = '<p>'; @@ -2169,14 +2207,14 @@ class Parser $output .= $paragraphStack; $paragraphStack = false; $this->mLastSection = 'p'; - } else if ($this->mLastSection != 'p') { + } else if ($this->mLastSection !== 'p') { $output .= $this->closeParagraph().'<p>'; $this->mLastSection = 'p'; } } } } - wfProfileOut( "$fname-paragraph" ); + wfProfileOut( __METHOD__."-paragraph" ); } // somewhere above we forget to get out of pre block (bug 785) if($preCloseMatch && $this->mInPre) { @@ -2187,7 +2225,7 @@ class Parser } } while ( $prefixLength ) { - $output .= $this->closeList( $pref2{$prefixLength-1} ); + $output .= $this->closeList( $prefix2[$prefixLength-1] ); --$prefixLength; } if ( '' != $this->mLastSection ) { @@ -2195,7 +2233,7 @@ class Parser $this->mLastSection = ''; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $output; } @@ -2208,13 +2246,12 @@ class Parser * return string the position of the ':', or false if none found */ function findColonNoLinks($str, &$before, &$after) { - $fname = 'Parser::findColonNoLinks'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $pos = strpos( $str, ':' ); if( $pos === false ) { // Nothing to find! - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } @@ -2223,7 +2260,7 @@ class Parser // Easy; no tag nesting to worry about $before = substr( $str, 0, $pos ); $after = substr( $str, $pos+1 ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $pos; } @@ -2247,7 +2284,7 @@ class Parser // We found it! $before = substr( $str, 0, $i ); $after = substr( $str, $i + 1 ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $i; } // Embedded in a tag; don't break it. @@ -2257,7 +2294,7 @@ class Parser $colon = strpos( $str, ':', $i ); if( $colon === false ) { // Nothing else interesting - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } $lt = strpos( $str, '<', $i ); @@ -2266,7 +2303,7 @@ class Parser // We found it! $before = substr( $str, 0, $colon ); $after = substr( $str, $colon + 1 ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $i; } } @@ -2313,18 +2350,18 @@ class Parser break; case 3: // self::COLON_STATE_CLOSETAG: // In a </tag> - if( $c == ">" ) { + if( $c === ">" ) { $stack--; if( $stack < 0 ) { - wfDebug( "Invalid input in $fname; too many close tags\n" ); - wfProfileOut( $fname ); + wfDebug( __METHOD__.": Invalid input; too many close tags\n" ); + wfProfileOut( __METHOD__ ); return false; } $state = self::COLON_STATE_TEXT; } break; case self::COLON_STATE_TAGSLASH: - if( $c == ">" ) { + if( $c === ">" ) { // Yes, a self-closed tag <blah/> $state = self::COLON_STATE_TEXT; } else { @@ -2333,33 +2370,33 @@ class Parser } break; case 5: // self::COLON_STATE_COMMENT: - if( $c == "-" ) { + if( $c === "-" ) { $state = self::COLON_STATE_COMMENTDASH; } break; case self::COLON_STATE_COMMENTDASH: - if( $c == "-" ) { + if( $c === "-" ) { $state = self::COLON_STATE_COMMENTDASHDASH; } else { $state = self::COLON_STATE_COMMENT; } break; case self::COLON_STATE_COMMENTDASHDASH: - if( $c == ">" ) { + if( $c === ">" ) { $state = self::COLON_STATE_TEXT; } else { $state = self::COLON_STATE_COMMENT; } break; default: - throw new MWException( "State machine error in $fname" ); + throw new MWException( "State machine error in " . __METHOD__ ); } } if( $stack > 0 ) { - wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" ); + wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" ); return false; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } @@ -2552,9 +2589,11 @@ class Parser case 'numberofpages': return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() ); case 'numberofadmins': - return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::admins() ); + return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::numberingroup('sysop') ); case 'numberofedits': return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() ); + case 'numberofviews': + return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::views() ); case 'currenttimestamp': return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts ); case 'localtimestamp': @@ -2589,12 +2628,11 @@ class Parser * @private */ function initialiseVariables() { - $fname = 'Parser::initialiseVariables'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $variableIDs = MagicWord::getVariableIDs(); $this->mVariables = new MagicWordArray( $variableIDs ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -2663,8 +2701,7 @@ class Parser return $text; } - $fname = __METHOD__; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); if ( $frame === false ) { $frame = $this->getPreprocessor()->newFrame(); @@ -2677,7 +2714,7 @@ class Parser $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0; $text = $frame->expand( $dom, $flags ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } @@ -2718,7 +2755,7 @@ class Parser function limitationWarn( $limitationType, $current=null, $max=null) { $msgName = $limitationType . '-warning'; //does no harm if $current and $max are present but are unnecessary for the message - $warning = wfMsg( $msgName, $current, $max); + $warning = wfMsgExt( $msgName, array( 'parsemag', 'escape' ), $current, $max ); $this->mOutput->addWarning( $warning ); $cat = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( $limitationType . '-category' ) ); if ( $cat ) { @@ -2739,9 +2776,8 @@ class Parser * @private */ function braceSubstitution( $piece, $frame ) { - global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces; - $fname = __METHOD__; - wfProfileIn( $fname ); + global $wgContLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces; + wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__.'-setup' ); # Flags @@ -2855,7 +2891,7 @@ class Parser # Workaround for PHP bug 35229 and similar if ( !is_callable( $callback ) ) { - throw new MWException( "Tag hook for $name is not callable\n" ); + throw new MWException( "Tag hook for $function is not callable\n" ); } $result = call_user_func_array( $callback, $allArgs ); $found = true; @@ -2898,19 +2934,19 @@ class Parser $titleText = $title->getPrefixedText(); # Check for language variants if the template is not found if($wgContLang->hasVariants() && $title->getArticleID() == 0){ - $wgContLang->findVariantLink($part1, $title); + $wgContLang->findVariantLink( $part1, $title, true ); } # Do infinite loop check if ( !$frame->loopCheck( $title ) ) { $found = true; - $text = "<span class=\"error\">Template loop detected: [[$titleText]]</span>"; + $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>'; wfDebug( __METHOD__.": template loop broken at '$titleText'\n" ); } # Do recursion depth check $limit = $this->mOptions->getMaxTemplateDepth(); if ( $frame->depth >= $limit ) { $found = true; - $text = "<span class=\"error\">Template recursion depth limit exceeded ($limit)</span>"; + $text = '<span class="error">' . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit ) . '</span>'; } } } @@ -2928,7 +2964,7 @@ class Parser } } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) { $found = false; //access denied - wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() ); + wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() ); } else { list( $text, $title ) = $this->getTemplateDom( $title ); if ( $text !== false ) { @@ -2962,7 +2998,7 @@ class Parser # Recover the source wikitext and return it if ( !$found ) { $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return array( 'object' => $text ); } @@ -3021,7 +3057,7 @@ class Parser $ret = array( 'text' => $text ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $ret; } @@ -3121,8 +3157,8 @@ class Parser if( $rev ) { $text = $rev->getText(); } elseif( $title->getNamespace() == NS_MEDIAWIKI ) { - global $wgLang; - $message = $wgLang->lcfirst( $title->getText() ); + global $wgContLang; + $message = $wgContLang->lcfirst( $title->getText() ); $text = wfMsgForContentNoTrans( $message ); if( wfEmptyMsg( $message, $text ) ) { $text = false; @@ -3308,7 +3344,7 @@ class Parser } } - if ( $name == 'html' || $name == 'nowiki' ) { + if ( $name === 'html' || $name === 'nowiki' ) { $this->mStripState->nowiki->setPair( $marker, $output ); } else { $this->mStripState->general->setPair( $marker, $output ); @@ -3384,6 +3420,16 @@ class Parser wfDebug( __METHOD__.": [[MediaWiki:hidden-category-category]] is not a valid title!\n" ); } } + # (bug 8068) Allow control over whether robots index a page. + # + # FIXME (bug 14899): __INDEX__ always overrides __NOINDEX__ here! This + # is not desirable, the last one on the page should win. + if( isset( $this->mDoubleUnderscores['noindex'] ) ) { + $this->mOutput->setIndexPolicy( 'noindex' ); + } elseif( isset( $this->mDoubleUnderscores['index'] ) ) { + $this->mOutput->setIndexPolicy( 'index' ); + } + return $text; } @@ -3402,13 +3448,14 @@ class Parser * @private */ function formatHeadings( $text, $isMain=true ) { - global $wgMaxTocLevel, $wgContLang; + global $wgMaxTocLevel, $wgContLang, $wgEnforceHtmlIds; $doNumberHeadings = $this->mOptions->getNumberHeadings(); - if( !$this->mTitle->quickUserCan( 'edit' ) ) { + $showEditLink = $this->mOptions->getEditSection(); + + // Do not call quickUserCan unless necessary + if( $showEditLink && !$this->mTitle->quickUserCan( 'edit' ) ) { $showEditLink = 0; - } else { - $showEditLink = $this->mOptions->getEditSection(); } # Inhibit editsection links if requested in the page @@ -3554,12 +3601,7 @@ class Parser # <!--LINK number--> # turns into # link text with suffix - $safeHeadline = preg_replace( '/<!--LINK ([0-9]*)-->/e', - "\$this->mLinkHolders['texts'][\$1]", - $safeHeadline ); - $safeHeadline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e', - "\$this->mInterwikiLinkHolders['texts'][\$1]", - $safeHeadline ); + $safeHeadline = $this->replaceLinkHoldersText( $safeHeadline ); # Strip out HTML (other than plain <sup> and <sub>: bug 8393) $tocline = preg_replace( @@ -3575,13 +3617,60 @@ class Parser # Save headline for section edit hint before it's escaped $headlineHint = $safeHeadline; - $safeHeadline = Sanitizer::escapeId( $safeHeadline ); - # HTML names must be case-insensitively unique (bug 10721) + + if ( $wgEnforceHtmlIds ) { + $legacyHeadline = false; + $safeHeadline = Sanitizer::escapeId( $safeHeadline, + 'noninitial' ); + } else { + # For reverse compatibility, provide an id that's + # HTML4-compatible, like we used to. + # + # It may be worth noting, academically, that it's possible for + # the legacy anchor to conflict with a non-legacy headline + # anchor on the page. In this case likely the "correct" thing + # would be to either drop the legacy anchors or make sure + # they're numbered first. However, this would require people + # to type in section names like "abc_.D7.93.D7.90.D7.A4" + # manually, so let's not bother worrying about it. + $legacyHeadline = Sanitizer::escapeId( $safeHeadline, + 'noninitial' ); + $safeHeadline = Sanitizer::escapeId( $safeHeadline, 'xml' ); + + if ( $legacyHeadline == $safeHeadline ) { + # No reason to have both (in fact, we can't) + $legacyHeadline = false; + } elseif ( $legacyHeadline != Sanitizer::escapeId( + $legacyHeadline, 'xml' ) ) { + # The legacy id is invalid XML. We used to allow this, but + # there's no reason to do so anymore. Backward + # compatibility will fail slightly in this case, but it's + # no big deal. + $legacyHeadline = false; + } + } + + # HTML names must be case-insensitively unique (bug 10721). FIXME: + # Does this apply to Unicode characters? Because we aren't + # handling those here. $arrayKey = strtolower( $safeHeadline ); + if ( $legacyHeadline === false ) { + $legacyArrayKey = false; + } else { + $legacyArrayKey = strtolower( $legacyHeadline ); + } # count how many in assoc. array so we can track dupes in anchors - isset( $refers[$arrayKey] ) ? $refers[$arrayKey]++ : $refers[$arrayKey] = 1; - $refcount[$headlineCount] = $refers[$arrayKey]; + if ( isset( $refers[$arrayKey] ) ) { + $refers[$arrayKey]++; + } else { + $refers[$arrayKey] = 1; + } + if ( isset( $refers[$legacyArrayKey] ) ) { + $refers[$legacyArrayKey]++; + } else { + $refers[$legacyArrayKey] = 1; + } # Don't number the heading if it is the only one (looks silly) if( $doNumberHeadings && count( $matches[3] ) > 1) { @@ -3591,8 +3680,12 @@ class Parser # Create the anchor for linking from the TOC to the section $anchor = $safeHeadline; - if($refcount[$headlineCount] > 1 ) { - $anchor .= '_' . $refcount[$headlineCount]; + $legacyAnchor = $legacyHeadline; + if ( $refers[$arrayKey] > 1 ) { + $anchor .= '_' . $refers[$arrayKey]; + } + if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) { + $legacyAnchor .= '_' . $refers[$legacyArrayKey]; } if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) { $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel); @@ -3603,14 +3696,16 @@ class Parser if( $isTemplate ) { # Put a T flag in the section identifier, to indicate to extractSections() # that sections inside <includeonly> should be counted. - $editlink = $sk->editSectionLinkForOther($titleText, "T-$sectionIndex"); + $editlink = $sk->doEditSectionLink(Title::newFromText( $titleText ), "T-$sectionIndex"); } else { - $editlink = $sk->editSectionLink($this->mTitle, $sectionIndex, $headlineHint); + $editlink = $sk->doEditSectionLink($this->mTitle, $sectionIndex, $headlineHint); } } else { $editlink = ''; } - $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink ); + $head[$headlineCount] = $sk->makeHeadline( $level, + $matches['attrib'][$headlineCount], $anchor, $headline, + $editlink, $legacyAnchor ); $headlineCount++; } @@ -3635,7 +3730,7 @@ class Parser $i = 0; foreach( $blocks as $block ) { - if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) { + if( $showEditLink && $headlineCount > 0 && $i == 0 && $block !== "\n" ) { # This is the [edit] link that appears for the top block of text when # section editing is enabled @@ -3737,11 +3832,13 @@ class Parser $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii! $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]] + $p4 = "/\[\[(:?$nc+:|:|)($tc+?)(($tc+))\\|]]/"; # [[ns:page(context)|]] $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]] $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text ); + $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text ); $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text ); $t = $this->mTitle->getText(); @@ -3787,7 +3884,7 @@ class Parser } else { # Failed to validate; fall back to the default $nickname = $username; - wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" ); + wfDebug( __METHOD__.": $username has bad XML tags in signature.\n" ); } } @@ -3811,7 +3908,7 @@ class Parser * @return mixed An expanded string, or false if invalid. */ function validateSig( $text ) { - return( wfIsWellFormedXmlFragment( $text ) ? $text : false ); + return( Xml::isWellFormedXmlFragment( $text ) ? $text : false ); } /** @@ -3833,6 +3930,11 @@ class Parser $this->setOutputType = self::OT_PREPROCESS; } + # Option to disable this feature + if ( !$this->mOptions->getCleanSignatures() ) { + return $text; + } + # FIXME: regex doesn't respect extension tags or nowiki # => Move this logic to braceSubstitution() $substWord = MagicWord::get( 'subst' ); @@ -3888,19 +3990,17 @@ class Parser global $wgTitle; static $executing = false; - $fname = "Parser::transformMsg"; - # Guard against infinite recursion if ( $executing ) { return $text; } $executing = true; - wfProfileIn($fname); + wfProfileIn(__METHOD__); $text = $this->preprocess( $text, $wgTitle, $options ); $executing = false; - wfProfileOut($fname); + wfProfileOut(__METHOD__); return $text; } @@ -3997,7 +4097,7 @@ class Parser # Add to function cache $mw = MagicWord::get( $id ); if( !$mw ) - throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' ); + throw new MWException( __METHOD__.'() expecting a magic word identifier.' ); $synonyms = $mw->getSynonyms(); $sensitive = intval( $mw->isCaseSensitive() ); @@ -4012,7 +4112,7 @@ class Parser $syn = '#' . $syn; } # Remove trailing colon - if ( substr( $syn, -1, 1 ) == ':' ) { + if ( substr( $syn, -1, 1 ) === ':' ) { $syn = substr( $syn, 0, -1 ); } $this->mFunctionSynonyms[$sensitive][$syn] = $id; @@ -4033,266 +4133,9 @@ class Parser * Replace <!--LINK--> link placeholders with actual links, in the buffer * Placeholders created in Skin::makeLinkObj() * Returns an array of link CSS classes, indexed by PDBK. - * $options is a bit field, RLH_FOR_UPDATE to select for update */ function replaceLinkHolders( &$text, $options = 0 ) { - global $wgUser; - global $wgContLang; - - $fname = 'Parser::replaceLinkHolders'; - wfProfileIn( $fname ); - - $pdbks = array(); - $colours = array(); - $linkcolour_ids = array(); - $sk = $this->mOptions->getSkin(); - $linkCache = LinkCache::singleton(); - - if ( !empty( $this->mLinkHolders['namespaces'] ) ) { - wfProfileIn( $fname.'-check' ); - $dbr = wfGetDB( DB_SLAVE ); - $page = $dbr->tableName( 'page' ); - $threshold = $wgUser->getOption('stubthreshold'); - - # Sort by namespace - asort( $this->mLinkHolders['namespaces'] ); - - # Generate query - $query = false; - $current = null; - foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { - # Make title object - $title = $this->mLinkHolders['titles'][$key]; - - # Skip invalid entries. - # Result will be ugly, but prevents crash. - if ( is_null( $title ) ) { - continue; - } - $pdbk = $pdbks[$key] = $title->getPrefixedDBkey(); - - # Check if it's a static known link, e.g. interwiki - if ( $title->isAlwaysKnown() ) { - $colours[$pdbk] = ''; - } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) { - $colours[$pdbk] = ''; - $this->mOutput->addLink( $title, $id ); - } elseif ( $linkCache->isBadLink( $pdbk ) ) { - $colours[$pdbk] = 'new'; - } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) { - $colours[$pdbk] = 'new'; - } else { - # Not in the link cache, add it to the query - if ( !isset( $current ) ) { - $current = $ns; - $query = "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len"; - $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN("; - } elseif ( $current != $ns ) { - $current = $ns; - $query .= ")) OR (page_namespace=$ns AND page_title IN("; - } else { - $query .= ', '; - } - - $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] ); - } - } - if ( $query ) { - $query .= '))'; - if ( $options & RLH_FOR_UPDATE ) { - $query .= ' FOR UPDATE'; - } - - $res = $dbr->query( $query, $fname ); - - # Fetch data and form into an associative array - # non-existent = broken - while ( $s = $dbr->fetchObject($res) ) { - $title = Title::makeTitle( $s->page_namespace, $s->page_title ); - $pdbk = $title->getPrefixedDBkey(); - $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect ); - $this->mOutput->addLink( $title, $s->page_id ); - $colours[$pdbk] = $sk->getLinkColour( $title, $threshold ); - //add id to the extension todolist - $linkcolour_ids[$s->page_id] = $pdbk; - } - //pass an array of page_ids to an extension - wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) ); - } - wfProfileOut( $fname.'-check' ); - - # Do a second query for different language variants of links and categories - if($wgContLang->hasVariants()){ - $linkBatch = new LinkBatch(); - $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders) - $categoryMap = array(); // maps $category_variant => $category (dbkeys) - $varCategories = array(); // category replacements oldDBkey => newDBkey - - $categories = $this->mOutput->getCategoryLinks(); - - // Add variants of links to link batch - foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { - $title = $this->mLinkHolders['titles'][$key]; - if ( is_null( $title ) ) - continue; - - $pdbk = $title->getPrefixedDBkey(); - $titleText = $title->getText(); - - // generate all variants of the link title text - $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText); - - // if link was not found (in first query), add all variants to query - if ( !isset($colours[$pdbk]) ){ - foreach($allTextVariants as $textVariant){ - if($textVariant != $titleText){ - $variantTitle = Title::makeTitle( $ns, $textVariant ); - if(is_null($variantTitle)) continue; - $linkBatch->addObj( $variantTitle ); - $variantMap[$variantTitle->getPrefixedDBkey()][] = $key; - } - } - } - } - - // process categories, check if a category exists in some variant - foreach( $categories as $category ){ - $variants = $wgContLang->convertLinkToAllVariants($category); - foreach($variants as $variant){ - if($variant != $category){ - $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) ); - if(is_null($variantTitle)) continue; - $linkBatch->addObj( $variantTitle ); - $categoryMap[$variant] = $category; - } - } - } - - - if(!$linkBatch->isEmpty()){ - // construct query - $titleClause = $linkBatch->constructSet('page', $dbr); - - $variantQuery = "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len"; - - $variantQuery .= " FROM $page WHERE $titleClause"; - if ( $options & RLH_FOR_UPDATE ) { - $variantQuery .= ' FOR UPDATE'; - } - - $varRes = $dbr->query( $variantQuery, $fname ); - - // for each found variants, figure out link holders and replace - while ( $s = $dbr->fetchObject($varRes) ) { - - $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title ); - $varPdbk = $variantTitle->getPrefixedDBkey(); - $vardbk = $variantTitle->getDBkey(); - - $holderKeys = array(); - if(isset($variantMap[$varPdbk])){ - $holderKeys = $variantMap[$varPdbk]; - $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect ); - $this->mOutput->addLink( $variantTitle, $s->page_id ); - } - - // loop over link holders - foreach($holderKeys as $key){ - $title = $this->mLinkHolders['titles'][$key]; - if ( is_null( $title ) ) continue; - - $pdbk = $title->getPrefixedDBkey(); - - if(!isset($colours[$pdbk])){ - // found link in some of the variants, replace the link holder data - $this->mLinkHolders['titles'][$key] = $variantTitle; - $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey(); - - // set pdbk and colour - $pdbks[$key] = $varPdbk; - $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold ); - $linkcolour_ids[$s->page_id] = $pdbk; - } - wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) ); - } - - // check if the object is a variant of a category - if(isset($categoryMap[$vardbk])){ - $oldkey = $categoryMap[$vardbk]; - if($oldkey != $vardbk) - $varCategories[$oldkey]=$vardbk; - } - } - - // rebuild the categories in original order (if there are replacements) - if(count($varCategories)>0){ - $newCats = array(); - $originalCats = $this->mOutput->getCategories(); - foreach($originalCats as $cat => $sortkey){ - // make the replacement - if( array_key_exists($cat,$varCategories) ) - $newCats[$varCategories[$cat]] = $sortkey; - else $newCats[$cat] = $sortkey; - } - $this->mOutput->setCategoryLinks($newCats); - } - } - } - - # Construct search and replace arrays - wfProfileIn( $fname.'-construct' ); - $replacePairs = array(); - foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { - $pdbk = $pdbks[$key]; - $searchkey = "<!--LINK $key-->"; - $title = $this->mLinkHolders['titles'][$key]; - if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) { - $linkCache->addBadLinkObj( $title ); - $colours[$pdbk] = 'new'; - $this->mOutput->addLink( $title, 0 ); - $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title, - $this->mLinkHolders['texts'][$key], - $this->mLinkHolders['queries'][$key] ); - } else { - $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk], - $this->mLinkHolders['texts'][$key], - $this->mLinkHolders['queries'][$key] ); - } - } - $replacer = new HashtableReplacer( $replacePairs, 1 ); - wfProfileOut( $fname.'-construct' ); - - # Do the thing - wfProfileIn( $fname.'-replace' ); - $text = preg_replace_callback( - '/(<!--LINK .*?-->)/', - $replacer->cb(), - $text); - - wfProfileOut( $fname.'-replace' ); - } - - # Now process interwiki link holders - # This is quite a bit simpler than internal links - if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) { - wfProfileIn( $fname.'-interwiki' ); - # Make interwiki link HTML - $replacePairs = array(); - foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) { - $title = $this->mInterwikiLinkHolders['titles'][$key]; - $replacePairs[$key] = $sk->makeLinkObj( $title, $link ); - } - $replacer = new HashtableReplacer( $replacePairs, 1 ); - - $text = preg_replace_callback( - '/<!--IWLINK (.*?)-->/', - $replacer->cb(), - $text ); - wfProfileOut( $fname.'-interwiki' ); - } - - wfProfileOut( $fname ); - return $colours; + return $this->mLinkHolders->replace( $text ); } /** @@ -4302,36 +4145,7 @@ class Parser * @return string */ function replaceLinkHoldersText( $text ) { - $fname = 'Parser::replaceLinkHoldersText'; - wfProfileIn( $fname ); - - $text = preg_replace_callback( - '/<!--(LINK|IWLINK) (.*?)-->/', - array( &$this, 'replaceLinkHoldersTextCallback' ), - $text ); - - wfProfileOut( $fname ); - return $text; - } - - /** - * @param array $matches - * @return string - * @private - */ - function replaceLinkHoldersTextCallback( $matches ) { - $type = $matches[1]; - $key = $matches[2]; - if( $type == 'LINK' ) { - if( isset( $this->mLinkHolders['texts'][$key] ) ) { - return $this->mLinkHolders['texts'][$key]; - } - } elseif( $type == 'IWLINK' ) { - if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) { - return $this->mInterwikiLinkHolders['texts'][$key]; - } - } - return $matches[0]; + return $this->mLinkHolders->replaceText( $text ); } /** @@ -4342,7 +4156,7 @@ class Parser $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' ); $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); - return wfOpenElement( 'pre', $attribs ) . + return Xml::openElement( 'pre', $attribs ) . Xml::escapeTagsOnly( $content ) . '</pre>'; } @@ -4385,7 +4199,7 @@ class Parser wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) ); - $lines = explode( "\n", $text ); + $lines = StringUtils::explode( "\n", $text ); foreach ( $lines as $line ) { # match lines like these: # Image:someimage.jpg|This is some image @@ -4398,7 +4212,7 @@ class Parser if ( strpos( $matches[0], '%' ) !== false ) $matches[1] = urldecode( $matches[1] ); - $tp = Title::newFromText( $matches[1] ); + $tp = Title::newFromText( $matches[1]/*, NS_FILE*/ ); $nt =& $tp; if( is_null( $nt ) ) { # Bogus title. Ignore these so we don't bomb out later. @@ -4415,7 +4229,7 @@ class Parser $ig->add( $nt, $html ); # Only add real images (bug #5586) - if ( $nt->getNamespace() == NS_IMAGE ) { + if ( $nt->getNamespace() == NS_FILE ) { $this->mOutput->addImage( $nt->getDBkey() ); } } @@ -4435,7 +4249,7 @@ class Parser 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom' ), 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless', - 'upright', 'border' ), + 'upright', 'border', 'link', 'alt' ), ); static $internalParamMap; if ( !$internalParamMap ) { @@ -4464,20 +4278,24 @@ class Parser /** * Parse image options text and use it to make an image + * @param Title $title + * @param string $options + * @param LinkHolderArray $holders */ - function makeImage( $title, $options ) { + function makeImage( $title, $options, $holders = false ) { # Check if the options text is of the form "options|alt text" # Options are: - # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang - # * left no resizing, just left align. label is used for alt= only - # * right same, but right aligned - # * none same, but not aligned - # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox - # * center center the image - # * framed Keep original image size, no magnify-button. - # * frameless like 'thumb' but without a frame. Keeps user preferences for width - # * upright reduce width for upright images, rounded to full __0 px - # * border draw a 1px border around the image + # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang + # * left no resizing, just left align. label is used for alt= only + # * right same, but right aligned + # * none same, but not aligned + # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox + # * center center the image + # * framed Keep original image size, no magnify-button. + # * frameless like 'thumb' but without a frame. Keeps user preferences for width + # * upright reduce width for upright images, rounded to full __0 px + # * border draw a 1px border around the image + # * alt Text for HTML alt attribute (defaults to empty) # vertical-align values (no % or length right now): # * baseline # * sub @@ -4488,7 +4306,7 @@ class Parser # * bottom # * text-bottom - $parts = array_map( 'trim', explode( '|', $options) ); + $parts = StringUtils::explode( "|", $options ); $sk = $this->mOptions->getSkin(); # Give extensions a chance to select the file revision for us @@ -4496,11 +4314,21 @@ class Parser wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time, &$descQuery ) ); if ( $skip ) { - return $sk->makeLinkObj( $title ); + return $sk->link( $title ); } + # Get the file + $imagename = $title->getDBkey(); + if ( isset( $this->mFileCache[$imagename][$time] ) ) { + $file = $this->mFileCache[$imagename][$time]; + } else { + $file = wfFindFile( $title, $time ); + if ( count( $this->mFileCache ) > 1000 ) { + $this->mFileCache = array(); + } + $this->mFileCache[$imagename][$time] = $file; + } # Get parameter map - $file = wfFindFile( $title, $time ); $handler = $file ? $file->getHandler() : false; list( $paramMap, $mwArray ) = $this->getImageParams( $handler ); @@ -4510,13 +4338,14 @@ class Parser $params = array( 'frame' => array(), 'handler' => array(), 'horizAlign' => array(), 'vertAlign' => array() ); foreach( $parts as $part ) { + $part = trim( $part ); list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part ); $validated = false; if( isset( $paramMap[$magicName] ) ) { list( $type, $paramName ) = $paramMap[$magicName]; // Special case; width and height come in one variable together - if( $type == 'handler' && $paramName == 'width' ) { + if( $type === 'handler' && $paramName === 'width' ) { $m = array(); # (bug 13500) In both cases (width/height and width only), # permit trailing "px" for backward compatibility. @@ -4539,16 +4368,42 @@ class Parser } } // else no validation -- bug 13436 } else { - if ( $type == 'handler' ) { + if ( $type === 'handler' ) { # Validate handler parameter $validated = $handler->validateParam( $paramName, $value ); } else { # Validate internal parameters switch( $paramName ) { - case "manualthumb": - /// @fixme - possibly check validity here? - /// downstream behavior seems odd with missing manual thumbs. + case 'manualthumb': + case 'alt': + // @fixme - possibly check validity here for + // manualthumb? downstream behavior seems odd with + // missing manual thumbs. $validated = true; + $value = $this->stripAltText( $value, $holders ); + break; + case 'link': + $chars = self::EXT_LINK_URL_CLASS; + $prots = $this->mUrlProtocols; + if ( $value === '' ) { + $paramName = 'no-link'; + $value = true; + $validated = true; + } elseif ( preg_match( "/^$prots/", $value ) ) { + if ( preg_match( "/^($prots)$chars+$/", $value, $m ) ) { + $paramName = 'link-url'; + $this->mOutput->addExternalLink( $value ); + $validated = true; + } + } else { + $linkTitle = Title::newFromText( $value ); + if ( $linkTitle ) { + $paramName = 'link-title'; + $value = $linkTitle; + $this->mOutput->addLink( $linkTitle ); + $validated = true; + } + } break; default: // Most other things appear to be empty or numeric... @@ -4574,17 +4429,32 @@ class Parser $params['frame']['valign'] = key( $params['vertAlign'] ); } - # Strip bad stuff out of the alt text - $alt = $this->replaceLinkHoldersText( $caption ); + $params['frame']['caption'] = $caption; - # make sure there are no placeholders in thumbnail attributes - # that are later expanded to html- so expand them now and - # remove the tags - $alt = $this->mStripState->unstripBoth( $alt ); - $alt = Sanitizer::stripAllTags( $alt ); + $params['frame']['title'] = $this->stripAltText( $caption, $holders ); - $params['frame']['alt'] = $alt; - $params['frame']['caption'] = $caption; + # In the old days, [[Image:Foo|text...]] would set alt text. Later it + # came to also set the caption, ordinary text after the image -- which + # makes no sense, because that just repeats the text multiple times in + # screen readers. It *also* came to set the title attribute. + # + # Now that we have an alt attribute, we should not set the alt text to + # equal the caption: that's worse than useless, it just repeats the + # text. This is the framed/thumbnail case. If there's no caption, we + # use the unnamed parameter for alt text as well, just for the time be- + # ing, if the unnamed param is set and the alt param is not. + # + # For the future, we need to figure out if we want to tweak this more, + # e.g., introducing a title= parameter for the title; ignoring the un- + # named parameter entirely for images without a caption; adding an ex- + # plicit caption= parameter and preserving the old magic unnamed para- + # meter for BC; ... + if( $caption !== '' && !isset( $params['frame']['alt'] ) + && !isset( $params['frame']['framed'] ) + && !isset( $params['frame']['thumbnail'] ) + && !isset( $params['frame']['manualthumb'] ) ) { + $params['frame']['alt'] = $params['frame']['title']; + } wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) ); @@ -4598,6 +4468,25 @@ class Parser return $ret; } + + protected function stripAltText( $caption, $holders ) { + # Strip bad stuff out of the title (tooltip). We can't just use + # replaceLinkHoldersText() here, because if this function is called + # from replaceInternalLinks2(), mLinkHolders won't be up-to-date. + if ( $holders ) { + $tooltip = $holders->replaceText( $caption ); + } else { + $tooltip = $this->replaceLinkHoldersText( $caption ); + } + + # make sure there are no placeholders in thumbnail attributes + # that are later expanded to html- so expand them now and + # remove the tags + $tooltip = $this->mStripState->unstripBoth( $tooltip ); + $tooltip = Sanitizer::stripAllTags( $tooltip ); + + return $tooltip; + } /** * Set a flag in the output object indicating that the content is dynamic and @@ -4678,7 +4567,7 @@ class Parser $sectionParts = explode( '-', $section ); $sectionIndex = array_pop( $sectionParts ); foreach ( $sectionParts as $part ) { - if ( $part == 'T' ) { + if ( $part === 'T' ) { $flags |= self::PTD_FOR_INCLUSION; } } @@ -4695,14 +4584,14 @@ class Parser $targetLevel = 1000; } else { while ( $node ) { - if ( $node->getName() == 'h' ) { + if ( $node->getName() === 'h' ) { $bits = $node->splitHeading(); if ( $bits['i'] == $sectionIndex ) { $targetLevel = $bits['level']; break; } } - if ( $mode == 'replace' ) { + if ( $mode === 'replace' ) { $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); } $node = $node->getNextSibling(); @@ -4711,7 +4600,7 @@ class Parser if ( !$node ) { // Not found - if ( $mode == 'get' ) { + if ( $mode === 'get' ) { return $newText; } else { return $text; @@ -4720,21 +4609,21 @@ class Parser // Find the end of the section, including nested sections do { - if ( $node->getName() == 'h' ) { + if ( $node->getName() === 'h' ) { $bits = $node->splitHeading(); $curLevel = $bits['level']; if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) { break; } } - if ( $mode == 'get' ) { + if ( $mode === 'get' ) { $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); } $node = $node->getNextSibling(); } while ( $node ); // Write out the remainder (in replace mode only) - if ( $mode == 'replace' ) { + if ( $mode === 'replace' ) { // Output the replacement text // Add two newlines on -- trailing whitespace in $newText is conventionally // stripped by the editor, so we need both newlines to restore the paragraph gap @@ -4820,16 +4709,28 @@ class Parser * @return string */ public function getDefaultSort() { + global $wgCategoryPrefixedDefaultSortkey; if( $this->mDefaultSort !== false ) { return $this->mDefaultSort; + } elseif ($this->mTitle->getNamespace() == NS_CATEGORY || + !$wgCategoryPrefixedDefaultSortkey) { + return $this->mTitle->getText(); } else { - return $this->mTitle->getNamespace() == NS_CATEGORY - ? $this->mTitle->getText() - : $this->mTitle->getPrefixedText(); + return $this->mTitle->getPrefixedText(); } } /** + * Accessor for $mDefaultSort + * Unlike getDefaultSort(), will return false if none is set + * + * @return string or false + */ + public function getCustomDefaultSort() { + return $this->mDefaultSort; + } + + /** * Try to guess the section anchor name based on a wikitext fragment * presumably extracted from a heading, for example "Header" from * "== Header ==". @@ -4962,7 +4863,7 @@ class StripState { do { $oldText = $text; $text = $this->general->replace( $text ); - } while ( $text != $oldText ); + } while ( $text !== $oldText ); wfProfileOut( __METHOD__ ); return $text; } @@ -4972,7 +4873,7 @@ class StripState { do { $oldText = $text; $text = $this->nowiki->replace( $text ); - } while ( $text != $oldText ); + } while ( $text !== $oldText ); wfProfileOut( __METHOD__ ); return $text; } @@ -4983,7 +4884,7 @@ class StripState { $oldText = $text; $text = $this->general->replace( $text ); $text = $this->nowiki->replace( $text ); - } while ( $text != $oldText ); + } while ( $text !== $oldText ); wfProfileOut( __METHOD__ ); return $text; } @@ -4997,7 +4898,7 @@ class OnlyIncludeReplacer { var $output = ''; function replace( $matches ) { - if ( substr( $matches[1], -1 ) == "\n" ) { + if ( substr( $matches[1], -1 ) === "\n" ) { $this->output .= substr( $matches[1], 0, -1 ); } else { $this->output .= $matches[1]; diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index bf11da2e..7e61157a 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -35,9 +35,9 @@ class ParserCache { } else { $edit = ''; } - $pageid = intval( $article->getID() ); + $pageid = $article->getID(); $renderkey = (int)($action == 'render'); - $key = wfMemcKey( 'pcache', 'idhash', "$pageid-$renderkey!$hash$edit" ); + $key = wfMemcKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}{$edit}" ); return $key; } diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index 330ec446..5b8cd3ee 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -13,6 +13,7 @@ class ParserOptions var $mInterwikiMagic; # Interlanguage links are removed and returned in an array var $mAllowExternalImages; # Allow external images inline var $mAllowExternalImagesFrom; # If not, any exception? + var $mEnableImageWhitelist; # If not or it doesn't match, should we check an on-wiki whitelist? var $mSkin; # Reference to the preferred skin var $mDateFormat; # Date format index var $mEditSection; # Create "edit section" links @@ -29,6 +30,7 @@ class ParserOptions var $mTemplateCallback; # Callback for template fetching var $mEnableLimitReport; # Enable limit report in an HTML comment on output var $mTimestamp; # Timestamp used for {{CURRENTDAY}} etc. + var $mExternalLinkTarget; # Target attribute for external links var $mUser; # Stored user object, just used to initialise the skin @@ -37,6 +39,7 @@ class ParserOptions function getInterwikiMagic() { return $this->mInterwikiMagic; } function getAllowExternalImages() { return $this->mAllowExternalImages; } function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; } + function getEnableImageWhitelist() { return $this->mEnableImageWhitelist; } function getEditSection() { return $this->mEditSection; } function getNumberHeadings() { return $this->mNumberHeadings; } function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; } @@ -49,6 +52,8 @@ class ParserOptions function getRemoveComments() { return $this->mRemoveComments; } function getTemplateCallback() { return $this->mTemplateCallback; } function getEnableLimitReport() { return $this->mEnableLimitReport; } + function getCleanSignatures() { return $this->mCleanSignatures; } + function getExternalLinkTarget() { return $this->mExternalLinkTarget; } function getSkin() { if ( !isset( $this->mSkin ) ) { @@ -76,6 +81,7 @@ class ParserOptions function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); } function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); } + function setEnableImageWhitelist( $x ) { return wfSetVar( $this->mEnableImageWhitelist, $x ); } function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); } function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); } function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); } @@ -91,6 +97,8 @@ class ParserOptions function setTemplateCallback( $x ) { return wfSetVar( $this->mTemplateCallback, $x ); } function enableLimitReport( $x = true ) { return wfSetVar( $this->mEnableLimitReport, $x ); } function setTimestamp( $x ) { return wfSetVar( $this->mTimestamp, $x ); } + function setCleanSignatures( $x ) { return wfSetVar( $this->mCleanSignatures, $x ); } + function setExternalLinkTarget( $x ) { return wfSetVar( $this->mExternalLinkTarget, $x ); } function __construct( $user = null ) { $this->initialiseFromUser( $user ); @@ -107,8 +115,9 @@ class ParserOptions /** Get user options */ function initialiseFromUser( $userInput ) { global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages; - global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize; - global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth; + global $wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion, $wgMaxArticleSize; + global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth, $wgCleanSignatures; + global $wgExternalLinkTarget; $fname = 'ParserOptions::initialiseFromUser'; wfProfileIn( $fname ); if ( !$userInput ) { @@ -129,6 +138,7 @@ class ParserOptions $this->mInterwikiMagic = $wgInterwikiMagic; $this->mAllowExternalImages = $wgAllowExternalImages; $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom; + $this->mEnableImageWhitelist = $wgEnableImageWhitelist; $this->mSkin = null; # Deferred $this->mDateFormat = null; # Deferred $this->mEditSection = true; @@ -144,6 +154,8 @@ class ParserOptions $this->mRemoveComments = true; $this->mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' ); $this->mEnableLimitReport = false; + $this->mCleanSignatures = $wgCleanSignatures; + $this->mExternalLinkTarget = $wgExternalLinkTarget; wfProfileOut( $fname ); } } diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index f98d5641..35cb5c92 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -5,25 +5,26 @@ */ class ParserOutput { - var $mText, # The output text - $mLanguageLinks, # List of the full text of language links, in the order they appear - $mCategories, # Map of category names to sort keys - $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} - $mCacheTime, # Time when this object was generated, or -1 for uncacheable. Used in ParserCache. - $mVersion, # Compatibility check - $mTitleText, # title text of the chosen language variant - $mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken. - $mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken. - $mTemplateIds, # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken. - $mImages, # DB keys of the images used, in the array key only - $mExternalLinks, # External link URLs, in the key only - $mNewSection, # Show a new section link? - $mNoGallery, # No gallery on category page? (__NOGALLERY__) - $mHeadItems, # Items to put in the <head> section - $mOutputHooks, # Hook tags as per $wgParserOutputHooks - $mWarnings, # Warning text to be returned to the user. Wikitext formatted, in the key only - $mSections, # Table of contents - $mProperties; # Name/value pairs to be cached in the DB + var $mText, # The output text + $mLanguageLinks, # List of the full text of language links, in the order they appear + $mCategories, # Map of category names to sort keys + $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} + $mTitleText, # title text of the chosen language variant + $mCacheTime = '', # Time when this object was generated, or -1 for uncacheable. Used in ParserCache. + $mVersion = Parser::VERSION, # Compatibility check + $mLinks = array(), # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken. + $mTemplates = array(), # 2-D map of NS/DBK to ID for the template references. ID=zero for broken. + $mTemplateIds = array(), # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken. + $mImages = array(), # DB keys of the images used, in the array key only + $mExternalLinks = array(), # External link URLs, in the key only + $mNewSection = false, # Show a new section link? + $mNoGallery = false, # No gallery on category page? (__NOGALLERY__) + $mHeadItems = array(), # Items to put in the <head> section + $mOutputHooks = array(), # Hook tags as per $wgParserOutputHooks + $mWarnings = array(), # Warning text to be returned to the user. Wikitext formatted, in the key only + $mSections = array(), # Table of contents + $mProperties = array(); # Name/value pairs to be cached in the DB + private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change. /** * Overridden title for display @@ -37,21 +38,7 @@ class ParserOutput $this->mLanguageLinks = $languageLinks; $this->mCategories = $categoryLinks; $this->mContainsOldMagic = $containsOldMagic; - $this->mCacheTime = ''; - $this->mVersion = Parser::VERSION; $this->mTitleText = $titletext; - $this->mSections = array(); - $this->mLinks = array(); - $this->mTemplates = array(); - $this->mImages = array(); - $this->mExternalLinks = array(); - $this->mNewSection = false; - $this->mNoGallery = false; - $this->mHeadItems = array(); - $this->mTemplateIds = array(); - $this->mOutputHooks = array(); - $this->mWarnings = array(); - $this->mProperties = array(); } function getText() { return $this->mText; } @@ -69,6 +56,7 @@ class ParserOutput function getSubtitle() { return $this->mSubtitle; } function getOutputHooks() { return (array)$this->mOutputHooks; } function getWarnings() { return array_keys( $this->mWarnings ); } + function getIndexPolicy() { return $this->mIndexPolicy; } function containsOldMagic() { return $this->mContainsOldMagic; } function setText( $text ) { return wfSetVar( $this->mText, $text ); } @@ -78,6 +66,7 @@ class ParserOutput function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } function setTitleText( $t ) { return wfSetVar( $this->mTitleText, $t ); } function setSections( $toc ) { return wfSetVar( $this->mSections, $toc ); } + function setIndexPolicy( $policy ) { return wfSetVar( $this->mIndexPolicy, $policy ); } function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; } function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; } @@ -98,6 +87,14 @@ class ParserOutput function addLink( $title, $id = null ) { $ns = $title->getNamespace(); $dbk = $title->getDBkey(); + if ( $ns == NS_MEDIA ) { + // Normalize this pseudo-alias if it makes it down here... + $ns = NS_FILE; + } elseif( $ns == NS_SPECIAL ) { + // We don't record Special: links currently + // It might actually be wise to, but we'd need to do some normalization. + return; + } if ( !isset( $this->mLinks[$ns] ) ) { $this->mLinks[$ns] = array(); } diff --git a/includes/parser/Parser_DiffTest.php b/includes/parser/Parser_DiffTest.php index be3702cf..608c883a 100644 --- a/includes/parser/Parser_DiffTest.php +++ b/includes/parser/Parser_DiffTest.php @@ -6,6 +6,7 @@ class Parser_DiffTest { var $parsers, $conf; + var $shortOutput = false; var $dfUniqPrefix; @@ -28,6 +29,9 @@ class Parser_DiffTest $doneHook = true; $wgHooks['ParserClearState'][] = array( $this, 'onClearState' ); } + if ( isset( $this->conf['shortOutput'] ) ) { + $this->shortOutput = $this->conf['shortOutput']; + } foreach ( $this->conf['parsers'] as $i => $parserConf ) { if ( !is_array( $parserConf ) ) { @@ -65,13 +69,37 @@ class Parser_DiffTest $lastResult = $currentResult; } if ( $mismatch ) { - throw new MWException( "Parser_DiffTest: results mismatch on call to $name\n" . - 'Arguments: ' . var_export( $args, true ) . "\n" . - 'Results: ' . var_export( $results, true ) . "\n" ); + if ( count( $results ) == 2 ) { + $resultsList = array(); + foreach ( $this->parsers as $i => $parser ) { + $resultsList[] = var_export( $results[$i], true ); + } + $diff = wfDiff( $resultsList[0], $resultsList[1] ); + } else { + $diff = '[too many parsers]'; + } + $msg = "Parser_DiffTest: results mismatch on call to $name\n"; + if ( !$this->shortOutput ) { + $msg .= 'Arguments: ' . $this->formatArray( $args ) . "\n"; + } + $msg .= 'Results: ' . $this->formatArray( $results ) . "\n" . + "Diff: $diff\n"; + throw new MWException( $msg ); } return $lastResult; } + function formatArray( $array ) { + if ( $this->shortOutput ) { + foreach ( $array as $key => $value ) { + if ( $value instanceof ParserOutput ) { + $array[$key] = "ParserOutput: {$value->getText()}"; + } + } + } + return var_export( $array, true ); + } + function setFunctionHook( $id, $callback, $flags = 0 ) { $this->init(); foreach ( $this->parsers as $i => $parser ) { diff --git a/includes/parser/Parser_LinkHooks.php b/includes/parser/Parser_LinkHooks.php new file mode 100644 index 00000000..2b306933 --- /dev/null +++ b/includes/parser/Parser_LinkHooks.php @@ -0,0 +1,315 @@ +<?php +/** + * Parser with LinkHooks experiment + * @ingroup Parser + */ +class Parser_LinkHooks extends Parser +{ + /** + * Update this version number when the ParserOutput format + * changes in an incompatible way, so the parser cache + * can automatically discard old data. + */ + const VERSION = '1.6.4'; + + # Flags for Parser::setLinkHook + # Also available as global constants from Defines.php + const SLH_PATTERN = 1; + + # Constants needed for external link processing + # Everything except bracket, space, or control characters + const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]'; + const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+) + \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx'; + + /**#@+ + * @private + */ + # Persistent: + var $mLinkHooks; + + /**#@-*/ + + /** + * Constructor + * + * @public + */ + function __construct( $conf = array() ) { + parent::__construct( $conf ); + $this->mLinkHooks = array(); + } + + /** + * Do various kinds of initialisation on the first call of the parser + */ + function firstCallInit() { + parent::__construct(); + if ( !$this->mFirstCall ) { + return; + } + $this->mFirstCall = false; + + wfProfileIn( __METHOD__ ); + + $this->setHook( 'pre', array( $this, 'renderPreTag' ) ); + CoreParserFunctions::register( $this ); + CoreLinkFunctions::register( $this ); + $this->initialiseVariables(); + + wfRunHooks( 'ParserFirstCallInit', array( &$this ) ); + wfProfileOut( __METHOD__ ); + } + + /** + * Create a link hook, e.g. [[Namepsace:...|display}} + * The callback function should have the form: + * function myLinkCallback( $parser, $holders, $markers, + * Title $title, $titleText, &$sortText = null, &$leadingColon = false ) { ... } + * + * Or with SLH_PATTERN: + * function myLinkCallback( $parser, $holders, $markers, ) + * &$titleText, &$sortText = null, &$leadingColon = false ) { ... } + * + * The callback may either return a number of different possible values: + * String) Text result of the link + * True) (Treat as link) Parse the link according to normal link rules + * False) (Bad link) Just output the raw wikitext (You may modify the text first) + * + * @public + * + * @param integer|string $ns The Namespace ID or regex pattern if SLH_PATTERN is set + * @param mixed $callback The callback function (and object) to use + * @param integer $flags a combination of the following flags: + * SLH_PATTERN Use a regex link pattern rather than a namespace + * + * @return The old callback function for this name, if any + */ + function setLinkHook( $ns, $callback, $flags = 0 ) { + if( $flags & SLH_PATTERN && !is_string($ns) ) + throw new MWException( __METHOD__.'() expecting a regex string pattern.' ); + elseif( $flags | ~SLH_PATTERN && !is_int($ns) ) + throw new MWException( __METHOD__.'() expecting a namespace index.' ); + $oldVal = isset( $this->mLinkHooks[$ns] ) ? $this->mLinkHooks[$ns][0] : null; + $this->mLinkHooks[$ns] = array( $callback, $flags ); + return $oldVal; + } + + /** + * Get all registered link hook identifiers + * + * @return array + */ + function getLinkHooks() { + return array_keys( $this->mLinkHooks ); + } + + /** + * Process [[ ]] wikilinks + * @return LinkHolderArray + * + * @private + */ + function replaceInternalLinks2( &$s ) { + global $wgContLang; + + wfProfileIn( __METHOD__ ); + + wfProfileIn( __METHOD__.'-setup' ); + static $tc = FALSE, $titleRegex;//$e1, $e1_img; + if( !$tc ) { + # the % is needed to support urlencoded titles as well + $tc = Title::legalChars() . '#%'; + # Match a link having the form [[namespace:link|alternate]]trail + //$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; + # Match cases where there is no "]]", which might still be images + //$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; + # Match a valid plain title + $titleRegex = "/^([{$tc}]+)$/sD"; + } + + $sk = $this->mOptions->getSkin(); + $holders = new LinkHolderArray( $this ); + + if( is_null( $this->mTitle ) ) { + wfProfileOut( __METHOD__ ); + wfProfileOut( __METHOD__.'-setup' ); + throw new MWException( __METHOD__.": \$this->mTitle is null\n" ); + } + $nottalk = !$this->mTitle->isTalkPage(); + + if($wgContLang->hasVariants()) { + $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText()); + } else { + $selflink = array($this->mTitle->getPrefixedText()); + } + wfProfileOut( __METHOD__.'-setup' ); + + $offset = 0; + $offsetStack = array(); + $markers = new LinkMarkerReplacer( $this, $holders, array( &$this, 'replaceInternalLinksCallback' ) ); + while( true ) { + $startBracketOffset = strpos( $s, '[[', $offset ); + $endBracketOffset = strpos( $s, ']]', $offset ); + # Finish when there are no more brackets + if( $startBracketOffset === false && $endBracketOffset === false ) break; + # Determine if the bracket is a starting or ending bracket + # When we find both, use the first one + elseif( $startBracketOffset !== false && $endBracketOffset !== false ) + $isStart = $startBracketOffset <= $endBracketOffset; + # When we only found one, check which it is + else $isStart = $startBracketOffset !== false; + $bracketOffset = $isStart ? $startBracketOffset : $endBracketOffset; + if( $isStart ) { + /** Opening bracket **/ + # Just push our current offset in the string onto the stack + $offsetStack[] = $startBracketOffset; + } else { + /** Closing bracket **/ + # Pop the start pos for our current link zone off the stack + $startBracketOffset = array_pop($offsetStack); + # Just to clean up the code, lets place offsets on the outer ends + $endBracketOffset += 2; + + # Only do logic if we actually have a opening bracket for this + if( isset($startBracketOffset) ) { + # Extract text inside the link + @list( $titleText, $paramText ) = explode('|', + substr($s, $startBracketOffset+2, $endBracketOffset-$startBracketOffset-4), 2); + # Create markers only for valid links + if( preg_match( $titleRegex, $titleText ) ) { + # Store the text for the marker + $marker = $markers->addMarker($titleText, $paramText); + # Replace the current link with the marker + $s = substr($s,0,$startBracketOffset). + $marker. + substr($s, $endBracketOffset); + # We have modified $s, because of this we need to set the + # offset manually since the end position is different now + $offset = $startBracketOffset+strlen($marker); + continue; + } + # ToDo: Some LinkHooks may allow recursive links inside of + # the link text, create a regex that also matches our + # <!-- LINKMARKER ### --> sequence in titles + # ToDO: Some LinkHooks use patterns rather than namespaces + # these need to be tested at this point here + } + + } + # Bump our offset to after our current bracket + $offset = $bracketOffset+2; + } + + + # Now expand our tree + wfProfileIn( __METHOD__.'-expand' ); + $s = $markers->expand( $s ); + wfProfileOut( __METHOD__.'-expand' ); + + wfProfileOut( __METHOD__ ); + return $holders; + } + + function replaceInternalLinksCallback( $parser, $holders, $markers, $titleText, $paramText ) { + wfProfileIn( __METHOD__ ); + $wt = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]"; + wfProfileIn( __METHOD__."-misc" ); + # Don't allow internal links to pages containing + # PROTO: where PROTO is a valid URL protocol; these + # should be external links. + if( preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $titleText) ) { + wfProfileOut( __METHOD__ ); + return $wt; + } + + # Make subpage if necessary + if( $this->areSubpagesAllowed() ) { + $titleText = $this->maybeDoSubpageLink( $titleText, $paramText ); + } + + # Check for a leading colon and strip it if it is there + $leadingColon = $titleText[0] == ':'; + if( $leadingColon ) $titleText = substr( $titleText, 1 ); + + wfProfileOut( __METHOD__."-misc" ); + # Make title object + wfProfileIn( __METHOD__."-title" ); + $title = Title::newFromText( $this->mStripState->unstripNoWiki($titleText) ); + if( !$title ) { + wfProfileOut( __METHOD__."-title" ); + wfProfileOut( __METHOD__ ); + return $wt; + } + $ns = $title->getNamespace(); + wfProfileOut( __METHOD__."-title" ); + + # Default for Namespaces is a default link + # ToDo: Default for patterns is plain wikitext + $return = true; + if( isset($this->mLinkHooks[$ns]) ) { + list( $callback, $flags ) = $this->mLinkHooks[$ns]; + if( $flags & SLH_PATTERN ) { + $args = array( $parser, $holders, $markers, $titleText, &$paramText, &$leadingColon ); + } else { + $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon ); + } + # Workaround for PHP bug 35229 and similar + if ( !is_callable( $callback ) ) { + throw new MWException( "Tag hook for $name is not callable\n" ); + } + $return = call_user_func_array( $callback, $args ); + } + if( $return === true ) { + # True (treat as plain link) was returned, call the defaultLinkHook + $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon ); + $return = call_user_func_array( array( 'CoreLinkFunctions', 'defaultLinkHook' ), $args ); + } + if( $return === false ) { + # False (no link) was returned, output plain wikitext + # Build it again as the hook is allowed to modify $paramText + return isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]"; + } + # Content was returned, return it + return $return; + } + +} + +class LinkMarkerReplacer { + + protected $markers, $nextId, $parser, $holders, $callback; + + function __construct( $parser, $holders, $callback ) { + $this->nextId = 0; + $this->markers = array(); + $this->parser = $parser; + $this->holders = $holders; + $this->callback = $callback; + } + + function addMarker($titleText, $paramText) { + $id = $this->nextId++; + $this->markers[$id] = array( $titleText, $paramText ); + return "<!-- LINKMARKER $id -->"; + } + + function findMarker( $string ) { + return (bool) preg_match('/<!-- LINKMARKER [0-9]+ -->/', $string ); + } + + function expand( $string ) { + return StringUtils::delimiterReplaceCallback( "<!-- LINKMARKER ", " -->", array( &$this, 'callback' ), $string ); + } + + function callback( $m ) { + $id = intval($m[1]); + if( !array_key_exists($id, $this->markers) ) return $m[0]; + $args = $this->markers[$id]; + array_unshift( $args, $this ); + array_unshift( $args, $this->holders ); + array_unshift( $args, $this->parser ); + return call_user_func_array( $this->callback, $args ); + } + +} diff --git a/includes/parser/Parser_OldPP.php b/includes/parser/Parser_OldPP.php deleted file mode 100644 index 487d3ffd..00000000 --- a/includes/parser/Parser_OldPP.php +++ /dev/null @@ -1,4944 +0,0 @@ -<?php -/** - * Parser with old preprocessor - * @ingroup Parser - */ -class Parser_OldPP -{ - /** - * Update this version number when the ParserOutput format - * changes in an incompatible way, so the parser cache - * can automatically discard old data. - */ - const VERSION = '1.6.4'; - - # Flags for Parser::setFunctionHook - # Also available as global constants from Defines.php - const SFH_NO_HASH = 1; - - # Constants needed for external link processing - # Everything except bracket, space, or control characters - const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]'; - const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/S'; - - // State constants for the definition list colon extraction - const COLON_STATE_TEXT = 0; - const COLON_STATE_TAG = 1; - const COLON_STATE_TAGSTART = 2; - const COLON_STATE_CLOSETAG = 3; - const COLON_STATE_TAGSLASH = 4; - const COLON_STATE_COMMENT = 5; - const COLON_STATE_COMMENTDASH = 6; - const COLON_STATE_COMMENTDASHDASH = 7; - - // Allowed values for $this->mOutputType - // Parameter to startExternalParse(). - const OT_HTML = 1; - const OT_WIKI = 2; - const OT_PREPROCESS = 3; - const OT_MSG = 4; - - /**#@+ - * @private - */ - # Persistent: - var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables, - $mImageParams, $mImageParamsMagicArray, $mExtLinkBracketedRegex; - - # Cleared with clearState(): - var $mOutput, $mAutonumber, $mDTopen, $mStripState; - var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; - var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix; - var $mIncludeSizes, $mDefaultSort; - var $mTemplates, // cache of already loaded templates, avoids - // multiple SQL queries for the same string - $mTemplatePath; // stores an unsorted hash of all the templates already loaded - // in this path. Used for loop detection. - - # Temporary - # These are variables reset at least once per parse regardless of $clearState - var $mOptions, // ParserOptions object - $mTitle, // Title context, used for self-link rendering and similar things - $mOutputType, // Output type, one of the OT_xxx constants - $ot, // Shortcut alias, see setOutputType() - $mRevisionId, // ID to display in {{REVISIONID}} tags - $mRevisionTimestamp, // The timestamp of the specified revision ID - $mRevIdForTs; // The revision ID which was used to fetch the timestamp - - /**#@-*/ - - /** - * Constructor - * - * @public - */ - function __construct( $conf = array() ) { - $this->mTagHooks = array(); - $this->mTransparentTagHooks = array(); - $this->mFunctionHooks = array(); - $this->mFunctionSynonyms = array( 0 => array(), 1 => array() ); - $this->mFirstCall = true; - $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'. - '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S'; - } - - /** - * Do various kinds of initialisation on the first call of the parser - */ - function firstCallInit() { - if ( !$this->mFirstCall ) { - return; - } - $this->mFirstCall = false; - - wfProfileIn( __METHOD__ ); - global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions; - - $this->setHook( 'pre', array( $this, 'renderPreTag' ) ); - - # Syntax for arguments (see self::setFunctionHook): - # "name for lookup in localized magic words array", - # function callback, - # optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...} - # instead of {{#int:...}}) - $this->setFunctionHook( 'int', array( 'CoreParserFunctions', 'intFunction' ), SFH_NO_HASH ); - $this->setFunctionHook( 'ns', array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH ); - $this->setFunctionHook( 'urlencode', array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH ); - $this->setFunctionHook( 'lcfirst', array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH ); - $this->setFunctionHook( 'ucfirst', array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH ); - $this->setFunctionHook( 'lc', array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH ); - $this->setFunctionHook( 'uc', array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH ); - $this->setFunctionHook( 'localurl', array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH ); - $this->setFunctionHook( 'localurle', array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH ); - $this->setFunctionHook( 'fullurl', array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH ); - $this->setFunctionHook( 'fullurle', array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH ); - $this->setFunctionHook( 'formatnum', array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH ); - $this->setFunctionHook( 'grammar', array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH ); - $this->setFunctionHook( 'plural', array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofpages', array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofusers', array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberoffiles', array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofadmins', array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH ); - $this->setFunctionHook( 'numberofedits', array( 'CoreParserFunctions', 'numberofedits' ), SFH_NO_HASH ); - $this->setFunctionHook( 'language', array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH ); - $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH ); - $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH ); - $this->setFunctionHook( 'anchorencode', array( 'CoreParserFunctions', 'anchorencode' ), SFH_NO_HASH ); - $this->setFunctionHook( 'special', array( 'CoreParserFunctions', 'special' ) ); - $this->setFunctionHook( 'defaultsort', array( 'CoreParserFunctions', 'defaultsort' ), SFH_NO_HASH ); - $this->setFunctionHook( 'filepath', array( 'CoreParserFunctions', 'filepath' ), SFH_NO_HASH ); - - if ( $wgAllowDisplayTitle ) { - $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH ); - } - if ( $wgAllowSlowParserFunctions ) { - $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH ); - } - - $this->initialiseVariables(); - - wfRunHooks( 'ParserFirstCallInit', array( &$this ) ); - wfProfileOut( __METHOD__ ); - } - - /** - * Clear Parser state - * - * @private - */ - function clearState() { - wfProfileIn( __METHOD__ ); - if ( $this->mFirstCall ) { - $this->firstCallInit(); - } - $this->mOutput = new ParserOutput; - $this->mAutonumber = 0; - $this->mLastSection = ''; - $this->mDTopen = false; - $this->mIncludeCount = array(); - $this->mStripState = new StripState; - $this->mArgStack = array(); - $this->mInPre = false; - $this->mInterwikiLinkHolders = array( - 'texts' => array(), - 'titles' => array() - ); - $this->mLinkHolders = array( - 'namespaces' => array(), - 'dbkeys' => array(), - 'queries' => array(), - 'texts' => array(), - 'titles' => array() - ); - $this->mRevisionTimestamp = $this->mRevisionId = null; - - /** - * Prefix for temporary replacement strings for the multipass parser. - * \x07 should never appear in input as it's disallowed in XML. - * Using it at the front also gives us a little extra robustness - * since it shouldn't match when butted up against identifier-like - * string constructs. - */ - $this->mUniqPrefix = "\x07UNIQ" . self::getRandomString(); - - # Clear these on every parse, bug 4549 - $this->mTemplates = array(); - $this->mTemplatePath = array(); - - $this->mShowToc = true; - $this->mForceTocPosition = false; - $this->mIncludeSizes = array( - 'pre-expand' => 0, - 'post-expand' => 0, - 'arg' => 0 - ); - $this->mDefaultSort = false; - - wfRunHooks( 'ParserClearState', array( &$this ) ); - wfProfileOut( __METHOD__ ); - } - - function setOutputType( $ot ) { - $this->mOutputType = $ot; - // Shortcut alias - $this->ot = array( - 'html' => $ot == self::OT_HTML, - 'wiki' => $ot == self::OT_WIKI, - 'msg' => $ot == self::OT_MSG, - 'pre' => $ot == self::OT_PREPROCESS, - ); - } - - /** - * Accessor for mUniqPrefix. - * - * @public - */ - function uniqPrefix() { - return $this->mUniqPrefix; - } - - /** - * Convert wikitext to HTML - * Do not call this function recursively. - * - * @param string $text Text we want to parse - * @param Title &$title A title object - * @param array $options - * @param boolean $linestart - * @param boolean $clearState - * @param int $revid number to pass in {{REVISIONID}} - * @return ParserOutput a ParserOutput - */ - public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) { - /** - * First pass--just handle <nowiki> sections, pass the rest off - * to internalParse() which does all the real work. - */ - - global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang; - $fname = 'Parser::parse-' . wfGetCaller(); - wfProfileIn( __METHOD__ ); - wfProfileIn( $fname ); - - if ( $clearState ) { - $this->clearState(); - } - - $this->mOptions = $options; - $this->mTitle =& $title; - $oldRevisionId = $this->mRevisionId; - $oldRevisionTimestamp = $this->mRevisionTimestamp; - if( $revid !== null ) { - $this->mRevisionId = $revid; - $this->mRevisionTimestamp = null; - } - $this->setOutputType( self::OT_HTML ); - wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->strip( $text, $this->mStripState ); - wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->internalParse( $text ); - $text = $this->mStripState->unstripGeneral( $text ); - - # Clean up special characters, only run once, next-to-last before doBlockLevels - $fixtags = array( - # french spaces, last one Guillemet-left - # only if there is something before the space - '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 \\2', - # french spaces, Guillemet-right - '/(\\302\\253) /' => '\\1 ', - ); - $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text ); - - # only once and last - $text = $this->doBlockLevels( $text, $linestart ); - - $this->replaceLinkHolders( $text ); - - # the position of the parserConvert() call should not be changed. it - # assumes that the links are all replaced and the only thing left - # is the <nowiki> mark. - # Side-effects: this calls $this->mOutput->setTitleText() - $text = $wgContLang->parserConvert( $text, $this ); - - $text = $this->mStripState->unstripNoWiki( $text ); - - wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) ); - -//!JF Move to its own function - - $uniq_prefix = $this->mUniqPrefix; - $matches = array(); - $elements = array_keys( $this->mTransparentTagHooks ); - $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); - - foreach( $matches as $marker => $data ) { - list( $element, $content, $params, $tag ) = $data; - $tagName = strtolower( $element ); - if( isset( $this->mTransparentTagHooks[$tagName] ) ) { - $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], - array( $content, $params, $this ) ); - } else { - $output = $tag; - } - $this->mStripState->general->setPair( $marker, $output ); - } - $text = $this->mStripState->unstripGeneral( $text ); - - $text = Sanitizer::normalizeCharReferences( $text ); - - if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) { - $text = self::tidy($text); - } else { - # attempt to sanitize at least some nesting problems - # (bug #2702 and quite a few others) - $tidyregs = array( - # ''Something [http://www.cool.com cool''] --> - # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a> - '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' => - '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9', - # fix up an anchor inside another anchor, only - # at least for a single single nested link (bug 3695) - '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' => - '\\1\\2</a>\\3</a>\\1\\4</a>', - # fix div inside inline elements- doBlockLevels won't wrap a line which - # contains a div, so fix it up here; replace - # div with escaped text - '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' => - '\\1\\3<div\\5>\\6</div>\\8\\9', - # remove empty italic or bold tag pairs, some - # introduced by rules above - '/<([bi])><\/\\1>/' => '', - ); - - $text = preg_replace( - array_keys( $tidyregs ), - array_values( $tidyregs ), - $text ); - } - - wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); - - # Information on include size limits, for the benefit of users who try to skirt them - if ( $this->mOptions->getEnableLimitReport() ) { - $max = $this->mOptions->getMaxIncludeSize(); - $limitReport = - "Pre-expand include size: {$this->mIncludeSizes['pre-expand']}/$max bytes\n" . - "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" . - "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n"; - wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) ); - $text .= "<!-- \n$limitReport-->\n"; - } - $this->mOutput->setText( $text ); - $this->mRevisionId = $oldRevisionId; - $this->mRevisionTimestamp = $oldRevisionTimestamp; - wfProfileOut( $fname ); - wfProfileOut( __METHOD__ ); - - return $this->mOutput; - } - - /** - * Recursive parser entry point that can be called from an extension tag - * hook. - */ - function recursiveTagParse( $text ) { - wfProfileIn( __METHOD__ ); - wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->strip( $text, $this->mStripState ); - wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->internalParse( $text ); - wfProfileOut( __METHOD__ ); - return $text; - } - - /** - * Expand templates and variables in the text, producing valid, static wikitext. - * Also removes comments. - */ - function preprocess( $text, $title, $options, $revid = null ) { - wfProfileIn( __METHOD__ ); - $this->clearState(); - $this->setOutputType( self::OT_PREPROCESS ); - $this->mOptions = $options; - $this->mTitle = $title; - if( $revid !== null ) { - $this->mRevisionId = $revid; - } - wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->strip( $text, $this->mStripState ); - wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); - if ( $this->mOptions->getRemoveComments() ) { - $text = Sanitizer::removeHTMLcomments( $text ); - } - $text = $this->replaceVariables( $text ); - $text = $this->mStripState->unstripBoth( $text ); - wfProfileOut( __METHOD__ ); - return $text; - } - - /** - * Get a random string - * - * @private - * @static - */ - function getRandomString() { - return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff)); - } - - function &getTitle() { return $this->mTitle; } - function getOptions() { return $this->mOptions; } - function getRevisionId() { return $this->mRevisionId; } - - function getFunctionLang() { - global $wgLang, $wgContLang; - return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang; - } - - /** - * Replaces all occurrences of HTML-style comments and the given tags - * in the text with a random marker and returns teh next text. The output - * parameter $matches will be an associative array filled with data in - * the form: - * 'UNIQ-xxxxx' => array( - * 'element', - * 'tag content', - * array( 'param' => 'x' ), - * '<element param="x">tag content</element>' ) ) - * - * @param $elements list of element names. Comments are always extracted. - * @param $text Source text string. - * @param $uniq_prefix - * - * @public - * @static - */ - function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){ - static $n = 1; - $stripped = ''; - $matches = array(); - - $taglist = implode( '|', $elements ); - $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i"; - - while ( '' != $text ) { - $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE ); - $stripped .= $p[0]; - if( count( $p ) < 5 ) { - break; - } - if( count( $p ) > 5 ) { - // comment - $element = $p[4]; - $attributes = ''; - $close = ''; - $inside = $p[5]; - } else { - // tag - $element = $p[1]; - $attributes = $p[2]; - $close = $p[3]; - $inside = $p[4]; - } - - $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07"; - $stripped .= $marker; - - if ( $close === '/>' ) { - // Empty element tag, <tag /> - $content = null; - $text = $inside; - $tail = null; - } else { - if( $element == '!--' ) { - $end = '/(-->)/'; - } else { - $end = "/(<\\/$element\\s*>)/i"; - } - $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE ); - $content = $q[0]; - if( count( $q ) < 3 ) { - # No end tag -- let it run out to the end of the text. - $tail = ''; - $text = ''; - } else { - $tail = $q[1]; - $text = $q[2]; - } - } - - $matches[$marker] = array( $element, - $content, - Sanitizer::decodeTagAttributes( $attributes ), - "<$element$attributes$close$content$tail" ); - } - return $stripped; - } - - /** - * Strips and renders nowiki, pre, math, hiero - * If $render is set, performs necessary rendering operations on plugins - * Returns the text, and fills an array with data needed in unstrip() - * - * @param StripState $state - * - * @param bool $stripcomments when set, HTML comments <!-- like this --> - * will be stripped in addition to other tags. This is important - * for section editing, where these comments cause confusion when - * counting the sections in the wikisource - * - * @param array dontstrip contains tags which should not be stripped; - * used to prevent stipping of <gallery> when saving (fixes bug 2700) - * - * @private - */ - function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) { - global $wgContLang; - wfProfileIn( __METHOD__ ); - $render = ($this->mOutputType == self::OT_HTML); - - $uniq_prefix = $this->mUniqPrefix; - $commentState = new ReplacementArray; - $nowikiItems = array(); - $generalItems = array(); - - $elements = array_merge( - array( 'nowiki', 'gallery' ), - array_keys( $this->mTagHooks ) ); - global $wgRawHtml; - if( $wgRawHtml ) { - $elements[] = 'html'; - } - if( $this->mOptions->getUseTeX() ) { - $elements[] = 'math'; - } - - # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700) - foreach ( $elements AS $k => $v ) { - if ( !in_array ( $v , $dontstrip ) ) continue; - unset ( $elements[$k] ); - } - - $matches = array(); - $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); - - foreach( $matches as $marker => $data ) { - list( $element, $content, $params, $tag ) = $data; - if( $render ) { - $tagName = strtolower( $element ); - wfProfileIn( __METHOD__."-render-$tagName" ); - switch( $tagName ) { - case '!--': - // Comment - if( substr( $tag, -3 ) == '-->' ) { - $output = $tag; - } else { - // Unclosed comment in input. - // Close it so later stripping can remove it - $output = "$tag-->"; - } - break; - case 'html': - if( $wgRawHtml ) { - $output = $content; - break; - } - // Shouldn't happen otherwise. :) - case 'nowiki': - $output = Xml::escapeTagsOnly( $content ); - break; - case 'math': - $output = $wgContLang->armourMath( - MathRenderer::renderMath( $content, $params ) ); - break; - case 'gallery': - $output = $this->renderImageGallery( $content, $params ); - break; - default: - if( isset( $this->mTagHooks[$tagName] ) ) { - $output = call_user_func_array( $this->mTagHooks[$tagName], - array( $content, $params, $this ) ); - } else { - throw new MWException( "Invalid call hook $element" ); - } - } - wfProfileOut( __METHOD__."-render-$tagName" ); - } else { - // Just stripping tags; keep the source - $output = $tag; - } - - // Unstrip the output, to support recursive strip() calls - $output = $state->unstripBoth( $output ); - - if( !$stripcomments && $element == '!--' ) { - $commentState->setPair( $marker, $output ); - } elseif ( $element == 'html' || $element == 'nowiki' ) { - $nowikiItems[$marker] = $output; - } else { - $generalItems[$marker] = $output; - } - } - # Add the new items to the state - # We do this after the loop instead of during it to avoid slowing - # down the recursive unstrip - $state->nowiki->mergeArray( $nowikiItems ); - $state->general->mergeArray( $generalItems ); - - # Unstrip comments unless explicitly told otherwise. - # (The comments are always stripped prior to this point, so as to - # not invoke any extension tags / parser hooks contained within - # a comment.) - if ( !$stripcomments ) { - // Put them all back and forget them - $text = $commentState->replace( $text ); - } - - wfProfileOut( __METHOD__ ); - return $text; - } - - /** - * Restores pre, math, and other extensions removed by strip() - * - * always call unstripNoWiki() after this one - * @private - * @deprecated use $this->mStripState->unstrip() - */ - function unstrip( $text, $state ) { - return $state->unstripGeneral( $text ); - } - - /** - * Always call this after unstrip() to preserve the order - * - * @private - * @deprecated use $this->mStripState->unstrip() - */ - function unstripNoWiki( $text, $state ) { - return $state->unstripNoWiki( $text ); - } - - /** - * @deprecated use $this->mStripState->unstripBoth() - */ - function unstripForHTML( $text ) { - return $this->mStripState->unstripBoth( $text ); - } - - /** - * Add an item to the strip state - * Returns the unique tag which must be inserted into the stripped text - * The tag will be replaced with the original text in unstrip() - * - * @private - */ - function insertStripItem( $text, &$state ) { - $rnd = $this->mUniqPrefix . '-item' . self::getRandomString(); - $state->general->setPair( $rnd, $text ); - return $rnd; - } - - /** - * Interface with html tidy, used if $wgUseTidy = true. - * If tidy isn't able to correct the markup, the original will be - * returned in all its glory with a warning comment appended. - * - * Either the external tidy program or the in-process tidy extension - * will be used depending on availability. Override the default - * $wgTidyInternal setting to disable the internal if it's not working. - * - * @param string $text Hideous HTML input - * @return string Corrected HTML output - * @public - * @static - */ - function tidy( $text ) { - global $wgTidyInternal; - $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'. -' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'. -'<head><title>test</title></head><body>'.$text.'</body></html>'; - if( $wgTidyInternal ) { - $correctedtext = self::internalTidy( $wrappedtext ); - } else { - $correctedtext = self::externalTidy( $wrappedtext ); - } - if( is_null( $correctedtext ) ) { - wfDebug( "Tidy error detected!\n" ); - return $text . "\n<!-- Tidy found serious XHTML errors -->\n"; - } - return $correctedtext; - } - - /** - * Spawn an external HTML tidy process and get corrected markup back from it. - * - * @private - * @static - */ - function externalTidy( $text ) { - global $wgTidyConf, $wgTidyBin, $wgTidyOpts; - $fname = 'Parser::externalTidy'; - wfProfileIn( $fname ); - - $cleansource = ''; - $opts = ' -utf8'; - - $descriptorspec = array( - 0 => array('pipe', 'r'), - 1 => array('pipe', 'w'), - 2 => array('file', wfGetNull(), 'a') - ); - $pipes = array(); - $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes); - if (is_resource($process)) { - // Theoretically, this style of communication could cause a deadlock - // here. If the stdout buffer fills up, then writes to stdin could - // block. This doesn't appear to happen with tidy, because tidy only - // writes to stdout after it's finished reading from stdin. Search - // for tidyParseStdin and tidySaveStdout in console/tidy.c - fwrite($pipes[0], $text); - fclose($pipes[0]); - while (!feof($pipes[1])) { - $cleansource .= fgets($pipes[1], 1024); - } - fclose($pipes[1]); - proc_close($process); - } - - wfProfileOut( $fname ); - - if( $cleansource == '' && $text != '') { - // Some kind of error happened, so we couldn't get the corrected text. - // Just give up; we'll use the source text and append a warning. - return null; - } else { - return $cleansource; - } - } - - /** - * Use the HTML tidy PECL extension to use the tidy library in-process, - * saving the overhead of spawning a new process. - * - * 'pear install tidy' should be able to compile the extension module. - * - * @private - * @static - */ - function internalTidy( $text ) { - global $wgTidyConf, $IP; - $fname = 'Parser::internalTidy'; - wfProfileIn( $fname ); - - $tidy = new tidy; - $tidy->parseString( $text, $wgTidyConf, 'utf8' ); - $tidy->cleanRepair(); - if( $tidy->getStatus() == 2 ) { - // 2 is magic number for fatal error - // http://www.php.net/manual/en/function.tidy-get-status.php - $cleansource = null; - } else { - $cleansource = tidy_get_output( $tidy ); - } - wfProfileOut( $fname ); - return $cleansource; - } - - /** - * parse the wiki syntax used to render tables - * - * @private - */ - function doTableStuff ( $text ) { - $fname = 'Parser::doTableStuff'; - wfProfileIn( $fname ); - - $lines = explode ( "\n" , $text ); - $td_history = array (); // Is currently a td tag open? - $last_tag_history = array (); // Save history of last lag activated (td, th or caption) - $tr_history = array (); // Is currently a tr tag open? - $tr_attributes = array (); // history of tr attributes - $has_opened_tr = array(); // Did this table open a <tr> element? - $indent_level = 0; // indent level of the table - foreach ( $lines as $key => $line ) - { - $line = trim ( $line ); - - if( $line == '' ) { // empty line, go to next line - continue; - } - $first_character = $line{0}; - $matches = array(); - - if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) { - // First check if we are starting a new table - $indent_level = strlen( $matches[1] ); - - $attributes = $this->mStripState->unstripBoth( $matches[2] ); - $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' ); - - $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>"; - array_push ( $td_history , false ); - array_push ( $last_tag_history , '' ); - array_push ( $tr_history , false ); - array_push ( $tr_attributes , '' ); - array_push ( $has_opened_tr , false ); - } else if ( count ( $td_history ) == 0 ) { - // Don't do any of the following - continue; - } else if ( substr ( $line , 0 , 2 ) == '|}' ) { - // We are ending a table - $line = '</table>' . substr ( $line , 2 ); - $last_tag = array_pop ( $last_tag_history ); - - if ( !array_pop ( $has_opened_tr ) ) { - $line = "<tr><td></td></tr>{$line}"; - } - - if ( array_pop ( $tr_history ) ) { - $line = "</tr>{$line}"; - } - - if ( array_pop ( $td_history ) ) { - $line = "</{$last_tag}>{$line}"; - } - array_pop ( $tr_attributes ); - $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level ); - } else if ( substr ( $line , 0 , 2 ) == '|-' ) { - // Now we have a table row - $line = preg_replace( '#^\|-+#', '', $line ); - - // Whats after the tag is now only attributes - $attributes = $this->mStripState->unstripBoth( $line ); - $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' ); - array_pop ( $tr_attributes ); - array_push ( $tr_attributes , $attributes ); - - $line = ''; - $last_tag = array_pop ( $last_tag_history ); - array_pop ( $has_opened_tr ); - array_push ( $has_opened_tr , true ); - - if ( array_pop ( $tr_history ) ) { - $line = '</tr>'; - } - - if ( array_pop ( $td_history ) ) { - $line = "</{$last_tag}>{$line}"; - } - - $lines[$key] = $line; - array_push ( $tr_history , false ); - array_push ( $td_history , false ); - array_push ( $last_tag_history , '' ); - } - else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) { - // This might be cell elements, td, th or captions - if ( substr ( $line , 0 , 2 ) == '|+' ) { - $first_character = '+'; - $line = substr ( $line , 1 ); - } - - $line = substr ( $line , 1 ); - - if ( $first_character == '!' ) { - $line = str_replace ( '!!' , '||' , $line ); - } - - // Split up multiple cells on the same line. - // FIXME : This can result in improper nesting of tags processed - // by earlier parser steps, but should avoid splitting up eg - // attribute values containing literal "||". - $cells = StringUtils::explodeMarkup( '||' , $line ); - - $lines[$key] = ''; - - // Loop through each table cell - foreach ( $cells as $cell ) - { - $previous = ''; - if ( $first_character != '+' ) - { - $tr_after = array_pop ( $tr_attributes ); - if ( !array_pop ( $tr_history ) ) { - $previous = "<tr{$tr_after}>\n"; - } - array_push ( $tr_history , true ); - array_push ( $tr_attributes , '' ); - array_pop ( $has_opened_tr ); - array_push ( $has_opened_tr , true ); - } - - $last_tag = array_pop ( $last_tag_history ); - - if ( array_pop ( $td_history ) ) { - $previous = "</{$last_tag}>{$previous}"; - } - - if ( $first_character == '|' ) { - $last_tag = 'td'; - } else if ( $first_character == '!' ) { - $last_tag = 'th'; - } else if ( $first_character == '+' ) { - $last_tag = 'caption'; - } else { - $last_tag = ''; - } - - array_push ( $last_tag_history , $last_tag ); - - // A cell could contain both parameters and data - $cell_data = explode ( '|' , $cell , 2 ); - - // Bug 553: Note that a '|' inside an invalid link should not - // be mistaken as delimiting cell parameters - if ( strpos( $cell_data[0], '[[' ) !== false ) { - $cell = "{$previous}<{$last_tag}>{$cell}"; - } else if ( count ( $cell_data ) == 1 ) - $cell = "{$previous}<{$last_tag}>{$cell_data[0]}"; - else { - $attributes = $this->mStripState->unstripBoth( $cell_data[0] ); - $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag ); - $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}"; - } - - $lines[$key] .= $cell; - array_push ( $td_history , true ); - } - } - } - - // Closing open td, tr && table - while ( count ( $td_history ) > 0 ) - { - if ( array_pop ( $td_history ) ) { - $lines[] = '</td>' ; - } - if ( array_pop ( $tr_history ) ) { - $lines[] = '</tr>' ; - } - if ( !array_pop ( $has_opened_tr ) ) { - $lines[] = "<tr><td></td></tr>" ; - } - - $lines[] = '</table>' ; - } - - $output = implode ( "\n" , $lines ) ; - - // special case: don't return empty table - if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) { - $output = ''; - } - - wfProfileOut( $fname ); - - return $output; - } - - /** - * Helper function for parse() that transforms wiki markup into - * HTML. Only called for $mOutputType == OT_HTML. - * - * @private - */ - function internalParse( $text ) { - $args = array(); - $isMain = true; - $fname = 'Parser::internalParse'; - wfProfileIn( $fname ); - - # Hook to suspend the parser in this state - if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) { - wfProfileOut( $fname ); - return $text ; - } - - # Remove <noinclude> tags and <includeonly> sections - $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) ); - $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') ); - $text = StringUtils::delimiterReplace( '<includeonly>', '</includeonly>', '', $text ); - - $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) ); - - $text = $this->replaceVariables( $text, $args ); - wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); - - // Tables need to come after variable replacement for things to work - // properly; putting them before other transformations should keep - // exciting things like link expansions from showing up in surprising - // places. - $text = $this->doTableStuff( $text ); - - $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text ); - - $text = $this->stripToc( $text ); - $this->stripNoGallery( $text ); - $text = $this->doHeadings( $text ); - if($this->mOptions->getUseDynamicDates()) { - $df =& DateFormatter::getInstance(); - $text = $df->reformat( $this->mOptions->getDateFormat(), $text ); - } - $text = $this->doAllQuotes( $text ); - $text = $this->replaceInternalLinks( $text ); - $text = $this->replaceExternalLinks( $text ); - - # replaceInternalLinks may sometimes leave behind - # absolute URLs, which have to be masked to hide them from replaceExternalLinks - $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text); - - $text = $this->doMagicLinks( $text ); - $text = $this->formatHeadings( $text, $isMain ); - - wfProfileOut( $fname ); - return $text; - } - - /** - * Replace special strings like "ISBN xxx" and "RFC xxx" with - * magic external links. - * - * @private - */ - function &doMagicLinks( &$text ) { - wfProfileIn( __METHOD__ ); - $text = preg_replace_callback( - '!(?: # Start cases - <a.*?</a> | # Skip link text - <.*?> | # Skip stuff inside HTML elements - (?:RFC|PMID)\s+([0-9]+) | # RFC or PMID, capture number as m[1] - ISBN\s+(\b # ISBN, capture number as m[2] - (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix - (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters - [0-9Xx] # check digit - \b) - )!x', array( &$this, 'magicLinkCallback' ), $text ); - wfProfileOut( __METHOD__ ); - return $text; - } - - function magicLinkCallback( $m ) { - if ( substr( $m[0], 0, 1 ) == '<' ) { - # Skip HTML element - return $m[0]; - } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) { - $isbn = $m[2]; - $num = strtr( $isbn, array( - '-' => '', - ' ' => '', - 'x' => 'X', - )); - $titleObj = SpecialPage::getTitleFor( 'Booksources' ); - $text = '<a href="' . - $titleObj->escapeLocalUrl( "isbn=$num" ) . - "\" class=\"internal\">ISBN $isbn</a>"; - } else { - if ( substr( $m[0], 0, 3 ) == 'RFC' ) { - $keyword = 'RFC'; - $urlmsg = 'rfcurl'; - $id = $m[1]; - } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) { - $keyword = 'PMID'; - $urlmsg = 'pubmedurl'; - $id = $m[1]; - } else { - throw new MWException( __METHOD__.': unrecognised match type "' . - substr($m[0], 0, 20 ) . '"' ); - } - - $url = wfMsg( $urlmsg, $id); - $sk = $this->mOptions->getSkin(); - $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); - $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>"; - } - return $text; - } - - /** - * Parse headers and return html - * - * @private - */ - function doHeadings( $text ) { - $fname = 'Parser::doHeadings'; - wfProfileIn( $fname ); - for ( $i = 6; $i >= 1; --$i ) { - $h = str_repeat( '=', $i ); - $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m", - "<h{$i}>\\1</h{$i}>\\2", $text ); - } - wfProfileOut( $fname ); - return $text; - } - - /** - * Replace single quotes with HTML markup - * @private - * @return string the altered text - */ - function doAllQuotes( $text ) { - $fname = 'Parser::doAllQuotes'; - wfProfileIn( $fname ); - $outtext = ''; - $lines = explode( "\n", $text ); - foreach ( $lines as $line ) { - $outtext .= $this->doQuotes ( $line ) . "\n"; - } - $outtext = substr($outtext, 0,-1); - wfProfileOut( $fname ); - return $outtext; - } - - /** - * Helper function for doAllQuotes() - */ - public function doQuotes( $text ) { - $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE ); - if ( count( $arr ) == 1 ) - return $text; - else - { - # First, do some preliminary work. This may shift some apostrophes from - # being mark-up to being text. It also counts the number of occurrences - # of bold and italics mark-ups. - $i = 0; - $numbold = 0; - $numitalics = 0; - foreach ( $arr as $r ) - { - if ( ( $i % 2 ) == 1 ) - { - # If there are ever four apostrophes, assume the first is supposed to - # be text, and the remaining three constitute mark-up for bold text. - if ( strlen( $arr[$i] ) == 4 ) - { - $arr[$i-1] .= "'"; - $arr[$i] = "'''"; - } - # If there are more than 5 apostrophes in a row, assume they're all - # text except for the last 5. - else if ( strlen( $arr[$i] ) > 5 ) - { - $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 ); - $arr[$i] = "'''''"; - } - # Count the number of occurrences of bold and italics mark-ups. - # We are not counting sequences of five apostrophes. - if ( strlen( $arr[$i] ) == 2 ) { $numitalics++; } - else if ( strlen( $arr[$i] ) == 3 ) { $numbold++; } - else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; } - } - $i++; - } - - # If there is an odd number of both bold and italics, it is likely - # that one of the bold ones was meant to be an apostrophe followed - # by italics. Which one we cannot know for certain, but it is more - # likely to be one that has a single-letter word before it. - if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) - { - $i = 0; - $firstsingleletterword = -1; - $firstmultiletterword = -1; - $firstspace = -1; - foreach ( $arr as $r ) - { - if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) ) - { - $x1 = substr ($arr[$i-1], -1); - $x2 = substr ($arr[$i-1], -2, 1); - if ($x1 == ' ') { - if ($firstspace == -1) $firstspace = $i; - } else if ($x2 == ' ') { - if ($firstsingleletterword == -1) $firstsingleletterword = $i; - } else { - if ($firstmultiletterword == -1) $firstmultiletterword = $i; - } - } - $i++; - } - - # If there is a single-letter word, use it! - if ($firstsingleletterword > -1) - { - $arr [ $firstsingleletterword ] = "''"; - $arr [ $firstsingleletterword-1 ] .= "'"; - } - # If not, but there's a multi-letter word, use that one. - else if ($firstmultiletterword > -1) - { - $arr [ $firstmultiletterword ] = "''"; - $arr [ $firstmultiletterword-1 ] .= "'"; - } - # ... otherwise use the first one that has neither. - # (notice that it is possible for all three to be -1 if, for example, - # there is only one pentuple-apostrophe in the line) - else if ($firstspace > -1) - { - $arr [ $firstspace ] = "''"; - $arr [ $firstspace-1 ] .= "'"; - } - } - - # Now let's actually convert our apostrophic mush to HTML! - $output = ''; - $buffer = ''; - $state = ''; - $i = 0; - foreach ($arr as $r) - { - if (($i % 2) == 0) - { - if ($state == 'both') - $buffer .= $r; - else - $output .= $r; - } - else - { - if (strlen ($r) == 2) - { - if ($state == 'i') - { $output .= '</i>'; $state = ''; } - else if ($state == 'bi') - { $output .= '</i>'; $state = 'b'; } - else if ($state == 'ib') - { $output .= '</b></i><b>'; $state = 'b'; } - else if ($state == 'both') - { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; } - else # $state can be 'b' or '' - { $output .= '<i>'; $state .= 'i'; } - } - else if (strlen ($r) == 3) - { - if ($state == 'b') - { $output .= '</b>'; $state = ''; } - else if ($state == 'bi') - { $output .= '</i></b><i>'; $state = 'i'; } - else if ($state == 'ib') - { $output .= '</b>'; $state = 'i'; } - else if ($state == 'both') - { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; } - else # $state can be 'i' or '' - { $output .= '<b>'; $state .= 'b'; } - } - else if (strlen ($r) == 5) - { - if ($state == 'b') - { $output .= '</b><i>'; $state = 'i'; } - else if ($state == 'i') - { $output .= '</i><b>'; $state = 'b'; } - else if ($state == 'bi') - { $output .= '</i></b>'; $state = ''; } - else if ($state == 'ib') - { $output .= '</b></i>'; $state = ''; } - else if ($state == 'both') - { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; } - else # ($state == '') - { $buffer = ''; $state = 'both'; } - } - } - $i++; - } - # Now close all remaining tags. Notice that the order is important. - if ($state == 'b' || $state == 'ib') - $output .= '</b>'; - if ($state == 'i' || $state == 'bi' || $state == 'ib') - $output .= '</i>'; - if ($state == 'bi') - $output .= '</b>'; - # There might be lonely ''''', so make sure we have a buffer - if ($state == 'both' && $buffer) - $output .= '<b><i>'.$buffer.'</i></b>'; - return $output; - } - } - - /** - * Replace external links - * - * Note: this is all very hackish and the order of execution matters a lot. - * Make sure to run maintenance/parserTests.php if you change this code. - * - * @private - */ - function replaceExternalLinks( $text ) { - global $wgContLang; - $fname = 'Parser::replaceExternalLinks'; - wfProfileIn( $fname ); - - $sk = $this->mOptions->getSkin(); - - $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); - - $s = $this->replaceFreeExternalLinks( array_shift( $bits ) ); - - $i = 0; - while ( $i<count( $bits ) ) { - $url = $bits[$i++]; - $protocol = $bits[$i++]; - $text = $bits[$i++]; - $trail = $bits[$i++]; - - # The characters '<' and '>' (which were escaped by - # removeHTMLtags()) should not be included in - # URLs, per RFC 2396. - $m2 = array(); - if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { - $text = substr($url, $m2[0][1]) . ' ' . $text; - $url = substr($url, 0, $m2[0][1]); - } - - # If the link text is an image URL, replace it with an <img> tag - # This happened by accident in the original parser, but some people used it extensively - $img = $this->maybeMakeExternalImage( $text ); - if ( $img !== false ) { - $text = $img; - } - - $dtrail = ''; - - # Set linktype for CSS - if URL==text, link is essentially free - $linktype = ($text == $url) ? 'free' : 'text'; - - # No link text, e.g. [http://domain.tld/some.link] - if ( $text == '' ) { - # Autonumber if allowed. See bug #5918 - if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) { - $text = '[' . ++$this->mAutonumber . ']'; - $linktype = 'autonumber'; - } else { - # Otherwise just use the URL - $text = htmlspecialchars( $url ); - $linktype = 'free'; - } - } else { - # Have link text, e.g. [http://domain.tld/some.link text]s - # Check for trail - list( $dtrail, $trail ) = Linker::splitTrail( $trail ); - } - - $text = $wgContLang->markNoConversion($text); - - $url = Sanitizer::cleanUrl( $url ); - - # Process the trail (i.e. everything after this link up until start of the next link), - # replacing any non-bracketed links - $trail = $this->replaceFreeExternalLinks( $trail ); - - # Use the encoded URL - # This means that users can paste URLs directly into the text - # Funny characters like ö aren't valid in URLs anyway - # This was changed in August 2004 - $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail; - - # Register link in the output object. - # Replace unnecessary URL escape codes with the referenced character - # This prevents spammers from hiding links from the filters - $pasteurized = self::replaceUnusualEscapes( $url ); - $this->mOutput->addExternalLink( $pasteurized ); - } - - wfProfileOut( $fname ); - return $s; - } - - /** - * Replace anything that looks like a URL with a link - * @private - */ - function replaceFreeExternalLinks( $text ) { - global $wgContLang; - $fname = 'Parser::replaceFreeExternalLinks'; - wfProfileIn( $fname ); - - $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); - $s = array_shift( $bits ); - $i = 0; - - $sk = $this->mOptions->getSkin(); - - while ( $i < count( $bits ) ){ - $protocol = $bits[$i++]; - $remainder = $bits[$i++]; - - $m = array(); - if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) { - # Found some characters after the protocol that look promising - $url = $protocol . $m[1]; - $trail = $m[2]; - - # special case: handle urls as url args: - # http://www.example.com/foo?=http://www.example.com/bar - if(strlen($trail) == 0 && - isset($bits[$i]) && - preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) && - preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) - { - # add protocol, arg - $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link - $i += 2; - $trail = $m[2]; - } - - # The characters '<' and '>' (which were escaped by - # removeHTMLtags()) should not be included in - # URLs, per RFC 2396. - $m2 = array(); - if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { - $trail = substr($url, $m2[0][1]) . $trail; - $url = substr($url, 0, $m2[0][1]); - } - - # Move trailing punctuation to $trail - $sep = ',;\.:!?'; - # If there is no left bracket, then consider right brackets fair game too - if ( strpos( $url, '(' ) === false ) { - $sep .= ')'; - } - - $numSepChars = strspn( strrev( $url ), $sep ); - if ( $numSepChars ) { - $trail = substr( $url, -$numSepChars ) . $trail; - $url = substr( $url, 0, -$numSepChars ); - } - - $url = Sanitizer::cleanUrl( $url ); - - # Is this an external image? - $text = $this->maybeMakeExternalImage( $url ); - if ( $text === false ) { - # Not an image, make a link - $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() ); - # Register it in the output object... - # Replace unnecessary URL escape codes with their equivalent characters - $pasteurized = self::replaceUnusualEscapes( $url ); - $this->mOutput->addExternalLink( $pasteurized ); - } - $s .= $text . $trail; - } else { - $s .= $protocol . $remainder; - } - } - wfProfileOut( $fname ); - return $s; - } - - /** - * Replace unusual URL escape codes with their equivalent characters - * @param string - * @return string - * @static - * @todo This can merge genuinely required bits in the path or query string, - * breaking legit URLs. A proper fix would treat the various parts of - * the URL differently; as a workaround, just use the output for - * statistical records, not for actual linking/output. - */ - static function replaceUnusualEscapes( $url ) { - return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', - array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url ); - } - - /** - * Callback function used in replaceUnusualEscapes(). - * Replaces unusual URL escape codes with their equivalent character - * @static - * @private - */ - private static function replaceUnusualEscapesCallback( $matches ) { - $char = urldecode( $matches[0] ); - $ord = ord( $char ); - // Is it an unsafe or HTTP reserved character according to RFC 1738? - if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) { - // No, shouldn't be escaped - return $char; - } else { - // Yes, leave it escaped - return $matches[0]; - } - } - - /** - * make an image if it's allowed, either through the global - * option or through the exception - * @private - */ - function maybeMakeExternalImage( $url ) { - $sk = $this->mOptions->getSkin(); - $imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); - $imagesexception = !empty($imagesfrom); - $text = false; - if ( $this->mOptions->getAllowExternalImages() - || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) { - if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) { - # Image found - $text = $sk->makeExternalImage( $url ); - } - } - return $text; - } - - /** - * Process [[ ]] wikilinks - * - * @private - */ - function replaceInternalLinks( $s ) { - global $wgContLang; - static $fname = 'Parser::replaceInternalLinks' ; - - wfProfileIn( $fname ); - - wfProfileIn( $fname.'-setup' ); - static $tc = FALSE; - # the % is needed to support urlencoded titles as well - if ( !$tc ) { $tc = Title::legalChars() . '#%'; } - - $sk = $this->mOptions->getSkin(); - - #split the entire text string on occurences of [[ - $a = explode( '[[', ' ' . $s ); - #get the first element (all text up to first [[), and remove the space we added - $s = array_shift( $a ); - $s = substr( $s, 1 ); - - # Match a link having the form [[namespace:link|alternate]]trail - static $e1 = FALSE; - if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; } - # Match cases where there is no "]]", which might still be images - static $e1_img = FALSE; - if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; } - # Match the end of a line for a word that's not followed by whitespace, - # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched - $e2 = wfMsgForContent( 'linkprefix' ); - - $useLinkPrefixExtension = $wgContLang->linkPrefixExtension(); - if( is_null( $this->mTitle ) ) { - throw new MWException( __METHOD__.": \$this->mTitle is null\n" ); - } - $nottalk = !$this->mTitle->isTalkPage(); - - if ( $useLinkPrefixExtension ) { - $m = array(); - if ( preg_match( $e2, $s, $m ) ) { - $first_prefix = $m[2]; - } else { - $first_prefix = false; - } - } else { - $prefix = ''; - } - - if($wgContLang->hasVariants()) { - $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText()); - } else { - $selflink = array($this->mTitle->getPrefixedText()); - } - $useSubpages = $this->areSubpagesAllowed(); - wfProfileOut( $fname.'-setup' ); - - # Loop for each link - for ($k = 0; isset( $a[$k] ); $k++) { - $line = $a[$k]; - if ( $useLinkPrefixExtension ) { - wfProfileIn( $fname.'-prefixhandling' ); - if ( preg_match( $e2, $s, $m ) ) { - $prefix = $m[2]; - $s = $m[1]; - } else { - $prefix=''; - } - # first link - if($first_prefix) { - $prefix = $first_prefix; - $first_prefix = false; - } - wfProfileOut( $fname.'-prefixhandling' ); - } - - $might_be_img = false; - - wfProfileIn( "$fname-e1" ); - if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt - $text = $m[2]; - # If we get a ] at the beginning of $m[3] that means we have a link that's something like: - # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up, - # the real problem is with the $e1 regex - # See bug 1300. - # - # Still some problems for cases where the ] is meant to be outside punctuation, - # and no image is in sight. See bug 2095. - # - if( $text !== '' && - substr( $m[3], 0, 1 ) === ']' && - strpos($text, '[') !== false - ) - { - $text .= ']'; # so that replaceExternalLinks($text) works later - $m[3] = substr( $m[3], 1 ); - } - # fix up urlencoded title texts - if( strpos( $m[1], '%' ) !== false ) { - # Should anchors '#' also be rejected? - $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) ); - } - $trail = $m[3]; - } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption - $might_be_img = true; - $text = $m[2]; - if ( strpos( $m[1], '%' ) !== false ) { - $m[1] = urldecode($m[1]); - } - $trail = ""; - } else { # Invalid form; output directly - $s .= $prefix . '[[' . $line ; - wfProfileOut( "$fname-e1" ); - continue; - } - wfProfileOut( "$fname-e1" ); - wfProfileIn( "$fname-misc" ); - - # Don't allow internal links to pages containing - # PROTO: where PROTO is a valid URL protocol; these - # should be external links. - if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) { - $s .= $prefix . '[[' . $line ; - continue; - } - - # Make subpage if necessary - if( $useSubpages ) { - $link = $this->maybeDoSubpageLink( $m[1], $text ); - } else { - $link = $m[1]; - } - - $noforce = (substr($m[1], 0, 1) != ':'); - if (!$noforce) { - # Strip off leading ':' - $link = substr($link, 1); - } - - wfProfileOut( "$fname-misc" ); - wfProfileIn( "$fname-title" ); - $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) ); - if( !$nt ) { - $s .= $prefix . '[[' . $line; - wfProfileOut( "$fname-title" ); - continue; - } - - $ns = $nt->getNamespace(); - $iw = $nt->getInterWiki(); - wfProfileOut( "$fname-title" ); - - if ($might_be_img) { # if this is actually an invalid link - wfProfileIn( "$fname-might_be_img" ); - if ($ns == NS_IMAGE && $noforce) { #but might be an image - $found = false; - while (isset ($a[$k+1]) ) { - #look at the next 'line' to see if we can close it there - $spliced = array_splice( $a, $k + 1, 1 ); - $next_line = array_shift( $spliced ); - $m = explode( ']]', $next_line, 3 ); - if ( count( $m ) == 3 ) { - # the first ]] closes the inner link, the second the image - $found = true; - $text .= "[[{$m[0]}]]{$m[1]}"; - $trail = $m[2]; - break; - } elseif ( count( $m ) == 2 ) { - #if there's exactly one ]] that's fine, we'll keep looking - $text .= "[[{$m[0]}]]{$m[1]}"; - } else { - #if $next_line is invalid too, we need look no further - $text .= '[[' . $next_line; - break; - } - } - if ( !$found ) { - # we couldn't find the end of this imageLink, so output it raw - #but don't ignore what might be perfectly normal links in the text we've examined - $text = $this->replaceInternalLinks($text); - $s .= "{$prefix}[[$link|$text"; - # note: no $trail, because without an end, there *is* no trail - wfProfileOut( "$fname-might_be_img" ); - continue; - } - } else { #it's not an image, so output it raw - $s .= "{$prefix}[[$link|$text"; - # note: no $trail, because without an end, there *is* no trail - wfProfileOut( "$fname-might_be_img" ); - continue; - } - wfProfileOut( "$fname-might_be_img" ); - } - - $wasblank = ( '' == $text ); - if( $wasblank ) $text = $link; - - # Link not escaped by : , create the various objects - if( $noforce ) { - - # Interwikis - wfProfileIn( "$fname-interwiki" ); - if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) { - $this->mOutput->addLanguageLink( $nt->getFullText() ); - $s = rtrim($s . $prefix); - $s .= trim($trail, "\n") == '' ? '': $prefix . $trail; - wfProfileOut( "$fname-interwiki" ); - continue; - } - wfProfileOut( "$fname-interwiki" ); - - if ( $ns == NS_IMAGE ) { - wfProfileIn( "$fname-image" ); - if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) { - # recursively parse links inside the image caption - # actually, this will parse them in any other parameters, too, - # but it might be hard to fix that, and it doesn't matter ATM - $text = $this->replaceExternalLinks($text); - $text = $this->replaceInternalLinks($text); - - # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them - $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail; - $this->mOutput->addImage( $nt->getDBkey() ); - - wfProfileOut( "$fname-image" ); - continue; - } else { - # We still need to record the image's presence on the page - $this->mOutput->addImage( $nt->getDBkey() ); - } - wfProfileOut( "$fname-image" ); - - } - - if ( $ns == NS_CATEGORY ) { - wfProfileIn( "$fname-category" ); - $s = rtrim($s . "\n"); # bug 87 - - if ( $wasblank ) { - $sortkey = $this->getDefaultSort(); - } else { - $sortkey = $text; - } - $sortkey = Sanitizer::decodeCharReferences( $sortkey ); - $sortkey = str_replace( "\n", '', $sortkey ); - $sortkey = $wgContLang->convertCategoryKey( $sortkey ); - $this->mOutput->addCategory( $nt->getDBkey(), $sortkey ); - - /** - * Strip the whitespace Category links produce, see bug 87 - * @todo We might want to use trim($tmp, "\n") here. - */ - $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail; - - wfProfileOut( "$fname-category" ); - continue; - } - } - - # Self-link checking - if( $nt->getFragment() === '' ) { - if( in_array( $nt->getPrefixedText(), $selflink, true ) ) { - $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); - continue; - } - } - - # Special and Media are pseudo-namespaces; no pages actually exist in them - if( $ns == NS_MEDIA ) { - # Give extensions a chance to select the file revision for us - $skip = $time = false; - wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) ); - if ( $skip ) { - $link = $sk->makeLinkObj( $nt ); - } else { - $link = $sk->makeMediaLinkObj( $nt, $text, $time ); - } - # Cloak with NOPARSE to avoid replacement in replaceExternalLinks - $s .= $prefix . $this->armorLinks( $link ) . $trail; - $this->mOutput->addImage( $nt->getDBkey() ); - continue; - } elseif( $ns == NS_SPECIAL ) { - if( SpecialPage::exists( $nt->getDBkey() ) ) { - $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); - } else { - $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); - } - continue; - } elseif( $ns == NS_IMAGE ) { - $img = wfFindFile( $nt ); - if( $img ) { - // Force a blue link if the file exists; may be a remote - // upload on the shared repository, and we want to see its - // auto-generated page. - $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); - $this->mOutput->addLink( $nt ); - continue; - } - } - $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); - } - wfProfileOut( $fname ); - return $s; - } - - /** - * Make a link placeholder. The text returned can be later resolved to a real link with - * replaceLinkHolders(). This is done for two reasons: firstly to avoid further - * parsing of interwiki links, and secondly to allow all existence checks and - * article length checks (for stub links) to be bundled into a single query. - * - */ - function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - wfProfileIn( __METHOD__ ); - if ( ! is_object($nt) ) { - # Fail gracefully - $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}"; - } else { - # Separate the link trail from the rest of the link - list( $inside, $trail ) = Linker::splitTrail( $trail ); - - if ( $nt->isExternal() ) { - $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside ); - $this->mInterwikiLinkHolders['titles'][] = $nt; - $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}"; - } else { - $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() ); - $this->mLinkHolders['dbkeys'][] = $nt->getDBkey(); - $this->mLinkHolders['queries'][] = $query; - $this->mLinkHolders['texts'][] = $prefix.$text.$inside; - $this->mLinkHolders['titles'][] = $nt; - - $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}"; - } - } - wfProfileOut( __METHOD__ ); - return $retVal; - } - - /** - * Render a forced-blue link inline; protect against double expansion of - * URLs if we're in a mode that prepends full URL prefixes to internal links. - * Since this little disaster has to split off the trail text to avoid - * breaking URLs in the following text without breaking trails on the - * wiki links, it's been made into a horrible function. - * - * @param Title $nt - * @param string $text - * @param string $query - * @param string $trail - * @param string $prefix - * @return string HTML-wikitext mix oh yuck - */ - function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - list( $inside, $trail ) = Linker::splitTrail( $trail ); - $sk = $this->mOptions->getSkin(); - $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix ); - return $this->armorLinks( $link ) . $trail; - } - - /** - * Insert a NOPARSE hacky thing into any inline links in a chunk that's - * going to go through further parsing steps before inline URL expansion. - * - * In particular this is important when using action=render, which causes - * full URLs to be included. - * - * Oh man I hate our multi-layer parser! - * - * @param string more-or-less HTML - * @return string less-or-more HTML with NOPARSE bits - */ - function armorLinks( $text ) { - return preg_replace( '/\b(' . wfUrlProtocols() . ')/', - "{$this->mUniqPrefix}NOPARSE$1", $text ); - } - - /** - * Return true if subpage links should be expanded on this page. - * @return bool - */ - function areSubpagesAllowed() { - # Some namespaces don't allow subpages - global $wgNamespacesWithSubpages; - return !empty($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]); - } - - /** - * Handle link to subpage if necessary - * @param string $target the source of the link - * @param string &$text the link text, modified as necessary - * @return string the full name of the link - * @private - */ - function maybeDoSubpageLink($target, &$text) { - # Valid link forms: - # Foobar -- normal - # :Foobar -- override special treatment of prefix (images, language links) - # /Foobar -- convert to CurrentPage/Foobar - # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text - # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage - # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage - - $fname = 'Parser::maybeDoSubpageLink'; - wfProfileIn( $fname ); - $ret = $target; # default return value is no change - - # Some namespaces don't allow subpages, - # so only perform processing if subpages are allowed - if( $this->areSubpagesAllowed() ) { - $hash = strpos( $target, '#' ); - if( $hash !== false ) { - $suffix = substr( $target, $hash ); - $target = substr( $target, 0, $hash ); - } else { - $suffix = ''; - } - # bug 7425 - $target = trim( $target ); - # Look at the first character - if( $target != '' && $target{0} == '/' ) { - # / at end means we don't want the slash to be shown - $m = array(); - $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m ); - if( $trailingSlashes ) { - $noslash = $target = substr( $target, 1, -strlen($m[0][0]) ); - } else { - $noslash = substr( $target, 1 ); - } - - $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix; - if( '' === $text ) { - $text = $target . $suffix; - } # this might be changed for ugliness reasons - } else { - # check for .. subpage backlinks - $dotdotcount = 0; - $nodotdot = $target; - while( strncmp( $nodotdot, "../", 3 ) == 0 ) { - ++$dotdotcount; - $nodotdot = substr( $nodotdot, 3 ); - } - if($dotdotcount > 0) { - $exploded = explode( '/', $this->mTitle->GetPrefixedText() ); - if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page - $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) ); - # / at the end means don't show full path - if( substr( $nodotdot, -1, 1 ) == '/' ) { - $nodotdot = substr( $nodotdot, 0, -1 ); - if( '' === $text ) { - $text = $nodotdot . $suffix; - } - } - $nodotdot = trim( $nodotdot ); - if( $nodotdot != '' ) { - $ret .= '/' . $nodotdot; - } - $ret .= $suffix; - } - } - } - } - - wfProfileOut( $fname ); - return $ret; - } - - /**#@+ - * Used by doBlockLevels() - * @private - */ - /* private */ function closeParagraph() { - $result = ''; - if ( '' != $this->mLastSection ) { - $result = '</' . $this->mLastSection . ">\n"; - } - $this->mInPre = false; - $this->mLastSection = ''; - return $result; - } - # getCommon() returns the length of the longest common substring - # of both arguments, starting at the beginning of both. - # - /* private */ function getCommon( $st1, $st2 ) { - $fl = strlen( $st1 ); - $shorter = strlen( $st2 ); - if ( $fl < $shorter ) { $shorter = $fl; } - - for ( $i = 0; $i < $shorter; ++$i ) { - if ( $st1{$i} != $st2{$i} ) { break; } - } - return $i; - } - # These next three functions open, continue, and close the list - # element appropriate to the prefix character passed into them. - # - /* private */ function openList( $char ) { - $result = $this->closeParagraph(); - - if ( '*' == $char ) { $result .= '<ul><li>'; } - else if ( '#' == $char ) { $result .= '<ol><li>'; } - else if ( ':' == $char ) { $result .= '<dl><dd>'; } - else if ( ';' == $char ) { - $result .= '<dl><dt>'; - $this->mDTopen = true; - } - else { $result = '<!-- ERR 1 -->'; } - - return $result; - } - - /* private */ function nextItem( $char ) { - if ( '*' == $char || '#' == $char ) { return '</li><li>'; } - else if ( ':' == $char || ';' == $char ) { - $close = '</dd>'; - if ( $this->mDTopen ) { $close = '</dt>'; } - if ( ';' == $char ) { - $this->mDTopen = true; - return $close . '<dt>'; - } else { - $this->mDTopen = false; - return $close . '<dd>'; - } - } - return '<!-- ERR 2 -->'; - } - - /* private */ function closeList( $char ) { - if ( '*' == $char ) { $text = '</li></ul>'; } - else if ( '#' == $char ) { $text = '</li></ol>'; } - else if ( ':' == $char ) { - if ( $this->mDTopen ) { - $this->mDTopen = false; - $text = '</dt></dl>'; - } else { - $text = '</dd></dl>'; - } - } - else { return '<!-- ERR 3 -->'; } - return $text."\n"; - } - /**#@-*/ - - /** - * Make lists from lines starting with ':', '*', '#', etc. - * - * @private - * @return string the lists rendered as HTML - */ - function doBlockLevels( $text, $linestart ) { - $fname = 'Parser::doBlockLevels'; - wfProfileIn( $fname ); - - # Parsing through the text line by line. The main thing - # happening here is handling of block-level elements p, pre, - # and making lists from lines starting with * # : etc. - # - $textLines = explode( "\n", $text ); - - $lastPrefix = $output = ''; - $this->mDTopen = $inBlockElem = false; - $prefixLength = 0; - $paragraphStack = false; - - if ( !$linestart ) { - $output .= array_shift( $textLines ); - } - foreach ( $textLines as $oLine ) { - $lastPrefixLength = strlen( $lastPrefix ); - $preCloseMatch = preg_match('/<\\/pre/i', $oLine ); - $preOpenMatch = preg_match('/<pre/i', $oLine ); - if ( !$this->mInPre ) { - # Multiple prefixes may abut each other for nested lists. - $prefixLength = strspn( $oLine, '*#:;' ); - $pref = substr( $oLine, 0, $prefixLength ); - - # eh? - $pref2 = str_replace( ';', ':', $pref ); - $t = substr( $oLine, $prefixLength ); - $this->mInPre = !empty($preOpenMatch); - } else { - # Don't interpret any other prefixes in preformatted text - $prefixLength = 0; - $pref = $pref2 = ''; - $t = $oLine; - } - - # List generation - if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) { - # Same as the last item, so no need to deal with nesting or opening stuff - $output .= $this->nextItem( substr( $pref, -1 ) ); - $paragraphStack = false; - - if ( substr( $pref, -1 ) == ';') { - # The one nasty exception: definition lists work like this: - # ; title : definition text - # So we check for : in the remainder text to split up the - # title and definition, without b0rking links. - $term = $t2 = ''; - if ($this->findColonNoLinks($t, $term, $t2) !== false) { - $t = $t2; - $output .= $term . $this->nextItem( ':' ); - } - } - } elseif( $prefixLength || $lastPrefixLength ) { - # Either open or close a level... - $commonPrefixLength = $this->getCommon( $pref, $lastPrefix ); - $paragraphStack = false; - - while( $commonPrefixLength < $lastPrefixLength ) { - $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} ); - --$lastPrefixLength; - } - if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) { - $output .= $this->nextItem( $pref{$commonPrefixLength-1} ); - } - while ( $prefixLength > $commonPrefixLength ) { - $char = substr( $pref, $commonPrefixLength, 1 ); - $output .= $this->openList( $char ); - - if ( ';' == $char ) { - # FIXME: This is dupe of code above - if ($this->findColonNoLinks($t, $term, $t2) !== false) { - $t = $t2; - $output .= $term . $this->nextItem( ':' ); - } - } - ++$commonPrefixLength; - } - $lastPrefix = $pref2; - } - if( 0 == $prefixLength ) { - wfProfileIn( "$fname-paragraph" ); - # No prefix (not in list)--go to paragraph mode - // XXX: use a stack for nestable elements like span, table and div - $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t ); - $closematch = preg_match( - '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'. - '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t ); - if ( $openmatch or $closematch ) { - $paragraphStack = false; - # TODO bug 5718: paragraph closed - $output .= $this->closeParagraph(); - if ( $preOpenMatch and !$preCloseMatch ) { - $this->mInPre = true; - } - if ( $closematch ) { - $inBlockElem = false; - } else { - $inBlockElem = true; - } - } else if ( !$inBlockElem && !$this->mInPre ) { - if ( '' != $t and ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) { - // pre - if ($this->mLastSection != 'pre') { - $paragraphStack = false; - $output .= $this->closeParagraph().'<pre>'; - $this->mLastSection = 'pre'; - } - $t = substr( $t, 1 ); - } else { - // paragraph - if ( '' == trim($t) ) { - if ( $paragraphStack ) { - $output .= $paragraphStack.'<br />'; - $paragraphStack = false; - $this->mLastSection = 'p'; - } else { - if ($this->mLastSection != 'p' ) { - $output .= $this->closeParagraph(); - $this->mLastSection = ''; - $paragraphStack = '<p>'; - } else { - $paragraphStack = '</p><p>'; - } - } - } else { - if ( $paragraphStack ) { - $output .= $paragraphStack; - $paragraphStack = false; - $this->mLastSection = 'p'; - } else if ($this->mLastSection != 'p') { - $output .= $this->closeParagraph().'<p>'; - $this->mLastSection = 'p'; - } - } - } - } - wfProfileOut( "$fname-paragraph" ); - } - // somewhere above we forget to get out of pre block (bug 785) - if($preCloseMatch && $this->mInPre) { - $this->mInPre = false; - } - if ($paragraphStack === false) { - $output .= $t."\n"; - } - } - while ( $prefixLength ) { - $output .= $this->closeList( $pref2{$prefixLength-1} ); - --$prefixLength; - } - if ( '' != $this->mLastSection ) { - $output .= '</' . $this->mLastSection . '>'; - $this->mLastSection = ''; - } - - wfProfileOut( $fname ); - return $output; - } - - /** - * Split up a string on ':', ignoring any occurences inside tags - * to prevent illegal overlapping. - * @param string $str the string to split - * @param string &$before set to everything before the ':' - * @param string &$after set to everything after the ':' - * return string the position of the ':', or false if none found - */ - function findColonNoLinks($str, &$before, &$after) { - $fname = 'Parser::findColonNoLinks'; - wfProfileIn( $fname ); - - $pos = strpos( $str, ':' ); - if( $pos === false ) { - // Nothing to find! - wfProfileOut( $fname ); - return false; - } - - $lt = strpos( $str, '<' ); - if( $lt === false || $lt > $pos ) { - // Easy; no tag nesting to worry about - $before = substr( $str, 0, $pos ); - $after = substr( $str, $pos+1 ); - wfProfileOut( $fname ); - return $pos; - } - - // Ugly state machine to walk through avoiding tags. - $state = self::COLON_STATE_TEXT; - $stack = 0; - $len = strlen( $str ); - for( $i = 0; $i < $len; $i++ ) { - $c = $str{$i}; - - switch( $state ) { - // (Using the number is a performance hack for common cases) - case 0: // self::COLON_STATE_TEXT: - switch( $c ) { - case "<": - // Could be either a <start> tag or an </end> tag - $state = self::COLON_STATE_TAGSTART; - break; - case ":": - if( $stack == 0 ) { - // We found it! - $before = substr( $str, 0, $i ); - $after = substr( $str, $i + 1 ); - wfProfileOut( $fname ); - return $i; - } - // Embedded in a tag; don't break it. - break; - default: - // Skip ahead looking for something interesting - $colon = strpos( $str, ':', $i ); - if( $colon === false ) { - // Nothing else interesting - wfProfileOut( $fname ); - return false; - } - $lt = strpos( $str, '<', $i ); - if( $stack === 0 ) { - if( $lt === false || $colon < $lt ) { - // We found it! - $before = substr( $str, 0, $colon ); - $after = substr( $str, $colon + 1 ); - wfProfileOut( $fname ); - return $i; - } - } - if( $lt === false ) { - // Nothing else interesting to find; abort! - // We're nested, but there's no close tags left. Abort! - break 2; - } - // Skip ahead to next tag start - $i = $lt; - $state = self::COLON_STATE_TAGSTART; - } - break; - case 1: // self::COLON_STATE_TAG: - // In a <tag> - switch( $c ) { - case ">": - $stack++; - $state = self::COLON_STATE_TEXT; - break; - case "/": - // Slash may be followed by >? - $state = self::COLON_STATE_TAGSLASH; - break; - default: - // ignore - } - break; - case 2: // self::COLON_STATE_TAGSTART: - switch( $c ) { - case "/": - $state = self::COLON_STATE_CLOSETAG; - break; - case "!": - $state = self::COLON_STATE_COMMENT; - break; - case ">": - // Illegal early close? This shouldn't happen D: - $state = self::COLON_STATE_TEXT; - break; - default: - $state = self::COLON_STATE_TAG; - } - break; - case 3: // self::COLON_STATE_CLOSETAG: - // In a </tag> - if( $c == ">" ) { - $stack--; - if( $stack < 0 ) { - wfDebug( "Invalid input in $fname; too many close tags\n" ); - wfProfileOut( $fname ); - return false; - } - $state = self::COLON_STATE_TEXT; - } - break; - case self::COLON_STATE_TAGSLASH: - if( $c == ">" ) { - // Yes, a self-closed tag <blah/> - $state = self::COLON_STATE_TEXT; - } else { - // Probably we're jumping the gun, and this is an attribute - $state = self::COLON_STATE_TAG; - } - break; - case 5: // self::COLON_STATE_COMMENT: - if( $c == "-" ) { - $state = self::COLON_STATE_COMMENTDASH; - } - break; - case self::COLON_STATE_COMMENTDASH: - if( $c == "-" ) { - $state = self::COLON_STATE_COMMENTDASHDASH; - } else { - $state = self::COLON_STATE_COMMENT; - } - break; - case self::COLON_STATE_COMMENTDASHDASH: - if( $c == ">" ) { - $state = self::COLON_STATE_TEXT; - } else { - $state = self::COLON_STATE_COMMENT; - } - break; - default: - throw new MWException( "State machine error in $fname" ); - } - } - if( $stack > 0 ) { - wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" ); - return false; - } - wfProfileOut( $fname ); - return false; - } - - /** - * Return value of a magic variable (like PAGENAME) - * - * @private - */ - function getVariableValue( $index ) { - global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath; - - /** - * Some of these require message or data lookups and can be - * expensive to check many times. - */ - static $varCache = array(); - if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) { - if ( isset( $varCache[$index] ) ) { - return $varCache[$index]; - } - } - - $ts = time(); - wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) ); - - # Use the time zone - global $wgLocaltimezone; - if ( isset( $wgLocaltimezone ) ) { - $oldtz = getenv( 'TZ' ); - putenv( 'TZ='.$wgLocaltimezone ); - } - - wfSuppressWarnings(); // E_STRICT system time bitching - $localTimestamp = date( 'YmdHis', $ts ); - $localMonth = date( 'm', $ts ); - $localMonthName = date( 'n', $ts ); - $localDay = date( 'j', $ts ); - $localDay2 = date( 'd', $ts ); - $localDayOfWeek = date( 'w', $ts ); - $localWeek = date( 'W', $ts ); - $localYear = date( 'Y', $ts ); - $localHour = date( 'H', $ts ); - if ( isset( $wgLocaltimezone ) ) { - putenv( 'TZ='.$oldtz ); - } - wfRestoreWarnings(); - - switch ( $index ) { - case 'currentmonth': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) ); - case 'currentmonthname': - return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) ); - case 'currentmonthnamegen': - return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) ); - case 'currentmonthabbrev': - return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) ); - case 'currentday': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) ); - case 'currentday2': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) ); - case 'localmonth': - return $varCache[$index] = $wgContLang->formatNum( $localMonth ); - case 'localmonthname': - return $varCache[$index] = $wgContLang->getMonthName( $localMonthName ); - case 'localmonthnamegen': - return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName ); - case 'localmonthabbrev': - return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName ); - case 'localday': - return $varCache[$index] = $wgContLang->formatNum( $localDay ); - case 'localday2': - return $varCache[$index] = $wgContLang->formatNum( $localDay2 ); - case 'pagename': - return wfEscapeWikiText( $this->mTitle->getText() ); - case 'pagenamee': - return $this->mTitle->getPartialURL(); - case 'fullpagename': - return wfEscapeWikiText( $this->mTitle->getPrefixedText() ); - case 'fullpagenamee': - return $this->mTitle->getPrefixedURL(); - case 'subpagename': - return wfEscapeWikiText( $this->mTitle->getSubpageText() ); - case 'subpagenamee': - return $this->mTitle->getSubpageUrlForm(); - case 'basepagename': - return wfEscapeWikiText( $this->mTitle->getBaseText() ); - case 'basepagenamee': - return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ); - case 'talkpagename': - if( $this->mTitle->canTalk() ) { - $talkPage = $this->mTitle->getTalkPage(); - return wfEscapeWikiText( $talkPage->getPrefixedText() ); - } else { - return ''; - } - case 'talkpagenamee': - if( $this->mTitle->canTalk() ) { - $talkPage = $this->mTitle->getTalkPage(); - return $talkPage->getPrefixedUrl(); - } else { - return ''; - } - case 'subjectpagename': - $subjPage = $this->mTitle->getSubjectPage(); - return wfEscapeWikiText( $subjPage->getPrefixedText() ); - case 'subjectpagenamee': - $subjPage = $this->mTitle->getSubjectPage(); - return $subjPage->getPrefixedUrl(); - case 'revisionid': - return $this->mRevisionId; - case 'revisionday': - return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) ); - case 'revisionday2': - return substr( $this->getRevisionTimestamp(), 6, 2 ); - case 'revisionmonth': - return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) ); - case 'revisionyear': - return substr( $this->getRevisionTimestamp(), 0, 4 ); - case 'revisiontimestamp': - return $this->getRevisionTimestamp(); - case 'namespace': - return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) ); - case 'namespacee': - return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); - case 'talkspace': - return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : ''; - case 'talkspacee': - return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : ''; - case 'subjectspace': - return $this->mTitle->getSubjectNsText(); - case 'subjectspacee': - return( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); - case 'currentdayname': - return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 ); - case 'currentyear': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true ); - case 'currenttime': - return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false ); - case 'currenthour': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true ); - case 'currentweek': - // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to - // int to remove the padding - return $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) ); - case 'currentdow': - return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) ); - case 'localdayname': - return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 ); - case 'localyear': - return $varCache[$index] = $wgContLang->formatNum( $localYear, true ); - case 'localtime': - return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false ); - case 'localhour': - return $varCache[$index] = $wgContLang->formatNum( $localHour, true ); - case 'localweek': - // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to - // int to remove the padding - return $varCache[$index] = $wgContLang->formatNum( (int)$localWeek ); - case 'localdow': - return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek ); - case 'numberofarticles': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() ); - case 'numberoffiles': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() ); - case 'numberofusers': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() ); - case 'numberofpages': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() ); - case 'numberofadmins': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::admins() ); - case 'numberofedits': - return $varCache[$index] = $wgContLang->formatNum( SiteStats::edits() ); - case 'currenttimestamp': - return $varCache[$index] = wfTimestampNow(); - case 'localtimestamp': - return $varCache[$index] = $localTimestamp; - case 'currentversion': - return $varCache[$index] = SpecialVersion::getVersion(); - case 'sitename': - return $wgSitename; - case 'server': - return $wgServer; - case 'servername': - return $wgServerName; - case 'scriptpath': - return $wgScriptPath; - case 'directionmark': - return $wgContLang->getDirMark(); - case 'contentlanguage': - global $wgContLanguageCode; - return $wgContLanguageCode; - default: - $ret = null; - if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) ) - return $ret; - else - return null; - } - } - - /** - * initialise the magic variables (like CURRENTMONTHNAME) - * - * @private - */ - function initialiseVariables() { - $fname = 'Parser::initialiseVariables'; - wfProfileIn( $fname ); - $variableIDs = MagicWord::getVariableIDs(); - - $this->mVariables = array(); - foreach ( $variableIDs as $id ) { - $mw =& MagicWord::get( $id ); - $mw->addToArray( $this->mVariables, $id ); - } - wfProfileOut( $fname ); - } - - /** - * parse any parentheses in format ((title|part|part)) - * and call callbacks to get a replacement text for any found piece - * - * @param string $text The text to parse - * @param array $callbacks rules in form: - * '{' => array( # opening parentheses - * 'end' => '}', # closing parentheses - * 'cb' => array(2 => callback, # replacement callback to call if {{..}} is found - * 3 => callback # replacement callback to call if {{{..}}} is found - * ) - * ) - * 'min' => 2, # Minimum parenthesis count in cb - * 'max' => 3, # Maximum parenthesis count in cb - * @private - */ - function replace_callback ($text, $callbacks) { - wfProfileIn( __METHOD__ ); - $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet - $lastOpeningBrace = -1; # last not closed parentheses - - $validOpeningBraces = implode( '', array_keys( $callbacks ) ); - - $i = 0; - while ( $i < strlen( $text ) ) { - # Find next opening brace, closing brace or pipe - if ( $lastOpeningBrace == -1 ) { - $currentClosing = ''; - $search = $validOpeningBraces; - } else { - $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd']; - $search = $validOpeningBraces . '|' . $currentClosing; - } - $rule = null; - $i += strcspn( $text, $search, $i ); - if ( $i < strlen( $text ) ) { - if ( $text[$i] == '|' ) { - $found = 'pipe'; - } elseif ( $text[$i] == $currentClosing ) { - $found = 'close'; - } elseif ( isset( $callbacks[$text[$i]] ) ) { - $found = 'open'; - $rule = $callbacks[$text[$i]]; - } else { - # Some versions of PHP have a strcspn which stops on null characters - # Ignore and continue - ++$i; - continue; - } - } else { - # All done - break; - } - - if ( $found == 'open' ) { - # found opening brace, let's add it to parentheses stack - $piece = array('brace' => $text[$i], - 'braceEnd' => $rule['end'], - 'title' => '', - 'parts' => null); - - # count opening brace characters - $piece['count'] = strspn( $text, $piece['brace'], $i ); - $piece['startAt'] = $piece['partStart'] = $i + $piece['count']; - $i += $piece['count']; - - # we need to add to stack only if opening brace count is enough for one of the rules - if ( $piece['count'] >= $rule['min'] ) { - $lastOpeningBrace ++; - $openingBraceStack[$lastOpeningBrace] = $piece; - } - } elseif ( $found == 'close' ) { - # lets check if it is enough characters for closing brace - $maxCount = $openingBraceStack[$lastOpeningBrace]['count']; - $count = strspn( $text, $text[$i], $i, $maxCount ); - - # check for maximum matching characters (if there are 5 closing - # characters, we will probably need only 3 - depending on the rules) - $matchingCount = 0; - $matchingCallback = null; - $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]; - if ( $count > $cbType['max'] ) { - # The specified maximum exists in the callback array, unless the caller - # has made an error - $matchingCount = $cbType['max']; - } else { - # Count is less than the maximum - # Skip any gaps in the callback array to find the true largest match - # Need to use array_key_exists not isset because the callback can be null - $matchingCount = $count; - while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) { - --$matchingCount; - } - } - - if ($matchingCount <= 0) { - $i += $count; - continue; - } - $matchingCallback = $cbType['cb'][$matchingCount]; - - # let's set a title or last part (if '|' was found) - if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { - $openingBraceStack[$lastOpeningBrace]['title'] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], - $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - } else { - $openingBraceStack[$lastOpeningBrace]['parts'][] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], - $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - } - - $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount; - $pieceEnd = $i + $matchingCount; - - if( is_callable( $matchingCallback ) ) { - $cbArgs = array ( - 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart), - 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']), - 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'], - 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")), - ); - # finally we can call a user callback and replace piece of text - $replaceWith = call_user_func( $matchingCallback, $cbArgs ); - $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd); - $i = $pieceStart + strlen($replaceWith); - } else { - # null value for callback means that parentheses should be parsed, but not replaced - $i += $matchingCount; - } - - # reset last opening parentheses, but keep it in case there are unused characters - $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'], - 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'], - 'count' => $openingBraceStack[$lastOpeningBrace]['count'], - 'title' => '', - 'parts' => null, - 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']); - $openingBraceStack[$lastOpeningBrace--] = null; - - if ($matchingCount < $piece['count']) { - $piece['count'] -= $matchingCount; - $piece['startAt'] -= $matchingCount; - $piece['partStart'] = $piece['startAt']; - # do we still qualify for any callback with remaining count? - $currentCbList = $callbacks[$piece['brace']]['cb']; - while ( $piece['count'] ) { - if ( array_key_exists( $piece['count'], $currentCbList ) ) { - $lastOpeningBrace++; - $openingBraceStack[$lastOpeningBrace] = $piece; - break; - } - --$piece['count']; - } - } - } elseif ( $found == 'pipe' ) { - # lets set a title if it is a first separator, or next part otherwise - if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { - $openingBraceStack[$lastOpeningBrace]['title'] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], - $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - $openingBraceStack[$lastOpeningBrace]['parts'] = array(); - } else { - $openingBraceStack[$lastOpeningBrace]['parts'][] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], - $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - } - $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i; - } - } - - wfProfileOut( __METHOD__ ); - return $text; - } - - /** - * Replace magic variables, templates, and template arguments - * with the appropriate text. Templates are substituted recursively, - * taking care to avoid infinite loops. - * - * Note that the substitution depends on value of $mOutputType: - * self::OT_WIKI: only {{subst:}} templates - * self::OT_MSG: only magic variables - * self::OT_HTML: all templates and magic variables - * - * @param string $tex The text to transform - * @param array $args Key-value pairs representing template parameters to substitute - * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion - * @private - */ - function replaceVariables( $text, $args = array(), $argsOnly = false ) { - # Prevent too big inclusions - if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) { - return $text; - } - - $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; - wfProfileIn( $fname ); - - # This function is called recursively. To keep track of arguments we need a stack: - array_push( $this->mArgStack, $args ); - - $braceCallbacks = array(); - if ( !$argsOnly ) { - $braceCallbacks[2] = array( &$this, 'braceSubstitution' ); - } - if ( $this->mOutputType != self::OT_MSG ) { - $braceCallbacks[3] = array( &$this, 'argSubstitution' ); - } - if ( $braceCallbacks ) { - $callbacks = array( - '{' => array( - 'end' => '}', - 'cb' => $braceCallbacks, - 'min' => $argsOnly ? 3 : 2, - 'max' => isset( $braceCallbacks[3] ) ? 3 : 2, - ), - '[' => array( - 'end' => ']', - 'cb' => array(2=>null), - 'min' => 2, - 'max' => 2, - ) - ); - $text = $this->replace_callback ($text, $callbacks); - - array_pop( $this->mArgStack ); - } - wfProfileOut( $fname ); - return $text; - } - - /** - * Replace magic variables - * @private - */ - function variableSubstitution( $matches ) { - global $wgContLang; - $fname = 'Parser::variableSubstitution'; - $varname = $wgContLang->lc($matches[1]); - wfProfileIn( $fname ); - $skip = false; - if ( $this->mOutputType == self::OT_WIKI ) { - # Do only magic variables prefixed by SUBST - $mwSubst =& MagicWord::get( 'subst' ); - if (!$mwSubst->matchStartAndRemove( $varname )) - $skip = true; - # Note that if we don't substitute the variable below, - # we don't remove the {{subst:}} magic word, in case - # it is a template rather than a magic variable. - } - if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) { - $id = $this->mVariables[$varname]; - # Now check if we did really match, case sensitive or not - $mw =& MagicWord::get( $id ); - if ($mw->match($matches[1])) { - $text = $this->getVariableValue( $id ); - if (MagicWord::getCacheTTL($id)>-1) - $this->mOutput->mContainsOldMagic = true; - } else { - $text = $matches[0]; - } - } else { - $text = $matches[0]; - } - wfProfileOut( $fname ); - return $text; - } - - - /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too. - static function createAssocArgs( $args ) { - $assocArgs = array(); - $index = 1; - foreach( $args as $arg ) { - $eqpos = strpos( $arg, '=' ); - if ( $eqpos === false ) { - $assocArgs[$index++] = $arg; - } else { - $name = trim( substr( $arg, 0, $eqpos ) ); - $value = trim( substr( $arg, $eqpos+1 ) ); - if ( $value === false ) { - $value = ''; - } - if ( $name !== false ) { - $assocArgs[$name] = $value; - } - } - } - - return $assocArgs; - } - - /** - * Return the text of a template, after recursively - * replacing any variables or templates within the template. - * - * @param array $piece The parts of the template - * $piece['text']: matched text - * $piece['title']: the title, i.e. the part before the | - * $piece['parts']: the parameter array - * @return string the text of the template - * @private - */ - function braceSubstitution( $piece ) { - global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces; - $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; - wfProfileIn( $fname ); - wfProfileIn( __METHOD__.'-setup' ); - - # Flags - $found = false; # $text has been filled - $nowiki = false; # wiki markup in $text should be escaped - $noparse = false; # Unsafe HTML tags should not be stripped, etc. - $noargs = false; # Don't replace triple-brace arguments in $text - $replaceHeadings = false; # Make the edit section links go to the template not the article - $headingOffset = 0; # Skip headings when number, to account for those that weren't transcluded. - $isHTML = false; # $text is HTML, armour it against wikitext transformation - $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered - - # Title object, where $text came from - $title = NULL; - - $linestart = ''; - - - # $part1 is the bit before the first |, and must contain only title characters - # $args is a list of arguments, starting from index 0, not including $part1 - - $titleText = $part1 = $piece['title']; - # If the third subpattern matched anything, it will start with | - - if (null == $piece['parts']) { - $replaceWith = $this->variableSubstitution (array ($piece['text'], $piece['title'])); - if ($replaceWith != $piece['text']) { - $text = $replaceWith; - $found = true; - $noparse = true; - $noargs = true; - } - } - - $args = (null == $piece['parts']) ? array() : $piece['parts']; - wfProfileOut( __METHOD__.'-setup' ); - - # SUBST - wfProfileIn( __METHOD__.'-modifiers' ); - if ( !$found ) { - $mwSubst =& MagicWord::get( 'subst' ); - if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) { - # One of two possibilities is true: - # 1) Found SUBST but not in the PST phase - # 2) Didn't find SUBST and in the PST phase - # In either case, return without further processing - $text = $piece['text']; - $found = true; - $noparse = true; - $noargs = true; - } - } - - # MSG, MSGNW and RAW - if ( !$found ) { - # Check for MSGNW: - $mwMsgnw =& MagicWord::get( 'msgnw' ); - if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) { - $nowiki = true; - } else { - # Remove obsolete MSG: - $mwMsg =& MagicWord::get( 'msg' ); - $mwMsg->matchStartAndRemove( $part1 ); - } - - # Check for RAW: - $mwRaw =& MagicWord::get( 'raw' ); - if ( $mwRaw->matchStartAndRemove( $part1 ) ) { - $forceRawInterwiki = true; - } - } - wfProfileOut( __METHOD__.'-modifiers' ); - - //save path level before recursing into functions & templates. - $lastPathLevel = $this->mTemplatePath; - - # Parser functions - if ( !$found ) { - wfProfileIn( __METHOD__ . '-pfunc' ); - - $colonPos = strpos( $part1, ':' ); - if ( $colonPos !== false ) { - # Case sensitive functions - $function = substr( $part1, 0, $colonPos ); - if ( isset( $this->mFunctionSynonyms[1][$function] ) ) { - $function = $this->mFunctionSynonyms[1][$function]; - } else { - # Case insensitive functions - $function = strtolower( $function ); - if ( isset( $this->mFunctionSynonyms[0][$function] ) ) { - $function = $this->mFunctionSynonyms[0][$function]; - } else { - $function = false; - } - } - if ( $function ) { - $funcArgs = array_map( 'trim', $args ); - $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs ); - $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs ); - $found = true; - - // The text is usually already parsed, doesn't need triple-brace tags expanded, etc. - //$noargs = true; - //$noparse = true; - - if ( is_array( $result ) ) { - if ( isset( $result[0] ) ) { - $text = $linestart . $result[0]; - unset( $result[0] ); - } - - // Extract flags into the local scope - // This allows callers to set flags such as nowiki, noparse, found, etc. - extract( $result ); - } else { - $text = $linestart . $result; - } - } - } - wfProfileOut( __METHOD__ . '-pfunc' ); - } - - # Template table test - - # Did we encounter this template already? If yes, it is in the cache - # and we need to check for loops. - if ( !$found && isset( $this->mTemplates[$piece['title']] ) ) { - $found = true; - - # Infinite loop test - if ( isset( $this->mTemplatePath[$part1] ) ) { - $noparse = true; - $noargs = true; - $found = true; - $text = $linestart . - "[[$part1]]<!-- WARNING: template loop detected -->"; - wfDebug( __METHOD__.": template loop broken at '$part1'\n" ); - } else { - # set $text to cached message. - $text = $linestart . $this->mTemplates[$piece['title']]; - #treat title for cached page the same as others - $ns = NS_TEMPLATE; - $subpage = ''; - $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); - if ($subpage !== '') { - $ns = $this->mTitle->getNamespace(); - } - $title = Title::newFromText( $part1, $ns ); - //used by include size checking - $titleText = $title->getPrefixedText(); - //used by edit section links - $replaceHeadings = true; - - } - } - - # Load from database - if ( !$found ) { - wfProfileIn( __METHOD__ . '-loadtpl' ); - $ns = NS_TEMPLATE; - # declaring $subpage directly in the function call - # does not work correctly with references and breaks - # {{/subpage}}-style inclusions - $subpage = ''; - $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); - if ($subpage !== '') { - $ns = $this->mTitle->getNamespace(); - } - $title = Title::newFromText( $part1, $ns ); - - - if ( !is_null( $title ) ) { - $titleText = $title->getPrefixedText(); - # Check for language variants if the template is not found - if($wgContLang->hasVariants() && $title->getArticleID() == 0){ - $wgContLang->findVariantLink($part1, $title); - } - - if ( !$title->isExternal() ) { - if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) { - $text = SpecialPage::capturePath( $title ); - if ( is_string( $text ) ) { - $found = true; - $noparse = true; - $noargs = true; - $isHTML = true; - $this->disableCache(); - } - } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) { - $found = false; //access denied - wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() ); - } else { - list($articleContent,$title) = $this->fetchTemplateAndtitle( $title ); - if ( $articleContent !== false ) { - $found = true; - $text = $articleContent; - $replaceHeadings = true; - } - } - - # If the title is valid but undisplayable, make a link to it - if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) { - $text = "[[:$titleText]]"; - $found = true; - } - } elseif ( $title->isTrans() ) { - // Interwiki transclusion - if ( $this->ot['html'] && !$forceRawInterwiki ) { - $text = $this->interwikiTransclude( $title, 'render' ); - $isHTML = true; - $noparse = true; - } else { - $text = $this->interwikiTransclude( $title, 'raw' ); - $replaceHeadings = true; - } - $found = true; - } - - # Template cache array insertion - # Use the original $piece['title'] not the mangled $part1, so that - # modifiers such as RAW: produce separate cache entries - if( $found ) { - if( $isHTML ) { - // A special page; don't store it in the template cache. - } else { - $this->mTemplates[$piece['title']] = $text; - } - $text = $linestart . $text; - } - } - wfProfileOut( __METHOD__ . '-loadtpl' ); - } - - if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) { - # Error, oversize inclusion - $text = $linestart . - "[[$titleText]]<!-- WARNING: template omitted, pre-expand include size too large -->"; - $noparse = true; - $noargs = true; - } - - # Recursive parsing, escaping and link table handling - # Only for HTML output - if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) { - $text = wfEscapeWikiText( $text ); - } elseif ( !$this->ot['msg'] && $found ) { - if ( $noargs ) { - $assocArgs = array(); - } else { - # Clean up argument array - $assocArgs = self::createAssocArgs($args); - # Add a new element to the templace recursion path - $this->mTemplatePath[$part1] = 1; - } - - if ( !$noparse ) { - # If there are any <onlyinclude> tags, only include them - if ( in_string( '<onlyinclude>', $text ) && in_string( '</onlyinclude>', $text ) ) { - $replacer = new OnlyIncludeReplacer; - StringUtils::delimiterReplaceCallback( '<onlyinclude>', '</onlyinclude>', - array( &$replacer, 'replace' ), $text ); - $text = $replacer->output; - } - # Remove <noinclude> sections and <includeonly> tags - $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text ); - $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) ); - - if( $this->ot['html'] || $this->ot['pre'] ) { - # Strip <nowiki>, <pre>, etc. - $text = $this->strip( $text, $this->mStripState ); - if ( $this->ot['html'] ) { - $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs ); - } elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) { - $text = Sanitizer::removeHTMLcomments( $text ); - } - } - $text = $this->replaceVariables( $text, $assocArgs ); - - # If the template begins with a table or block-level - # element, it should be treated as beginning a new line. - if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{ - $text = "\n" . $text; - } - } elseif ( !$noargs ) { - # $noparse and !$noargs - # Just replace the arguments, not any double-brace items - # This is used for rendered interwiki transclusion - $text = $this->replaceVariables( $text, $assocArgs, true ); - } - } - # Prune lower levels off the recursion check path - $this->mTemplatePath = $lastPathLevel; - - if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) { - # Error, oversize inclusion - $text = $linestart . - "[[$titleText]]<!-- WARNING: template omitted, post-expand include size too large -->"; - $noparse = true; - $noargs = true; - } - - if ( !$found ) { - wfProfileOut( $fname ); - return $piece['text']; - } else { - wfProfileIn( __METHOD__ . '-placeholders' ); - if ( $isHTML ) { - # Replace raw HTML by a placeholder - # Add a blank line preceding, to prevent it from mucking up - # immediately preceding headings - $text = "\n\n" . $this->insertStripItem( $text, $this->mStripState ); - } else { - # replace ==section headers== - # XXX this needs to go away once we have a better parser. - if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) { - if( !is_null( $title ) ) - $encodedname = base64_encode($title->getPrefixedDBkey()); - else - $encodedname = base64_encode(""); - $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1, - PREG_SPLIT_DELIM_CAPTURE); - $text = ''; - $nsec = $headingOffset; - - for( $i = 0; $i < count($m); $i += 2 ) { - $text .= $m[$i]; - if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue; - $hl = $m[$i + 1]; - if( strstr($hl, "<!--MWTEMPLATESECTION") ) { - $text .= $hl; - continue; - } - $m2 = array(); - preg_match('/^(={1,6})(.*?)(={1,6}\s*?)$/m', $hl, $m2); - $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION=" - . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3]; - - $nsec++; - } - } - } - wfProfileOut( __METHOD__ . '-placeholders' ); - } - - # Prune lower levels off the recursion check path - $this->mTemplatePath = $lastPathLevel; - - if ( !$found ) { - wfProfileOut( $fname ); - return $piece['text']; - } else { - wfProfileOut( $fname ); - return $text; - } - } - - /** - * Fetch the unparsed text of a template and register a reference to it. - */ - function fetchTemplateAndTitle( $title ) { - $templateCb = $this->mOptions->getTemplateCallback(); - $stuff = call_user_func( $templateCb, $title, $this ); - $text = $stuff['text']; - $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title; - if ( isset( $stuff['deps'] ) ) { - foreach ( $stuff['deps'] as $dep ) { - $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); - } - } - return array($text,$finalTitle); - } - - function fetchTemplate( $title ) { - $rv = $this->fetchTemplateAndtitle($title); - return $rv[0]; - } - - /** - * Static function to get a template - * Can be overridden via ParserOptions::setTemplateCallback(). - * - * Returns an associative array: - * text The unparsed template text - * finalTitle (Optional) The title after following redirects - * deps (Optional) An array of associative array dependencies: - * title: The dependency title, to be registered in templatelinks - * page_id: The page_id of the title - * rev_id: The revision ID loaded - */ - static function statelessFetchTemplate( $title, $parser=false ) { - $text = $skip = false; - $finalTitle = $title; - $deps = array(); - - // Loop to fetch the article, with up to 1 redirect - for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) { - # Give extensions a chance to select the revision instead - $id = false; // Assume current - wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) ); - - if( $skip ) { - $text = false; - $deps[] = array( - 'title' => $title, - 'page_id' => $title->getArticleID(), - 'rev_id' => null ); - break; - } - $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title ); - $rev_id = $rev ? $rev->getId() : 0; - - $deps[] = array( - 'title' => $title, - 'page_id' => $title->getArticleID(), - 'rev_id' => $rev_id ); - - if( $rev ) { - $text = $rev->getText(); - } elseif( $title->getNamespace() == NS_MEDIAWIKI ) { - global $wgLang; - $message = $wgLang->lcfirst( $title->getText() ); - $text = wfMsgForContentNoTrans( $message ); - if( wfEmptyMsg( $message, $text ) ) { - $text = false; - break; - } - } else { - break; - } - if ( $text === false ) { - break; - } - // Redirect? - $finalTitle = $title; - $title = Title::newFromRedirect( $text ); - } - return array( - 'text' => $text, - 'finalTitle' => $finalTitle, - 'deps' => $deps ); - } - - /** - * Transclude an interwiki link. - */ - function interwikiTransclude( $title, $action ) { - global $wgEnableScaryTranscluding; - - if (!$wgEnableScaryTranscluding) - return wfMsg('scarytranscludedisabled'); - - $url = $title->getFullUrl( "action=$action" ); - - if (strlen($url) > 255) - return wfMsg('scarytranscludetoolong'); - return $this->fetchScaryTemplateMaybeFromCache($url); - } - - function fetchScaryTemplateMaybeFromCache($url) { - global $wgTranscludeCacheExpiry; - $dbr = wfGetDB(DB_SLAVE); - $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'), - array('tc_url' => $url)); - if ($obj) { - $time = $obj->tc_time; - $text = $obj->tc_contents; - if ($time && time() < $time + $wgTranscludeCacheExpiry ) { - return $text; - } - } - - $text = Http::get($url); - if (!$text) - return wfMsg('scarytranscludefailed', $url); - - $dbw = wfGetDB(DB_MASTER); - $dbw->replace('transcache', array('tc_url'), array( - 'tc_url' => $url, - 'tc_time' => time(), - 'tc_contents' => $text)); - return $text; - } - - - /** - * Triple brace replacement -- used for template arguments - * @private - */ - function argSubstitution( $matches ) { - $arg = trim( $matches['title'] ); - $text = $matches['text']; - $inputArgs = end( $this->mArgStack ); - - if ( array_key_exists( $arg, $inputArgs ) ) { - $text = $inputArgs[$arg]; - } else if (($this->mOutputType == self::OT_HTML || $this->mOutputType == self::OT_PREPROCESS ) && - null != $matches['parts'] && count($matches['parts']) > 0) { - $text = $matches['parts'][0]; - } - if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) { - $text = $matches['text'] . - '<!-- WARNING: argument omitted, expansion size too large -->'; - } - - return $text; - } - - /** - * Increment an include size counter - * - * @param string $type The type of expansion - * @param integer $size The size of the text - * @return boolean False if this inclusion would take it over the maximum, true otherwise - */ - function incrementIncludeSize( $type, $size ) { - if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) { - return false; - } else { - $this->mIncludeSizes[$type] += $size; - return true; - } - } - - /** - * Detect __NOGALLERY__ magic word and set a placeholder - */ - function stripNoGallery( &$text ) { - # if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML, - # do not add TOC - $mw = MagicWord::get( 'nogallery' ); - $this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ; - } - - /** - * Find the first __TOC__ magic word and set a <!--MWTOC--> - * placeholder that will then be replaced by the real TOC in - * ->formatHeadings, this works because at this points real - * comments will have already been discarded by the sanitizer. - * - * Any additional __TOC__ magic words left over will be discarded - * as there can only be one TOC on the page. - */ - function stripToc( $text ) { - # if the string __NOTOC__ (not case-sensitive) occurs in the HTML, - # do not add TOC - $mw = MagicWord::get( 'notoc' ); - if( $mw->matchAndRemove( $text ) ) { - $this->mShowToc = false; - } - - $mw = MagicWord::get( 'toc' ); - if( $mw->match( $text ) ) { - $this->mShowToc = true; - $this->mForceTocPosition = true; - - // Set a placeholder. At the end we'll fill it in with the TOC. - $text = $mw->replace( '<!--MWTOC-->', $text, 1 ); - - // Only keep the first one. - $text = $mw->replace( '', $text ); - } - return $text; - } - - /** - * This function accomplishes several tasks: - * 1) Auto-number headings if that option is enabled - * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page - * 3) Add a Table of contents on the top for users who have enabled the option - * 4) Auto-anchor headings - * - * It loops through all headlines, collects the necessary data, then splits up the - * string and re-inserts the newly formatted headlines. - * - * @param string $text - * @param boolean $isMain - * @private - */ - function formatHeadings( $text, $isMain=true ) { - global $wgMaxTocLevel, $wgContLang; - - $doNumberHeadings = $this->mOptions->getNumberHeadings(); - if( !$this->mTitle->quickUserCan( 'edit' ) ) { - $showEditLink = 0; - } else { - $showEditLink = $this->mOptions->getEditSection(); - } - - # Inhibit editsection links if requested in the page - $esw =& MagicWord::get( 'noeditsection' ); - if( $esw->matchAndRemove( $text ) ) { - $showEditLink = 0; - } - - # Get all headlines for numbering them and adding funky stuff like [edit] - # links - this is for later, but we need the number of headlines right now - $matches = array(); - $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches ); - - # if there are fewer than 4 headlines in the article, do not show TOC - # unless it's been explicitly enabled. - $enoughToc = $this->mShowToc && - (($numMatches >= 4) || $this->mForceTocPosition); - - # Allow user to stipulate that a page should have a "new section" - # link added via __NEWSECTIONLINK__ - $mw =& MagicWord::get( 'newsectionlink' ); - if( $mw->matchAndRemove( $text ) ) - $this->mOutput->setNewSection( true ); - - # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML, - # override above conditions and always show TOC above first header - $mw =& MagicWord::get( 'forcetoc' ); - if ($mw->matchAndRemove( $text ) ) { - $this->mShowToc = true; - $enoughToc = true; - } - - # We need this to perform operations on the HTML - $sk = $this->mOptions->getSkin(); - - # headline counter - $headlineCount = 0; - $sectionCount = 0; # headlineCount excluding template sections - $numVisible = 0; - - # Ugh .. the TOC should have neat indentation levels which can be - # passed to the skin functions. These are determined here - $toc = ''; - $full = ''; - $head = array(); - $sublevelCount = array(); - $levelCount = array(); - $toclevel = 0; - $level = 0; - $prevlevel = 0; - $toclevel = 0; - $prevtoclevel = 0; - $tocraw = array(); - - foreach( $matches[3] as $headline ) { - $istemplate = 0; - $templatetitle = ''; - $templatesection = 0; - $numbering = ''; - $mat = array(); - if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) { - $istemplate = 1; - $templatetitle = base64_decode($mat[1]); - $templatesection = 1 + (int)base64_decode($mat[2]); - $headline = preg_replace("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", "", $headline); - } - - if( $toclevel ) { - $prevlevel = $level; - $prevtoclevel = $toclevel; - } - $level = $matches[1][$headlineCount]; - - if( $doNumberHeadings || $enoughToc ) { - - if ( $level > $prevlevel ) { - # Increase TOC level - $toclevel++; - $sublevelCount[$toclevel] = 0; - if( $toclevel<$wgMaxTocLevel ) { - $prevtoclevel = $toclevel; - $toc .= $sk->tocIndent(); - $numVisible++; - } - } - elseif ( $level < $prevlevel && $toclevel > 1 ) { - # Decrease TOC level, find level to jump to - - if ( $toclevel == 2 && $level <= $levelCount[1] ) { - # Can only go down to level 1 - $toclevel = 1; - } else { - for ($i = $toclevel; $i > 0; $i--) { - if ( $levelCount[$i] == $level ) { - # Found last matching level - $toclevel = $i; - break; - } - elseif ( $levelCount[$i] < $level ) { - # Found first matching level below current level - $toclevel = $i + 1; - break; - } - } - } - if( $toclevel<$wgMaxTocLevel ) { - if($prevtoclevel < $wgMaxTocLevel) { - # Unindent only if the previous toc level was shown :p - $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel ); - } else { - $toc .= $sk->tocLineEnd(); - } - } - } - else { - # No change in level, end TOC line - if( $toclevel<$wgMaxTocLevel ) { - $toc .= $sk->tocLineEnd(); - } - } - - $levelCount[$toclevel] = $level; - - # count number of headlines for each level - @$sublevelCount[$toclevel]++; - $dot = 0; - for( $i = 1; $i <= $toclevel; $i++ ) { - if( !empty( $sublevelCount[$i] ) ) { - if( $dot ) { - $numbering .= '.'; - } - $numbering .= $wgContLang->formatNum( $sublevelCount[$i] ); - $dot = 1; - } - } - } - - # The canonized header is a version of the header text safe to use for links - # Avoid insertion of weird stuff like <math> by expanding the relevant sections - $canonized_headline = $this->mStripState->unstripBoth( $headline ); - - # Remove link placeholders by the link text. - # <!--LINK number--> - # turns into - # link text with suffix - $canonized_headline = preg_replace( '/<!--LINK ([0-9]*)-->/e', - "\$this->mLinkHolders['texts'][\$1]", - $canonized_headline ); - $canonized_headline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e', - "\$this->mInterwikiLinkHolders['texts'][\$1]", - $canonized_headline ); - - # Strip out HTML (other than plain <sup> and <sub>: bug 8393) - $tocline = preg_replace( - array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ), - array( '', '<$1>'), - $canonized_headline - ); - $tocline = trim( $tocline ); - - # For the anchor, strip out HTML-y stuff period - $canonized_headline = preg_replace( '/<.*?'.'>/', '', $canonized_headline ); - $canonized_headline = trim( $canonized_headline ); - - # Save headline for section edit hint before it's escaped - $headline_hint = $canonized_headline; - $canonized_headline = Sanitizer::escapeId( $canonized_headline ); - $refers[$headlineCount] = $canonized_headline; - - # count how many in assoc. array so we can track dupes in anchors - isset( $refers[$canonized_headline] ) ? $refers[$canonized_headline]++ : $refers[$canonized_headline] = 1; - $refcount[$headlineCount]=$refers[$canonized_headline]; - - # Don't number the heading if it is the only one (looks silly) - if( $doNumberHeadings && count( $matches[3] ) > 1) { - # the two are different if the line contains a link - $headline=$numbering . ' ' . $headline; - } - - # Create the anchor for linking from the TOC to the section - $anchor = $canonized_headline; - if($refcount[$headlineCount] > 1 ) { - $anchor .= '_' . $refcount[$headlineCount]; - } - if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) { - $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel); - $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering ); - } - # give headline the correct <h#> tag - if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) { - if( $istemplate ) - $editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection); - else - $editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint); - } else { - $editlink = ''; - } - $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink ); - - $headlineCount++; - if( !$istemplate ) - $sectionCount++; - } - - $this->mOutput->setSections( $tocraw ); - - # Never ever show TOC if no headers - if( $numVisible < 1 ) { - $enoughToc = false; - } - - if( $enoughToc ) { - if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) { - $toc .= $sk->tocUnindent( $prevtoclevel - 1 ); - } - $toc = $sk->tocList( $toc ); - } - - # split up and insert constructed headlines - - $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text ); - $i = 0; - - foreach( $blocks as $block ) { - if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) { - # This is the [edit] link that appears for the top block of text when - # section editing is enabled - - # Disabled because it broke block formatting - # For example, a bullet point in the top line - # $full .= $sk->editSectionLink(0); - } - $full .= $block; - if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) { - # Top anchor now in skin - $full = $full.$toc; - } - - if( !empty( $head[$i] ) ) { - $full .= $head[$i]; - } - $i++; - } - if( $this->mForceTocPosition ) { - return str_replace( '<!--MWTOC-->', $toc, $full ); - } else { - return $full; - } - } - - /** - * Transform wiki markup when saving a page by doing \r\n -> \n - * conversion, substitting signatures, {{subst:}} templates, etc. - * - * @param string $text the text to transform - * @param Title &$title the Title object for the current article - * @param User &$user the User object describing the current user - * @param ParserOptions $options parsing options - * @param bool $clearState whether to clear the parser state first - * @return string the altered wiki markup - * @public - */ - function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) { - $this->mOptions = $options; - $this->mTitle =& $title; - $this->setOutputType( self::OT_WIKI ); - - if ( $clearState ) { - $this->clearState(); - } - - $stripState = new StripState; - $pairs = array( - "\r\n" => "\n", - ); - $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text ); - $text = $this->strip( $text, $stripState, true, array( 'gallery' ) ); - $text = $this->pstPass2( $text, $stripState, $user ); - $text = $stripState->unstripBoth( $text ); - return $text; - } - - /** - * Pre-save transform helper function - * @private - */ - function pstPass2( $text, &$stripState, $user ) { - global $wgContLang, $wgLocaltimezone; - - /* Note: This is the timestamp saved as hardcoded wikitext to - * the database, we use $wgContLang here in order to give - * everyone the same signature and use the default one rather - * than the one selected in each user's preferences. - */ - if ( isset( $wgLocaltimezone ) ) { - $oldtz = getenv( 'TZ' ); - putenv( 'TZ='.$wgLocaltimezone ); - } - $d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) . - ' (' . date( 'T' ) . ')'; - if ( isset( $wgLocaltimezone ) ) { - putenv( 'TZ='.$oldtz ); - } - - # Variable replacement - # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags - $text = $this->replaceVariables( $text ); - - # Strip out <nowiki> etc. added via replaceVariables - $text = $this->strip( $text, $stripState, false, array( 'gallery' ) ); - - # Signatures - $sigText = $this->getUserSig( $user ); - $text = strtr( $text, array( - '~~~~~' => $d, - '~~~~' => "$sigText $d", - '~~~' => $sigText - ) ); - - # Context links: [[|name]] and [[name (context)|]] - # - global $wgLegalTitleChars; - $tc = "[$wgLegalTitleChars]"; - $nc = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii! - - $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]] - $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]] - $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] - - # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" - $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text ); - $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text ); - - $t = $this->mTitle->getText(); - $m = array(); - if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) { - $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); - } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) { - $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); - } else { - # if there's no context, don't bother duplicating the title - $text = preg_replace( $p2, '[[\\1]]', $text ); - } - - # Trim trailing whitespace - $text = rtrim( $text ); - - return $text; - } - - /** - * Fetch the user's signature text, if any, and normalize to - * validated, ready-to-insert wikitext. - * - * @param User $user - * @return string - * @private - */ - function getUserSig( &$user ) { - global $wgMaxSigChars; - - $username = $user->getName(); - $nickname = $user->getOption( 'nickname' ); - $nickname = $nickname === '' ? $username : $nickname; - - if( mb_strlen( $nickname ) > $wgMaxSigChars ) { - $nickname = $username; - wfDebug( __METHOD__ . ": $username has overlong signature.\n" ); - } elseif( $user->getBoolOption( 'fancysig' ) !== false ) { - # Sig. might contain markup; validate this - if( $this->validateSig( $nickname ) !== false ) { - # Validated; clean up (if needed) and return it - return $this->cleanSig( $nickname, true ); - } else { - # Failed to validate; fall back to the default - $nickname = $username; - wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" ); - } - } - - // Make sure nickname doesnt get a sig in a sig - $nickname = $this->cleanSigInSig( $nickname ); - - # If we're still here, make it a link to the user page - $userText = wfEscapeWikiText( $username ); - $nickText = wfEscapeWikiText( $nickname ); - if ( $user->isAnon() ) { - return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText ); - } else { - return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText ); - } - } - - /** - * Check that the user's signature contains no bad XML - * - * @param string $text - * @return mixed An expanded string, or false if invalid. - */ - function validateSig( $text ) { - return( wfIsWellFormedXmlFragment( $text ) ? $text : false ); - } - - /** - * Clean up signature text - * - * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig - * 2) Substitute all transclusions - * - * @param string $text - * @param $parsing Whether we're cleaning (preferences save) or parsing - * @return string Signature text - */ - function cleanSig( $text, $parsing = false ) { - global $wgTitle; - $this->startExternalParse( $this->mTitle, new ParserOptions(), $parsing ? self::OT_WIKI : self::OT_MSG ); - - $substWord = MagicWord::get( 'subst' ); - $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase(); - $substText = '{{' . $substWord->getSynonym( 0 ); - - $text = preg_replace( $substRegex, $substText, $text ); - $text = $this->cleanSigInSig( $text ); - $text = $this->replaceVariables( $text ); - - $this->clearState(); - return $text; - } - - /** - * Strip ~~~, ~~~~ and ~~~~~ out of signatures - * @param string $text - * @return string Signature text with /~{3,5}/ removed - */ - function cleanSigInSig( $text ) { - $text = preg_replace( '/~{3,5}/', '', $text ); - return $text; - } - - /** - * Set up some variables which are usually set up in parse() - * so that an external function can call some class members with confidence - * @public - */ - function startExternalParse( &$title, $options, $outputType, $clearState = true ) { - $this->mTitle =& $title; - $this->mOptions = $options; - $this->setOutputType( $outputType ); - if ( $clearState ) { - $this->clearState(); - } - } - - /** - * Transform a MediaWiki message by replacing magic variables. - * - * @param string $text the text to transform - * @param ParserOptions $options options - * @return string the text with variables substituted - * @public - */ - function transformMsg( $text, $options ) { - global $wgTitle; - static $executing = false; - - $fname = "Parser::transformMsg"; - - # Guard against infinite recursion - if ( $executing ) { - return $text; - } - $executing = true; - - wfProfileIn($fname); - - if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) { - $this->mTitle = $wgTitle; - } else { - $this->mTitle = Title::newFromText('msg'); - } - $this->mOptions = $options; - $this->setOutputType( self::OT_MSG ); - $this->clearState(); - $text = $this->replaceVariables( $text ); - - $executing = false; - wfProfileOut($fname); - return $text; - } - - /** - * Create an HTML-style tag, e.g. <yourtag>special text</yourtag> - * The callback should have the following form: - * function myParserHook( $text, $params, &$parser ) { ... } - * - * Transform and return $text. Use $parser for any required context, e.g. use - * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions - * - * @public - * - * @param mixed $tag The tag to use, e.g. 'hook' for <hook> - * @param mixed $callback The callback function (and object) to use for the tag - * - * @return The old value of the mTagHooks array associated with the hook - */ - function setHook( $tag, $callback ) { - $tag = strtolower( $tag ); - $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null; - $this->mTagHooks[$tag] = $callback; - - return $oldVal; - } - - function setTransparentTagHook( $tag, $callback ) { - $tag = strtolower( $tag ); - $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null; - $this->mTransparentTagHooks[$tag] = $callback; - - return $oldVal; - } - - /** - * Create a function, e.g. {{sum:1|2|3}} - * The callback function should have the form: - * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... } - * - * The callback may either return the text result of the function, or an array with the text - * in element 0, and a number of flags in the other elements. The names of the flags are - * specified in the keys. Valid flags are: - * found The text returned is valid, stop processing the template. This - * is on by default. - * nowiki Wiki markup in the return value should be escaped - * noparse Unsafe HTML tags should not be stripped, etc. - * noargs Don't replace triple-brace arguments in the return value - * isHTML The returned text is HTML, armour it against wikitext transformation - * - * @public - * - * @param string $id The magic word ID - * @param mixed $callback The callback function (and object) to use - * @param integer $flags a combination of the following flags: - * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}} - * - * @return The old callback function for this name, if any - */ - function setFunctionHook( $id, $callback, $flags = 0 ) { - $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null; - $this->mFunctionHooks[$id] = $callback; - - # Add to function cache - $mw = MagicWord::get( $id ); - if( !$mw ) - throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' ); - - $synonyms = $mw->getSynonyms(); - $sensitive = intval( $mw->isCaseSensitive() ); - - foreach ( $synonyms as $syn ) { - # Case - if ( !$sensitive ) { - $syn = strtolower( $syn ); - } - # Add leading hash - if ( !( $flags & SFH_NO_HASH ) ) { - $syn = '#' . $syn; - } - # Remove trailing colon - if ( substr( $syn, -1, 1 ) == ':' ) { - $syn = substr( $syn, 0, -1 ); - } - $this->mFunctionSynonyms[$sensitive][$syn] = $id; - } - return $oldVal; - } - - /** - * Get all registered function hook identifiers - * - * @return array - */ - function getFunctionHooks() { - return array_keys( $this->mFunctionHooks ); - } - - /** - * Replace <!--LINK--> link placeholders with actual links, in the buffer - * Placeholders created in Skin::makeLinkObj() - * Returns an array of links found, indexed by PDBK: - * 0 - broken - * 1 - normal link - * 2 - stub - * $options is a bit field, RLH_FOR_UPDATE to select for update - */ - function replaceLinkHolders( &$text, $options = 0 ) { - global $wgUser; - global $wgContLang; - - $fname = 'Parser::replaceLinkHolders'; - wfProfileIn( $fname ); - - $pdbks = array(); - $colours = array(); - $sk = $this->mOptions->getSkin(); - $linkCache = LinkCache::singleton(); - - if ( !empty( $this->mLinkHolders['namespaces'] ) ) { - wfProfileIn( $fname.'-check' ); - $dbr = wfGetDB( DB_SLAVE ); - $page = $dbr->tableName( 'page' ); - $threshold = $wgUser->getOption('stubthreshold'); - - # Sort by namespace - asort( $this->mLinkHolders['namespaces'] ); - - # Generate query - $query = false; - $current = null; - foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { - # Make title object - $title = $this->mLinkHolders['titles'][$key]; - - # Skip invalid entries. - # Result will be ugly, but prevents crash. - if ( is_null( $title ) ) { - continue; - } - $pdbk = $pdbks[$key] = $title->getPrefixedDBkey(); - - # Check if it's a static known link, e.g. interwiki - if ( $title->isAlwaysKnown() ) { - $colours[$pdbk] = 1; - } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) { - $colours[$pdbk] = 1; - $this->mOutput->addLink( $title, $id ); - } elseif ( $linkCache->isBadLink( $pdbk ) ) { - $colours[$pdbk] = 0; - } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) { - $colours[$pdbk] = 0; - } else { - # Not in the link cache, add it to the query - if ( !isset( $current ) ) { - $current = $ns; - $query = "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect"; - $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN("; - } elseif ( $current != $ns ) { - $current = $ns; - $query .= ")) OR (page_namespace=$ns AND page_title IN("; - } else { - $query .= ', '; - } - - $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] ); - } - } - if ( $query ) { - $query .= '))'; - if ( $options & RLH_FOR_UPDATE ) { - $query .= ' FOR UPDATE'; - } - - $res = $dbr->query( $query, $fname ); - - # Fetch data and form into an associative array - # non-existent = broken - # 1 = known - # 2 = stub - while ( $s = $dbr->fetchObject($res) ) { - $title = Title::makeTitle( $s->page_namespace, $s->page_title ); - $pdbk = $title->getPrefixedDBkey(); - $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect ); - $this->mOutput->addLink( $title, $s->page_id ); - - $colours[$pdbk] = ( $threshold == 0 || ( - $s->page_len >= $threshold || # always true if $threshold <= 0 - $s->page_is_redirect || - !MWNamespace::isContent( $s->page_namespace ) ) - ? 1 : 2 ); - } - } - wfProfileOut( $fname.'-check' ); - - # Do a second query for different language variants of links and categories - if( $wgContLang->hasVariants() ) { - $linkBatch = new LinkBatch(); - $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders) - $categoryMap = array(); // maps $category_variant => $category (dbkeys) - $varCategories = array(); // category replacements oldDBkey => newDBkey - - $categories = $this->mOutput->getCategoryLinks(); - - // Add variants of links to link batch - foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { - $title = $this->mLinkHolders['titles'][$key]; - if ( is_null( $title ) ) - continue; - - $pdbk = $title->getPrefixedDBkey(); - $titleText = $title->getText(); - - // generate all variants of the link title text - $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText); - - // if link was not found (in first query), add all variants to query - if ( !isset($colours[$pdbk]) ){ - foreach($allTextVariants as $textVariant){ - if($textVariant != $titleText){ - $variantTitle = Title::makeTitle( $ns, $textVariant ); - if(is_null($variantTitle)) continue; - $linkBatch->addObj( $variantTitle ); - $variantMap[$variantTitle->getPrefixedDBkey()][] = $key; - } - } - } - } - - // process categories, check if a category exists in some variant - foreach( $categories as $category ){ - $variants = $wgContLang->convertLinkToAllVariants($category); - foreach($variants as $variant){ - if($variant != $category){ - $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) ); - if(is_null($variantTitle)) continue; - $linkBatch->addObj( $variantTitle ); - $categoryMap[$variant] = $category; - } - } - } - - - if ( !$linkBatch->isEmpty() ){ - // construct query - $titleClause = $linkBatch->constructSet('page', $dbr); - - $variantQuery = "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect"; - - $variantQuery .= " FROM $page WHERE $titleClause"; - if ( $options & RLH_FOR_UPDATE ) { - $variantQuery .= ' FOR UPDATE'; - } - - $varRes = $dbr->query( $variantQuery, $fname ); - - // for each found variants, figure out link holders and replace - while ( $s = $dbr->fetchObject($varRes) ) { - - $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title ); - $varPdbk = $variantTitle->getPrefixedDBkey(); - $vardbk = $variantTitle->getDBkey(); - - $holderKeys = array(); - if(isset($variantMap[$varPdbk])){ - $holderKeys = $variantMap[$varPdbk]; - $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect ); - $this->mOutput->addLink( $variantTitle, $s->page_id ); - } - - // loop over link holders - foreach($holderKeys as $key){ - $title = $this->mLinkHolders['titles'][$key]; - if ( is_null( $title ) ) continue; - - $pdbk = $title->getPrefixedDBkey(); - - if(!isset($colours[$pdbk])){ - // found link in some of the variants, replace the link holder data - $this->mLinkHolders['titles'][$key] = $variantTitle; - $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey(); - - // set pdbk and colour - $pdbks[$key] = $varPdbk; - if ( $threshold > 0 ) { - $size = $s->page_len; - if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) { - $colours[$varPdbk] = 1; - } else { - $colours[$varPdbk] = 2; - } - } - else { - $colours[$varPdbk] = 1; - } - } - } - - // check if the object is a variant of a category - if(isset($categoryMap[$vardbk])){ - $oldkey = $categoryMap[$vardbk]; - if($oldkey != $vardbk) - $varCategories[$oldkey]=$vardbk; - } - } - - // rebuild the categories in original order (if there are replacements) - if(count($varCategories)>0){ - $newCats = array(); - $originalCats = $this->mOutput->getCategories(); - foreach($originalCats as $cat => $sortkey){ - // make the replacement - if( array_key_exists($cat,$varCategories) ) - $newCats[$varCategories[$cat]] = $sortkey; - else $newCats[$cat] = $sortkey; - } - $this->mOutput->setCategoryLinks($newCats); - } - } - } - - # Construct search and replace arrays - wfProfileIn( $fname.'-construct' ); - $replacePairs = array(); - foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { - $pdbk = $pdbks[$key]; - $searchkey = "<!--LINK $key-->"; - $title = $this->mLinkHolders['titles'][$key]; - if ( empty( $colours[$pdbk] ) ) { - $linkCache->addBadLinkObj( $title ); - $colours[$pdbk] = 0; - $this->mOutput->addLink( $title, 0 ); - $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title, - $this->mLinkHolders['texts'][$key], - $this->mLinkHolders['queries'][$key] ); - } elseif ( $colours[$pdbk] == 1 ) { - $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title, - $this->mLinkHolders['texts'][$key], - $this->mLinkHolders['queries'][$key] ); - } elseif ( $colours[$pdbk] == 2 ) { - $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title, - $this->mLinkHolders['texts'][$key], - $this->mLinkHolders['queries'][$key] ); - } - } - $replacer = new HashtableReplacer( $replacePairs, 1 ); - wfProfileOut( $fname.'-construct' ); - - # Do the thing - wfProfileIn( $fname.'-replace' ); - $text = preg_replace_callback( - '/(<!--LINK .*?-->)/', - $replacer->cb(), - $text); - - wfProfileOut( $fname.'-replace' ); - } - - # Now process interwiki link holders - # This is quite a bit simpler than internal links - if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) { - wfProfileIn( $fname.'-interwiki' ); - # Make interwiki link HTML - $replacePairs = array(); - foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) { - $title = $this->mInterwikiLinkHolders['titles'][$key]; - $replacePairs[$key] = $sk->makeLinkObj( $title, $link ); - } - $replacer = new HashtableReplacer( $replacePairs, 1 ); - - $text = preg_replace_callback( - '/<!--IWLINK (.*?)-->/', - $replacer->cb(), - $text ); - wfProfileOut( $fname.'-interwiki' ); - } - - wfProfileOut( $fname ); - return $colours; - } - - /** - * Replace <!--LINK--> link placeholders with plain text of links - * (not HTML-formatted). - * @param string $text - * @return string - */ - function replaceLinkHoldersText( $text ) { - $fname = 'Parser::replaceLinkHoldersText'; - wfProfileIn( $fname ); - - $text = preg_replace_callback( - '/<!--(LINK|IWLINK) (.*?)-->/', - array( &$this, 'replaceLinkHoldersTextCallback' ), - $text ); - - wfProfileOut( $fname ); - return $text; - } - - /** - * @param array $matches - * @return string - * @private - */ - function replaceLinkHoldersTextCallback( $matches ) { - $type = $matches[1]; - $key = $matches[2]; - if( $type == 'LINK' ) { - if( isset( $this->mLinkHolders['texts'][$key] ) ) { - return $this->mLinkHolders['texts'][$key]; - } - } elseif( $type == 'IWLINK' ) { - if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) { - return $this->mInterwikiLinkHolders['texts'][$key]; - } - } - return $matches[0]; - } - - /** - * Tag hook handler for 'pre'. - */ - function renderPreTag( $text, $attribs ) { - // Backwards-compatibility hack - $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' ); - - $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); - return wfOpenElement( 'pre', $attribs ) . - Xml::escapeTagsOnly( $content ) . - '</pre>'; - } - - /** - * Renders an image gallery from a text with one line per image. - * text labels may be given by using |-style alternative text. E.g. - * Image:one.jpg|The number "1" - * Image:tree.jpg|A tree - * given as text will return the HTML of a gallery with two images, - * labeled 'The number "1"' and - * 'A tree'. - */ - function renderImageGallery( $text, $params ) { - $ig = new ImageGallery(); - $ig->setContextTitle( $this->mTitle ); - $ig->setShowBytes( false ); - $ig->setShowFilename( false ); - $ig->setParser( $this ); - $ig->setHideBadImages(); - $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) ); - $ig->useSkin( $this->mOptions->getSkin() ); - $ig->mRevisionId = $this->mRevisionId; - - if( isset( $params['caption'] ) ) { - $caption = $params['caption']; - $caption = htmlspecialchars( $caption ); - $caption = $this->replaceInternalLinks( $caption ); - $ig->setCaptionHtml( $caption ); - } - if( isset( $params['perrow'] ) ) { - $ig->setPerRow( $params['perrow'] ); - } - if( isset( $params['widths'] ) ) { - $ig->setWidths( $params['widths'] ); - } - if( isset( $params['heights'] ) ) { - $ig->setHeights( $params['heights'] ); - } - - wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) ); - - $lines = explode( "\n", $text ); - foreach ( $lines as $line ) { - # match lines like these: - # Image:someimage.jpg|This is some image - $matches = array(); - preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches ); - # Skip empty lines - if ( count( $matches ) == 0 ) { - continue; - } - $tp = Title::newFromText( $matches[1] ); - $nt =& $tp; - if( is_null( $nt ) ) { - # Bogus title. Ignore these so we don't bomb out later. - continue; - } - if ( isset( $matches[3] ) ) { - $label = $matches[3]; - } else { - $label = ''; - } - - $pout = $this->parse( $label, - $this->mTitle, - $this->mOptions, - false, // Strip whitespace...? - false // Don't clear state! - ); - $html = $pout->getText(); - - $ig->add( $nt, $html ); - - # Only add real images (bug #5586) - if ( $nt->getNamespace() == NS_IMAGE ) { - $this->mOutput->addImage( $nt->getDBkey() ); - } - } - return $ig->toHTML(); - } - - function getImageParams( $handler ) { - if ( $handler ) { - $handlerClass = get_class( $handler ); - } else { - $handlerClass = ''; - } - if ( !isset( $this->mImageParams[$handlerClass] ) ) { - // Initialise static lists - static $internalParamNames = array( - 'horizAlign' => array( 'left', 'right', 'center', 'none' ), - 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', - 'bottom', 'text-bottom' ), - 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless', - 'upright', 'border' ), - ); - static $internalParamMap; - if ( !$internalParamMap ) { - $internalParamMap = array(); - foreach ( $internalParamNames as $type => $names ) { - foreach ( $names as $name ) { - $magicName = str_replace( '-', '_', "img_$name" ); - $internalParamMap[$magicName] = array( $type, $name ); - } - } - } - - // Add handler params - $paramMap = $internalParamMap; - if ( $handler ) { - $handlerParamMap = $handler->getParamMap(); - foreach ( $handlerParamMap as $magic => $paramName ) { - $paramMap[$magic] = array( 'handler', $paramName ); - } - } - $this->mImageParams[$handlerClass] = $paramMap; - $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) ); - } - return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ); - } - - /** - * Parse image options text and use it to make an image - */ - function makeImage( $title, $options ) { - # @TODO: let the MediaHandler specify its transform parameters - # - # Check if the options text is of the form "options|alt text" - # Options are: - # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang - # * left no resizing, just left align. label is used for alt= only - # * right same, but right aligned - # * none same, but not aligned - # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox - # * center center the image - # * framed Keep original image size, no magnify-button. - # * frameless like 'thumb' but without a frame. Keeps user preferences for width - # * upright reduce width for upright images, rounded to full __0 px - # * border draw a 1px border around the image - # vertical-align values (no % or length right now): - # * baseline - # * sub - # * super - # * top - # * text-top - # * middle - # * bottom - # * text-bottom - - $parts = array_map( 'trim', explode( '|', $options) ); - $sk = $this->mOptions->getSkin(); - - # Give extensions a chance to select the file revision for us - $skip = $time = false; - wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time ) ); - - if ( $skip ) { - return $sk->makeLinkObj( $title ); - } - - # Get parameter map - $file = wfFindFile( $title, $time ); - $handler = $file ? $file->getHandler() : false; - - list( $paramMap, $mwArray ) = $this->getImageParams( $handler ); - - # Process the input parameters - $caption = ''; - $params = array( 'frame' => array(), 'handler' => array(), - 'horizAlign' => array(), 'vertAlign' => array() ); - foreach( $parts as $part ) { - list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part ); - if ( isset( $paramMap[$magicName] ) ) { - list( $type, $paramName ) = $paramMap[$magicName]; - $params[$type][$paramName] = $value; - - // Special case; width and height come in one variable together - if( $type == 'handler' && $paramName == 'width' ) { - $m = array(); - if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $value, $m ) ) { - $params[$type]['width'] = intval( $m[1] ); - $params[$type]['height'] = intval( $m[2] ); - } else { - $params[$type]['width'] = intval( $value ); - } - } - } else { - $caption = $part; - } - } - - # Process alignment parameters - if ( $params['horizAlign'] ) { - $params['frame']['align'] = key( $params['horizAlign'] ); - } - if ( $params['vertAlign'] ) { - $params['frame']['valign'] = key( $params['vertAlign'] ); - } - - # Validate the handler parameters - if ( $handler ) { - foreach ( $params['handler'] as $name => $value ) { - if ( !$handler->validateParam( $name, $value ) ) { - unset( $params['handler'][$name] ); - } - } - } - - # Strip bad stuff out of the alt text - $alt = $this->replaceLinkHoldersText( $caption ); - - # make sure there are no placeholders in thumbnail attributes - # that are later expanded to html- so expand them now and - # remove the tags - $alt = $this->mStripState->unstripBoth( $alt ); - $alt = Sanitizer::stripAllTags( $alt ); - - $params['frame']['alt'] = $alt; - $params['frame']['caption'] = $caption; - - # Linker does the rest - $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'] ); - - # Give the handler a chance to modify the parser object - if ( $handler ) { - $handler->parserTransformHook( $this, $file ); - } - - return $ret; - } - - /** - * Set a flag in the output object indicating that the content is dynamic and - * shouldn't be cached. - */ - function disableCache() { - wfDebug( "Parser output marked as uncacheable.\n" ); - $this->mOutput->mCacheTime = -1; - } - - /**#@+ - * Callback from the Sanitizer for expanding items found in HTML attribute - * values, so they can be safely tested and escaped. - * @param string $text - * @param array $args - * @return string - * @private - */ - function attributeStripCallback( &$text, $args ) { - $text = $this->replaceVariables( $text, $args ); - $text = $this->mStripState->unstripBoth( $text ); - return $text; - } - - /**#@-*/ - - /**#@+ - * Accessor/mutator - */ - function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); } - function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); } - function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); } - /**#@-*/ - - /**#@+ - * Accessor - */ - function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); } - /**#@-*/ - - - /** - * Break wikitext input into sections, and either pull or replace - * some particular section's text. - * - * External callers should use the getSection and replaceSection methods. - * - * @param $text Page wikitext - * @param $section Numbered section. 0 pulls the text before the first - * heading; other numbers will pull the given section - * along with its lower-level subsections. - * @param $mode One of "get" or "replace" - * @param $newtext Replacement text for section data. - * @return string for "get", the extracted section text. - * for "replace", the whole page with the section replaced. - */ - private function extractSections( $text, $section, $mode, $newtext='' ) { - # I.... _hope_ this is right. - # Otherwise, sometimes we don't have things initialized properly. - $this->clearState(); - - # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML - # comments to be stripped as well) - $stripState = new StripState; - - $oldOutputType = $this->mOutputType; - $oldOptions = $this->mOptions; - $this->mOptions = new ParserOptions(); - $this->setOutputType( self::OT_WIKI ); - - $striptext = $this->strip( $text, $stripState, true ); - - $this->setOutputType( $oldOutputType ); - $this->mOptions = $oldOptions; - - # now that we can be sure that no pseudo-sections are in the source, - # split it up by section - $uniq = preg_quote( $this->uniqPrefix(), '/' ); - $comment = "(?:$uniq-!--.*?QINU\x07)"; - $secs = preg_split( - "/ - ( - ^ - (?:$comment|<\/?noinclude>)* # Initial comments will be stripped - (=+) # Should this be limited to 6? - .+? # Section title... - \\2 # Ending = count must match start - (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok - $ - | - <h([1-6])\b.*?> - .*? - <\/h\\3\s*> - ) - /mix", - $striptext, -1, - PREG_SPLIT_DELIM_CAPTURE); - - if( $mode == "get" ) { - if( $section == 0 ) { - // "Section 0" returns the content before any other section. - $rv = $secs[0]; - } else { - //track missing section, will replace if found. - $rv = $newtext; - } - } elseif( $mode == "replace" ) { - if( $section == 0 ) { - $rv = $newtext . "\n\n"; - $remainder = true; - } else { - $rv = $secs[0]; - $remainder = false; - } - } - $count = 0; - $sectionLevel = 0; - for( $index = 1; $index < count( $secs ); ) { - $headerLine = $secs[$index++]; - if( $secs[$index] ) { - // A wiki header - $headerLevel = strlen( $secs[$index++] ); - } else { - // An HTML header - $index++; - $headerLevel = intval( $secs[$index++] ); - } - $content = $secs[$index++]; - - $count++; - if( $mode == "get" ) { - if( $count == $section ) { - $rv = $headerLine . $content; - $sectionLevel = $headerLevel; - } elseif( $count > $section ) { - if( $sectionLevel && $headerLevel > $sectionLevel ) { - $rv .= $headerLine . $content; - } else { - // Broke out to a higher-level section - break; - } - } - } elseif( $mode == "replace" ) { - if( $count < $section ) { - $rv .= $headerLine . $content; - } elseif( $count == $section ) { - $rv .= $newtext . "\n\n"; - $sectionLevel = $headerLevel; - } elseif( $count > $section ) { - if( $headerLevel <= $sectionLevel ) { - // Passed the section's sub-parts. - $remainder = true; - } - if( $remainder ) { - $rv .= $headerLine . $content; - } - } - } - } - if (is_string($rv)) - # reinsert stripped tags - $rv = trim( $stripState->unstripBoth( $rv ) ); - - return $rv; - } - - /** - * This function returns the text of a section, specified by a number ($section). - * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or - * the first section before any such heading (section 0). - * - * If a section contains subsections, these are also returned. - * - * @param $text String: text to look in - * @param $section Integer: section number - * @param $deftext: default to return if section is not found - * @return string text of the requested section - */ - public function getSection( $text, $section, $deftext='' ) { - return $this->extractSections( $text, $section, "get", $deftext ); - } - - public function replaceSection( $oldtext, $section, $text ) { - return $this->extractSections( $oldtext, $section, "replace", $text ); - } - - /** - * Get the timestamp associated with the current revision, adjusted for - * the default server-local timestamp - */ - function getRevisionTimestamp() { - if ( is_null( $this->mRevisionTimestamp ) ) { - wfProfileIn( __METHOD__ ); - global $wgContLang; - $dbr = wfGetDB( DB_SLAVE ); - $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', - array( 'rev_id' => $this->mRevisionId ), __METHOD__ ); - - // Normalize timestamp to internal MW format for timezone processing. - // This has the added side-effect of replacing a null value with - // the current time, which gives us more sensible behavior for - // previews. - $timestamp = wfTimestamp( TS_MW, $timestamp ); - - // The cryptic '' timezone parameter tells to use the site-default - // timezone offset instead of the user settings. - // - // Since this value will be saved into the parser cache, served - // to other users, and potentially even used inside links and such, - // it needs to be consistent for all visitors. - $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' ); - - wfProfileOut( __METHOD__ ); - } - return $this->mRevisionTimestamp; - } - - /** - * Mutator for $mDefaultSort - * - * @param $sort New value - */ - public function setDefaultSort( $sort ) { - $this->mDefaultSort = $sort; - } - - /** - * Accessor for $mDefaultSort - * Will use the title/prefixed title if none is set - * - * @return string - */ - public function getDefaultSort() { - if( $this->mDefaultSort !== false ) { - return $this->mDefaultSort; - } else { - return $this->mTitle->getNamespace() == NS_CATEGORY - ? $this->mTitle->getText() - : $this->mTitle->getPrefixedText(); - } - } - - /** - * Try to guess the section anchor name based on a wikitext fragment - * presumably extracted from a heading, for example "Header" from - * "== Header ==". - */ - public function guessSectionNameFromWikiText( $text ) { - # Strip out wikitext links(they break the anchor) - $text = $this->stripSectionName( $text ); - $headline = Sanitizer::decodeCharReferences( $text ); - # strip out HTML - $headline = StringUtils::delimiterReplace( '<', '>', '', $headline ); - $headline = trim( $headline ); - $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) ); - $replacearray = array( - '%3A' => ':', - '%' => '.' - ); - return str_replace( - array_keys( $replacearray ), - array_values( $replacearray ), - $sectionanchor ); - } - - /** - * Strips a text string of wikitext for use in a section anchor - * - * Accepts a text string and then removes all wikitext from the - * string and leaves only the resultant text (i.e. the result of - * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of - * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended - * to create valid section anchors by mimicing the output of the - * parser when headings are parsed. - * - * @param $text string Text string to be stripped of wikitext - * for use in a Section anchor - * @return Filtered text string - */ - public function stripSectionName( $text ) { - # Strip internal link markup - $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text); - $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text); - - # Strip external link markup (FIXME: Not Tolerant to blank link text - # I.E. [http://www.mediawiki.org] will render as [1] or something depending - # on how many empty links there are on the page - need to figure that out. - $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text); - - # Parse wikitext quotes (italics & bold) - $text = $this->doQuotes($text); - - # Strip HTML tags - $text = StringUtils::delimiterReplace( '<', '>', '', $text ); - return $text; - } - - /** - * strip/replaceVariables/unstrip for preprocessor regression testing - */ - function srvus( $text ) { - $text = $this->strip( $text, $this->mStripState ); - $text = Sanitizer::removeHTMLtags( $text ); - $text = $this->replaceVariables( $text ); - $text = preg_replace( '/<!--MWTEMPLATESECTION.*?-->/', '', $text ); - $text = $this->mStripState->unstripBoth( $text ); - return $text; - } -} diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php index 34d58967..af591b67 100644 --- a/includes/parser/Preprocessor_DOM.php +++ b/includes/parser/Preprocessor_DOM.php @@ -770,6 +770,7 @@ class PPFrame_DOM implements PPFrame { /** * Recursion depth of this frame, top = 0 + * Note that this is NOT the same as expansion depth in expand() */ var $depth; @@ -826,20 +827,21 @@ class PPFrame_DOM implements PPFrame { } function expand( $root, $flags = 0 ) { - static $depth = 0; + static $expansionDepth = 0; if ( is_string( $root ) ) { return $root; } + wfProfileIn( __METHOD__ ); if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount ) { return '<span class="error">Node-count limit exceeded</span>'; } - if ( $depth > $this->parser->mOptions->mMaxPPExpandDepth ) { + if ( $expansionDepth > $this->parser->mOptions->mMaxPPExpandDepth ) { return '<span class="error">Expansion depth limit exceeded</span>'; } - ++$depth; + ++$expansionDepth; if ( $root instanceof PPNode_DOM ) { $root = $root->node; @@ -1005,6 +1007,7 @@ class PPFrame_DOM implements PPFrame { $newIterator = $contextNode->childNodes; } } else { + wfProfileOut( __METHOD__ ); throw new MWException( __METHOD__.': Invalid parameter type' ); } @@ -1027,7 +1030,8 @@ class PPFrame_DOM implements PPFrame { } } } - --$depth; + --$expansionDepth; + wfProfileOut( __METHOD__ ); return $outStack[0]; } @@ -1218,6 +1222,32 @@ class PPTemplateFrame_DOM extends PPFrame_DOM { return !count( $this->numberedArgs ) && !count( $this->namedArgs ); } + function getArguments() { + $arguments = array(); + foreach ( array_merge( + array_keys($this->numberedArgs), + array_keys($this->namedArgs)) as $key ) { + $arguments[$key] = $this->getArgument($key); + } + return $arguments; + } + + function getNumberedArguments() { + $arguments = array(); + foreach ( array_keys($this->numberedArgs) as $key ) { + $arguments[$key] = $this->getArgument($key); + } + return $arguments; + } + + function getNamedArguments() { + $arguments = array(); + foreach ( array_keys($this->namedArgs) as $key ) { + $arguments[$key] = $this->getArgument($key); + } + return $arguments; + } + function getNumberedArgument( $index ) { if ( !isset( $this->numberedArgs[$index] ) ) { return false; @@ -1291,6 +1321,9 @@ class PPCustomFrame_DOM extends PPFrame_DOM { } function getArgument( $index ) { + if ( !isset( $this->args[$index] ) ) { + return false; + } return $this->args[$index]; } } diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php index b5775243..62028291 100644 --- a/includes/parser/Preprocessor_Hash.php +++ b/includes/parser/Preprocessor_Hash.php @@ -758,6 +758,7 @@ class PPFrame_Hash implements PPFrame { /** * Recursion depth of this frame, top = 0 + * Note that this is NOT the same as expansion depth in expand() */ var $depth; @@ -810,6 +811,7 @@ class PPFrame_Hash implements PPFrame { } function expand( $root, $flags = 0 ) { + static $expansionDepth = 0; if ( is_string( $root ) ) { return $root; } @@ -818,10 +820,10 @@ class PPFrame_Hash implements PPFrame { { return '<span class="error">Node-count limit exceeded</span>'; } - if ( $this->depth > $this->parser->mOptions->mMaxPPExpandDepth ) { + if ( $expansionDepth > $this->parser->mOptions->mMaxPPExpandDepth ) { return '<span class="error">Expansion depth limit exceeded</span>'; } - ++$this->depth; + ++$expansionDepth; $outStack = array( '', '' ); $iteratorStack = array( false, $root ); @@ -974,7 +976,7 @@ class PPFrame_Hash implements PPFrame { } } } - --$this->depth; + --$expansionDepth; return $outStack[0]; } @@ -1173,6 +1175,32 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { return !count( $this->numberedArgs ) && !count( $this->namedArgs ); } + function getArguments() { + $arguments = array(); + foreach ( array_merge( + array_keys($this->numberedArgs), + array_keys($this->namedArgs)) as $key ) { + $arguments[$key] = $this->getArgument($key); + } + return $arguments; + } + + function getNumberedArguments() { + $arguments = array(); + foreach ( array_keys($this->numberedArgs) as $key ) { + $arguments[$key] = $this->getArgument($key); + } + return $arguments; + } + + function getNamedArguments() { + $arguments = array(); + foreach ( array_keys($this->namedArgs) as $key ) { + $arguments[$key] = $this->getArgument($key); + } + return $arguments; + } + function getNumberedArgument( $index ) { if ( !isset( $this->numberedArgs[$index] ) ) { return false; @@ -1246,6 +1274,9 @@ class PPCustomFrame_Hash extends PPFrame_Hash { } function getArgument( $index ) { + if ( !isset( $this->args[$index] ) ) { + return false; + } return $this->args[$index]; } } diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php index c2a8de4e..0ff94b49 100644 --- a/includes/specials/SpecialAllmessages.php +++ b/includes/specials/SpecialAllmessages.php @@ -29,15 +29,19 @@ function wfSpecialAllmessages() { $wgMessageCache->loadAllMessages(); - $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) ); + $sortedArray = array_merge( Language::getMessagesFor( 'en' ), + $wgMessageCache->getExtensionMessagesFor( 'en' ) ); ksort( $sortedArray ); - $messages = array(); - foreach ( $sortedArray as $key => $value ) { + $messages = array(); + foreach( $sortedArray as $key => $value ) { $messages[$key]['enmsg'] = $value; - $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false ); // wfMsgNoDbNoTrans doesn't exist + $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false ); $messages[$key]['msg'] = wfMsgNoTrans( $key ); + $sortedArray[$key] = NULL; // trade bytes from $sortedArray to this + } + unset($sortedArray); // trade bytes from $sortedArray to this wfProfileOut( __METHOD__ . '-setup' ); @@ -63,13 +67,14 @@ function wfSpecialAllmessages() { wfProfileOut( __METHOD__ ); } -function wfAllMessagesMakeXml( $messages ) { +function wfAllMessagesMakeXml( &$messages ) { global $wgLang; $lang = $wgLang->getCode(); $txt = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"; $txt .= "<messages lang=\"$lang\">\n"; foreach( $messages as $key => $m ) { $txt .= "\t" . Xml::element( 'message', array( 'name' => $key ), $m['msg'] ) . "\n"; + $messages[$key] = NULL; // trade bytes } $txt .= "</messages>"; return $txt; @@ -81,7 +86,7 @@ function wfAllMessagesMakeXml( $messages ) { * @return The PHP messages array. * @todo Make suitable for language files. */ -function wfAllMessagesMakePhp( $messages ) { +function wfAllMessagesMakePhp( &$messages ) { global $wgLang; $txt = "\n\n\$messages = array(\n"; foreach( $messages as $key => $m ) { @@ -94,6 +99,7 @@ function wfAllMessagesMakePhp( $messages ) { $comment = ''; } $txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n"; + $messages[$key] = NULL; // trade bytes } $txt .= ');'; return $txt; @@ -104,7 +110,7 @@ function wfAllMessagesMakePhp( $messages ) { * @param $messages Messages array. * @return The HTML list of messages. */ -function wfAllMessagesMakeHTMLText( $messages ) { +function wfAllMessagesMakeHTMLText( &$messages ) { global $wgLang, $wgContLang, $wgUser; wfProfileIn( __METHOD__ ); @@ -123,7 +129,8 @@ function wfAllMessagesMakeHTMLText( $messages ) { 'onclick' => 'allmessagesmodified()' ), '' ); - $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . " {$input}{$checkbox} " . '</span>'; + $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . + " {$input}{$checkbox} " . '</span>'; $txt .= ' <table border="1" cellspacing="0" width="100%" id="allmessagestable"> @@ -144,11 +151,14 @@ function wfAllMessagesMakeHTMLText( $messages ) { NS_MEDIAWIKI_TALK => array() ); $dbr = wfGetDB( DB_SLAVE ); - $page = $dbr->tableName( 'page' ); - $sql = "SELECT page_namespace,page_title FROM $page WHERE page_namespace IN (" . NS_MEDIAWIKI . ", " . NS_MEDIAWIKI_TALK . ")"; - $res = $dbr->query( $sql ); + $res = $dbr->select( 'page', + array( 'page_namespace', 'page_title' ), + array( 'page_namespace' => array(NS_MEDIAWIKI,NS_MEDIAWIKI_TALK) ), + __METHOD__, + array( 'USE INDEX' => 'name_title' ) + ); while( $s = $dbr->fetchObject( $res ) ) { - $pageExists[$s->page_namespace][$s->page_title] = true; + $pageExists[$s->page_namespace][$s->page_title] = 1; } $dbr->freeResult( $res ); wfProfileOut( __METHOD__ . "-check" ); @@ -163,19 +173,21 @@ function wfAllMessagesMakeHTMLText( $messages ) { $title .= '/' . $wgLang->getCode(); } - $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title ); - $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title ); + $titleObj = Title::makeTitle( NS_MEDIAWIKI, $title ); + $talkPage = Title::makeTitle( NS_MEDIAWIKI_TALK, $title ); $changed = ( $m['statmsg'] != $m['msg'] ); $message = htmlspecialchars( $m['statmsg'] ); $mw = htmlspecialchars( $m['msg'] ); - if( isset( $pageExists[NS_MEDIAWIKI][$title] ) ) { - $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' ); + if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI] ) ) { + $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . + htmlspecialchars( $key ) . '</span>' ); } else { - $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' ); + $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . + htmlspecialchars( $key ) . '</span>' ); } - if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) { + if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI_TALK] ) ) { $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) ); } else { $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) ); @@ -186,27 +198,28 @@ function wfAllMessagesMakeHTMLText( $messages ) { if( $changed ) { $txt .= " - <tr class=\"orig\" id=\"sp-allmessages-r1-$i\"> - <td rowspan=\"2\"> - $anchor$pageLink<br />$talkLink - </td><td> -$message - </td> - </tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\"> - <td> -$mw - </td> - </tr>"; + <tr class=\"orig\" id=\"sp-allmessages-r1-$i\"> + <td rowspan=\"2\"> + $anchor$pageLink<br />$talkLink + </td><td> + $message + </td> + </tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\"> + <td> + $mw + </td> + </tr>"; } else { $txt .= " - <tr class=\"def\" id=\"sp-allmessages-r1-$i\"> - <td> - $anchor$pageLink<br />$talkLink - </td><td> -$mw - </td> - </tr>"; + <tr class=\"def\" id=\"sp-allmessages-r1-$i\"> + <td> + $anchor$pageLink<br />$talkLink + </td><td> + $mw + </td> + </tr>"; } + $messages[$key] = NULL; // trade bytes $i++; } $txt .= '</table>'; diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php index 7223e317..bf68dfa6 100644 --- a/includes/specials/SpecialAllpages.php +++ b/includes/specials/SpecialAllpages.php @@ -1,404 +1,445 @@ <?php -/** - * @file - * @ingroup SpecialPage - */ - -/** - * Entry point : initialise variables and call subfunctions. - * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL) - * @param $specialPage See the SpecialPage object. - */ -function wfSpecialAllpages( $par=NULL, $specialPage ) { - global $wgRequest, $wgOut, $wgContLang; - - # GET values - $from = $wgRequest->getVal( 'from' ); - $namespace = $wgRequest->getInt( 'namespace' ); - - $namespaces = $wgContLang->getNamespaces(); - - $indexPage = new SpecialAllpages(); - - $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ? - wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : - wfMsg( 'allarticles' ) - ); - - if ( isset($par) ) { - $indexPage->showChunk( $namespace, $par, $specialPage->including() ); - } elseif ( isset($from) ) { - $indexPage->showChunk( $namespace, $from, $specialPage->including() ); - } else { - $indexPage->showToplevel ( $namespace, $specialPage->including() ); - } -} /** * Implements Special:Allpages * @ingroup SpecialPage */ -class SpecialAllpages { +class SpecialAllpages extends IncludableSpecialPage { + /** * Maximum number of pages to show on single subpage. */ - protected $maxPerPage = 960; + protected $maxPerPage = 345; /** - * Name of this special page. Used to make title objects that reference back - * to this page. + * Maximum number of pages to show on single index subpage. */ - protected $name = 'Allpages'; + protected $maxLineCount = 200; + + /** + * Maximum number of chars to show for an entry. + */ + protected $maxPageLength = 70; /** * Determines, which message describes the input field 'nsfrom'. */ protected $nsfromMsg = 'allpagesfrom'; -/** - * HTML for the top form - * @param integer $namespace A namespace constant (default NS_MAIN). - * @param string $from Article name we are starting listing at. - */ -function namespaceForm ( $namespace = NS_MAIN, $from = '' ) { - global $wgScript; - $t = SpecialPage::getTitleFor( $this->name ); - - $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); - $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); - $out .= Xml::hidden( 'title', $t->getPrefixedText() ); - $out .= Xml::openElement( 'fieldset' ); - $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) ); - $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); - $out .= "<tr> - <td class='mw-label'>" . - Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) . - "</td> - <td class='mw-input'>" . - Xml::input( 'from', 20, $from, array( 'id' => 'nsfrom' ) ) . - "</td> - </tr> - <tr> - <td class='mw-label'>" . - Xml::label( wfMsg( 'namespace' ), 'namespace' ) . - "</td> - <td class='mw-input'>" . - Xml::namespaceSelector( $namespace, null ) . ' ' . - Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . - "</td> - </tr>"; - $out .= Xml::closeElement( 'table' ); - $out .= Xml::closeElement( 'fieldset' ); - $out .= Xml::closeElement( 'form' ); - $out .= Xml::closeElement( 'div' ); - return $out; -} + function __construct( $name = 'Allpages' ){ + parent::__construct( $name ); + } -/** - * @param integer $namespace (default NS_MAIN) - */ -function showToplevel ( $namespace = NS_MAIN, $including = false ) { - global $wgOut, $wgContLang; - $align = $wgContLang->isRtl() ? 'left' : 'right'; + /** + * Entry point : initialise variables and call subfunctions. + * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL) + * @param $specialPage See the SpecialPage object. + */ + function execute( $par ) { + global $wgRequest, $wgOut, $wgContLang; - # TODO: Either make this *much* faster or cache the title index points - # in the querycache table. + $this->setHeaders(); + $this->outputHeader(); - $dbr = wfGetDB( DB_SLAVE ); - $out = ""; - $where = array( 'page_namespace' => $namespace ); + # GET values + $from = $wgRequest->getVal( 'from', null ); + $to = $wgRequest->getVal( 'to', null ); + $namespace = $wgRequest->getInt( 'namespace' ); - global $wgMemc; - $key = wfMemcKey( 'allpages', 'ns', $namespace ); - $lines = $wgMemc->get( $key ); + $namespaces = $wgContLang->getNamespaces(); - if( !is_array( $lines ) ) { - $options = array( 'LIMIT' => 1 ); - if ( ! $dbr->implicitOrderby() ) { - $options['ORDER BY'] = 'page_title'; + $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ? + wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : + wfMsg( 'allarticles' ) + ); + + if( isset($par) ) { + $this->showChunk( $namespace, $par, $to ); + } elseif( isset($from) && !isset($to) ) { + $this->showChunk( $namespace, $from, $to ); + } else { + $this->showToplevel( $namespace, $from, $to ); } - $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options ); - $lastTitle = $firstTitle; - - # This array is going to hold the page_titles in order. - $lines = array( $firstTitle ); - - # If we are going to show n rows, we need n+1 queries to find the relevant titles. - $done = false; - for( $i = 0; !$done; ++$i ) { - // Fetch the last title of this chunk and the first of the next - $chunk = is_null( $lastTitle ) - ? '' - : 'page_title >= ' . $dbr->addQuotes( $lastTitle ); - $res = $dbr->select( - 'page', /* FROM */ - 'page_title', /* WHAT */ - $where + array($chunk), - __METHOD__, - array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') ); + } - if ( $s = $dbr->fetchObject( $res ) ) { - array_push( $lines, $s->page_title ); - } else { - // Final chunk, but ended prematurely. Go back and find the end. - $endTitle = $dbr->selectField( 'page', 'MAX(page_title)', - array( - 'page_namespace' => $namespace, - $chunk - ), __METHOD__ ); - array_push( $lines, $endTitle ); - $done = true; + /** + * HTML for the top form + * @param integer $namespace A namespace constant (default NS_MAIN). + * @param string $from dbKey we are starting listing at. + * @param string $to dbKey we are ending listing at. + */ + function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '' ) { + global $wgScript; + $t = $this->getTitle(); + + $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); + $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $out .= Xml::hidden( 'title', $t->getPrefixedText() ); + $out .= Xml::openElement( 'fieldset' ); + $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) ); + $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); + $out .= "<tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'allpagesto' ), 'nsto' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + "</td> + <td class='mw-input'>" . + Xml::namespaceSelector( $namespace, null ) . ' ' . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + "</td> + </tr>"; + $out .= Xml::closeElement( 'table' ); + $out .= Xml::closeElement( 'fieldset' ); + $out .= Xml::closeElement( 'form' ); + $out .= Xml::closeElement( 'div' ); + return $out; + } + + /** + * @param integer $namespace (default NS_MAIN) + */ + function showToplevel( $namespace = NS_MAIN, $from = '', $to = '' ) { + global $wgOut, $wgContLang; + $align = $wgContLang->isRtl() ? 'left' : 'right'; + + # TODO: Either make this *much* faster or cache the title index points + # in the querycache table. + + $dbr = wfGetDB( DB_SLAVE ); + $out = ""; + $where = array( 'page_namespace' => $namespace ); + + $from = Title::makeTitleSafe( $namespace, $from ); + $to = Title::makeTitleSafe( $namespace, $to ); + $from = ( $from && $from->isLocal() ) ? $from->getDBKey() : null; + $to = ( $to && $to->isLocal() ) ? $to->getDBKey() : null; + + if( isset($from) ) + $where[] = 'page_title >= '.$dbr->addQuotes( $from ); + if( isset($to) ) + $where[] = 'page_title <= '.$dbr->addQuotes( $to ); + + global $wgMemc; + $key = wfMemcKey( 'allpages', 'ns', $namespace, $from, $to ); + $lines = $wgMemc->get( $key ); + + $count = $dbr->estimateRowCount( 'page', '*', $where, __METHOD__ ); + $maxPerSubpage = intval($count/$this->maxLineCount); + $maxPerSubpage = max($maxPerSubpage,$this->maxPerPage); + + if( !is_array( $lines ) ) { + $options = array( 'LIMIT' => 1 ); + $options['ORDER BY'] = 'page_title ASC'; + $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options ); + $lastTitle = $firstTitle; + # This array is going to hold the page_titles in order. + $lines = array( $firstTitle ); + # If we are going to show n rows, we need n+1 queries to find the relevant titles. + $done = false; + while( !$done ) { + // Fetch the last title of this chunk and the first of the next + $chunk = ( $lastTitle === false ) + ? array() + : array( 'page_title >= ' . $dbr->addQuotes( $lastTitle ) ); + $res = $dbr->select( 'page', /* FROM */ + 'page_title', /* WHAT */ + array_merge($where,$chunk), + __METHOD__, + array ('LIMIT' => 2, 'OFFSET' => $maxPerSubpage - 1, 'ORDER BY' => 'page_title ASC') + ); + + if( $s = $dbr->fetchObject( $res ) ) { + array_push( $lines, $s->page_title ); + } else { + // Final chunk, but ended prematurely. Go back and find the end. + $endTitle = $dbr->selectField( 'page', 'MAX(page_title)', + array_merge($where,$chunk), + __METHOD__ ); + array_push( $lines, $endTitle ); + $done = true; + } + if( $s = $res->fetchObject() ) { + array_push( $lines, $s->page_title ); + $lastTitle = $s->page_title; + } else { + // This was a final chunk and ended exactly at the limit. + // Rare but convenient! + $done = true; + } + $res->free(); } - if( $s = $dbr->fetchObject( $res ) ) { - array_push( $lines, $s->page_title ); - $lastTitle = $s->page_title; + $wgMemc->add( $key, $lines, 3600 ); + } + + // If there are only two or less sections, don't even display them. + // Instead, display the first section directly. + if( count( $lines ) <= 2 ) { + if( !empty($lines) ) { + $this->showChunk( $namespace, $lines[0], $lines[count($lines)-1] ); } else { - // This was a final chunk and ended exactly at the limit. - // Rare but convenient! - $done = true; + $wgOut->addHTML( $this->namespaceForm( $namespace, $from, $to ) ); } - $dbr->freeResult( $res ); + return; } - $wgMemc->add( $key, $lines, 3600 ); - } - // If there are only two or less sections, don't even display them. - // Instead, display the first section directly. - if( count( $lines ) <= 2 ) { - $this->showChunk( $namespace, '', $including ); - return; - } + # At this point, $lines should contain an even number of elements. + $out .= "<table class='allpageslist' style='background: inherit;'>"; + while( count ( $lines ) > 0 ) { + $inpoint = array_shift( $lines ); + $outpoint = array_shift( $lines ); + $out .= $this->showline( $inpoint, $outpoint, $namespace ); + } + $out .= '</table>'; + $nsForm = $this->namespaceForm( $namespace, $from, $to ); - # At this point, $lines should contain an even number of elements. - $out .= "<table class='allpageslist' style='background: inherit;'>"; - while ( count ( $lines ) > 0 ) { - $inpoint = array_shift ( $lines ); - $outpoint = array_shift ( $lines ); - $out .= $this->showline ( $inpoint, $outpoint, $namespace, false ); - } - $out .= '</table>'; - $nsForm = $this->namespaceForm( $namespace, '', false ); - - # Is there more? - if ( $including ) { - $out2 = ''; - } else { - $morelinks = ''; - if ( $morelinks != '' ) { - $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">'; - $out2 .= '<tr valign="top"><td>' . $nsForm; - $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">'; - $out2 .= $morelinks . '</td></tr></table><hr />'; + # Is there more? + if( $this->including() ) { + $out2 = ''; } else { - $out2 = $nsForm . '<hr />'; + if( isset($from) || isset($to) ) { + global $wgUser; + $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">'; + $out2 .= '<tr valign="top"><td>' . $nsForm; + $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' . + $wgUser->getSkin()->makeKnownLinkObj( $this->getTitle(), wfMsgHtml ( 'allpages' ) ); + $out2 .= "</td></tr></table><hr />"; + } else { + $out2 = $nsForm . '<hr />'; + } } + $wgOut->addHTML( $out2 . $out ); } - $wgOut->addHtml( $out2 . $out ); -} - -/** - * @todo Document - * @param string $from - * @param integer $namespace (Default NS_MAIN) - */ -function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { - global $wgContLang; - $align = $wgContLang->isRtl() ? 'left' : 'right'; - $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) ); - $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) ); - $queryparams = ($namespace ? "namespace=$namespace" : ''); - $special = SpecialPage::getTitleFor( $this->name, $inpoint ); - $link = $special->escapeLocalUrl( $queryparams ); - - $out = wfMsgHtml( - 'alphaindexline', - "<a href=\"$link\">$inpointf</a></td><td><a href=\"$link\">", - "</a></td><td><a href=\"$link\">$outpointf</a>" - ); - return '<tr><td align="' . $align . '">'.$out.'</td></tr>'; -} + /** + * Show a line of "ABC to DEF" ranges of articles + * @param string $inpoint Lower limit of pagenames + * @param string $outpout Upper limit of pagenames + * @param integer $namespace (Default NS_MAIN) + */ + function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { + global $wgContLang; + $align = $wgContLang->isRtl() ? 'left' : 'right'; + $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) ); + $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) ); + // Don't let the length runaway + $inpointf = $wgContLang->truncate( $inpointf, $this->maxPageLength, '...' ); + $outpointf = $wgContLang->truncate( $outpointf, $this->maxPageLength, '...' ); + + $queryparams = $namespace ? "namespace=$namespace&" : ''; + $special = $this->getTitle(); + $link = $special->escapeLocalUrl( $queryparams . 'from=' . urlencode($inpoint) . '&to=' . urlencode($outpoint) ); + + $out = wfMsgHtml( 'alphaindexline', + "<a href=\"$link\">$inpointf</a></td><td>", + "</td><td><a href=\"$link\">$outpointf</a>" + ); + return '<tr><td align="' . $align . '">'.$out.'</td></tr>'; + } -/** - * @param integer $namespace (Default NS_MAIN) - * @param string $from list all pages from this name (default FALSE) - */ -function showChunk( $namespace = NS_MAIN, $from, $including = false ) { - global $wgOut, $wgUser, $wgContLang; + /** + * @param integer $namespace (Default NS_MAIN) + * @param string $from list all pages from this name (default FALSE) + * @param string $to list all pages to this name (default FALSE) + */ + function showChunk( $namespace = NS_MAIN, $from = false, $to = false ) { + global $wgOut, $wgUser, $wgContLang; - $sk = $wgUser->getSkin(); + $sk = $wgUser->getSkin(); - $fromList = $this->getNamespaceKeyAndText($namespace, $from); - $namespaces = $wgContLang->getNamespaces(); - $align = $wgContLang->isRtl() ? 'left' : 'right'; + $fromList = $this->getNamespaceKeyAndText($namespace, $from); + $toList = $this->getNamespaceKeyAndText( $namespace, $to ); + $namespaces = $wgContLang->getNamespaces(); + $align = $wgContLang->isRtl() ? 'left' : 'right'; - $n = 0; + $n = 0; - if ( !$fromList ) { - $out = wfMsgWikiHtml( 'allpagesbadtitle' ); - } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) { - // Show errormessage and reset to NS_MAIN - $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace ); - $namespace = NS_MAIN; - } else { - list( $namespace, $fromKey, $from ) = $fromList; + if ( !$fromList || !$toList ) { + $out = wfMsgWikiHtml( 'allpagesbadtitle' ); + } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) { + // Show errormessage and reset to NS_MAIN + $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace ); + $namespace = NS_MAIN; + } else { + list( $namespace, $fromKey, $from ) = $fromList; + list( $namespace2, $toKey, $to ) = $toList; - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'page', - array( 'page_namespace', 'page_title', 'page_is_redirect' ), - array( + $dbr = wfGetDB( DB_SLAVE ); + $conds = array( 'page_namespace' => $namespace, 'page_title >= ' . $dbr->addQuotes( $fromKey ) - ), - __METHOD__, - array( - 'ORDER BY' => 'page_title', - 'LIMIT' => $this->maxPerPage + 1, - 'USE INDEX' => 'name_title', - ) - ); + ); + if( $toKey !== "" ) { + $conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey ); + } - if( $res->numRows() > 0 ) { - $out = '<table style="background: inherit;" border="0" width="100%">'; - - while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { - $t = Title::makeTitle( $s->page_namespace, $s->page_title ); - if( $t ) { - $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) . - $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) . - ($s->page_is_redirect ? '</div>' : '' ); - } else { - $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; - } - if( $n % 3 == 0 ) { - $out .= '<tr>'; + $res = $dbr->select( 'page', + array( 'page_namespace', 'page_title', 'page_is_redirect' ), + $conds, + __METHOD__, + array( + 'ORDER BY' => 'page_title', + 'LIMIT' => $this->maxPerPage + 1, + 'USE INDEX' => 'name_title', + ) + ); + + if( $res->numRows() > 0 ) { + $out = '<table style="background: inherit;" border="0" width="100%">'; + + while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { + $t = Title::makeTitle( $s->page_namespace, $s->page_title ); + if( $t ) { + $link = ( $s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) . + $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) . + ($s->page_is_redirect ? '</div>' : '' ); + } else { + $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; + } + if( $n % 3 == 0 ) { + $out .= '<tr>'; + } + $out .= "<td width=\"33%\">$link</td>"; + $n++; + if( $n % 3 == 0 ) { + $out .= '</tr>'; + } } - $out .= "<td width=\"33%\">$link</td>"; - $n++; - if( $n % 3 == 0 ) { + if( ($n % 3) != 0 ) { $out .= '</tr>'; } + $out .= '</table>'; + } else { + $out = ''; } - if( ($n % 3) != 0 ) { - $out .= '</tr>'; - } - $out .= '</table>'; - } else { - $out = ''; } - } - if ( $including ) { - $out2 = ''; - } else { - if( $from == '' ) { - // First chunk; no previous link. - $prevTitle = null; + if ( $this->including() ) { + $out2 = ''; } else { - # Get the last title from previous chunk - $dbr = wfGetDB( DB_SLAVE ); - $res_prev = $dbr->select( - 'page', - 'page_title', - array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ), - __METHOD__, - array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) ) - ); - - # Get first title of previous complete chunk - if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) { - $pt = $dbr->fetchObject( $res_prev ); - $prevTitle = Title::makeTitle( $namespace, $pt->page_title ); + if( $from == '' ) { + // First chunk; no previous link. + $prevTitle = null; } else { - # The previous chunk is not complete, need to link to the very first title - # available in the database - $options = array( 'LIMIT' => 1 ); - if ( ! $dbr->implicitOrderby() ) { - $options['ORDER BY'] = 'page_title'; - } - $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), __METHOD__, $options ); - # Show the previous link if it s not the current requested chunk - if( $from != $reallyFirstPage_title ) { - $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title ); + # Get the last title from previous chunk + $dbr = wfGetDB( DB_SLAVE ); + $res_prev = $dbr->select( + 'page', + 'page_title', + array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ), + __METHOD__, + array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) ) + ); + + # Get first title of previous complete chunk + if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) { + $pt = $dbr->fetchObject( $res_prev ); + $prevTitle = Title::makeTitle( $namespace, $pt->page_title ); } else { - $prevTitle = null; + # The previous chunk is not complete, need to link to the very first title + # available in the database + $options = array( 'LIMIT' => 1 ); + if ( ! $dbr->implicitOrderby() ) { + $options['ORDER BY'] = 'page_title'; + } + $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', + array( 'page_namespace' => $namespace ), __METHOD__, $options ); + # Show the previous link if it s not the current requested chunk + if( $from != $reallyFirstPage_title ) { + $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title ); + } else { + $prevTitle = null; + } } } - } - $nsForm = $this->namespaceForm( $namespace, $from ); - $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">'; - $out2 .= '<tr valign="top"><td>' . $nsForm; - $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' . - $sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ), - wfMsgHtml ( 'allpages' ) ); - - $self = SpecialPage::getTitleFor( 'Allpages' ); - - # Do we put a previous link ? - if( isset( $prevTitle ) && $pt = $prevTitle->getText() ) { - $q = 'from=' . $prevTitle->getPartialUrl() - . ( $namespace ? '&namespace=' . $namespace : '' ); - $prevLink = $sk->makeKnownLinkObj( $self, - wfMsgHTML( 'prevpage', htmlspecialchars( $pt ) ), $q ); - $out2 .= ' | ' . $prevLink; - } + $self = $this->getTitle(); - if( $n == $this->maxPerPage && $s = $dbr->fetchObject($res) ) { - # $s is the first link of the next chunk - $t = Title::MakeTitle($namespace, $s->page_title); - $q = 'from=' . $t->getPartialUrl() - . ( $namespace ? '&namespace=' . $namespace : '' ); - $nextLink = $sk->makeKnownLinkObj( $self, - wfMsgHtml( 'nextpage', htmlspecialchars( $t->getText() ) ), $q ); - $out2 .= ' | ' . $nextLink; - } - $out2 .= "</td></tr></table><hr />"; - } + $nsForm = $this->namespaceForm( $namespace, $from, $to ); + $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">'; + $out2 .= '<tr valign="top"><td>' . $nsForm; + $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' . + $sk->makeKnownLinkObj( $self, + wfMsgHtml ( 'allpages' ) ); + + # Do we put a previous link ? + if( isset( $prevTitle ) && $pt = $prevTitle->getText() ) { + $q = 'from=' . $prevTitle->getPartialUrl() + . ( $namespace ? '&namespace=' . $namespace : '' ); + $prevLink = $sk->makeKnownLinkObj( $self, + wfMsgHTML( 'prevpage', htmlspecialchars( $pt ) ), $q ); + $out2 .= ' | ' . $prevLink; + } - $wgOut->addHtml( $out2 . $out ); - if( isset($prevLink) or isset($nextLink) ) { - $wgOut->addHtml( '<hr /><p style="font-size: smaller; float: ' . $align . '">' ); - if( isset( $prevLink ) ) { - $wgOut->addHTML( $prevLink ); - } - if( isset( $prevLink ) && isset( $nextLink ) ) { - $wgOut->addHTML( ' | ' ); - } - if( isset( $nextLink ) ) { - $wgOut->addHTML( $nextLink ); + if( $n == $this->maxPerPage && $s = $res->fetchObject() ) { + # $s is the first link of the next chunk + $t = Title::MakeTitle($namespace, $s->page_title); + $q = 'from=' . $t->getPartialUrl() + . ( $namespace ? '&namespace=' . $namespace : '' ); + $nextLink = $sk->makeKnownLinkObj( $self, + wfMsgHtml( 'nextpage', htmlspecialchars( $t->getText() ) ), $q ); + $out2 .= ' | ' . $nextLink; + } + $out2 .= "</td></tr></table><hr />"; } - $wgOut->addHTML( '</p>' ); - } + $wgOut->addHTML( $out2 . $out ); + if( isset($prevLink) or isset($nextLink) ) { + $wgOut->addHTML( '<hr /><p style="font-size: smaller; float: ' . $align . '">' ); + if( isset( $prevLink ) ) { + $wgOut->addHTML( $prevLink ); + } + if( isset( $prevLink ) && isset( $nextLink ) ) { + $wgOut->addHTML( ' | ' ); + } + if( isset( $nextLink ) ) { + $wgOut->addHTML( $nextLink ); + } + $wgOut->addHTML( '</p>' ); -} + } -/** - * @param int $ns the namespace of the article - * @param string $text the name of the article - * @return array( int namespace, string dbkey, string pagename ) or NULL on error - * @static (sort of) - * @access private - */ -function getNamespaceKeyAndText ($ns, $text) { - if ( $text == '' ) - return array( $ns, '', '' ); # shortcut for common case - - $t = Title::makeTitleSafe($ns, $text); - if ( $t && $t->isLocal() ) { - return array( $t->getNamespace(), $t->getDBkey(), $t->getText() ); - } else if ( $t ) { - return NULL; } - # try again, in case the problem was an empty pagename - $text = preg_replace('/(#|$)/', 'X$1', $text); - $t = Title::makeTitleSafe($ns, $text); - if ( $t && $t->isLocal() ) { - return array( $t->getNamespace(), '', '' ); - } else { - return NULL; + /** + * @param int $ns the namespace of the article + * @param string $text the name of the article + * @return array( int namespace, string dbkey, string pagename ) or NULL on error + * @static (sort of) + * @access private + */ + function getNamespaceKeyAndText($ns, $text) { + if ( $text == '' ) + return array( $ns, '', '' ); # shortcut for common case + + $t = Title::makeTitleSafe($ns, $text); + if ( $t && $t->isLocal() ) { + return array( $t->getNamespace(), $t->getDBkey(), $t->getText() ); + } else if ( $t ) { + return NULL; + } + + # try again, in case the problem was an empty pagename + $text = preg_replace('/(#|$)/', 'X$1', $text); + $t = Title::makeTitleSafe($ns, $text); + if ( $t && $t->isLocal() ) { + return array( $t->getNamespace(), '', '' ); + } else { + return NULL; + } } } -} diff --git a/includes/specials/SpecialBlockip.php b/includes/specials/SpecialBlockip.php index 52829d92..4d82997f 100644 --- a/includes/specials/SpecialBlockip.php +++ b/includes/specials/SpecialBlockip.php @@ -47,7 +47,7 @@ class IPBlockForm { # var $BlockEmail; function IPBlockForm( $par ) { - global $wgRequest, $wgUser; + global $wgRequest, $wgUser, $wgBlockAllowsUTEdit; $this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip', $par ) ); $this->BlockAddress = strtr( $this->BlockAddress, '_', ' ' ); @@ -66,6 +66,8 @@ class IPBlockForm { $this->BlockWatchUser = $wgRequest->getBool( 'wpWatchUser', false ); # Re-check user's rights to hide names, very serious, defaults to 0 $this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0; + $this->BlockAllowUsertalk = ( $wgRequest->getBool( 'wpAllowUsertalk', $byDefault ) && $wgBlockAllowsUTEdit ); + $this->BlockReblock = $wgRequest->getBool( 'wpChangeBlock', false ); } function showForm( $err ) { @@ -85,10 +87,26 @@ class IPBlockForm { $mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' ); $titleObj = SpecialPage::getTitleFor( 'Blockip' ); - - if ( "" != $err ) { + $user = User::newFromName( $this->BlockAddress ); + + $alreadyBlocked = false; + if ( $err && $err[0] != 'ipb_already_blocked' ) { + $key = array_shift($err); + $msg = wfMsgReal($key, $err); $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) ); - $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $err ) ); + $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $msg ) ); + } elseif ( $this->BlockAddress ) { + $userId = 0; + if ( is_object( $user ) ) + $userId = $user->getId(); + $currentBlock = Block::newFromDB( $this->BlockAddress, $userId ); + if ( !is_null($currentBlock) && !$currentBlock->mAuto && # The block exists and isn't an autoblock + ( $currentBlock->mRangeStart == $currentBlock->mRangeEnd || # The block isn't a rangeblock + # or if it is, the range is what we're about to block + ( $currentBlock->mAddress == $this->BlockAddress ) ) ) { + $wgOut->addWikiMsg( 'ipb-needreblock', $this->BlockAddress ); + $alreadyBlocked = true; + } } $scBlockExpiryOptions = wfMsgForContent( 'ipboptions' ); @@ -108,7 +126,7 @@ class IPBlockForm { $reasonDropDown = Xml::listDropDown( 'wpBlockReasonList', wfMsgForContent( 'ipbreason-dropdown' ), - wfMsgForContent( 'ipbreasonotherlist' ), '', 'wpBlockDropDown', 4 ); + wfMsgForContent( 'ipbreasonotherlist' ), $this->BlockReasonList, 'wpBlockDropDown', 4 ); global $wgStylePath, $wgStyleVersion; $wgOut->addHTML( @@ -201,7 +219,7 @@ class IPBlockForm { </tr>" ); - global $wgSysopEmailBans; + global $wgSysopEmailBans, $wgBlockAllowsUTEdit; if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) { $wgOut->addHTML(" <tr id='wpEnableEmailBan'> @@ -240,25 +258,37 @@ class IPBlockForm { </td> </tr>" ); + if( $wgBlockAllowsUTEdit ){ + $wgOut->addHTML(" + <tr id='wpAllowUsertalkRow'> + <td> </td> + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'ipballowusertalk' ), + 'wpAllowUsertalk', 'wpAllowUsertalk', $this->BlockAllowUsertalk, + array( 'tabindex' => '12' ) ) . " + </td> + </tr>" + ); + } $wgOut->addHTML(" <tr> <td style='padding-top: 1em'> </td> <td class='mw-submit' style='padding-top: 1em'>" . - Xml::submitButton( wfMsg( 'ipbsubmit' ), - array( 'name' => 'wpBlock', 'tabindex' => '12' ) ) . " + Xml::submitButton( wfMsg( $alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit' ), + array( 'name' => 'wpBlock', 'tabindex' => '13', 'accesskey' => 's' ) ) . " </td> </tr>" . Xml::closeElement( 'table' ) . Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . + ( $alreadyBlocked ? Xml::hidden( 'wpChangeBlock', 1 ) : "" ) . Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . Xml::tags( 'script', array( 'type' => 'text/javascript' ), 'updateBlockOptions()' ) . "\n" ); - $wgOut->addHtml( $this->getConvenienceLinks() ); + $wgOut->addHTML( $this->getConvenienceLinks() ); - $user = User::newFromName( $this->BlockAddress ); if( is_object( $user ) ) { $this->showLogFragment( $wgOut, $user->getUserPage() ); } elseif( preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->BlockAddress ) ) { @@ -273,9 +303,8 @@ class IPBlockForm { * $userID and $expiry will be filled accordingly * @return array(message key, arguments) on failure, empty array on success */ - function doBlock(&$userId = null, &$expiry = null) - { - global $wgUser, $wgSysopUserBans, $wgSysopRangeBans; + function doBlock( &$userId = null, &$expiry = null ) { + global $wgUser, $wgSysopUserBans, $wgSysopRangeBans, $wgBlockAllowsUTEdit; $userId = 0; # Expand valid IPv6 addresses, usernames are left as is @@ -327,8 +356,12 @@ class IPBlockForm { } } + if ( $wgUser->isBlocked() && ( $wgUser->getId() !== $userId ) ) { + return array( 'cant-block-while-blocked' ); + } + $reasonstr = $this->BlockReasonList; - if ( $reasonstr != 'other' && $this->BlockReason != '') { + if ( $reasonstr != 'other' && $this->BlockReason != '' ) { // Entry from drop down menu + additional comment $reasonstr .= ': ' . $this->BlockReason; } elseif ( $reasonstr == 'other' ) { @@ -339,7 +372,7 @@ class IPBlockForm { if( $expirestr == 'other' ) $expirestr = $this->BlockOther; - if ((strlen($expirestr) == 0) || (strlen($expirestr) > 50)) { + if ( ( strlen( $expirestr ) == 0) || ( strlen( $expirestr ) > 50) ) { return array('ipb_expiry_invalid'); } @@ -358,14 +391,27 @@ class IPBlockForm { $block = new Block( $this->BlockAddress, $userId, $wgUser->getId(), $reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly, $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName, - $this->BlockEmail ); + $this->BlockEmail, isset( $this->BlockAllowUsertalk ) ? $this->BlockAllowUsertalk : $wgBlockAllowsUTEdit ); if ( wfRunHooks('BlockIp', array(&$block, &$wgUser)) ) { if ( !$block->insert() ) { - return array('ipb_already_blocked', htmlspecialchars($this->BlockAddress)); + if ( !$this->BlockReblock ) { + return array( 'ipb_already_blocked' ); + } else { + # This returns direct blocks before autoblocks/rangeblocks, since we should + # be sure the user is blocked by now it should work for our purposes + $currentBlock = Block::newFromDB( $this->BlockAddress, $userId ); + if( $block->equals( $currentBlock ) ) { + return array( 'ipb_already_blocked' ); + } + $currentBlock->delete(); + $block->insert(); + $log_action = 'reblock'; + } + } else { + $log_action = 'block'; } - wfRunHooks('BlockIpComplete', array($block, $wgUser)); if ( $this->BlockWatchUser ) { @@ -380,7 +426,7 @@ class IPBlockForm { # Make log entry, if the name is hidden, put it in the oversight log $log_type = ($this->BlockHideName) ? 'suppress' : 'block'; $log = new LogPage( $log_type ); - $log->addEntry( 'block', Title::makeTitle( NS_USER, $this->BlockAddress ), + $log->addEntry( $log_action, Title::makeTitle( NS_USER, $this->BlockAddress ), $reasonstr, $logParams ); # Report to the user @@ -404,8 +450,7 @@ class IPBlockForm { urlencode( $this->BlockAddress ) ) ); return; } - $key = array_shift($retval); - $this->showForm(wfMsgReal($key, $retval)); + $this->showForm( $retval ); } function showSuccess() { @@ -414,12 +459,22 @@ class IPBlockForm { $wgOut->setPagetitle( wfMsg( 'blockip' ) ); $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) ); $text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress ); - $wgOut->addHtml( $text ); + $wgOut->addHTML( $text ); } function showLogFragment( $out, $title ) { - $out->addHtml( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) ); - LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText() ); + global $wgUser; + $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) ); + $count = LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText(), '', 10 ); + if($count > 10){ + $out->addHTML( $wgUser->getSkin()->link( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'blocklog-fulllog' ), + array(), + array( + 'type' => 'block', + 'page' => $title->getPrefixedText() ) ) ); + } } /** @@ -429,6 +484,7 @@ class IPBlockForm { * @return array */ private function blockLogFlags() { + global $wgBlockAllowsUTEdit; $flags = array(); if( $this->BlockAnonOnly && IP::isIPAddress( $this->BlockAddress ) ) // when blocking a user the option 'anononly' is not available/has no effect -> do not write this into log @@ -439,6 +495,8 @@ class IPBlockForm { $flags[] = 'noautoblock'; if ( $this->BlockEmail ) $flags[] = 'noemail'; + if ( !$this->BlockAllowUsertalk && $wgBlockAllowsUTEdit ) + $flags[] = 'nousertalk'; return implode( ',', $flags ); } @@ -450,11 +508,25 @@ class IPBlockForm { private function getConvenienceLinks() { global $wgUser; $skin = $wgUser->getSkin(); - $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) ); + if( $this->BlockAddress ) + $links[] = $this->getContribsLink( $skin ); $links[] = $this->getUnblockLink( $skin ); $links[] = $this->getBlockListLink( $skin ); + $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) ); return '<p class="mw-ipb-conveniencelinks">' . implode( ' | ', $links ) . '</p>'; } + + /** + * Build a convenient link to a user or IP's contribs + * form + * + * @param $skin Skin to use + * @return string + */ + private function getContribsLink( $skin ) { + $contribsPage = SpecialPage::getTitleFor( 'Contributions', $this->BlockAddress ); + return $skin->link( $contribsPage, wfMsgHtml( 'ipb-blocklist-contribs', $this->BlockAddress ) ); + } /** * Build a convenient link to unblock the given username or IP @@ -491,4 +563,77 @@ class IPBlockForm { return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) ); } } + + /** + * Block a list of selected users + * @param array $users + * @param string $reason + * @param string $tag replaces user pages + * @param string $talkTag replaces user talk pages + * @returns array, list of html-safe usernames + */ + public static function doMassUserBlock( $users, $reason = '', $tag = '', $talkTag = '' ) { + global $wgUser; + $counter = $blockSize = 0; + $safeUsers = array(); + $log = new LogPage( 'block' ); + foreach( $users as $name ) { + # Enforce limits + $counter++; + $blockSize++; + # Lets not go *too* fast + if( $blockSize >= 20 ) { + $blockSize = 0; + wfWaitForSlaves( 5 ); + } + $u = User::newFromName( $name, false ); + // If user doesn't exist, it ought to be an IP then + if( is_null($u) || (!$u->getId() && !IP::isIPAddress( $u->getName() )) ) { + continue; + } + $userTitle = $u->getUserPage(); + $userTalkTitle = $u->getTalkPage(); + $userpage = new Article( $userTitle ); + $usertalk = new Article( $userTalkTitle ); + $safeUsers[] = '[[' . $userTitle->getPrefixedText() . '|' . $userTitle->getText() . ']]'; + $expirestr = $u->getId() ? 'indefinite' : '1 week'; + $expiry = Block::parseExpiryInput( $expirestr ); + $anonOnly = IP::isIPAddress( $u->getName() ) ? 1 : 0; + // Create the block + $block = new Block( $u->getName(), // victim + $u->getId(), // uid + $wgUser->getId(), // blocker + $reason, // comment + wfTimestampNow(), // block time + 0, // auto ? + $expiry, // duration + $anonOnly, // anononly? + 1, // block account creation? + 1, // autoblocking? + 0, // suppress name? + 0 // block from sending email? + ); + $oldblock = Block::newFromDB( $u->getName(), $u->getId() ); + if( !$oldblock ) { + $block->insert(); + # Prepare log parameters + $logParams = array(); + $logParams[] = $expirestr; + if( $anonOnly ) { + $logParams[] = 'anononly'; + } + $logParams[] = 'nocreate'; + # Add log entry + $log->addEntry( 'block', $userTitle, $reason, $logParams ); + } + # Tag userpage! (check length to avoid mistakes) + if( strlen($tag) > 2 ) { + $userpage->doEdit( $tag, $reason, EDIT_MINOR ); + } + if( strlen($talkTag) > 2 ) { + $usertalk->doEdit( $talkTag, $reason, EDIT_MINOR ); + } + } + return $safeUsers; + } } diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php index 0690c5c0..12b119d8 100644 --- a/includes/specials/SpecialBooksources.php +++ b/includes/specials/SpecialBooksources.php @@ -30,20 +30,62 @@ class SpecialBookSources extends SpecialPage { public function execute( $isbn ) { global $wgOut, $wgRequest; $this->setHeaders(); - $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) ); + $this->isbn = self::cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) ); $wgOut->addWikiMsg( 'booksources-summary' ); - $wgOut->addHtml( $this->makeForm() ); - if( strlen( $this->isbn ) > 0 ) + $wgOut->addHTML( $this->makeForm() ); + if( strlen( $this->isbn ) > 0 ) { + if( !$this->isValidIsbn( $this->isbn ) ) { + $wgOut->wrapWikiMsg( '<div class="error">$1</div>', 'booksources-invalid-isbn' ); + } $this->showList(); + } } /** + * Returns whether a given ISBN (10 or 13) is valid. True indicates validity. + * @param isbn ISBN passed for check + */ + public static function isValidISBN( $isbn ) { + $isbn = self::cleanIsbn( $isbn ); + $sum = 0; + $check = -1; + if( strlen( $isbn ) == 13 ) { + for( $i = 0; $i < 12; $i++ ) { + if($i % 2 == 0) { + $sum += $isbn{$i}; + } else { + $sum += 3 * $isbn{$i}; + } + } + + $check = (10 - ($sum % 10)) % 10; + if ($check == $isbn{12}) { + return true; + } + } elseif( strlen( $isbn ) == 10 ) { + for($i = 0; $i < 9; $i++) { + $sum += $isbn{$i} * ($i + 1); + } + + $check = $sum % 11; + if($check == 10) { + $check = "X"; + } + if($check == $isbn{9}) { + return true; + } + } + return false; + } + + + /** * Trim ISBN and remove characters which aren't required * * @param $isbn Unclean ISBN * @return string */ - private function cleanIsbn( $isbn ) { + private static function cleanIsbn( $isbn ) { return trim( preg_replace( '![^0-9X]!', '', $isbn ) ); } @@ -88,11 +130,11 @@ class SpecialBookSources extends SpecialPage { # Fall back to the defaults given in the language file $wgOut->addWikiMsg( 'booksources-text' ); - $wgOut->addHtml( '<ul>' ); + $wgOut->addHTML( '<ul>' ); $items = $wgContLang->getBookstoreList(); foreach( $items as $label => $url ) - $wgOut->addHtml( $this->makeListItem( $label, $url ) ); - $wgOut->addHtml( '</ul>' ); + $wgOut->addHTML( $this->makeListItem( $label, $url ) ); + $wgOut->addHTML( '</ul>' ); return true; } diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php index 951c2228..8c2ae2b6 100644 --- a/includes/specials/SpecialCategories.php +++ b/includes/specials/SpecialCategories.php @@ -14,11 +14,13 @@ function wfSpecialCategories( $par=null ) { } $cap = new CategoryPager( $from ); $wgOut->addHTML( + XML::openElement( 'div', array('class' => 'mw-spcontent') ) . wfMsgExt( 'categoriespagetext', array( 'parse' ) ) . $cap->getStartForm( $from ) . $cap->getNavigationBar() . '<ul>' . $cap->getBody() . '</ul>' . - $cap->getNavigationBar() + $cap->getNavigationBar() . + XML::closeElement( 'div' ) ); } diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php index 9075fb95..e19aa99b 100644 --- a/includes/specials/SpecialConfirmemail.php +++ b/includes/specials/SpecialConfirmemail.php @@ -36,8 +36,9 @@ class EmailConfirmation extends UnlistedSpecialPage { $title = SpecialPage::getTitleFor( 'Userlogin' ); $self = SpecialPage::getTitleFor( 'Confirmemail' ); $skin = $wgUser->getSkin(); - $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $self->getPrefixedUrl() ); - $wgOut->addHtml( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) ); + $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), + 'returnto=' . $self->getPrefixedUrl() ); + $wgOut->addHTML( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) ); } } else { $this->attemptConfirm( $code ); @@ -58,19 +59,24 @@ class EmailConfirmation extends UnlistedSpecialPage { } } else { if( $wgUser->isEmailConfirmed() ) { + // date and time are separate parameters to facilitate localisation. + // $time is kept for backward compat reasons. + // 'emailauthenticated' is also used in SpecialPreferences.php $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true ); - $wgOut->addWikiMsg( 'emailauthenticated', $time ); + $d = $wgLang->date( $wgUser->mEmailAuthenticated, true ); + $t = $wgLang->time( $wgUser->mEmailAuthenticated, true ); + $wgOut->addWikiMsg( 'emailauthenticated', $time, $d, $t ); } if( $wgUser->isEmailConfirmationPending() ) { $wgOut->addWikiMsg( 'confirmemail_pending' ); } $wgOut->addWikiMsg( 'confirmemail_text' ); $self = SpecialPage::getTitleFor( 'Confirmemail' ); - $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); - $form .= wfHidden( 'token', $wgUser->editToken() ); - $form .= wfSubmitButton( wfMsgHtml( 'confirmemail_send' ) ); - $form .= wfCloseElement( 'form' ); - $wgOut->addHtml( $form ); + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); + $form .= Xml::hidden( 'token', $wgUser->editToken() ); + $form .= Xml::submitButton( wfMsgHtml( 'confirmemail_send' ) ); + $form .= Xml::closeElement( 'form' ); + $wgOut->addHTML( $form ); } } diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 4a131f15..3d8c18dd 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -4,6 +4,363 @@ * @file * @ingroup SpecialPage */ + +class SpecialContributions extends SpecialPage { + + public function __construct() { + parent::__construct( 'Contributions' ); + } + + public function execute( $par ) { + global $wgUser, $wgOut, $wgLang, $wgRequest; + + $this->setHeaders(); + $this->outputHeader(); + + $this->opts = array(); + + if( $par == 'newbies' ) { + $target = 'newbies'; + $this->opts['contribs'] = 'newbie'; + } elseif( isset( $par ) ) { + $target = $par; + } else { + $target = $wgRequest->getVal( 'target' ); + } + + // check for radiobox + if( $wgRequest->getVal( 'contribs' ) == 'newbie' ) { + $target = 'newbies'; + $this->opts['contribs'] = 'newbie'; + } + + if( !strlen( $target ) ) { + $wgOut->addHTML( $this->getForm( '' ) ); + return; + } + + $this->opts['limit'] = $wgRequest->getInt( 'limit', 50 ); + $this->opts['target'] = $target; + + $nt = Title::makeTitleSafe( NS_USER, $target ); + if( !$nt ) { + $wgOut->addHTML( $this->getForm( '' ) ); + return; + } + $id = User::idFromName( $nt->getText() ); + + if( $target != 'newbies' ) { + $target = $nt->getText(); + $wgOut->setSubtitle( $this->contributionsSub( $nt, $id ) ); + $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'contributions-title', $target ) ) ); + } else { + $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') ); + $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'sp-contributions-newbies-title' ) ) ); + } + + if( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) { + $this->opts['namespace'] = intval( $ns ); + } else { + $this->opts['namespace'] = ''; + } + + // Allows reverts to have the bot flag in recent changes. It is just here to + // be passed in the form at the top of the page + if( $wgUser->isAllowed( 'markbotedits' ) && $wgRequest->getBool( 'bot' ) ) { + $this->opts['bot'] = '1'; + } + + $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev'; + # Offset overrides year/month selection + if( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) { + $this->opts['month'] = intval( $month ); + } else { + $this->opts['month'] = ''; + } + if( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) { + $this->opts['year'] = intval( $year ); + } else if( $this->opts['month'] ) { + $thisMonth = intval( gmdate( 'n' ) ); + $thisYear = intval( gmdate( 'Y' ) ); + if( intval( $this->opts['month'] ) > $thisMonth ) { + $thisYear--; + } + $this->opts['year'] = $thisYear; + } else { + $this->opts['year'] = ''; + } + + if( $skip ) { + $this->opts['year'] = ''; + $this->opts['month'] = ''; + } + + // Add RSS/atom links + $this->setSyndicated(); + $feedType = $wgRequest->getVal( 'feed' ); + if( $feedType ) { + return $this->feed( $feedType ); + } + + wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id ); + + $wgOut->addHTML( $this->getForm( $this->opts ) ); + + $pager = new ContribsPager( $target, $this->opts['namespace'], $this->opts['year'], $this->opts['month'] ); + if( !$pager->getNumRows() ) { + $wgOut->addWikiMsg( 'nocontribs' ); + return; + } + + # Show a message about slave lag, if applicable + if( ( $lag = $pager->getDatabase()->getLag() ) > 0 ) + $wgOut->showLagWarning( $lag ); + + $wgOut->addHTML( + '<p>' . $pager->getNavigationBar() . '</p>' . + $pager->getBody() . + '<p>' . $pager->getNavigationBar() . '</p>' + ); + + # If there were contributions, and it was a valid user or IP, show + # the appropriate "footer" message - WHOIS tools, etc. + if( $target != 'newbies' ) { + $message = IP::isIPAddress( $target ) ? + 'sp-contributions-footer-anon' : 'sp-contributions-footer'; + + $text = wfMsgNoTrans( $message, $target ); + if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { + $wgOut->addHTML( '<div class="mw-contributions-footer">' ); + $wgOut->addWikiText( $text ); + $wgOut->addHTML( '</div>' ); + } + } + } + + protected function setSyndicated() { + global $wgOut; + $queryParams = array( + 'namespace' => $this->opts['namespace'], + 'target' => $this->opts['target'] + ); + $wgOut->setSyndicated( true ); + $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) ); + } + + /** + * Generates the subheading with links + * @param Title $nt Title object for the target + * @param integer $id User ID for the target + * @return String: appropriately-escaped HTML to be output literally + */ + protected function contributionsSub( $nt, $id ) { + global $wgSysopUserBans, $wgLang, $wgUser; + + $sk = $wgUser->getSkin(); + + if( 0 == $id ) { + $user = $nt->getText(); + } else { + $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) ); + } + $talk = $nt->getTalkPage(); + if( $talk ) { + # Talk page link + $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) ); + if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && IP::isIPAddress( $nt->getText() ) ) ) { + # Block link + if( $wgUser->isAllowed( 'block' ) ) + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', + $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) ); + # Block log link + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() ); + } + # Other logs link + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), + 'user=' . $nt->getPartialUrl() ); + + # Add link to deleted user contributions for priviledged users + if( $wgUser->isAllowed( 'deletedhistory' ) ) { + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'DeletedContributions', + $nt->getDBkey() ), wfMsgHtml( 'deletedcontributions' ) ); + } + + wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); + + $links = implode( ' | ', $tools ); + } + + // Old message 'contribsub' had one parameter, but that doesn't work for + // languages that want to put the "for" bit right after $user but before + // $links. If 'contribsub' is around, use it for reverse compatibility, + // otherwise use 'contribsub2'. + if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) { + return wfMsgHtml( 'contribsub2', $user, $links ); + } else { + return wfMsgHtml( 'contribsub', "$user ($links)" ); + } + } + + /** + * Generates the namespace selector form with hidden attributes. + * @param $this->opts Array: the options to be included. + */ + protected function getForm() { + global $wgScript, $wgTitle; + + $this->opts['title'] = $wgTitle->getPrefixedText(); + if( !isset( $this->opts['target'] ) ) { + $this->opts['target'] = ''; + } else { + $this->opts['target'] = str_replace( '_' , ' ' , $this->opts['target'] ); + } + + if( !isset( $this->opts['namespace'] ) ) { + $this->opts['namespace'] = ''; + } + + if( !isset( $this->opts['contribs'] ) ) { + $this->opts['contribs'] = 'user'; + } + + if( !isset( $this->opts['year'] ) ) { + $this->opts['year'] = ''; + } + + if( !isset( $this->opts['month'] ) ) { + $this->opts['month'] = ''; + } + + if( $this->opts['contribs'] == 'newbie' ) { + $this->opts['target'] = ''; + } + + $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + + foreach ( $this->opts as $name => $value ) { + if( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) { + continue; + } + $f .= "\t" . Xml::hidden( $name, $value ) . "\n"; + } + + $f .= '<fieldset>' . + Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) . + Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), + 'contribs', 'newbie' , 'newbie', $this->opts['contribs'] == 'newbie' ? true : false ) . '<br />' . + Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), + 'contribs' , 'user', 'user', $this->opts['contribs'] == 'user' ? true : false ) . ' ' . + Xml::input( 'target', 20, $this->opts['target']) . ' '. + '<span style="white-space: nowrap">' . + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . + Xml::namespaceSelector( $this->opts['namespace'], '' ) . + '</span>' . + Xml::openElement( 'p' ) . + '<span style="white-space: nowrap">' . + Xml::label( wfMsg( 'year' ), 'year' ) . ' '. + Xml::input( 'year', 4, $this->opts['year'], array('id' => 'year', 'maxlength' => 4) ) . + '</span>' . + ' '. + '<span style="white-space: nowrap">' . + Xml::label( wfMsg( 'month' ), 'month' ) . ' '. + Xml::monthSelector( $this->opts['month'], -1 ) . ' '. + '</span>' . + Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) . + Xml::closeElement( 'p' ); + + $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' ); + if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) ) + $f .= "<p>{$explain}</p>"; + + $f .= '</fieldset>' . + Xml::closeElement( 'form' ); + return $f; + } + + /** + * Output a subscription feed listing recent edits to this page. + * @param string $type + */ + protected function feed( $type ) { + global $wgRequest, $wgFeed, $wgFeedClasses, $wgFeedLimit; + + if( !$wgFeed ) { + global $wgOut; + $wgOut->addWikiMsg( 'feed-unavailable' ); + return; + } + + if( !isset( $wgFeedClasses[$type] ) ) { + global $wgOut; + $wgOut->addWikiMsg( 'feed-invalid' ); + return; + } + + $feed = new $wgFeedClasses[$type]( + $this->feedTitle(), + wfMsgExt( 'tagline', 'parsemag' ), + $this->getTitle()->getFullUrl() ); + + // Already valid title + $nt = Title::makeTitleSafe( NS_USER, $this->opts['target'] ); + $target = $this->opts['target'] == 'newbies' ? 'newbies' : $nt->getText(); + + $pager = new ContribsPager( $target, $this->opts['namespace'], + $this->opts['year'], $this->opts['month'] ); + + $pager->mLimit = min( $this->opts['limit'], $wgFeedLimit ); + + $feed->outHeader(); + if( $pager->getNumRows() > 0 ) { + while( $row = $pager->mResult->fetchObject() ) { + $feed->outItem( $this->feedItem( $row ) ); + } + } + $feed->outFooter(); + } + + protected function feedTitle() { + global $wgContLanguageCode, $wgSitename; + $page = SpecialPage::getPage( 'Contributions' ); + $desc = $page->getDescription(); + return "$wgSitename - $desc [$wgContLanguageCode]"; + } + + protected function feedItem( $row ) { + $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title ); + if( $title ) { + $date = $row->rev_timestamp; + $comments = $title->getTalkPage()->getFullURL(); + $revision = Revision::newFromTitle( $title, $row->rev_id ); + + return new FeedItem( + $title->getPrefixedText(), + $this->feedItemDesc( $revision ), + $title->getFullURL(), + $date, + $this->feedItemAuthor( $revision ), + $comments + ); + } else { + return NULL; + } + } + + protected function feedItemAuthor( $revision ) { + return $revision->getUserText(); + } + + protected function feedItemDesc( $revision ) { + if( $revision ) { + return '<p>' . htmlspecialchars( $revision->getUserText() ) . ': ' . + htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . + "</p>\n<hr />\n<div>" . + nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>"; + } + return ''; + } +} /** * Pager for Special:Contributions @@ -12,7 +369,7 @@ class ContribsPager extends ReverseChronologicalPager { public $mDefaultDirection = true; var $messages, $target; - var $namespace = '', $year = '', $month = '', $mDb; + var $namespace = '', $mDb; function __construct( $target, $namespace = false, $year = false, $month = false ) { parent::__construct(); @@ -22,12 +379,7 @@ class ContribsPager extends ReverseChronologicalPager { $this->target = $target; $this->namespace = $namespace; - $year = intval($year); - $month = intval($month); - - $this->year = $year > 0 ? $year : false; - $this->month = ($month > 0 && $month < 13) ? $month : false; - $this->getDateCond(); + $this->getDateCond( $year, $month ); $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); } @@ -35,23 +387,22 @@ class ContribsPager extends ReverseChronologicalPager { function getDefaultQuery() { $query = parent::getDefaultQuery(); $query['target'] = $this->target; - $query['month'] = $this->month; - $query['year'] = $this->year; return $query; } function getQueryInfo() { - list( $index, $userCond ) = $this->getUserCond(); + list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond(); $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() ); $queryInfo = array( - 'tables' => array( 'page', 'revision' ), + 'tables' => $tables, 'fields' => array( 'page_namespace', 'page_title', 'page_is_new', 'page_latest', 'rev_id', 'rev_page', 'rev_text_id', 'rev_timestamp', 'rev_comment', 'rev_minor_edit', 'rev_user', 'rev_user_text', 'rev_parent_id', 'rev_deleted' ), 'conds' => $conds, - 'options' => array( 'USE INDEX' => array('revision' => $index) ) + 'options' => array( 'USE INDEX' => array('revision' => $index) ), + 'join_conds' => $join_cond ); wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) ); return $queryInfo; @@ -59,73 +410,31 @@ class ContribsPager extends ReverseChronologicalPager { function getUserCond() { $condition = array(); - - if ( $this->target == 'newbies' ) { + $join_conds = array(); + if( $this->target == 'newbies' ) { + $tables = array( 'user_groups', 'page', 'revision' ); $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); $condition[] = 'rev_user >' . (int)($max - $max / 100); + $condition[] = 'ug_group IS NULL'; $index = 'user_timestamp'; + # FIXME: other groups may have 'bot' rights + $join_conds['user_groups'] = array( 'LEFT JOIN', "ug_user = rev_user AND ug_group = 'bot'" ); } else { + $tables = array( 'page', 'revision' ); $condition['rev_user_text'] = $this->target; $index = 'usertext_timestamp'; } - return array( $index, $condition ); + return array( $tables, $index, $condition, $join_conds ); } function getNamespaceCond() { - if ( $this->namespace !== '' ) { + if( $this->namespace !== '' ) { return array( 'page_namespace' => (int)$this->namespace ); } else { return array(); } } - function getDateCond() { - // Given an optional year and month, we need to generate a timestamp - // to use as "WHERE rev_timestamp <= result" - // Examples: year = 2006 equals < 20070101 (+000000) - // year=2005, month=1 equals < 20050201 - // year=2005, month=12 equals < 20060101 - - if (!$this->year && !$this->month) - return; - - if ( $this->year ) { - $year = $this->year; - } - else { - // If no year given, assume the current one - $year = gmdate( 'Y' ); - // If this month hasn't happened yet this year, go back to last year's month - if( $this->month > gmdate( 'n' ) ) { - $year--; - } - } - - if ( $this->month ) { - $month = $this->month + 1; - // For December, we want January 1 of the next year - if ($month > 12) { - $month = 1; - $year++; - } - } - else { - // No month implies we want up to the end of the year in question - $month = 1; - $year++; - } - - if ($year > 2032) - $year = 2032; - $ymd = (int)sprintf( "%04d%02d01", $year, $month ); - - // Y2K38 bug - if ($ymd > 20320101) - $ymd = 20320101; - - $this->mOffset = $this->mDb->timestamp( "${ymd}000000" ); - } - function getIndexField() { return 'rev_timestamp'; } @@ -167,8 +476,7 @@ class ContribsPager extends ReverseChronologicalPager { $difftext .= $this->messages['newarticle']; } - if( !$page->getUserPermissionsErrors( 'rollback', $wgUser ) - && !$page->getUserPermissionsErrors( 'edit', $wgUser ) ) { + if( $page->userCan( 'rollback') && $page->userCan( 'edit' ) ) { $topmarktext .= ' '.$sk->generateRollback( $rev ); } @@ -182,7 +490,8 @@ class ContribsPager extends ReverseChronologicalPager { $histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')'; $comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true ); - $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true ); + $date = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true ); + $d = $sk->makeKnownLinkObj( $page, $date, 'oldid='.intval($row->rev_id) ); if( $this->target == 'newbies' ) { $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text ); @@ -207,7 +516,7 @@ class ContribsPager extends ReverseChronologicalPager { $mflag = ''; } - $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink}{$comment} {$topmarktext}"; + $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}"; if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $ret .= ' ' . wfMsgHtml( 'deletedrev' ); } @@ -229,242 +538,3 @@ class ContribsPager extends ReverseChronologicalPager { } } - -/** - * Special page "user contributions". - * Shows a list of the contributions of a user. - * - * @return none - * @param $par String: (optional) user name of the user for which to show the contributions - */ -function wfSpecialContributions( $par = null ) { - global $wgUser, $wgOut, $wgLang, $wgRequest; - - $options = array(); - - if ( isset( $par ) && $par == 'newbies' ) { - $target = 'newbies'; - $options['contribs'] = 'newbie'; - } elseif ( isset( $par ) ) { - $target = $par; - } else { - $target = $wgRequest->getVal( 'target' ); - } - - // check for radiobox - if ( $wgRequest->getVal( 'contribs' ) == 'newbie' ) { - $target = 'newbies'; - $options['contribs'] = 'newbie'; - } - - if ( !strlen( $target ) ) { - $wgOut->addHTML( contributionsForm( '' ) ); - return; - } - - $options['limit'] = $wgRequest->getInt( 'limit', 50 ); - $options['target'] = $target; - - $nt = Title::makeTitleSafe( NS_USER, $target ); - if ( !$nt ) { - $wgOut->addHTML( contributionsForm( '' ) ); - return; - } - $id = User::idFromName( $nt->getText() ); - - if ( $target != 'newbies' ) { - $target = $nt->getText(); - $wgOut->setSubtitle( contributionsSub( $nt, $id ) ); - } else { - $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') ); - } - - if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) { - $options['namespace'] = intval( $ns ); - } else { - $options['namespace'] = ''; - } - if ( $wgUser->isAllowed( 'markbotedit' ) && $wgRequest->getBool( 'bot' ) ) { - $options['bot'] = '1'; - } - - $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev'; - # Offset overrides year/month selection - if ( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) { - $options['month'] = intval( $month ); - } else { - $options['month'] = ''; - } - if ( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) { - $options['year'] = intval( $year ); - } else if( $options['month'] ) { - $thisMonth = intval( gmdate( 'n' ) ); - $thisYear = intval( gmdate( 'Y' ) ); - if( intval( $options['month'] ) > $thisMonth ) { - $thisYear--; - } - $options['year'] = $thisYear; - } else { - $options['year'] = ''; - } - - wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id ); - - if( $skip ) { - $options['year'] = ''; - $options['month'] = ''; - } - - $wgOut->addHTML( contributionsForm( $options ) ); - - $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] ); - if ( !$pager->getNumRows() ) { - $wgOut->addWikiMsg( 'nocontribs' ); - return; - } - - # Show a message about slave lag, if applicable - if( ( $lag = $pager->getDatabase()->getLag() ) > 0 ) - $wgOut->showLagWarning( $lag ); - - $wgOut->addHTML( - '<p>' . $pager->getNavigationBar() . '</p>' . - $pager->getBody() . - '<p>' . $pager->getNavigationBar() . '</p>' ); - - # If there were contributions, and it was a valid user or IP, show - # the appropriate "footer" message - WHOIS tools, etc. - if( $target != 'newbies' ) { - $message = IP::isIPAddress( $target ) - ? 'sp-contributions-footer-anon' - : 'sp-contributions-footer'; - - - $text = wfMsgNoTrans( $message, $target ); - if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { - $wgOut->addHtml( '<div class="mw-contributions-footer">' ); - $wgOut->addWikiText( $text ); - $wgOut->addHtml( '</div>' ); - } - } -} - -/** - * Generates the subheading with links - * @param Title $nt Title object for the target - * @param integer $id User ID for the target - * @return String: appropriately-escaped HTML to be output literally - */ -function contributionsSub( $nt, $id ) { - global $wgSysopUserBans, $wgLang, $wgUser; - - $sk = $wgUser->getSkin(); - - if ( 0 == $id ) { - $user = $nt->getText(); - } else { - $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) ); - } - $talk = $nt->getTalkPage(); - if( $talk ) { - # Talk page link - $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) ); - if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) { - # Block link - if( $wgUser->isAllowed( 'block' ) ) - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) ); - # Block log link - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() ); - } - # Other logs link - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() ); - - wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); - - $links = implode( ' | ', $tools ); - } - - // Old message 'contribsub' had one parameter, but that doesn't work for - // languages that want to put the "for" bit right after $user but before - // $links. If 'contribsub' is around, use it for reverse compatibility, - // otherwise use 'contribsub2'. - if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) { - return wfMsgHtml( 'contribsub2', $user, $links ); - } else { - return wfMsgHtml( 'contribsub', "$user ($links)" ); - } -} - -/** - * Generates the namespace selector form with hidden attributes. - * @param $options Array: the options to be included. - */ -function contributionsForm( $options ) { - global $wgScript, $wgTitle, $wgRequest; - - $options['title'] = $wgTitle->getPrefixedText(); - if ( !isset( $options['target'] ) ) { - $options['target'] = ''; - } else { - $options['target'] = str_replace( '_' , ' ' , $options['target'] ); - } - - if ( !isset( $options['namespace'] ) ) { - $options['namespace'] = ''; - } - - if ( !isset( $options['contribs'] ) ) { - $options['contribs'] = 'user'; - } - - if ( !isset( $options['year'] ) ) { - $options['year'] = ''; - } - - if ( !isset( $options['month'] ) ) { - $options['month'] = ''; - } - - if ( $options['contribs'] == 'newbie' ) { - $options['target'] = ''; - } - - $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); - - foreach ( $options as $name => $value ) { - if ( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) { - continue; - } - $f .= "\t" . Xml::hidden( $name, $value ) . "\n"; - } - - $f .= '<fieldset>' . - Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) . - Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), 'contribs' , 'newbie' , 'newbie', $options['contribs'] == 'newbie' ? true : false ) . '<br />' . - Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), 'contribs' , 'user', 'user', $options['contribs'] == 'user' ? true : false ) . ' ' . - Xml::input( 'target', 20, $options['target']) . ' '. - '<span style="white-space: nowrap">' . - Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . - Xml::namespaceSelector( $options['namespace'], '' ) . - '</span>' . - Xml::openElement( 'p' ) . - '<span style="white-space: nowrap">' . - Xml::label( wfMsg( 'year' ), 'year' ) . ' '. - Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) . - '</span>' . - ' '. - '<span style="white-space: nowrap">' . - Xml::label( wfMsg( 'month' ), 'month' ) . ' '. - Xml::monthSelector( $options['month'], -1 ) . ' '. - '</span>' . - Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) . - Xml::closeElement( 'p' ); - - $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' ); - if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) ) - $f .= "<p>{$explain}</p>"; - - $f .= '</fieldset>' . - Xml::closeElement( 'form' ); - return $f; -} diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php new file mode 100644 index 00000000..513d25e2 --- /dev/null +++ b/includes/specials/SpecialDeletedContributions.php @@ -0,0 +1,369 @@ +<?php +/** + * Implements Special:DeletedContributions to display archived revisions + * @ingroup SpecialPage + */ + +class DeletedContribsPager extends IndexPager { + public $mDefaultDirection = true; + var $messages, $target; + var $namespace = '', $mDb; + + function __construct( $target, $namespace = false ) { + parent::__construct(); + foreach( explode( ' ', 'deletionlog undeletebtn minoreditletter diff' ) as $msg ) { + $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') ); + } + $this->target = $target; + $this->namespace = $namespace; + $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); + } + + function getDefaultQuery() { + $query = parent::getDefaultQuery(); + $query['target'] = $this->target; + return $query; + } + + function getQueryInfo() { + list( $index, $userCond ) = $this->getUserCond(); + $conds = array_merge( $userCond, $this->getNamespaceCond() ); + + return array( + 'tables' => array( 'archive' ), + 'fields' => array( + 'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp', 'ar_comment', 'ar_minor_edit', + 'ar_user', 'ar_user_text', 'ar_deleted' + ), + 'conds' => $conds, + 'options' => array( 'FORCE INDEX' => $index ) + ); + } + + function getUserCond() { + $condition = array(); + + $condition['ar_user_text'] = $this->target; + $index = 'usertext_timestamp'; + + return array( $index, $condition ); + } + + function getIndexField() { + return 'ar_timestamp'; + } + + function getStartBody() { + return "<ul>\n"; + } + + function getEndBody() { + return "</ul>\n"; + } + + function getNavigationBar() { + if ( isset( $this->mNavigationBar ) ) { + return $this->mNavigationBar; + } + $linkTexts = array( + 'prev' => wfMsgHtml( 'pager-newer-n', $this->mLimit ), + 'next' => wfMsgHtml( 'pager-older-n', $this->mLimit ), + 'first' => wfMsgHtml( 'histlast' ), + 'last' => wfMsgHtml( 'histfirst' ) + ); + + $pagingLinks = $this->getPagingLinks( $linkTexts ); + $limitLinks = $this->getLimitLinks(); + $limits = implode( ' | ', $limitLinks ); + + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . + wfMsgExt( 'viewprevnext', array( 'parsemag' ), $pagingLinks['prev'], $pagingLinks['next'], $limits ); + return $this->mNavigationBar; + } + + function getNamespaceCond() { + if ( $this->namespace !== '' ) { + return array( 'ar_namespace' => (int)$this->namespace ); + } else { + return array(); + } + } + + /** + * Generates each row in the contributions list. + * + * Contributions which are marked "top" are currently on top of the history. + * For these contributions, a [rollback] link is shown for users with sysop + * privileges. The rollback link restores the most recent version that was not + * written by the target user. + * + * @todo This would probably look a lot nicer in a table. + */ + function formatRow( $row ) { + wfProfileIn( __METHOD__ ); + + global $wgLang, $wgUser; + + $sk = $this->getSkin(); + + $rev = new Revision( array( + 'id' => $row->ar_rev_id, + 'comment' => $row->ar_comment, + 'user' => $row->ar_user, + 'user_text' => $row->ar_user_text, + 'timestamp' => $row->ar_timestamp, + 'minor_edit' => $row->ar_minor_edit, + 'rev_deleted' => $row->ar_deleted, + ) ); + + $page = Title::makeTitle( $row->ar_namespace, $row->ar_title ); + + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + + $logs = SpecialPage::getTitleFor( 'Log' ); + $dellog = $sk->makeKnownLinkObj( $logs, + $this->messages['deletionlog'], + 'type=delete&page=' . $page->getPrefixedUrl() ); + + $reviewlink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ), + $this->messages['undeletebtn'] ); + + $link = $sk->makeKnownLinkObj( $undelete, + htmlspecialchars( $page->getPrefixedText() ), + 'target=' . $page->getPrefixedUrl() . + '×tamp=' . $rev->getTimestamp() ); + + $last = $sk->makeKnownLinkObj( $undelete, + $this->messages['diff'], + "target=" . $page->getPrefixedUrl() . + "×tamp=" . $rev->getTimestamp() . + "&diff=prev" ); + + $comment = $sk->revComment( $rev ); + $d = $wgLang->timeanddate( $rev->getTimestamp(), true ); + + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $d = '<span class="history-deleted">' . $d . '</span>'; + } else { + $link = $sk->makeKnownLinkObj( $undelete, $d, + 'target=' . $page->getPrefixedUrl() . + '×tamp=' . $rev->getTimestamp() ); + } + + $pagelink = $sk->makeLinkObj( $page ); + + if( $rev->isMinor() ) { + $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> '; + } else { + $mflag = ''; + } + + + $ret = "{$link} ($last) ({$dellog}) ({$reviewlink}) . . {$mflag} {$pagelink} {$comment}"; + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $ret .= ' ' . wfMsgHtml( 'deletedrev' ); + } + + $ret = "<li>$ret</li>\n"; + + wfProfileOut( __METHOD__ ); + return $ret; + } + + /** + * Get the Database object in use + * + * @return Database + */ + public function getDatabase() { + return $this->mDb; + } +} + +class DeletedContributionsPage extends SpecialPage { + function __construct() { + parent::__construct( 'DeletedContributions', 'deletedhistory', + /*listed*/ true, /*function*/ false, /*file*/ false ); + } + + /** + * Special page "deleted user contributions". + * Shows a list of the deleted contributions of a user. + * + * @return none + * @param $par String: (optional) user name of the user for which to show the contributions + */ + function execute( $par ) { + global $wgUser; + $this->setHeaders(); + + if ( !$this->userCanExecute( $wgUser ) ) { + $this->displayRestrictionError(); + return; + } + + global $wgUser, $wgOut, $wgLang, $wgRequest; + + $options = array(); + + if ( isset( $par ) ) { + $target = $par; + } else { + $target = $wgRequest->getVal( 'target' ); + } + + if ( !strlen( $target ) ) { + $wgOut->addHTML( $this->getForm( '' ) ); + return; + } + + $options['limit'] = $wgRequest->getInt( 'limit', 50 ); + $options['target'] = $target; + + $nt = Title::makeTitleSafe( NS_USER, $target ); + if ( !$nt ) { + $wgOut->addHTML( $this->getForm( '' ) ); + return; + } + $id = User::idFromName( $nt->getText() ); + + $target = $nt->getText(); + $wgOut->setSubtitle( $this->getSubTitle( $nt, $id ) ); + + if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) { + $options['namespace'] = intval( $ns ); + } else { + $options['namespace'] = ''; + } + + $wgOut->addHTML( $this->getForm( $options ) ); + + $pager = new DeletedContribsPager( $target, $options['namespace'] ); + if ( !$pager->getNumRows() ) { + $wgOut->addWikiText( wfMsg( 'nocontribs' ) ); + return; + } + + # Show a message about slave lag, if applicable + if( ( $lag = $pager->getDatabase()->getLag() ) > 0 ) + $wgOut->showLagWarning( $lag ); + + $wgOut->addHTML( + '<p>' . $pager->getNavigationBar() . '</p>' . + $pager->getBody() . + '<p>' . $pager->getNavigationBar() . '</p>' ); + + # If there were contributions, and it was a valid user or IP, show + # the appropriate "footer" message - WHOIS tools, etc. + if( $target != 'newbies' ) { + $message = IP::isIPAddress( $target ) + ? 'sp-contributions-footer-anon' + : 'sp-contributions-footer'; + + + $text = wfMsgNoTrans( $message, $target ); + if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { + $wgOut->addHTML( '<div class="mw-contributions-footer">' ); + $wgOut->addWikiText( $text ); + $wgOut->addHTML( '</div>' ); + } + } + } + + /** + * Generates the subheading with links + * @param $nt @see Title object for the target + */ + function getSubTitle( $nt, $id ) { + global $wgSysopUserBans, $wgLang, $wgUser; + + $sk = $wgUser->getSkin(); + + if ( 0 == $id ) { + $user = $nt->getText(); + } else { + $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) ); + } + $talk = $nt->getTalkPage(); + if( $talk ) { + # Talk page link + $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) ); + if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) { + # Block link + if( $wgUser->isAllowed( 'block' ) ) + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), + wfMsgHtml( 'blocklink' ) ); + # Block log link + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() ); + } + # Other logs link + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() ); + # Link to undeleted contributions + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $nt->getDBkey() ), + wfMsgHtml( 'contributions' ) ); + + wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); + + $links = implode( ' | ', $tools ); + } + + // Old message 'contribsub' had one parameter, but that doesn't work for + // languages that want to put the "for" bit right after $user but before + // $links. If 'contribsub' is around, use it for reverse compatibility, + // otherwise use 'contribsub2'. + if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) { + return wfMsgHtml( 'contribsub2', $user, $links ); + } else { + return wfMsgHtml( 'contribsub', "$user ($links)" ); + } + } + + /** + * Generates the namespace selector form with hidden attributes. + * @param $options Array: the options to be included. + */ + function getForm( $options ) { + global $wgScript, $wgTitle, $wgRequest; + + $options['title'] = $wgTitle->getPrefixedText(); + if ( !isset( $options['target'] ) ) { + $options['target'] = ''; + } else { + $options['target'] = str_replace( '_' , ' ' , $options['target'] ); + } + + if ( !isset( $options['namespace'] ) ) { + $options['namespace'] = ''; + } + + if ( !isset( $options['contribs'] ) ) { + $options['contribs'] = 'user'; + } + + if ( $options['contribs'] == 'newbie' ) { + $options['target'] = ''; + } + + $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + + foreach ( $options as $name => $value ) { + if ( in_array( $name, array( 'namespace', 'target', 'contribs' ) ) ) { + continue; + } + $f .= "\t" . Xml::hidden( $name, $value ) . "\n"; + } + + $f .= Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) . + Xml::tags( 'label', array( 'for' => 'target' ), wfMsgExt( 'sp-contributions-username', 'parseinline' ) ) . ' ' . + Xml::input( 'target', 20, $options['target']) . ' '. + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . + Xml::namespaceSelector( $options['namespace'], '' ) . ' ' . + Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ); + return $f; + } +} diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php index 34045660..0a728b68 100644 --- a/includes/specials/SpecialDisambiguations.php +++ b/includes/specials/SpecialDisambiguations.php @@ -84,13 +84,13 @@ class DisambiguationsPage extends PageQueryPage { function formatResult( $skin, $result ) { global $wgContLang; - $title = Title::newFromId( $result->value ); + $title = Title::newFromID( $result->value ); $dp = Title::makeTitle( $result->namespace, $result->title ); - $from = $skin->makeKnownLinkObj( $title, '' ); - $edit = $skin->makeKnownLinkObj( $title, "(".wfMsgHtml("qbedit").")" , 'redirect=no&action=edit' ); + $from = $skin->link( $title ); + $edit = $skin->link( $title, "(".wfMsgHtml("qbedit").")", array(), array( 'redirect' => 'no', 'action' => 'edit' ) ); $arr = $wgContLang->getArrow(); - $to = $skin->makeKnownLinkObj( $dp, '' ); + $to = $skin->link( $dp ); return "$from $edit $arr $to"; } diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index 3874c6a1..cf90f94d 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -5,17 +5,22 @@ */ /** - * @todo document + * Constructor for Special:Emailuser. */ function wfSpecialEmailuser( $par ) { global $wgRequest, $wgUser, $wgOut; + if ( !EmailUserForm::userEmailEnabled() ) { + $wgOut->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' ); + return; + } + $action = $wgRequest->getVal( 'action' ); $target = isset($par) ? $par : $wgRequest->getVal( 'target' ); $targetUser = EmailUserForm::validateEmailTarget( $target ); if ( !( $targetUser instanceof User ) ) { - $wgOut->showErrorPage( $targetUser[0], $targetUser[1] ); + $wgOut->showErrorPage( $targetUser.'title', $targetUser.'text' ); return; } @@ -30,7 +35,7 @@ function wfSpecialEmailuser( $par ) { $error = EmailUserForm::getPermissionsError( $wgUser, $wgRequest->getVal( 'wpEditToken' ) ); if ( $error ) { - switch ( $error[0] ) { + switch ( $error ) { case 'blockedemailuser': $wgOut->blockedPage(); return; @@ -40,12 +45,11 @@ function wfSpecialEmailuser( $par ) { case 'sessionfailure': $form->showForm(); return; - default: - $wgOut->showErrorPage( $error[0], $error[1] ); + case 'mailnologin': + $wgOut->showErrorPage( 'mailnologin', 'mailnologintext' ); return; } } - if ( "submit" == $action && $wgRequest->wasPosted() ) { $result = $form->doSubmit(); @@ -94,46 +98,64 @@ class EmailUserForm { $this->subject = wfMsgExt( 'defemailsubject', array( 'content', 'parsemag' ) ); } - $emf = wfMsg( "emailfrom" ); - $senderLink = $skin->makeLinkObj( - $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) ); - $emt = wfMsg( "emailto" ); - $recipientLink = $skin->makeLinkObj( - $this->target->getUserPage(), htmlspecialchars( $this->target->getName() ) ); - $emr = wfMsg( "emailsubject" ); - $emm = wfMsg( "emailmessage" ); - $ems = wfMsg( "emailsend" ); - $emc = wfMsg( "emailccme" ); - $encSubject = htmlspecialchars( $this->subject ); - $titleObj = SpecialPage::getTitleFor( "Emailuser" ); - $action = $titleObj->escapeLocalURL( "target=" . + $action = $titleObj->getLocalURL( "target=" . urlencode( $this->target->getName() ) . "&action=submit" ); - $token = htmlspecialchars( $wgUser->editToken() ); - - $wgOut->addHTML( " -<form id=\"emailuser\" method=\"post\" action=\"{$action}\"> -<table border='0' id='mailheader'><tr> -<td align='right'>{$emf}:</td> -<td align='left'><strong>{$senderLink}</strong></td> -</tr><tr> -<td align='right'>{$emt}:</td> -<td align='left'><strong>{$recipientLink}</strong></td> -</tr><tr> -<td align='right'>{$emr}:</td> -<td align='left'> -<input type='text' size='60' maxlength='200' name=\"wpSubject\" value=\"{$encSubject}\" /> -</td> -</tr> -</table> -<span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span> -<textarea id=\"wpText\" name=\"wpText\" rows='20' cols='80' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) . -"</textarea> -" . wfCheckLabel( $emc, 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . "<br /> -<input type='submit' name=\"wpSend\" value=\"{$ems}\" /> -<input type='hidden' name='wpEditToken' value=\"$token\" /> -</form>\n" ); + $wgOut->addHTML( + Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'emailuser' ) ) . + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMsgExt( 'email-legend', 'parsemag' ) ) . + Xml::openElement( 'table', array( 'class' => 'mw-emailuser-table' ) ) . + "<tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'emailfrom' ), 'emailfrom' ) . + "</td> + <td class='mw-input' id='mw-emailuser-sender'>" . + $skin->link( $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) ) . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'emailto' ), 'emailto' ) . + "</td> + <td class='mw-input' id='mw-emailuser-recipient'>" . + $skin->link( $this->target->getUserPage(), htmlspecialchars( $this->target->getName() ) ) . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'emailsubject' ), 'wpSubject' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'wpSubject', 60, $this->subject, array( 'type' => 'text', 'maxlength' => 200 ) ) . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'emailmessage' ), 'wpText' ) . + "</td> + <td class='mw-input'>" . + Xml::textarea( 'wpText', $this->text, 80, 20, array( 'id' => 'wpText' ) ) . + "</td> + </tr> + <tr> + <td></td> + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'emailccme' ), 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . + "</td> + </tr> + <tr> + <td></td> + <td class='mw-submit'>" . + Xml::submitButton( wfMsg( 'emailsend' ), array( 'name' => 'wpSend', 'accesskey' => 's' ) ) . + "</td> + </tr>" . + Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ) + ); } /* @@ -149,7 +171,7 @@ class EmailUserForm { $subject = $this->subject; // Add a standard footer and trim up trailing newlines - $this->text = rtrim($this->text) . "\n\n---\n" . wfMsgExt( 'emailuserfooter', + $this->text = rtrim($this->text) . "\n\n-- \n" . wfMsgExt( 'emailuserfooter', array( 'content', 'parsemag' ), array( $from->name, $to->name ) ); if( wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$this->text ) ) ) { @@ -228,27 +250,33 @@ class EmailUserForm { return $this->target; } - static function validateEmailTarget ( $target ) { + static function userEmailEnabled() { global $wgEnableEmail, $wgEnableUserEmail; - - if( !( $wgEnableEmail && $wgEnableUserEmail ) ) - return array( "nosuchspecialpage", "nospecialpagetext" ); + return $wgEnableEmail && $wgEnableUserEmail; + } + static function validateEmailTarget ( $target ) { if ( "" == $target ) { wfDebug( "Target is empty.\n" ); - return array( "notargettitle", "notargettext" ); + return "notarget"; } $nt = Title::newFromURL( $target ); if ( is_null( $nt ) ) { wfDebug( "Target is invalid title.\n" ); - return array( "notargettitle", "notargettext" ); + return "notarget"; } $nu = User::newFromName( $nt->getText() ); - if( is_null( $nu ) || !$nu->canReceiveEmail() ) { - wfDebug( "Target is invalid user or can't receive.\n" ); - return array( "noemailtitle", "noemailtext" ); + if( is_null( $nu ) || !$nu->getId() ) { + wfDebug( "Target is invalid user.\n" ); + return "notarget"; + } else if ( !$nu->isEmailConfirmed() ) { + wfDebug( "User has no valid email.\n" ); + return "noemail"; + } else if ( !$nu->canReceiveEmail() ) { + wfDebug( "User does not allow user emails.\n" ); + return "nowikiemail"; } return $nu; @@ -256,22 +284,22 @@ class EmailUserForm { static function getPermissionsError ( $user, $editToken ) { if( !$user->canSendEmail() ) { wfDebug( "User can't send.\n" ); - return array( "mailnologin", "mailnologintext" ); + return "mailnologin"; } if( $user->isBlockedFromEmailuser() ) { wfDebug( "User is blocked from sending e-mail.\n" ); - return array( "blockedemailuser", "" ); + return "blockedemailuser"; } if( $user->pingLimiter( 'emailuser' ) ) { wfDebug( "Ping limiter triggered.\n" ); - return array( 'actionthrottledtext', '' ); + return 'actionthrottledtext'; } if( !$user->matchEditToken( $editToken ) ) { wfDebug( "Matching edit token failed.\n" ); - return array( 'sessionfailure', '' ); + return 'sessionfailure'; } return; diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index 38bfc83e..898b5a78 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -1,5 +1,5 @@ <?php -# Copyright (C) 2003 Brion Vibber <brion@pobox.com> +# Copyright (C) 2003-2008 Brion Vibber <brion@pobox.com> # http://www.mediawiki.org/ # # This program is free software; you can redistribute it and/or modify @@ -71,7 +71,7 @@ function wfExportGetTemplates( $inputPages, $pageSet ) { function wfExportGetImages( $inputPages, $pageSet ) { return wfExportGetLinks( $inputPages, $pageSet, 'imagelinks', - array( NS_IMAGE . ' AS namespace', 'il_to AS title' ), + array( NS_FILE . ' AS namespace', 'il_to AS title' ), array( 'page_id=il_from' ) ); } @@ -93,7 +93,7 @@ function wfExportGetLinks( $inputPages, $pageSet, $table, $fields, $join ) { array_merge( $join, array( 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbKey() ) ), + 'page_title' => $title->getDBKey() ) ), __METHOD__ ); foreach( $result as $row ) { $template = Title::makeTitle( $row->namespace, $row->title ); @@ -126,7 +126,7 @@ function wfSpecialExport( $page = '' ) { $catname = $wgRequest->getText( 'catname' ); if ( $catname !== '' && $catname !== NULL && $catname !== false ) { - $t = Title::makeTitleSafe( NS_CATEGORY, $catname ); + $t = Title::makeTitleSafe( NS_MAIN, $catname ); if ( $t ) { /** * @fixme This can lead to hitting memory limit for very large @@ -223,8 +223,23 @@ function wfSpecialExport( $page = '' ) { /* Ok, let's get to it... */ - $db = wfGetDB( DB_SLAVE ); - $exporter = new WikiExporter( $db, $history ); + if( $history == WikiExporter::CURRENT ) { + $lb = false; + $db = wfGetDB( DB_SLAVE ); + $buffer = WikiExporter::BUFFER; + } else { + // Use an unbuffered query; histories may be very long! + $lb = wfGetLBFactory()->newMainLB(); + $db = $lb->getConnection( DB_SLAVE ); + $buffer = WikiExporter::STREAM; + + // This might take a while... :D + wfSuppressWarnings(); + set_time_limit(0); + wfRestoreWarnings(); + } + + $exporter = new WikiExporter( $db, $history, $buffer ); $exporter->list_authors = $list_authors ; $exporter->openStream(); @@ -251,11 +266,14 @@ function wfSpecialExport( $page = '' ) { } $exporter->closeStream(); + if( $lb ) { + $lb->closeAll(); + } return; } $self = SpecialPage::getTitleFor( 'Export' ); - $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) ); + $wgOut->addHTML( wfMsgExt( 'exporttext', 'parse' ) ); $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl( 'action=submit' ) ) ); @@ -271,14 +289,14 @@ function wfSpecialExport( $page = '' ) { if( $wgExportAllowHistory ) { $form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '<br />'; } else { - $wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) ); + $wgOut->addHTML( wfMsgExt( 'exportnohistory', 'parse' ) ); } $form .= Xml::checkLabel( wfMsg( 'export-templates' ), 'templates', 'wpExportTemplates', false ) . '<br />'; // Enable this when we can do something useful exporting/importing image information. :) //$form .= Xml::checkLabel( wfMsg( 'export-images' ), 'images', 'wpExportImages', false ) . '<br />'; $form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '<br />'; - $form .= Xml::submitButton( wfMsg( 'export-submit' ) ); + $form .= Xml::submitButton( wfMsg( 'export-submit' ), array( 'accesskey' => 's' ) ); $form .= Xml::closeElement( 'form' ); - $wgOut->addHtml( $form ); + $wgOut->addHTML( $form ); } diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index 5236ca25..49a218c8 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -49,7 +49,7 @@ class FileDuplicateSearchPage extends QueryPage { function formatResult( $skin, $result ) { global $wgContLang, $wgLang; - $nt = Title::makeTitle( NS_IMAGE, $result->title ); + $nt = Title::makeTitle( NS_FILE, $result->title ); $text = $wgContLang->convert( $nt->getText() ); $plink = $skin->makeLink( $nt->getPrefixedText(), $text ); @@ -73,7 +73,7 @@ function wfSpecialFileDuplicateSearch( $par = null ) { if( $title && $title->getText() != '' ) { $dbr = wfGetDB( DB_SLAVE ); $image = $dbr->tableName( 'image' ); - $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDbKey() ) ); + $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDBKey() ) ); $sql = "SELECT img_sha1 from $image where img_name = $encFilename"; $res = $dbr->query( $sql ); $row = $dbr->fetchRow( $res ); @@ -100,7 +100,7 @@ function wfSpecialFileDuplicateSearch( $par = null ) { # Show a thumbnail of the file $img = wfFindFile( $title ); if ( $img ) { - $thumb = $img->getThumbnail( 120, 120 ); + $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) ); if( $thumb ) { $wgOut->addHTML( '<div style="float:' . $align . '" id="mw-fileduplicatesearch-icon">' . $thumb->toHtml( array( 'desc-link' => false ) ) . '<br />' . diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php index a2ba3e57..4a724b1f 100644 --- a/includes/specials/SpecialFilepath.php +++ b/includes/specials/SpecialFilepath.php @@ -9,9 +9,9 @@ function wfSpecialFilepath( $par ) { $file = isset( $par ) ? $par : $wgRequest->getText( 'file' ); - $title = Title::newFromText( $file, NS_IMAGE ); + $title = Title::makeTitleSafe( NS_FILE, $file ); - if ( ! $title instanceof Title || $title->getNamespace() != NS_IMAGE ) { + if ( ! $title instanceof Title || $title->getNamespace() != NS_FILE ) { $cform = new FilepathForm( $title ); $cform->execute(); } else { diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php index 1623245d..5e1a6533 100644 --- a/includes/specials/SpecialImport.php +++ b/includes/specials/SpecialImport.php @@ -23,28 +23,55 @@ * @ingroup SpecialPage */ -/** - * Constructor - */ -function wfSpecialImport( $page = '' ) { - global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources; - global $wgImportTargetNamespace; - - $interwiki = false; - $namespace = $wgImportTargetNamespace; - $frompage = ''; - $history = true; - - if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; +class SpecialImport extends SpecialPage { + + private $interwiki = false; + private $namespace; + private $frompage = ''; + private $logcomment= false; + private $history = true; + + /** + * Constructor + */ + public function __construct() { + parent::__construct( 'Import', 'import' ); + global $wgImportTargetNamespace; + $this->namespace = $wgImportTargetNamespace; } - - if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') { + + /** + * Execute + */ + function execute( $par ) { + global $wgRequest; + + $this->setHeaders(); + $this->outputHeader(); + + if ( wfReadOnly() ) { + global $wgOut; + $wgOut->readOnlyPage(); + return; + } + + if ( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit' ) { + $this->doImport(); + } + $this->showForm(); + } + + /** + * Do the actual import + */ + private function doImport() { + global $wgOut, $wgRequest, $wgUser, $wgImportSources; $isUpload = false; - $namespace = $wgRequest->getIntOrNull( 'namespace' ); + $this->namespace = $wgRequest->getIntOrNull( 'namespace' ); $sourceName = $wgRequest->getVal( "source" ); + $this->logcomment = $wgRequest->getText( 'log-comment' ); + if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'editToken' ) ) ) { $source = new WikiErrorMsg( 'import-token-mismatch' ); } elseif ( $sourceName == 'upload' ) { @@ -55,16 +82,16 @@ function wfSpecialImport( $page = '' ) { return $wgOut->permissionRequired( 'importupload' ); } } elseif ( $sourceName == "interwiki" ) { - $interwiki = $wgRequest->getVal( 'interwiki' ); - if ( !in_array( $interwiki, $wgImportSources ) ) { + $this->interwiki = $wgRequest->getVal( 'interwiki' ); + if ( !in_array( $this->interwiki, $wgImportSources ) ) { $source = new WikiErrorMsg( "import-invalid-interwiki" ); } else { - $history = $wgRequest->getCheck( 'interwikiHistory' ); - $frompage = $wgRequest->getText( "frompage" ); + $this->history = $wgRequest->getCheck( 'interwikiHistory' ); + $this->frompage = $wgRequest->getText( "frompage" ); $source = ImportStreamSource::newFromInterwiki( - $interwiki, - $frompage, - $history ); + $this->interwiki, + $this->frompage, + $this->history ); } } else { $source = new WikiErrorMsg( "importunknownsource" ); @@ -76,10 +103,10 @@ function wfSpecialImport( $page = '' ) { $wgOut->addWikiMsg( "importstart" ); $importer = new WikiImporter( $source ); - if( !is_null( $namespace ) ) { - $importer->setTargetNamespace( $namespace ); + if( !is_null( $this->namespace ) ) { + $importer->setTargetNamespace( $this->namespace ); } - $reporter = new ImportReporter( $importer, $isUpload, $interwiki ); + $reporter = new ImportReporter( $importer, $isUpload, $this->interwiki , $this->logcomment); $reporter->open(); $result = $importer->doImport(); @@ -99,79 +126,121 @@ function wfSpecialImport( $page = '' ) { } } - $action = $wgTitle->getLocalUrl( 'action=submit' ); - - if( $wgUser->isAllowed( 'importupload' ) ) { - $wgOut->addWikiMsg( "importtext" ); - $wgOut->addHTML( - Xml::openElement( 'fieldset' ). - Xml::element( 'legend', null, wfMsg( 'import-upload' ) ) . - Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) . - Xml::hidden( 'action', 'submit' ) . - Xml::hidden( 'source', 'upload' ) . - Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' . - Xml::hidden( 'editToken', $wgUser->editToken() ) . - Xml::submitButton( wfMsg( 'uploadbtn' ) ) . - Xml::closeElement( 'form' ) . - Xml::closeElement( 'fieldset' ) - ); - } else { - if( empty( $wgImportSources ) ) { - $wgOut->addWikiMsg( 'importnosources' ); + private function showForm() { + global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources; + # FIXME: Quick hack to disable import for non privileged users /Raymond + # Regression from 43963 + if( !$wgUser->isAllowed( 'import' ) && !$wgUser->isAllowed( 'importupload' ) ) + return $wgOut->permissionRequired( 'import' ); + + $action = $wgTitle->getLocalUrl( 'action=submit' ); + + if( $wgUser->isAllowed( 'importupload' ) ) { + $wgOut->addWikiMsg( "importtext" ); + $wgOut->addHTML( + Xml::openElement( 'fieldset' ). + Xml::element( 'legend', null, wfMsg( 'import-upload' ) ) . + Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) . + Xml::hidden( 'action', 'submit' ) . + Xml::hidden( 'source', 'upload' ) . + Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) . + + "<tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'import-upload-filename' ), 'xmlimport' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'import-comment' ), 'mw-import-comment' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'log-comment', 50, '', + array( 'id' => 'mw-import-comment', 'type' => 'text' ) ) . ' ' . + "</td> + </tr> + <tr> + <td></td> + <td class='mw-input'>" . + Xml::submitButton( wfMsg( 'uploadbtn' ) ) . + "</td> + </tr>" . + Xml::closeElement( 'table' ). + Xml::hidden( 'editToken', $wgUser->editToken() ) . + Xml::closeElement( 'form' ) . + Xml::closeElement( 'fieldset' ) + ); + } else { + if( empty( $wgImportSources ) ) { + $wgOut->addWikiMsg( 'importnosources' ); + } } - } - if( !empty( $wgImportSources ) ) { - $wgOut->addHTML( - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'importinterwiki' ) ) . - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action ) ) . - wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) . - Xml::hidden( 'action', 'submit' ) . - Xml::hidden( 'source', 'interwiki' ) . - Xml::hidden( 'editToken', $wgUser->editToken() ) . - Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) . - "<tr> - <td>" . - Xml::openElement( 'select', array( 'name' => 'interwiki' ) ) - ); - foreach( $wgImportSources as $prefix ) { - $selected = ( $interwiki === $prefix ) ? ' selected="selected"' : ''; - $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) ); + if( $wgUser->isAllowed( 'import' ) && !empty( $wgImportSources ) ) { + $wgOut->addHTML( + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMsg( 'importinterwiki' ) ) . + Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action ) ) . + wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) . + Xml::hidden( 'action', 'submit' ) . + Xml::hidden( 'source', 'interwiki' ) . + Xml::hidden( 'editToken', $wgUser->editToken() ) . + Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) . + "<tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'import-interwiki-source' ), 'interwiki' ) . + "</td> + <td class='mw-input'>" . + Xml::openElement( 'select', array( 'name' => 'interwiki' ) ) + ); + foreach( $wgImportSources as $prefix ) { + $selected = ( $this->interwiki === $prefix ) ? ' selected="selected"' : ''; + $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) ); + } + $wgOut->addHTML( + Xml::closeElement( 'select' ) . + Xml::input( 'frompage', 50, $this->frompage ) . + "</td> + </tr> + <tr> + <td> + </td> + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $this->history ) . + "</td> + </tr> + <tr> + <td>" . + Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) . + "</td> + <td class='mw-input'>" . + Xml::namespaceSelector( $this->namespace, '' ) . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'import-comment' ), 'mw-interwiki-comment' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'log-comment', 50, '', + array( 'id' => 'mw-interwiki-comment', 'type' => 'text' ) ) . ' ' . + "</td> + </tr> + <tr> + <td> + </td> + <td class='mw-input'>" . + Xml::submitButton( wfMsg( 'import-interwiki-submit' ), array( 'accesskey' => 's' ) ) . + "</td> + </tr>" . + Xml::closeElement( 'table' ). + Xml::closeElement( 'form' ) . + Xml::closeElement( 'fieldset' ) + ); } - $wgOut->addHTML( - Xml::closeElement( 'select' ) . - "</td> - <td>" . - Xml::input( 'frompage', 50, $frompage ) . - "</td> - </tr> - <tr> - <td> - </td> - <td>" . - Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $history ) . - "</td> - </tr> - <tr> - <td> - </td> - <td>" . - Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) . - Xml::namespaceSelector( $namespace, '' ) . - "</td> - </tr> - <tr> - <td> - </td> - <td>" . - Xml::submitButton( wfMsg( 'import-interwiki-submit' ) ) . - "</td> - </tr>" . - Xml::closeElement( 'table' ). - Xml::closeElement( 'form' ) . - Xml::closeElement( 'fieldset' ) - ); } } @@ -180,16 +249,19 @@ function wfSpecialImport( $page = '' ) { * @ingroup SpecialPage */ class ImportReporter { - function __construct( $importer, $upload, $interwiki ) { + private $reason=false; + + function __construct( $importer, $upload, $interwiki , $reason=false ) { $importer->setPageOutCallback( array( $this, 'reportPage' ) ); $this->mPageCount = 0; $this->mIsUpload = $upload; $this->mInterwiki = $interwiki; + $this->reason = $reason; } function open() { global $wgOut; - $wgOut->addHtml( "<ul>\n" ); + $wgOut->addHTML( "<ul>\n" ); } function reportPage( $title, $origTitle, $revisionCount, $successCount ) { @@ -203,7 +275,7 @@ class ImportReporter { $contentCount = $wgContLang->formatNum( $successCount ); if( $successCount > 0 ) { - $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . " " . + $wgOut->addHTML( "<li>" . $skin->makeKnownLinkObj( $title ) . " " . wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) . "</li>\n" ); @@ -212,949 +284,43 @@ class ImportReporter { if( $this->mIsUpload ) { $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ), $contentCount ); + if ( $this->reason ) { + $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason; + } $log->addEntry( 'upload', $title, $detail ); } else { $interwiki = '[[:' . $this->mInterwiki . ':' . $origTitle->getPrefixedText() . ']]'; $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ), $contentCount, $interwiki ); + if ( $this->reason ) { + $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason; + } $log->addEntry( 'interwiki', $title, $detail ); } $comment = $detail; // quick $dbw = wfGetDB( DB_MASTER ); + $latest = $title->getLatestRevID(); $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true ); $nullRevision->insertOn( $dbw ); $article = new Article( $title ); # Update page record $article->updateRevisionOn( $dbw, $nullRevision ); - wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) ); + wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) ); } else { - $wgOut->addHtml( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' ); + $wgOut->addHTML( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' ); } } function close() { global $wgOut; if( $this->mPageCount == 0 ) { - $wgOut->addHtml( "</ul>\n" ); + $wgOut->addHTML( "</ul>\n" ); return new WikiErrorMsg( "importnopages" ); } - $wgOut->addHtml( "</ul>\n" ); + $wgOut->addHTML( "</ul>\n" ); return $this->mPageCount; } } - -/** - * - * @ingroup SpecialPage - */ -class WikiRevision { - var $title = null; - var $id = 0; - var $timestamp = "20010115000000"; - var $user = 0; - var $user_text = ""; - var $text = ""; - var $comment = ""; - var $minor = false; - - function setTitle( $title ) { - if( is_object( $title ) ) { - $this->title = $title; - } elseif( is_null( $title ) ) { - throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." ); - } else { - throw new MWException( "WikiRevision given non-object title in import." ); - } - } - - function setID( $id ) { - $this->id = $id; - } - - function setTimestamp( $ts ) { - # 2003-08-05T18:30:02Z - $this->timestamp = wfTimestamp( TS_MW, $ts ); - } - - function setUsername( $user ) { - $this->user_text = $user; - } - - function setUserIP( $ip ) { - $this->user_text = $ip; - } - - function setText( $text ) { - $this->text = $text; - } - - function setComment( $text ) { - $this->comment = $text; - } - - function setMinor( $minor ) { - $this->minor = (bool)$minor; - } - - function setSrc( $src ) { - $this->src = $src; - } - - function setFilename( $filename ) { - $this->filename = $filename; - } - - function setSize( $size ) { - $this->size = intval( $size ); - } - - function getTitle() { - return $this->title; - } - - function getID() { - return $this->id; - } - - function getTimestamp() { - return $this->timestamp; - } - - function getUser() { - return $this->user_text; - } - - function getText() { - return $this->text; - } - - function getComment() { - return $this->comment; - } - - function getMinor() { - return $this->minor; - } - - function getSrc() { - return $this->src; - } - - function getFilename() { - return $this->filename; - } - - function getSize() { - return $this->size; - } - - function importOldRevision() { - $dbw = wfGetDB( DB_MASTER ); - - # Sneak a single revision into place - $user = User::newFromName( $this->getUser() ); - if( $user ) { - $userId = intval( $user->getId() ); - $userText = $user->getName(); - } else { - $userId = 0; - $userText = $this->getUser(); - } - - // avoid memory leak...? - $linkCache = LinkCache::singleton(); - $linkCache->clear(); - - $article = new Article( $this->title ); - $pageId = $article->getId(); - if( $pageId == 0 ) { - # must create the page... - $pageId = $article->insertOn( $dbw ); - $created = true; - } else { - $created = false; - - $prior = Revision::loadFromTimestamp( $dbw, $this->title, $this->timestamp ); - if( !is_null( $prior ) ) { - // FIXME: this could fail slightly for multiple matches :P - wfDebug( __METHOD__ . ": skipping existing revision for [[" . - $this->title->getPrefixedText() . "]], timestamp " . - $this->timestamp . "\n" ); - return false; - } - } - - # FIXME: Use original rev_id optionally - # FIXME: blah blah blah - - #if( $numrows > 0 ) { - # return wfMsg( "importhistoryconflict" ); - #} - - # Insert the row - $revision = new Revision( array( - 'page' => $pageId, - 'text' => $this->getText(), - 'comment' => $this->getComment(), - 'user' => $userId, - 'user_text' => $userText, - 'timestamp' => $this->timestamp, - 'minor_edit' => $this->minor, - ) ); - $revId = $revision->insertOn( $dbw ); - $changed = $article->updateIfNewerOn( $dbw, $revision ); - - if( $created ) { - wfDebug( __METHOD__ . ": running onArticleCreate\n" ); - Article::onArticleCreate( $this->title ); - - wfDebug( __METHOD__ . ": running create updates\n" ); - $article->createUpdates( $revision ); - - } elseif( $changed ) { - wfDebug( __METHOD__ . ": running onArticleEdit\n" ); - Article::onArticleEdit( $this->title ); - - wfDebug( __METHOD__ . ": running edit updates\n" ); - $article->editUpdates( - $this->getText(), - $this->getComment(), - $this->minor, - $this->timestamp, - $revId ); - } - - return true; - } - - function importUpload() { - wfDebug( __METHOD__ . ": STUB\n" ); - - /** - // from file revert... - $source = $this->file->getArchiveVirtualUrl( $this->oldimage ); - $comment = $wgRequest->getText( 'wpComment' ); - // TODO: Preserve file properties from database instead of reloading from file - $status = $this->file->upload( $source, $comment, $comment ); - if( $status->isGood() ) { - */ - - /** - // from file upload... - $this->mLocalFile = wfLocalFile( $nt ); - $this->mDestName = $this->mLocalFile->getName(); - //.... - $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, - File::DELETE_SOURCE, $this->mFileProps ); - if ( !$status->isGood() ) { - $resultDetails = array( 'internal' => $status->getWikiText() ); - */ - - // @fixme upload() uses $wgUser, which is wrong here - // it may also create a page without our desire, also wrong potentially. - // and, it will record a *current* upload, but we might want an archive version here - - $file = wfLocalFile( $this->getTitle() ); - if( !$file ) { - var_dump( $file ); - wfDebug( "IMPORT: Bad file. :(\n" ); - return false; - } - - $source = $this->downloadSource(); - if( !$source ) { - wfDebug( "IMPORT: Could not fetch remote file. :(\n" ); - return false; - } - - $status = $file->upload( $source, - $this->getComment(), - $this->getComment(), // Initial page, if none present... - File::DELETE_SOURCE, - false, // props... - $this->getTimestamp() ); - - if( $status->isGood() ) { - // yay? - wfDebug( "IMPORT: is ok?\n" ); - return true; - } - - wfDebug( "IMPORT: is bad? " . $status->getXml() . "\n" ); - return false; - - } - - function downloadSource() { - global $wgEnableUploads; - if( !$wgEnableUploads ) { - return false; - } - - $tempo = tempnam( wfTempDir(), 'download' ); - $f = fopen( $tempo, 'wb' ); - if( !$f ) { - wfDebug( "IMPORT: couldn't write to temp file $tempo\n" ); - return false; - } - - // @fixme! - $src = $this->getSrc(); - $data = Http::get( $src ); - if( !$data ) { - wfDebug( "IMPORT: couldn't fetch source $src\n" ); - fclose( $f ); - unlink( $tempo ); - return false; - } - - fwrite( $f, $data ); - fclose( $f ); - - return $tempo; - } - -} - -/** - * implements Special:Import - * @ingroup SpecialPage - */ -class WikiImporter { - var $mDebug = false; - var $mSource = null; - var $mPageCallback = null; - var $mPageOutCallback = null; - var $mRevisionCallback = null; - var $mUploadCallback = null; - var $mTargetNamespace = null; - var $lastfield; - var $tagStack = array(); - - function __construct( $source ) { - $this->setRevisionCallback( array( $this, "importRevision" ) ); - $this->setUploadCallback( array( $this, "importUpload" ) ); - $this->mSource = $source; - } - - function throwXmlError( $err ) { - $this->debug( "FAILURE: $err" ); - wfDebug( "WikiImporter XML error: $err\n" ); - } - - # -------------- - - function doImport() { - if( empty( $this->mSource ) ) { - return new WikiErrorMsg( "importnotext" ); - } - - $parser = xml_parser_create( "UTF-8" ); - - # case folding violates XML standard, turn it off - xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); - - xml_set_object( $parser, $this ); - xml_set_element_handler( $parser, "in_start", "" ); - - $offset = 0; // for context extraction on error reporting - do { - $chunk = $this->mSource->readChunk(); - if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) { - wfDebug( "WikiImporter::doImport encountered XML parsing error\n" ); - return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset ); - } - $offset += strlen( $chunk ); - } while( $chunk !== false && !$this->mSource->atEnd() ); - xml_parser_free( $parser ); - - return true; - } - - function debug( $data ) { - if( $this->mDebug ) { - wfDebug( "IMPORT: $data\n" ); - } - } - - function notice( $data ) { - global $wgCommandLineMode; - if( $wgCommandLineMode ) { - print "$data\n"; - } else { - global $wgOut; - $wgOut->addHTML( "<li>" . htmlspecialchars( $data ) . "</li>\n" ); - } - } - - /** - * Set debug mode... - */ - function setDebug( $debug ) { - $this->mDebug = $debug; - } - - /** - * Sets the action to perform as each new page in the stream is reached. - * @param $callback callback - * @return callback - */ - function setPageCallback( $callback ) { - $previous = $this->mPageCallback; - $this->mPageCallback = $callback; - return $previous; - } - - /** - * Sets the action to perform as each page in the stream is completed. - * Callback accepts the page title (as a Title object), a second object - * with the original title form (in case it's been overridden into a - * local namespace), and a count of revisions. - * - * @param $callback callback - * @return callback - */ - function setPageOutCallback( $callback ) { - $previous = $this->mPageOutCallback; - $this->mPageOutCallback = $callback; - return $previous; - } - - /** - * Sets the action to perform as each page revision is reached. - * @param $callback callback - * @return callback - */ - function setRevisionCallback( $callback ) { - $previous = $this->mRevisionCallback; - $this->mRevisionCallback = $callback; - return $previous; - } - - /** - * Sets the action to perform as each file upload version is reached. - * @param $callback callback - * @return callback - */ - function setUploadCallback( $callback ) { - $previous = $this->mUploadCallback; - $this->mUploadCallback = $callback; - return $previous; - } - - /** - * Set a target namespace to override the defaults - */ - function setTargetNamespace( $namespace ) { - if( is_null( $namespace ) ) { - // Don't override namespaces - $this->mTargetNamespace = null; - } elseif( $namespace >= 0 ) { - // FIXME: Check for validity - $this->mTargetNamespace = intval( $namespace ); - } else { - return false; - } - } - - /** - * Default per-revision callback, performs the import. - * @param $revision WikiRevision - * @private - */ - function importRevision( $revision ) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) ); - } - - /** - * Dummy for now... - */ - function importUpload( $revision ) { - //$dbw = wfGetDB( DB_MASTER ); - //return $dbw->deadlockLoop( array( $revision, 'importUpload' ) ); - return false; - } - - /** - * Alternate per-revision callback, for debugging. - * @param $revision WikiRevision - * @private - */ - function debugRevisionHandler( &$revision ) { - $this->debug( "Got revision:" ); - if( is_object( $revision->title ) ) { - $this->debug( "-- Title: " . $revision->title->getPrefixedText() ); - } else { - $this->debug( "-- Title: <invalid>" ); - } - $this->debug( "-- User: " . $revision->user_text ); - $this->debug( "-- Timestamp: " . $revision->timestamp ); - $this->debug( "-- Comment: " . $revision->comment ); - $this->debug( "-- Text: " . $revision->text ); - } - - /** - * Notify the callback function when a new <page> is reached. - * @param $title Title - * @private - */ - function pageCallback( $title ) { - if( is_callable( $this->mPageCallback ) ) { - call_user_func( $this->mPageCallback, $title ); - } - } - - /** - * Notify the callback function when a </page> is closed. - * @param $title Title - * @param $origTitle Title - * @param $revisionCount int - * @param $successCount Int: number of revisions for which callback returned true - * @private - */ - function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) { - if( is_callable( $this->mPageOutCallback ) ) { - call_user_func( $this->mPageOutCallback, $title, $origTitle, - $revisionCount, $successCount ); - } - } - - - # XML parser callbacks from here out -- beware! - function donothing( $parser, $x, $y="" ) { - #$this->debug( "donothing" ); - } - - function in_start( $parser, $name, $attribs ) { - $this->debug( "in_start $name" ); - if( $name != "mediawiki" ) { - return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" ); - } - xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); - } - - function in_mediawiki( $parser, $name, $attribs ) { - $this->debug( "in_mediawiki $name" ); - if( $name == 'siteinfo' ) { - xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" ); - } elseif( $name == 'page' ) { - $this->push( $name ); - $this->workRevisionCount = 0; - $this->workSuccessCount = 0; - $this->uploadCount = 0; - $this->uploadSuccessCount = 0; - xml_set_element_handler( $parser, "in_page", "out_page" ); - } else { - return $this->throwXMLerror( "Expected <page>, got <$name>" ); - } - } - function out_mediawiki( $parser, $name ) { - $this->debug( "out_mediawiki $name" ); - if( $name != "mediawiki" ) { - return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" ); - } - xml_set_element_handler( $parser, "donothing", "donothing" ); - } - - - function in_siteinfo( $parser, $name, $attribs ) { - // no-ops for now - $this->debug( "in_siteinfo $name" ); - switch( $name ) { - case "sitename": - case "base": - case "generator": - case "case": - case "namespaces": - case "namespace": - break; - default: - return $this->throwXMLerror( "Element <$name> not allowed in <siteinfo>." ); - } - } - - function out_siteinfo( $parser, $name ) { - if( $name == "siteinfo" ) { - xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); - } - } - - - function in_page( $parser, $name, $attribs ) { - $this->debug( "in_page $name" ); - switch( $name ) { - case "id": - case "title": - case "restrictions": - $this->appendfield = $name; - $this->appenddata = ""; - xml_set_element_handler( $parser, "in_nothing", "out_append" ); - xml_set_character_data_handler( $parser, "char_append" ); - break; - case "revision": - $this->push( "revision" ); - if( is_object( $this->pageTitle ) ) { - $this->workRevision = new WikiRevision; - $this->workRevision->setTitle( $this->pageTitle ); - $this->workRevisionCount++; - } else { - // Skipping items due to invalid page title - $this->workRevision = null; - } - xml_set_element_handler( $parser, "in_revision", "out_revision" ); - break; - case "upload": - $this->push( "upload" ); - if( is_object( $this->pageTitle ) ) { - $this->workRevision = new WikiRevision; - $this->workRevision->setTitle( $this->pageTitle ); - $this->uploadCount++; - } else { - // Skipping items due to invalid page title - $this->workRevision = null; - } - xml_set_element_handler( $parser, "in_upload", "out_upload" ); - break; - default: - return $this->throwXMLerror( "Element <$name> not allowed in a <page>." ); - } - } - - function out_page( $parser, $name ) { - $this->debug( "out_page $name" ); - $this->pop(); - if( $name != "page" ) { - return $this->throwXMLerror( "Expected </page>, got </$name>" ); - } - xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); - - $this->pageOutCallback( $this->pageTitle, $this->origTitle, - $this->workRevisionCount, $this->workSuccessCount ); - - $this->workTitle = null; - $this->workRevision = null; - $this->workRevisionCount = 0; - $this->workSuccessCount = 0; - $this->pageTitle = null; - $this->origTitle = null; - } - - function in_nothing( $parser, $name, $attribs ) { - $this->debug( "in_nothing $name" ); - return $this->throwXMLerror( "No child elements allowed here; got <$name>" ); - } - function char_append( $parser, $data ) { - $this->debug( "char_append '$data'" ); - $this->appenddata .= $data; - } - function out_append( $parser, $name ) { - $this->debug( "out_append $name" ); - if( $name != $this->appendfield ) { - return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" ); - } - - switch( $this->appendfield ) { - case "title": - $this->workTitle = $this->appenddata; - $this->origTitle = Title::newFromText( $this->workTitle ); - if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) { - $this->pageTitle = Title::makeTitle( $this->mTargetNamespace, - $this->origTitle->getDBkey() ); - } else { - $this->pageTitle = Title::newFromText( $this->workTitle ); - } - if( is_null( $this->pageTitle ) ) { - // Invalid page title? Ignore the page - $this->notice( "Skipping invalid page title '$this->workTitle'" ); - } else { - $this->pageCallback( $this->workTitle ); - } - break; - case "id": - if ( $this->parentTag() == 'revision' ) { - if( $this->workRevision ) - $this->workRevision->setID( $this->appenddata ); - } - break; - case "text": - if( $this->workRevision ) - $this->workRevision->setText( $this->appenddata ); - break; - case "username": - if( $this->workRevision ) - $this->workRevision->setUsername( $this->appenddata ); - break; - case "ip": - if( $this->workRevision ) - $this->workRevision->setUserIP( $this->appenddata ); - break; - case "timestamp": - if( $this->workRevision ) - $this->workRevision->setTimestamp( $this->appenddata ); - break; - case "comment": - if( $this->workRevision ) - $this->workRevision->setComment( $this->appenddata ); - break; - case "minor": - if( $this->workRevision ) - $this->workRevision->setMinor( true ); - break; - case "filename": - if( $this->workRevision ) - $this->workRevision->setFilename( $this->appenddata ); - break; - case "src": - if( $this->workRevision ) - $this->workRevision->setSrc( $this->appenddata ); - break; - case "size": - if( $this->workRevision ) - $this->workRevision->setSize( intval( $this->appenddata ) ); - break; - default: - $this->debug( "Bad append: {$this->appendfield}" ); - } - $this->appendfield = ""; - $this->appenddata = ""; - - $parent = $this->parentTag(); - xml_set_element_handler( $parser, "in_$parent", "out_$parent" ); - xml_set_character_data_handler( $parser, "donothing" ); - } - - function in_revision( $parser, $name, $attribs ) { - $this->debug( "in_revision $name" ); - switch( $name ) { - case "id": - case "timestamp": - case "comment": - case "minor": - case "text": - $this->appendfield = $name; - xml_set_element_handler( $parser, "in_nothing", "out_append" ); - xml_set_character_data_handler( $parser, "char_append" ); - break; - case "contributor": - $this->push( "contributor" ); - xml_set_element_handler( $parser, "in_contributor", "out_contributor" ); - break; - default: - return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." ); - } - } - - function out_revision( $parser, $name ) { - $this->debug( "out_revision $name" ); - $this->pop(); - if( $name != "revision" ) { - return $this->throwXMLerror( "Expected </revision>, got </$name>" ); - } - xml_set_element_handler( $parser, "in_page", "out_page" ); - - if( $this->workRevision ) { - $ok = call_user_func_array( $this->mRevisionCallback, - array( $this->workRevision, $this ) ); - if( $ok ) { - $this->workSuccessCount++; - } - } - } - - function in_upload( $parser, $name, $attribs ) { - $this->debug( "in_upload $name" ); - switch( $name ) { - case "timestamp": - case "comment": - case "text": - case "filename": - case "src": - case "size": - $this->appendfield = $name; - xml_set_element_handler( $parser, "in_nothing", "out_append" ); - xml_set_character_data_handler( $parser, "char_append" ); - break; - case "contributor": - $this->push( "contributor" ); - xml_set_element_handler( $parser, "in_contributor", "out_contributor" ); - break; - default: - return $this->throwXMLerror( "Element <$name> not allowed in an <upload>." ); - } - } - - function out_upload( $parser, $name ) { - $this->debug( "out_revision $name" ); - $this->pop(); - if( $name != "upload" ) { - return $this->throwXMLerror( "Expected </upload>, got </$name>" ); - } - xml_set_element_handler( $parser, "in_page", "out_page" ); - - if( $this->workRevision ) { - $ok = call_user_func_array( $this->mUploadCallback, - array( $this->workRevision, $this ) ); - if( $ok ) { - $this->workUploadSuccessCount++; - } - } - } - - function in_contributor( $parser, $name, $attribs ) { - $this->debug( "in_contributor $name" ); - switch( $name ) { - case "username": - case "ip": - case "id": - $this->appendfield = $name; - xml_set_element_handler( $parser, "in_nothing", "out_append" ); - xml_set_character_data_handler( $parser, "char_append" ); - break; - default: - $this->throwXMLerror( "Invalid tag <$name> in <contributor>" ); - } - } - - function out_contributor( $parser, $name ) { - $this->debug( "out_contributor $name" ); - $this->pop(); - if( $name != "contributor" ) { - return $this->throwXMLerror( "Expected </contributor>, got </$name>" ); - } - $parent = $this->parentTag(); - xml_set_element_handler( $parser, "in_$parent", "out_$parent" ); - } - - private function push( $name ) { - array_push( $this->tagStack, $name ); - $this->debug( "PUSH $name" ); - } - - private function pop() { - $name = array_pop( $this->tagStack ); - $this->debug( "POP $name" ); - return $name; - } - - private function parentTag() { - $name = $this->tagStack[count( $this->tagStack ) - 1]; - $this->debug( "PARENT $name" ); - return $name; - } - -} - -/** - * @todo document (e.g. one-sentence class description). - * @ingroup SpecialPage - */ -class ImportStringSource { - function __construct( $string ) { - $this->mString = $string; - $this->mRead = false; - } - - function atEnd() { - return $this->mRead; - } - - function readChunk() { - if( $this->atEnd() ) { - return false; - } else { - $this->mRead = true; - return $this->mString; - } - } -} - -/** - * @todo document (e.g. one-sentence class description). - * @ingroup SpecialPage - */ -class ImportStreamSource { - function __construct( $handle ) { - $this->mHandle = $handle; - } - - function atEnd() { - return feof( $this->mHandle ); - } - - function readChunk() { - return fread( $this->mHandle, 32768 ); - } - - static function newFromFile( $filename ) { - $file = @fopen( $filename, 'rt' ); - if( !$file ) { - return new WikiErrorMsg( "importcantopen" ); - } - return new ImportStreamSource( $file ); - } - - static function newFromUpload( $fieldname = "xmlimport" ) { - $upload =& $_FILES[$fieldname]; - - if( !isset( $upload ) || !$upload['name'] ) { - return new WikiErrorMsg( 'importnofile' ); - } - if( !empty( $upload['error'] ) ) { - switch($upload['error']){ - case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini. - return new WikiErrorMsg( 'importuploaderrorsize' ); - case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form. - return new WikiErrorMsg( 'importuploaderrorsize' ); - case 3: # The uploaded file was only partially uploaded - return new WikiErrorMsg( 'importuploaderrorpartial' ); - case 6: #Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3. - return new WikiErrorMsg( 'importuploaderrortemp' ); - # case else: # Currently impossible - } - - } - $fname = $upload['tmp_name']; - if( is_uploaded_file( $fname ) ) { - return ImportStreamSource::newFromFile( $fname ); - } else { - return new WikiErrorMsg( 'importnofile' ); - } - } - - static function newFromURL( $url, $method = 'GET' ) { - wfDebug( __METHOD__ . ": opening $url\n" ); - # Use the standard HTTP fetch function; it times out - # quicker and sorts out user-agent problems which might - # otherwise prevent importing from large sites, such - # as the Wikimedia cluster, etc. - $data = Http::request( $method, $url ); - if( $data !== false ) { - $file = tmpfile(); - fwrite( $file, $data ); - fflush( $file ); - fseek( $file, 0 ); - return new ImportStreamSource( $file ); - } else { - return new WikiErrorMsg( 'importcantopen' ); - } - } - - public static function newFromInterwiki( $interwiki, $page, $history=false ) { - if( $page == '' ) { - return new WikiErrorMsg( 'import-noarticle' ); - } - $link = Title::newFromText( "$interwiki:Special:Export/$page" ); - if( is_null( $link ) || $link->getInterwiki() == '' ) { - return new WikiErrorMsg( 'importbadinterwiki' ); - } else { - $params = $history ? 'history=1' : ''; - $url = $link->getFullUrl( $params ); - # For interwikis, use POST to avoid redirects. - return ImportStreamSource::newFromURL( $url, "POST" ); - } - } -} diff --git a/includes/specials/SpecialIpblocklist.php b/includes/specials/SpecialIpblocklist.php index 696c7efe..8d573547 100644 --- a/includes/specials/SpecialIpblocklist.php +++ b/includes/specials/SpecialIpblocklist.php @@ -10,7 +10,7 @@ function wfSpecialIpblocklist() { global $wgUser, $wgOut, $wgRequest; - $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) ); + $ip = trim( $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) ) ); $id = $wgRequest->getVal( 'id' ); $reason = $wgRequest->getText( 'wpUnblockReason' ); $action = $wgRequest->getText( 'action' ); @@ -71,9 +71,13 @@ class IPUnblockForm { var $ip, $reason, $id; function IPUnblockForm( $ip, $id, $reason ) { + global $wgRequest; $this->ip = strtr( $ip, '_', ' ' ); $this->id = $id; $this->reason = $reason; + $this->hideuserblocks = $wgRequest->getBool( 'hideuserblocks' ); + $this->hidetempblocks = $wgRequest->getBool( 'hidetempblocks' ); + $this->hideaddressblocks = $wgRequest->getBool( 'hideaddressblocks' ); } /** @@ -158,8 +162,7 @@ class IPUnblockForm { * @return array array(message key, parameters) on failure, empty array on success */ - static function doUnblock(&$id, &$ip, &$reason, &$range = null) - { + static function doUnblock(&$id, &$ip, &$reason, &$range = null) { if ( $id ) { $block = Block::newFromID( $id ); if ( !$block ) { @@ -241,10 +244,27 @@ class IPUnblockForm { // No extra conditions } elseif ( substr( $this->ip, 0, 1 ) == '#' ) { $conds['ipb_id'] = substr( $this->ip, 1 ); - } elseif ( IP::toUnsigned( $this->ip ) !== false ) { - $conds['ipb_address'] = $this->ip; + // Single IPs + } elseif ( IP::isIPAddress($this->ip) && strpos($this->ip,'/') === false ) { + if( $iaddr = IP::toHex($this->ip) ) { + # Only scan ranges which start in this /16, this improves search speed + # Blocks should not cross a /16 boundary. + $range = substr( $iaddr, 0, 4 ); + // Fixme -- encapsulate this sort of query-building. + $dbr = wfGetDB( DB_SLAVE ); + $encIp = $dbr->addQuotes( IP::sanitizeIP($this->ip) ); + $encRange = $dbr->addQuotes( "$range%" ); + $encAddr = $dbr->addQuotes( $iaddr ); + $conds[] = "(ipb_address = $encIp) OR + (ipb_range_start LIKE $encRange AND + ipb_range_start <= $encAddr + AND ipb_range_end >= $encAddr)"; + } else { + $conds['ipb_address'] = IP::sanitizeIP($this->ip); + } $conds['ipb_auto'] = 0; - } elseif( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/', $this->ip, $matches ) ) { + // IP range + } elseif ( IP::isIPAddress($this->ip) ) { $conds['ipb_address'] = Block::normaliseRange( $this->ip ); $conds['ipb_auto'] = 0; } else { @@ -257,6 +277,16 @@ class IPUnblockForm { $conds['ipb_auto'] = 0; } } + // Apply filters + if( $this->hideuserblocks ) { + $conds['ipb_user'] = 0; + } + if( $this->hidetempblocks ) { + $conds['ipb_expiry'] = 'infinity'; + } + if( $this->hideaddressblocks ) { + $conds[] = "ipb_user != 0 OR ipb_range_end > ipb_range_start"; + } $pager = new IPBlocklistPager( $this, $conds ); if ( $pager->getNumRows() ) { @@ -270,12 +300,38 @@ class IPUnblockForm { $wgOut->addHTML( $this->searchForm() ); $wgOut->addWikiMsg( 'ipblocklist-no-results' ); } else { + $wgOut->addHTML( $this->searchForm() ); $wgOut->addWikiMsg( 'ipblocklist-empty' ); } } function searchForm() { global $wgTitle, $wgScript, $wgRequest; + + $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ) ); + $nondefaults = array(); + if( $this->hideuserblocks ) { + $nondefaults['hideuserblocks'] = $this->hideuserblocks; + } + if( $this->hidetempblocks ) { + $nondefaults['hidetempblocks'] = $this->hidetempblocks; + } + if( $this->hideaddressblocks ) { + $nondefaults['hideaddressblocks'] = $this->hideaddressblocks; + } + $ubLink = $this->makeOptionsLink( $showhide[1-$this->hideuserblocks], + array( 'hideuserblocks' => 1-$this->hideuserblocks ), $nondefaults); + $tbLink = $this->makeOptionsLink( $showhide[1-$this->hidetempblocks], + array( 'hidetempblocks' => 1-$this->hidetempblocks ), $nondefaults); + $sipbLink = $this->makeOptionsLink( $showhide[1-$this->hideaddressblocks], + array( 'hideaddressblocks' => 1-$this->hideaddressblocks ), $nondefaults); + + $links = array(); + $links[] = wfMsgHtml( 'ipblocklist-sh-userblocks', $ubLink ); + $links[] = wfMsgHtml( 'ipblocklist-sh-tempblocks', $tbLink ); + $links[] = wfMsgHtml( 'ipblocklist-sh-addressblocks', $sipbLink ); + $hl = implode( ' ' . wfMsg( 'pipe-separator' ) . ' ', $links ); + return Xml::tags( 'form', array( 'action' => $wgScript ), Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) . @@ -283,16 +339,32 @@ class IPUnblockForm { Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) . Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) . ' ' . - Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) . + Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) . '<br />' . + $hl . Xml::closeElement( 'fieldset' ) ); } /** + * Makes change an option link which carries all the other options + * @param $title see Title + * @param $override + * @param $options + */ + function makeOptionsLink( $title, $override, $options, $active = false ) { + global $wgUser; + $sk = $wgUser->getSkin(); + $params = $override + $options; + $ipblocklist = SpecialPage::getTitleFor( 'IPBlockList' ); + return $sk->link( $ipblocklist, htmlspecialchars( $title ), + ( $active ? array( 'style'=>'font-weight: bold;' ) : array() ), $params, array( 'known' ) ); + } + + /** * Callback function to output a block */ function formatRow( $block ) { - global $wgUser, $wgLang; + global $wgUser, $wgLang, $wgBlockAllowsUTEdit; wfProfileIn( __METHOD__ ); @@ -302,8 +374,8 @@ class IPUnblockForm { $sk = $wgUser->getSkin(); if( is_null( $msg ) ) { $msg = array(); - $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink', - 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' ); + $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink', 'change-blocklink', + 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock', 'blocklist-nousertalk' ); foreach( $keys as $key ) { $msg[$key] = wfMsgHtml( $key ); } @@ -341,15 +413,33 @@ class IPUnblockForm { if ( $block->mBlockEmail && $block->mUser ) { $properties[] = $msg['emailblock']; } + + if ( !$block->mAllowUsertalk && $wgBlockAllowsUTEdit ) { + $properties[] = $msg['blocklist-nousertalk']; + } $properties = implode( ', ', $properties ); $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) ); $unblocklink = ''; - if ( $wgUser->isAllowed('block') ) { - $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); - $unblocklink = ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')'; + $changeblocklink = ''; + $toolLinks = ''; + if ( $wgUser->isAllowed( 'block' ) ) { + $unblocklink = $sk->link( SpecialPage::getTitleFor( 'Ipblocklist' ), + $msg['unblocklink'], + array(), + array( 'action' => 'unblock', 'id' => $block->mId ), + 'known' ); + + # Create changeblocklink for all blocks with exception of autoblocks + if( !$block->mAuto ) { + $changeblocklink = ' ' . wfMsg( 'pipe-separator' ) . ' ' . + $sk->link( SpecialPage::getTitleFor( 'Blockip', $block->mAddress ), + $msg['change-blocklink'], + array(), array(), 'known' ); + } + $toolLinks = "($unblocklink$changeblocklink)"; } $comment = $sk->commentBlock( $block->mReason ); @@ -359,7 +449,7 @@ class IPUnblockForm { $s = '<span class="history-deleted">' . $s . '</span>'; wfProfileOut( __METHOD__ ); - return "<li>$s $unblocklink</li>\n"; + return "<li>$s $toolLinks</li>\n"; } } diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php new file mode 100644 index 00000000..6b9df58f --- /dev/null +++ b/includes/specials/SpecialLinkSearch.php @@ -0,0 +1,185 @@ +<?php +/** + * @file + * @ingroup SpecialPage + * + * @author Brion Vibber + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + */ + +/** + * Special:LinkSearch to search the external-links table. + * @ingroup SpecialPage + */ + +function wfSpecialLinkSearch( $par ) { + + list( $limit, $offset ) = wfCheckLimits(); + global $wgOut, $wgRequest, $wgUrlProtocols, $wgMiserMode; + $target = $GLOBALS['wgRequest']->getVal( 'target', $par ); + $namespace = $GLOBALS['wgRequest']->getIntorNull( 'namespace', null ); + + $protocols_list[] = ''; + foreach( $wgUrlProtocols as $prot ) { + $protocols_list[] = $prot; + } + + $target2 = $target; + $protocol = ''; + $pr_sl = strpos($target2, '//' ); + $pr_cl = strpos($target2, ':' ); + if ( $pr_sl ) { + // For protocols with '//' + $protocol = substr( $target2, 0 , $pr_sl+2 ); + $target2 = substr( $target2, $pr_sl+2 ); + } elseif ( !$pr_sl && $pr_cl ) { + // For protocols without '//' like 'mailto:' + $protocol = substr( $target2, 0 , $pr_cl+1 ); + $target2 = substr( $target2, $pr_cl+1 ); + } elseif ( $protocol == '' && $target2 != '' ) { + // default + $protocol = 'http://'; + } + if ( !in_array( $protocol, $protocols_list ) ) { + // unsupported protocol, show original search request + $target2 = $target; + $protocol = ''; + } + + $self = Title::makeTitle( NS_SPECIAL, 'Linksearch' ); + + $wgOut->addWikiText( wfMsg( 'linksearch-text', '<nowiki>' . implode( ', ', $wgUrlProtocols) . '</nowiki>' ) ); + $s = Xml::openElement( 'form', array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $GLOBALS['wgScript'] ) ) . + Xml::hidden( 'title', $self->getPrefixedDbKey() ) . + '<fieldset>' . + Xml::element( 'legend', array(), wfMsg( 'linksearch' ) ) . + Xml::inputLabel( wfMsg( 'linksearch-pat' ), 'target', 'target', 50, $target ) . ' '; + if ( !$wgMiserMode ) { + $s .= Xml::label( wfMsg( 'linksearch-ns' ), 'namespace' ) . ' ' . + XML::namespaceSelector( $namespace, '' ); + } + $s .= Xml::submitButton( wfMsg( 'linksearch-ok' ) ) . + '</fieldset>' . + Xml::closeElement( 'form' ); + $wgOut->addHTML( $s ); + + if( $target != '' ) { + $searcher = new LinkSearchPage; + $searcher->setParams( array( + 'query' => $target2, + 'namespace' => $namespace, + 'protocol' => $protocol ) ); + $searcher->doQuery( $offset, $limit ); + } +} + +class LinkSearchPage extends QueryPage { + function setParams( $params ) { + $this->mQuery = $params['query']; + $this->mNs = $params['namespace']; + $this->mProt = $params['protocol']; + } + + function getName() { + return 'LinkSearch'; + } + + /** + * Disable RSS/Atom feeds + */ + function isSyndicated() { + return false; + } + + /** + * Return an appropriately formatted LIKE query and the clause + */ + static function mungeQuery( $query , $prot ) { + $field = 'el_index'; + $rv = LinkFilter::makeLike( $query , $prot ); + if ($rv === false) { + //makeLike doesn't handle wildcard in IP, so we'll have to munge here. + if (preg_match('/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/', $query)) { + $rv = $prot . rtrim($query, " \t*") . '%'; + $field = 'el_to'; + } + } + return array( $rv, $field ); + } + + function linkParameters() { + global $wgMiserMode; + $params = array(); + $params['target'] = $this->mProt . $this->mQuery; + if( isset( $this->mNs ) && !$wgMiserMode ) { + $params['namespace'] = $this->mNs; + } + return $params; + } + + function getSQL() { + global $wgMiserMode; + $dbr = wfGetDB( DB_SLAVE ); + $page = $dbr->tableName( 'page' ); + $externallinks = $dbr->tableName( 'externallinks' ); + + /* strip everything past first wildcard, so that index-based-only lookup would be done */ + list( $munged, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt ); + $stripped = substr($munged,0,strpos($munged,'%')+1); + $encSearch = $dbr->addQuotes( $stripped ); + + $encSQL = ''; + if ( isset ($this->mNs) && !$wgMiserMode ) + $encSQL = 'AND page_namespace=' . $dbr->addQuotes( $this->mNs ); + + $use_index = $dbr->useIndexClause( $clause ); + return + "SELECT + page_namespace AS namespace, + page_title AS title, + el_index AS value, + el_to AS url + FROM + $page, + $externallinks $use_index + WHERE + page_id=el_from + AND $clause LIKE $encSearch + $encSQL"; + } + + function formatResult( $skin, $result ) { + $title = Title::makeTitle( $result->namespace, $result->title ); + $url = $result->url; + $pageLink = $skin->makeKnownLinkObj( $title ); + $urlLink = $skin->makeExternalLink( $url, $url ); + + return wfMsgHtml( 'linksearch-line', $urlLink, $pageLink ); + } + + /** + * Override to check query validity. + */ + function doQuery( $offset, $limit, $shownavigation=true ) { + global $wgOut; + list( $this->mMungedQuery, $clause ) = LinkSearchPage::mungeQuery( $this->mQuery, $this->mProt ); + if( $this->mMungedQuery === false ) { + $wgOut->addWikiText( wfMsg( 'linksearch-error' ) ); + } else { + // For debugging + // Generates invalid xhtml with patterns that contain -- + //$wgOut->addHTML( "\n<!-- " . htmlspecialchars( $this->mMungedQuery ) . " -->\n" ); + parent::doQuery( $offset, $limit, $shownavigation ); + } + } + + /** + * Override to squash the ORDER BY. + * We do a truncated index search, so the optimizer won't trust + * it as good enough for optimizing sort. The implicit ordering + * from the scan will usually do well enough for our needs. + */ + function getOrder() { + return ''; + } +} diff --git a/includes/specials/SpecialListUserRestrictions.php b/includes/specials/SpecialListUserRestrictions.php new file mode 100644 index 00000000..27b24298 --- /dev/null +++ b/includes/specials/SpecialListUserRestrictions.php @@ -0,0 +1,161 @@ +<?php + +function wfSpecialListUserRestrictions() { + global $wgOut, $wgRequest; + + $wgOut->addWikiMsg( 'listuserrestrictions-intro' ); + $f = new SpecialListUserRestrictionsForm(); + $wgOut->addHTML( $f->getHTML() ); + + if( !mt_rand( 0, 10 ) ) + UserRestriction::purgeExpired(); + $pager = new UserRestrictionsPager( $f->getConds() ); + if( $pager->getNumRows() ) + $wgOut->addHTML( $pager->getNavigationBar() . + Xml::tags( 'ul', null, $pager->getBody() ) . + $pager->getNavigationBar() + ); + elseif( $f->getConds() ) + $wgOut->addWikiMsg( 'listuserrestrictions-notfound' ); + else + $wgOut->addWikiMsg( 'listuserrestrictions-empty' ); +} + +class SpecialListUserRestrictionsForm { + public function getHTML() { + global $wgRequest, $wgScript, $wgTitle; + $s = ''; + $s .= Xml::fieldset( wfMsg( 'listuserrestrictions-legend' ) ); + $s .= "<form action=\"{$wgScript}\">"; + $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); + $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-type' ), 'type' ) . ' ' . + self::typeSelector( 'type', $wgRequest->getVal( 'type' ), 'type' ); + $s .= ' '; + $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-user' ), 'user', 'user', + false, $wgRequest->getVal( 'user' ) ); + $s .= '<p>'; + $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-namespace' ), 'namespace' ) . ' ' . + Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ), '', 'namespace' ); + $s .= ' '; + $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-page' ), 'page', 'page', + false, $wgRequest->getVal( 'page' ) ); + $s .= Xml::submitButton( wfMsg( 'listuserrestrictions-submit' ) ); + $s .= "</p></form></fieldset>"; + return $s; + } + + public static function typeSelector( $name = 'type', $value = '', $id = false ) { + $s = new XmlSelect( $name, $id, $value ); + $s->addOption( wfMsg( 'userrestrictiontype-none' ), '' ); + $s->addOption( wfMsg( 'userrestrictiontype-page' ), UserRestriction::PAGE ); + $s->addOption( wfMsg( 'userrestrictiontype-namespace' ), UserRestriction::NAMESPACE ); + return $s->getHTML(); + } + + public function getConds() { + global $wgRequest; + $conds = array(); + + $type = $wgRequest->getVal( 'type' ); + if( in_array( $type, array( UserRestriction::PAGE, UserRestriction::NAMESPACE ) ) ) + $conds['ur_type'] = $type; + + $user = $wgRequest->getVal( 'user' ); + if( $user ) + $conds['ur_user_text'] = $user; + + $namespace = $wgRequest->getVal( 'namespace' ); + if( $namespace || $namespace === '0' ) + $conds['ur_namespace'] = $namespace; + + $page = $wgRequest->getVal( 'page' ); + $title = Title::newFromText( $page ); + if( $title ) { + $conds['ur_page_namespace'] = $title->getNamespace(); + $conds['ur_page_title'] = $title->getDBKey(); + } + + return $conds; + } +} + +class UserRestrictionsPager extends ReverseChronologicalPager { + public $mConds; + + public function __construct( $conds = array() ) { + $this->mConds = $conds; + parent::__construct(); + } + + public function getStartBody() { + # Copied from Special:Ipblocklist + wfProfileIn( __METHOD__ ); + # Do a link batch query + $this->mResult->seek( 0 ); + $lb = new LinkBatch; + + # Faster way + # Usernames and titles are in fact related by a simple substitution of space -> underscore + # The last few lines of Title::secureAndSplit() tell the story. + foreach( $this->mResult as $row ) { + $name = str_replace( ' ', '_', $row->ur_by_text ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + $name = str_replace( ' ', '_', $row->ur_user_text ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + if( $row->ur_type == UserRestriction::PAGE ) + $lb->add( $row->ur_page_namespace, $row->ur_page_title ); + } + $lb->execute(); + wfProfileOut( __METHOD__ ); + return ''; + } + + public function getQueryInfo() { + return array( + 'tables' => 'user_restrictions', + 'fields' => '*', + 'conds' => $this->mConds, + ); + } + + public function formatRow( $row ) { + return self::formatRestriction( UserRestriction::newFromRow( $row ) ); + } + + // Split off for use on Special:RestrictUser + public static function formatRestriction( $r ) { + global $wgUser, $wgLang; + $sk = $wgUser->getSkin(); + $timestamp = $wgLang->timeanddate( $r->getTimestamp(), true ); + $blockerlink = $sk->userLink( $r->getBlockerId(), $r->getBlockerText() ) . + $sk->userToolLinks( $r->getBlockerId(), $r->getBlockerText() ); + $subjlink = $sk->userLink( $r->getSubjectId(), $r->getSubjectText() ) . + $sk->userToolLinks( $r->getSubjectId(), $r->getSubjectText() ); + $expiry = is_numeric( $r->getExpiry() ) ? + wfMsg( 'listuserrestrictions-row-expiry', $wgLang->timeanddate( $r->getExpiry() ) ) : + wfMsg( 'ipbinfinite' ); + $msg = ''; + if( $r->isNamespace() ) { + $msg = wfMsgHtml( 'listuserrestrictions-row-ns', $subjlink, + $wgLang->getDisplayNsText( $r->getNamespace() ), $expiry ); + } + if( $r->isPage() ) { + $pagelink = $sk->link( $r->getPage() ); + $msg = wfMsgHtml( 'listuserrestrictions-row-page', $subjlink, + $pagelink, $expiry ); + } + $reason = $sk->commentBlock( $r->getReason() ); + $removelink = ''; + if( $wgUser->isAllowed( 'restrict' ) ) { + $removelink = '(' . $sk->link( SpecialPage::getTitleFor( 'RemoveRestrictions' ), + wfMsgHtml( 'listuserrestrictions-remove' ), array(), array( 'id' => $r->getId() ) ) . ')'; + } + return "<li>{$timestamp}, {$blockerlink} {$msg} {$reason} {$removelink}</li>\n"; + } + + public function getIndexField() { + return 'ur_timestamp'; + } +} diff --git a/includes/specials/SpecialImagelist.php b/includes/specials/SpecialListfiles.php index 3d449b54..d2178ee0 100644 --- a/includes/specials/SpecialImagelist.php +++ b/includes/specials/SpecialListfiles.php @@ -7,7 +7,7 @@ /** * */ -function wfSpecialImagelist() { +function wfSpecialListfiles() { global $wgOut; $pager = new ImageListPager; @@ -49,13 +49,17 @@ class ImageListPager extends TablePager { function getFieldNames() { if ( !$this->mFieldNames ) { + global $wgMiserMode; $this->mFieldNames = array( - 'img_timestamp' => wfMsg( 'imagelist_date' ), - 'img_name' => wfMsg( 'imagelist_name' ), - 'img_user_text' => wfMsg( 'imagelist_user' ), - 'img_size' => wfMsg( 'imagelist_size' ), - 'img_description' => wfMsg( 'imagelist_description' ), + 'img_timestamp' => wfMsg( 'listfiles_date' ), + 'img_name' => wfMsg( 'listfiles_name' ), + 'img_user_text' => wfMsg( 'listfiles_user' ), + 'img_size' => wfMsg( 'listfiles_size' ), + 'img_description' => wfMsg( 'listfiles_description' ), ); + if( !$wgMiserMode ) { + $this->mFieldNames['COUNT(oi_archive_name)'] = wfMsg( 'listfiles_count' ); + } } return $this->mFieldNames; } @@ -66,13 +70,22 @@ class ImageListPager extends TablePager { } function getQueryInfo() { - $fields = $this->getFieldNames(); - $fields = array_keys( $fields ); + $tables = array( 'image' ); + $fields = array_keys( $this->getFieldNames() ); $fields[] = 'img_user'; + $options = $join_conds = array(); + # Depends on $wgMiserMode + if( isset($this->mFieldNames['COUNT(oi_archive_name)']) ) { + $tables[] = 'oldimage'; + $options = array('GROUP BY' => 'img_name'); + $join_conds = array('oldimage' => array('LEFT JOIN','oi_name = img_name') ); + } return array( - 'tables' => 'image', - 'fields' => $fields, - 'conds' => $this->mQueryConds + 'tables' => $tables, + 'fields' => $fields, + 'conds' => $this->mQueryConds, + 'options' => $options, + 'join_conds' => $join_conds ); } @@ -106,7 +119,7 @@ class ImageListPager extends TablePager { if ( $imgfile === null ) $imgfile = wfMsg( 'imgfile' ); $name = $this->mCurrentRow->img_name; - $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value ); + $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_FILE, $name ), $value ); $image = wfLocalFile( $value ); $url = $image->getURL(); $download = Xml::element('a', array( 'href' => $url ), $imgfile ); @@ -123,6 +136,8 @@ class ImageListPager extends TablePager { return $this->getSkin()->formatSize( $value ); case 'img_description': return $this->getSkin()->commentBlock( $value ); + case 'COUNT(oi_archive_name)': + return intval($value)+1; } } @@ -130,14 +145,14 @@ class ImageListPager extends TablePager { global $wgRequest, $wgMiserMode; $search = $wgRequest->getText( 'ilsearch' ); - $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-imagelist-form' ) ) . + $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-listfiles-form' ) ) . Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'imagelist' ) ) . + Xml::element( 'legend', null, wfMsg( 'listfiles' ) ) . Xml::tags( 'label', null, wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) ); if ( !$wgMiserMode ) { $s .= "<br />\n" . - Xml::inputLabel( wfMsg( 'imagelist_search_for' ), 'ilsearch', 'mw-ilsearch', 20, $search ); + Xml::inputLabel( wfMsg( 'listfiles_search_for' ), 'ilsearch', 'mw-ilsearch', 20, $search ); } $s .= ' ' . Xml::submitButton( wfMsg( 'table_pager_limit_submit' ) ) ."\n" . @@ -148,14 +163,14 @@ class ImageListPager extends TablePager { } function getTableClass() { - return 'imagelist ' . parent::getTableClass(); + return 'listfiles ' . parent::getTableClass(); } function getNavClass() { - return 'imagelist_nav ' . parent::getNavClass(); + return 'listfiles_nav ' . parent::getNavClass(); } function getSortHeaderClass() { - return 'imagelist_sort ' . parent::getSortHeaderClass(); + return 'listfiles_sort ' . parent::getSortHeaderClass(); } } diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php index 131c0606..5c76df8c 100644 --- a/includes/specials/SpecialListgrouprights.php +++ b/includes/specials/SpecialListgrouprights.php @@ -24,7 +24,8 @@ class SpecialListGroupRights extends SpecialPage { * Show the special page */ public function execute( $par ) { - global $wgOut, $wgGroupPermissions, $wgImplicitGroups, $wgMessageCache; + global $wgOut, $wgImplicitGroups, $wgMessageCache; + global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups; $wgMessageCache->loadAllMessages(); $this->setHeaders(); @@ -69,13 +70,16 @@ class SpecialListGroupRights extends SpecialPage { $grouplink = ''; } + $addgroups = isset( $wgAddGroups[$group] ) ? $wgAddGroups[$group] : array(); + $removegroups = isset( $wgRemoveGroups[$group] ) ? $wgRemoveGroups[$group] : array(); + $wgOut->addHTML( '<tr> <td>' . $grouppage . $grouplink . '</td> <td>' . - self::formatPermissions( $permissions ) . + self::formatPermissions( $permissions, $addgroups, $removegroups ) . '</td> </tr>' ); @@ -91,18 +95,29 @@ class SpecialListGroupRights extends SpecialPage { * @param $permissions Array of permission => bool (from $wgGroupPermissions items) * @return string List of all granted permissions, separated by comma separator */ - private static function formatPermissions( $permissions ) { + private static function formatPermissions( $permissions, $add, $remove ) { + global $wgLang; $r = array(); foreach( $permissions as $permission => $granted ) { if ( $granted ) { - $description = wfMsgHTML( 'listgrouprights-right-display', - User::getRightDescription($permission), + $description = wfMsgExt( 'listgrouprights-right-display', array( 'parseinline' ), + User::getRightDescription( $permission ), $permission ); $r[] = $description; } } sort( $r ); + if( $add === true ){ + $r[] = wfMsgExt( 'listgrouprights-addgroup-all', array( 'escape' ) ); + } else if( is_array( $add ) && count( $add ) ) { + $r[] = wfMsgExt( 'listgrouprights-addgroup', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ), count( $add ) ); + } + if( $remove === true ){ + $r[] = wfMsgExt( 'listgrouprights-removegroup-all', array( 'escape' ) ); + } else if( is_array( $remove ) && count( $remove ) ) { + $r[] = wfMsgExt( 'listgrouprights-removegroup', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ), count( $remove ) ); + } if( empty( $r ) ) { return ''; } else { diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php index 808aab14..9555bd16 100644 --- a/includes/specials/SpecialListredirects.php +++ b/includes/specials/SpecialListredirects.php @@ -22,7 +22,8 @@ class ListredirectsPage extends QueryPage { function getSQL() { $dbr = wfGetDB( DB_SLAVE ); $page = $dbr->tableName( 'page' ); - $sql = "SELECT 'Listredirects' AS type, page_title AS title, page_namespace AS namespace, 0 AS value FROM $page WHERE page_is_redirect = 1"; + $sql = "SELECT 'Listredirects' AS type, page_title AS title, page_namespace AS namespace, + 0 AS value FROM $page WHERE page_is_redirect = 1"; return( $sql ); } diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php index 7dba44e2..17bec70e 100644 --- a/includes/specials/SpecialListusers.php +++ b/includes/specials/SpecialListusers.php @@ -35,10 +35,25 @@ */ class UsersPager extends AlphabeticPager { - function __construct($group=null) { + function __construct( $par=null ) { global $wgRequest; - $this->requestedGroup = $group != "" ? $group : $wgRequest->getVal( 'group' ); - $un = $wgRequest->getText( 'username' ); + $parms = explode( '/', ($par = ( $par !== null ) ? $par : '' ) ); + $symsForAll = array( '*', 'user' ); + if ( $parms[0] != '' && ( in_array( $par, User::getAllGroups() ) || in_array( $par, $symsForAll ) ) ) { + $this->requestedGroup = $par; + $un = $wgRequest->getText( 'username' ); + } else if ( count( $parms ) == 2 ) { + $this->requestedGroup = $parms[0]; + $un = $parms[1]; + } else { + $this->requestedGroup = $wgRequest->getVal( 'group' ); + $un = ( $par != '' ) ? $par : $wgRequest->getText( 'username' ); + } + if ( in_array( $this->requestedGroup, $symsForAll ) ) { + $this->requestedGroup = ''; + } + $this->editsOnly = $wgRequest->getBool( 'editsOnly' ); + $this->requestedUser = ''; if ( $un != '' ) { $username = Title::makeTitleSafe( NS_USER, $un ); @@ -56,9 +71,9 @@ class UsersPager extends AlphabeticPager { function getQueryInfo() { $dbr = wfGetDB( DB_SLAVE ); - $conds=array(); - // don't show hidden names - $conds[]='ipb_deleted IS NULL OR ipb_deleted = 0'; + $conds = array(); + // Don't show hidden names + $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0'; if ($this->requestedGroup != "") { $conds['ug_group'] = $this->requestedGroup; $useIndex = ''; @@ -68,6 +83,9 @@ class UsersPager extends AlphabeticPager { if ($this->requestedUser != "") { $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser ); } + if( $this->editsOnly ) { + $conds[] = 'user_editcount > 0'; + } list ($user,$user_groups,$ipblocks) = $dbr->tableNamesN('user','user_groups','ipblocks'); @@ -76,6 +94,7 @@ class UsersPager extends AlphabeticPager { LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_auto=0 ", 'fields' => array('user_name', 'MAX(user_id) AS user_id', + 'MAX(user_editcount) AS edits', 'COUNT(ug_group) AS numgroups', 'MAX(ug_group) AS singlegroup'), 'options' => array('GROUP BY' => 'user_name'), @@ -87,6 +106,8 @@ class UsersPager extends AlphabeticPager { } function formatRow( $row ) { + global $wgLang; + $userPage = Title::makeTitle( NS_USER, $row->user_name ); $name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) ); @@ -102,18 +123,24 @@ class UsersPager extends AlphabeticPager { } $item = wfSpecialList( $name, $groups ); + + global $wgEdititis; + if ( $wgEdititis ) { + $editCount = $wgLang->formatNum( $row->edits ); + $edits = ' [' . wfMsgExt( 'usereditcount', 'parsemag', $editCount ) . ']'; + } else { + $edits = ''; + } wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) ); - return "<li>{$item}</li>"; + return "<li>{$item}{$edits}</li>"; } function getBody() { - if (!$this->mQueryDone) { + if( !$this->mQueryDone ) { $this->doQuery(); } - $batch = new LinkBatch; - $this->mResult->rewind(); - + $batch = new LinkBatch; while ( $row = $this->mResult->fetchObject() ) { $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); } @@ -142,7 +169,9 @@ class UsersPager extends AlphabeticPager { Xml::option( wfMsg( 'group-all' ), '' ); foreach( $this->getAllGroups() as $group => $groupText ) $out .= Xml::option( $groupText, $group, $group == $this->requestedGroup ); - $out .= Xml::closeElement( 'select' ) . ' '; + $out .= Xml::closeElement( 'select' ) . '<br/>'; + $out .= Xml::checkLabel( wfMsg('listusers-editsonly'), 'editsOnly', 'editsOnly', $this->editsOnly ); + $out .= ' '; wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) ); @@ -186,14 +215,8 @@ class UsersPager extends AlphabeticPager { * @return array */ protected static function getGroups( $uid ) { - $dbr = wfGetDB( DB_SLAVE ); - $groups = array(); - $res = $dbr->select( 'user_groups', 'ug_group', array( 'ug_user' => $uid ), __METHOD__ ); - if( $res && $dbr->numRows( $res ) > 0 ) { - while( $row = $dbr->fetchObject( $res ) ) - $groups[] = $row->ug_group; - $dbr->freeResult( $res ); - } + $user = User::newFromId( $uid ); + $groups = array_diff( $user->getEffectiveGroups(), $user->getImplicitGroups() ); return $groups; } @@ -222,7 +245,8 @@ function wfSpecialListusers( $par = null ) { # getBody() first to check, if empty $usersbody = $up->getBody(); - $s = $up->getPageHeader(); + $s = XML::openElement( 'div', array('class' => 'mw-spcontent') ); + $s .= $up->getPageHeader(); if( $usersbody ) { $s .= $up->getNavigationBar(); $s .= '<ul>' . $usersbody . '</ul>'; @@ -230,6 +254,6 @@ function wfSpecialListusers( $par = null ) { } else { $s .= '<p>' . wfMsgHTML('listusers-noresult') . '</p>'; }; - + $s .= XML::closeElement( 'div' ); $wgOut->addHTML( $s ); } diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php index 04019223..5859d5b2 100644 --- a/includes/specials/SpecialLockdb.php +++ b/includes/specials/SpecialLockdb.php @@ -109,7 +109,7 @@ END } fwrite( $fp, $this->reason ); fwrite( $fp, "\n<p>(by " . $wgUser->getName() . " at " . - $wgLang->timeanddate( wfTimestampNow() ) . ")\n" ); + $wgLang->timeanddate( wfTimestampNow() ) . ")</p>\n" ); fclose( $fp ); $titleObj = SpecialPage::getTitleFor( 'Lockdb' ); diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php index 3154ed13..492c2608 100644 --- a/includes/specials/SpecialLog.php +++ b/includes/specials/SpecialLog.php @@ -26,10 +26,21 @@ * constructor */ function wfSpecialLog( $par = '' ) { - global $wgRequest, $wgOut, $wgUser; + global $wgRequest, $wgOut, $wgUser, $wgLogTypes; + # Get parameters - $type = $wgRequest->getVal( 'type', $par ); - $user = $wgRequest->getText( 'user' ); + $parms = explode( '/', ($par = ( $par !== null ) ? $par : '' ) ); + $symsForAll = array( '*', 'all' ); + if ( $parms[0] != '' && ( in_array( $par, $wgLogTypes ) || in_array( $par, $symsForAll ) ) ) { + $type = $par; + $user = $wgRequest->getText( 'user' ); + } else if ( count( $parms ) == 2 ) { + $type = $parms[0]; + $user = $parms[1]; + } else { + $type = $wgRequest->getVal( 'type' ); + $user = ( $par != '' ) ? $par : $wgRequest->getText( 'user' ); + } $title = $wgRequest->getText( 'page' ); $pattern = $wgRequest->getBool( 'pattern' ); $y = $wgRequest->getIntOrNull( 'year' ); @@ -40,15 +51,14 @@ function wfSpecialLog( $par = '' ) { $y = ''; $m = ''; } - # Create a LogPager item to get the results and a LogEventsList - # item to format them... + # Create a LogPager item to get the results and a LogEventsList item to format them... $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 ); $pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m ); # Set title and add header $loglist->showHeader( $pager->getType() ); # Show form options $loglist->showOptions( $pager->getType(), $pager->getUser(), $pager->getPage(), $pager->getPattern(), - $pager->getYear(), $pager->getMonth() ); + $pager->getYear(), $pager->getMonth(), $pager->getFilterParams() ); # Insert list $logBody = $pager->getBody(); if( $logBody ) { diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php index 5aafac7d..90da25fd 100644 --- a/includes/specials/SpecialLonelypages.php +++ b/includes/specials/SpecialLonelypages.php @@ -29,7 +29,7 @@ class LonelyPagesPage extends PageQueryPage { function getSQL() { $dbr = wfGetDB( DB_SLAVE ); - list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' ); + list( $page, $pagelinks, $templatelinks ) = $dbr->tableNamesN( 'page', 'pagelinks', 'templatelinks' ); return "SELECT 'Lonelypages' AS type, @@ -39,9 +39,12 @@ class LonelyPagesPage extends PageQueryPage { FROM $page LEFT JOIN $pagelinks ON page_namespace=pl_namespace AND page_title=pl_title + LEFT JOIN $templatelinks + ON page_namespace=tl_namespace AND page_title=tl_title WHERE pl_namespace IS NULL AND page_namespace=".NS_MAIN." - AND page_is_redirect=0"; + AND page_is_redirect=0 + AND tl_namespace IS NULL"; } } diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php index 82ee4be6..cdfde24e 100644 --- a/includes/specials/SpecialMIMEsearch.php +++ b/includes/specials/SpecialMIMEsearch.php @@ -46,7 +46,7 @@ class MIMEsearchPage extends QueryPage { return "SELECT 'MIMEsearch' AS type, - " . NS_IMAGE . " AS namespace, + " . NS_FILE . " AS namespace, img_name AS title, img_major_mime AS value, diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index 0460c207..f870406c 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -96,8 +96,10 @@ class MergehistoryForm { wfEscapeWikiText( $this->mDestObj->getPrefixedText() ) ); } - - // TODO: warn about target = dest? + + if ( $this->mTargetObj->equals( $this->mDestObj ) ) { + $errors[] = wfMsgExt( 'mergehistory-same-destination', array( 'parse' ) ); + } if ( count( $errors ) ) { $this->showMergeForm(); @@ -113,7 +115,7 @@ class MergehistoryForm { $wgOut->addWikiMsg( 'mergehistory-header' ); - $wgOut->addHtml( + $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . @@ -156,7 +158,7 @@ class MergehistoryForm { $action = $titleObj->getLocalURL( "action=submit" ); # Start the form here $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) ); - $wgOut->addHtml( $top ); + $wgOut->addHTML( $top ); if( $haveRevisions ) { # Format the user-visible controls (comment field, submission button) @@ -188,7 +190,7 @@ class MergehistoryForm { Xml::closeElement( 'table' ) . Xml::closeElement( 'fieldset' ); - $wgOut->addHtml( $table ); + $wgOut->addHTML( $table ); } $wgOut->addHTML( "<h2 id=\"mw-mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" ); @@ -215,7 +217,7 @@ class MergehistoryForm { $misc .= Xml::hidden( 'dest', $this->mDest ); $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() ); $misc .= Xml::closeElement( 'form' ); - $wgOut->addHtml( $misc ); + $wgOut->addHTML( $misc ); return true; } @@ -229,7 +231,7 @@ class MergehistoryForm { $last = $this->message['last']; $ts = wfTimestamp( TS_MW, $row->rev_timestamp ); - $checkBox = wfRadio( "mergepoint", $ts, false ); + $checkBox = Xml::radio( "mergepoint", $ts, false ); $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(), htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getId() ); @@ -370,7 +372,7 @@ class MergehistoryForm { $log->addEntry( 'merge', $targetTitle, $this->mComment, array($destTitle->getPrefixedText(),$TimestampLimit) ); - $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'), + $wgOut->addHTML( wfMsgExt( 'mergehistory-success', array('parseinline'), $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) ); wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) ); @@ -432,10 +434,10 @@ class MergeHistoryPager extends ReverseChronologicalPager { function getQueryInfo() { $conds = $this->mConds; $conds['rev_page'] = $this->articleID; + $conds[] = 'page_id = rev_page'; $conds[] = "rev_timestamp < {$this->maxTimestamp}"; - return array( - 'tables' => array('revision'), + 'tables' => array('revision','page'), 'fields' => array( 'rev_minor_edit', 'rev_timestamp', 'rev_user', 'rev_user_text', 'rev_comment', 'rev_id', 'rev_page', 'rev_text_id', 'rev_len', 'rev_deleted' ), 'conds' => $conds diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php index e6810999..1ba05626 100644 --- a/includes/specials/SpecialMostcategories.php +++ b/includes/specials/SpecialMostcategories.php @@ -39,9 +39,9 @@ class MostcategoriesPage extends QueryPage { function formatResult( $skin, $result ) { global $wgLang; $title = Title::makeTitleSafe( $result->namespace, $result->title ); - if ( !$title instanceof Title ) { throw new MWException('Invalid title in database'); } + $count = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) ); - $link = $skin->makeKnownLinkObj( $title, $title->getText() ); + $link = $skin->link( $title ); return wfSpecialList( $link, $count ); } } diff --git a/includes/specials/SpecialMostimages.php b/includes/specials/SpecialMostimages.php index 6cfeb7ad..5cc100ba 100644 --- a/includes/specials/SpecialMostimages.php +++ b/includes/specials/SpecialMostimages.php @@ -25,7 +25,7 @@ class MostimagesPage extends ImageQueryPage { " SELECT 'Mostimages' as type, - " . NS_IMAGE . " as namespace, + " . NS_FILE . " as namespace, il_to as title, COUNT(*) as value FROM $imagelinks diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php index d597a4e0..2d398a38 100644 --- a/includes/specials/SpecialMostlinkedtemplates.php +++ b/includes/specials/SpecialMostlinkedtemplates.php @@ -92,15 +92,12 @@ class SpecialMostlinkedtemplates extends QueryPage { */ public function formatResult( $skin, $result ) { $title = Title::makeTitleSafe( $result->namespace, $result->title ); - if( $title instanceof Title ) { - return wfSpecialList( - $skin->makeLinkObj( $title ), - $this->makeWlhLink( $title, $skin, $result ) - ); - } else { - $tsafe = htmlspecialchars( $result->title ); - return "Invalid title in result set; {$tsafe}"; - } + + $skin->link( $title ); + return wfSpecialList( + $skin->makeLinkObj( $title ), + $this->makeWlhLink( $title, $skin, $result ) + ); } /** @@ -115,8 +112,8 @@ class SpecialMostlinkedtemplates extends QueryPage { global $wgLang; $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' ); $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $result->value ) ); - return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() ); + $wgLang->formatNum( $result->value ) ); + return $skin->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) ); } } diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index efd2dcfd..acc27625 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -54,12 +54,13 @@ function wfSpecialMovepage( $par = null ) { * @ingroup SpecialPage */ class MovePageForm { - var $oldTitle, $newTitle, $reason; # Text input - var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects; + var $oldTitle, $newTitle; # Objects + var $reason; # Text input + var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect; # Checks private $watch = false; - function MovePageForm( $oldTitle, $newTitle ) { + function __construct( $oldTitle, $newTitle ) { global $wgRequest; $target = isset($par) ? $par : $wgRequest->getVal( 'target' ); $this->oldTitle = $oldTitle; @@ -68,48 +69,54 @@ class MovePageForm { if ( $wgRequest->wasPosted() ) { $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false ); $this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', false ); + $this->leaveRedirect = $wgRequest->getBool( 'wpLeaveRedirect', false ); } else { $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true ); $this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', true ); + $this->leaveRedirect = $wgRequest->getBool( 'wpLeaveRedirect', true ); } $this->moveSubpages = $wgRequest->getBool( 'wpMovesubpages', false ); $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' ); $this->watch = $wgRequest->getCheck( 'wpWatch' ); } - function showForm( $err, $hookErr = '' ) { - global $wgOut, $wgUser; + /** + * Show the form + * @param mixed $err Error message. May either be a string message name or + * array message name and parameters, like the second argument to + * OutputPage::wrapWikiMsg(). + */ + function showForm( $err ) { + global $wgOut, $wgUser, $wgFixDoubleRedirects; $skin = $wgUser->getSkin(); $oldTitleLink = $skin->makeLinkObj( $this->oldTitle ); - $oldTitle = $this->oldTitle->getPrefixedText(); - $wgOut->setPagetitle( wfMsg( 'move-page', $oldTitle ) ); + $wgOut->setPagetitle( wfMsg( 'move-page', $this->oldTitle->getPrefixedText() ) ); $wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) ); - if( $this->newTitle == '' ) { + $newTitle = $this->newTitle; + + if( !$newTitle ) { # Show the current title as a default # when the form is first opened. - $newTitle = $oldTitle; - } else { - if( $err == '' ) { - $nt = Title::newFromURL( $this->newTitle ); - if( $nt ) { - # If a title was supplied, probably from the move log revert - # link, check for validity. We can then show some diagnostic - # information and save a click. - $newerr = $this->oldTitle->isValidMoveOperation( $nt ); - if( is_string( $newerr ) ) { - $err = $newerr; - } + $newTitle = $this->oldTitle; + } + else { + if( empty($err) ) { + # If a title was supplied, probably from the move log revert + # link, check for validity. We can then show some diagnostic + # information and save a click. + $newerr = $this->oldTitle->isValidMoveOperation( $newTitle ); + if( $newerr ) { + $err = $newerr[0]; } } - $newTitle = $this->newTitle; } - if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) { - $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle ); + if ( !empty($err) && $err[0] == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) { + $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle->getPrefixedText() ); $movepagebtn = wfMsg( 'delete_and_move' ); $submitVar = 'wpDeleteAndMove'; $confirm = " @@ -131,12 +138,16 @@ class MovePageForm { $considerTalk = ( !$this->oldTitle->isTalkPage() && $oldTalk->exists() ); $dbr = wfGetDB( DB_SLAVE ); - $hasRedirects = $dbr->selectField( 'redirect', '1', - array( - 'rd_namespace' => $this->oldTitle->getNamespace(), - 'rd_title' => $this->oldTitle->getDBkey(), - ) , __METHOD__ ); - + if ( $wgFixDoubleRedirects ) { + $hasRedirects = $dbr->selectField( 'redirect', '1', + array( + 'rd_namespace' => $this->oldTitle->getNamespace(), + 'rd_title' => $this->oldTitle->getDBkey(), + ) , __METHOD__ ); + } else { + $hasRedirects = false; + } + if ( $considerTalk ) { $wgOut->addWikiMsg( 'movepagetalktext' ); } @@ -144,9 +155,10 @@ class MovePageForm { $titleObj = SpecialPage::getTitleFor( 'Movepage' ); $token = htmlspecialchars( $wgUser->editToken() ); - if ( $err != '' ) { + if ( !empty($err) ) { $wgOut->setSubtitle( wfMsg( 'formerror' ) ); - if( $err == 'hookaborted' ) { + if( $err[0] == 'hookaborted' ) { + $hookErr = $err[1]; $errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n"; $wgOut->addHTML( $errMsg ); } else { @@ -172,8 +184,8 @@ class MovePageForm { Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) . "</td> <td class='mw-input'>" . - Xml::input( 'wpNewTitle', 40, $newTitle, array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) . - Xml::hidden( 'wpOldTitle', $oldTitle ) . + Xml::input( 'wpNewTitle', 40, $newTitle->getPrefixedText(), array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) . + Xml::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) . "</td> </tr> <tr> @@ -197,6 +209,18 @@ class MovePageForm { ); } + if ( $wgUser->isAllowed( 'suppressredirect' ) ) { + $wgOut->addHTML( " + <tr> + <td></td> + <td class='mw-input' >" . + Xml::checkLabel( wfMsg( 'move-leave-redirect' ), 'wpLeaveRedirect', + 'wpLeaveRedirect', $this->leaveRedirect ) . + "</td> + </tr>" + ); + } + if ( $hasRedirects ) { $wgOut->addHTML( " <tr> @@ -205,7 +229,7 @@ class MovePageForm { Xml::checkLabel( wfMsg( 'fix-double-redirects' ), 'wpFixRedirects', 'wpFixRedirects', $this->fixRedirects ) . "</td> - </td>" + </tr>" ); } @@ -215,7 +239,7 @@ class MovePageForm { <tr> <td></td> <td class=\"mw-input\">" . - Xml::checkLabel( wfMsgHtml( + Xml::checkLabel( wfMsg( $this->oldTitle->hasSubpages() ? 'move-subpages' : 'move-talk-subpages' @@ -259,6 +283,7 @@ class MovePageForm { function doSubmit() { global $wgOut, $wgUser, $wgRequest, $wgMaximumMovedPages, $wgLang; + global $wgFixDoubleRedirects; if ( $wgUser->pingLimiter( 'move' ) ) { $wgOut->rateLimited(); @@ -280,6 +305,12 @@ class MovePageForm { return; } + // Delete an associated image if there is + $file = wfLocalFile( $nt ); + if( $file->exists() ) { + $file->delete( wfMsgForContent( 'delete_and_move_reason' ), false ); + } + // This may output an error message and exit $article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) ); } @@ -290,14 +321,20 @@ class MovePageForm { return; } - $error = $ot->moveTo( $nt, true, $this->reason ); + if ( $wgUser->isAllowed( 'suppressredirect' ) ) { + $createRedirect = $this->leaveRedirect; + } else { + $createRedirect = true; + } + + $error = $ot->moveTo( $nt, true, $this->reason, $createRedirect ); if ( $error !== true ) { - # FIXME: showForm() should handle multiple errors - call_user_func_array(array($this, 'showForm'), $error[0]); + # FIXME: show all the errors in a list, not just the first one + $this->showForm( reset( $error ) ); return; } - if ( $this->fixRedirects ) { + if ( $wgFixDoubleRedirects && $this->fixRedirects ) { DoubleRedirectJob::fixRedirects( 'move', $ot, $nt ); } @@ -312,7 +349,9 @@ class MovePageForm { $oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>"; $newLink = "<span class='plainlinks'>[$newUrl $newText]</span>"; + $msgName = $createRedirect ? 'movepage-moved-redirect' : 'movepage-moved-noredirect'; $wgOut->addWikiMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText ); + $wgOut->addWikiMsg( $msgName ); # Now we move extra pages we've been asked to move: subpages and talk # pages. First, if the old page or the new page is a talk page, we @@ -364,25 +403,26 @@ class MovePageForm { $conds = null; } - $extrapages = array(); + $extraPages = array(); if( !is_null( $conds ) ) { - $extrapages = $dbr->select( 'page', - array( 'page_id', 'page_namespace', 'page_title' ), - $conds, - __METHOD__ + $extraPages = TitleArray::newFromResult( + $dbr->select( 'page', + array( 'page_id', 'page_namespace', 'page_title' ), + $conds, + __METHOD__ + ) ); } $extraOutput = array(); $skin = $wgUser->getSkin(); $count = 1; - foreach( $extrapages as $row ) { - if( $row->page_id == $ot->getArticleId() ) { + foreach( $extraPages as $oldSubpage ) { + if( $oldSubpage->getArticleId() == $ot->getArticleId() ) { # Already did this one. continue; } - $oldSubpage = Title::newFromRow( $row ); $newPageName = preg_replace( '#^'.preg_quote( $ot->getDBKey(), '#' ).'#', $nt->getDBKey(), @@ -408,7 +448,7 @@ class MovePageForm { $link = $skin->makeKnownLinkObj( $newSubpage ); $extraOutput []= wfMsgHtml( 'movepage-page-exists', $link ); } else { - $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason ); + $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect ); if( $success === true ) { if ( $this->fixRedirects ) { DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage ); diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index e57f6fc1..575e37a7 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -5,66 +5,56 @@ * FIXME: this code is crap, should use Pager and Database::select(). */ -/** - * - */ function wfSpecialNewimages( $par, $specialPage ) { - global $wgUser, $wgOut, $wgLang, $wgRequest, $wgGroupPermissions, $wgMiserMode; + global $wgUser, $wgOut, $wgLang, $wgRequest, $wgMiserMode; $wpIlMatch = $wgRequest->getText( 'wpIlMatch' ); $dbr = wfGetDB( DB_SLAVE ); $sk = $wgUser->getSkin(); $shownav = !$specialPage->including(); - $hidebots = $wgRequest->getBool('hidebots',1); + $hidebots = $wgRequest->getBool( 'hidebots' , 1 ); $hidebotsql = ''; - if ($hidebots) { - - /** Make a list of group names which have the 'bot' flag - set. - */ - $botconds=array(); - foreach ($wgGroupPermissions as $groupname=>$perms) { - if(array_key_exists('bot',$perms) && $perms['bot']) { - $botconds[]="ug_group='$groupname'"; - } + if ( $hidebots ) { + # Make a list of group names which have the 'bot' flag set. + $botconds = array(); + foreach ( User::getGroupsWithPermission('bot') as $groupname ) { + $botconds[] = 'ug_group = ' . $dbr->addQuotes( $groupname ); } - /* If not bot groups, do not set $hidebotsql */ - if ($botconds) { - $isbotmember=$dbr->makeList($botconds, LIST_OR); - - /** This join, in conjunction with WHERE ug_group - IS NULL, returns only those rows from IMAGE - where the uploading user is not a member of - a group which has the 'bot' permission set. - */ - $ug = $dbr->tableName('user_groups'); - $hidebotsql = " LEFT OUTER JOIN $ug ON img_user=ug_user AND ($isbotmember)"; + # If not bot groups, do not set $hidebotsql + if ( $botconds ) { + $isbotmember = $dbr->makeList( $botconds, LIST_OR ); + + # This join, in conjunction with WHERE ug_group IS NULL, returns + # only those rows from IMAGE where the uploading user is not a mem- + # ber of a group which has the 'bot' permission set. + $ug = $dbr->tableName( 'user_groups' ); + $hidebotsql = " LEFT JOIN $ug ON img_user=ug_user AND ($isbotmember)"; } } - $image = $dbr->tableName('image'); + $image = $dbr->tableName( 'image' ); - $sql="SELECT img_timestamp from $image"; + $sql = "SELECT img_timestamp from $image"; if ($hidebotsql) { $sql .= "$hidebotsql WHERE ug_group IS NULL"; } - $sql.=' ORDER BY img_timestamp DESC LIMIT 1'; - $res = $dbr->query($sql, 'wfSpecialNewImages'); - $row = $dbr->fetchRow($res); - if($row!==false) { - $ts=$row[0]; + $sql .= ' ORDER BY img_timestamp DESC LIMIT 1'; + $res = $dbr->query( $sql, __FUNCTION__ ); + $row = $dbr->fetchRow( $res ); + if( $row !== false ) { + $ts = $row[0]; } else { - $ts=false; + $ts = false; } - $dbr->freeResult($res); - $sql=''; + $dbr->freeResult( $res ); + $sql = ''; - /** If we were clever, we'd use this to cache. */ - $latestTimestamp = wfTimestamp( TS_MW, $ts); + # If we were clever, we'd use this to cache. + $latestTimestamp = wfTimestamp( TS_MW, $ts ); - /** Hardcode this for now. */ + # Hardcode this for now. $limit = 48; if ( $parval = intval( $par ) ) { @@ -77,10 +67,8 @@ function wfSpecialNewimages( $par, $specialPage ) { $searchpar = ''; if ( $wpIlMatch != '' && !$wgMiserMode) { $nt = Title::newFromUrl( $wpIlMatch ); - if($nt ) { - $m = $dbr->strencode( strtolower( $nt->getDBkey() ) ); - $m = str_replace( '%', "\\%", $m ); - $m = str_replace( '_', "\\_", $m ); + if( $nt ) { + $m = $dbr->escapeLike( strtolower( $nt->getDBkey() ) ); $where[] = "LOWER(img_name) LIKE '%{$m}%'"; $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch ); } @@ -97,16 +85,16 @@ function wfSpecialNewimages( $par, $specialPage ) { $sql='SELECT img_size, img_name, img_user, img_user_text,'. "img_description,img_timestamp FROM $image"; - if($hidebotsql) { + if( $hidebotsql ) { $sql .= $hidebotsql; - $where[]='ug_group IS NULL'; + $where[] = 'ug_group IS NULL'; } - if(count($where)) { - $sql.=' WHERE '.$dbr->makeList($where, LIST_AND); + if( count( $where ) ) { + $sql .= ' WHERE ' . $dbr->makeList( $where, LIST_AND ); } $sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' ); - $sql.=' LIMIT '.($limit+1); - $res = $dbr->query($sql, 'wfSpecialNewImages'); + $sql.=' LIMIT ' . ( $limit + 1 ); + $res = $dbr->query( $sql, __FUNCTION__ ); /** * We have to flip things around to get the last N after a certain date @@ -126,7 +114,8 @@ function wfSpecialNewimages( $par, $specialPage ) { $lastTimestamp = null; $shownImages = 0; foreach( $images as $s ) { - if( ++$shownImages > $limit ) { + $shownImages++; + if( $shownImages > $limit ) { # One extra just to test for whether to show a page link; # don't actually show it. break; @@ -135,7 +124,7 @@ function wfSpecialNewimages( $par, $specialPage ) { $name = $s->img_name; $ut = $s->img_user_text; - $nt = Title::newFromText( $name, NS_IMAGE ); + $nt = Title::newFromText( $name, NS_FILE ); $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut ); $gallery->add( $nt, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" ); @@ -147,33 +136,35 @@ function wfSpecialNewimages( $par, $specialPage ) { $lastTimestamp = $timestamp; } + $titleObj = SpecialPage::getTitleFor( 'Newimages' ); + $action = $titleObj->getLocalURL( $hidebots ? '' : 'hidebots=0' ); + if ( $shownav && !$wgMiserMode ) { + $wgOut->addHTML( + Xml::openElement( 'form', array( 'action' => $action, 'method' => 'post', 'id' => 'imagesearch' ) ) . + Xml::fieldset( wfMsg( 'newimages-legend' ) ) . + Xml::inputLabel( wfMsg( 'newimages-label' ), 'wpIlMatch', 'wpIlMatch', 20, $wpIlMatch ) . ' ' . + Xml::submitButton( wfMsg( 'ilsubmit' ), array( 'name' => 'wpIlSubmit' ) ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ) + ); + } + $bydate = wfMsg( 'bydate' ); $lt = $wgLang->formatNum( min( $shownImages, $limit ) ); - if ($shownav) { + if ( $shownav ) { $text = wfMsgExt( 'imagelisttext', array('parse'), $lt, $bydate ); $wgOut->addHTML( $text . "\n" ); } - $sub = wfMsg( 'ilsubmit' ); - $titleObj = SpecialPage::getTitleFor( 'Newimages' ); - $action = $titleObj->escapeLocalURL( $hidebots ? '' : 'hidebots=0' ); - if ($shownav && !$wgMiserMode) { - $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" . - "{$action}\">" . - Xml::input( 'wpIlMatch', 20, $wpIlMatch ) . ' ' . - Xml::submitButton( $sub, array( 'name' => 'wpIlSubmit' ) ) . - "</form>" ); - } - /** * Paging controls... */ # If we change bot visibility, this needs to be carried along. - if(!$hidebots) { - $botpar='&hidebots=0'; + if( !$hidebots ) { + $botpar = '&hidebots=0'; } else { - $botpar=''; + $botpar = ''; } $now = wfTimestampNow(); $d = $wgLang->date( $now, true ); @@ -186,12 +177,12 @@ function wfSpecialNewimages( $par, $specialPage ) { $opts = array( 'parsemag', 'escapenoentities' ); - $prevLink = wfMsgExt( 'prevn', $opts, $wgLang->formatNum( $limit ) ); + $prevLink = wfMsgExt( 'pager-newer-n', $opts, $wgLang->formatNum( $limit ) ); if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) { $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar ); } - $nextLink = wfMsgExt( 'nextn', $opts, $wgLang->formatNum( $limit ) ); + $nextLink = wfMsgExt( 'pager-older-n', $opts, $wgLang->formatNum( $limit ) ); if( $shownImages > $limit && $lastTimestamp ) { $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar ); } diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 1a410ae0..08e776d8 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -12,7 +12,7 @@ class SpecialNewpages extends SpecialPage { // Some internal settings protected $showNavigation = false; - public function __construct(){ + public function __construct() { parent::__construct( 'Newpages' ); $this->includable( true ); } @@ -26,7 +26,8 @@ class SpecialNewpages extends SpecialPage { $opts->add( 'hideliu', false ); $opts->add( 'hidepatrolled', false ); $opts->add( 'hidebots', false ); - $opts->add( 'limit', 50 ); + $opts->add( 'hideredirs', true ); + $opts->add( 'limit', (int)$wgUser->getOption( 'rclimit' ) ); $opts->add( 'offset', '' ); $opts->add( 'namespace', '0' ); $opts->add( 'username', '' ); @@ -58,6 +59,8 @@ class SpecialNewpages extends SpecialPage { $this->opts->setValue( 'hidepatrolled', true ); if ( 'hidebots' == $bit ) $this->opts->setValue( 'hidebots', true ); + if ( 'showredirs' == $bit ) + $this->opts->setValue( 'hideredirs', false ); if ( is_numeric( $bit ) ) $this->opts->setValue( 'limit', intval( $bit ) ); @@ -67,6 +70,8 @@ class SpecialNewpages extends SpecialPage { // PG offsets not just digits! if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) $this->opts->setValue( 'offset', intval($m[1]) ); + if ( preg_match( '/^username=(.*)$/', $bit, $m ) ) + $this->opts->setValue( 'username', $m[1] ); if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) { $ns = $wgLang->getNsIndex( $m[1] ); if( $ns !== false ) { @@ -83,7 +88,7 @@ class SpecialNewpages extends SpecialPage { * @return string */ public function execute( $par ) { - global $wgLang, $wgGroupPermissions, $wgUser, $wgOut; + global $wgLang, $wgUser, $wgOut; $this->setHeaders(); $this->outputHeader(); @@ -125,7 +130,8 @@ class SpecialNewpages extends SpecialPage { $filters = array( 'hideliu' => 'rcshowhideliu', 'hidepatrolled' => 'rcshowhidepatr', - 'hidebots' => 'rcshowhidebots' + 'hidebots' => 'rcshowhidebots', + 'hideredirs' => 'whatlinkshere-hideredirs' ); // Disable some if needed @@ -142,8 +148,8 @@ class SpecialNewpages extends SpecialPage { $self = $this->getTitle(); foreach ( $filters as $key => $msg ) { $onoff = 1 - $this->opts->getValue($key); - $link = $this->skin->makeKnownLinkObj( $self, $showhide[$onoff], - wfArrayToCGI( array( $key => $onoff ), $changed ) + $link = $this->skin->link( $self, $showhide[$onoff], array(), + array( $key => $onoff ) + $changed ); $links[$key] = wfMsgHtml( $msg, $link ); } @@ -231,7 +237,7 @@ class SpecialNewpages extends SpecialPage { global $wgLang, $wgContLang, $wgUser; $dm = $wgContLang->getDirMark(); - $title = Title::makeTitleSafe( $result->page_namespace, $result->page_title ); + $title = Title::makeTitleSafe( $result->rc_namespace, $result->rc_title ); $time = $wgLang->timeAndDate( $result->rc_timestamp, true ); $plink = $this->skin->makeKnownLinkObj( $title, '', $this->patrollable( $result ) ? 'rcid=' . $result->rc_id : '' ); $hist = $this->skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' ); @@ -261,7 +267,7 @@ class SpecialNewpages extends SpecialPage { * @param string $type */ protected function feed( $type ) { - global $wgFeed, $wgFeedClasses; + global $wgFeed, $wgFeedClasses, $wgFeedLimit; if ( !$wgFeed ) { global $wgOut; @@ -277,16 +283,12 @@ class SpecialNewpages extends SpecialPage { $feed = new $wgFeedClasses[$type]( $this->feedTitle(), - wfMsg( 'tagline' ), + wfMsgExt( 'tagline', 'parsemag' ), $this->getTitle()->getFullUrl() ); $pager = new NewPagesPager( $this, $this->opts ); $limit = $this->opts->getValue( 'limit' ); - global $wgFeedLimit; - if( $limit > $wgFeedLimit ) { - $limit = $wgFeedLimit; - } - $pager->mLimit = $limit; + $pager->mLimit = min( $limit, $wgFeedLimit ); $feed->outHeader(); if( $pager->getNumRows() > 0 ) { @@ -305,7 +307,7 @@ class SpecialNewpages extends SpecialPage { } protected function feedItem( $row ) { - $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title ); + $title = Title::MakeTitle( intval( $row->rc_namespace ), $row->rc_title ); if( $title ) { $date = $row->rc_timestamp; $comments = $title->getTalkPage()->getFullURL(); @@ -322,13 +324,6 @@ class SpecialNewpages extends SpecialPage { } } - /** - * Quickie hack... strip out wikilinks to more legible form from the comment. - */ - protected function stripComment( $text ) { - return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); - } - protected function feedItemAuthor( $row ) { return isset( $row->rc_user_text ) ? $row->rc_user_text : ''; } @@ -337,7 +332,7 @@ class SpecialNewpages extends SpecialPage { $revision = Revision::newFromId( $row->rev_id ); if( $revision ) { return '<p>' . htmlspecialchars( $revision->getUserText() ) . ': ' . - htmlspecialchars( $revision->getComment() ) . + htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . "</p>\n<hr />\n<div>" . nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>"; } @@ -352,15 +347,13 @@ class NewPagesPager extends ReverseChronologicalPager { // Stored opts protected $opts, $mForm; - private $hideliu, $hidepatrolled, $hidebots, $namespace, $user, $spTitle; - function __construct( $form, FormOptions $opts ) { parent::__construct(); $this->mForm = $form; $this->opts = $opts; } - function getTitle(){ + function getTitle() { static $title = null; if ( $title === null ) $title = $this->mForm->getTitle(); @@ -379,13 +372,13 @@ class NewPagesPager extends ReverseChronologicalPager { $user = Title::makeTitleSafe( NS_USER, $username ); if( $namespace !== false ) { - $conds['page_namespace'] = $namespace; + $conds['rc_namespace'] = $namespace; $rcIndexes = array( 'new_name_timestamp' ); } else { $rcIndexes = array( 'rc_timestamp' ); } $conds[] = 'page_id = rc_cur_id'; - $conds['page_is_redirect'] = 0; + # $wgEnableNewpagesUserFilter - temp WMF hack if( $wgEnableNewpagesUserFilter && $user ) { $conds['rc_user_text'] = $user->getText(); @@ -402,9 +395,13 @@ class NewPagesPager extends ReverseChronologicalPager { $conds['rc_bot'] = 0; } + if ( $this->opts->getValue( 'hideredirs' ) ) { + $conds['page_is_redirect'] = 0; + } + return array( 'tables' => array( 'recentchanges', 'page' ), - 'fields' => 'page_namespace,page_title, rc_cur_id, rc_user,rc_user_text,rc_comment, + 'fields' => 'rc_namespace,rc_title, rc_cur_id, rc_user,rc_user_text,rc_comment, rc_timestamp,rc_patrolled,rc_id,page_len as length, page_latest as rev_id', 'conds' => $conds, 'options' => array( 'USE INDEX' => array('recentchanges' => $rcIndexes) ) @@ -425,7 +422,7 @@ class NewPagesPager extends ReverseChronologicalPager { while( $row = $this->mResult->fetchObject() ) { $linkBatch->add( NS_USER, $row->rc_user_text ); $linkBatch->add( NS_USER_TALK, $row->rc_user_text ); - $linkBatch->add( $row->page_namespace, $row->page_title ); + $linkBatch->add( $row->rc_namespace, $row->rc_title ); } $linkBatch->execute(); return "<ul>"; diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index b3468a3c..ca2236ee 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -21,11 +21,11 @@ function wfSpecialPreferences() { * @ingroup SpecialPage */ class PreferencesForm { - var $mQuickbar, $mOldpass, $mNewpass, $mRetypePass, $mStubs; + var $mQuickbar, $mStubs; var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick; var $mUserLanguage, $mUserVariant; - var $mSearch, $mRecent, $mRecentDays, $mHourDiff, $mSearchLines, $mSearchChars, $mAction; - var $mReset, $mPosted, $mToggles, $mUseAjaxSearch, $mSearchNs, $mRealName, $mImageSize; + var $mSearch, $mRecent, $mRecentDays, $mTimeZone, $mHourDiff, $mSearchLines, $mSearchChars, $mAction; + var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize; var $mUnderline, $mWatchlistEdits; /** @@ -36,13 +36,10 @@ class PreferencesForm { global $wgContLang, $wgUser, $wgAllowRealName; $this->mQuickbar = $request->getVal( 'wpQuickbar' ); - $this->mOldpass = $request->getVal( 'wpOldpass' ); - $this->mNewpass = $request->getVal( 'wpNewpass' ); - $this->mRetypePass =$request->getVal( 'wpRetypePass' ); $this->mStubs = $request->getVal( 'wpStubs' ); $this->mRows = $request->getVal( 'wpRows' ); $this->mCols = $request->getVal( 'wpCols' ); - $this->mSkin = $request->getVal( 'wpSkin' ); + $this->mSkin = Skin::normalizeKey( $request->getVal( 'wpSkin' ) ); $this->mMath = $request->getVal( 'wpMath' ); $this->mDate = $request->getVal( 'wpDate' ); $this->mUserEmail = $request->getVal( 'wpUserEmail' ); @@ -54,6 +51,7 @@ class PreferencesForm { $this->mSearch = $request->getVal( 'wpSearch' ); $this->mRecent = $request->getVal( 'wpRecent' ); $this->mRecentDays = $request->getVal( 'wpRecentDays' ); + $this->mTimeZone = $request->getVal( 'wpTimeZone' ); $this->mHourDiff = $request->getVal( 'wpHourDiff' ); $this->mSearchLines = $request->getVal( 'wpSearchLines' ); $this->mSearchChars = $request->getVal( 'wpSearchChars' ); @@ -66,7 +64,6 @@ class PreferencesForm { $this->mSuccess = $request->getCheck( 'success' ); $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' ); $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' ); - $this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' ); $this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' ); $this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) && @@ -105,10 +102,10 @@ class PreferencesForm { } function execute() { - global $wgUser, $wgOut; + global $wgUser, $wgOut, $wgTitle; if ( $wgUser->isAnon() ) { - $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext' ); + $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext', array($wgTitle->getPrefixedDBkey()) ); return; } if ( wfReadOnly() ) { @@ -174,34 +171,37 @@ class PreferencesForm { /** * Used to validate the user inputed timezone before saving it as - * 'timecorrection', will return '00:00' if fed bogus data. - * Note: It's not a 100% correct implementation timezone-wise, it will - * accept stuff like '14:30', + * 'timecorrection', will return 'System' if fed bogus data. * @access private - * @param string $s the user input + * @param string $tz the user input Zoneinfo timezone + * @param string $s the user input offset string * @return string */ - function validateTimeZone( $s ) { - if ( $s !== '' ) { - if ( strpos( $s, ':' ) ) { - # HH:MM - $array = explode( ':' , $s ); - $hour = intval( $array[0] ); - $minute = intval( $array[1] ); - } else { - $minute = intval( $s * 60 ); - $hour = intval( $minute / 60 ); - $minute = abs( $minute ) % 60; - } - # Max is +14:00 and min is -12:00, see: - # http://en.wikipedia.org/wiki/Timezone - $hour = min( $hour, 14 ); - $hour = max( $hour, -12 ); - $minute = min( $minute, 59 ); - $minute = max( $minute, 0 ); - $s = sprintf( "%02d:%02d", $hour, $minute ); + function validateTimeZone( $tz, $s ) { + $data = explode( '|', $tz, 3 ); + switch ( $data[0] ) { + case 'ZoneInfo': + case 'System': + return $tz; + case 'Offset': + default: + $data = explode( ':', $s, 2 ); + $minDiff = 0; + if( count( $data ) == 2 ) { + $data[0] = intval( $data[0] ); + $data[1] = intval( $data[1] ); + $minDiff = abs( $data[0] ) * 60 + $data[1]; + if ( $data[0] < 0 ) $minDiff = -$minDiff; + } else { + $minDiff = intval( $data[0] ) * 60; + } + + # Max is +14:00 and min is -12:00, see: + # http://en.wikipedia.org/wiki/Timezone + $minDiff = min( $minDiff, 840 ); # 14:00 + $minDiff = max( $minDiff, -720 ); # -12:00 + return 'Offset|'.$minDiff; } - return $s; } /** @@ -213,30 +213,6 @@ class PreferencesForm { global $wgEmailAuthentication, $wgRCMaxAge; global $wgAuth, $wgEmailConfirmToEdit; - - if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) { - if ( $this->mNewpass != $this->mRetypePass ) { - wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'badretype' ) ); - $this->mainPrefsForm( 'error', wfMsg( 'badretype' ) ); - return; - } - - if (!$wgUser->checkPassword( $this->mOldpass )) { - wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'wrongpassword' ) ); - $this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) ); - return; - } - - try { - $wgUser->setPassword( $this->mNewpass ); - wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'success' ) ); - $this->mNewpass = $this->mOldpass = $this->mRetypePass = ''; - } catch( PasswordError $e ) { - wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'error' ) ); - $this->mainPrefsForm( 'error', $e->getMessage() ); - return; - } - } $wgUser->setRealName( $this->mRealName ); $oldOptions = $wgUser->mOptions; @@ -269,7 +245,10 @@ class PreferencesForm { $wgUser->setOption( 'variant', $this->mUserVariant ); $wgUser->setOption( 'nickname', $this->mNick ); $wgUser->setOption( 'quickbar', $this->mQuickbar ); - $wgUser->setOption( 'skin', $this->mSkin ); + global $wgAllowUserSkin; + if( $wgAllowUserSkin ) { + $wgUser->setOption( 'skin', $this->mSkin ); + } global $wgUseTeX; if( $wgUseTeX ) { $wgUser->setOption( 'math', $this->mMath ); @@ -284,12 +263,11 @@ class PreferencesForm { $wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) ); $wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) ); $wgUser->setOption( 'stubthreshold', $this->validateIntOrNull( $this->mStubs ) ); - $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mHourDiff, -12, 14 ) ); + $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mTimeZone, $this->mHourDiff ) ); $wgUser->setOption( 'imagesize', $this->mImageSize ); $wgUser->setOption( 'thumbsize', $this->mThumbSize ); $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) ); $wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) ); - $wgUser->setOption( 'ajaxsearch', $this->mUseAjaxSearch ); $wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest ); # Set search namespace options @@ -370,9 +348,8 @@ class PreferencesForm { * @access private */ function resetPrefs() { - global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName; + global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName, $wgLocalTZoffset; - $this->mOldpass = $this->mNewpass = $this->mRetypePass = ''; $this->mUserEmail = $wgUser->getEmail(); $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp(); $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : ''; @@ -391,7 +368,47 @@ class PreferencesForm { $this->mRows = $wgUser->getOption( 'rows' ); $this->mCols = $wgUser->getOption( 'cols' ); $this->mStubs = $wgUser->getOption( 'stubthreshold' ); - $this->mHourDiff = $wgUser->getOption( 'timecorrection' ); + + $tz = $wgUser->getOption( 'timecorrection' ); + $data = explode( '|', $tz, 3 ); + $minDiff = null; + switch ( $data[0] ) { + case 'ZoneInfo': + $this->mTimeZone = $tz; + # Check if the specified TZ exists, and change to 'Offset' if + # not. + if ( !function_exists('timezone_open') || @timezone_open( $data[2] ) === false ) { + $this->mTimeZone = 'Offset'; + $minDiff = intval( $data[1] ); + } + break; + case '': + case 'System': + $this->mTimeZone = 'System|'.$wgLocalTZoffset; + break; + case 'Offset': + $this->mTimeZone = 'Offset'; + $minDiff = intval( $data[1] ); + break; + default: + $this->mTimeZone = 'Offset'; + $data = explode( ':', $tz, 2 ); + if( count( $data ) == 2 ) { + $data[0] = intval( $data[0] ); + $data[1] = intval( $data[1] ); + $minDiff = abs( $data[0] ) * 60 + $data[1]; + if ( $data[0] < 0 ) $minDiff = -$minDiff; + } else { + $minDiff = intval( $data[0] ) * 60; + } + break; + } + if ( is_null( $minDiff ) ) { + $this->mHourDiff = ''; + } else { + $this->mHourDiff = sprintf( '%+03d:%02d', floor($minDiff/60), abs($minDiff)%60 ); + } + $this->mSearch = $wgUser->getOption( 'searchlimit' ); $this->mSearchLines = $wgUser->getOption( 'contextlines' ); $this->mSearchChars = $wgUser->getOption( 'contextchars' ); @@ -402,7 +419,6 @@ class PreferencesForm { $this->mWatchlistEdits = $wgUser->getOption( 'wllimit' ); $this->mUnderline = $wgUser->getOption( 'underline' ); $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' ); - $this->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' ); $this->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' ); $togs = User::getToggles(); @@ -511,18 +527,18 @@ class PreferencesForm { * @access private */ function mainPrefsForm( $status , $message = '' ) { - global $wgUser, $wgOut, $wgLang, $wgContLang; + global $wgUser, $wgOut, $wgLang, $wgContLang, $wgAuth; global $wgAllowRealName, $wgImageLimits, $wgThumbLimits; - global $wgDisableLangConversion; + global $wgDisableLangConversion, $wgDisableTitleConversion; global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits; global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress; global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication; - global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth; - global $wgEmailConfirmToEdit, $wgAjaxSearch, $wgEnableMWSuggest; + global $wgContLanguageCode, $wgDefaultSkin, $wgCookieExpiration; + global $wgEmailConfirmToEdit, $wgEnableMWSuggest, $wgLocalTZoffset; $wgOut->setPageTitle( wfMsg( 'preferences' ) ); $wgOut->setArticleRelated( false ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addScriptFile( 'prefs.js' ); $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc. @@ -536,7 +552,6 @@ class PreferencesForm { } $qbs = $wgLang->getQuickbarSettings(); - $skinNames = $wgLang->getSkinNames(); $mathopts = $wgLang->getMathNames(); $dateopts = $wgLang->getDatePreferences(); $togs = User::getToggles(); @@ -552,6 +567,7 @@ class PreferencesForm { $this->mUsedToggles[ 'enotifrevealaddr' ] = true; $this->mUsedToggles[ 'ccmeonemails' ] = true; $this->mUsedToggles[ 'uselivepreview' ] = true; + $this->mUsedToggles[ 'noconvertlink' ] = true; if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; } @@ -560,7 +576,13 @@ class PreferencesForm { if ($wgEmailAuthentication && ($this->mUserEmail != '') ) { if( $wgUser->getEmailAuthenticationTimestamp() ) { - $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'<br />'; + // date and time are separate parameters to facilitate localisation. + // $time is kept for backward compat reasons. + // 'emailauthenticated' is also used in SpecialConfirmemail.php + $time = $wgLang->timeAndDate( $wgUser->getEmailAuthenticationTimestamp(), true ); + $d = $wgLang->date( $wgUser->getEmailAuthenticationTimestamp(), true ); + $t = $wgLang->time( $wgUser->getEmailAuthenticationTimestamp(), true ); + $emailauthenticated = wfMsg('emailauthenticated', $time, $d, $t ).'<br />'; $disableEmailPrefs = false; } else { $disableEmailPrefs = true; @@ -620,26 +642,26 @@ class PreferencesForm { $toolLinks = array(); $toolLinks[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'ListGroupRights' ), wfMsg( 'listgrouprights' ) ); # At the moment one tool link only but be prepared for the future... - # FIXME: Add a link to Special:Userrights for users who are allowed to use it. + # FIXME: Add a link to Special:Userrights for users who are allowed to use it. # $wgUser->isAllowed( 'userrights' ) seems to strict in some cases $userInformationHtml = $this->tableRow( wfMsgHtml( 'username' ), htmlspecialchars( $wgUser->getName() ) ) . - $this->tableRow( wfMsgHtml( 'uid' ), htmlspecialchars( $wgUser->getId() ) ) . + $this->tableRow( wfMsgHtml( 'uid' ), $wgLang->formatNum( htmlspecialchars( $wgUser->getId() ) ) ). $this->tableRow( wfMsgExt( 'prefs-memberingroups', array( 'parseinline' ), count( $userEffectiveGroupsArray ) ), - implode( wfMsg( 'comma-separator' ), $userEffectiveGroupsArray ) . + $wgLang->commaList( $userEffectiveGroupsArray ) . '<br />(' . implode( ' | ', $toolLinks ) . ')' ) . $this->tableRow( wfMsgHtml( 'prefs-edits' ), - $wgLang->formatNum( User::edits( $wgUser->getId() ) ) + $wgLang->formatNum( $wgUser->getEditCount() ) ); if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) { - $wgOut->addHtml( $userInformationHtml ); + $wgOut->addHTML( $userInformationHtml ); } if ( $wgAllowRealName ) { @@ -724,7 +746,7 @@ class PreferencesForm { } if(count($variantArray) > 1) { - $wgOut->addHtml( + $wgOut->addHTML( $this->tableRow( Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ), Xml::tags( 'select', @@ -734,30 +756,25 @@ class PreferencesForm { ) ); } + + if(count($variantArray) > 1 && !$wgDisableLangConversion && !$wgDisableTitleConversion) { + $wgOut->addHTML( + Xml::tags( 'tr', null, + Xml::tags( 'td', array( 'colspan' => '2' ), + $this->getToggle( "noconvertlink" ) + ) + ) + ); + } } # Password if( $wgAuth->allowPasswordChange() ) { + $link = $wgUser->getSkin()->link( SpecialPage::getTitleFor( 'ResetPass' ), wfMsgHtml( 'prefs-resetpass' ), + array() , array('returnto' => SpecialPage::getTitleFor( 'Preferences') ) ); $wgOut->addHTML( $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) . - $this->tableRow( - Xml::label( wfMsg( 'oldpassword' ), 'wpOldpass' ), - Xml::password( 'wpOldpass', 25, $this->mOldpass, array( 'id' => 'wpOldpass' ) ) - ) . - $this->tableRow( - Xml::label( wfMsg( 'newpassword' ), 'wpNewpass' ), - Xml::password( 'wpNewpass', 25, $this->mNewpass, array( 'id' => 'wpNewpass' ) ) - ) . - $this->tableRow( - Xml::label( wfMsg( 'retypenew' ), 'wpRetypePass' ), - Xml::password( 'wpRetypePass', 25, $this->mRetypePass, array( 'id' => 'wpRetypePass' ) ) - ) . - Xml::tags( 'tr', null, - Xml::tags( 'td', array( 'colspan' => '2' ), - $this->getToggle( "rememberpassword" ) - ) - ) - ); + $this->tableRow( '<ul><li>' . $link . '</li></ul>' ) ); } # <FIXME> @@ -799,48 +816,49 @@ class PreferencesForm { # Quickbar # if ($this->mSkin == 'cologneblue' || $this->mSkin == 'standard') { - $wgOut->addHtml( "<fieldset>\n<legend>" . wfMsg( 'qbsettings' ) . "</legend>\n" ); + $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg( 'qbsettings' ) . "</legend>\n" ); for ( $i = 0; $i < count( $qbs ); ++$i ) { if ( $i == $this->mQuickbar ) { $checked = ' checked="checked"'; } else { $checked = ""; } $wgOut->addHTML( "<div><label><input type='radio' name='wpQuickbar' value=\"$i\"$checked />{$qbs[$i]}</label></div>\n" ); } - $wgOut->addHtml( "</fieldset>\n\n" ); + $wgOut->addHTML( "</fieldset>\n\n" ); } else { # Need to output a hidden option even if the relevant skin is not in use, # otherwise the preference will get reset to 0 on submit - $wgOut->addHtml( wfHidden( 'wpQuickbar', $this->mQuickbar ) ); + $wgOut->addHTML( Xml::hidden( 'wpQuickbar', $this->mQuickbar ) ); } # Skin # - $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg('skin') . "</legend>\n" ); - $mptitle = Title::newMainPage(); - $previewtext = wfMsg('skinpreview'); - # Only show members of Skin::getSkinNames() rather than - # $skinNames (skins is all skin names from Language.php) - $validSkinNames = Skin::getSkinNames(); - # Sort by UI skin name. First though need to update validSkinNames as sometimes - # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI). - foreach ($validSkinNames as $skinkey => & $skinname ) { - if ( isset( $skinNames[$skinkey] ) ) { - $skinname = $skinNames[$skinkey]; + global $wgAllowUserSkin; + if( $wgAllowUserSkin ) { + $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg( 'skin' ) . "</legend>\n" ); + $mptitle = Title::newMainPage(); + $previewtext = wfMsg( 'skin-preview' ); + # Only show members of Skin::getSkinNames() rather than + # $skinNames (skins is all skin names from Language.php) + $validSkinNames = Skin::getUsableSkins(); + # Sort by UI skin name. First though need to update validSkinNames as sometimes + # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI). + foreach ( $validSkinNames as $skinkey => &$skinname ) { + $msgName = "skinname-{$skinkey}"; + $localisedSkinName = wfMsg( $msgName ); + if ( !wfEmptyMsg( $msgName, $localisedSkinName ) ) { + $skinname = $localisedSkinName; + } } - } - asort($validSkinNames); - foreach ($validSkinNames as $skinkey => $sn ) { - if ( in_array( $skinkey, $wgSkipSkins ) ) { - continue; + asort($validSkinNames); + foreach ($validSkinNames as $skinkey => $sn ) { + $checked = $skinkey == $this->mSkin ? ' checked="checked"' : ''; + $mplink = htmlspecialchars( $mptitle->getLocalURL( "useskin=$skinkey" ) ); + $previewlink = "(<a target='_blank' href=\"$mplink\">$previewtext</a>)"; + if( $skinkey == $wgDefaultSkin ) + $sn .= ' (' . wfMsg( 'default' ) . ')'; + $wgOut->addHTML( "<input type='radio' name='wpSkin' id=\"wpSkin$skinkey\" value=\"$skinkey\"$checked /> <label for=\"wpSkin$skinkey\">{$sn}</label> $previewlink<br />\n" ); } - $checked = $skinkey == $this->mSkin ? ' checked="checked"' : ''; - - $mplink = htmlspecialchars($mptitle->getLocalURL("useskin=$skinkey")); - $previewlink = "<a target='_blank' href=\"$mplink\">$previewtext</a>"; - if( $skinkey == $wgDefaultSkin ) - $sn .= ' (' . wfMsg( 'default' ) . ')'; - $wgOut->addHTML( "<input type='radio' name='wpSkin' id=\"wpSkin$skinkey\" value=\"$skinkey\"$checked /> <label for=\"wpSkin$skinkey\">{$sn}</label> $previewlink<br />\n" ); + $wgOut->addHTML( "</fieldset>\n\n" ); } - $wgOut->addHTML( "</fieldset>\n\n" ); # Math # @@ -860,10 +878,6 @@ class PreferencesForm { # Files # - $wgOut->addHTML( - "<fieldset>\n" . Xml::element( 'legend', null, wfMsg( 'files' ) ) . "\n" - ); - $imageLimitOptions = null; foreach ( $wgImageLimits as $index => $limits ) { $selected = ($index == $this->mImageSize); @@ -871,14 +885,6 @@ class PreferencesForm { wfMsg('unit-pixel'), $index, $selected ); } - $imageSizeId = 'wpImageSize'; - $wgOut->addHTML( - "<div>" . Xml::label( wfMsg('imagemaxsize'), $imageSizeId ) . " " . - Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) . - $imageLimitOptions . - Xml::closeElement( 'select' ) . "</div>\n" - ); - $imageThumbOptions = null; foreach ( $wgThumbLimits as $index => $size ) { $selected = ($index == $this->mThumbSize); @@ -886,16 +892,34 @@ class PreferencesForm { $selected); } + $imageSizeId = 'wpImageSize'; $thumbSizeId = 'wpThumbSize'; $wgOut->addHTML( - "<div>" . Xml::label( wfMsg('thumbsize'), $thumbSizeId ) . " " . - Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) . - $imageThumbOptions . - Xml::closeElement( 'select' ) . "</div>\n" + Xml::fieldset( wfMsg( 'files' ) ) . "\n" . + Xml::openElement( 'table' ) . + '<tr> + <td class="mw-label">' . + Xml::label( wfMsg( 'imagemaxsize' ), $imageSizeId ) . + '</td> + <td class="mw-input">' . + Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) . + $imageLimitOptions . + Xml::closeElement( 'select' ) . + '</td> + </tr><tr> + <td class="mw-label">' . + Xml::label( wfMsg( 'thumbsize' ), $thumbSizeId ) . + '</td> + <td class="mw-input">' . + Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) . + $imageThumbOptions . + Xml::closeElement( 'select' ) . + '</td> + </tr>' . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) ); - $wgOut->addHTML( "</fieldset>\n\n" ); - # Date format # # Date/Time @@ -929,18 +953,61 @@ class PreferencesForm { $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . "\n" ); } - $nowlocal = $wgLang->time( $now = wfTimestampNow(), true ); - $nowserver = $wgLang->time( $now, false ); + $nowlocal = Xml::openElement( 'span', array( 'id' => 'wpLocalTime' ) ) . + $wgLang->time( $now = wfTimestampNow(), true ) . + Xml::closeElement( 'span' ); + $nowserver = $wgLang->time( $now, false ) . + Xml::hidden( 'wpServerTime', substr( $now, 8, 2 ) * 60 + substr( $now, 10, 2 ) ); $wgOut->addHTML( Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'timezonelegend' ) ) . Xml::openElement( 'table' ) . $this->addRow( wfMsg( 'servertime' ), $nowserver ) . - $this->addRow( wfMsg( 'localtime' ), $nowlocal ) . + $this->addRow( wfMsg( 'localtime' ), $nowlocal ) + ); + $opt = Xml::openElement( 'select', array( + 'name' => 'wpTimeZone', + 'id' => 'wpTimeZone', + 'onchange' => 'javascript:updateTimezoneSelection(false)' ) ); + $opt .= Xml::option( wfMsg( 'timezoneuseserverdefault' ), "System|$wgLocalTZoffset", $this->mTimeZone === "System|$wgLocalTZoffset" ); + $opt .= Xml::option( wfMsg( 'timezoneuseoffset' ), 'Offset', $this->mTimeZone === 'Offset' ); + if ( function_exists( 'timezone_identifiers_list' ) ) { + $optgroup = ''; + $tzs = timezone_identifiers_list(); + sort( $tzs ); + $selZone = explode( '|', $this->mTimeZone, 3); + $selZone = ( $selZone[0] == 'ZoneInfo' ) ? $selZone[2] : null; + $now = date_create( 'now' ); + foreach ( $tzs as $tz ) { + $z = explode( '/', $tz, 2 ); + # timezone_identifiers_list() returns a number of + # backwards-compatibility entries. This filters them out of the + # list presented to the user. + if ( count( $z ) != 2 || !in_array( $z[0], array( 'Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific' ) ) ) continue; + if ( $optgroup != $z[0] ) { + if ( $optgroup !== '' ) $opt .= Xml::closeElement( 'optgroup' ); + $optgroup = $z[0]; + $opt .= Xml::openElement( 'optgroup', array( 'label' => $z[0] ) ); + } + $minDiff = floor( timezone_offset_get( timezone_open( $tz ), $now ) / 60 ); + $opt .= Xml::option( str_replace( '_', ' ', $tz ), "ZoneInfo|$minDiff|$tz", $selZone === $tz, array( 'label' => $z[1] ) ); + } + if ( $optgroup !== '' ) $opt .= Xml::closeElement( 'optgroup' ); + } + $opt .= Xml::closeElement( 'select' ); + $wgOut->addHTML( + $this->addRow( + Xml::label( wfMsg( 'timezoneselect' ), 'wpTimeZone' ), + $opt ) + ); + $wgOut->addHTML( $this->addRow( Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff' ), - Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( 'id' => 'wpHourDiff' ) ) ) . + Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( + 'id' => 'wpHourDiff', + 'onfocus' => 'javascript:updateTimezoneSelection(true)', + 'onblur' => 'javascript:updateTimezoneSelection(false)' ) ) ) . "<tr> <td></td> <td class='mw-submit'>" . @@ -961,12 +1028,11 @@ class PreferencesForm { # Editing # global $wgLivePreview; - $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'textboxsize' ) . '</legend> - <div>' . - wfInputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) . - ' ' . - wfInputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) . - "</div>" . + $wgOut->addHTML( + Xml::fieldset( wfMsg( 'textboxsize' ) ) . + wfMsgHTML( 'prefs-edit-boxsize' ) . ' ' . + Xml::inputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) . ' ' . + Xml::inputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) . $this->getToggles( array( 'editsection', 'editsectiononrightclick', @@ -980,62 +1046,76 @@ class PreferencesForm { 'externaldiff', $wgLivePreview ? 'uselivepreview' : false, 'forceeditsummary', - ) ) . '</fieldset>' + ) ) ); - # Recent changes - $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-rc' ) . '</legend>' ); - - $rc = '<table><tr>'; - $rc .= '<td>' . Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . '</td>'; - $rc .= '<td>' . Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . '</td>'; - $rc .= '</tr><tr>'; - $rc .= '<td>' . Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . '</td>'; - $rc .= '<td>' . Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . '</td>'; - $rc .= '</tr></table>'; - $wgOut->addHtml( $rc ); + $wgOut->addHTML( Xml::closeElement( 'fieldset' ) ); - $wgOut->addHtml( '<br />' ); + # Recent changes + global $wgRCMaxAge; + $wgOut->addHTML( + Xml::fieldset( wfMsg( 'prefs-rc' ) ) . + Xml::openElement( 'table' ) . + '<tr> + <td class="mw-label">' . + Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . + '</td> + <td class="mw-input">' . + Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . ' ' . + wfMsgExt( 'recentchangesdays-max', 'parsemag', + $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600 * 24 ) ) ) ) . + '</td> + </tr><tr> + <td class="mw-label">' . + Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . + '</td> + <td class="mw-input">' . + Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . + '</td> + </tr>' . + Xml::closeElement( 'table' ) . + '<br />' + ); $toggles[] = 'hideminor'; if( $wgRCShowWatchingUsers ) $toggles[] = 'shownumberswatching'; $toggles[] = 'usenewrc'; - $wgOut->addHtml( $this->getToggles( $toggles ) ); - $wgOut->addHtml( '</fieldset>' ); + $wgOut->addHTML( + $this->getToggles( $toggles ) . + Xml::closeElement( 'fieldset' ) + ); # Watchlist - $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-watchlist' ) . '</legend>' ); - - $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) ); - $wgOut->addHtml( '<br /><br />' ); - - $wgOut->addHtml( $this->getToggle( 'extendwatchlist' ) ); - $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) ); - $wgOut->addHtml( '<br /><br />' ); + $wgOut->addHTML( + Xml::fieldset( wfMsg( 'prefs-watchlist' ) ) . + Xml::inputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) . ' ' . + wfMsgHTML( 'prefs-watchlist-days-max' ) . + '<br /><br />' . + $this->getToggle( 'extendwatchlist' ) . + Xml::inputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) . ' ' . + wfMsgHTML( 'prefs-watchlist-edits-max' ) . + '<br /><br />' . + $this->getToggles( array( 'watchlisthideminor', 'watchlisthidebots', 'watchlisthideown', 'watchlisthideanons', 'watchlisthideliu' ) ) + ); - $wgOut->addHtml( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'watchlisthideminor' ) ) ); + if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) ) { + $wgOut->addHTML( $this->getToggle( 'watchcreations' ) ); + } - if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) ) - $wgOut->addHtml( $this->getToggle( 'watchcreations' ) ); foreach( array( 'edit' => 'watchdefault', 'move' => 'watchmoves', 'delete' => 'watchdeletion' ) as $action => $toggle ) { if( $wgUser->isAllowed( $action ) ) - $wgOut->addHtml( $this->getToggle( $toggle ) ); + $wgOut->addHTML( $this->getToggle( $toggle ) ); } $this->mUsedToggles['watchcreations'] = true; $this->mUsedToggles['watchdefault'] = true; $this->mUsedToggles['watchmoves'] = true; $this->mUsedToggles['watchdeletion'] = true; - $wgOut->addHtml( '</fieldset>' ); + $wgOut->addHTML( Xml::closeElement( 'fieldset' ) ); # Search - $ajaxsearch = $wgAjaxSearch ? - $this->addRow( - Xml::label( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ), - Xml::check( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) ) - ) : ''; $mwsuggest = $wgEnableMWSuggest ? $this->addRow( Xml::label( wfMsg( 'mwsuggest-disable' ), 'wpDisableMWSuggest' ), @@ -1049,7 +1129,6 @@ class PreferencesForm { Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'prefs-searchoptions' ) ) . Xml::openElement( 'table' ) . - $ajaxsearch . $this->addRow( Xml::label( wfMsg( 'resultsperpage' ), 'wpSearch' ), Xml::input( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) ) @@ -1078,8 +1157,8 @@ class PreferencesForm { # Misc # $wgOut->addHTML('<fieldset><legend>' . wfMsg('prefs-misc') . '</legend>'); - $wgOut->addHtml( '<label for="wpStubs">' . wfMsg( 'stub-threshold' ) . '</label> ' ); - $wgOut->addHtml( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) ); + $wgOut->addHTML( '<label for="wpStubs">' . wfMsg( 'stub-threshold' ) . '</label> ' ); + $wgOut->addHTML( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) ); $msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) ); $msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) ); $msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) ); @@ -1098,9 +1177,13 @@ class PreferencesForm { foreach ( $togs as $tname ) { if( !array_key_exists( $tname, $this->mUsedToggles ) ) { - $wgOut->addHTML( $this->getToggle( $tname ) ); + if( $tname == 'norollbackdiff' && $wgUser->isAllowed( 'rollback' ) ) + $wgOut->addHTML( $this->getToggle( $tname ) ); + else + $wgOut->addHTML( $this->getToggle( $tname ) ); } } + $wgOut->addHTML( '</fieldset>' ); wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) ); @@ -1119,7 +1202,7 @@ class PreferencesForm { <input type='hidden' name='wpEditToken' value=\"{$token}\" /> </div></form>\n" ); - $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ), + $wgOut->addHTML( Xml::tags( 'div', array( 'class' => "prefcache" ), wfMsgExt( 'clearyourcache', 'parseinline' ) ) ); } diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php index 9c880349..ea0c1135 100644 --- a/includes/specials/SpecialPrefixindex.php +++ b/includes/specials/SpecialPrefixindex.php @@ -1,40 +1,4 @@ <?php -/** - * @file - * @ingroup SpecialPage - */ - -/** - * Entry point : initialise variables and call subfunctions. - * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default NULL) - * @param $specialPage SpecialPage object. - */ -function wfSpecialPrefixIndex( $par=NULL, $specialPage ) { - global $wgRequest, $wgOut, $wgContLang; - - # GET values - $from = $wgRequest->getVal( 'from' ); - $prefix = $wgRequest->getVal( 'prefix' ); - $namespace = $wgRequest->getInt( 'namespace' ); - $namespaces = $wgContLang->getNamespaces(); - - $indexPage = new SpecialPrefixIndex(); - - $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) ) - ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) - : wfMsg( 'allarticles' ) - ); - - if ( isset($par) ) { - $indexPage->showChunk( $namespace, $par, $specialPage->including(), $from ); - } elseif ( isset($prefix) ) { - $indexPage->showChunk( $namespace, $prefix, $specialPage->including(), $from ); - } elseif ( isset($from) ) { - $indexPage->showChunk( $namespace, $from, $specialPage->including(), $from ); - } else { - $wgOut->addHtml($indexPage->namespaceForm ( $namespace, null )); - } -} /** * implements Special:Prefixindex @@ -44,18 +8,90 @@ class SpecialPrefixindex extends SpecialAllpages { // Inherit $maxPerPage // Define other properties - protected $name = 'Prefixindex'; protected $nsfromMsg = 'allpagesprefix'; + + function __construct(){ + parent::__construct( 'Prefixindex' ); + } + + /** + * Entry point : initialise variables and call subfunctions. + * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default null) + */ + function execute( $par ) { + global $wgRequest, $wgOut, $wgContLang; + + $this->setHeaders(); + $this->outputHeader(); + + # GET values + $from = $wgRequest->getVal( 'from' ); + $prefix = $wgRequest->getVal( 'prefix' ); + $namespace = $wgRequest->getInt( 'namespace' ); + $namespaces = $wgContLang->getNamespaces(); + + $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) ) + ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) + : wfMsg( 'prefixindex' ) + ); + + if( isset( $par ) ){ + $this->showPrefixChunk( $namespace, $par, $from ); + } elseif( isset( $prefix ) ){ + $this->showPrefixChunk( $namespace, $prefix, $from ); + } elseif( isset( $from ) ){ + $this->showPrefixChunk( $namespace, $from, $from ); + } else { + $wgOut->addHTML( $this->namespacePrefixForm( $namespace, null ) ); + } + } + + /** + * HTML for the top form + * @param integer $namespace A namespace constant (default NS_MAIN). + * @param string $from dbKey we are starting listing at. + */ + function namespacePrefixForm( $namespace = NS_MAIN, $from = '' ) { + global $wgScript; + $t = $this->getTitle(); + + $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); + $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $out .= Xml::hidden( 'title', $t->getPrefixedText() ); + $out .= Xml::openElement( 'fieldset' ); + $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) ); + $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); + $out .= "<tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + "</td> + <td class='mw-input'>" . + Xml::namespaceSelector( $namespace, null ) . ' ' . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + "</td> + </tr>"; + $out .= Xml::closeElement( 'table' ); + $out .= Xml::closeElement( 'fieldset' ); + $out .= Xml::closeElement( 'form' ); + $out .= Xml::closeElement( 'div' ); + return $out; + } /** * @param integer $namespace (Default NS_MAIN) * @param string $from list all pages from this name (default FALSE) */ - function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = null ) { + function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null ) { global $wgOut, $wgUser, $wgContLang; - $fname = 'indexShowChunk'; - $sk = $wgUser->getSkin(); if (!isset($from)) $from = $prefix; @@ -86,7 +122,7 @@ class SpecialPrefixindex extends SpecialAllpages { 'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'', 'page_title >= ' . $dbr->addQuotes( $fromKey ), ), - $fname, + __METHOD__, array( 'ORDER BY' => 'page_title', 'LIMIT' => $this->maxPerPage + 1, @@ -100,7 +136,7 @@ class SpecialPrefixindex extends SpecialAllpages { if( $res->numRows() > 0 ) { $out = '<table style="background: inherit;" border="0" width="100%">'; - while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { + while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { $t = Title::makeTitle( $s->page_namespace, $s->page_title ); if( $t ) { $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) . @@ -127,26 +163,27 @@ class SpecialPrefixindex extends SpecialAllpages { } } - if ( $including ) { + if ( $this->including() ) { $out2 = ''; } else { - $nsForm = $this->namespaceForm ( $namespace, $prefix ); + $nsForm = $this->namespacePrefixForm( $namespace, $prefix ); + $self = $this->getTitle(); $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">'; $out2 .= '<tr valign="top"><td>' . $nsForm; $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' . - $sk->makeKnownLink( $wgContLang->specialPage( $this->name ), + $sk->makeKnownLinkObj( $self, wfMsg ( 'allpages' ) ); - if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { + if( isset( $res ) && $res && ( $n == $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { $namespaceparam = $namespace ? "&namespace=$namespace" : ""; - $out2 .= " | " . $sk->makeKnownLink( - $wgContLang->specialPage( $this->name ), + $out2 .= " | " . $sk->makeKnownLinkObj( + $self, wfMsgHtml( 'nextpage', htmlspecialchars( $s->page_title ) ), - "from=" . wfUrlEncode ( $s->page_title ) . - "&prefix=" . wfUrlEncode ( $prefix ) . $namespaceparam ); + "from=" . wfUrlEncode( $s->page_title ) . + "&prefix=" . wfUrlEncode( $prefix ) . $namespaceparam ); } $out2 .= "</td></tr></table><hr />"; } - $wgOut->addHtml( $out2 . $out ); + $wgOut->addHTML( $out2 . $out ); } } diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php index 3025c055..4e56ca42 100644 --- a/includes/specials/SpecialProtectedpages.php +++ b/includes/specials/SpecialProtectedpages.php @@ -16,7 +16,6 @@ class ProtectedPagesForm { public function showList( $msg = '' ) { global $wgOut, $wgRequest; - $wgOut->setPagetitle( wfMsg( "protectedpages" ) ); if ( "" != $msg ) { $wgOut->setSubtitle( $msg ); } @@ -32,10 +31,11 @@ class ProtectedPagesForm { $size = $wgRequest->getIntOrNull( 'size' ); $NS = $wgRequest->getIntOrNull( 'namespace' ); $indefOnly = $wgRequest->getBool( 'indefonly' ) ? 1 : 0; + $cascadeOnly = $wgRequest->getBool('cascadeonly') ? 1 : 0; - $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly ); + $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly, $cascadeOnly ); - $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly ) ); + $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly, $cascadeOnly ) ); if ( $pager->getNumRows() ) { $s = $pager->getNavigationBar(); @@ -83,7 +83,7 @@ class ProtectedPagesForm { if ( $row->pr_expiry != 'infinity' && strlen($row->pr_expiry) ) { $expiry = Block::decodeExpiry( $row->pr_expiry ); - $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) ); + $expiry_description = wfMsg( 'protect-expiring' , $wgLang->timeanddate( $expiry ) , $wgLang->date( $expiry ) , $wgLang->time( $expiry ) ); $description_items[] = $expiry_description; } @@ -111,21 +111,24 @@ class ProtectedPagesForm { * @param $level string * @param $minsize int * @param $indefOnly bool + * @param $cascadeOnly bool * @return string Input form * @private */ - protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly ) { + protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly, $cascadeOnly ) { global $wgScript; $title = SpecialPage::getTitleFor( 'ProtectedPages' ); return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) . - Xml::hidden( 'title', $title->getPrefixedDBkey() ) . " \n" . + Xml::hidden( 'title', $title->getPrefixedDBkey() ) . "\n" . $this->getNamespaceMenu( $namespace ) . " \n" . $this->getTypeMenu( $type ) . " \n" . $this->getLevelMenu( $level ) . " \n" . - "<br /><span style='white-space: nowrap'> " . + "<br/><span style='white-space: nowrap'>" . $this->getExpiryCheck( $indefOnly ) . " \n" . + $this->getCascadeCheck( $cascadeOnly ) . " \n" . + "</span><br/><span style='white-space: nowrap'>" . $this->getSizeLimit( $sizetype, $size ) . " \n" . "</span>" . " " . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . @@ -153,6 +156,14 @@ class ProtectedPagesForm { return Xml::checkLabel( wfMsg('protectedpages-indef'), 'indefonly', 'indefonly', $indefOnly ) . "\n"; } + + /** + * @return string Formatted HTML + */ + protected function getCascadeCheck( $cascadeOnly ) { + return + Xml::checkLabel( wfMsg('protectedpages-cascade'), 'cascadeonly', 'cascadeonly', $cascadeOnly ) . "\n"; + } /** * @return string Formatted HTML @@ -237,7 +248,8 @@ class ProtectedPagesPager extends AlphabeticPager { public $mForm, $mConds; private $type, $level, $namespace, $sizetype, $size, $indefonly; - function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0, $indefonly=false ) { + function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', + $size=0, $indefonly = false, $cascadeonly = false ) { $this->mForm = $form; $this->mConds = $conds; $this->type = ( $type ) ? $type : 'edit'; @@ -246,6 +258,7 @@ class ProtectedPagesPager extends AlphabeticPager { $this->sizetype = $sizetype; $this->size = intval($size); $this->indefonly = (bool)$indefonly; + $this->cascadeonly = (bool)$cascadeonly; parent::__construct(); } @@ -281,6 +294,9 @@ class ProtectedPagesPager extends AlphabeticPager { if( $this->indefonly ) { $conds[] = "pr_expiry = 'infinity' OR pr_expiry IS NULL"; } + if ( $this->cascadeonly ) { + $conds[] = "pr_cascade = '1'"; + } if( $this->level ) $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level ); diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php index 2ec68a66..7e8126d9 100644 --- a/includes/specials/SpecialProtectedtitles.php +++ b/includes/specials/SpecialProtectedtitles.php @@ -16,7 +16,6 @@ class ProtectedTitlesForm { function showList( $msg = '' ) { global $wgOut, $wgRequest; - $wgOut->setPagetitle( wfMsg( "protectedtitles" ) ); if ( "" != $msg ) { $wgOut->setSubtitle( $msg ); } @@ -75,7 +74,7 @@ class ProtectedTitlesForm { if ( $row->pt_expiry != 'infinity' && strlen($row->pt_expiry) ) { $expiry = Block::decodeExpiry( $row->pt_expiry ); - $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) ); + $expiry_description = wfMsg( 'protect-expiring', $wgLang->timeanddate( $expiry ) , $wgLang->date( $expiry ) , $wgLang->time( $expiry ) ); $description_items[] = $expiry_description; } @@ -102,7 +101,7 @@ class ProtectedTitlesForm { Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) . Xml::hidden( 'title', $special ) . " \n" . $this->getNamespaceMenu( $namespace ) . " \n" . - // $this->getLevelMenu( $level ) . "<br/>\n" . + $this->getLevelMenu( $level ) . " \n" . " " . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . "</fieldset></form>"; } @@ -137,7 +136,10 @@ class ProtectedTitlesForm { $m[$text] = $type; } } - + // Is there only one level (aside from "all")? + if( count($m) <= 2 ) { + return ''; + } // Third pass generates sorted XHTML content foreach( $m as $text => $type ) { $selected = ($type == $pr_level ); @@ -190,7 +192,8 @@ class ProtectedtitlesPager extends AlphabeticPager { function getQueryInfo() { $conds = $this->mConds; $conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() ); - + if( $this->level ) + $conds['pt_create_perm'] = $this->level; if( !is_null($this->namespace) ) $conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace ); return array( diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php index 0e7ada1d..f4bff30b 100644 --- a/includes/specials/SpecialRandompage.php +++ b/includes/specials/SpecialRandompage.php @@ -8,19 +8,23 @@ * @license GNU General Public Licence 2.0 or later */ class RandomPage extends SpecialPage { - private $namespace = NS_MAIN; // namespace to select pages from + private $namespaces; // namespaces to select pages from function __construct( $name = 'Randompage' ){ + global $wgContentNamespaces; + + $this->namespaces = $wgContentNamespaces; + parent::__construct( $name ); } - public function getNamespace() { - return $this->namespace; + public function getNamespaces() { + return $this->namespaces; } public function setNamespace ( $ns ) { if( $ns < NS_MAIN ) $ns = NS_MAIN; - $this->namespace = $ns; + $this->namespaces = array( $ns ); } // select redirects instead of normal pages? @@ -39,7 +43,7 @@ class RandomPage extends SpecialPage { if( is_null( $title ) ) { $this->setHeaders(); - $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages' ); + $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages', $wgContLang->getNsText( $this->namespace ) ); return; } @@ -67,7 +71,7 @@ class RandomPage extends SpecialPage { $row = $this->selectRandomPageFromDB( "0" ); if( $row ) - return Title::makeTitleSafe( $this->namespace, $row->page_title ); + return Title::makeTitleSafe( $row->page_namespace, $row->page_title ); else return null; } @@ -81,13 +85,13 @@ class RandomPage extends SpecialPage { $use_index = $dbr->useIndexClause( 'page_random' ); $page = $dbr->tableName( 'page' ); - $ns = (int) $this->namespace; + $ns = implode( ",", $this->namespaces ); $redirect = $this->isRedirect() ? 1 : 0; $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : ""; - $sql = "SELECT page_title + $sql = "SELECT page_title, page_namespace FROM $page $use_index - WHERE page_namespace = $ns + WHERE page_namespace IN ( $ns ) AND page_is_redirect = $redirect AND page_random >= $randstr $extra diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index cb718bdc..8c14e1fc 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -6,7 +6,7 @@ */ class SpecialRecentChanges extends SpecialPage { public function __construct() { - SpecialPage::SpecialPage( 'Recentchanges' ); + parent::__construct( 'Recentchanges' ); $this->includable( true ); } @@ -16,13 +16,14 @@ class SpecialRecentChanges extends SpecialPage { * @return FormOptions */ public function getDefaultOptions() { + global $wgUser; $opts = new FormOptions(); - $opts->add( 'days', (int)User::getDefaultOption( 'rcdays' ) ); - $opts->add( 'limit', (int)User::getDefaultOption( 'rclimit' ) ); + $opts->add( 'days', (int)$wgUser->getOption( 'rcdays' ) ); + $opts->add( 'limit', (int)$wgUser->getOption( 'rclimit' ) ); $opts->add( 'from', '' ); - $opts->add( 'hideminor', false ); + $opts->add( 'hideminor', (bool)$wgUser->getOption( 'hideminor' ) ); $opts->add( 'hidebots', true ); $opts->add( 'hideanons', false ); $opts->add( 'hideliu', false ); @@ -34,7 +35,6 @@ class SpecialRecentChanges extends SpecialPage { $opts->add( 'categories', '' ); $opts->add( 'categories_any', false ); - return $opts; } @@ -44,16 +44,13 @@ class SpecialRecentChanges extends SpecialPage { * @return FormOptions */ public function setup( $parameters ) { - global $wgUser, $wgRequest; + global $wgRequest; $opts = $this->getDefaultOptions(); - $opts['days'] = (int)$wgUser->getOption( 'rcdays', $opts['days'] ); - $opts['limit'] = (int)$wgUser->getOption( 'rclimit', $opts['limit'] ); - $opts['hideminor'] = $wgUser->getOption( 'hideminor', $opts['hideminor'] ); $opts->fetchValuesFromRequest( $wgRequest ); // Give precedence to subpage syntax - if ( $parameters !== null ) { + if( $parameters !== null ) { $this->parseParameters( $parameters, $opts ); } @@ -85,9 +82,9 @@ class SpecialRecentChanges extends SpecialPage { # 10 seconds server-side caching max $wgOut->setSquidMaxage( 10 ); - + # Check if the client has a cached version $lastmod = $this->checkLastModified( $feedFormat ); - if( $lastmod === false ){ + if( $lastmod === false ) { return; } @@ -97,33 +94,32 @@ class SpecialRecentChanges extends SpecialPage { // Fetch results, prepare a batch link existence check query $rows = array(); - $batch = new LinkBatch; $conds = $this->buildMainQueryConds( $opts ); - $res = $this->doMainQuery( $conds, $opts ); - if( $res === false ){ - $this->doHeader( $opts ); + $rows = $this->doMainQuery( $conds, $opts ); + if( $rows === false ){ + if( !$this->including() ) { + $this->doHeader( $opts ); + } return; } - $dbr = wfGetDB( DB_SLAVE ); - while( $row = $dbr->fetchObject( $res ) ){ - $rows[] = $row; - if ( !$feedFormat ) { - // User page and talk links + + if( !$feedFormat ) { + $batch = new LinkBatch; + foreach( $rows as $row ) { $batch->add( NS_USER, $row->rc_user_text ); $batch->add( NS_USER_TALK, $row->rc_user_text ); } - + $batch->execute(); } - $dbr->freeResult( $res ); - if ( $feedFormat ) { + if( $feedFormat ) { list( $feed, $feedObj ) = $this->getFeedObject( $feedFormat ); $feed->execute( $feedObj, $rows, $opts['limit'], $opts['hideminor'], $lastmod ); } else { - $batch->execute(); $this->webOutput( $rows, $opts ); } - + + $rows->free(); } /** @@ -149,21 +145,21 @@ class SpecialRecentChanges extends SpecialPage { */ public function parseParameters( $par, FormOptions $opts ) { $bits = preg_split( '/\s*,\s*/', trim( $par ) ); - foreach ( $bits as $bit ) { - if ( 'hidebots' === $bit ) $opts['hidebots'] = true; - if ( 'bots' === $bit ) $opts['hidebots'] = false; - if ( 'hideminor' === $bit ) $opts['hideminor'] = true; - if ( 'minor' === $bit ) $opts['hideminor'] = false; - if ( 'hideliu' === $bit ) $opts['hideliu'] = true; - if ( 'hidepatrolled' === $bit ) $opts['hidepatrolled'] = true; - if ( 'hideanons' === $bit ) $opts['hideanons'] = true; - if ( 'hidemyself' === $bit ) $opts['hidemyself'] = true; - - if ( is_numeric( $bit ) ) $opts['limit'] = $bit; + foreach( $bits as $bit ) { + if( 'hidebots' === $bit ) $opts['hidebots'] = true; + if( 'bots' === $bit ) $opts['hidebots'] = false; + if( 'hideminor' === $bit ) $opts['hideminor'] = true; + if( 'minor' === $bit ) $opts['hideminor'] = false; + if( 'hideliu' === $bit ) $opts['hideliu'] = true; + if( 'hidepatrolled' === $bit ) $opts['hidepatrolled'] = true; + if( 'hideanons' === $bit ) $opts['hideanons'] = true; + if( 'hidemyself' === $bit ) $opts['hidemyself'] = true; + + if( is_numeric( $bit ) ) $opts['limit'] = $bit; $m = array(); - if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) $opts['limit'] = $m[1]; - if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) $opts['days'] = $m[1]; + if( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) $opts['limit'] = $m[1]; + if( preg_match( '/^days=(\d+)$/', $bit, $m ) ) $opts['days'] = $m[1]; } } @@ -173,14 +169,14 @@ class SpecialRecentChanges extends SpecialPage { * update the timestamp * * @param $feedFormat String - * @return int or false + * @return string or false */ public function checkLastModified( $feedFormat ) { global $wgUseRCPatrol, $wgOut; $dbr = wfGetDB( DB_SLAVE ); $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __FUNCTION__ ); - if ( $feedFormat || !$wgUseRCPatrol ) { - if( $lastmod && $wgOut->checkLastModified( $lastmod ) ){ + if( $feedFormat || !$wgUseRCPatrol ) { + if( $lastmod && $wgOut->checkLastModified( $lastmod ) ) { # Client cache fresh and headers sent, nothing more to do. return false; } @@ -232,12 +228,12 @@ class SpecialRecentChanges extends SpecialPage { $hideLoggedInUsers = $opts['hideliu'] && !$forcebot; $hideAnonymousUsers = $opts['hideanons'] && !$forcebot; - if ( $opts['hideminor'] ) $conds['rc_minor'] = 0; - if ( $opts['hidebots'] ) $conds['rc_bot'] = 0; - if ( $hidePatrol ) $conds['rc_patrolled'] = 0; - if ( $forcebot ) $conds['rc_bot'] = 1; - if ( $hideLoggedInUsers ) $conds[] = 'rc_user = 0'; - if ( $hideAnonymousUsers ) $conds[] = 'rc_user != 0'; + if( $opts['hideminor'] ) $conds['rc_minor'] = 0; + if( $opts['hidebots'] ) $conds['rc_bot'] = 0; + if( $hidePatrol ) $conds['rc_patrolled'] = 0; + if( $forcebot ) $conds['rc_bot'] = 1; + if( $hideLoggedInUsers ) $conds[] = 'rc_user = 0'; + if( $hideAnonymousUsers ) $conds[] = 'rc_user != 0'; if( $opts['hidemyself'] ) { if( $wgUser->getId() ) { @@ -248,8 +244,8 @@ class SpecialRecentChanges extends SpecialPage { } # Namespace filtering - if ( $opts['namespace'] !== '' ) { - if ( !$opts['invert'] ) { + if( $opts['namespace'] !== '' ) { + if( !$opts['invert'] ) { $conds[] = 'rc_namespace = ' . $dbr->addQuotes( $opts['namespace'] ); } else { $conds[] = 'rc_namespace != ' . $dbr->addQuotes( $opts['namespace'] ); @@ -281,7 +277,8 @@ class SpecialRecentChanges extends SpecialPage { // JOIN on watchlist for users if( $uid ) { $tables[] = 'watchlist'; - $join_conds = array( 'watchlist' => array('LEFT JOIN',"wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace") ); + $join_conds = array( 'watchlist' => array('LEFT JOIN', + "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace") ); } wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) ); @@ -329,7 +326,7 @@ class SpecialRecentChanges extends SpecialPage { $limit = $opts['limit']; - if ( !$this->including() ) { + if( !$this->including() ) { // Output options box $this->doHeader( $opts ); } @@ -337,55 +334,46 @@ class SpecialRecentChanges extends SpecialPage { // And now for the content $wgOut->setSyndicated( true ); - $list = ChangesList::newFromUser( $wgUser ); - - if ( $wgAllowCategorizedRecentChanges ) { + if( $wgAllowCategorizedRecentChanges ) { $this->filterByCategories( $rows, $opts ); } - $s = $list->beginRecentChangesList(); - $counter = 1; - $showWatcherCount = $wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' ); $watcherCache = array(); $dbr = wfGetDB( DB_SLAVE ); - foreach( $rows as $obj ){ - if( $limit == 0) { - break; - } - - if ( ! ( $opts['hideminor'] && $obj->rc_minor ) && - ! ( $opts['hidepatrolled'] && $obj->rc_patrolled ) ) { - $rc = RecentChange::newFromRow( $obj ); - $rc->counter = $counter++; - - if ($wgShowUpdatedMarker - && !empty( $obj->wl_notificationtimestamp ) - && ($obj->rc_timestamp >= $obj->wl_notificationtimestamp)) { - $rc->notificationtimestamp = true; - } else { - $rc->notificationtimestamp = false; - } + $counter = 1; + $list = ChangesList::newFromUser( $wgUser ); - $rc->numberofWatchingusers = 0; // Default - if ($showWatcherCount && $obj->rc_namespace >= 0) { - if (!isset($watcherCache[$obj->rc_namespace][$obj->rc_title])) { - $watcherCache[$obj->rc_namespace][$obj->rc_title] = - $dbr->selectField( 'watchlist', - 'COUNT(*)', - array( - 'wl_namespace' => $obj->rc_namespace, - 'wl_title' => $obj->rc_title, - ), - __METHOD__ . '-watchers' ); - } - $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title]; + $s = $list->beginRecentChangesList(); + foreach( $rows as $obj ) { + if( $limit == 0 ) break; + $rc = RecentChange::newFromRow( $obj ); + $rc->counter = $counter++; + # Check if the page has been updated since the last visit + if( $wgShowUpdatedMarker && !empty($obj->wl_notificationtimestamp) ) { + $rc->notificationtimestamp = ($obj->rc_timestamp >= $obj->wl_notificationtimestamp); + } else { + $rc->notificationtimestamp = false; // Default + } + # Check the number of users watching the page + $rc->numberofWatchingusers = 0; // Default + if( $showWatcherCount && $obj->rc_namespace >= 0 ) { + if( !isset($watcherCache[$obj->rc_namespace][$obj->rc_title]) ) { + $watcherCache[$obj->rc_namespace][$obj->rc_title] = + $dbr->selectField( 'watchlist', + 'COUNT(*)', + array( + 'wl_namespace' => $obj->rc_namespace, + 'wl_title' => $obj->rc_title, + ), + __METHOD__ . '-watchers' ); } - $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) ); - --$limit; + $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title]; } + $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) ); + --$limit; } $s .= $list->endRecentChangesList(); $wgOut->addHTML( $s ); @@ -411,22 +399,29 @@ class SpecialRecentChanges extends SpecialPage { $panel[] = '<hr />'; $extraOpts = $this->getExtraOptions( $opts ); + $extraOptsCount = count( $extraOpts ); + $count = 0; + $submit = ' ' . Xml::submitbutton( wfMsg( 'allpagessubmit' ) ); + + $out = Xml::openElement( 'table', array( 'class' => 'mw-recentchanges-table' ) ); + foreach( $extraOpts as $optionRow ) { + # Add submit button to the last row only + ++$count; + $addSubmit = $count === $extraOptsCount ? $submit : ''; - $out = Xml::openElement( 'table' ); - foreach ( $extraOpts as $optionRow ) { $out .= Xml::openElement( 'tr' ); - if ( is_array($optionRow) ) { - $out .= Xml::tags( 'td', null, $optionRow[0] ); - $out .= Xml::tags( 'td', null, $optionRow[1] ); + if( is_array( $optionRow ) ) { + $out .= Xml::tags( 'td', array( 'class' => 'mw-label' ), $optionRow[0] ); + $out .= Xml::tags( 'td', array( 'class' => 'mw-input' ), $optionRow[1] . $addSubmit ); } else { - $out .= Xml::tags( 'td', array( 'colspan' => 2 ), $optionRow ); + $out .= Xml::tags( 'td', array( 'class' => 'mw-input', 'colspan' => 2 ), $optionRow . $addSubmit ); } $out .= Xml::closeElement( 'tr' ); } $out .= Xml::closeElement( 'table' ); $unconsumed = $opts->getUnconsumedValues(); - foreach ( $unconsumed as $key => $value ) { + foreach( $unconsumed as $key => $value ) { $out .= Xml::hidden( $key, $value ); } @@ -437,7 +432,7 @@ class SpecialRecentChanges extends SpecialPage { $panelString = implode( "\n", $panel ); $wgOut->addHTML( - Xml::fieldset( wfMsg( strtolower( $this->mName ) ), $panelString, array( 'class' => 'rcoptions' ) ) + Xml::fieldset( wfMsg( 'recentchanges-legend' ), $panelString, array( 'class' => 'rcoptions' ) ) ); $this->setBottomText( $wgOut, $opts ); @@ -454,12 +449,11 @@ class SpecialRecentChanges extends SpecialPage { $extraOpts['namespace'] = $this->namespaceFilterForm( $opts ); global $wgAllowCategorizedRecentChanges; - if ( $wgAllowCategorizedRecentChanges ) { + if( $wgAllowCategorizedRecentChanges ) { $extraOpts['category'] = $this->categoryFilterForm( $opts ); } wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) ); - $extraOpts['submit'] = Xml::submitbutton( wfMsg('allpagessubmit') ); return $extraOpts; } @@ -469,7 +463,7 @@ class SpecialRecentChanges extends SpecialPage { * @param $out OutputPage * @param $opts FormOptions */ - function setTopText( &$out, $opts ){ + function setTopText( OutputPage $out, FormOptions $opts ){ $out->addWikiText( wfMsgForContentNoTrans( 'recentchangestext' ) ); } @@ -480,7 +474,7 @@ class SpecialRecentChanges extends SpecialPage { * @param $out OutputPage * @param $opts FormOptions */ - function setBottomText( &$out, $opts ){} + function setBottomText( OutputPage $out, FormOptions $opts ){} /** * Creates the choose namespace selection @@ -489,7 +483,7 @@ class SpecialRecentChanges extends SpecialPage { * @return string */ protected function namespaceFilterForm( FormOptions $opts ) { - $nsSelect = HTMLnamespaceselector( $opts['namespace'], '' ); + $nsSelect = Xml::namespaceSelector( $opts['namespace'], '' ); $nsLabel = Xml::label( wfMsg('namespace'), 'namespace' ); $invert = Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $opts['invert'] ); return array( $nsLabel, "$nsSelect $invert" ); @@ -526,30 +520,30 @@ class SpecialRecentChanges extends SpecialPage { # Filter categories $cats = array(); - foreach ( $categories as $cat ) { + foreach( $categories as $cat ) { $cat = trim( $cat ); - if ( $cat == "" ) continue; + if( $cat == "" ) continue; $cats[] = $cat; } # Filter articles $articles = array(); $a2r = array(); - foreach ( $rows AS $k => $r ) { + foreach( $rows AS $k => $r ) { $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title ); $id = $nt->getArticleID(); - if ( $id == 0 ) continue; # Page might have been deleted... - if ( !in_array($id, $articles) ) { + if( $id == 0 ) continue; # Page might have been deleted... + if( !in_array($id, $articles) ) { $articles[] = $id; } - if ( !isset($a2r[$id]) ) { + if( !isset($a2r[$id]) ) { $a2r[$id] = array(); } $a2r[$id][] = $k; } # Shortcut? - if ( !count($articles) || !count($cats) ) + if( !count($articles) || !count($cats) ) return ; # Look up @@ -559,8 +553,8 @@ class SpecialRecentChanges extends SpecialPage { # Filter $newrows = array(); - foreach ( $match AS $id ) { - foreach ( $a2r[$id] AS $rev ) { + foreach( $match AS $id ) { + foreach( $a2r[$id] AS $rev ) { $k = $rev; $newrows[$k] = $rows[$k]; } @@ -577,8 +571,9 @@ class SpecialRecentChanges extends SpecialPage { function makeOptionsLink( $title, $override, $options, $active = false ) { global $wgUser; $sk = $wgUser->getSkin(); - return $sk->makeKnownLinkObj( $this->getTitle(), htmlspecialchars( $title ), - wfArrayToCGI( $override, $options ), '', '', $active ? 'style="font-weight: bold;"' : '' ); + $params = $override + $options; + return $sk->link( $this->getTitle(), htmlspecialchars( $title ), + ( $active ? array( 'style'=>'font-weight: bold;' ) : array() ), $params, array( 'known' ) ); } /** @@ -591,43 +586,41 @@ class SpecialRecentChanges extends SpecialPage { $options = $nondefaults + $defaults; - if( $options['from'] ) - $note = wfMsgExt( 'rcnotefrom', array( 'parseinline' ), + $note = ''; + if( $options['from'] ) { + $note .= wfMsgExt( 'rcnotefrom', array( 'parseinline' ), $wgLang->formatNum( $options['limit'] ), - $wgLang->timeanddate( $options['from'], true ) ); - else - $note = wfMsgExt( 'rcnote', array( 'parseinline' ), - $wgLang->formatNum( $options['limit'] ), - $wgLang->formatNum( $options['days'] ), - $wgLang->timeAndDate( wfTimestampNow(), true ), - $wgLang->date( wfTimestampNow(), true ), - $wgLang->time( wfTimestampNow(), true ) ); + $wgLang->timeanddate( $options['from'], true ) ) . '<br />'; + } + if( !wfEmptyMsg( 'rclegend', wfMsg('rclegend') ) ) { + $note .= wfMsgExt( 'rclegend', array('parseinline') ) . '<br />'; + } # Sort data for display and make sure it's unique after we've added user data. $wgRCLinkLimits[] = $options['limit']; $wgRCLinkDays[] = $options['days']; - sort($wgRCLinkLimits); - sort($wgRCLinkDays); - $wgRCLinkLimits = array_unique($wgRCLinkLimits); - $wgRCLinkDays = array_unique($wgRCLinkDays); + sort( $wgRCLinkLimits ); + sort( $wgRCLinkDays ); + $wgRCLinkLimits = array_unique( $wgRCLinkLimits ); + $wgRCLinkDays = array_unique( $wgRCLinkDays ); // limit links foreach( $wgRCLinkLimits as $value ) { $cl[] = $this->makeOptionsLink( $wgLang->formatNum( $value ), array( 'limit' => $value ), $nondefaults, $value == $options['limit'] ) ; } - $cl = implode( ' | ', $cl); + $cl = implode( ' | ', $cl ); // day links, reset 'from' to none foreach( $wgRCLinkDays as $value ) { $dl[] = $this->makeOptionsLink( $wgLang->formatNum( $value ), array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] ) ; } - $dl = implode( ' | ', $dl); + $dl = implode( ' | ', $dl ); // show/hide links - $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' )); + $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ) ); $minorLink = $this->makeOptionsLink( $showhide[1-$options['hideminor']], array( 'hideminor' => 1-$options['hideminor'] ), $nondefaults); $botLink = $this->makeOptionsLink( $showhide[1-$options['hidebots']], @@ -652,11 +645,11 @@ class SpecialRecentChanges extends SpecialPage { // show from this onward link $now = $wgLang->timeanddate( wfTimestampNow(), true ); - $tl = $this->makeOptionsLink( $now, array( 'from' => wfTimestampNow()), $nondefaults ); + $tl = $this->makeOptionsLink( $now, array( 'from' => wfTimestampNow() ), $nondefaults ); - $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter'), + $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter' ), $cl, $dl, $hl ); - $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter'), $tl ); - return "$note<br />$rclinks<br />$rclistfrom"; + $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter' ), $tl ); + return "{$note}$rclinks<br />$rclistfrom"; } } diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index d773fb77..c0734354 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -7,7 +7,8 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { function __construct(){ - SpecialPage::SpecialPage( 'Recentchangeslinked' ); + SpecialPage::SpecialPage( 'Recentchangeslinked' ); + $this->includable( true ); } public function getDefaultOptions() { @@ -92,10 +93,13 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { } else { // for now, always join on these tables; really should be configurable as in whatlinkshere $link_tables = array( 'pagelinks', 'templatelinks' ); - // imagelinks only contains links to pages in NS_IMAGE - if( $ns == NS_IMAGE || !$showlinkedto ) $link_tables[] = 'imagelinks'; + // imagelinks only contains links to pages in NS_FILE + if( $ns == NS_FILE || !$showlinkedto ) $link_tables[] = 'imagelinks'; } + if( $id == 0 && !$showlinkedto ) + return false; // nonexistent pages can't link to any pages + // field name prefixes for all the various tables we might want to join with $prefix = array( 'pagelinks' => 'pl', 'templatelinks' => 'tl', 'categorylinks' => 'cl', 'imagelinks' => 'il' ); @@ -105,7 +109,7 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { $pfx = $prefix[$link_table]; // imagelinks and categorylinks tables have no xx_namespace field, and have xx_to instead of xx_title - if( $link_table == 'imagelinks' ) $link_ns = NS_IMAGE; + if( $link_table == 'imagelinks' ) $link_ns = NS_FILE; else if( $link_table == 'categorylinks' ) $link_ns = NS_CATEGORY; else $link_ns = 0; @@ -145,7 +149,7 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { $res = $dbr->query( $sql, __METHOD__ ); - if( $dbr->numRows( $res ) == 0 ) + if( $res->numRows() == 0 ) $this->mResultEmpty = true; return $res; @@ -159,17 +163,21 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) . Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' . Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) ); - $extraOpts['submit'] = Xml::submitbutton( wfMsg('allpagessubmit') ); return $extraOpts; } - - function setTopText( &$out, $opts ){} - - function setBottomText( &$out, $opts ){ + + function setTopText( OutputPage $out, FormOptions $opts ) { + global $wgUser; + $skin = $wgUser->getSkin(); + if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) ) + $out->setSubtitle( wfMsg( 'recentchangeslinked-backlink', $skin->link( $this->mTargetTitle, + $this->mTargetTitle->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) ); + } + + function setBottomText( OutputPage $out, FormOptions $opts ){ if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) ){ global $wgUser; $out->setFeedAppendQuery( "target=" . urlencode( $this->mTargetTitle->getPrefixedDBkey() ) ); - $out->addHTML("< ".$wgUser->getSkin()->makeLinkObj( $this->mTargetTitle, "", "redirect=no" )."<hr />\n"); } if( isset( $this->mResultEmpty ) && $this->mResultEmpty ){ $out->addWikiMsg( 'recentchangeslinked-noresult' ); diff --git a/includes/specials/SpecialRemoveRestrictions.php b/includes/specials/SpecialRemoveRestrictions.php new file mode 100644 index 00000000..ded6cbe3 --- /dev/null +++ b/includes/specials/SpecialRemoveRestrictions.php @@ -0,0 +1,60 @@ +<?php + +function wfSpecialRemoveRestrictions() { + global $wgOut, $wgRequest, $wgUser, $wgLang, $wgTitle; + $sk = $wgUser->getSkin(); + + $id = $wgRequest->getVal( 'id' ); + if( !is_numeric( $id ) ) { + $wgOut->addWikiMsg( 'removerestrictions-noid' ); + return; + } + + UserRestriction::purgeExpired(); + $r = UserRestriction::newFromId( $id, true ); + if( !$r ) { + $wgOut->addWikiMsg( 'removerestrictions-wrongid' ); + return; + } + + $form = array(); + $form['removerestrictions-user'] = $sk->userLink( $r->getSubjectId(), $r->getSubjectText() ) . + $sk->userToolLinks( $r->getSubjectId(), $r->getSubjectText() ); + $form['removerestrictions-type'] = UserRestriction::formatType( $r->getType() ); + if( $r->isPage() ) + $form['removerestrictions-page'] = $sk->link( $r->getPage() ); + if( $r->isNamespace() ) + $form['removerestrictions-namespace'] = $wgLang->getDisplayNsText( $r->getNamespace() ); + $form['removerestrictions-reason'] = Xml::input( 'reason' ); + + $result = null; + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) + $result = wfSpecialRemoveRestrictionsProcess( $r ); + + $wgOut->addWikiMsg( 'removerestrictions-intro' ); + $wgOut->addHTML( Xml::fieldset( wfMsgHtml( 'removerestrictions-legend' ) ) ); + if( $result ) + $wgOut->addHTML( '<strong class="success">' . wfMsgExt( 'removerestrictions-success', + 'parseinline', $r->getSubjectText() ) . '</strong>' ); + $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl( array( 'id' => $id ) ), + 'method' => 'post' ) ) ); + $wgOut->addHTML( Xml::buildForm( $form, 'removerestrictions-submit' ) ); + $wgOut->addHTML( Xml::hidden( 'id', $r->getId() ) ); + $wgOut->addHTML( Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) ); + $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); + $wgOut->addHTML( "</form></fieldset>" ); +} + +function wfSpecialRemoveRestrictionsProcess( $r ) { + global $wgUser, $wgRequest; + $reason = $wgRequest->getVal( 'reason' ); + $result = $r->delete(); + $log = new LogPage( 'restrict' ); + $params = array( $r->getType() ); + if( $r->isPage() ) + $params[] = $r->getPage()->getPrefixedDbKey(); + if( $r->isNamespace() ) + $params[] = $r->getNamespace(); + $log->addEntry( 'remove', Title::makeTitle( NS_USER, $r->getSubjectText() ), $reason, $params ); + return $result; +} diff --git a/includes/specials/SpecialResetpass.php b/includes/specials/SpecialResetpass.php index 707b941d..059f8dbd 100644 --- a/includes/specials/SpecialResetpass.php +++ b/includes/specials/SpecialResetpass.php @@ -4,26 +4,13 @@ * @ingroup SpecialPage */ -/** Constructor */ -function wfSpecialResetpass( $par ) { - $form = new PasswordResetForm(); - $form->execute( $par ); -} - /** * Let users recover their password. * @ingroup SpecialPage */ -class PasswordResetForm extends SpecialPage { - function __construct( $name=null, $reset=null ) { - if( $name !== null ) { - $this->mName = $name; - $this->mTemporaryPassword = $reset; - } else { - global $wgRequest; - $this->mName = $wgRequest->getVal( 'wpName' ); - $this->mTemporaryPassword = $wgRequest->getVal( 'wpPassword' ); - } +class SpecialResetpass extends SpecialPage { + public function __construct() { + parent::__construct( 'Resetpass' ); } /** @@ -32,36 +19,46 @@ class PasswordResetForm extends SpecialPage { function execute( $par ) { global $wgUser, $wgAuth, $wgOut, $wgRequest; + $this->mUserName = $wgRequest->getVal( 'wpName' ); + $this->mOldpass = $wgRequest->getVal( 'wpPassword' ); + $this->mNewpass = $wgRequest->getVal( 'wpNewPassword' ); + $this->mRetype = $wgRequest->getVal( 'wpRetype' ); + + $this->setHeaders(); + $this->outputHeader(); + if( !$wgAuth->allowPasswordChange() ) { $this->error( wfMsg( 'resetpass_forbidden' ) ); return; } - if( $this->mName === null && !$wgRequest->wasPosted() ) { - $this->error( wfMsg( 'resetpass_missing' ) ); + if( !$wgRequest->wasPosted() && !$wgUser->isLoggedIn() ) { + $this->error( wfMsg( 'resetpass-no-info' ) ); return; } - if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) { - $newpass = $wgRequest->getVal( 'wpNewPassword' ); - $retype = $wgRequest->getVal( 'wpRetype' ); + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal('token') ) ) { try { - $this->attemptReset( $newpass, $retype ); + $this->attemptReset( $this->mNewpass, $this->mRetype ); $wgOut->addWikiMsg( 'resetpass_success' ); - - $data = array( - 'action' => 'submitlogin', - 'wpName' => $this->mName, - 'wpPassword' => $newpass, - 'returnto' => $wgRequest->getVal( 'returnto' ), - ); - if( $wgRequest->getCheck( 'wpRemember' ) ) { - $data['wpRemember'] = 1; + if( !$wgUser->isLoggedIn() ) { + $data = array( + 'action' => 'submitlogin', + 'wpName' => $this->mUserName, + 'wpPassword' => $this->mNewpass, + 'returnto' => $wgRequest->getVal( 'returnto' ), + ); + if( $wgRequest->getCheck( 'wpRemember' ) ) { + $data['wpRemember'] = 1; + } + $login = new LoginForm( new FauxRequest( $data, true ) ); + $login->execute(); } - $login = new LoginForm( new FauxRequest( $data, true ) ); - $login->execute(); - - return; + $titleObj = Title::newFromText( $wgRequest->getVal( 'returnto' ) ); + if ( !$titleObj instanceof Title ) { + $titleObj = Title::newMainPage(); + } + $wgOut->redirect( $titleObj->getFullURL() ); } catch( PasswordError $e ) { $this->error( $e->getMessage() ); } @@ -71,9 +68,7 @@ class PasswordResetForm extends SpecialPage { function error( $msg ) { global $wgOut; - $wgOut->addHtml( '<div class="errorbox">' . - htmlspecialchars( $msg ) . - '</div>' ); + $wgOut->addHTML( Xml::element('p', array( 'class' => 'error' ), $msg ) ); } function showForm() { @@ -82,44 +77,54 @@ class PasswordResetForm extends SpecialPage { $wgOut->disallowUserJs(); $self = SpecialPage::getTitleFor( 'Resetpass' ); - $form = - '<div id="userloginForm">' . - wfOpenElement( 'form', + if ( !$this->mUserName ) { + $this->mUserName = $wgUser->getName(); + } + $rememberMe = ''; + if ( !$wgUser->isLoggedIn() ) { + $rememberMe = '<tr>' . + '<td></td>' . + '<td class="mw-input">' . + Xml::checkLabel( wfMsg( 'remembermypassword' ), + 'wpRemember', 'wpRemember', + $wgRequest->getCheck( 'wpRemember' ) ) . + '</td>' . + '</tr>'; + $submitMsg = 'resetpass_submit'; + $oldpassMsg = 'resetpass-temp-password'; + } else { + $oldpassMsg = 'oldpassword'; + $submitMsg = 'resetpass-submit-loggedin'; + } + $wgOut->addHTML( + Xml::fieldset( wfMsg( 'resetpass_header' ) ) . + Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $self->getLocalUrl() ) ) . - '<h2>' . wfMsgHtml( 'resetpass_header' ) . '</h2>' . - '<div id="userloginprompt">' . + 'action' => $self->getLocalUrl(), + 'id' => 'mw-resetpass-form' ) ) . + Xml::hidden( 'token', $wgUser->editToken() ) . + Xml::hidden( 'wpName', $this->mUserName ) . + Xml::hidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) . wfMsgExt( 'resetpass_text', array( 'parse' ) ) . - '</div>' . - '<table>' . - wfHidden( 'token', $wgUser->editToken() ) . - wfHidden( 'wpName', $this->mName ) . - wfHidden( 'wpPassword', $this->mTemporaryPassword ) . - wfHidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) . + Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) . $this->pretty( array( - array( 'wpName', 'username', 'text', $this->mName ), + array( 'wpName', 'username', 'text', $this->mUserName ), + array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ), array( 'wpNewPassword', 'newpassword', 'password', '' ), - array( 'wpRetype', 'yourpasswordagain', 'password', '' ), + array( 'wpRetype', 'retypenew', 'password', '' ), ) ) . + $rememberMe . '<tr>' . '<td></td>' . - '<td>' . - Xml::checkLabel( wfMsg( 'remembermypassword' ), - 'wpRemember', 'wpRemember', - $wgRequest->getCheck( 'wpRemember' ) ) . - '</td>' . - '</tr>' . - '<tr>' . - '<td></td>' . - '<td>' . - wfSubmitButton( wfMsgHtml( 'resetpass_submit' ) ) . + '<td class="mw-input">' . + Xml::submitButton( wfMsg( $submitMsg ) ) . '</td>' . '</tr>' . - '</table>' . - wfCloseElement( 'form' ) . - '</div>'; - $wgOut->addHtml( $form ); + Xml::closeElement( 'table' ) . + Xml::closeElement( 'form' ) . + Xml::closeElement( 'fieldset' ) + ); } function pretty( $fields ) { @@ -127,16 +132,19 @@ class PasswordResetForm extends SpecialPage { foreach( $fields as $list ) { list( $name, $label, $type, $value ) = $list; if( $type == 'text' ) { - $field = '<tt>' . htmlspecialchars( $value ) . '</tt>'; + $field = htmlspecialchars( $value ); } else { $field = Xml::input( $name, 20, $value, array( 'id' => $name, 'type' => $type ) ); } $out .= '<tr>'; - $out .= '<td align="right">'; - $out .= Xml::label( wfMsg( $label ), $name ); + $out .= "<td class='mw-label'>"; + if ( $type != 'text' ) + $out .= Xml::label( wfMsg( $label ), $name ); + else + $out .= wfMsg( $label ); $out .= '</td>'; - $out .= '<td>'; + $out .= "<td class='mw-input'>"; $out .= $field; $out .= '</td>'; $out .= '</tr>'; @@ -147,21 +155,33 @@ class PasswordResetForm extends SpecialPage { /** * @throws PasswordError when cannot set the new password because requirements not met. */ - function attemptReset( $newpass, $retype ) { - $user = User::newFromName( $this->mName ); - if( $user->isAnon() ) { + protected function attemptReset( $newpass, $retype ) { + $user = User::newFromName( $this->mUserName ); + if( !$user || $user->isAnon() ) { throw new PasswordError( 'no such user' ); } - - if( !$user->checkTemporaryPassword( $this->mTemporaryPassword ) ) { - throw new PasswordError( wfMsg( 'resetpass_bad_temporary' ) ); - } - + if( $newpass !== $retype ) { + wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'badretype' ) ); throw new PasswordError( wfMsg( 'badretype' ) ); } - $user->setPassword( $newpass ); + if( !$user->checkTemporaryPassword($this->mOldpass) && !$user->checkPassword($this->mOldpass) ) { + wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'wrongpassword' ) ); + throw new PasswordError( wfMsg( 'resetpass-wrong-oldpass' ) ); + } + + try { + $user->setPassword( $this->mNewpass ); + wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'success' ) ); + $this->mNewpass = $this->mOldpass = $this->mRetypePass = ''; + } catch( PasswordError $e ) { + wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'error' ) ); + throw new PasswordError( $e->getMessage() ); + return; + } + + $user->setCookies(); $user->saveSettings(); } } diff --git a/includes/specials/SpecialRestrictUser.php b/includes/specials/SpecialRestrictUser.php new file mode 100644 index 00000000..761e0cd6 --- /dev/null +++ b/includes/specials/SpecialRestrictUser.php @@ -0,0 +1,189 @@ +<?php + +function wfSpecialRestrictUser( $par = null ) { + global $wgOut, $wgRequest; + $user = $userOrig = null; + if( $par ) { + $userOrig = $par; + } elseif( $wgRequest->getVal( 'user' ) ) { + $userOrig = $wgRequest->getVal( 'user' ); + } else { + $wgOut->addHTML( RestrictUserForm::selectUserForm() ); + return; + } + $isIP = User::isIP( $userOrig ); + $user = $isIP ? $userOrig : User::getCanonicalName( $userOrig ); + $uid = User::idFromName( $user ); + if( !$uid && !$isIP ) { + $err = '<strong class="error">' . wfMsgHtml( 'restrictuser-notfound' ) . '</strong>'; + $wgOut->addHTML( RestrictUserForm::selectUserForm( $userOrig, $err ) ); + return; + } + $wgOut->addHTML( RestrictUserForm::selectUserForm( $user ) ); + + UserRestriction::purgeExpired(); + $old = UserRestriction::fetchForUser( $user, true ); + + RestrictUserForm::pageRestrictionForm( $uid, $user, $old ); + RestrictUserForm::namespaceRestrictionForm( $uid, $user, $old ); + + // Renew it after possible changes in previous two functions + $old = UserRestriction::fetchForUser( $user, true ); + if( $old ) { + $wgOut->addHTML( RestrictUserForm::existingRestrictions( $old ) ); + } +} + +class RestrictUserForm { + public static function selectUserForm( $val = null, $error = null ) { + global $wgScript, $wgTitle; + $s = Xml::fieldset( wfMsg( 'restrictuser-userselect' ) ) . "<form action=\"{$wgScript}\">"; + if( $error ) + $s .= '<p>' . $error . '</p>'; + $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); + $form = array( 'restrictuser-user' => Xml::input( 'user', false, $val ) ); + $s .= Xml::buildForm( $form, 'restrictuser-go' ); + $s .= "</form></fieldset>"; + return $s; + } + + public static function existingRestrictions( $restrictions ) { + //TODO: autoload? + require_once( dirname( __FILE__ ) . '/SpecialListUserRestrictions.php' ); + $s = Xml::fieldset( wfMsg( 'restrictuser-existing' ) ) . '<ul>'; + foreach( $restrictions as $r ) + $s .= UserRestrictionsPager::formatRestriction( $r ); + $s .= "</ul></fieldset>"; + return $s; + } + + public static function pageRestrictionForm( $uid, $user, $oldRestrictions ) { + global $wgOut, $wgTitle, $wgRequest, $wgUser; + $error = ''; + $success = false; + if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::PAGE && + $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) { + + $title = Title::newFromText( $wgRequest->getVal( 'page' ) ); + if( !$title ) { + $error = array( 'restrictuser-badtitle', $wgRequest->getVal( 'page' ) ); + } elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false ) { + $error = array( 'restrictuser-badexpiry', $wgRequest->getVal( 'expiry' ) ); + } else { + foreach( $oldRestrictions as $r ) { + if( $r->isPage() && $r->getPage()->equals( $title ) ) + $error = array( 'restrictuser-duptitle' ); + } + } + if( !$error ) { + self::doPageRestriction( $uid, $user ); + $success = array('restrictuser-success', $user); + } + } + $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::PAGE; + $wgOut->addHTML( Xml::fieldset( wfMsg( 'restrictuser-legend-page' ) ) ); + + self::printSuccessError( $success, $error ); + + $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(), + 'method' => 'post' ) ) ); + $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::PAGE ) ); + $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); + $wgOut->addHTML( Xml::hidden( 'user', $user ) ); + $form = array(); + $form['restrictuser-title'] = Xml::input( 'page', false, + $useRequestValues ? $wgRequest->getVal( 'page' ) : false ); + $form['restrictuser-expiry'] = Xml::input( 'expiry', false, + $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false ); + $form['restrictuser-reason'] = Xml::input( 'reason', false, + $useRequestValues ? $wgRequest->getVal( 'reason' ) : false ); + $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-submit' ) ); + $wgOut->addHTML( "</form></fieldset>" ); + } + + public static function printSuccessError( $success, $error ) { + global $wgOut; + if ( $error ) + $wgOut->wrapWikiMsg( '<strong class="error">$1</strong>', $error ); + if ( $success ) + $wgOut->wrapWikiMsg( '<strong class="success">$1</strong>', $success ); + } + + public static function doPageRestriction( $uid, $user ) { + global $wgUser, $wgRequest; + $r = new UserRestriction(); + $r->setType( UserRestriction::PAGE ); + $r->setPage( Title::newFromText( $wgRequest->getVal( 'page' ) ) ); + $r->setSubjectId( $uid ); + $r->setSubjectText( $user ); + $r->setBlockerId( $wgUser->getId() ); + $r->setBlockerText( $wgUser->getName() ); + $r->setReason( $wgRequest->getVal( 'reason' ) ); + $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) ); + $r->setTimestamp( wfTimestampNow( TS_MW ) ); + $r->commit(); + $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity(); + $l = new LogPage( 'restrict' ); + $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(), + array( $r->getType(), $r->getPage()->getFullText(), $logExpiry) ); + } + + public static function namespaceRestrictionForm( $uid, $user, $oldRestrictions ) { + global $wgOut, $wgTitle, $wgRequest, $wgUser, $wgContLang; + $error = ''; + $success = false; + if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE && + $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) { + $ns = $wgRequest->getVal( 'namespace' ); + if( $wgContLang->getNsText( $ns ) === false ) + $error = wfMsgExt( 'restrictuser-badnamespace', 'parseinline' ); + elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false ) + $error = wfMsgExt( 'restrictuser-badexpiry', 'parseinline', $wgRequest->getVal( 'expiry' ) ); + else + foreach( $oldRestrictions as $r ) + if( $r->isNamespace() && $r->getNamespace() == $ns ) + $error = wfMsgExt( 'restrictuser-dupnamespace', 'parse' ); + if( !$error ) { + self::doNamespaceRestriction( $uid, $user ); + $success = array('restrictuser-success', $user); + } + } + $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE; + $wgOut->addHTML( Xml::fieldset( wfMsg( 'restrictuser-legend-namespace' ) ) ); + + self::printSuccessError( $success, $error ); + + $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(), + 'method' => 'post' ) ) ); + $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::NAMESPACE ) ); + $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); + $wgOut->addHTML( Xml::hidden( 'user', $user ) ); + $form = array(); + $form['restrictuser-namespace'] = Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ) ); + $form['restrictuser-expiry'] = Xml::input( 'expiry', false, + $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false ); + $form['restrictuser-reason'] = Xml::input( 'reason', false, + $useRequestValues ? $wgRequest->getVal( 'reason' ) : false ); + $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-submit' ) ); + $wgOut->addHTML( "</form></fieldset>" ); + } + + public static function doNamespaceRestriction( $uid, $user ) { + global $wgUser, $wgRequest; + $r = new UserRestriction(); + $r->setType( UserRestriction::NAMESPACE ); + $r->setNamespace( $wgRequest->getVal( 'namespace' ) ); + $r->setSubjectId( $uid ); + $r->setSubjectText( $user ); + $r->setBlockerId( $wgUser->getId() ); + $r->setBlockerText( $wgUser->getName() ); + $r->setReason( $wgRequest->getVal( 'reason' ) ); + $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) ); + $r->setTimestamp( wfTimestampNow( TS_MW ) ); + $r->commit(); + $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity(); + $l = new LogPage( 'restrict' ); + $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(), + array( $r->getType(), $r->getNamespace(), $logExpiry ) ); + } +} diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php index e94fc222..74b118e2 100644 --- a/includes/specials/SpecialRevisiondelete.php +++ b/includes/specials/SpecialRevisiondelete.php @@ -171,7 +171,7 @@ class RevisionDeleteForm { $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count ); $bitfields = 0; - $wgOut->addHtml( "<ul>" ); + $wgOut->addHTML( "<ul>" ); $where = $revObjs = array(); $dbr = wfGetDB( DB_SLAVE ); @@ -204,7 +204,7 @@ class RevisionDeleteForm { $UserAllowed = false; } $revisions++; - $wgOut->addHtml( $this->historyLine( $revObjs[$revid] ) ); + $wgOut->addHTML( $this->historyLine( $revObjs[$revid] ) ); $bitfields |= $revObjs[$revid]->mDeleted; } // The archives... @@ -245,7 +245,7 @@ class RevisionDeleteForm { $UserAllowed = false; } $revisions++; - $wgOut->addHtml( $this->historyLine( $revObjs[$timestamp] ) ); + $wgOut->addHTML( $this->historyLine( $revObjs[$timestamp] ) ); $bitfields |= $revObjs[$timestamp]->mDeleted; } } @@ -254,7 +254,7 @@ class RevisionDeleteForm { return; } - $wgOut->addHtml( "</ul>" ); + $wgOut->addHTML( "</ul>" ); $wgOut->addWikiMsg( 'revdelete-text' ); @@ -278,7 +278,7 @@ class RevisionDeleteForm { $hidden[] = Xml::hidden( 'artimestamp[]', $rev->getTimestamp() ); } $special = SpecialPage::getTitleFor( 'Revisiondelete' ); - $wgOut->addHtml( + $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 'id' => 'mw-revdel-form-revisions' ) ) . Xml::openElement( 'fieldset' ) . @@ -287,15 +287,15 @@ class RevisionDeleteForm { // FIXME: all items checked for just one rev are checked, even if not set for the others foreach( $this->checks as $item ) { list( $message, $name, $field ) = $item; - $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) ); + $wgOut->addHTML( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) ); } foreach( $items as $item ) { - $wgOut->addHtml( Xml::tags( 'p', null, $item ) ); + $wgOut->addHTML( Xml::tags( 'p', null, $item ) ); } foreach( $hidden as $item ) { - $wgOut->addHtml( $item ); + $wgOut->addHTML( $item ); } - $wgOut->addHtml( + $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n" ); @@ -317,7 +317,7 @@ class RevisionDeleteForm { $wgLang->formatNum($count) ); $bitfields = 0; - $wgOut->addHtml( "<ul>" ); + $wgOut->addHTML( "<ul>" ); $where = $filesObjs = array(); $dbr = wfGetDB( DB_SLAVE ); @@ -326,11 +326,11 @@ class RevisionDeleteForm { if( $this->deleteKey=='oldimage' ) { // Run through and pull all our data in one query foreach( $this->ofiles as $timestamp ) { - $where[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDbKey() ); + $where[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDBKey() ); } $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')'; $result = $dbr->select( 'oldimage', '*', - array( 'oi_name' => $this->page->getDbKey(), + array( 'oi_name' => $this->page->getDBKey(), $whereClause ), __METHOD__ ); while( $row = $dbr->fetchObject( $result ) ) { @@ -340,7 +340,7 @@ class RevisionDeleteForm { } // Check through our images foreach( $this->ofiles as $timestamp ) { - $archivename = $timestamp.'!'.$this->page->getDbKey(); + $archivename = $timestamp.'!'.$this->page->getDBKey(); if( !isset($filesObjs[$archivename]) ) { continue; } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) { @@ -353,7 +353,7 @@ class RevisionDeleteForm { } $revisions++; // Inject history info - $wgOut->addHtml( $this->fileLine( $filesObjs[$archivename] ) ); + $wgOut->addHTML( $this->fileLine( $filesObjs[$archivename] ) ); $bitfields |= $filesObjs[$archivename]->deleted; } // Archived files... @@ -364,7 +364,7 @@ class RevisionDeleteForm { } $whereClause = 'fa_id IN(' . implode(',',$where) . ')'; $result = $dbr->select( 'filearchive', '*', - array( 'fa_name' => $this->page->getDbKey(), + array( 'fa_name' => $this->page->getDBKey(), $whereClause ), __METHOD__ ); while( $row = $dbr->fetchObject( $result ) ) { @@ -384,7 +384,7 @@ class RevisionDeleteForm { } $revisions++; // Inject history info - $wgOut->addHtml( $this->archivedfileLine( $filesObjs[$fileid] ) ); + $wgOut->addHTML( $this->archivedfileLine( $filesObjs[$fileid] ) ); $bitfields |= $filesObjs[$fileid]->deleted; } } @@ -393,7 +393,7 @@ class RevisionDeleteForm { return; } - $wgOut->addHtml( "</ul>" ); + $wgOut->addHTML( "</ul>" ); $wgOut->addWikiMsg('revdelete-text' ); //Normal sysops can always see what they did, but can't always change it @@ -416,7 +416,7 @@ class RevisionDeleteForm { $hidden[] = Xml::hidden( 'fileid[]', $fileid ); } $special = SpecialPage::getTitleFor( 'Revisiondelete' ); - $wgOut->addHtml( + $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 'id' => 'mw-revdel-form-filerevisions' ) ) . Xml::fieldset( wfMsg( 'revdelete-legend' ) ) @@ -424,16 +424,16 @@ class RevisionDeleteForm { // FIXME: all items checked for just one file are checked, even if not set for the others foreach( $this->checks as $item ) { list( $message, $name, $field ) = $item; - $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) ); + $wgOut->addHTML( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) ); } foreach( $items as $item ) { - $wgOut->addHtml( "<p>$item</p>" ); + $wgOut->addHTML( "<p>$item</p>" ); } foreach( $hidden as $item ) { - $wgOut->addHtml( $item ); + $wgOut->addHTML( $item ); } - $wgOut->addHtml( + $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n" ); @@ -449,7 +449,7 @@ class RevisionDeleteForm { $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) ); $bitfields = 0; - $wgOut->addHtml( "<ul>" ); + $wgOut->addHTML( "<ul>" ); $where = $logRows = array(); $dbr = wfGetDB( DB_SLAVE ); @@ -480,7 +480,7 @@ class RevisionDeleteForm { $UserAllowed = false; } $logItems++; - $wgOut->addHtml( $this->logLine( $logRows[$logid] ) ); + $wgOut->addHTML( $this->logLine( $logRows[$logid] ) ); $bitfields |= $logRows[$logid]->log_deleted; } if( !$logItems ) { @@ -488,7 +488,7 @@ class RevisionDeleteForm { return; } - $wgOut->addHtml( "</ul>" ); + $wgOut->addHTML( "</ul>" ); $wgOut->addWikiMsg( 'revdelete-text' ); // Normal sysops can always see what they did, but can't always change it @@ -506,7 +506,7 @@ class RevisionDeleteForm { } $special = SpecialPage::getTitleFor( 'Revisiondelete' ); - $wgOut->addHtml( + $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 'id' => 'mw-revdel-form-logs' ) ) . Xml::fieldset( wfMsg( 'revdelete-legend' ) ) @@ -514,16 +514,16 @@ class RevisionDeleteForm { // FIXME: all items checked for just on event are checked, even if not set for the others foreach( $this->checks as $item ) { list( $message, $name, $field ) = $item; - $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) ); + $wgOut->addHTML( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) ); } foreach( $items as $item ) { - $wgOut->addHtml( "<p>$item</p>" ); + $wgOut->addHTML( "<p>$item</p>" ); } foreach( $hidden as $item ) { - $wgOut->addHtml( $item ); + $wgOut->addHTML( $item ); } - $wgOut->addHtml( + $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n" ); @@ -606,7 +606,7 @@ class RevisionDeleteForm { * @returns string */ private function archivedfileLine( $file ) { - global $wgLang, $wgTitle; + global $wgLang; $target = $this->page->getPrefixedText(); $date = $wgLang->timeanddate( $file->getTimestamp(), true ); @@ -939,11 +939,11 @@ class RevisionDeleter { $set = array(); // Run through and pull all our data in one query foreach( $items as $timestamp ) { - $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() ); + $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDBKey() ); } $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')'; $result = $this->dbw->select( 'oldimage', '*', - array( 'oi_name' => $title->getDbKey(), + array( 'oi_name' => $title->getDBKey(), $whereClause ), __METHOD__ ); while( $row = $this->dbw->fetchObject( $result ) ) { @@ -953,7 +953,7 @@ class RevisionDeleter { } // To work! foreach( $items as $timestamp ) { - $archivename = $timestamp.'!'.$title->getDbKey(); + $archivename = $timestamp.'!'.$title->getDBKey(); if( !isset($filesObjs[$archivename]) ) { $success = false; continue; // Must exist @@ -1036,7 +1036,7 @@ class RevisionDeleter { } $whereClause = 'fa_id IN(' . implode(',',$where) . ')'; $result = $this->dbw->select( 'filearchive', '*', - array( 'fa_name' => $title->getDbKey(), + array( 'fa_name' => $title->getDBKey(), $whereClause ), __METHOD__ ); while( $row = $this->dbw->fetchObject( $result ) ) { @@ -1344,7 +1344,7 @@ class RevisionDeleter { function updatePage( $title ) { $title->invalidateCache(); $title->purgeSquid(); - + $title->touchLinks(); // Extensions that require referencing previous revisions may need this wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) ); } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index f13c1676..f3117242 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -29,13 +29,18 @@ * @param $par String: (default '') */ function wfSpecialSearch( $par = '' ) { - global $wgRequest, $wgUser; - - $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $par ) ); - $searchPage = new SpecialSearch( $wgRequest, $wgUser ); + global $wgRequest, $wgUser, $wgUseOldSearchUI; + // Strip underscores from title parameter; most of the time we'll want + // text form here. But don't strip underscores from actual text params! + $titleParam = str_replace( '_', ' ', $par ); + // Fetch the search term + $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $titleParam ) ); + $class = $wgUseOldSearchUI ? 'SpecialSearchOld' : 'SpecialSearch'; + $searchPage = new $class( $wgRequest, $wgUser ); if( $wgRequest->getVal( 'fulltext' ) || !is_null( $wgRequest->getVal( 'offset' )) - || !is_null( $wgRequest->getVal( 'searchx' ))) { + || !is_null( $wgRequest->getVal( 'searchx' )) ) + { $searchPage->showResults( $search, 'search' ); } else { $searchPage->goResult( $search ); @@ -56,9 +61,806 @@ class SpecialSearch { * @param User $user * @public */ - function SpecialSearch( &$request, &$user ) { + function __construct( &$request, &$user ) { list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' ); + $this->mPrefix = $request->getVal('prefix', ''); + # Extract requested namespaces + $this->namespaces = $this->powerSearch( $request ); + if( empty( $this->namespaces ) ) { + $this->namespaces = SearchEngine::userNamespaces( $user ); + } + $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false; + $this->searchAdvanced = $request->getVal( 'advanced' ); + $this->active = 'advanced'; + $this->sk = $user->getSkin(); + $this->didYouMeanHtml = ''; # html of did you mean... link + } + + /** + * If an exact title match can be found, jump straight ahead to it. + * @param string $term + */ + public function goResult( $term ) { + global $wgOut; + $this->setupPage( $term ); + # Try to go to page as entered. + $t = Title::newFromText( $term ); + # If the string cannot be used to create a title + if( is_null( $t ) ) { + return $this->showResults( $term ); + } + # If there's an exact or very near match, jump right there. + $t = SearchEngine::getNearMatch( $term ); + if( !is_null( $t ) ) { + $wgOut->redirect( $t->getFullURL() ); + return; + } + # No match, generate an edit URL + $t = Title::newFromText( $term ); + if( !is_null( $t ) ) { + global $wgGoToEdit; + wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) ); + # If the feature is enabled, go straight to the edit page + if( $wgGoToEdit ) { + $wgOut->redirect( $t->getFullURL( 'action=edit' ) ); + return; + } + } + return $this->showResults( $term ); + } + + /** + * @param string $term + */ + public function showResults( $term ) { + global $wgOut, $wgUser, $wgDisableTextSearch, $wgContLang; + wfProfileIn( __METHOD__ ); + + $sk = $wgUser->getSkin(); + + $this->searchEngine = SearchEngine::create(); + $search =& $this->searchEngine; + $search->setLimitOffset( $this->limit, $this->offset ); + $search->setNamespaces( $this->namespaces ); + $search->showRedirects = $this->searchRedirects; + $search->prefix = $this->mPrefix; + $term = $search->transformSearchTerm($term); + + $this->setupPage( $term ); + + if( $wgDisableTextSearch ) { + global $wgSearchForwardUrl; + if( $wgSearchForwardUrl ) { + $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl ); + $wgOut->redirect( $url ); + wfProfileOut( __METHOD__ ); + return; + } + global $wgInputEncoding; + $wgOut->addHTML( + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMsg( 'search-external' ) ) . + Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) . + wfMsg( 'googlesearch', + htmlspecialchars( $term ), + htmlspecialchars( $wgInputEncoding ), + htmlspecialchars( wfMsg( 'searchbutton' ) ) + ) . + Xml::closeElement( 'fieldset' ) + ); + wfProfileOut( __METHOD__ ); + return; + } + + $t = Title::newFromText( $term ); + + // fetch search results + $rewritten = $search->replacePrefixes($term); + + $titleMatches = $search->searchTitle( $rewritten ); + if( !($titleMatches instanceof SearchResultTooMany)) + $textMatches = $search->searchText( $rewritten ); + + // did you mean... suggestions + if( $textMatches && $textMatches->hasSuggestion() ) { + $st = SpecialPage::getTitleFor( 'Search' ); + $stParams = wfArrayToCGI( + array( 'search' => $textMatches->getSuggestionQuery(), 'fulltext' => wfMsg('search') ), + $this->powerSearchOptions() + ); + $suggestLink = $sk->makeKnownLinkObj( $st, + $textMatches->getSuggestionSnippet(), + $stParams ); + + $this->didYouMeanHtml = '<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>'; + } + + // start rendering the page + $wgOut->addHtml( + Xml::openElement( 'table', array( 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) . + Xml::openElement( 'tr' ) . + Xml::openElement( 'td' ) . "\n" . + ( $this->searchAdvanced ? $this->powerSearchBox( $term ) : $this->shortDialog( $term ) ) . + Xml::closeElement('td') . + Xml::closeElement('tr') . + Xml::closeElement('table') + ); + + // Sometimes the search engine knows there are too many hits + if( $titleMatches instanceof SearchResultTooMany ) { + $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" ); + wfProfileOut( __METHOD__ ); + return; + } + + $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':'; + if( '' === trim( $term ) || $filePrefix === trim( $term ) ) { + $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() ); + // Empty query -- straight view of search form + wfProfileOut( __METHOD__ ); + return; + } + + // show direct page/create link + if( !is_null($t) ) { + if( !$t->exists() ) { + $wgOut->addWikiMsg( 'searchmenu-new', wfEscapeWikiText( $t->getPrefixedText() ) ); + } else { + $wgOut->addWikiMsg( 'searchmenu-exists', wfEscapeWikiText( $t->getPrefixedText() ) ); + } + } + + // Get number of results + $titleMatchesSQL = $titleMatches ? $titleMatches->numRows() : 0; + $textMatchesSQL = $textMatches ? $textMatches->numRows() : 0; + // Total initial query matches (possible false positives) + $numSQL = $titleMatchesSQL + $textMatchesSQL; + // Get total actual results (after second filtering, if any) + $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ? + $titleMatches->getTotalHits() : $titleMatchesSQL; + $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ? + $textMatches->getTotalHits() : $textMatchesSQL; + $totalRes = $numTitleMatches + $numTextMatches; + + // show number of results and current offset + if( $numSQL > 0 ) { + if( $numSQL > 0 ) { + $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), + $this->offset+1, $this->offset+$numSQL, $totalRes, $numSQL ); + } elseif( $numSQL >= $this->limit ) { + $top = wfShowingResults( $this->offset, $this->limit ); + } else { + $top = wfShowingResultsNum( $this->offset, $this->limit, $numSQL ); + } + $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" ); + } + + // prev/next links + if( $numSQL || $this->offset ) { + $prevnext = wfViewPrevNext( $this->offset, $this->limit, + SpecialPage::getTitleFor( 'Search' ), + wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ), + max( $titleMatchesSQL, $textMatchesSQL ) < $this->limit + ); + $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" ); + wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); + } else { + wfRunHooks( 'SpecialSearchNoResults', array( $term ) ); + } + + $wgOut->addHtml( "<div class='searchresults'>" ); + if( $titleMatches ) { + if( $numTitleMatches > 0 ) { + $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' ); + $wgOut->addHTML( $this->showMatches( $titleMatches ) ); + } + $titleMatches->free(); + } + if( $textMatches ) { + // output appropriate heading + if( $numTextMatches > 0 && $numTitleMatches > 0 ) { + // if no title matches the heading is redundant + $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' ); + } elseif( $totalRes == 0 ) { + # Don't show the 'no text matches' if we received title matches + $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' ); + } + // show interwiki results if any + if( $textMatches->hasInterwikiResults() ) { + $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) ); + } + // show results + if( $numTextMatches > 0 ) { + $wgOut->addHTML( $this->showMatches( $textMatches ) ); + } + + $textMatches->free(); + } + if( $totalRes === 0 ) { + $wgOut->addWikiMsg( 'search-nonefound' ); + } + $wgOut->addHtml( "</div>" ); + if( $totalRes === 0 ) { + $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() ); + } + + if( $numSQL || $this->offset ) { + $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); + } + wfProfileOut( __METHOD__ ); + } + + /** + * + */ + protected function setupPage( $term ) { + global $wgOut; + // Figure out the active search profile header + $nsAllSet = array_keys( SearchEngine::searchableNamespaces() ); + if( $this->searchAdvanced ) + $this->active = 'advanced'; + else if( $this->namespaces === NS_FILE || $this->startsWithImage( $term ) ) + $this->active = 'images'; + elseif( $this->namespaces === $nsAllSet ) + $this->active = 'all'; + elseif( $this->namespaces === SearchEngine::defaultNamespaces() ) + $this->active = 'default'; + elseif( $this->namespaces === SearchEngine::projectNamespaces() ) + $this->active = 'project'; + else + $this->active = 'advanced'; + # Should advanced UI be used? + $this->searchAdvanced = ($this->active === 'advanced'); + if( !empty( $term ) ) { + $wgOut->setPageTitle( wfMsg( 'searchresults') ); + $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) ); + } + $wgOut->setArticleRelated( false ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + } + + /** + * Extract "power search" namespace settings from the request object, + * returning a list of index numbers to search. + * + * @param WebRequest $request + * @return array + */ + protected function powerSearch( &$request ) { + $arr = array(); + foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { + if( $request->getCheck( 'ns' . $ns ) ) { + $arr[] = $ns; + } + } + return $arr; + } + /** + * Reconstruct the 'power search' options for links + * @return array + */ + protected function powerSearchOptions() { + $opt = array(); + foreach( $this->namespaces as $n ) { + $opt['ns' . $n] = 1; + } + $opt['redirs'] = $this->searchRedirects ? 1 : 0; + if( $this->searchAdvanced ) { + $opt['advanced'] = $this->searchAdvanced; + } + return $opt; + } + + /** + * Show whole set of results + * + * @param SearchResultSet $matches + */ + protected function showMatches( &$matches ) { + global $wgContLang; + wfProfileIn( __METHOD__ ); + + $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); + + $out = ""; + $infoLine = $matches->getInfo(); + if( !is_null($infoLine) ) { + $out .= "\n<!-- {$infoLine} -->\n"; + } + $off = $this->offset + 1; + $out .= "<ul class='mw-search-results'>\n"; + while( $result = $matches->next() ) { + $out .= $this->showHit( $result, $terms ); + } + $out .= "</ul>\n"; + + // convert the whole thing to desired language variant + $out = $wgContLang->convert( $out ); + wfProfileOut( __METHOD__ ); + return $out; + } + + /** + * Format a single hit result + * @param SearchResult $result + * @param array $terms terms to highlight + */ + protected function showHit( $result, $terms ) { + global $wgContLang, $wgLang, $wgUser; + wfProfileIn( __METHOD__ ); + + if( $result->isBrokenTitle() ) { + wfProfileOut( __METHOD__ ); + return "<!-- Broken link in search result -->\n"; + } + + $sk = $wgUser->getSkin(); + $t = $result->getTitle(); + + $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms)); + + //If page content is not readable, just return the title. + //This is not quite safe, but better than showing excerpts from non-readable pages + //Note that hiding the entry entirely would screw up paging. + if( !$t->userCanRead() ) { + wfProfileOut( __METHOD__ ); + return "<li>{$link}</li>\n"; + } + + // If the page doesn't *exist*... our search index is out of date. + // The least confusing at this point is to drop the result. + // You may get less results, but... oh well. :P + if( $result->isMissingRevision() ) { + wfProfileOut( __METHOD__ ); + return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n"; + } + + // format redirects / relevant sections + $redirectTitle = $result->getRedirectTitle(); + $redirectText = $result->getRedirectSnippet($terms); + $sectionTitle = $result->getSectionTitle(); + $sectionText = $result->getSectionSnippet($terms); + $redirect = ''; + if( !is_null($redirectTitle) ) + $redirect = "<span class='searchalttitle'>" + .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText)) + ."</span>"; + $section = ''; + if( !is_null($sectionTitle) ) + $section = "<span class='searchalttitle'>" + .wfMsg('search-section', $this->sk->makeKnownLinkObj( $sectionTitle, $sectionText)) + ."</span>"; + + // format text extract + $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>"; + + // format score + if( is_null( $result->getScore() ) ) { + // Search engine doesn't report scoring info + $score = ''; + } else { + $percent = sprintf( '%2.1f', $result->getScore() * 100 ); + $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) ) + . ' - '; + } + + // format description + $byteSize = $result->getByteSize(); + $wordCount = $result->getWordCount(); + $timestamp = $result->getTimestamp(); + $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ), + $this->sk->formatSize( $byteSize ), $wordCount ); + $date = $wgLang->timeanddate( $timestamp ); + + // link to related articles if supported + $related = ''; + if( $result->hasRelated() ) { + $st = SpecialPage::getTitleFor( 'Search' ); + $stParams = wfArrayToCGI( $this->powerSearchOptions(), + array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(), + 'fulltext' => wfMsg('search') )); + + $related = ' -- ' . $sk->makeKnownLinkObj( $st, + wfMsg('search-relatedarticle'), $stParams ); + } + + // Include a thumbnail for media files... + if( $t->getNamespace() == NS_FILE ) { + $img = wfFindFile( $t ); + if( $img ) { + $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) ); + if( $thumb ) { + $desc = $img->getShortDesc(); + wfProfileOut( __METHOD__ ); + // Float doesn't seem to interact well with the bullets. + // Table messes up vertical alignment of the bullets. + // Bullets are therefore disabled (didn't look great anyway). + return "<li>" . + '<table class="searchResultImage">' . + '<tr>' . + '<td width="120" align="center" valign="top">' . + $thumb->toHtml( array( 'desc-link' => true ) ) . + '</td>' . + '<td valign="top">' . + $link . + $extract . + "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" . + '</td>' . + '</tr>' . + '</table>' . + "</li>\n"; + } + } + } + + wfProfileOut( __METHOD__ ); + return "<li>{$link} {$redirect} {$section} {$extract}\n" . + "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" . + "</li>\n"; + + } + + /** + * Show results from other wikis + * + * @param SearchResultSet $matches + */ + protected function showInterwiki( &$matches, $query ) { + global $wgContLang; + wfProfileIn( __METHOD__ ); + $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); + + $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>". + wfMsg('search-interwiki-caption')."</div>\n"; + $off = $this->offset + 1; + $out .= "<ul class='mw-search-iwresults'>\n"; + + // work out custom project captions + $customCaptions = array(); + $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption> + foreach($customLines as $line) { + $parts = explode(":",$line,2); + if(count($parts) == 2) // validate line + $customCaptions[$parts[0]] = $parts[1]; + } + + $prev = null; + while( $result = $matches->next() ) { + $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions ); + $prev = $result->getInterwikiPrefix(); + } + // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax).. + $out .= "</ul></div>\n"; + + // convert the whole thing to desired language variant + $out = $wgContLang->convert( $out ); + wfProfileOut( __METHOD__ ); + return $out; + } + + /** + * Show single interwiki link + * + * @param SearchResult $result + * @param string $lastInterwiki + * @param array $terms + * @param string $query + * @param array $customCaptions iw prefix -> caption + */ + protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) { + wfProfileIn( __METHOD__ ); + global $wgContLang, $wgLang; + + if( $result->isBrokenTitle() ) { + wfProfileOut( __METHOD__ ); + return "<!-- Broken link in search result -->\n"; + } + + $t = $result->getTitle(); + + $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms)); + + // format redirect if any + $redirectTitle = $result->getRedirectTitle(); + $redirectText = $result->getRedirectSnippet($terms); + $redirect = ''; + if( !is_null($redirectTitle) ) + $redirect = "<span class='searchalttitle'>" + .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText)) + ."</span>"; + + $out = ""; + // display project name + if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) { + if( key_exists($t->getInterwiki(),$customCaptions) ) + // captions from 'search-interwiki-custom' + $caption = $customCaptions[$t->getInterwiki()]; + else{ + // default is to show the hostname of the other wiki which might suck + // if there are many wikis on one hostname + $parsed = parse_url($t->getFullURL()); + $caption = wfMsg('search-interwiki-default', $parsed['host']); + } + // "more results" link (special page stuff could be localized, but we might not know target lang) + $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search"); + $searchLink = $this->sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'), + wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); + $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'> + {$searchLink}</span>{$caption}</div>\n<ul>"; + } + + $out .= "<li>{$link} {$redirect}</li>\n"; + wfProfileOut( __METHOD__ ); + return $out; + } + + + /** + * Generates the power search box at bottom of [[Special:Search]] + * @param $term string: search term + * @return $out string: HTML form + */ + protected function powerSearchBox( $term ) { + global $wgScript; + + $namespaces = SearchEngine::searchableNamespaces(); + + $tables = $this->namespaceTables( $namespaces ); + + $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) ); + $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' ); + $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) ); + $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' )) . "\n"; + $searchTitle = SpecialPage::getTitleFor( 'Search' ); + + $redirectText = ''; + // show redirects check only if backend supports it + if( $this->searchEngine->acceptListRedirects() ) { + $redirectText = "<p>". $redirect . " " . $redirectLabel ."</p>"; + } + + $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) . + Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n" . + "<p>" . + wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . + "</p>\n" . + '<input type="hidden" name="advanced" value="'.$this->searchAdvanced."\"/>\n". + $tables . + "<hr style=\"clear: both;\" />\n". + $redirectText ."\n". + "<div style=\"padding-top:2px;padding-bottom:2px;\">". + wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) . + " " . + $searchField . + " " . + $searchButton . + "</div>". + "</form>"; + $t = Title::newFromText( $term ); + /* if( $t != null && count($this->namespaces) === 1 ) { + $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term ); + } */ + return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) . + Xml::element( 'legend', null, wfMsg('powersearch-legend') ) . + $this->formHeader($term) . $out . $this->didYouMeanHtml . + Xml::closeElement( 'fieldset' ); + } + + protected function searchFocus() { + global $wgJsMimeType; + return "<script type=\"$wgJsMimeType\">" . + "hookEvent(\"load\", function() {" . + "document.getElementById('searchText').focus();" . + "});" . + "</script>"; + } + + protected function powerSearchFocus() { + global $wgJsMimeType; + return "<script type=\"$wgJsMimeType\">" . + "hookEvent(\"load\", function() {" . + "document.getElementById('powerSearchText').focus();" . + "});" . + "</script>"; + } + + protected function formHeader( $term ) { + global $wgContLang, $wgCanonicalNamespaceNames; + + $sep = ' '; + $out = Xml::openElement('div', array( 'style' => 'padding-bottom:0.5em;' ) ); + + $bareterm = $term; + if( $this->startsWithImage( $term ) ) + $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); // delete all/image prefix + + $nsAllSet = array_keys( SearchEngine::searchableNamespaces() ); + + // search profiles headers + $m = wfMsg( 'searchprofile-articles' ); + $tt = wfMsg( 'searchprofile-articles-tooltip', + implode( ', ', SearchEngine::namespacesAsText( SearchEngine::defaultNamespaces() ) ) ); + if( $this->active == 'default' ) { + $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); + } else { + $out .= $this->makeSearchLink( $bareterm, SearchEngine::defaultNamespaces(), $m, $tt ); + } + $out .= $sep; + + $m = wfMsg( 'searchprofile-images' ); + $tt = wfMsg( 'searchprofile-images-tooltip' ); + if( $this->active == 'images' ) { + $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); + } else { + $imageTextForm = $wgContLang->getFormattedNsText(NS_FILE).':'.$bareterm; + $out .= $this->makeSearchLink( $imageTextForm, array( NS_FILE ) , $m, $tt ); + } + $out .= $sep; + + /* + $m = wfMsg( 'searchprofile-articles-and-proj' ); + $tt = wfMsg( 'searchprofile-project-tooltip', + implode( ', ', SearchEngine::namespacesAsText( SearchEngine::defaultAndProjectNamespaces() ) ) ); + if( $this->active == 'withproject' ) { + $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); + } else { + $out .= $this->makeSearchLink( $bareterm, SearchEngine::defaultAndProjectNamespaces(), $m, $tt ); + } + $out .= $sep; + */ + + $m = wfMsg( 'searchprofile-project' ); + $tt = wfMsg( 'searchprofile-project-tooltip', + implode( ', ', SearchEngine::namespacesAsText( SearchEngine::projectNamespaces() ) ) ); + if( $this->active == 'project' ) { + $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); + } else { + $out .= $this->makeSearchLink( $bareterm, SearchEngine::projectNamespaces(), $m, $tt ); + } + $out .= $sep; + + $m = wfMsg( 'searchprofile-everything' ); + $tt = wfMsg( 'searchprofile-everything-tooltip' ); + if( $this->active == 'all' ) { + $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); + } else { + $out .= $this->makeSearchLink( $bareterm, $nsAllSet, $m, $tt ); + } + $out .= $sep; + + $m = wfMsg( 'searchprofile-advanced' ); + $tt = wfMsg( 'searchprofile-advanced-tooltip' ); + if( $this->active == 'advanced' ) { + $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); + } else { + $out .= $this->makeSearchLink( $bareterm, $this->namespaces, $m, $tt, array( 'advanced' => '1' ) ); + } + $out .= Xml::closeElement('div') ; + + return $out; + } + + protected function shortDialog( $term ) { + global $wgScript; + $searchTitle = SpecialPage::getTitleFor( 'Search' ); + $searchable = SearchEngine::searchableNamespaces(); + $out = Xml::openElement( 'form', array( 'id' => 'search', 'method' => 'get', 'action' => $wgScript ) ); + $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n"; + // show namespaces only for advanced search + if( $this->active == 'advanced' ) { + $active = array(); + foreach( $this->namespaces as $ns ) { + $active[$ns] = $searchable[$ns]; + } + $out .= wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . "<br/>\n"; + $out .= $this->namespaceTables( $active, 1 )."<br/>\n"; + // Still keep namespace settings otherwise, but don't show them + } else { + foreach( $this->namespaces as $ns ) { + $out .= Xml::hidden( "ns{$ns}", '1' ); + } + } + // Keep redirect setting + $out .= Xml::hidden( "redirs", (int)$this->searchRedirects ); + // Term box + $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . "\n"; + $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) ); + $out .= ' (' . wfMsgExt('searchmenu-help',array('parseinline') ) . ')'; + $out .= Xml::closeElement( 'form' ); + // Add prefix link for single-namespace searches + $t = Title::newFromText( $term ); + /*if( $t != null && count($this->namespaces) === 1 ) { + $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term ); + }*/ + return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) . + Xml::element( 'legend', null, wfMsg('searchmenu-legend') ) . + $this->formHeader($term) . $out . $this->didYouMeanHtml . + Xml::closeElement( 'fieldset' ); + } + + /** Make a search link with some target namespaces */ + protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params=array() ) { + $opt = $params; + foreach( $namespaces as $n ) { + $opt['ns' . $n] = 1; + } + $opt['redirs'] = $this->searchRedirects ? 1 : 0; + + $st = SpecialPage::getTitleFor( 'Search' ); + $stParams = wfArrayToCGI( array( 'search' => $term, 'fulltext' => wfMsg( 'search' ) ), $opt ); + + return Xml::element( 'a', + array( 'href'=> $st->getLocalURL( $stParams ), 'title' => $tooltip ), + $label ); + } + + /** Check if query starts with image: prefix */ + protected function startsWithImage( $term ) { + global $wgContLang; + + $p = explode( ':', $term ); + if( count( $p ) > 1 ) { + return $wgContLang->getNsIndex( $p[0] ) == NS_FILE; + } + return false; + } + + protected function namespaceTables( $namespaces, $rowsPerTable = 3 ) { + global $wgContLang; + // Group namespaces into rows according to subject. + // Try not to make too many assumptions about namespace numbering. + $rows = array(); + $tables = ""; + foreach( $namespaces as $ns => $name ) { + $subj = MWNamespace::getSubject( $ns ); + if( !array_key_exists( $subj, $rows ) ) { + $rows[$subj] = ""; + } + $name = str_replace( '_', ' ', $name ); + if( '' == $name ) { + $name = wfMsg( 'blanknamespace' ); + } + $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) . + Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) . + Xml::closeElement( 'td' ) . "\n"; + } + $rows = array_values( $rows ); + $numRows = count( $rows ); + // Lay out namespaces in multiple floating two-column tables so they'll + // be arranged nicely while still accommodating different screen widths + // Float to the right on RTL wikis + $tableStyle = $wgContLang->isRTL() ? + 'float: right; margin: 0 0 0em 1em' : 'float: left; margin: 0 1em 0em 0'; + // Build the final HTML table... + for( $i = 0; $i < $numRows; $i += $rowsPerTable ) { + $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) ); + for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) { + $tables .= "<tr>\n" . $rows[$j] . "</tr>"; + } + $tables .= Xml::closeElement( 'table' ) . "\n"; + } + return $tables; + } +} + +/** + * implements Special:Search - Run text & title search and display the output + * @ingroup SpecialPage + */ +class SpecialSearchOld { + + /** + * Set up basic search parameters from the request and user settings. + * Typically you'll pass $wgRequest and $wgUser. + * + * @param WebRequest $request + * @param User $user + * @public + */ + function __construct( &$request, &$user ) { + list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' ); + $this->mPrefix = $request->getVal('prefix', ''); $this->namespaces = $this->powerSearch( $request ); if( empty( $this->namespaces ) ) { $this->namespaces = SearchEngine::userNamespaces( $user ); @@ -119,13 +921,38 @@ class SpecialSearch { * @public */ function showResults( $term ) { - $fname = 'SpecialSearch::showResults'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); global $wgOut, $wgUser; $sk = $wgUser->getSkin(); + $search = SearchEngine::create(); + $search->setLimitOffset( $this->limit, $this->offset ); + $search->setNamespaces( $this->namespaces ); + $search->showRedirects = $this->searchRedirects; + $search->prefix = $this->mPrefix; + $term = $search->transformSearchTerm($term); + $this->setupPage( $term ); + $rewritten = $search->replacePrefixes($term); + $titleMatches = $search->searchTitle( $rewritten ); + $textMatches = $search->searchText( $rewritten ); + + // did you mean... suggestions + if($textMatches && $textMatches->hasSuggestion()){ + $st = SpecialPage::getTitleFor( 'Search' ); + $stParams = wfArrayToCGI( array( + 'search' => $textMatches->getSuggestionQuery(), + 'fulltext' => wfMsg('search')), + $this->powerSearchOptions()); + + $suggestLink = $sk->makeKnownLinkObj( $st, + $textMatches->getSuggestionSnippet(), + $stParams ); + + $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>'); + } + $wgOut->addWikiMsg( 'searchresulttext' ); if( '' === trim( $term ) ) { @@ -133,7 +960,7 @@ class SpecialSearch { $wgOut->setSubtitle( '' ); $wgOut->addHTML( $this->powerSearchBox( $term ) ); $wgOut->addHTML( $this->powerSearchFocus() ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return; } @@ -143,6 +970,7 @@ class SpecialSearch { if( $wgSearchForwardUrl ) { $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl ); $wgOut->redirect( $url ); + wfProfileOut( __METHOD__ ); return; } global $wgInputEncoding; @@ -157,45 +985,21 @@ class SpecialSearch { ) . Xml::closeElement( 'fieldset' ) ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return; } - $wgOut->addHTML( $this->shortDialog( $term ) ); - - $search = SearchEngine::create(); - $search->setLimitOffset( $this->limit, $this->offset ); - $search->setNamespaces( $this->namespaces ); - $search->showRedirects = $this->searchRedirects; - $rewritten = $search->replacePrefixes($term); - - $titleMatches = $search->searchTitle( $rewritten ); + $wgOut->addHTML( $this->shortDialog( $term ) ); // Sometimes the search engine knows there are too many hits if ($titleMatches instanceof SearchResultTooMany) { $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" ); $wgOut->addHTML( $this->powerSearchBox( $term ) ); $wgOut->addHTML( $this->powerSearchFocus() ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return; } - $textMatches = $search->searchText( $rewritten ); - - // did you mean... suggestions - if($textMatches && $textMatches->hasSuggestion()){ - $st = SpecialPage::getTitleFor( 'Search' ); - $stParams = wfArrayToCGI( array( - 'search' => $textMatches->getSuggestionQuery(), - 'fulltext' => wfMsg('search')), - $this->powerSearchOptions()); - - $suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'. - $textMatches->getSuggestionSnippet().'</a>'; - - $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>'); - } - // show number of results $num = ( $titleMatches ? $titleMatches->numRows() : 0 ) + ( $textMatches ? $textMatches->numRows() : 0); @@ -207,7 +1011,7 @@ class SpecialSearch { if ( $num > 0 ) { if ( $totalNum > 0 ){ $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), - $this->offset+1, $this->offset+$num, $totalNum ); + $this->offset+1, $this->offset+$num, $totalNum, $num ); } elseif ( $num >= $this->limit ) { $top = wfShowingResults( $this->offset, $this->limit ); } else { @@ -251,7 +1055,7 @@ class SpecialSearch { } // show interwiki results if any if( $textMatches->hasInterwikiResults() ) - $wgOut->addHtml( $this->showInterwiki( $textMatches->getInterwikiResults(), $term )); + $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term )); // show results if( $textMatches->numRows() ) $wgOut->addHTML( $this->showMatches( $textMatches ) ); @@ -266,7 +1070,7 @@ class SpecialSearch { $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); } $wgOut->addHTML( $this->powerSearchBox( $term ) ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } #------------------------------------------------------------------ @@ -277,12 +1081,14 @@ class SpecialSearch { */ function setupPage( $term ) { global $wgOut; - if( !empty( $term ) ) - $wgOut->setPageTitle( wfMsg( 'searchresults' ) ); + if( !empty( $term ) ){ + $wgOut->setPageTitle( wfMsg( 'searchresults') ); + $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term) ) ); + } $subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' ); $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) ); $wgOut->setArticleRelated( false ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); } /** @@ -323,8 +1129,7 @@ class SpecialSearch { * @param SearchResultSet $matches */ function showMatches( &$matches ) { - $fname = 'SpecialSearch::showMatches'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); global $wgContLang; $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); @@ -347,7 +1152,7 @@ class SpecialSearch { // convert the whole thing to desired language variant global $wgContLang; $out = $wgContLang->convert( $out ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $out; } @@ -357,12 +1162,11 @@ class SpecialSearch { * @param array $terms terms to highlight */ function showHit( $result, $terms ) { - $fname = 'SpecialSearch::showHit'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); global $wgUser, $wgContLang, $wgLang; if( $result->isBrokenTitle() ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return "<!-- Broken link in search result -->\n"; } @@ -375,7 +1179,7 @@ class SpecialSearch { //This is not quite safe, but better than showing excerpts from non-readable pages //Note that hiding the entry entirely would screw up paging. if (!$t->userCanRead()) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return "<li>{$link}</li>\n"; } @@ -383,7 +1187,7 @@ class SpecialSearch { // The least confusing at this point is to drop the result. // You may get less results, but... oh well. :P if( $result->isMissingRevision() ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n"; } @@ -434,18 +1238,18 @@ class SpecialSearch { array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(), 'fulltext' => wfMsg('search') )); - $related = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'. - wfMsg('search-relatedarticle').'</a>'; + $related = ' -- ' . $sk->makeKnownLinkObj( $st, + wfMsg('search-relatedarticle'), $stParams ); } // Include a thumbnail for media files... - if( $t->getNamespace() == NS_IMAGE ) { + if( $t->getNamespace() == NS_FILE ) { $img = wfFindFile( $t ); if( $img ) { - $thumb = $img->getThumbnail( 120, 120 ); + $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) ); if( $thumb ) { $desc = $img->getShortDesc(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); // Ugly table. :D // Float doesn't seem to interact well with the bullets. // Table messes up vertical alignment of the bullet, but I'm @@ -468,7 +1272,7 @@ class SpecialSearch { } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return "<li>{$link} {$redirect} {$section} {$extract}\n" . "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" . "</li>\n"; @@ -481,8 +1285,7 @@ class SpecialSearch { * @param SearchResultSet $matches */ function showInterwiki( &$matches, $query ) { - $fname = 'SpecialSearch::showInterwiki'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); global $wgContLang; $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); @@ -512,7 +1315,7 @@ class SpecialSearch { // convert the whole thing to desired language variant global $wgContLang; $out = $wgContLang->convert( $out ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $out; } @@ -525,13 +1328,12 @@ class SpecialSearch { * @param string $query * @param array $customCaptions iw prefix -> caption */ - function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){ - $fname = 'SpecialSearch::showInterwikiHit'; - wfProfileIn( $fname ); + function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) { + wfProfileIn( __METHOD__ ); global $wgUser, $wgContLang, $wgLang; if( $result->isBrokenTitle() ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return "<!-- Broken link in search result -->\n"; } @@ -569,7 +1371,7 @@ class SpecialSearch { } $out .= "<li>{$link} {$redirect}</li>\n"; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $out; } @@ -580,35 +1382,64 @@ class SpecialSearch { * @return $out string: HTML form */ function powerSearchBox( $term ) { - global $wgScript; + global $wgScript, $wgContLang; - $namespaces = ''; - foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { + $namespaces = SearchEngine::searchableNamespaces(); + + // group namespaces into rows according to subject; try not to make too + // many assumptions about namespace numbering + $rows = array(); + foreach( $namespaces as $ns => $name ) { + $subj = MWNamespace::getSubject( $ns ); + if( !array_key_exists( $subj, $rows ) ) { + $rows[$subj] = ""; + } $name = str_replace( '_', ' ', $name ); if( '' == $name ) { $name = wfMsg( 'blanknamespace' ); } - $namespaces .= Xml::openElement( 'span', array( 'style' => 'white-space: nowrap' ) ) . + $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) . Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) . - Xml::closeElement( 'span' ) . "\n"; + Xml::closeElement( 'td' ) . "\n"; + } + $rows = array_values( $rows ); + $numRows = count( $rows ); + + // lay out namespaces in multiple floating two-column tables so they'll + // be arranged nicely while still accommodating different screen widths + $rowsPerTable = 3; // seems to look nice + + // float to the right on RTL wikis + $tableStyle = ( $wgContLang->isRTL() ? + 'float: right; margin: 0 0 1em 1em' : + 'float: left; margin: 0 1em 1em 0' ); + + $tables = ""; + for( $i = 0; $i < $numRows; $i += $rowsPerTable ) { + $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) ); + for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) { + $tables .= "<tr>\n" . $rows[$j] . "</tr>"; + } + $tables .= Xml::closeElement( 'table' ) . "\n"; } $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) ); $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' ); $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) ); $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' ) ) . "\n"; - + $searchTitle = SpecialPage::getTitleFor( 'Search' ); + $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) . Xml::fieldset( wfMsg( 'powersearch-legend' ), - Xml::hidden( 'title', 'Special:Search' ) . + Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n" . "<p>" . wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . - "<br />" . - $namespaces . - "</p>" . + "</p>\n" . + $tables . + "<hr style=\"clear: both\" />\n" . "<p>" . $redirect . " " . $redirectLabel . - "</p>" . + "</p>\n" . wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) . " " . $searchField . @@ -636,7 +1467,8 @@ class SpecialSearch { 'method' => 'get', 'action' => $wgScript )); - $out .= Xml::hidden( 'title', 'Special:Search' ); + $searchTitle = SpecialPage::getTitleFor( 'Search' ); + $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() ); $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' '; foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { if( in_array( $ns, $this->namespaces ) ) { diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php index ca91ad51..560ba445 100644 --- a/includes/specials/SpecialSpecialpages.php +++ b/includes/specials/SpecialSpecialpages.php @@ -12,7 +12,7 @@ function wfSpecialSpecialpages() { $wgMessageCache->loadAllMessages(); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); # Is this really needed? + $wgOut->setRobotPolicy( 'noindex,nofollow' ); # Is this really needed? $sk = $wgUser->getSkin(); $pages = SpecialPage::getUsablePages(); diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php index 570a21c6..109c5c30 100644 --- a/includes/specials/SpecialStatistics.php +++ b/includes/specials/SpecialStatistics.php @@ -13,50 +13,214 @@ * * @param mixed $par (not used) */ -function wfSpecialStatistics( $par = '' ) { - global $wgOut, $wgLang, $wgRequest; - $dbr = wfGetDB( DB_SLAVE ); +class SpecialStatistics extends SpecialPage { + + private $views, $edits, $good, $images, $total, $users, + $activeUsers, $admins, $numJobs = 0; + + public function __construct() { + parent::__construct( 'Statistics' ); + } + + public function execute( $par ) { + global $wgOut, $wgRequest, $wgMessageCache; + global $wgDisableCounters, $wgMiserMode; + $wgMessageCache->loadAllMessages(); + + $this->setHeaders(); + + $this->views = SiteStats::views(); + $this->edits = SiteStats::edits(); + $this->good = SiteStats::articles(); + $this->images = SiteStats::images(); + $this->total = SiteStats::pages(); + $this->users = SiteStats::users(); + $this->activeUsers = SiteStats::activeUsers(); + $this->admins = SiteStats::numberingroup('sysop'); + $this->numJobs = SiteStats::jobs(); + + # Staticic - views + $viewsStats = ''; + if( !$wgDisableCounters ) { + $viewsStats = $this->getViewsStats(); + } + + # Set active user count + if( !$wgMiserMode ) { + $dbw = wfGetDB( DB_MASTER ); + SiteStatsUpdate::cacheUpdate( $dbw ); + } + + # Do raw output + if( $wgRequest->getVal( 'action' ) == 'raw' ) { + $this->doRawOutput(); + } - $views = SiteStats::views(); - $edits = SiteStats::edits(); - $good = SiteStats::articles(); - $images = SiteStats::images(); - $total = SiteStats::pages(); - $users = SiteStats::users(); - $admins = SiteStats::admins(); - $numJobs = SiteStats::jobs(); + $text = Xml::openElement( 'table', array( 'class' => 'mw-statistics-table' ) ); - if( $wgRequest->getVal( 'action' ) == 'raw' ) { - $wgOut->disable(); - header( 'Pragma: nocache' ); - echo "total=$total;good=$good;views=$views;edits=$edits;users=$users;admins=$admins;images=$images;jobs=$numJobs\n"; - return; - } else { - $text = "__NOTOC__\n"; - $text .= '==' . wfMsgNoTrans( 'sitestats' ) . "==\n"; - $text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ), - $wgLang->formatNum( $total ), - $wgLang->formatNum( $good ), - $wgLang->formatNum( $views ), - $wgLang->formatNum( $edits ), - $wgLang->formatNum( sprintf( '%.2f', $total ? $edits / $total : 0 ) ), - $wgLang->formatNum( sprintf( '%.2f', $edits ? $views / $edits : 0 ) ), - $wgLang->formatNum( $numJobs ), - $wgLang->formatNum( $images ) - )."\n"; + # Statistic - pages + $text .= $this->getPageStats(); + + # Statistic - edits + $text .= $this->getEditStats(); - $text .= "==" . wfMsgNoTrans( 'userstats' ) . "==\n"; - $text .= wfMsgExt( 'userstatstext', array ( 'parsemag' ), - $wgLang->formatNum( $users ), - $wgLang->formatNum( $admins ), - '[[' . wfMsgForContent( 'grouppage-sysop' ) . ']]', # TODO somehow remove, kept for backwards compatibility - $wgLang->formatNum( @sprintf( '%.2f', $admins / $users * 100 ) ), - User::makeGroupLinkWiki( 'sysop' ) - )."\n"; + # Statistic - users + $text .= $this->getUserStats(); - global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang; + # Statistic - usergroups + $text .= $this->getGroupStats(); + $text .= $viewsStats; + + # Statistic - popular pages if( !$wgDisableCounters && !$wgMiserMode ) { - $res = $dbr->select( + $text .= $this->getMostViewedPages(); + } + + $text .= Xml::closeElement( 'table' ); + + # Customizable footer + $footer = wfMsgExt( 'statistics-footer', array('parseinline') ); + if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' ) { + $text .= "\n" . $footer; + } + + $wgOut->addHTML( $text ); + } + + /** + * Format a row + * @param string $text description of the row + * @param float $number a number + * @param array $trExtraParams + * @param string $descMsg + * @param string $descMsgParam + * @return string table row in HTML format + */ + private function formatRow( $text, $number, $trExtraParams = array(), $descMsg = '', $descMsgParam = '' ) { + global $wgStylePath; + if( $descMsg ) { + $descriptionText = wfMsgExt( $descMsg, array( 'parseinline' ), $descMsgParam ); + if ( !wfEmptyMsg( $descMsg, $descriptionText ) ) { + $descriptionText = " ($descriptionText)"; + $text .= "<br />" . Xml::element( 'small', array( 'class' => 'mw-statistic-desc'), + $descriptionText ); + } + } + return Xml::openElement( 'tr', $trExtraParams ) . + Xml::openElement( 'td' ) . $text . Xml::closeElement( 'td' ) . + Xml::openElement( 'td', array( 'class' => 'mw-statistics-numbers' ) ) . $number . Xml::closeElement( 'td' ) . + Xml::closeElement( 'tr' ); + } + + /** + * Each of these methods is pretty self-explanatory, get a particular + * row for the table of statistics + * @return string + */ + private function getPageStats() { + global $wgLang; + return Xml::openElement( 'tr' ) . + Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-pages', array( 'parseinline' ) ) ) . + Xml::closeElement( 'tr' ) . + $this->formatRow( wfMsgExt( 'statistics-articles', array( 'parseinline' ) ), + $wgLang->formatNum( $this->good ), + array( 'class' => 'mw-statistics-articles' ) ) . + $this->formatRow( wfMsgExt( 'statistics-pages', array( 'parseinline' ) ), + $wgLang->formatNum( $this->total ), + array( 'class' => 'mw-statistics-pages' ), + 'statistics-pages-desc' ) . + $this->formatRow( wfMsgExt( 'statistics-files', array( 'parseinline' ) ), + $wgLang->formatNum( $this->images ), + array( 'class' => 'mw-statistics-files' ) ); + } + private function getEditStats() { + global $wgLang; + return Xml::openElement( 'tr' ) . + Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-edits', array( 'parseinline' ) ) ) . + Xml::closeElement( 'tr' ) . + $this->formatRow( wfMsgExt( 'statistics-edits', array( 'parseinline' ) ), + $wgLang->formatNum( $this->edits ), + array( 'class' => 'mw-statistics-edits' ) ) . + $this->formatRow( wfMsgExt( 'statistics-edits-average', array( 'parseinline' ) ), + $wgLang->formatNum( sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 ) ), + array( 'class' => 'mw-statistics-edits-average' ) ) . + $this->formatRow( wfMsgExt( 'statistics-jobqueue', array( 'parseinline' ) ), + $wgLang->formatNum( $this->numJobs ), + array( 'class' => 'mw-statistics-jobqueue' ) ); + } + private function getUserStats() { + global $wgLang, $wgRCMaxAge; + return Xml::openElement( 'tr' ) . + Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-users', array( 'parseinline' ) ) ) . + Xml::closeElement( 'tr' ) . + $this->formatRow( wfMsgExt( 'statistics-users', array( 'parseinline' ) ), + $wgLang->formatNum( $this->users ), + array( 'class' => 'mw-statistics-users' ) ) . + $this->formatRow( wfMsgExt( 'statistics-users-active', array( 'parseinline' ) ), + $wgLang->formatNum( $this->activeUsers ), + array( 'class' => 'mw-statistics-users-active' ), + 'statistics-users-active-desc', + $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600 * 24 ) ) ) ); + } + private function getGroupStats() { + global $wgGroupPermissions, $wgImplicitGroups, $wgLang, $wgUser; + $sk = $wgUser->getSkin(); + $text = ''; + foreach( $wgGroupPermissions as $group => $permissions ) { + # Skip generic * and implicit groups + if ( in_array( $group, $wgImplicitGroups ) || $group == '*' ) { + continue; + } + $groupname = htmlspecialchars( $group ); + $msg = wfMsg( 'group-' . $groupname ); + if ( wfEmptyMsg( 'group-' . $groupname, $msg ) || $msg == '' ) { + $groupnameLocalized = $groupname; + } else { + $groupnameLocalized = $msg; + } + $msg = wfMsgForContent( 'grouppage-' . $groupname ); + if ( wfEmptyMsg( 'grouppage-' . $groupname, $msg ) || $msg == '' ) { + $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname; + } else { + $grouppageLocalized = $msg; + } + $grouppage = $sk->makeLink( $grouppageLocalized, htmlspecialchars( $groupnameLocalized ) ); + $grouplink = $sk->link( SpecialPage::getTitleFor( 'Listusers' ), + wfMsgHtml( 'listgrouprights-members' ), + array(), + array( 'group' => $group ), + 'known' ); + # Add a class when a usergroup contains no members to allow hiding these rows + $classZero = ''; + $countUsers = SiteStats::numberingroup( $groupname ); + if( $countUsers == 0 ) { + $classZero = ' statistics-group-zero'; + } + $text .= $this->formatRow( $grouppage . ' ' . $grouplink, + $wgLang->formatNum( $countUsers ), + array( 'class' => 'statistics-group-' . Sanitizer::escapeClass( $group ) . $classZero ) ); + } + return $text; + } + private function getViewsStats() { + global $wgLang; + return Xml::openElement( 'tr' ) . + Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-views', array( 'parseinline' ) ) ) . + Xml::closeElement( 'tr' ) . + $this->formatRow( wfMsgExt( 'statistics-views-total', array( 'parseinline' ) ), + $wgLang->formatNum( $this->views ), + array ( 'class' => 'mw-statistics-views-total' ) ) . + $this->formatRow( wfMsgExt( 'statistics-views-peredit', array( 'parseinline' ) ), + $wgLang->formatNum( sprintf( '%.2f', $this->edits ? + $this->views / $this->edits : 0 ) ), + array ( 'class' => 'mw-statistics-views-peredit' ) ); + } + private function getMostViewedPages() { + global $wgLang, $wgUser; + $text = ''; + $dbr = wfGetDB( DB_SLAVE ); + $sk = $wgUser->getSkin(); + $res = $dbr->select( 'page', array( 'page_namespace', @@ -74,20 +238,33 @@ function wfSpecialStatistics( $par = '' ) { ) ); if( $res->numRows() > 0 ) { - $text .= "==" . wfMsgNoTrans( 'statistics-mostpopular' ) . "==\n"; + $text .= Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-mostpopular', array( 'parseinline' ) ) ); while( $row = $res->fetchObject() ) { $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); - if( $title instanceof Title ) - $text .= '* [[:' . $title->getPrefixedText() . ']] (' . $wgLang->formatNum( $row->page_counter ) . ")\n"; + if( $title instanceof Title ) { + $text .= $this->formatRow( $sk->link( $title ), + $wgLang->formatNum( $row->page_counter ) ); + + } } $res->free(); } - } - - $footer = wfMsgNoTrans( 'statistics-footer' ); - if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' ) - $text .= "\n" . $footer; - - $wgOut->addWikiText( $text ); + return $text; + } + + /** + * Do the action=raw output for this page. Legacy, but we support + * it for backwards compatibility + * http://lists.wikimedia.org/pipermail/wikitech-l/2008-August/039202.html + */ + private function doRawOutput() { + global $wgOut; + $wgOut->disable(); + header( 'Pragma: nocache' ); + echo "total=" . $this->total . ";good=" . $this->good . ";views=" . + $this->views . ";edits=" . $this->edits . ";users=" . $this->users . ";"; + echo "activeusers=" . $this->activeUsers . ";admins=" . $this->admins . + ";images=" . $this->images . ";jobs=" . $this->numJobs . "\n"; + return; } -} +}
\ No newline at end of file diff --git a/includes/specials/SpecialUncategorizedimages.php b/includes/specials/SpecialUncategorizedimages.php index 986ec967..25310081 100644 --- a/includes/specials/SpecialUncategorizedimages.php +++ b/includes/specials/SpecialUncategorizedimages.php @@ -31,7 +31,7 @@ class UncategorizedImagesPage extends ImageQueryPage { function getSQL() { $dbr = wfGetDB( DB_SLAVE ); list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' ); - $ns = NS_IMAGE; + $ns = NS_FILE; return "SELECT 'Uncategorizedimages' AS type, page_namespace AS namespace, page_title AS title, page_title AS value diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index d862ebb3..a9fb4ef1 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -119,7 +119,7 @@ class PageArchive { * @todo Does this belong in Image for fuller encapsulation? */ function listFiles() { - if( $this->title->getNamespace() == NS_IMAGE ) { + if( $this->title->getNamespace() == NS_FILE ) { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'filearchive', array( @@ -336,7 +336,7 @@ class PageArchive { $restoreText = $restoreAll || !empty( $timestamps ); $restoreFiles = $restoreAll || !empty( $fileVersions ); - if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) { + if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) { $img = wfLocalFile( $this->title ); $this->fileStatus = $img->restore( $fileVersions, $unsuppress ); $filesRestored = $this->fileStatus->successCount; @@ -412,7 +412,7 @@ class PageArchive { # we'll update the latest revision field in the record. $newid = 0; $pageId = $page->page_id; - $previousRevId = $page->page_latest; + $previousRevId = $page->page_latest; # Get the time span of this page $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp', array( 'rev_id' => $previousRevId ), @@ -461,25 +461,10 @@ class PageArchive { 'ar_title' => $this->title->getDBkey(), $oldones ), __METHOD__, - /* options */ array( - 'ORDER BY' => 'ar_timestamp' ) + /* options */ array( 'ORDER BY' => 'ar_timestamp' ) ); $ret = $dbw->resultObject( $result ); - $rev_count = $dbw->numRows( $result ); - if( $rev_count ) { - # We need to seek around as just using DESC in the ORDER BY - # would leave the revisions inserted in the wrong order - $first = $ret->fetchObject(); - $ret->seek( $rev_count - 1 ); - $last = $ret->fetchObject(); - // We don't handle well changing the top revision's settings - if( !$unsuppress && $last->ar_deleted && $last->ar_timestamp > $previousTimestamp ) { - wfDebug( __METHOD__.": restoration would result in a deleted top revision\n" ); - return false; - } - $ret->seek( 0 ); - } if( $makepage ) { $newid = $article->insertOn( $dbw ); @@ -502,6 +487,12 @@ class PageArchive { // a new text table entry will be created for it. $revText = Revision::getRevisionText( $row, 'ar_' ); } + // Check for key dupes due to shitty archive integrity. + if( $row->ar_rev_id ) { + $exists = $dbw->selectField( 'revision', '1', array('rev_id' => $row->ar_rev_id), __METHOD__ ); + if( $exists ) continue; // don't throw DB errors + } + $revision = new Revision( array( 'page' => $pageId, 'id' => $row->ar_rev_id, @@ -520,17 +511,32 @@ class PageArchive { wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) ); } + # Now that it's safely stored, take it out of the archive + $dbw->delete( 'archive', + /* WHERE */ array( + 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey(), + $oldones ), + __METHOD__ ); + // Was anything restored at all? - if($restored == 0) + if( $restored == 0 ) return 0; if( $revision ) { // Attach the latest revision to the page... $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId ); - if( $newid || $wasnew ) { // Update site stats, link tables, etc $article->createUpdates( $revision ); + // We don't handle well with top revision deleted + if( $revision->getVisibility() ) { + $dbw->update( 'revision', + array( 'rev_deleted' => 0 ), + array( 'rev_id' => $revision->getId() ), + __METHOD__ + ); + } } if( $newid ) { @@ -541,7 +547,7 @@ class PageArchive { Article::onArticleEdit( $this->title ); } - if( $this->title->getNamespace() == NS_IMAGE ) { + if( $this->title->getNamespace() == NS_FILE ) { $update = new HTMLCacheUpdate( $this->title, 'imagelinks' ); $update->doUpdate(); } @@ -550,14 +556,6 @@ class PageArchive { return self::UNDELETE_UNKNOWNERR; } - # Now that it's safely stored, take it out of the archive - $dbw->delete( 'archive', - /* WHERE */ array( - 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey(), - $oldones ), - __METHOD__ ); - return $restored; } @@ -570,7 +568,7 @@ class PageArchive { * @ingroup SpecialPage */ class UndeleteForm { - var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj; + var $mAction, $mTarget, $mTimestamp, $mRestore, $mInvert, $mTargetObj; var $mTargetTimestamp, $mAllowed, $mComment, $mToken; function UndeleteForm( $request, $par = "" ) { @@ -585,6 +583,7 @@ class UndeleteForm { $posted = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); $this->mRestore = $request->getCheck( 'restore' ) && $posted; + $this->mInvert = $request->getCheck( 'invert' ) && $posted; $this->mPreview = $request->getCheck( 'preview' ) && $posted; $this->mDiff = $request->getCheck( 'diff' ); $this->mComment = $request->getText( 'wpComment' ); @@ -606,7 +605,7 @@ class UndeleteForm { } else { $this->mTargetObj = NULL; } - if( $this->mRestore ) { + if( $this->mRestore || $this->mInvert ) { $timestamps = array(); $this->mFileVersions = array(); foreach( $_REQUEST as $key => $val ) { @@ -666,6 +665,9 @@ class UndeleteForm { if( $this->mRestore && $this->mAction == "submit" ) { return $this->undelete(); } + if( $this->mInvert && $this->mAction == "submit" ) { + return $this->showHistory( ); + } return $this->showHistory(); } @@ -673,21 +675,20 @@ class UndeleteForm { global $wgOut, $wgScript; $wgOut->addWikiMsg( 'undelete-header' ); - $wgOut->addHtml( + $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . - '<fieldset>' . - Xml::element( 'legend', array(), - wfMsg( 'undelete-search-box' ) ) . + Xml::fieldset( wfMsg( 'undelete-search-box' ) ) . Xml::hidden( 'title', SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) . Xml::inputLabel( wfMsg( 'undelete-search-prefix' ), 'prefix', 'prefix', 20, - $this->mSearchPrefix ) . + $this->mSearchPrefix ) . ' ' . Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) . - '</fieldset>' . - '</form>' ); + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ) + ); } // Generic list of deleted pages @@ -699,7 +700,7 @@ class UndeleteForm { return; } - $wgOut->addWikiMsg( "undeletepagetext" ); + $wgOut->addWikiMsg( 'undeletepagetext', $wgLang->formatNum( $result->numRows() ) ); $sk = $wgUser->getSkin(); $undelete = SpecialPage::getTitleFor( 'Undelete' ); @@ -708,11 +709,10 @@ class UndeleteForm { $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); $link = $sk->makeKnownLinkObj( $undelete, htmlspecialchars( $title->getPrefixedText() ), 'target=' . $title->getPrefixedUrl() ); - #$revs = wfMsgHtml( 'undeleterevisions', $wgLang->formatNum( $row->count ) ); $revs = wfMsgExt( 'undeleterevisions', array( 'parseinline' ), $wgLang->formatNum( $row->count ) ); - $wgOut->addHtml( "<li>{$link} ({$revs})</li>\n" ); + $wgOut->addHTML( "<li>{$link} ({$revs})</li>\n" ); } $result->free(); $wgOut->addHTML( "</ul>\n" ); @@ -752,8 +752,6 @@ class UndeleteForm { SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ), htmlspecialchars( $this->mTargetObj->getPrefixedText() ) ); - $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) ); - $user = $skin->revUserTools( $rev ); if( $this->mDiff ) { $previousRev = $archive->getPreviousRevision( $timestamp ); @@ -762,59 +760,66 @@ class UndeleteForm { if( $wgUser->getOption( 'diffonly' ) ) { return; } else { - $wgOut->addHtml( '<hr />' ); + $wgOut->addHTML( '<hr />' ); } } else { - $wgOut->addHtml( wfMsgHtml( 'undelete-nodiff' ) ); + $wgOut->addHTML( wfMsgHtml( 'undelete-nodiff' ) ); } } - $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' ); + // date and time are separate parameters to facilitate localisation. + // $time is kept for backward compat reasons. + $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) ); + $d = htmlspecialchars( $wgLang->date( $timestamp, true ) ); + $t = htmlspecialchars( $wgLang->time( $timestamp, true ) ); + $user = $skin->revUserTools( $rev ); + + $wgOut->addHTML( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '</p>' ); wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ); if( $this->mPreview ) { - $wgOut->addHtml( "<hr />\n" ); + $wgOut->addHTML( "<hr />\n" ); //Hide [edit]s $popts = $wgOut->parserOptions(); $popts->setEditSection( false ); $wgOut->parserOptions( $popts ); - $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, true ); + $wgOut->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER ), $this->mTargetObj, true ); } - $wgOut->addHtml( - wfElement( 'textarea', array( + $wgOut->addHTML( + Xml::element( 'textarea', array( 'readonly' => 'readonly', 'cols' => intval( $wgUser->getOption( 'cols' ) ), 'rows' => intval( $wgUser->getOption( 'rows' ) ) ), - $rev->revText() . "\n" ) . - wfOpenElement( 'div' ) . - wfOpenElement( 'form', array( + $rev->getText( Revision::FOR_THIS_USER ) . "\n" ) . + Xml::openElement( 'div' ) . + Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalURL( "action=submit" ) ) ) . - wfElement( 'input', array( + Xml::element( 'input', array( 'type' => 'hidden', 'name' => 'target', 'value' => $this->mTargetObj->getPrefixedDbKey() ) ) . - wfElement( 'input', array( + Xml::element( 'input', array( 'type' => 'hidden', 'name' => 'timestamp', 'value' => $timestamp ) ) . - wfElement( 'input', array( + Xml::element( 'input', array( 'type' => 'hidden', 'name' => 'wpEditToken', 'value' => $wgUser->editToken() ) ) . - wfElement( 'input', array( + Xml::element( 'input', array( 'type' => 'submit', 'name' => 'preview', 'value' => wfMsg( 'showpreview' ) ) ) . - wfElement( 'input', array( + Xml::element( 'input', array( 'name' => 'diff', 'type' => 'submit', 'value' => wfMsg( 'showdiff' ) ) ) . - wfCloseElement( 'form' ) . - wfCloseElement( 'div' ) ); + Xml::closeElement( 'form' ) . + Xml::closeElement( 'div' ) ); } /** @@ -829,7 +834,7 @@ class UndeleteForm { $diffEngine = new DifferenceEngine(); $diffEngine->showDiffStyle(); - $wgOut->addHtml( + $wgOut->addHTML( "<div>" . "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" . "<col class='diff-marker' />" . @@ -838,11 +843,11 @@ class UndeleteForm { "<col class='diff-content' />" . "<tr>" . "<td colspan='2' width='50%' align='center' class='diff-otitle'>" . - $this->diffHeader( $previousRev ) . - "</td>" . + $this->diffHeader( $previousRev, 'o' ) . + "</td>\n" . "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" . - $this->diffHeader( $currentRev ) . - "</td>" . + $this->diffHeader( $currentRev, 'n' ) . + "</td>\n" . "</tr>" . $diffEngine->generateDiffBody( $previousRev->getText(), $currentRev->getText() ) . @@ -851,7 +856,7 @@ class UndeleteForm { } - private function diffHeader( $rev ) { + private function diffHeader( $rev, $prefix ) { global $wgUser, $wgLang, $wgLang; $sk = $wgUser->getSkin(); $isDeleted = !( $rev->getId() && $rev->getTitle() ); @@ -868,17 +873,17 @@ class UndeleteForm { $targetQuery = 'oldid=' . $rev->getId(); } return - '<div id="mw-diff-otitle1"><strong>' . + '<div id="mw-diff-'.$prefix.'title1"><strong>' . $sk->makeLinkObj( $targetPage, wfMsgHtml( 'revisionasof', $wgLang->timeanddate( $rev->getTimestamp(), true ) ), $targetQuery ) . ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) . '</strong></div>' . - '<div id="mw-diff-otitle2">' . + '<div id="mw-diff-'.$prefix.'title2">' . $sk->revUserTools( $rev ) . '<br/>' . '</div>' . - '<div id="mw-diff-otitle3">' . + '<div id="mw-diff-'.$prefix.'title3">' . $sk->revComment( $rev ) . '<br/>' . '</div>'; } @@ -891,7 +896,8 @@ class UndeleteForm { $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile ); $wgOut->addWikiMsg( 'undelete-show-file-confirm', $this->mTargetObj->getText(), - $wgLang->timeanddate( $file->getTimestamp() ) ); + $wgLang->date( $file->getTimestamp() ), + $wgLang->time( $file->getTimestamp() ) ); $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'POST', @@ -925,7 +931,7 @@ class UndeleteForm { $store->stream( $key ); } - private function showHistory() { + private function showHistory( ) { global $wgLang, $wgUser, $wgOut; $sk = $wgUser->getSkin(); @@ -984,7 +990,7 @@ class UndeleteForm { $action = $titleObj->getLocalURL( "action=submit" ); # Start the form here $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) ); - $wgOut->addHtml( $top ); + $wgOut->addHTML( $top ); } # Show relevant lines from the deletion log: @@ -1007,8 +1013,7 @@ class UndeleteForm { $unsuppressBox = ""; } $table = - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'undelete-fieldset-title' ) ). + Xml::fieldset( wfMsg( 'undelete-fieldset-title' ) ) . Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) . "<tr> <td colspan='2'>" . @@ -1026,15 +1031,16 @@ class UndeleteForm { <tr> <td> </td> <td class='mw-submit'>" . - Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . - Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) . + Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . ' ' . + Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) . ' ' . + Xml::submitButton( wfMsg( 'undeleteinvert' ), array( 'name' => 'invert', 'id' => 'mw-undelete-invert' ) ) . "</td> </tr>" . $unsuppressBox . Xml::closeElement( 'table' ) . Xml::closeElement( 'fieldset' ); - $wgOut->addHtml( $table ); + $wgOut->addHTML( $table ); } $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'history' ) ) . "\n" ); @@ -1044,7 +1050,7 @@ class UndeleteForm { $wgOut->addHTML("<ul>"); $target = urlencode( $this->mTarget ); $remaining = $revisions->numRows(); - $earliestLiveTime = $this->getEarliestTime( $this->mTargetObj ); + $earliestLiveTime = $this->mTargetObj->getEarliestRevTime(); while( $row = $revisions->fetchObject() ) { $remaining--; @@ -1057,8 +1063,8 @@ class UndeleteForm { } if( $haveFiles ) { - $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" ); - $wgOut->addHtml( "<ul>" ); + $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" ); + $wgOut->addHTML( "<ul>" ); while( $row = $files->fetchObject() ) { $wgOut->addHTML( $this->formatFileRow( $row, $sk ) ); } @@ -1071,7 +1077,7 @@ class UndeleteForm { $misc = Xml::hidden( 'target', $this->mTarget ); $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() ); $misc .= Xml::closeElement( 'form' ); - $wgOut->addHtml( $misc ); + $wgOut->addHTML( $misc ); } return true; @@ -1093,7 +1099,15 @@ class UndeleteForm { $stxt = ''; $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); if( $this->mAllowed ) { - $checkBox = Xml::check( "ts$ts" ); + if( $this->mInvert){ + if( in_array( $ts, $this->mTargetTimestamp ) ) { + $checkBox = Xml::check( "ts$ts"); + } else { + $checkBox = Xml::check( "ts$ts", true ); + } + } else { + $checkBox = Xml::check( "ts$ts" ); + } $titleObj = SpecialPage::getTitleFor( "Undelete" ); $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk ); # Last link @@ -1123,7 +1137,6 @@ class UndeleteForm { // If revision was hidden from sysops $del = wfMsgHtml('rev-delundel'); } else { - $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); $del = $sk->makeKnownLinkObj( $revdel, wfMsgHtml('rev-delundel'), 'target=' . $this->mTargetObj->getPrefixedUrl() . "&artimestamp=$ts" ); @@ -1183,18 +1196,6 @@ class UndeleteForm { return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n"; } - private function getEarliestTime( $title ) { - $dbr = wfGetDB( DB_SLAVE ); - if( $title->exists() ) { - $min = $dbr->selectField( 'revision', - 'MIN(rev_timestamp)', - array( 'rev_page' => $title->getArticleId() ), - __METHOD__ ); - return wfTimestampOrNull( TS_MW, $min ); - } - return null; - } - /** * Fetch revision text link if it's available to all users * @return string @@ -1286,10 +1287,10 @@ class UndeleteForm { $skin = $wgUser->getSkin(); $link = $skin->makeKnownLinkObj( $this->mTargetObj ); - $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) ); + $wgOut->addHTML( wfMsgWikiHtml( 'undeletedpage', $link ) ); } else { $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); - $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' ); + $wgOut->addHTML( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' ); } // Show file deletion warnings and errors diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php index d71b638f..4adf405d 100644 --- a/includes/specials/SpecialUnusedimages.php +++ b/includes/specials/SpecialUnusedimages.php @@ -33,7 +33,7 @@ class UnusedimagesPage extends ImageQueryPage { FROM ((($page AS I LEFT JOIN $categorylinks AS L ON I.page_id = L.cl_from) LEFT JOIN $imagelinks AS P ON I.page_title = P.il_to) INNER JOIN $image AS G ON I.page_title = G.img_name) - WHERE I.page_namespace = ".NS_IMAGE." AND L.cl_from IS NULL AND P.il_to IS NULL"; + WHERE I.page_namespace = ".NS_FILE." AND L.cl_from IS NULL AND P.il_to IS NULL"; } else { list( $image, $imagelinks ) = $dbr->tableNamesN( 'image','imagelinks' ); diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index 3a79e052..450c8728 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -23,7 +23,7 @@ class UploadForm { const BEFORE_PROCESSING = 1; const LARGE_FILE_SERVER = 2; const EMPTY_FILE = 3; - const MIN_LENGHT_PARTNAME = 4; + const MIN_LENGTH_PARTNAME = 4; const ILLEGAL_FILENAME = 5; const PROTECTED_PAGE = 6; const OVERWRITE_EXISTING_FILE = 7; @@ -300,7 +300,7 @@ class UploadForm { $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) ); break; - case self::MIN_LENGHT_PARTNAME: + case self::MIN_LENGTH_PARTNAME: $this->mainUploadForm( wfMsgHtml( 'minlength1' ) ); break; @@ -328,10 +328,7 @@ class UploadForm { wfMsgExt( 'filetype-banned-type', array( 'parseinline' ), htmlspecialchars( $finalExt ), - implode( - wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ), - $wgFileExtensions - ), + $wgLang->commaList( $wgFileExtensions ), $wgLang->formatNum( count($wgFileExtensions) ) ) ); @@ -402,7 +399,15 @@ class UploadForm { $basename = $this->mSrcName; } $filtered = wfStripIllegalFilenameChars( $basename ); - + + /* Normalize to title form before we do any further processing */ + $nt = Title::makeTitleSafe( NS_FILE, $filtered ); + if( is_null( $nt ) ) { + $resultDetails = array( 'filtered' => $filtered ); + return self::ILLEGAL_FILENAME; + } + $filtered = $nt->getDBkey(); + /** * We'll want to blacklist against *any* 'extension', and use * only the final one for the whitelist. @@ -423,14 +428,9 @@ class UploadForm { } if( strlen( $partname ) < 1 ) { - return self::MIN_LENGHT_PARTNAME; + return self::MIN_LENGTH_PARTNAME; } - $nt = Title::makeTitleSafe( NS_IMAGE, $filtered ); - if( is_null( $nt ) ) { - $resultDetails = array( 'filtered' => $filtered ); - return self::ILLEGAL_FILENAME; - } $this->mLocalFile = wfLocalFile( $nt ); $this->mDestName = $this->mLocalFile->getName(); @@ -520,10 +520,7 @@ class UploadForm { wfMsgExt( 'filetype-unwanted-type', array( 'parseinline' ), htmlspecialchars( $finalExt ), - implode( - wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ), - $wgFileExtensions - ), + $wgLang->commaList( $wgFileExtensions ), $wgLang->formatNum( count($wgFileExtensions) ) ) . '</li>'; } @@ -544,7 +541,7 @@ class UploadForm { $warning .= self::getExistsWarning( $this->mLocalFile ); } - $warning .= $this->getDupeWarning( $this->mTempPath ); + $warning .= $this->getDupeWarning( $this->mTempPath, $finalExt ); if( $warning != '' ) { /** @@ -610,7 +607,7 @@ class UploadForm { // extensions (eg 'jpg' rather than 'JPEG'). // // Check for another file using the normalized form... - $nt_lc = Title::makeTitle( NS_IMAGE, $partname . '.' . $file->getExtension() ); + $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() ); $file_lc = wfLocalFile( $nt_lc ); } else { $file_lc = false; @@ -737,7 +734,7 @@ class UploadForm { public static function ajaxGetLicensePreview( $license ) { global $wgParser, $wgUser; $text = '{{' . $license . '}}'; - $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' ); + $title = Title::makeTitle( NS_FILE, 'Sample.jpg' ); $options = ParserOptions::newFromUser( $wgUser ); // Expand subst: first, then live templates... @@ -751,9 +748,10 @@ class UploadForm { * Check for duplicate files and throw up a warning before the upload * completes. */ - function getDupeWarning( $tempfile ) { + function getDupeWarning( $tempfile, $extension ) { $hash = File::sha1Base36( $tempfile ); $dupes = RepoGroup::singleton()->findBySha1( $hash ); + $archivedImage = new ArchivedFile( null, 0, $hash.".$extension" ); if( $dupes ) { global $wgOut; $msg = "<gallery>"; @@ -767,6 +765,10 @@ class UploadForm { wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) . $wgOut->parse( $msg ) . "</li>\n"; + } elseif ( $archivedImage->getID() > 0 ) { + global $wgOut; + $name = Title::makeTitle( NS_FILE, $archivedImage->getName() )->getPrefixedText(); + return Xml::tags( 'li', null, wfMsgExt( 'file-deleted-duplicate', array( 'parseinline' ), array( $name ) ) ); } else { return ''; } @@ -961,7 +963,7 @@ wgUploadAutoFill = {$autofill}; } if( $this->mDesiredDestName ) { - $title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName ); + $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); // Show a subtitle link to deleted revisions (to sysops et al only) if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) { $link = wfMsgExt( @@ -972,7 +974,7 @@ wgUploadAutoFill = {$autofill}; wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count ) ) ); - $wgOut->addHtml( "<div id=\"contentSub2\">{$link}</div>" ); + $wgOut->addHTML( "<div id=\"contentSub2\">{$link}</div>" ); } // Show the relevant lines from deletion log (for still deleted files only) @@ -1005,21 +1007,20 @@ wgUploadAutoFill = {$autofill}; $allowedExtensions = ''; if( $wgCheckFileExtensions ) { - $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ); if( $wgStrictFileExtensions ) { # Everything not permitted is banned $extensionsList = '<div id="mw-upload-permitted">' . - wfMsgWikiHtml( 'upload-permitted', implode( $wgFileExtensions, $delim ) ) . + wfMsgWikiHtml( 'upload-permitted', $wgLang->commaList( $wgFileExtensions ) ) . "</div>\n"; } else { # We have to list both preferred and prohibited $extensionsList = '<div id="mw-upload-preferred">' . - wfMsgWikiHtml( 'upload-preferred', implode( $wgFileExtensions, $delim ) ) . + wfMsgWikiHtml( 'upload-preferred', $wgLang->commaList( $wgFileExtensions ) ) . "</div>\n" . '<div id="mw-upload-prohibited">' . - wfMsgWikiHtml( 'upload-prohibited', implode( $wgFileBlacklist, $delim ) ) . + wfMsgWikiHtml( 'upload-prohibited', $wgLang->commaList( $wgFileBlacklist ) ) . "</div>\n"; } } else { @@ -1169,7 +1170,7 @@ wgUploadAutoFill = {$autofill}; <tr>" ); if( $useAjaxLicensePreview ) { - $wgOut->addHtml( " + $wgOut->addHTML( " <td></td> <td id=\"mw-license-preview\"></td> </tr> @@ -1205,7 +1206,7 @@ wgUploadAutoFill = {$autofill}; ); } - $wgOut->addHtml( " + $wgOut->addHTML( " <td></td> <td> <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' /> @@ -1279,7 +1280,7 @@ wgUploadAutoFill = {$autofill}; * * @return array */ - function splitExtensions( $filename ) { + public function splitExtensions( $filename ) { $bits = explode( '.', $filename ); $basename = array_shift( $bits ); return array( $basename, $bits ); @@ -1305,7 +1306,7 @@ wgUploadAutoFill = {$autofill}; * @param array $list * @return bool */ - function checkFileExtensionList( $ext, $list ) { + public function checkFileExtensionList( $ext, $list ) { foreach( $ext as $e ) { if( in_array( strtolower( $e ), $list ) ) { return true; @@ -1754,7 +1755,7 @@ wgUploadAutoFill = {$autofill}; function showError( $description ) { global $wgOut; $wgOut->setPageTitle( wfMsg( "internalerror" ) ); - $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setRobotPolicy( "noindex,nofollow" ); $wgOut->setArticleRelated( false ); $wgOut->enableClientCache( false ); $wgOut->addWikiText( $description ); @@ -1797,14 +1798,14 @@ wgUploadAutoFill = {$autofill}; $loglist = new LogEventsList( $wgUser->getSkin(), $out ); $pager = new LogPager( $loglist, 'delete', false, $filename ); if( $pager->getNumRows() > 0 ) { - $out->addHtml( '<div id="mw-upload-deleted-warn">' ); + $out->addHTML( '<div class="mw-warning-with-logexcerpt">' ); $out->addWikiMsg( 'upload-wasdeleted' ); $out->addHTML( $loglist->beginLogEventsList() . $pager->getBody() . $loglist->endLogEventsList() ); - $out->addHtml( '</div>' ); + $out->addHTML( '</div>' ); } } } diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php index 27009eed..6a4da7a4 100644 --- a/includes/specials/SpecialUserlogin.php +++ b/includes/specials/SpecialUserlogin.php @@ -33,6 +33,7 @@ class LoginForm { const RESET_PASS = 7; const ABORTED = 8; const CREATE_BLOCKED = 9; + const THROTTLED = 10; var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted; var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword; @@ -128,9 +129,10 @@ class LoginForm { $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' ); wfRunHooks( 'AddNewAccount', array( $u, true ) ); + $u->addNewUserLogEntry(); $wgOut->setPageTitle( wfMsg( 'accmailtitle' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); if( WikiError::isError( $result ) ) { @@ -174,14 +176,16 @@ class LoginForm { # Save settings (including confirmation token) $u->saveSettings(); - # If not logged in, assume the new account as the current one and set session cookies - # then show a "welcome" message or a "need cookies" message as needed + # If not logged in, assume the new account as the current one and set + # session cookies then show a "welcome" message or a "need cookies" + # message as needed if( $wgUser->isAnon() ) { $wgUser = $u; $wgUser->setCookies(); wfRunHooks( 'AddNewAccount', array( $wgUser ) ); + $wgUser->addNewUserLogEntry(); if( $this->hasSessionCookie() ) { - return $this->successfulLogin( 'welcomecreation', $wgUser->getName(), false ); + return $this->successfulCreation(); } else { return $this->cookieRedirectCheck( 'new' ); } @@ -192,9 +196,10 @@ class LoginForm { $wgOut->setPageTitle( wfMsgHtml( 'accountcreated' ) ); $wgOut->setArticleRelated( false ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) ); + $wgOut->addHTML( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) ); $wgOut->returnToMain( false, $self ); wfRunHooks( 'AddNewAccount', array( $u ) ); + $u->addNewUserLogEntry(); return true; } } @@ -215,12 +220,11 @@ class LoginForm { return false; } - // If we are not allowing users to login locally, we should - // be checking to see if the user is actually able to - // authenticate to the authentication server before they - // create an account (otherwise, they can create a local account - // and login as any domain user). We only need to check this for - // domains that aren't local. + // If we are not allowing users to login locally, we should be checking + // to see if the user is actually able to authenticate to the authenti- + // cation server before they create an account (otherwise, they can + // create a local account and login as any domain user). We only need + // to check this for domains that aren't local. if( 'local' != $this->mDomain && '' != $this->mDomain ) { if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) { $this->mainLoginForm( wfMsg( 'wrongpassword' ) ); @@ -280,7 +284,8 @@ class LoginForm { } } - # if you need a confirmed email address to edit, then obviously you need an email address. + # if you need a confirmed email address to edit, then obviously you + # need an email address. if ( $wgEmailConfirmToEdit && empty( $this->mEmail ) ) { $this->mainLoginForm( wfMsg( 'noemailtitle' ) ); return false; @@ -291,8 +296,8 @@ class LoginForm { return false; } - # Set some additional data so the AbortNewAccount hook can be - # used for more than just username validation + # Set some additional data so the AbortNewAccount hook can be used for + # more than just username validation $u->setEmail( $this->mEmail ); $u->setRealName( $this->mRealName ); @@ -306,14 +311,15 @@ class LoginForm { if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) { $key = wfMemcKey( 'acctcreate', 'ip', $ip ); - $value = $wgMemc->incr( $key ); + $value = $wgMemc->get( $key ); if ( !$value ) { - $wgMemc->set( $key, 1, 86400 ); + $wgMemc->set( $key, 0, 86400 ); } - if ( $value > $wgAccountCreationThrottle ) { + if ( $value >= $wgAccountCreationThrottle ) { $this->throttleHit( $wgAccountCreationThrottle ); return false; } + $wgMemc->incr( $key ); } if( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) { @@ -372,12 +378,32 @@ class LoginForm { if ( '' == $this->mName ) { return self::NO_NAME; } + + global $wgPasswordAttemptThrottle; + + $throttleCount=0; + if ( is_array($wgPasswordAttemptThrottle) ) { + $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) ); + $count = $wgPasswordAttemptThrottle['count']; + $period = $wgPasswordAttemptThrottle['seconds']; + + global $wgMemc; + $throttleCount = $wgMemc->get($throttleKey); + if ( !$throttleCount ) { + $wgMemc->add( $throttleKey, 1, $period ); // start counter + } else if ( $throttleCount < $count ) { + $wgMemc->incr($throttleKey); + } else if ( $throttleCount >= $count ) { + return self::THROTTLED; + } + } - // Load $wgUser now, and check to see if we're logging in as the same name. - // This is necessary because loading $wgUser (say by calling getName()) calls - // the UserLoadFromSession hook, which potentially creates the user in the - // database. Until we load $wgUser, checking for user existence using - // User::newFromName($name)->getId() below will effectively be using stale data. + // Load $wgUser now, and check to see if we're logging in as the same + // name. This is necessary because loading $wgUser (say by calling + // getName()) calls the UserLoadFromSession hook, which potentially + // creates the user in the database. Until we load $wgUser, checking + // for user existence using User::newFromName($name)->getId() below + // will effectively be using stale data. if ( $wgUser->getName() === $this->mName ) { wfDebug( __METHOD__.": already logged in as {$this->mName}\n" ); return self::SUCCESS; @@ -407,34 +433,30 @@ class LoginForm { if (!$u->checkPassword( $this->mPassword )) { if( $u->checkTemporaryPassword( $this->mPassword ) ) { - // The e-mailed temporary password should not be used - // for actual logins; that's a very sloppy habit, - // and insecure if an attacker has a few seconds to - // click "search" on someone's open mail reader. + // The e-mailed temporary password should not be used for actu- + // al logins; that's a very sloppy habit, and insecure if an + // attacker has a few seconds to click "search" on someone's o- + // pen mail reader. // - // Allow it to be used only to reset the password - // a single time to a new value, which won't be in - // the user's e-mail archives. + // Allow it to be used only to reset the password a single time + // to a new value, which won't be in the user's e-mail ar- + // chives. // - // For backwards compatibility, we'll still recognize - // it at the login form to minimize surprises for - // people who have been logging in with a temporary - // password for some time. - // - // As a side-effect, we can authenticate the user's - // e-mail address if it's not already done, since - // the temporary password was sent via e-mail. + // For backwards compatibility, we'll still recognize it at the + // login form to minimize surprises for people who have been + // logging in with a temporary password for some time. // + // As a side-effect, we can authenticate the user's e-mail ad- + // dress if it's not already done, since the temporary password + // was sent via e-mail. if( !$u->isEmailConfirmed() ) { $u->confirmEmail(); $u->saveSettings(); } - // At this point we just return an appropriate code - // indicating that the UI should show a password - // reset form; bot interfaces etc will probably just - // fail cleanly here. - // + // At this point we just return an appropriate code/ indicating + // that the UI should show a password reset form; bot inter- + // faces etc will probably just fail cleanly here. $retval = self::RESET_PASS; } else { $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS; @@ -443,6 +465,11 @@ class LoginForm { $wgAuth->updateUser( $u ); $wgUser = $u; + // Please reset throttle for successful logins, thanks! + if($throttleCount) { + $wgMemc->delete($throttleKey); + } + if ( $isAutoCreated ) { // Must be run after $wgUser is set, for correct new user log wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) ); @@ -455,16 +482,16 @@ class LoginForm { } /** - * Attempt to automatically create a user on login. - * Only succeeds if there is an external authentication method which allows it. + * Attempt to automatically create a user on login. Only succeeds if there + * is an external authentication method which allows it. * @return integer Status code */ function attemptAutoCreate( $user ) { global $wgAuth, $wgUser; /** - * If the external authentication plugin allows it, - * automatically create a new account for users that - * are externally defined but have not yet logged in. + * If the external authentication plugin allows it, automatically cre- + * ate a new account for users that are externally defined but have not + * yet logged in. */ if ( !$wgAuth->autoCreate() ) { return self::NOT_EXISTS; @@ -502,14 +529,19 @@ class LoginForm { } $wgUser->setCookies(); + // Reset the throttle + $key = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) ); + global $wgMemc; + $wgMemc->delete( $key ); + if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) { - /* Replace the language object to provide user interface in correct - * language immediately on this first page load. + /* Replace the language object to provide user interface in + * correct language immediately on this first page load. */ global $wgLang, $wgRequest; $code = $wgRequest->getVal( 'uselang', $wgUser->getOption( 'language' ) ); $wgLang = Language::factory( $code ); - return $this->successfulLogin( 'loginsuccess', $wgUser->getName() ); + return $this->successfulLogin(); } else { return $this->cookieRedirectCheck( 'login' ); } @@ -524,7 +556,7 @@ class LoginForm { break; case self::NOT_EXISTS: if( $wgUser->isAllowed( 'createaccount' ) ){ - $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) ); + $this->mainLoginForm( wfMsgWikiHtml( 'nosuchuser', htmlspecialchars( $this->mName ) ) ); } else { $this->mainLoginForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->mName ) ) ); } @@ -541,6 +573,9 @@ class LoginForm { case self::CREATE_BLOCKED: $this->userBlockedMessage(); break; + case self::THROTTLED: + $this->mainLoginForm( wfMsg( 'login-throttled' ) ); + break; default: throw new MWException( "Unhandled case value" ); } @@ -548,8 +583,8 @@ class LoginForm { function resetLoginForm( $error ) { global $wgOut; - $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" ); - $reset = new PasswordResetForm( $this->mName, $this->mPassword ); + $wgOut->addHTML( Xml::element('p', array( 'class' => 'error' ), $error ) ); + $reset = new SpecialResetpass(); $reset->execute( null ); } @@ -587,14 +622,15 @@ class LoginForm { return; } if ( 0 == $u->getID() ) { - $this->mainLoginForm( wfMsg( 'nosuchuser', $u->getName() ) ); + $this->mainLoginForm( wfMsgWikiHtml( 'nosuchuser', htmlspecialchars( $u->getName() ) ) ); return; } # Check against password throttle if ( $u->isPasswordReminderThrottled() ) { global $wgPasswordReminderResendTime; - # Round the time in hours to 3 d.p., in case someone is specifying minutes or seconds. + # Round the time in hours to 3 d.p., in case someone is specifying + # minutes or seconds. $this->mainLoginForm( wfMsgExt( 'throttled-mailpassword', array( 'parsemag' ), round( $wgPasswordReminderResendTime, 3 ) ) ); return; @@ -618,20 +654,22 @@ class LoginForm { * @private */ function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) { - global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure; - global $wgServer, $wgScript; + global $wgServer, $wgScript, $wgUser; if ( '' == $u->getEmail() ) { return new WikiError( wfMsg( 'noemail', $u->getName() ) ); } + $ip = wfGetIP(); + if( !$ip ) { + return new WikiError( wfMsg( 'badipaddress' ) ); + } + + wfRunHooks( 'User::mailPasswordInternal', array(&$wgUser, &$ip, &$u) ); $np = $u->randomPassword(); $u->setNewpassword( $np, $throttle ); $u->saveSettings(); - $ip = wfGetIP(); - if ( '' == $ip ) { $ip = '(Unknown)'; } - $m = wfMsg( $emailText, $ip, $u->getName(), $np, $wgServer . $wgScript ); $result = $u->sendMail( wfMsg( $emailTitle ), $m ); @@ -640,29 +678,66 @@ class LoginForm { /** - * @param string $msg Message key that will be shown on success - * @param $params String: parameters for the above message - * @param bool $auto Toggle auto-redirect to main page; default true + * Run any hooks registered for logins, then HTTP redirect to + * $this->mReturnTo (or Main Page if that's undefined). Formerly we had a + * nice message here, but that's really not as useful as just being sent to + * wherever you logged in from. It should be clear that the action was + * successful, given the lack of error messages plus the appearance of your + * name in the upper right. + * * @private */ - function successfulLogin( $msg, $params, $auto = true ) { - global $wgUser; - global $wgOut; + function successfulLogin() { + global $wgUser, $wgOut; - # Run any hooks; ignore results + # Run any hooks; display injected HTML if any, else redirect + $injected_html = ''; + wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html)); + if( $injected_html !== '' ) { + $this->displaySuccessfulLogin( 'loginsuccess', $injected_html ); + } else { + $titleObj = Title::newFromText( $this->mReturnTo ); + if ( !$titleObj instanceof Title ) { + $titleObj = Title::newMainPage(); + } + + $wgOut->redirect( $titleObj->getFullURL() ); + } + } + + /** + * Run any hooks registered for logins, then display a message welcoming + * the user. + * + * @private + */ + function successfulCreation() { + global $wgUser, $wgOut; + + # Run any hooks; display injected HTML $injected_html = ''; wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html)); + $this->displaySuccessfulLogin( 'welcomecreation', $injected_html ); + } + + /** + * Display a "login successful" page. + */ + private function displaySuccessfulLogin( $msgname, $injected_html ) { + global $wgOut, $wgUser; + $wgOut->setPageTitle( wfMsg( 'loginsuccesstitle' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addWikiMsgArray( $msg, $params ); - $wgOut->addHtml( $injected_html ); + $wgOut->addWikiMsg( $msgname, $wgUser->getName() ); + $wgOut->addHTML( $injected_html ); + if ( !empty( $this->mReturnTo ) ) { - $wgOut->returnToMain( $auto, $this->mReturnTo ); + $wgOut->returnToMain( null, $this->mReturnTo ); } else { - $wgOut->returnToMain( $auto ); + $wgOut->returnToMain( null ); } } @@ -671,11 +746,12 @@ class LoginForm { global $wgOut; $wgOut->setPageTitle( wfMsg( 'permissionserrors' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); $wgOut->addWikitext( $wgOut->formatPermissionsErrorMessage( $errors, 'createaccount' ) ); - // Stuff that might want to be added at the end. For example, instructions if blocked. + // Stuff that might want to be added at the end. For example, instruc- + // tions if blocked. $wgOut->addWikiMsg( 'cantcreateaccount-nonblock-text' ); $wgOut->returnToMain( false ); @@ -694,7 +770,7 @@ class LoginForm { # out. $wgOut->setPageTitle( wfMsg( 'cantcreateaccounttitle' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); $ip = wfGetIP(); @@ -713,8 +789,8 @@ class LoginForm { */ function mainLoginForm( $msg, $msgtype = 'error' ) { global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail; - global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector; - global $wgAuth, $wgEmailConfirmToEdit; + global $wgCookiePrefix, $wgLoginLanguageSelector; + global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration; $titleObj = SpecialPage::getTitleFor( 'Userlogin' ); @@ -792,6 +868,7 @@ class LoginForm { $template->set( 'useemail', $wgEnableEmail ); $template->set( 'emailrequired', $wgEmailConfirmToEdit ); $template->set( 'canreset', $wgAuth->allowPasswordChange() ); + $template->set( 'canremember', ( $wgCookieExpiration > 0 ) ); $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember ); # Prepare language selection links as needed @@ -810,7 +887,7 @@ class LoginForm { } $wgOut->setPageTitle( wfMsg( 'userlogin' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); $wgOut->disallowUserJs(); // just in case... $wgOut->addTemplate( $template ); @@ -832,9 +909,9 @@ class LoginForm { /** * Check if a session cookie is present. * - * This will not pick up a cookie set during _this_ request, but is - * meant to ensure that the client is returning the cookie which was - * set on a previous pass through the system. + * This will not pick up a cookie set during _this_ request, but is meant + * to ensure that the client is returning the cookie which was set on a + * previous pass through the system. * * @private */ @@ -850,7 +927,9 @@ class LoginForm { global $wgOut; $titleObj = SpecialPage::getTitleFor( 'Userlogin' ); - $check = $titleObj->getFullURL( 'wpCookieCheck='.$type ); + $query = array( 'wpCookieCheck' => $type ); + if ( $this->mReturnTo ) $query['returnto'] = $this->mReturnTo; + $check = $titleObj->getFullURL( $query ); return $wgOut->redirect( $check ); } @@ -871,7 +950,7 @@ class LoginForm { return $this->mainLoginForm( wfMsg( 'error' ) ); } } else { - return $this->successfulLogin( 'loginsuccess', $wgUser->getName() ); + return $this->successfulLogin(); } } @@ -879,9 +958,7 @@ class LoginForm { * @private */ function throttleHit( $limit ) { - global $wgOut; - - $wgOut->addWikiMsg( 'acct_creation_throttle_hit', $limit ); + $this->mainLoginForm( wfMsgExt( 'acct_creation_throttle_hit', array( 'parseinline' ), $limit ) ); } /** diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php index 137eadb4..3d497bd7 100644 --- a/includes/specials/SpecialUserlogout.php +++ b/includes/specials/SpecialUserlogout.php @@ -12,7 +12,7 @@ function wfSpecialUserlogout() { $oldName = $wgUser->getName(); $wgUser->logout(); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); // Hook. $injected_html = ''; diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index fd3c690b..ce0097b2 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -26,10 +26,14 @@ class UserrightsPage extends SpecialPage { } public function userCanExecute( $user ) { + return $this->userCanChangeRights( $user, false ); + } + + public function userCanChangeRights( $user, $checkIfSelf = true ) { $available = $this->changeableGroups(); return !empty( $available['add'] ) or !empty( $available['remove'] ) - or ($this->isself and + or ( ( $this->isself || !$checkIfSelf ) and (!empty( $available['add-self'] ) or !empty( $available['remove-self'] ))); } @@ -65,7 +69,7 @@ class UserrightsPage extends SpecialPage { if ($this->mTarget == $wgUser->getName()) $this->isself = true; - if( !$this->userCanExecute( $wgUser ) ) { + if( !$this->userCanChangeRights( $wgUser, true ) ) { // fixme... there may be intermediate groups we can mention. global $wgOut; $wgOut->showPermissionsErrorPage( array( @@ -141,13 +145,8 @@ class UserrightsPage extends SpecialPage { // Validate input set... $changeable = $this->changeableGroups(); - if ($wgUser->getId() != 0 && $wgUser->getId() == $user->getId()) { - $addable = array_merge($changeable['add'], $wgGroupsAddToSelf); - $removable = array_merge($changeable['remove'], $wgGroupsRemoveFromSelf); - } else { - $addable = $changeable['add']; - $removable = $changeable['remove']; - } + $addable = array_merge( $changeable['add'], $this->isself ? $changeable['add-self'] : array() ); + $removable = array_merge( $changeable['remove'], $this->isself ? $changeable['remove-self'] : array() ); $removegroup = array_unique( array_intersect( (array)$removegroup, $removable ) ); @@ -289,7 +288,7 @@ class UserrightsPage extends SpecialPage { function makeGroupNameList( $ids ) { if( empty( $ids ) ) { - return wfMsg( 'rightsnone' ); + return wfMsgForContent( 'rightsnone' ); } else { return implode( ', ', $ids ); } @@ -329,14 +328,13 @@ class UserrightsPage extends SpecialPage { * @return Array: Tuple of addable, then removable groups */ protected function splitGroups( $groups ) { - global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; - list($addable, $removable) = array_values( $this->changeableGroups() ); + list($addable, $removable, $addself, $removeself) = array_values( $this->changeableGroups() ); $removable = array_intersect( - array_merge($this->isself ? $wgGroupsRemoveFromSelf : array(), $removable), + array_merge( $this->isself ? $removeself : array(), $removable ), $groups ); // Can't remove groups the user doesn't have $addable = array_diff( - array_merge($this->isself ? $wgGroupsAddToSelf : array(), $addable), + array_merge( $this->isself ? $addself : array(), $addable ), $groups ); // Can't add groups the user does have return array( $addable, $removable ); @@ -351,10 +349,8 @@ class UserrightsPage extends SpecialPage { protected function showEditUserGroupsForm( $user, $groups ) { global $wgOut, $wgUser, $wgLang; - list( $addable, $removable ) = $this->splitGroups( $groups ); - $list = array(); - foreach( $user->getGroups() as $group ) + foreach( $groups as $group ) $list[] = self::buildGroupLink( $group ); $grouplist = ''; @@ -384,7 +380,7 @@ class UserrightsPage extends SpecialPage { <tr> <td></td> <td class='mw-submit'>" . - Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) . + Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups', 'accesskey' => 's' ) ) . "</td> </tr>" . Xml::closeElement( 'table' ) . "\n" . @@ -510,10 +506,10 @@ class UserrightsPage extends SpecialPage { /** * Returns an array of the groups that the user can add/remove. * - * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) ) + * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) ) */ function changeableGroups() { - global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; + global $wgUser; if( $wgUser->isAllowed( 'userrights' ) ) { // This group gives the right to modify everything (reverse- @@ -533,8 +529,8 @@ class UserrightsPage extends SpecialPage { $groups = array( 'add' => array(), 'remove' => array(), - 'add-self' => $wgGroupsAddToSelf, - 'remove-self' => $wgGroupsRemoveFromSelf); + 'add-self' => array(), + 'remove-self' => array() ); $addergroups = $wgUser->getEffectiveGroups(); foreach ($addergroups as $addergroup) { @@ -543,7 +539,13 @@ class UserrightsPage extends SpecialPage { ); $groups['add'] = array_unique( $groups['add'] ); $groups['remove'] = array_unique( $groups['remove'] ); + $groups['add-self'] = array_unique( $groups['add-self'] ); + $groups['remove-self'] = array_unique( $groups['remove-self'] ); } + + // Run a hook because we can + wfRunHooks( 'UserrightsChangeableGroups', array( $this, $wgUser, $addergroups, &$groups ) ); + return $groups; } @@ -551,12 +553,12 @@ class UserrightsPage extends SpecialPage { * Returns an array of the groups that a particular group can add/remove. * * @param $group String: the group to check for whether it can add/remove - * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) ) + * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) ) */ private function changeableByGroup( $group ) { - global $wgAddGroups, $wgRemoveGroups; + global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; - $groups = array( 'add' => array(), 'remove' => array() ); + $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); if( empty($wgAddGroups[$group]) ) { // Don't add anything to $groups } elseif( $wgAddGroups[$group] === true ) { @@ -573,6 +575,40 @@ class UserrightsPage extends SpecialPage { } elseif( is_array($wgRemoveGroups[$group]) ) { $groups['remove'] = $wgRemoveGroups[$group]; } + + // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility + if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) { + foreach($wgGroupsAddToSelf as $key => $value) { + if( is_int($key) ) { + $wgGroupsAddToSelf['user'][] = $value; + } + } + } + + if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) { + foreach($wgGroupsRemoveFromSelf as $key => $value) { + if( is_int($key) ) { + $wgGroupsRemoveFromSelf['user'][] = $value; + } + } + } + + // Now figure out what groups the user can add to him/herself + if( empty($wgGroupsAddToSelf[$group]) ) { + } elseif( $wgGroupsAddToSelf[$group] === true ) { + // No idea WHY this would be used, but it's there + $groups['add-self'] = User::getAllGroups(); + } elseif( is_array($wgGroupsAddToSelf[$group]) ) { + $groups['add-self'] = $wgGroupsAddToSelf[$group]; + } + + if( empty($wgGroupsRemoveFromSelf[$group]) ) { + } elseif( $wgGroupsRemoveFromSelf[$group] === true ) { + $groups['remove-self'] = User::getAllGroups(); + } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) { + $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; + } + return $groups; } @@ -583,7 +619,7 @@ class UserrightsPage extends SpecialPage { * @param $output OutputPage to use */ protected function showLogFragment( $user, $output ) { - $output->addHtml( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) ); + $output->addHTML( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) ); LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage()->getPrefixedText() ); } } diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index 8c8e386d..29f527f2 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -1,42 +1,37 @@ <?php -/**#@+ + +/** * Give information about the version of MediaWiki, PHP, the DB and extensions * - * @file * @ingroup SpecialPage * * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ - -/** - * constructor - */ -function wfSpecialVersion() { - $version = new SpecialVersion; - $version->execute(); -} - -/** - * @ingroup SpecialPage - */ -class SpecialVersion { +class SpecialVersion extends SpecialPage { private $firstExtOpened = true; + function __construct(){ + parent::__construct( 'Version' ); + } + /** * main() */ - function execute() { + function execute( $par ) { global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks; $wgMessageCache->loadAllMessages(); + $this->setHeaders(); + $this->outputHeader(); + $wgOut->addHTML( '<div dir="ltr">' ); $text = $this->MediaWikiCredits() . $this->softwareInformation() . $this->extensionCredits(); - if ( $wgSpecialVersionShowHooks ) { + if ( $wgSpecialVersionShowHooks ) { $text .= $this->wgHooks(); } $wgOut->addWikiText( $text ); @@ -162,15 +157,21 @@ class SpecialVersion { usort( $wgExtensionCredits[$type], array( $this, 'compare' ) ); foreach ( $wgExtensionCredits[$type] as $extension ) { + $version = null; + $subVersion = ''; if ( isset( $extension['version'] ) ) { $version = $extension['version']; - } elseif ( isset( $extension['svn-revision'] ) && + } + if ( isset( $extension['svn-revision'] ) && preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/', - $extension['svn-revision'], $m ) ) - { - $version = 'r' . $m[1]; - } else { - $version = null; + $extension['svn-revision'], $m ) ) { + $subVersion = 'r' . $m[1]; + } + + if( $version && $subVersion ) { + $version = $version . ' [' . $subVersion . ']'; + } elseif ( !$version && $subVersion ) { + $version = $subVersion; } $out .= $this->formatCredits( @@ -287,8 +288,6 @@ class SpecialVersion { } /** - * @static - * * @return string */ function IPInfo() { @@ -306,35 +305,34 @@ class SpecialVersion { if ( $cnt == 1 ) { // Enforce always returning a string - return (string)$this->arrayToString( $list[0] ); + return (string)self::arrayToString( $list[0] ); } elseif ( $cnt == 0 ) { return ''; } else { + global $wgLang; sort( $list ); - $t = array_slice( $list, 0, $cnt - 1 ); - $one = array_map( array( &$this, 'arrayToString' ), $t ); - $two = $this->arrayToString( $list[$cnt - 1] ); - $and = wfMsg( 'and' ); - - return implode( ', ', $one ) . " $and $two"; + return $wgLang->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) ); } } /** - * @static - * * @param mixed $list Will convert an array to string if given and return * the paramater unaltered otherwise * @return mixed */ - function arrayToString( $list ) { + static function arrayToString( $list ) { + if( is_array( $list ) && count( $list ) == 1 ) + $list = $list[0]; if( is_object( $list ) ) { $class = get_class( $list ); return "($class)"; - } elseif ( ! is_array( $list ) ) { + } elseif ( !is_array( $list ) ) { return $list; } else { - $class = get_class( $list[0] ); + if( is_object( $list[0] ) ) + $class = get_class( $list[0] ); + else + $class = $list[0]; return "($class, {$list[1]})"; } } @@ -387,5 +385,3 @@ class SpecialVersion { /**#@-*/ } - -/**#@-*/ diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php new file mode 100644 index 00000000..c2731fa9 --- /dev/null +++ b/includes/specials/SpecialWantedfiles.php @@ -0,0 +1,90 @@ +<?php +/* + * @file + * @ingroup SpecialPage + */ + +/** + * Querypage that lists the most wanted files - implements Special:Wantedfiles + * + * @ingroup SpecialPage + * + * @author Soxred93 <soxred93@gmail.com> + * @copyright Copyright © 2008, Soxred93 + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + */ +class WantedFilesPage extends QueryPage { + + function getName() { + return 'Wantedfiles'; + } + + function isExpensive() { + return true; + } + + function isSyndicated() { + return false; + } + + function getSQL() { + $dbr = wfGetDB( DB_SLAVE ); + list( $imagelinks, $page ) = $dbr->tableNamesN( 'imagelinks', 'page' ); + $name = $dbr->addQuotes( $this->getName() ); + return + " + SELECT + $name as type, + " . NS_FILE . " as namespace, + il_to as title, + COUNT(*) as value + FROM $imagelinks + LEFT JOIN $page ON il_to = page_title AND page_namespace = ". NS_FILE ." + WHERE page_title IS NULL + GROUP BY il_to + "; + } + + function sortDescending() { return true; } + + /** + * Fetch user page links and cache their existence + */ + function preprocessResults( $db, $res ) { + $batch = new LinkBatch; + while ( $row = $db->fetchObject( $res ) ) + $batch->add( $row->namespace, $row->title ); + $batch->execute(); + + // Back to start for display + if ( $db->numRows( $res ) > 0 ) + // If there are no rows we get an error seeking. + $db->dataSeek( $res, 0 ); + } + + function formatResult( $skin, $result ) { + global $wgLang, $wgContLang; + + $nt = Title::makeTitle( $result->namespace, $result->title ); + $text = $wgContLang->convert( $nt->getText() ); + + $plink = $this->isCached() ? + $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) : + $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) ); + + $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'), + $wgLang->formatNum( $result->value ) ); + return wfSpecialList($plink, $nlinks); + } +} + +/** + * constructor + */ +function wfSpecialWantedFiles() { + list( $limit, $offset ) = wfCheckLimits(); + + $wpp = new WantedFilesPage(); + + $wpp->doQuery( $offset, $limit ); +} diff --git a/includes/specials/SpecialWantedtemplates.php b/includes/specials/SpecialWantedtemplates.php new file mode 100644 index 00000000..43b5cf8f --- /dev/null +++ b/includes/specials/SpecialWantedtemplates.php @@ -0,0 +1,110 @@ +<?php +/** + * @file + * @ingroup SpecialPage + */ + +/** + * A querypage to list the most wanted templates - implements Special:Wantedtemplates + * based on SpecialWantedcategories.php by Ævar Arnfjörð Bjarmason <avarab@gmail.com> + * makeWlhLink() taken from SpecialMostlinkedtemplates by Rob Church <robchur@gmail.com> + * + * @ingroup SpecialPage + * + * @author Danny B. + * @copyright Copyright © 2008, Danny B. + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + */ +class WantedTemplatesPage extends QueryPage { + + function getName() { + return 'Wantedtemplates'; + } + + function isExpensive() { + return true; + } + + function isSyndicated() { + return false; + } + + function getSQL() { + $dbr = wfGetDB( DB_SLAVE ); + list( $templatelinks, $page ) = $dbr->tableNamesN( 'templatelinks', 'page' ); + $name = $dbr->addQuotes( $this->getName() ); + return + " + SELECT $name as type, + tl_namespace as namespace, + tl_title as title, + COUNT(*) as value + FROM $templatelinks LEFT JOIN + $page ON tl_title = page_title AND tl_namespace = page_namespace + WHERE page_title IS NULL AND tl_namespace = ". NS_TEMPLATE ." + GROUP BY tl_title + "; + } + + function sortDescending() { return true; } + + /** + * Fetch user page links and cache their existence + */ + function preprocessResults( $db, $res ) { + $batch = new LinkBatch; + while ( $row = $db->fetchObject( $res ) ) + $batch->add( $row->namespace, $row->title ); + $batch->execute(); + + // Back to start for display + if ( $db->numRows( $res ) > 0 ) + // If there are no rows we get an error seeking. + $db->dataSeek( $res, 0 ); + } + + function formatResult( $skin, $result ) { + global $wgLang, $wgContLang; + + $nt = Title::makeTitle( $result->namespace, $result->title ); + $text = $wgContLang->convert( $nt->getText() ); + + $plink = $this->isCached() ? + $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) : + $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) ); + + $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'), + $wgLang->formatNum( $result->value ) ); + return wfSpecialList( + $plink, + $this->makeWlhLink( $nt, $skin, $result ) + ); + } + + /** + * Make a "what links here" link for a given title + * + * @param Title $title Title to make the link for + * @param Skin $skin Skin to use + * @param object $result Result row + * @return string + */ + private function makeWlhLink( $title, $skin, $result ) { + global $wgLang; + $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' ); + $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $result->value ) ); + return $skin->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) ); + } +} + +/** + * constructor + */ +function wfSpecialWantedTemplates() { + list( $limit, $offset ) = wfCheckLimits(); + + $wpp = new WantedTemplatesPage(); + + $wpp->doQuery( $offset, $limit ); +} diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index db7cd423..61dd6b3e 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -13,7 +13,6 @@ function wfSpecialWatchlist( $par ) { global $wgUser, $wgOut, $wgLang, $wgRequest; global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker; global $wgEnotifWatchlist; - $fname = 'wfSpecialWatchlist'; $skin = $wgUser->getSkin(); $specialTitle = SpecialPage::getTitleFor( 'Watchlist' ); @@ -22,8 +21,9 @@ function wfSpecialWatchlist( $par ) { # Anons don't get a watchlist if( $wgUser->isAnon() ) { $wgOut->setPageTitle( wfMsg( 'watchnologin' ) ); - $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() ); - $wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) ); + $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), + wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() ); + $wgOut->addHTML( wfMsgWikiHtml( 'watchlistanontext', $llink ) ); return; } @@ -40,40 +40,56 @@ function wfSpecialWatchlist( $par ) { } $uid = $wgUser->getId(); - if( ($wgEnotifWatchlist || $wgShowUpdatedMarker) && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) { + if( ($wgEnotifWatchlist || $wgShowUpdatedMarker) && $wgRequest->getVal( 'reset' ) && + $wgRequest->wasPosted() ) + { $wgUser->clearAllNotifications( $uid ); $wgOut->redirect( $specialTitle->getFullUrl() ); return; } $defaults = array( - /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */ - /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ), - /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ), - /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ), + /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */ + /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ), + /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ), + /* bool */ 'hideAnons' => (int)$wgUser->getBoolOption( 'watchlisthideanons' ), + /* bool */ 'hideLiu' => (int)$wgUser->getBoolOption( 'watchlisthideliu' ), + /* bool */ 'hidePatrolled' => (int)$wgUser->getBoolOption( 'watchlisthidepatrolled' ), // TODO + /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ), /* ? */ 'namespace' => 'all', + /* ? */ 'invert' => false, ); extract($defaults); # Extract variables from the request, falling back to user preferences or # other default values if these don't exist - $prefs['days' ] = floatval( $wgUser->getOption( 'watchlistdays' ) ); - $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' ); - $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' ); + $prefs['days'] = floatval( $wgUser->getOption( 'watchlistdays' ) ); $prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' ); + $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' ); + $prefs['hideanons'] = $wgUser->getBoolOption( 'watchlisthideanon' ); + $prefs['hideliu'] = $wgUser->getBoolOption( 'watchlisthideliu' ); + $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' ); + $prefs['hidepatrolled' ] = $wgUser->getBoolOption( 'watchlisthidepatrolled' ); # Get query variables - $days = $wgRequest->getVal( 'days', $prefs['days'] ); - $hideOwn = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] ); - $hideBots = $wgRequest->getBool( 'hideBots', $prefs['hidebots'] ); + $days = $wgRequest->getVal( 'days' , $prefs['days'] ); $hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] ); + $hideBots = $wgRequest->getBool( 'hideBots' , $prefs['hidebots'] ); + $hideAnons = $wgRequest->getBool( 'hideAnons', $prefs['hideanons'] ); + $hideLiu = $wgRequest->getBool( 'hideLiu' , $prefs['hideliu'] ); + $hideOwn = $wgRequest->getBool( 'hideOwn' , $prefs['hideown'] ); + $hidePatrolled = $wgRequest->getBool( 'hidePatrolled' , $prefs['hidepatrolled'] ); # Get namespace value, if supplied, and prepare a WHERE fragment $nameSpace = $wgRequest->getIntOrNull( 'namespace' ); + $invert = $wgRequest->getIntOrNull( 'invert' ); if( !is_null( $nameSpace ) ) { $nameSpace = intval( $nameSpace ); - $nameSpaceClause = " AND rc_namespace = $nameSpace"; + if( $invert && $nameSpace !== 'all' ) + $nameSpaceClause = "rc_namespace != $nameSpace"; + else + $nameSpaceClause = "rc_namespace = $nameSpace"; } else { $nameSpace = ''; $nameSpaceClause = ''; @@ -103,32 +119,24 @@ function wfSpecialWatchlist( $par ) { // Dump everything here $nondefaults = array(); - wfAppendToArrayIfNotDefault('days' , $days , $defaults, $nondefaults); - wfAppendToArrayIfNotDefault('hideOwn' , (int)$hideOwn , $defaults, $nondefaults); - wfAppendToArrayIfNotDefault('hideBots' , (int)$hideBots, $defaults, $nondefaults); + wfAppendToArrayIfNotDefault( 'days' , $days , $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults ); - wfAppendToArrayIfNotDefault('namespace', $nameSpace , $defaults, $nondefaults); - - $hookSql = ""; - if( ! wfRunHooks('BeforeWatchlist', array($nondefaults, $wgUser, &$hookSql)) ) { - return; - } - - if($nitems == 0) { + wfAppendToArrayIfNotDefault( 'hideBots' , (int)$hideBots , $defaults, $nondefaults); + wfAppendToArrayIfNotDefault( 'hideAnons', (int)$hideAnons, $defaults, $nondefaults ); + wfAppendToArrayIfNotDefault( 'hideLiu' , (int)$hideLiu , $defaults, $nondefaults ); + wfAppendToArrayIfNotDefault( 'hideOwn' , (int)$hideOwn , $defaults, $nondefaults); + wfAppendToArrayIfNotDefault( 'namespace', $nameSpace , $defaults, $nondefaults); + wfAppendToArrayIfNotDefault( 'hidePatrolled', (int)$hidePatrolled, $defaults, $nondefaults ); + + if( $nitems == 0 ) { $wgOut->addWikiMsg( 'nowatchlist' ); return; } - if ( $days <= 0 ) { + if( $days <= 0 ) { $andcutoff = ''; } else { - $andcutoff = "AND rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'"; - /* - $sql = "SELECT COUNT(*) AS n FROM $page, $revision WHERE rev_timestamp>'$cutoff' AND page_id=rev_page"; - $res = $dbr->query( $sql, $fname ); - $s = $dbr->fetchObject( $res ); - $npages = $s->n; - */ + $andcutoff = "rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'"; } # If the watchlist is relatively short, it's simplest to zip @@ -140,128 +148,158 @@ function wfSpecialWatchlist( $par ) { # Up estimate of watched items by 15% to compensate for talk pages... # Toggles - $andHideOwn = $hideOwn ? "AND (rc_user <> $uid)" : ''; - $andHideBots = $hideBots ? "AND (rc_bot = 0)" : ''; - $andHideMinor = $hideMinor ? 'AND rc_minor = 0' : ''; - - # Show watchlist header - $header = ''; - if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) { - $header .= wfMsg( 'wlheader-enotif' ) . "\n"; - } - if ( $wgShowUpdatedMarker ) { - $header .= wfMsg( 'wlheader-showupdated' ) . "\n"; - } - - # Toggle watchlist content (all recent edits or just the latest) + $andHideOwn = $hideOwn ? "rc_user != $uid" : ''; + $andHideBots = $hideBots ? "rc_bot = 0" : ''; + $andHideMinor = $hideMinor ? "rc_minor = 0" : ''; + $andHideLiu = $hideLiu ? "rc_user = 0" : ''; + $andHideAnons = $hideAnons ? "rc_user != 0" : ''; + $andHidePatrolled = $wgUser->useRCPatrol() && $hidePatrolled ? "rc_patrolled != 1" : ''; + + # Toggle watchlist content (all recent edits or just the latest) if( $wgUser->getOption( 'extendwatchlist' )) { $andLatest=''; - $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) ); + $limitWatchlist = intval( $wgUser->getOption( 'wllimit' ) ); } else { # Top log Ids for a page are not stored - $andLatest = 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') '; - $limitWatchlist = ''; + $andLatest = 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG; + $limitWatchlist = 0; } - $header .= wfMsgExt( 'watchlist-details', array( 'parsemag' ), $wgLang->formatNum( $nitems ) ); - $wgOut->addWikiText( $header ); - # Show a message about slave lag, if applicable if( ( $lag = $dbr->getLag() ) > 0 ) $wgOut->showLagWarning( $lag ); - if ( $wgShowUpdatedMarker ) { - $wgOut->addHTML( '<form action="' . - $specialTitle->escapeLocalUrl() . - '" method="post"><input type="submit" name="dummy" value="' . - htmlspecialchars( wfMsg( 'enotif_reset' ) ) . - '" /><input type="hidden" name="reset" value="all" /></form>' . - "\n\n" ); + # Create output form + $form = Xml::fieldset( wfMsg( 'watchlist-options' ), false, array( 'id' => 'mw-watchlist-options' ) ); + + # Show watchlist header + $form .= wfMsgExt( 'watchlist-details', array( 'parseinline' ), $wgLang->formatNum( $nitems ) ); + + if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) { + $form .= wfMsgExt( 'wlheader-enotif', 'parse' ) . "\n"; } - if ( $wgShowUpdatedMarker ) { - $wltsfield = ", ${watchlist}.wl_notificationtimestamp "; - } else { - $wltsfield = ''; + if( $wgShowUpdatedMarker ) { + $form .= Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $specialTitle->getLocalUrl(), + 'id' => 'mw-watchlist-resetbutton' ) ) . + wfMsgExt( 'wlheader-showupdated', array( 'parseinline' ) ) . ' ' . + Xml::submitButton( wfMsg( 'enotif_reset' ), array( 'name' => 'dummy' ) ) . + Xml::hidden( 'reset', 'all' ) . + Xml::closeElement( 'form' ); + } + $form .= '<hr />'; + + $tables = array( 'recentchanges', 'watchlist', 'page' ); + $fields = array( "{$recentchanges}.*" ); + $conds = array(); + $join_conds = array( + 'watchlist' => array('INNER JOIN',"wl_user='{$uid}' AND wl_namespace=rc_namespace AND wl_title=rc_title"), + 'page' => array('LEFT JOIN','rc_cur_id=page_id') + ); + $options = array( 'ORDER BY' => 'rc_timestamp DESC' ); + if( $wgShowUpdatedMarker ) { + $fields[] = 'wl_notificationtimestamp'; + } + if( $limitWatchlist ) { + $options['LIMIT'] = $limitWatchlist; } - $sql = "SELECT ${recentchanges}.* ${wltsfield} - FROM $watchlist,$recentchanges - LEFT JOIN $page ON rc_cur_id=page_id - WHERE wl_user=$uid - AND wl_namespace=rc_namespace - AND wl_title=rc_title - $andcutoff - $andLatest - $andHideOwn - $andHideBots - $andHideMinor - $nameSpaceClause - $hookSql - ORDER BY rc_timestamp DESC - $limitWatchlist"; - - $res = $dbr->query( $sql, $fname ); + if( $andcutoff ) $conds[] = $andcutoff; + if( $andLatest ) $conds[] = $andLatest; + if( $andHideOwn ) $conds[] = $andHideOwn; + if( $andHideBots ) $conds[] = $andHideBots; + if( $andHideMinor ) $conds[] = $andHideMinor; + if( $andHideLiu ) $conds[] = $andHideLiu; + if( $andHideAnons ) $conds[] = $andHideAnons; + if( $andHidePatrolled ) $conds[] = $andHidePatrolled; + if( $nameSpaceClause ) $conds[] = $nameSpaceClause; + + wfRunHooks('SpecialWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) ); + + $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds ); $numRows = $dbr->numRows( $res ); /* Start bottom header */ - $wgOut->addHTML( "<hr />\n" ); - if($days >= 1) { - $wgOut->addHTML( - wfMsgExt( 'rcnote', 'parseinline', + $wlInfo = ''; + if( $days >= 1 ) { + $wlInfo = wfMsgExt( 'rcnote', 'parseinline', $wgLang->formatNum( $numRows ), $wgLang->formatNum( $days ), $wgLang->timeAndDate( wfTimestampNow(), true ), $wgLang->date( wfTimestampNow(), true ), $wgLang->time( wfTimestampNow(), true ) - ) . '<br />' - ); - } elseif($days > 0) { - $wgOut->addHtml( - wfMsgExt( 'wlnote', 'parseinline', + ) . '<br />'; + } elseif( $days > 0 ) { + $wlInfo = wfMsgExt( 'wlnote', 'parseinline', $wgLang->formatNum( $numRows ), $wgLang->formatNum( round($days*24) ) - ) . '<br />' - ); + ) . '<br />'; } - $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" ); + $cutofflinks = "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n"; # Spit out some control panel links $thisTitle = SpecialPage::getTitleFor( 'Watchlist' ); $skin = $wgUser->getSkin(); + $showLinktext = wfMsgHtml( 'show' ); + $hideLinktext = wfMsgHtml( 'hide' ); + # Hide/show minor edits + $label = $hideMinor ? $showLinktext : $hideLinktext; + $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults ); + $links[] = wfMsgHtml( 'rcshowhideminor', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); + # Hide/show bot edits - $label = $hideBots ? wfMsgHtml( 'watchlist-show-bots' ) : wfMsgHtml( 'watchlist-hide-bots' ); + $label = $hideBots ? $showLinktext : $hideLinktext; $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults ); - $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ); + $links[] = wfMsgHtml( 'rcshowhidebots', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); + + # Hide/show anonymous edits + $label = $hideAnons ? $showLinktext : $hideLinktext; + $linkBits = wfArrayToCGI( array( 'hideAnons' => 1 - (int)$hideAnons ), $nondefaults ); + $links[] = wfMsgHtml( 'rcshowhideanons', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); + + # Hide/show logged in edits + $label = $hideLiu ? $showLinktext : $hideLinktext; + $linkBits = wfArrayToCGI( array( 'hideLiu' => 1 - (int)$hideLiu ), $nondefaults ); + $links[] = wfMsgHtml( 'rcshowhideliu', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); # Hide/show own edits - $label = $hideOwn ? wfMsgHtml( 'watchlist-show-own' ) : wfMsgHtml( 'watchlist-hide-own' ); + $label = $hideOwn ? $showLinktext : $hideLinktext; $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults ); - $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ); + $links[] = wfMsgHtml( 'rcshowhidemine', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); - # Hide/show minor edits - $label = $hideMinor ? wfMsgHtml( 'watchlist-show-minor' ) : wfMsgHtml( 'watchlist-hide-minor' ); - $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults ); - $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ); - - $wgOut->addHTML( implode( ' | ', $links ) ); + # Hide/show patrolled edits + if( $wgUser->useRCPatrol() ) { + $label = $hidePatrolled ? $showLinktext : $hideLinktext; + $linkBits = wfArrayToCGI( array( 'hidePatrolled' => 1 - (int)$hidePatrolled ), $nondefaults ); + $links[] = wfMsgHtml( 'rcshowhidepatr', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); + } - # Form for namespace filtering - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) ); - $form .= '<p>'; + # Namespace filter and put the whole form together. + $form .= $wlInfo; + $form .= $cutofflinks; + $form .= implode( ' | ', $links ); + $form .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) ); + $form .= '<hr /><p>'; $form .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' '; $form .= Xml::namespaceSelector( $nameSpace, '' ) . ' '; + $form .= Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $invert ) . ' '; $form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>'; $form .= Xml::hidden( 'days', $days ); - if( $hideOwn ) - $form .= Xml::hidden( 'hideOwn', 1 ); - if( $hideBots ) - $form .= Xml::hidden( 'hideBots', 1 ); if( $hideMinor ) $form .= Xml::hidden( 'hideMinor', 1 ); + if( $hideBots ) + $form .= Xml::hidden( 'hideBots', 1 ); + if( $hideAnons ) + $form .= Xml::hidden( 'hideAnons', 1 ); + if( $hideLiu ) + $form .= Xml::hidden( 'hideLiu', 1 ); + if( $hideOwn ) + $form .= Xml::hidden( 'hideOwn', 1 ); $form .= Xml::closeElement( 'form' ); - $wgOut->addHtml( $form ); + $form .= Xml::closeElement( 'fieldset' ); + $wgOut->addHTML( $form ); # If there's nothing to show, stop here if( $numRows == 0 ) { @@ -316,7 +354,6 @@ function wfSpecialWatchlist( $par ) { $dbr->freeResult( $res ); $wgOut->addHTML( $s ); - } function wlHoursLink( $h, $page, $options = array() ) { @@ -370,7 +407,8 @@ function wlCountItems( &$user, $talk = true ) { $dbr = wfGetDB( DB_SLAVE, 'watchlist' ); # Fetch the raw count - $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->mId ), 'wlCountItems' ); + $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', + array( 'wl_user' => $user->mId ), 'wlCountItems' ); $row = $dbr->fetchObject( $res ); $count = $row->count; $dbr->freeResult( $res ); diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php index 3502e33c..d91b4960 100644 --- a/includes/specials/SpecialWhatlinkshere.php +++ b/includes/specials/SpecialWhatlinkshere.php @@ -74,9 +74,7 @@ class WhatLinksHerePage { $this->selfTitle = SpecialPage::getTitleFor( 'Whatlinkshere', $this->target->getPrefixedDBkey() ); $wgOut->setPageTitle( wfMsg( 'whatlinkshere-title', $this->target->getPrefixedText() ) ); - $wgOut->setSubtitle( wfMsgHtml( 'linklistsub' ) ); - - $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n"); + $wgOut->setSubtitle( wfMsg( 'whatlinkshere-backlink', $this->skin->link( $this->target, $this->target->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) ); $this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ), $opts->getValue( 'from' ), $opts->getValue( 'back' ) ); @@ -98,7 +96,7 @@ class WhatLinksHerePage { $hidelinks = $this->opts->getValue( 'hidelinks' ); $hideredirs = $this->opts->getValue( 'hideredirs' ); $hidetrans = $this->opts->getValue( 'hidetrans' ); - $hideimages = $target->getNamespace() != NS_IMAGE || $this->opts->getValue( 'hideimages' ); + $hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' ); $fetchlinks = (!$hidelinks || !$hideredirs); @@ -169,11 +167,13 @@ class WhatLinksHerePage { if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) { if ( 0 == $level ) { $wgOut->addHTML( $this->whatlinkshereForm() ); - $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere'; - $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() ); + // Show filters only if there are links if( $hidelinks || $hidetrans || $hideredirs || $hideimages ) $wgOut->addHTML( $this->getFilterPanel() ); + + $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere'; + $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() ); } return; } @@ -256,7 +256,7 @@ class WhatLinksHerePage { } protected function listStart() { - return Xml::openElement( 'ul' ); + return Xml::openElement( 'ul', array ( 'id' => 'mw-whatlinkshere-list' ) ); } protected function listItem( $row, $nt, $notClose = false ) { @@ -267,7 +267,7 @@ class WhatLinksHerePage { 'whatlinkshere-links', 'isimage' ); $msgcache = array(); foreach ( $msgs as $msg ) { - $msgcache[$msg] = wfMsgHtml( $msg ); + $msgcache[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); } } @@ -377,6 +377,8 @@ class WhatLinksHerePage { $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . Xml::namespaceSelector( $namespace, '' ); + $f .= ' '; + # Submit $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ); @@ -395,7 +397,7 @@ class WhatLinksHerePage { $links = array(); $types = array( 'hidetrans', 'hidelinks', 'hideredirs' ); - if( $this->target->getNamespace() == NS_IMAGE ) + if( $this->target->getNamespace() == NS_FILE ) $types[] = 'hideimages'; foreach( $types as $type ) { $chosen = $this->opts->getValue( $type ); diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php index 75a7e95a..5f7e93c7 100644 --- a/includes/templates/NoLocalSettings.php +++ b/includes/templates/NoLocalSettings.php @@ -10,12 +10,31 @@ if ( isset( $wgVersion ) ) { } else { $wgVersion = 'VERSION'; } -# Set the path in case we hit a page such as /index.php/Main_Page -# Could use <base href> but then we have to worry about http[s]/port #/etc. -$ext = strpos( $_SERVER['SCRIPT_NAME'], 'index.php5' ) === false ? 'php' : 'php5'; + +$scriptName = $_SERVER['SCRIPT_NAME']; +$ext = substr( $scriptName, strrpos( $scriptName, "." ) + 1 ); $path = ''; -if( isset( $_SERVER['SCRIPT_NAME'] )) { - $path = htmlspecialchars( preg_replace('/index.php5?/', '', $_SERVER['SCRIPT_NAME']) ); +# Add any directories in the main folder that could contain an entrypoint (even possibly). +# We cannot just do a dir listing here, as we do not know where it is yet +# These must not also be the names of subfolders that may contain an entrypoint +$topdirs = array( 'extensions', 'includes' ); +foreach( $topdirs as $dir ){ + # Check whether a directory by this name is in the path + if( strrpos( $scriptName, "/" . $dir . "/" ) ){ + # If so, check whether it is the right folder + # First, get the number of directories up it is (to generate path) + $numToGoUp = substr_count( substr( $scriptName, strrpos( $scriptName, "/" . $dir . "/" ) + 1 ), "/" ); + # And generate the path using ..'s + for( $i = 0; $i < $numToGoUp; $i++ ){ + $realPath = "../" . $realPath; + } + # Checking existance (using the image here as it is something not likely to change, and to always be here) + if( file_exists( $realPath . "skins/common/images/mediawiki.png" ) ) { + # If so, get the path that we can use in this file, and stop looking + $path = substr( $scriptName, 0, strrpos( $scriptName, "/" . $dir . "/" ) + 1 ); + break; + } + } } ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> diff --git a/includes/templates/PHP4.php b/includes/templates/PHP4.php new file mode 100644 index 00000000..058351a0 --- /dev/null +++ b/includes/templates/PHP4.php @@ -0,0 +1,100 @@ +<?php +/** + * @file + * @ingroup Templates + */ + +if( !defined( 'MW_PHP4' ) ) { + die( "Not an entry point."); +} + +if( isset( $_SERVER['SCRIPT_NAME'] ) ) { + // Probably IIS; doesn't set REQUEST_URI + $scriptUrl = $_SERVER['SCRIPT_NAME']; +} elseif( isset( $_SERVER['REQUEST_URI'] ) ) { + // We're trying SCRIPT_NAME first because it won't include PATH_INFO... hopefully + $scriptUrl = $_SERVER['REQUEST_URI']; +} else { + $scriptUrl = ''; +} +if ( preg_match( '!^(.*)/config/[^/]*.php$!', $scriptUrl, $m ) ) { + $baseUrl = $m[1]; +} elseif ( preg_match( '!^(.*)/[^/]*.php$!', $scriptUrl, $m ) ) { + $baseUrl = $m[1]; +} else { + $baseUrl = dirname( $scriptUrl ); +} + +?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'> + <head> + <title>MediaWiki <?php echo htmlspecialchars( $wgVersion ); ?></title> + <meta http-equiv='Content-Type' content='text/html; charset=utf-8' /> + <style type='text/css' media='screen, projection'> + html, body { + color: #000; + background-color: #fff; + font-family: sans-serif; + text-align: center; + } + + p { + text-align: left; + margin-left: 2em; + margin-right: 2em; + } + + h1 { + font-size: 150%; + } + </style> + </head> + <body> + <img src="<?php echo htmlspecialchars( $baseUrl ) ?>/skins/common/images/mediawiki.png" alt='The MediaWiki logo' /> + + <h1>MediaWiki <?php echo htmlspecialchars( $wgVersion ); ?></h1> + <div class='error'> +<p> + MediaWiki requires PHP 5.0.0 or higher. You are running PHP + <?php echo htmlspecialchars( phpversion() ); ?>. +</p> +<?php +flush(); +/** + * Test the *.php5 extension + */ +$downloadOther = true; +if ( $baseUrl ) { + $testUrl = "$wgServer$baseUrl/php5.php5"; + if( function_exists( 'file_get_contents' ) ) { + $errorLevel = error_reporting(); + error_reporting( $errorLevel & !E_WARNING ); + + ini_set( 'allow_url_fopen', '1' ); + $s = file_get_contents( $testUrl ); + + error_reporting( $errorLevel ); + } + + if ( strpos( $s, 'yes' ) !== false ) { + $encUrl = htmlspecialchars( str_replace( '.php', '.php5', $scriptUrl ) ); + echo "<p>You may be able to use MediaWiki using a <a href=\"$encUrl\">.php5</a> file extension.</p>"; + $downloadOther = false; + } +} +if ( $downloadOther ) { +?> +<p>Please consider +<a href="http://www.php.net/downloads.php">upgrading your copy of PHP</a>. +PHP 4 is at the end of its lifecycle and will not receive further security updates.</p> +<p>If for some reason you really really need to run MediaWiki on PHP 4, you will need to +<a href="http://www.mediawiki.org/wiki/Download">download version 1.6.x</a> +from our website. </p> +<?php +} +?> + + </div> + </body> +</html> diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index deeeb274..c4a60b6c 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -16,7 +16,7 @@ class UserloginTemplate extends QuickTemplate { ?> <div class="<?php $this->text('messagetype') ?>box"> <?php if ( $this->data['messagetype'] == 'error' ) { ?> - <h2><?php $this->msg('loginerror') ?>:</h2> + <h2><?php $this->msg('loginerror') ?></h2> <?php } ?> <?php $this->html('message') ?> </div> @@ -54,7 +54,7 @@ class UserloginTemplate extends QuickTemplate { $doms .= "<option>" . htmlspecialchars( $dom ) . "</option>"; } ?> - <tr> + <tr id="mw-user-domain-section"> <td class="mw-label"><?php $this->msg( 'yourdomainname' ) ?></td> <td class="mw-input"> <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>" @@ -63,7 +63,8 @@ class UserloginTemplate extends QuickTemplate { </select> </td> </tr> - <?php } ?> + <?php } + if( $this->data['canremember'] ) { ?> <tr> <td></td> <td class="mw-input"> @@ -74,6 +75,7 @@ class UserloginTemplate extends QuickTemplate { /> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label> </td> </tr> + <?php } ?> <tr> <td></td> <td class="mw-submit"> @@ -111,7 +113,7 @@ class UsercreateTemplate extends QuickTemplate { ?> <div class="<?php $this->text('messagetype') ?>box"> <?php if ( $this->data['messagetype'] == 'error' ) { ?> - <h2><?php $this->msg('loginerror') ?>:</h2> + <h2><?php $this->msg('loginerror') ?></h2> <?php } ?> <?php $this->html('message') ?> </div> @@ -196,6 +198,7 @@ class UsercreateTemplate extends QuickTemplate { </td> <?php } ?> </tr> + <?php if( $this->data['canremember'] ) { ?> <tr> <td></td> <td class="mw-input"> @@ -206,7 +209,8 @@ class UsercreateTemplate extends QuickTemplate { /> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label> </td> </tr> -<?php +<?php } + $tabIndex = 8; if ( isset( $this->data['extraInput'] ) && is_array( $this->data['extraInput'] ) ) { foreach ( $this->data['extraInput'] as $inputItem ) { ?> diff --git a/includes/zhtable/simpphrases.manual b/includes/zhtable/simpphrases.manual index 8e754b7f..60d0861c 100644 --- a/includes/zhtable/simpphrases.manual +++ b/includes/zhtable/simpphrases.manual @@ -16,13 +16,13 @@ 乾红 乾乾 乾清宫 -乾象; -乾宅; -乾造; -乾曜; -乾元; -乾卦; -李乾德; +乾象 +乾宅 +乾造 +乾曜 +乾元 +乾卦 +李乾德 挨着 爱着 暗着 @@ -374,6 +374,10 @@ 写著作 写著名 遇着 +杀着 +杀著名 +杀著作 +杀著者 於乎 於戏 魏徵 diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual index bc2222f4..feeca9dc 100644 --- a/includes/zhtable/toCN.manual +++ b/includes/zhtable/toCN.manual @@ -5,6 +5,7 @@ 記憶體 内存 預設 默认 串列 串行 +串列加速器 串列加速器 乙太網 以太网 點陣圖 位图 常式 例程 @@ -109,150 +110,92 @@ 簡訊 短信 烏茲別克 乌兹别克斯坦 查德 乍得 -乍得 乍得 -也門 葉門 也门 伯利茲 伯利兹 貝里斯 伯利兹 維德角 佛得角 -佛得角 佛得角 -克羅地亞 克罗地亚 克羅埃西亞 克罗地亚 -岡比亞 冈比亚 甘比亞 冈比亚 -幾內亞比紹 几内亚比绍 幾內亞比索 几内亚比绍 列支敦斯登 列支敦士登 -列支敦士登 列支敦士登 -利比里亞 利比里亚 賴比瑞亞 利比里亚 -加納 加纳 迦納 加纳 加彭 加蓬 -加蓬 加蓬 -博茨瓦納 博茨瓦纳 波札那 博茨瓦纳 -卡塔爾 卡塔尔 卡達 卡塔尔 -盧旺達 卢旺达 盧安達 卢旺达 -危地馬拉 危地马拉 瓜地馬拉 危地马拉 厄瓜多爾 厄瓜多尔 +厄瓜多尔 厄瓜多尔 厄瓜多 厄瓜多尔 -厄立特里亞 厄立特里亚 厄利垂亞 厄立特里亚 -吉布堤 吉布提 吉布地 吉布提 哈薩克 哈萨克斯坦 -哥斯達黎加 哥斯达黎加 哥斯大黎加 哥斯达黎加 -圖瓦盧 图瓦卢 吐瓦魯 图瓦卢 土庫曼 土库曼斯坦 -聖盧西亞 圣卢西亚 聖露西亞 圣卢西亚 聖吉斯納域斯 圣基茨和尼维斯 聖克里斯多福及尼維斯 圣基茨和尼维斯 -聖文森特和格林納丁斯 圣文森特和格林纳丁斯 聖文森及格瑞那丁 圣文森特和格林纳丁斯 -聖馬力諾 圣马力诺 聖馬利諾 圣马力诺 -圭亞那 圭亚那 蓋亞那 圭亚那 -坦桑尼亞 坦桑尼亚 坦尚尼亞 坦桑尼亚 -埃塞俄比亞 埃塞俄比亚 衣索匹亞 埃塞俄比亚 衣索比亞 埃塞俄比亚 吉里巴斯 基里巴斯 -基里巴斯 基里巴斯 塔吉克 塔吉克斯坦 塞拉利昂 塞拉利昂 塞普勒斯 塞浦路斯 -塞浦路斯 塞浦路斯 -塞舌爾 塞舌尔 塞席爾 塞舌尔 -多明尼加共和國 多米尼加 -多明尼加 多米尼加 -多明尼加聯邦 多米尼加联邦 -多米尼克 多米尼加联邦 -安提瓜和巴布達 安提瓜和巴布达 +多米尼克 多米尼加国 安地卡及巴布達 安提瓜和巴布达 尼日利亞 尼日利亚 +尼日利亚 尼日利亚 奈及利亞 尼日利亚 尼日爾 尼日尔 +尼日尔 尼日尔 尼日 尼日尔 巴貝多 巴巴多斯 -巴巴多斯 巴巴多斯 -巴布亞新畿內亞 巴布亚新几内亚 巴布亞紐幾內亞 巴布亚新几内亚 布基納法索 布基纳法索 布吉納法索 布基纳法索 蒲隆地 布隆迪 -布隆迪 布隆迪 -希臘 希腊 帛琉 帕劳 義大利 意大利 -意大利 意大利 -所羅門群島 所罗门群岛 索羅門群島 所罗门群岛 汶萊 文莱 -斯威士蘭 斯威士兰 史瓦濟蘭 斯威士兰 -斯洛文尼亞 斯洛文尼亚 斯洛維尼亞 斯洛文尼亚 -新西蘭 新西兰 紐西蘭 新西兰 -格林納達 格林纳达 格瑞那達 格林纳达 -格魯吉亞 乔治亚 -喬治亞 乔治亚 -梵蒂岡 梵蒂冈 -毛里塔尼亞 毛里塔尼亚 茅利塔尼亞 毛里塔尼亚 毛里裘斯 毛里求斯 模里西斯 毛里求斯 沙地阿拉伯 沙特阿拉伯 沙烏地阿拉伯 沙特阿拉伯 -波斯尼亞黑塞哥維那 波斯尼亚和黑塞哥维那 波士尼亞赫塞哥維納 波斯尼亚和黑塞哥维那 -津巴布韋 津巴布韦 辛巴威 津巴布韦 宏都拉斯 洪都拉斯 -洪都拉斯 洪都拉斯 -特立尼達和多巴哥 特立尼达和托巴哥 千里達托貝哥 特立尼达和托巴哥 -瑙魯 瑙鲁 諾魯 瑙鲁 -瓦努阿圖 瓦努阿图 萬那杜 瓦努阿图 溫納圖 瓦努阿图 -科摩羅 科摩罗 葛摩 科摩罗 象牙海岸 科特迪瓦 突尼西亞 突尼斯 -索馬里 索马里 索馬利亞 索马里 -老撾 老挝 寮國 老挝 肯雅 肯尼亚 肯亞 肯尼亚 蘇利南 苏里南 莫三比克 莫桑比克 -莫桑比克 莫桑比克 -萊索托 莱索托 賴索托 莱索托 -貝寧 贝宁 貝南 贝宁 -贊比亞 赞比亚 尚比亞 赞比亚 亞塞拜然 阿塞拜疆 -阿塞拜疆 阿塞拜疆 -阿拉伯聯合酋長國 阿拉伯联合酋长国 阿拉伯聯合大公國 阿拉伯联合酋长国 南韓 韩国 -馬爾代夫 马尔代夫 馬爾地夫 马尔代夫 馬爾他 马耳他 馬利共和國 马里共和国 @@ -262,7 +205,7 @@ 泡麵 方便面 笨豬跳 蹦极跳 绑紧跳 蹦极跳 -冷盤 凉菜 +冷盤 凉菜 冷菜 凉菜 散钱 零钱 谐星 笑星 @@ -290,16 +233,12 @@ 積架 捷豹 福斯 大众 福士 大众 -雪鐵龍 雪铁龙 萬事得 马自达 -馬自達 马自达 寶獅 标志 拿破崙 拿破仑 布殊 布什 布希 布什 柯林頓 克林顿 -克林頓 克林顿 -薩達姆 萨达姆 海珊 萨达姆 梵谷 凡高 大衛碧咸 大卫·贝克汉姆 diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual index 9afddb77..916b4020 100644 --- a/includes/zhtable/toHK.manual +++ b/includes/zhtable/toHK.manual @@ -6,25 +6,17 @@ 凶殘 兇殘 緝凶 緝兇 買凶 買兇 -打印机 打印機 印表機 打印機 字节 位元組 字節 位元組 -打印 打印 列印 打印 硬件 硬件 硬體 硬件 -二极管 二極管 二極體 二極管 -三极管 三極管 三極體 三極管 -数码 數碼 數位 數碼 -软件 軟件 軟體 軟件 -网络 網絡 網路 網絡 -人工智能 人工智能 人工智慧 人工智能 航天飞机 穿梭機 太空梭 穿梭機 @@ -34,141 +26,85 @@ 機器人 機械人 移动电话 流動電話 行動電話 流動電話 -调制解调器 調制解調器 數據機 調制解調器 短信 短訊 簡訊 短訊 -乍得 乍得 查德 乍得 -也门 也門 葉門 也門 -伯利兹 伯利茲 貝里斯 伯利茲 -佛得角 佛得角 維德角 佛得角 -克罗地亚 克羅地亞 克羅埃西亞 克羅地亞 -冈比亚 岡比亞 甘比亞 岡比亞 -几内亚比绍 幾內亞比紹 幾內亞比索 幾內亞比紹 -列支敦士登 列支敦士登 列支敦斯登 列支敦士登 -利比里亚 利比里亞 賴比瑞亞 利比里亞 -加纳 加納 迦納 加納 -加蓬 加蓬 加彭 加蓬 -博茨瓦纳 博茨瓦納 波札那 博茨瓦納 -卡塔尔 卡塔爾 卡達 卡塔爾 -卢旺达 盧旺達 盧安達 盧旺達 -危地马拉 危地馬拉 瓜地馬拉 危地馬拉 厄瓜多尔 厄瓜多爾 +厄瓜多爾 厄瓜多爾 厄瓜多 厄瓜多爾 -厄立特里亚 厄立特里亞 厄利垂亞 厄立特里亞 -吉布提 吉布堤 吉布地 吉布堤 -哥斯达黎加 哥斯達黎加 哥斯大黎加 哥斯達黎加 -图瓦卢 圖瓦盧 吐瓦魯 圖瓦盧 -圣卢西亚 聖盧西亞 聖露西亞 聖盧西亞 圣基茨和尼维斯 聖吉斯納域斯 聖克里斯多福及尼維斯 聖吉斯納域斯 -圣文森特和格林纳丁斯 聖文森特和格林納丁斯 聖文森及格瑞那丁 聖文森特和格林納丁斯 -圣马力诺 聖馬力諾 聖馬利諾 聖馬力諾 -圭亚那 圭亞那 蓋亞那 圭亞那 -坦桑尼亚 坦桑尼亞 坦尚尼亞 坦桑尼亞 -埃塞俄比亚 埃塞俄比亞 衣索匹亞 埃塞俄比亞 衣索比亞 埃塞俄比亞 -基里巴斯 基里巴斯 吉里巴斯 基里巴斯 -狮子山 獅子山 塞普勒斯 塞浦路斯 -塞舌尔 塞舌爾 塞席爾 塞舌爾 -多米尼加 多明尼加共和國 -多明尼加 多明尼加共和國 -多米尼加联邦 多明尼加聯邦 -多米尼克 多明尼加聯邦 -安提瓜和巴布达 安提瓜和巴布達 +多米尼克 多明尼加國 安地卡及巴布達 安提瓜和巴布達 尼日利亚 尼日利亞 +尼日利亞 尼日利亞 奈及利亞 尼日利亞 尼日尔 尼日爾 +尼日爾 尼日爾 尼日 尼日爾 -巴巴多斯 巴巴多斯 巴貝多 巴巴多斯 -巴布亚新几内亚 巴布亞新畿內亞 巴布亞紐幾內亞 巴布亞新畿內亞 -布基纳法索 布基納法索 布吉納法索 布基納法索 -布隆迪 布隆迪 蒲隆地 布隆迪 +帕劳 帛琉 義大利 意大利 -所罗门群岛 所羅門群島 索羅門群島 所羅門群島 -斯威士兰 斯威士蘭 +文莱 汶萊 史瓦濟蘭 斯威士蘭 -斯洛文尼亚 斯洛文尼亞 斯洛維尼亞 斯洛文尼亞 -新西兰 新西蘭 紐西蘭 新西蘭 -格林纳达 格林納達 格瑞那達 格林納達 -格鲁吉亚 喬治亞 -格魯吉亞 喬治亞 -梵蒂冈 梵蒂岡 -毛里塔尼亚 毛里塔尼亞 茅利塔尼亞 毛里塔尼亞 毛里求斯 毛里裘斯 模里西斯 毛里裘斯 +沙地阿拉伯 沙特阿拉伯 沙烏地阿拉伯 沙特阿拉伯 -波斯尼亚和黑塞哥维那 波斯尼亞黑塞哥維那 波士尼亞赫塞哥維納 波斯尼亞黑塞哥維那 -津巴布韦 津巴布韋 辛巴威 津巴布韋 -洪都拉斯 洪都拉斯 宏都拉斯 洪都拉斯 -特立尼达和托巴哥 特立尼達和多巴哥 千里達托貝哥 特立尼達和多巴哥 -瑙鲁 瑙魯 諾魯 瑙魯 -瓦努阿图 瓦努阿圖 萬那杜 瓦努阿圖 -科摩罗 科摩羅 葛摩 科摩羅 -索马里 索馬里 索馬利亞 索馬里 -老挝 老撾 寮國 老撾 肯尼亚 肯雅 肯亞 肯雅 -莫桑比克 莫桑比克 莫三比克 莫桑比克 -莱索托 萊索托 賴索托 萊索托 -贝宁 貝寧 貝南 貝寧 -赞比亚 贊比亞 尚比亞 贊比亞 -阿塞拜疆 阿塞拜疆 亞塞拜然 阿塞拜疆 -阿拉伯联合酋长国 阿拉伯聯合酋長國 阿拉伯聯合大公國 阿拉伯聯合酋長國 -马尔代夫 馬爾代夫 馬爾地夫 馬爾代夫 馬利共和國 馬里共和國 方便面 即食麵 @@ -200,11 +136,9 @@ 拿破崙 拿破侖 布什 布殊 布希 布殊 -克林顿 克林頓 柯林頓 克林頓 萨达姆 薩達姆 海珊 侯賽因 -侯赛因 侯賽因 大卫·贝克汉姆 大衛碧咸 迈克尔·欧文 米高奧雲 珍妮弗·卡普里亚蒂 卡佩雅蒂 diff --git a/includes/zhtable/toSG.manual b/includes/zhtable/toSG.manual index 5f7cb0ca..3c0cbc1d 100644 --- a/includes/zhtable/toSG.manual +++ b/includes/zhtable/toSG.manual @@ -5,6 +5,7 @@ 方便面 快速面 速食麵 快速面 即食麵 快速面 +泡麵 快速面 蹦极跳 绑紧跳 笨豬跳 绑紧跳 凉菜 冷菜 @@ -16,4 +17,3 @@ 民乐 华乐 住房 住屋 房价 屋价 -泡麵 快速面 diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual index 13a81a69..b0041ccf 100644 --- a/includes/zhtable/toTW.manual +++ b/includes/zhtable/toTW.manual @@ -21,6 +21,7 @@ 復蘇 復甦 缺省 預設 串行 串列 +串列加速器 串列加速器 以太网 乙太網 位图 點陣圖 例程 常式 @@ -85,7 +86,6 @@ 服务器 伺服器 等于 等於 局域网 區域網 -计算机 電腦 扫瞄仪 掃瞄器 宽带 寬頻 数据库 資料庫 @@ -143,7 +143,6 @@ 伯利兹 貝里斯 伯利茲 貝里斯 佛得角 維德角 -佛得角 維德角 克罗地亚 克羅埃西亞 克羅地亞 克羅埃西亞 冈比亚 甘比亞 @@ -201,10 +200,11 @@ 塞浦路斯 塞普勒斯 塞舌尔 塞席爾 塞舌爾 塞席爾 -多米尼加 多明尼加 +多米尼加共和国 多明尼加 +多米尼加共和國 多明尼加 多明尼加共和國 多明尼加 -多米尼加联邦 多米尼克 -多明尼加聯邦 多米尼克 +多米尼加国 多米尼克 +多明尼加國 多米尼克 安提瓜和巴布达 安地卡及巴布達 安提瓜和巴布達 安地卡及巴布達 尼日利亚 奈及利亞 @@ -212,17 +212,14 @@ 尼日尔 尼日 尼日爾 尼日 巴巴多斯 巴貝多 -巴巴多斯 巴貝多 巴布亚新几内亚 巴布亞紐幾內亞 巴布亞新畿內亞 巴布亞紐幾內亞 布基纳法索 布吉納法索 布基納法索 布吉納法索 布隆迪 蒲隆地 布隆迪 蒲隆地 -希腊 希臘 帕劳 帛琉 意大利 義大利 -意大利 義大利 所罗门群岛 索羅門群島 所羅門群島 索羅門群島 文莱 汶萊 @@ -276,7 +273,6 @@ 赞比亚 尚比亞 贊比亞 尚比亞 阿塞拜疆 亞塞拜然 -阿塞拜疆 亞塞拜然 阿拉伯联合酋长国 阿拉伯聯合大公國 阿拉伯聯合酋長國 阿拉伯聯合大公國 马尔代夫 馬爾地夫 @@ -307,7 +303,6 @@ 積架 捷豹 福士 福斯 雪铁龙 雪鐵龍 -马自达 馬自達 萬事得 馬自達 拿破仑 拿破崙 拿破侖 拿破崙 diff --git a/includes/zhtable/tradphrases.manual b/includes/zhtable/tradphrases.manual index 9caa3cc5..02d07d20 100644 --- a/includes/zhtable/tradphrases.manual +++ b/includes/zhtable/tradphrases.manual @@ -14,6 +14,7 @@ 千隻 萬隻 億隻 +多只是 多隻 0多隻 零多隻 @@ -308,7 +309,6 @@ 豐濱 豐濱鄉 象徵著 -這么著 這麼著 那麼著 配合著 @@ -327,7 +327,6 @@ 披頭散髮 髮禁 鬥著 -鬧著玩儿 鬧著玩兒 鯰魚 世界盃 @@ -996,6 +995,10 @@ 藥籤 萬籤插架 雲笈七籤 +上簽名 +上簽字 +上簽收 +上簽寫 犖确 磽确 确瘠 diff --git a/includes/zhtable/tradphrases_exclude.manual b/includes/zhtable/tradphrases_exclude.manual index 40dc5c09..0db69513 100644 --- a/includes/zhtable/tradphrases_exclude.manual +++ b/includes/zhtable/tradphrases_exclude.manual @@ -72,6 +72,7 @@ 白麵 切麵 和麵 +過水麵 復甦 複蘇 甦醒 @@ -131,6 +132,7 @@ 採邑 嚮日 佔城 +水錶 名錶 錶面 彆腳 |