/**
* Constructor from a page id
*
- * Always override this for all subclasses (until we use PHP with LSB)
- *
* @param $id Int article ID to load
*
* @return WikiPage
*/
public static function newFromID( $id ) {
$t = Title::newFromID( $id );
- # @todo FIXME: Doesn't inherit right
- return $t == null ? null : new self( $t );
- # return $t == null ? null : new static( $t ); // PHP 5.3
+ if ( $t ) {
+ return self::factory( $t );
+ }
+ return null;
}
/**
* (and only when) $wgActions[$action] === true. This allows subclasses
* to override the default behavior.
*
+ * @todo: move this UI stuff somewhere else
+ *
* @return Array
*/
public function getActionOverrides() {
$dbw = wfGetDB( DB_MASTER );
$dbw->replace( 'redirect', array( 'rd_from' ),
array(
- 'rd_from' => $this->getId(),
- 'rd_namespace' => $rt->getNamespace(),
- 'rd_title' => $rt->getDBkey(),
- 'rd_fragment' => $rt->getFragment(),
- 'rd_interwiki' => $rt->getInterwiki(),
+ 'rd_from' => $this->getId(),
+ 'rd_namespace' => $rt->getNamespace(),
+ 'rd_title' => $rt->getDBkey(),
+ 'rd_fragment' => $rt->getFragment(),
+ 'rd_interwiki' => $rt->getInterwiki(),
),
__METHOD__
);
* A DB query result object or...
* "fromdb" to get from a slave DB or...
* "fromdbmaster" to get from the master DB
+ * @return void
*/
public function loadPageData( $data = 'fromdb' ) {
- if ( $data === 'fromdb' || $data === 'fromdbmaster' ) {
- $db = ( $data == 'fromdbmaster' )
- ? wfGetDB( DB_MASTER )
- : wfGetDB( DB_SLAVE );
- $data = $this->pageDataFromTitle( $db, $this->mTitle );
+ if ( $data === 'fromdbmaster' ) {
+ $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
+ } elseif ( $data === 'fromdb' ) { // slave
+ $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle );
+ # Use a "last rev inserted" timestamp key to dimish the issue of slave lag.
+ # Note that DB also stores the master position in the session and checks it.
+ $touched = $this->getCachedLastEditTime();
+ if ( $touched ) { // key set
+ if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) {
+ $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
+ }
+ }
}
$lc = LinkCache::singleton();
if ( $data ) {
- $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect, $data->page_latest );
+ $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
$this->mTitle->loadFromRow( $data );
* @return string MW timestamp of last article revision
*/
public function getTimestamp() {
- // Check if the field has been filled by ParserCache::get()
+ // Check if the field has been filled by WikiPage::setTimestamp()
if ( !$this->mTimestamp ) {
$this->loadLastEdit();
}
&& $user->getStubThreshold() == 0
&& $this->exists()
&& empty( $oldid )
- && !$this->mTitle->isCssOrJsPage()
- && !$this->mTitle->isCssJsSubpage();
+ && $this->mTitle->isWikitextPage();
}
/**
*
* @param $dbw DatabaseBase: object
* @param $revision Revision: For ID number, and text used to set
- length and redirect status fields
+ * 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
$conditions['page_latest'] = $lastRevision;
}
+ $now = wfTimestampNow();
$dbw->update( 'page',
array( /* SET */
'page_latest' => $revision->getId(),
- 'page_touched' => $dbw->timestamp(),
+ 'page_touched' => $dbw->timestamp( $now ),
'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
'page_is_redirect' => $rt !== null ? 1 : 0,
'page_len' => strlen( $text ),
$result = $dbw->affectedRows() != 0;
if ( $result ) {
$this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
+ $this->setCachedLastEditTime( $now );
}
wfProfileOut( __METHOD__ );
return $result;
}
+ /**
+ * Get the cached timestamp for the last time the page changed.
+ * This is only used to help handle slave lag by comparing to page_touched.
+ * @return string MW timestamp
+ */
+ protected function getCachedLastEditTime() {
+ global $wgMemc;
+ $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
+ return $wgMemc->get( $key );
+ }
+
+ /**
+ * Set the cached timestamp for the last time the page changed.
+ * This is only used to help handle slave lag by comparing to page_touched.
+ * @param $timestamp string
+ * @return void
+ */
+ public function setCachedLastEditTime( $timestamp ) {
+ global $wgMemc;
+ $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
+ $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60*15 );
+ }
+
/**
* Add row to the redirect table if this is a redirect, remove otherwise.
*
* 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 an
+ * If EDIT_UPDATE is specified and the article doesn't exist, the function will return 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.
# Provide autosummaries if one is not provided and autosummaries are enabled.
if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
- $summary = $this->getAutosummary( $oldtext, $text, $flags );
+ $summary = self::getAutosummary( $oldtext, $text, $flags );
}
$editInfo = $this->prepareTextForEdit( $text, null, $user );
# Do updates right now unless deferral was requested
if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
- wfDoUpdates();
+ DeferredUpdates::doUpdates();
}
// Return the new revision (or null) to the caller
/**
* Update the article's restriction field, and leave a log entry.
*
+ * @todo: seperate the business/permission stuff out from backend code
+ *
* @param $limit Array: set of restriction keys
* @param $reason String
* @param &$cascade Integer. Set to false if cascading protection isn't allowed.
$encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] );
if ( $restrictions != '' ) {
- $protect_description .= "[$action=$restrictions] (";
+ $protect_description .= $wgContLang->getDirMark() . "[$action=$restrictions] (";
if ( $encodedExpiry[$action] != 'infinity' ) {
$protect_description .= wfMsgForContent( 'protect-expiring',
$wgContLang->timeanddate( $expiry[$action], false, false ) ,
public function doDeleteArticle(
$reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
) {
- global $wgDeferredUpdateList, $wgUseTrackbacks, $wgUser;
+ global $wgUseTrackbacks, $wgEnableInterwikiTemplatesTracking, $wgGlobalDatabase, $wgUser;
$user = is_null( $user ) ? $wgUser : $user;
wfDebug( __METHOD__ . "\n" );
return false;
}
- $u = new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 );
- array_push( $wgDeferredUpdateList, $u );
+ DeferredUpdates::addUpdate(
+ new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 )
+ );
// Bitfields to further suppress the content
if ( $suppress ) {
'ar_timestamp' => 'rev_timestamp',
'ar_minor_edit' => 'rev_minor_edit',
'ar_rev_id' => 'rev_id',
+ 'ar_parent_id' => 'rev_parent_id',
'ar_text_id' => 'rev_text_id',
'ar_text' => '\'\'', // Be explicit to appease
'ar_flags' => '\'\'', // MySQL's "strict mode"...
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
- $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) );
- $dbw->delete( 'imagelinks', array( 'il_from' => $id ) );
- $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) );
- $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) );
- $dbw->delete( 'externallinks', array( 'el_from' => $id ) );
- $dbw->delete( 'langlinks', array( 'll_from' => $id ) );
- $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ) );
- $dbw->delete( 'redirect', array( 'rd_from' => $id ) );
+ $dbw->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
+ $dbw->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
+ $dbw->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
+ $dbw->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
+ $dbw->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
+ $dbw->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
+ $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
+ $dbw->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
+ $dbw->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
+
+ if ( $wgEnableInterwikiTemplatesTracking && $wgGlobalDatabase ) {
+ $dbw2 = wfGetDB( DB_MASTER, array(), $wgGlobalDatabase );
+ $dbw2->delete( 'globaltemplatelinks',
+ array( 'gtl_from_wiki' => wfGetID(),
+ 'gtl_from_page' => $id )
+ );
+ }
}
# If using cleanup triggers, we can skip some manual deletes
# Log the deletion, if the page was suppressed, log it at Oversight instead
$logtype = $suppress ? 'suppress' : 'delete';
- $log = new LogPage( $logtype );
- # Make sure logging got through
- $log->addEntry( 'delete', $this->mTitle, $reason, array() );
+ $logEntry = new ManualLogEntry( $logtype, 'delete' );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $this->mTitle );
+ $logEntry->setComment( $reason );
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
if ( $commit ) {
$dbw->commit();
* and return value documentation
*
* NOTE: This function does NOT check ANY permissions, it just commits the
- * rollback to the DB Therefore, you should only call this function direct-
+ * rollback to the DB. Therefore, you should only call this function direct-
* ly if you want to use custom permissions checks. If you don't, use
* doRollback() instead.
* @param $fromP String: Name of the user whose edits to rollback.
* @param $user User The relevant user
*/
public function doViewUpdates( User $user ) {
- global $wgDeferredUpdateList, $wgDisableCounters;
+ global $wgDisableCounters;
if ( wfReadOnly() ) {
return;
}
# Don't update page view counters on views from bot users (bug 14044)
if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->getId() ) {
- $wgDeferredUpdateList[] = new ViewCountUpdate( $this->getId() );
- $wgDeferredUpdateList[] = new SiteStatsUpdate( 1, 0, 0 );
+ DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) );
+ DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) );
}
# Update newtalk / watchlist notification status
$edit->revid = $revid;
$edit->newText = $text;
$edit->pst = $this->preSaveTransform( $text, $user, $popts );
- $edit->popts = $this->getParserOptions( true );
+ $edit->popts = $this->makeParserOptions( new User );
$edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
$edit->oldText = $this->getRawText();
* - null: don't change the article count
*/
public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
- global $wgDeferredUpdateList, $wgEnableParserCache;
+ global $wgEnableParserCache;
wfProfileIn( __METHOD__ );
$total = 0;
}
- $wgDeferredUpdateList[] = new SiteStatsUpdate( 0, 1, $good, $total );
- $wgDeferredUpdateList[] = new SearchUpdate( $id, $title, $text );
+ DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
+ DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
# If this is another user's talk page, update newtalk.
# Don't do this if $options['changed'] = false (null-edits) nor if
$title->touchLinks();
$title->purgeSquid();
$title->deleteTitleProtection();
+
+ # Invalidate caches of distant articles which transclude this page
+ DeferredUpdates::addHTMLCacheUpdate( $title, 'globaltemplatelinks' );
}
/**
# Image redirects
RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
+
+ # Invalidate caches of distant articles which transclude this page
+ DeferredUpdates::addHTMLCacheUpdate( $title, 'globaltemplatelinks' );
}
/**
* @todo: verify that $title is always a Title object (and never false or null), add Title hint to parameter $title
*/
public static function onArticleEdit( $title ) {
- global $wgDeferredUpdateList;
-
// Invalidate caches of articles which include this page
- $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' );
+ DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' );
+
+ // Invalidate caches of distant articles which transclude this page
+ DeferredUpdates::addHTMLCacheUpdate( $title, 'globaltemplatelinks' );
// Invalidate the caches of all pages which redirect here
- $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' );
+ DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' );
# Purge squid for this page only
$title->purgeSquid();
return $result;
}
+ /**
+ * Return a list of distant templates used by this article.
+ * Uses the globaltemplatelinks table
+ *
+ * @return Array of Title objects
+ */
+ public function getUsedDistantTemplates() {
+ global $wgGlobalDatabase;
+
+ $result = array();
+
+ if ( $wgGlobalDatabase ) {
+ $id = $this->mTitle->getArticleID();
+
+ if ( $id == 0 ) {
+ return array();
+ }
+
+ $dbr = wfGetDB( DB_SLAVE, array(), $wgGlobalDatabase );
+ $res = $dbr->select( 'globaltemplatelinks',
+ array( 'gtl_to_prefix', 'gtl_to_namespace', 'gtl_to_title' ),
+ array( 'gtl_from_wiki' => wfWikiID( ), 'gtl_from_page' => $id ),
+ __METHOD__ );
+
+ if ( $res !== false ) {
+ foreach ( $res as $row ) {
+ $result[] = Title::makeTitle( $row->gtl_to_namespace, $row->gtl_to_title, null, $row->gtl_to_prefix );
+ }
+ }
+ }
+
+ return $result;
+ }
+
/**
* Returns a list of hidden categories this page is a member of.
* Uses the page_props and categorylinks tables.
}
/**
- * Get parser options suitable for rendering the primary article wikitext
- * @param $canonical boolean Determines that the generated options must not depend on user preferences (see bug 14404)
- * @return mixed ParserOptions object or boolean false
+ * Auto-generates a deletion reason
+ *
+ * @param &$hasHistory Boolean: whether the page has a history
+ * @return mixed String containing deletion reason or empty string, or boolean false
+ * if no revision occurred
*/
- public function getParserOptions( $canonical = false ) {
- global $wgUser, $wgLanguageCode;
+ public function getAutoDeleteReason( &$hasHistory ) {
+ global $wgContLang;
+
+ $dbw = wfGetDB( DB_MASTER );
+ // Get the last revision
+ $rev = Revision::newFromTitle( $this->getTitle() );
+
+ 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 == '' ) {
+ $prev = $rev->getPrevious();
+
+ if ( $prev ) {
+ $contents = $prev->getText();
+ $blank = true;
+ }
+ }
+
+ // Find out if there was only one contributor
+ // Only scan the last 20 revisions
+ $res = $dbw->select( 'revision', 'rev_user_text',
+ array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
+ __METHOD__,
+ array( 'LIMIT' => 20 )
+ );
+
+ if ( $res === false ) {
+ // This page has no revisions, which is very weird
+ return false;
+ }
- if ( !$this->mParserOptions || $canonical ) {
- $user = !$canonical ? $wgUser : new User;
- $parserOptions = new ParserOptions( $user );
- $parserOptions->setTidy( true );
- $parserOptions->enableLimitReport();
+ $hasHistory = ( $res->numRows() > 1 );
+ $row = $dbw->fetchObject( $res );
- if ( $canonical ) {
- $parserOptions->setUserLang( $wgLanguageCode ); # Must be set explicitely
- return $parserOptions;
+ if ( $row ) { // $row is false if the only contributor is hidden
+ $onlyAuthor = $row->rev_user_text;
+ // Try to find a second contributor
+ foreach ( $res as $row ) {
+ if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
+ $onlyAuthor = false;
+ break;
+ }
}
- $this->mParserOptions = $parserOptions;
+ } else {
+ $onlyAuthor = false;
+ }
+
+ // Generate the summary with a '$1' placeholder
+ 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' );
+ } else {
+ if ( $onlyAuthor ) {
+ $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
+ } else {
+ $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 );
+ // 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 );
+ $contents = $wgContLang->truncate( $contents, $maxLength );
+ // Remove possible unfinished links
+ $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
+ // Now replace the '$1' placeholder
+ $reason = str_replace( '$1', $contents, $reason );
+
+ return $reason;
+ }
+
+ /**
+ * Get parser options suitable for rendering the primary article wikitext
+ * @return mixed ParserOptions object or boolean false
+ */
+ public function getParserOptions() {
+ global $wgUser;
+ if ( !$this->mParserOptions ) {
+ $this->mParserOptions = $this->makeParserOptions( $wgUser );
}
// Clone to allow modifications of the return value without affecting cache
return clone $this->mParserOptions;
* @return ParserOptions
*/
public function makeParserOptions( User $user ) {
+ global $wgLanguageCode;
$options = ParserOptions::newFromUser( $user );
$options->enableLimitReport(); // show inclusion/loop reports
$options->setTidy( true ); // fix bad HTML
+ if ( $user->isAnon() ) {
+ $options->setUserLang( $wgLanguageCode ); # Must be set explicitily
+ }
return $options;
}