/** * Writes the data in this object to the database * @param bool $noudp */ public function save($noudp = false) { global $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang; $dbw = wfGetDB(DB_MASTER); if (!is_array($this->mExtra)) { $this->mExtra = array(); } if (!$wgPutIPinRC) { $this->mAttribs['rc_ip'] = ''; } # If our database is strict about IP addresses, use NULL instead of an empty string if ($dbw->strictIPs() && $this->mAttribs['rc_ip'] == '') { unset($this->mAttribs['rc_ip']); } # Trim spaces on user supplied text $this->mAttribs['rc_comment'] = trim($this->mAttribs['rc_comment']); # Make sure summary is truncated (whole multibyte characters) $this->mAttribs['rc_comment'] = $wgContLang->truncate($this->mAttribs['rc_comment'], 255); # Fixup database timestamps $this->mAttribs['rc_timestamp'] = $dbw->timestamp($this->mAttribs['rc_timestamp']); $this->mAttribs['rc_id'] = $dbw->nextSequenceValue('recentchanges_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() && $this->mAttribs['rc_cur_id'] == 0) { unset($this->mAttribs['rc_cur_id']); } # Insert new row $dbw->insert('recentchanges', $this->mAttribs, __METHOD__); # Set the ID $this->mAttribs['rc_id'] = $dbw->insertId(); # Notify extensions Hooks::run('RecentChange_save', array(&$this)); # Notify external application via UDP if (!$noudp) { $this->notifyRCFeeds(); } # E-mail notifications if ($wgUseEnotif || $wgShowUpdatedMarker) { $editor = $this->getPerformer(); $title = $this->getTitle(); // Never send an RC notification email about categorization changes if ($this->mAttribs['rc_type'] != RC_CATEGORIZE) { if (Hooks::run('AbortEmailNotification', array($editor, $title, $this))) { # @todo FIXME: This would be better as an extension hook $enotif = new EmailNotification(); $enotif->notifyOnPageChange($editor, $title, $this->mAttribs['rc_timestamp'], $this->mAttribs['rc_comment'], $this->mAttribs['rc_minor'], $this->mAttribs['rc_last_oldid'], $this->mExtra['pageStatus']); } } } // Update the cached list of active users if ($this->mAttribs['rc_user'] > 0) { JobQueueGroup::singleton()->lazyPush(RecentChangesUpdateJob::newCacheUpdateJob()); } }
/** * 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. * * @param Revision $revision * @param User $user User object that did the revision * @param array $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) * - moved: boolean, whether the page was moved (default false) * - oldcountable: boolean, null, or string 'no-change' (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: if created is false, don't update the article count; if created * is true, do update the article count * - 'no-change': don't update the article count, ever */ public function doEditUpdates(Revision $revision, User $user, array $options = array()) { $options += array('changed' => true, 'created' => false, 'moved' => false, 'oldcountable' => null); $content = $revision->getContent(); // Parse the text // Be careful not to do pre-save transform twice: $text is usually // already pre-save transformed once. if (!$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag('vary-revision')) { wfDebug(__METHOD__ . ": No prepared edit or vary-revision is set...\n"); $editInfo = $this->prepareContentForEdit($content, $revision, $user); } else { wfDebug(__METHOD__ . ": No vary-revision, using prepared edit...\n"); $editInfo = $this->mPreparedEdit; } // Save it to the parser cache. // Make sure the cache time matches page_touched to avoid double parsing. ParserCache::singleton()->save($editInfo->output, $this, $editInfo->popts, $revision->getTimestamp(), $editInfo->revid); // Update the links tables and other secondary data if ($content) { $recursive = $options['changed']; // bug 50785 $updates = $content->getSecondaryDataUpdates($this->getTitle(), null, $recursive, $editInfo->output); foreach ($updates as $update) { if ($update instanceof LinksUpdate) { $update->setRevision($revision); } DeferredUpdates::addUpdate($update); } } Hooks::run('ArticleEditUpdates', array(&$this, &$editInfo, $options['changed'])); if (Hooks::run('ArticleEditUpdatesDeleteFromRecentchanges', array(&$this))) { // Flush old entries from the `recentchanges` table if (mt_rand(0, 9) == 0) { JobQueueGroup::singleton()->lazyPush(RecentChangesUpdateJob::newPurgeJob()); } } if (!$this->exists()) { return; } $id = $this->getId(); $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); if ($options['oldcountable'] === 'no-change' || !$options['changed'] && !$options['moved']) { $good = 0; } elseif ($options['created']) { $good = (int) $this->isCountable($editInfo); } elseif ($options['oldcountable'] !== null) { $good = (int) $this->isCountable($editInfo) - (int) $options['oldcountable']; } else { $good = 0; } $edits = $options['changed'] ? 1 : 0; $total = $options['created'] ? 1 : 0; DeferredUpdates::addUpdate(new SiteStatsUpdate(0, $edits, $good, $total)); DeferredUpdates::addUpdate(new SearchUpdate($id, $title, $content)); // 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'))) { $recipient = User::newFromName($shortTitle, false); if (!$recipient) { wfDebug(__METHOD__ . ": invalid username\n"); } else { // Allow extensions to prevent user notification // when a new message is added to their talk page if (Hooks::run('ArticleEditUpdateNewTalk', array(&$this, $recipient))) { if (User::isIP($shortTitle)) { // An anonymous user $recipient->setNewtalk(true, $revision); } elseif ($recipient->isLoggedIn()) { $recipient->setNewtalk(true, $revision); } else { wfDebug(__METHOD__ . ": don't need to notify a nonexistent user\n"); } } } } if ($this->mTitle->getNamespace() == NS_MEDIAWIKI) { // XXX: could skip pseudo-messages like js/css here, based on content model. $msgtext = $content ? $content->getWikitextForTransclusion() : null; if ($msgtext === false || $msgtext === null) { $msgtext = ''; } MessageCache::singleton()->replace($shortTitle, $msgtext); } if ($options['created']) { self::onArticleCreate($this->mTitle); } elseif ($options['changed']) { // bug 50785 self::onArticleEdit($this->mTitle, $revision); } }
/** * 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. * * @param Revision $revision * @param User $user User object that did the revision * @param array $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) * - moved: boolean, whether the page was moved (default false) * - restored: boolean, whether the page was undeleted (default false) * - oldrevision: Revision object for the pre-update revision (default null) * - oldcountable: boolean, null, or string 'no-change' (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: if created is false, don't update the article count; if created * is true, do update the article count * - 'no-change': don't update the article count, ever */ public function doEditUpdates(Revision $revision, User $user, array $options = []) { global $wgRCWatchCategoryMembership, $wgContLang; $options += ['changed' => true, 'created' => false, 'moved' => false, 'restored' => false, 'oldrevision' => null, 'oldcountable' => null]; $content = $revision->getContent(); $logger = LoggerFactory::getInstance('SaveParse'); // See if the parser output before $revision was inserted is still valid $editInfo = false; if (!$this->mPreparedEdit) { $logger->debug(__METHOD__ . ": No prepared edit...\n"); } elseif ($this->mPreparedEdit->output->getFlag('vary-revision')) { $logger->info(__METHOD__ . ": Prepared edit has vary-revision...\n"); } elseif ($this->mPreparedEdit->output->getFlag('vary-revision-id') && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()) { $logger->info(__METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n"); } elseif ($this->mPreparedEdit->output->getFlag('vary-user') && !$options['changed']) { $logger->info(__METHOD__ . ": Prepared edit has vary-user and is null...\n"); } else { wfDebug(__METHOD__ . ": Using prepared edit...\n"); $editInfo = $this->mPreparedEdit; } if (!$editInfo) { // Parse the text again if needed. Be careful not to do pre-save transform twice: // $text is usually already pre-save transformed once. Avoid using the edit stash // as any prepared content from there or in doEditContent() was already rejected. $editInfo = $this->prepareContentForEdit($content, $revision, $user, null, false); } // Save it to the parser cache. // Make sure the cache time matches page_touched to avoid double parsing. ParserCache::singleton()->save($editInfo->output, $this, $editInfo->popts, $revision->getTimestamp(), $editInfo->revid); // Update the links tables and other secondary data if ($content) { $recursive = $options['changed']; // bug 50785 $updates = $content->getSecondaryDataUpdates($this->getTitle(), null, $recursive, $editInfo->output); foreach ($updates as $update) { if ($update instanceof LinksUpdate) { $update->setRevision($revision); $update->setTriggeringUser($user); } DeferredUpdates::addUpdate($update); } if ($wgRCWatchCategoryMembership && $this->getContentHandler()->supportsCategories() === true && ($options['changed'] || $options['created']) && !$options['restored']) { // Note: jobs are pushed after deferred updates, so the job should be able to see // the recent change entry (also done via deferred updates) and carry over any // bot/deletion/IP flags, ect. JobQueueGroup::singleton()->lazyPush(new CategoryMembershipChangeJob($this->getTitle(), ['pageId' => $this->getId(), 'revTimestamp' => $revision->getTimestamp()])); } } Hooks::run('ArticleEditUpdates', [&$this, &$editInfo, $options['changed']]); if (Hooks::run('ArticleEditUpdatesDeleteFromRecentchanges', [&$this])) { // Flush old entries from the `recentchanges` table if (mt_rand(0, 9) == 0) { JobQueueGroup::singleton()->lazyPush(RecentChangesUpdateJob::newPurgeJob()); } } if (!$this->exists()) { return; } $id = $this->getId(); $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); if ($options['oldcountable'] === 'no-change' || !$options['changed'] && !$options['moved']) { $good = 0; } elseif ($options['created']) { $good = (int) $this->isCountable($editInfo); } elseif ($options['oldcountable'] !== null) { $good = (int) $this->isCountable($editInfo) - (int) $options['oldcountable']; } else { $good = 0; } $edits = $options['changed'] ? 1 : 0; $total = $options['created'] ? 1 : 0; DeferredUpdates::addUpdate(new SiteStatsUpdate(0, $edits, $good, $total)); DeferredUpdates::addUpdate(new SearchUpdate($id, $title, $content)); // 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'))) { $recipient = User::newFromName($shortTitle, false); if (!$recipient) { wfDebug(__METHOD__ . ": invalid username\n"); } else { // Allow extensions to prevent user notification // when a new message is added to their talk page if (Hooks::run('ArticleEditUpdateNewTalk', [&$this, $recipient])) { if (User::isIP($shortTitle)) { // An anonymous user $recipient->setNewtalk(true, $revision); } elseif ($recipient->isLoggedIn()) { $recipient->setNewtalk(true, $revision); } else { wfDebug(__METHOD__ . ": don't need to notify a nonexistent user\n"); } } } } if ($this->mTitle->getNamespace() == NS_MEDIAWIKI) { // XXX: could skip pseudo-messages like js/css here, based on content model. $msgtext = $content ? $content->getWikitextForTransclusion() : null; if ($msgtext === false || $msgtext === null) { $msgtext = ''; } MessageCache::singleton()->replace($shortTitle, $msgtext); if ($wgContLang->hasVariants()) { $wgContLang->updateConversionTable($this->mTitle); } } if ($options['created']) { self::onArticleCreate($this->mTitle); } elseif ($options['changed']) { // bug 50785 self::onArticleEdit($this->mTitle, $revision); } ResourceLoaderWikiModule::invalidateModuleCache($this->mTitle, $options['oldrevision'], $revision, wfWikiID()); }
/** * Writes the data in this object to the database * @param bool $noudp */ public function save($noudp = false) { global $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang; $dbw = wfGetDB(DB_MASTER); if (!is_array($this->mExtra)) { $this->mExtra = []; } if (!$wgPutIPinRC) { $this->mAttribs['rc_ip'] = ''; } # Strict mode fixups (not-NULL fields) foreach (['minor', 'bot', 'new', 'patrolled', 'deleted'] as $field) { $this->mAttribs["rc_{$field}"] = (int) $this->mAttribs["rc_{$field}"]; } # ...more fixups (NULL fields) foreach (['old_len', 'new_len'] as $field) { $this->mAttribs["rc_{$field}"] = isset($this->mAttribs["rc_{$field}"]) ? (int) $this->mAttribs["rc_{$field}"] : null; } # If our database is strict about IP addresses, use NULL instead of an empty string $strictIPs = in_array($dbw->getType(), ['oracle', 'postgres']); // legacy if ($strictIPs && $this->mAttribs['rc_ip'] == '') { unset($this->mAttribs['rc_ip']); } # Trim spaces on user supplied text $this->mAttribs['rc_comment'] = trim($this->mAttribs['rc_comment']); # Make sure summary is truncated (whole multibyte characters) $this->mAttribs['rc_comment'] = $wgContLang->truncate($this->mAttribs['rc_comment'], 255); # Fixup database timestamps $this->mAttribs['rc_timestamp'] = $dbw->timestamp($this->mAttribs['rc_timestamp']); $this->mAttribs['rc_id'] = $dbw->nextSequenceValue('recentchanges_rc_id_seq'); # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL if ($this->mAttribs['rc_cur_id'] == 0) { unset($this->mAttribs['rc_cur_id']); } # Insert new row $dbw->insert('recentchanges', $this->mAttribs, __METHOD__); # Set the ID $this->mAttribs['rc_id'] = $dbw->insertId(); # Notify extensions Hooks::run('RecentChange_save', [&$this]); if (count($this->tags)) { ChangeTags::addTags($this->tags, $this->mAttribs['rc_id'], $this->mAttribs['rc_this_oldid'], $this->mAttribs['rc_logid'], null, $this); } # Notify external application via UDP if (!$noudp) { $this->notifyRCFeeds(); } # E-mail notifications if ($wgUseEnotif || $wgShowUpdatedMarker) { $editor = $this->getPerformer(); $title = $this->getTitle(); // Never send an RC notification email about categorization changes if ($this->mAttribs['rc_type'] != RC_CATEGORIZE && Hooks::run('AbortEmailNotification', [$editor, $title, $this])) { // @FIXME: This would be better as an extension hook // Send emails or email jobs once this row is safely committed $dbw->onTransactionIdle(function () use($editor, $title) { $enotif = new EmailNotification(); $enotif->notifyOnPageChange($editor, $title, $this->mAttribs['rc_timestamp'], $this->mAttribs['rc_comment'], $this->mAttribs['rc_minor'], $this->mAttribs['rc_last_oldid'], $this->mExtra['pageStatus']); }, __METHOD__); } } // Update the cached list of active users if ($this->mAttribs['rc_user'] > 0) { JobQueueGroup::singleton()->lazyPush(RecentChangesUpdateJob::newCacheUpdateJob()); } }