* Some fields are public only for backwards-compatibility. Use accessors.
* In the past, this class was part of Article.php and everything was public.
*
- * @TODO: dependency inject $wgUser as an argument to functions
- *
* @internal documentation reviewed 15 Mar 2010
*/
class WikiPage extends Page {
+ /**
+ * @var Title
+ * @protected
+ */
+ public $mTitle = null;
+
/**@{{
* @protected
- * Fields are public for backwards-compatibility. Use accessors.
- * In the past, this class was part of Article.php and everything was public.
- */
- public $mTitle = null; // !< Title object
- public $mCounter = -1; // !< Not loaded
- public $mDataLoaded = false; // !<
- public $mIsRedirect = false; // !<
- public $mLatest = false; // !<
- public $mPreparedEdit = false; // !<
+ */
+ public $mCounter = -1; // !< Integer (-1 means "not loaded")
+ public $mDataLoaded = false; // !< Boolean
+ public $mIsRedirect = false; // !< Boolean
+ public $mLatest = false; // !< Boolean
+ public $mPreparedEdit = false; // !< Array
public $mRedirectTarget = null; // !< Title object
public $mLastRevision = null; // !< Revision object
- public $mTimestamp = ''; // !<
- public $mTouched = '19700101000000'; // !<
+ public $mTimestamp = ''; // !< String
+ public $mTouched = '19700101000000'; // !< String
/**@}}*/
/**
$this->mTitle = $title;
}
+ /**
+ * Create a WikiPage object of the appropriate class for the given title.
+ *
+ * @param $title Title
+ * @return WikiPage object of the appropriate type
+ */
+ public static function factory( Title $title ) {
+ $ns = $title->getNamespace();
+
+ if ( $ns == NS_MEDIA ) {
+ throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
+ } elseif ( $ns < 0 ) {
+ throw new MWException( "Invalid or virtual namespace $ns given." );
+ }
+
+ switch ( $ns ) {
+ case NS_FILE:
+ $page = new WikiFilePage( $title );
+ break;
+ case NS_CATEGORY:
+ $page = new WikiCategoryPage( $title );
+ break;
+ default:
+ $page = new WikiPage( $title );
+ }
+
+ return $page;
+ }
+
/**
* Constructor from a page id
+ *
* @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;
+ }
+
+ /**
+ * Returns overrides for action handlers.
+ * Classes listed here will be used instead of the default one when
+ * (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() {
+ return array();
}
/**
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'redirect',
array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ),
- array( 'rd_from' => $this->getID() ),
+ array( 'rd_from' => $this->getId() ),
__METHOD__
);
$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__
);
/**
* Return the list of revision fields that should be selected to create
* a new page.
+ *
+ * @return array
*/
public static function selectFields() {
return array(
* @param $title Title object
* @return mixed Database result resource, or false on failure
*/
- protected function pageDataFromTitle( $dbr, $title ) {
+ public function pageDataFromTitle( $dbr, $title ) {
return $this->pageData( $dbr, array(
'page_namespace' => $title->getNamespace(),
'page_title' => $title->getDBkey() ) );
* @param $id Integer
* @return mixed Database result resource, or false on failure
*/
- protected function pageDataFromId( $dbr, $id ) {
+ public function pageDataFromId( $dbr, $id ) {
return $this->pageData( $dbr, array( 'page_id' => $id ) );
}
* Set the general counter, title etc data loaded from
* some source.
*
- * @param $data Object|String $res->fetchObject() object or the string "fromdb" to reload
+ * @param $data Object|String One of the following:
+ * 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' ) {
- $dbr = wfGetDB( DB_SLAVE );
- $data = $this->pageDataFromTitle( $dbr, $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->mArticleID = intval( $data->page_id );
+ $this->mTitle->loadFromRow( $data );
# Old-fashioned restrictions
$this->mTitle->loadRestrictions( $data->page_restrictions );
$this->mLatest = intval( $data->page_latest );
} else {
$lc->addBadLinkObj( $this->mTitle );
- $this->mTitle->mArticleID = 0;
+
+ $this->mTitle->loadFromRow( false );
}
$this->mDataLoaded = true;
/**
* @return int Page ID
*/
- public function getID() {
+ public function getId() {
return $this->mTitle->getArticleID();
}
*/
public function getCount() {
if ( -1 == $this->mCounter ) {
- $id = $this->getID();
+ $id = $this->getId();
if ( $id == 0 ) {
$this->mCounter = 0;
return; // already loaded
}
- # New or non-existent articles have no user information
- $id = $this->getID();
- if ( 0 == $id ) {
- return;
+ $latest = $this->getLatest();
+ if ( !$latest ) {
+ return; // page doesn't exist or is missing page_latest info
}
- $revision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id );
- if ( $revision ) {
+ $revision = Revision::newFromPageId( $this->getId(), $latest );
+ if ( $revision ) { // sanity
$this->setLastEdit( $revision );
}
}
/**
* Get the latest revision
- * @return Revision|false
+ * @return Revision|null
*/
public function getRevision() {
$this->loadLastEdit();
if ( $this->mLastRevision ) {
return $this->mLastRevision;
}
- return false;
+ return null;
}
/**
* Revision::FOR_THIS_USER to be displayed to $wgUser
* Revision::RAW get the text regardless of permissions
* @return String|false The text of the current revision
- */
+ */
public function getText( $audience = Revision::FOR_PUBLIC ) {
$this->loadLastEdit();
if ( $this->mLastRevision ) {
* @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();
}
/**
* Should the parser cache be used?
*
+ * @param $user User The relevant user
* @return boolean
*/
- public function useParserCache( $oldid ) {
- global $wgUser, $wgEnableParserCache;
+ public function isParserCacheUsed( User $user, $oldid ) {
+ global $wgEnableParserCache;
return $wgEnableParserCache
- && $wgUser->getStubThreshold() == 0
+ && $user->getStubThreshold() == 0
&& $this->exists()
&& empty( $oldid )
- && !$this->mTitle->isCssOrJsPage()
- && !$this->mTitle->isCssJsSubpage();
+ && $this->mTitle->isWikitextPage();
}
/**
}
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- if ( $this->getID() == 0 ) {
+ if ( $this->getId() == 0 ) {
$text = false;
} else {
$text = $this->getRawText();
*
* @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.
*
* Change an existing article or create a new article. Updates RC and all necessary caches,
* optionally via the deferred update array.
*
- * $wgUser must be set before calling this function.
- *
* @param $text String: new text
* @param $summary String: edit summary
* @param $flags Integer bitfield:
* 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.
*
* @param $baseRevId the revision ID this edit was based off, if any
- * @param $user User (optional), $wgUser will be used if not passed
+ * @param $user User the user doing the edit
*
* @return Status object. Possible errors:
* edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
$oldtext = $this->getRawText(); // current revision
$oldsize = strlen( $oldtext );
+ $oldcountable = $this->isCountable();
# 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 );
$userAbort = ignore_user_abort( true );
}
+ $revision = new Revision( array(
+ 'page' => $this->getId(),
+ 'comment' => $summary,
+ 'minor_edit' => $isminor,
+ 'text' => $text,
+ 'parent_id' => $this->mLatest,
+ 'user' => $user->getId(),
+ 'user_text' => $user->getName(),
+ 'timestamp' => $now
+ ) );
+
$changed = ( strcmp( $text, $oldtext ) != 0 );
if ( $changed ) {
return $status;
}
- $revision = new Revision( array(
- 'page' => $this->getId(),
- 'comment' => $summary,
- 'minor_edit' => $isminor,
- 'text' => $text,
- 'parent_id' => $this->mLatest,
- 'user' => $user->getId(),
- 'user_text' => $user->getName(),
- 'timestamp' => $now
- ) );
-
$dbw->begin();
$revisionId = $revision->insertOn( $dbw );
$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->getLatest();
- // Update page_touched, this is usually implicit in the page update
- // Other cache updates are done in onArticleEdit()
- $this->mTitle->invalidateCache();
}
if ( !$wgDBtransactions ) {
return $status;
}
- # Invalidate cache of this article and all pages using this article
- # as a template. Partly deferred.
- self::onArticleEdit( $this->mTitle );
# Update links tables, site stats, etc.
- $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed, $user );
+ $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
+ 'oldcountable' => $oldcountable ) );
+
+ if ( !$changed ) {
+ $status->warning( 'edit-no-change' );
+ $revision = null;
+ // Keep the same revision ID, but do some updates on it
+ $revisionId = $this->getLatest();
+ // Update page_touched, this is usually implicit in the page update
+ // Other cache updates are done in onArticleEdit()
+ $this->mTitle->invalidateCache();
+ }
} else {
# Create new article
$status->value['new'] = true;
$dbw->commit();
# Update links, etc.
- $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, true, $user, true );
-
- # Clear caches
- self::onArticleCreate( $this->mTitle );
+ $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
$flags & EDIT_MINOR, null, null, &$flags, $revision ) );
# 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.
* @param $expiry Array: per restriction type expiration
+ * @param $user User The user updating the restrictions
* @return bool true on success
*/
- public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) {
+ public function updateRestrictions(
+ $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null
+ ) {
global $wgUser, $wgContLang;
+ $user = is_null( $user ) ? $wgUser : $user;
$restrictionTypes = $this->mTitle->getRestrictionTypes();
return false;
}
- if ( !$this->mTitle->userCan( 'protect' ) ) {
+ if ( count( $this->mTitle->getUserPermissionsErrors( 'protect', $user ) ) ) {
wfDebug( "updateRestrictions failed: insufficient permissions\n" );
return false;
}
# 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] ) {
+ if ( $aRChanged &&
+ $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] )
+ {
$changed = true;
}
}
# If nothing's changed, do nothing
if ( $changed ) {
- if ( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
+ if ( wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) {
$dbw = wfGetDB( DB_MASTER );
# Prepare a null revision to be added to the history
$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 ) ,
), __METHOD__
);
- wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $wgUser ) );
- wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) );
+ wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) );
# Update the protection log
$log = new LogPage( 'protect' );
* Revision::DELETED_RESTRICTED
* @param $id int article ID
* @param $commit boolean defaults to true, triggers transaction end
+ * @param &$errors Array of errors to append to
+ * @param $user User The relevant user
* @return boolean true if successful
*/
- public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true, &$error = '' ) {
- global $wgDeferredUpdateList, $wgUseTrackbacks;
- global $wgUser;
+ public function doDeleteArticle(
+ $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
+ ) {
+ global $wgUseTrackbacks, $wgEnableInterwikiTemplatesTracking, $wgGlobalDatabase, $wgUser;
+ $user = is_null( $user ) ? $wgUser : $user;
wfDebug( __METHOD__ . "\n" );
- if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$wgUser, &$reason, &$error ) ) ) {
+ if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error ) ) ) {
return false;
}
$dbw = wfGetDB( DB_MASTER );
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();
}
- wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$wgUser, $reason, $id ) );
+ wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) );
return true;
}
* Roll back the most recent consecutive set of edits to a page
* from the same user; fails if there are no eligible edits to
* roll back to, e.g. user is the sole contributor. This function
- * performs permissions checks on $wgUser, then calls commitRollback()
+ * performs permissions checks on $user, then calls commitRollback()
* to do the dirty work
*
* @param $fromP String: Name of the user whose edits to rollback.
* 'alreadyrolled' : 'current' (rev)
* success : 'summary' (str), 'current' (rev), 'target' (rev)
*
+ * @param $user User The user performing the rollback
* @return array of errors, each error formatted as
* array(messagekey, param1, param2, ...).
* On success, the array is empty. This array can also be passed to
* OutputPage::showPermissionsErrorPage().
*/
- public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) {
- global $wgUser;
-
+ public function doRollback(
+ $fromP, $summary, $token, $bot, &$resultDetails, User $user
+ ) {
$resultDetails = null;
# Check permissions
- $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
- $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser );
+ $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
+ $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
$errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
- if ( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) {
+ if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) {
$errors[] = array( 'sessionfailure' );
}
- if ( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) {
+ if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
$errors[] = array( 'actionthrottledtext' );
}
return $errors;
}
- return $this->commitRollback( $fromP, $summary, $bot, $resultDetails );
+ return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user );
}
/**
* 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 $summary String: Custom summary. Set to default summary if empty.
+ * @param $bot Boolean: If true, mark all reverted edits as bot.
+ *
+ * @param $resultDetails Array: contains result-specific array of additional values
+ * @param $guser User The user performing the rollback
*/
- public function commitRollback( $fromP, $summary, $bot, &$resultDetails ) {
- global $wgUseRCPatrol, $wgUser, $wgContLang;
+ public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) {
+ global $wgUseRCPatrol, $wgContLang;
$dbw = wfGetDB( DB_MASTER );
}
$set = array();
- if ( $bot && $wgUser->isAllowed( 'markbotedits' ) ) {
+ if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
# Mark all reverted edits as bot
$set['rc_bot'] = 1;
}
# Save
$flags = EDIT_UPDATE;
- if ( $wgUser->isAllowed( 'minoredit' ) ) {
+ if ( $guser->isAllowed( 'minoredit' ) ) {
$flags |= EDIT_MINOR;
}
- if ( $bot && ( $wgUser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
+ if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
$flags |= EDIT_FORCE_BOT;
}
$revId = false;
}
- wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target, $current ) );
+ wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) );
$resultDetails = array(
'summary' => $summary,
/**
* Do standard deferred updates after page view
+ * @param $user User The relevant user
*/
- public function viewUpdates() {
- global $wgDeferredUpdateList, $wgDisableCounters, $wgUser;
+ public function doViewUpdates( User $user ) {
+ global $wgDisableCounters;
if ( wfReadOnly() ) {
return;
}
# Don't update page view counters on views from bot users (bug 14044)
- if ( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) && $this->getID() ) {
- $wgDeferredUpdateList[] = new ViewCountUpdate( $this->getID() );
- $wgDeferredUpdateList[] = new SiteStatsUpdate( 1, 0, 0 );
+ if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->getId() ) {
+ DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) );
+ DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) );
}
# Update newtalk / watchlist notification status
- $wgUser->clearNotification( $this->mTitle );
+ $user->clearNotification( $this->mTitle );
}
/**
* Returns a stdclass with source, pst and output members
*/
public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
- if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid ) {
+ global $wgParser, $wgUser;
+ $user = is_null( $user ) ? $wgUser : $user;
+ // @TODO fixme: check $user->getId() here???
+ if ( $this->mPreparedEdit
+ && $this->mPreparedEdit->newText == $text
+ && $this->mPreparedEdit->revid == $revid
+ ) {
// Already prepared
return $this->mPreparedEdit;
}
- global $wgParser;
-
- if( $user === null ) {
- global $wgUser;
- $user = $wgUser;
- }
$popts = ParserOptions::newFromUser( $user );
wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) );
$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();
* Every 100th edit, prune the recent changes table.
*
* @private
- * @param $text String: New text of the article
- * @param $summary String: Edit summary
- * @param $minoredit Boolean: Minor edit
- * @param $timestamp_of_pagechange String timestamp associated with the page change
- * @param $newid Integer: rev_id value of the new revision
- * @param $changed Boolean: Whether or not the content actually changed
- * @param $user User object: User doing the edit
- * @param $created Boolean: Whether the edit created the page
- */
- public function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid,
- $changed = true, User $user = null, $created = false )
- {
- global $wgDeferredUpdateList, $wgUser, $wgEnableParserCache;
+ * @param $revision Revision object
+ * @param $user User object that did the revision
+ * @param $options Array of options, following indexes are used:
+ * - changed: boolean, whether the revision changed the content (default true)
+ * - created: boolean, whether the revision created the page (default false)
+ * - oldcountable: boolean or null (default null):
+ * - boolean: whether the page was counted as an article before that
+ * revision, only used in changed is true and created is false
+ * - null: don't change the article count
+ */
+ public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
+ global $wgEnableParserCache;
wfProfileIn( __METHOD__ );
+ $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
+ $text = $revision->getText();
+
# 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' ) ) {
wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
- $editInfo = $this->prepareTextForEdit( $text, $newid, $user );
+ $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
} else {
wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
$editInfo = $this->mPreparedEdit;
$u = new LinksUpdate( $this->mTitle, $editInfo->output );
$u->doUpdate();
- wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $changed ) );
+ wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
if ( 0 == mt_rand( 0, 99 ) ) {
}
}
- $id = $this->getID();
+ $id = $this->getId();
$title = $this->mTitle->getPrefixedDBkey();
$shortTitle = $this->mTitle->getDBkey();
return;
}
- if ( !$changed ) {
+ if ( !$options['changed'] ) {
$good = 0;
$total = 0;
- } elseif ( $created ) {
+ } elseif ( $options['created'] ) {
$good = (int)$this->isCountable( $editInfo );
$total = 1;
+ } elseif ( $options['oldcountable'] !== null ) {
+ $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
+ $total = 0;
} else {
- $good = (int)$this->isCountable( $editInfo ) - (int)$this->isCountable();
+ $good = 0;
$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 $changed = false otherwise some idiot can null-edit a
- # 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 this is another user's talk page, update newtalk.
+ # Don't do this if $options['changed'] = false (null-edits) nor if
+ # it's a minor edit and the user doesn't want notifications for those.
+ if ( $options['changed']
+ && $this->mTitle->getNamespace() == NS_USER_TALK
+ && $shortTitle != $user->getTitleKey()
+ && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
) {
if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) {
$other = User::newFromName( $shortTitle, false );
MessageCache::singleton()->replace( $shortTitle, $text );
}
+ if( $options['created'] ) {
+ self::onArticleCreate( $this->mTitle );
+ } else {
+ self::onArticleEdit( $this->mTitle );
+ }
+
wfProfileOut( __METHOD__ );
}
* @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
+ * other shitty functions like doEditUpdates and such so it's not needed
* anymore.
+ * @deprecated since 1.18, use doEditUpdates()
*/
public function createUpdates( $rev ) {
- $this->editUpdates( $rev->getText(), $rev->getComment(),
- $rev->isMinor(), wfTimestamp(), $rev->getId(), true, null, true );
+ global $wgUser;
+ $this->doEditUpdates( $rev, $wgUser, array( 'created' => true ) );
}
/**
* so we can do things like signatures and links-in-context.
*
* @param $text String article contents
- * @param $user User object: user doing the edit, $wgUser will be used if
- * null is given
+ * @param $user User object: user doing the edit
* @param $popts ParserOptions object: parser options, default options for
* the user loaded if null given
* @return string article contents with altered wikitext markup (signatures
* converted, {{subst:}}, templates, etc.)
*/
public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) {
- global $wgParser;
-
- if ( $user === null ) {
- global $wgUser;
- $user = $wgUser;
- }
+ global $wgParser, $wgUser;
+ $user = is_null( $user ) ? $wgUser : $user;
if ( $popts === null ) {
$popts = ParserOptions::newFromUser( $user );
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
-
return !$this->mIsRedirect;
}
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
-
return $this->mTouched;
}
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
-
return (int)$this->mLatest;
}
* are not updated, caches are not flushed.
*
* @param $text String: text submitted
+ * @param $user User The relevant user
* @param $comment String: comment submitted
* @param $minor Boolean: whereas it's a minor modification
*/
- public function quickEdit( $text, $comment = '', $minor = 0 ) {
+ public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
wfProfileIn( __METHOD__ );
$dbw = wfGetDB( DB_MASTER );
'text' => $text,
'comment' => $comment,
'minor_edit' => $minor ? 1 : 0,
- ) );
+ ) );
$revision->insertOn( $dbw );
$this->updateRevisionOn( $dbw, $revision );
- global $wgUser;
- wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $wgUser ) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
wfProfileOut( __METHOD__ );
}
$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.
$rt = Title::newFromRedirect( $newtext );
if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
- return wfMsgForContent( 'autoredircomment', $rt->getFullText() );
+ $truncatedtext = $wgContLang->truncate(
+ str_replace( "\n", ' ', $newtext ),
+ max( 0, 250
+ - strlen( wfMsgForContent( 'autoredircomment' ) )
+ - strlen( $rt->getFullText() )
+ ) );
+ return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
}
# New page autosummaries
}
/**
- * 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;
- if ( !$this->mParserOptions || $canonical ) {
- $user = !$canonical ? $wgUser : new User;
- $parserOptions = new ParserOptions( $user );
- $parserOptions->setTidy( true );
- $parserOptions->enableLimitReport();
+ $dbw = wfGetDB( DB_MASTER );
+ // Get the last revision
+ $rev = Revision::newFromTitle( $this->getTitle() );
- if ( $canonical ) {
- $parserOptions->setUserLang( $wgLanguageCode ); # Must be set explicitely
- return $parserOptions;
+ 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;
}
- $this->mParserOptions = $parserOptions;
+ }
+
+ // 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;
+ }
+
+ $hasHistory = ( $res->numRows() > 1 );
+ $row = $dbw->fetchObject( $res );
+
+ 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;
+ }
+ }
+ } 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;
}
}
/**
- * Lightweight method to get the parser output for a page, checking the parser cache
- * and so on. Doesn't consider most of the stuff that WikiPage::view is forced to
- * consider, so it's not appropriate to use there.
- *
- * @since 1.16 (r52326) for LiquidThreads
+ * Updates cascading protections
*
- * @param $oldid mixed integer Revision ID or null
- * @return ParserOutput or false if the given revsion ID is not found
- */
- public function getParserOutput( $oldid = null ) {
- global $wgEnableParserCache, $wgUser;
+ * @param $parserOutput ParserOutput object for the current version
+ **/
+ public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) {
+ if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) {
+ return;
+ }
+
+ // templatelinks table may have become out of sync,
+ // especially if using variable-based transclusions.
+ // For paranoia, check if things have changed and if
+ // so apply updates to the database. This will ensure
+ // that cascaded protections apply as soon as the changes
+ // are visible.
- // Should the parser cache be used?
- $useParserCache = $wgEnableParserCache &&
- $wgUser->getStubThreshold() == 0 &&
- $this->exists() &&
- $oldid === null;
+ # Get templates from templatelinks
+ $id = $this->mTitle->getArticleID();
+
+ $tlTemplates = array();
- wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( array( 'templatelinks' ),
+ array( 'tl_namespace', 'tl_title' ),
+ array( 'tl_from' => $id ),
+ __METHOD__
+ );
- if ( $wgUser->getStubThreshold() ) {
- wfIncrStats( 'pcache_miss_stub' );
+ foreach ( $res as $row ) {
+ $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true;
}
- if ( $useParserCache ) {
- $parserOutput = ParserCache::singleton()->get( $this, $this->getParserOptions() );
- if ( $parserOutput !== false ) {
- return $parserOutput;
+ # Get templates from parser output.
+ $poTemplates = array();
+ foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
+ foreach ( $templates as $dbk => $id ) {
+ $poTemplates["$ns:$dbk"] = true;
}
}
- // Cache miss; parse and output it.
- if ( $oldid === null ) {
- $text = $this->getRawText();
- } else {
- $rev = Revision::newFromTitle( $this->getTitle(), $oldid );
- if ( $rev === null ) {
- return false;
- }
- $text = $rev->getText();
+ # Get the diff
+ $templates_diff = array_diff_key( $poTemplates, $tlTemplates );
+
+ if ( count( $templates_diff ) > 0 ) {
+ # Whee, link updates time.
+ $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
+ $u->doUpdate();
}
+ }
- return $this->getOutputFromWikitext( $text, $useParserCache );
+ /*
+ * @deprecated since 1.18
+ */
+ public function quickEdit( $text, $comment = '', $minor = 0 ) {
+ global $wgUser;
+ return $this->doQuickEdit( $text, $wgUser, $comment, $minor );
+ }
+
+ /*
+ * @deprecated since 1.18
+ */
+ public function viewUpdates() {
+ global $wgUser;
+ return $this->doViewUpdates( $wgUser );
+ }
+
+ /*
+ * @deprecated since 1.18
+ */
+ public function useParserCache( $oldid ) {
+ global $wgUser;
+ return $this->isParserCacheUsed( $wgUser, $oldid );
}
}