function tagProxyChange($recentChange) { global $wgTagProxyActions, $wgUser; if ($wgTagProxyActions && self::isProxy(wfGetIP()) && !$wgUser->isAllowed('notagproxychanges')) { ChangeTags::addTags('proxy', $recentChange->mAttribs['rc_id'], $recentChange->mAttribs['rc_this_oldid'], $recentChange->mAttribs['rc_logid']); } return true; }
public function execute() { global $wgVisualEditorNamespaces, $wgVisualEditorUseChangeTagging; $user = $this->getUser(); $params = $this->extractRequestParams(); $page = Title::newFromText($params['page']); if (!$page) { $this->dieUsageMsg('invalidtitle', $params['page']); } if (!in_array($page->getNamespace(), $wgVisualEditorNamespaces)) { $this->dieUsage("VisualEditor is not enabled in namespace " . $page->getNamespace(), 'novenamespace'); } $parserParams = array(); if (isset($params['oldwt'])) { $parserParams['oldwt'] = $params['oldwt']; } else { if (isset($params['oldid'])) { $parserParams['oldid'] = $params['oldid']; } } if ($params['cachekey'] !== null) { $wikitext = $this->trySerializationCache($params['cachekey']); if (!is_string($wikitext)) { $this->dieUsage('No cached serialization found with that key', 'badcachekey'); } } else { $wikitext = $this->postHTML($page, $params['html'], $parserParams); if ($wikitext === false) { $this->dieUsage('Error contacting the Parsoid server', 'parsoidserver'); } } $saveresult = $this->saveWikitext($page, $wikitext, $params); $editStatus = $saveresult['edit']['result']; // Error if ($editStatus !== 'Success') { $result = array('result' => 'error', 'edit' => $saveresult['edit']); // Success } else { if (isset($saveresult['edit']['newrevid']) && $wgVisualEditorUseChangeTagging) { ChangeTags::addTags('visualeditor', null, intval($saveresult['edit']['newrevid']), null); if ($params['needcheck']) { ChangeTags::addTags('visualeditor-needcheck', null, intval($saveresult['edit']['newrevid']), null); } } // Return result of parseWikitext instead of saveWikitext so that the // frontend can update the page rendering without a refresh. $result = $this->parseWikitext($page, $params['useskin']); if ($result === false) { $this->dieUsage('Error contacting the Parsoid server', 'parsoidserver'); } $result['isRedirect'] = $page->isRedirect(); if (isset($saveresult['edit']['newrevid'])) { $result['newrevid'] = intval($saveresult['edit']['newrevid']); } $result['result'] = 'success'; } $this->getResult()->addValue(null, $this->getModuleName(), $result); }
/** * @desc Mark all edits made via mobile skin with a mobileedit tag * * @param $article * @param $user * @param $text * @param $summary * @param $minoredit * @param $watchthis * @param $sectionanchor * @param $flags * @param $revision Revision * @param $status * @param $baseRevId */ public static function onArticleSaveComplete(&$article, &$user, $text, $summary, $minoredit, $watchthis, $sectionanchor, &$flags, $revision, &$status, $baseRevId) { $app = F::app(); //Add Mobile Edit tag when an article was saved via mobile skin if ($app->checkSkin('wikiamobile') && !is_null($revision)) { ChangeTags::addTags('mobileedit', null, $revision->getId(), null); } return true; }
protected function addTags($items, $dry) { $count = count($items); if (!$count) { $this->output("No revisions to tag\n"); return; } if ($dry) { $this->output("{$count} revisions would be tagged\n"); return; } $this->output("{$count} rows are tagged\n"); foreach ($items as $item) { list($row, $revId) = $item; ChangeTags::addTags('contenttranslation', null, $revId, null, FormatJson::encode(array('from' => $row->translation_source_language, 'to' => $row->translation_target_language))); } }
public function execute() { $this->useTransactionalTimeLimit(); $user = $this->getUser(); $params = $this->extractRequestParams(); // WikiPage::doRollback needs a Web UI token, so get one of those if we // validated based on an API rollback token. $token = $params['token']; if ($user->matchEditToken($token, 'rollback', $this->getRequest())) { $token = $this->getUser()->getEditToken($this->getWebUITokenSalt($params), $this->getRequest()); } $titleObj = $this->getRbTitle($params); $pageObj = WikiPage::factory($titleObj); $summary = $params['summary']; $details = array(); // If change tagging was requested, check that the user is allowed to tag, // and the tags are valid if (count($params['tags'])) { $tagStatus = ChangeTags::canAddTagsAccompanyingChange($params['tags'], $user); if (!$tagStatus->isOK()) { $this->dieStatus($tagStatus); } } $retval = $pageObj->doRollback($this->getRbUser($params), $summary, $token, $params['markbot'], $details, $user); if ($retval) { // We don't care about multiple errors, just report one of them $this->dieUsageMsg(reset($retval)); } $watch = 'preferences'; if (isset($params['watchlist'])) { $watch = $params['watchlist']; } // Watch pages $this->setWatch($watch, $titleObj, 'watchrollback'); if (count($params['tags'])) { ChangeTags::addTags($params['tags'], null, intval($details['newid']), null, null); } $info = array('title' => $titleObj->getPrefixedText(), 'pageid' => intval($details['current']->getPage()), 'summary' => $details['summary'], 'revid' => intval($details['newid']), 'old_revid' => intval($details['current']->getID()), 'last_revid' => intval($details['target']->getID())); $this->getResult()->addValue(null, $this->getModuleName(), $info); }
public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); $page = Title::newFromText($params['page']); if (!$page) { $this->dieUsageMsg('invalidtitle', $params['page']); } $availableNamespaces = $this->veConfig->get('VisualEditorAvailableNamespaces'); if (!isset($availableNamespaces[$page->getNamespace()]) || !$availableNamespaces[$page->getNamespace()]) { $this->dieUsage("VisualEditor is not enabled in namespace " . $page->getNamespace(), 'novenamespace'); } $parserParams = array(); if (isset($params['oldid'])) { $parserParams['oldid'] = $params['oldid']; } $html = $params['html']; if (substr($html, 0, 11) === 'rawdeflate,') { $html = gzinflate(base64_decode(substr($html, 11))); } if ($params['cachekey'] !== null) { $wikitext = $this->trySerializationCache($params['cachekey']); if (!is_string($wikitext)) { $this->dieUsage('No cached serialization found with that key', 'badcachekey'); } } else { $wikitext = $this->postHTML($page, $html, $parserParams, $params['etag']); if ($wikitext === false) { $this->dieUsage('Error contacting the Parsoid/RESTbase server', 'docserver'); } } $saveresult = $this->saveWikitext($page, $wikitext, $params); $editStatus = $saveresult['edit']['result']; // Error if ($editStatus !== 'Success') { $result = array('result' => 'error', 'edit' => $saveresult['edit']); if (isset($saveresult['edit']['spamblacklist'])) { $matches = explode('|', $saveresult['edit']['spamblacklist']); $matcheslist = $this->getLanguage()->listToText($matches); $result['edit']['sberrorparsed'] = $this->msg('spamprotectiontext')->parse() . ' ' . $this->msg('spamprotectionmatch', $matcheslist)->parse(); } // Success } else { if (isset($saveresult['edit']['newrevid'])) { $newRevId = intval($saveresult['edit']['newrevid']); if ($this->veConfig->get('VisualEditorUseChangeTagging')) { // Defer till after the RC row is inserted // @TODO: doEditContent should let callers specify desired tags DeferredUpdates::addCallableUpdate(function () use($newRevId) { ChangeTags::addTags('visualeditor', null, $newRevId, null); }); } } else { $newRevId = $page->getLatestRevId(); } // Return result of parseWikitext instead of saveWikitext so that the // frontend can update the page rendering without a refresh. $result = $this->parseWikitext($page, $newRevId); if ($result === false) { $this->dieUsage('Error contacting the Parsoid/RESTBase server', 'docserver'); } $result['isRedirect'] = $page->isRedirect(); if (class_exists('FlaggablePageView')) { $view = FlaggablePageView::singleton(); // Defeat !$this->isPageView( $request ) || $request->getVal( 'oldid' ) check in setPageContent $view->getContext()->setRequest(new DerivativeRequest($this->getRequest(), array('diff' => null, 'oldid' => '', 'action' => 'view') + $this->getRequest()->getValues())); // The two parameters here are references but we don't care // about what FlaggedRevs does with them. $outputDone = null; $useParserCache = null; $view->setPageContent($outputDone, $useParserCache); $view->displayTag(); } $result['contentSub'] = $this->getOutput()->getSubtitle(); $lang = $this->getLanguage(); if (isset($saveresult['edit']['newtimestamp'])) { $ts = $saveresult['edit']['newtimestamp']; $result['lastModified'] = array('date' => $lang->userDate($ts, $user), 'time' => $lang->userTime($ts, $user)); } if (isset($saveresult['edit']['newrevid'])) { $result['newrevid'] = intval($saveresult['edit']['newrevid']); } $result['result'] = 'success'; } $this->getResult()->addValue(null, $this->getModuleName(), $result); }
/** * Makes an entry in the database corresponding to page creation * Note: the title object must be loaded with the new id using resetArticleID() * * @param string $timestamp * @param Title $title * @param bool $minor * @param User $user * @param string $comment * @param bool $bot * @param string $ip * @param int $size * @param int $newId * @param int $patrol * @param array $tags * @return RecentChange */ public static function notifyNew($timestamp, &$title, $minor, &$user, $comment, $bot, $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = array()) { $rc = new RecentChange(); $rc->mTitle = $title; $rc->mPerformer = $user; $rc->mAttribs = array('rc_timestamp' => $timestamp, 'rc_namespace' => $title->getNamespace(), 'rc_title' => $title->getDBkey(), 'rc_type' => RC_NEW, 'rc_source' => self::SRC_NEW, 'rc_minor' => $minor ? 1 : 0, 'rc_cur_id' => $title->getArticleID(), 'rc_user' => $user->getId(), 'rc_user_text' => $user->getName(), 'rc_comment' => $comment, 'rc_this_oldid' => $newId, 'rc_last_oldid' => 0, 'rc_bot' => $bot ? 1 : 0, 'rc_ip' => self::checkIPAddress($ip), 'rc_patrolled' => intval($patrol), 'rc_new' => 1, 'rc_old_len' => 0, 'rc_new_len' => $size, 'rc_deleted' => 0, 'rc_logid' => 0, 'rc_log_type' => null, 'rc_log_action' => '', 'rc_params' => ''); $rc->mExtra = array('prefixedDBkey' => $title->getPrefixedDBkey(), 'lastTimestamp' => 0, 'oldSize' => 0, 'newSize' => $size, 'pageStatus' => 'created'); DeferredUpdates::addCallableUpdate(function () use($rc, $tags) { $rc->save(); if ($rc->mAttribs['rc_patrolled']) { PatrolLog::record($rc, true, $rc->getPerformer()); } if (count($tags)) { ChangeTags::addTags($tags, $rc->mAttribs['rc_id'], $rc->mAttribs['rc_this_oldid'], null, null); } }); return $rc; }
/** * Attempt submission (no UI) * * @param array $result Array to add statuses to, currently with the * possible keys: * - spam (string): Spam string from content if any spam is detected by * matchSpamRegex. * - sectionanchor (string): Section anchor for a section save. * - nullEdit (boolean): Set if doEditContent is OK. True if null edit, * false otherwise. * - redirect (bool): Set if doEditContent is OK. True if resulting * revision is a redirect. * @param bool $bot True if edit is being made under the bot right. * * @return Status Status object, possibly with a message, but always with * one of the AS_* constants in $status->value, * * @todo FIXME: This interface is TERRIBLE, but hard to get rid of due to * various error display idiosyncrasies. There are also lots of cases * where error metadata is set in the object and retrieved later instead * of being returned, e.g. AS_CONTENT_TOO_BIG and * AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some * time. */ function internalAttemptSave(&$result, $bot = false) { global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize; global $wgContentHandlerUseDB; $status = Status::newGood(); if (!Hooks::run('EditPage::attemptSave', array($this))) { wfDebug("Hook 'EditPage::attemptSave' aborted article saving\n"); $status->fatal('hookaborted'); $status->value = self::AS_HOOK_ERROR; return $status; } $spam = $wgRequest->getText('wpAntispam'); if ($spam !== '') { wfDebugLog('SimpleAntiSpam', $wgUser->getName() . ' editing "' . $this->mTitle->getPrefixedText() . '" submitted bogus field "' . $spam . '"'); $status->fatal('spamprotectionmatch', false); $status->value = self::AS_SPAM_ERROR; return $status; } try { # Construct Content object $textbox_content = $this->toEditContent($this->textbox1); } catch (MWContentSerializationException $ex) { $status->fatal('content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage()); $status->value = self::AS_PARSE_ERROR; return $status; } # Check image redirect if ($this->mTitle->getNamespace() == NS_FILE && $textbox_content->isRedirect() && !$wgUser->isAllowed('upload')) { $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; $status->setResult(false, $code); return $status; } # Check for spam $match = self::matchSummarySpamRegex($this->summary); if ($match === false && $this->section == 'new') { # $wgSpamRegex is enforced on this new heading/summary because, unlike # regular summaries, it is added to the actual wikitext. if ($this->sectiontitle !== '') { # This branch is taken when the API is used with the 'sectiontitle' parameter. $match = self::matchSpamRegex($this->sectiontitle); } else { # This branch is taken when the "Add Topic" user interface is used, or the API # is used with the 'summary' parameter. $match = self::matchSpamRegex($this->summary); } } if ($match === false) { $match = self::matchSpamRegex($this->textbox1); } if ($match !== false) { $result['spam'] = $match; $ip = $wgRequest->getIP(); $pdbk = $this->mTitle->getPrefixedDBkey(); $match = str_replace("\n", '', $match); wfDebugLog('SpamRegex', "{$ip} spam regex hit [[{$pdbk}]]: \"{$match}\""); $status->fatal('spamprotectionmatch', $match); $status->value = self::AS_SPAM_ERROR; return $status; } if (!Hooks::run('EditFilter', array($this, $this->textbox1, $this->section, &$this->hookError, $this->summary))) { # Error messages etc. could be handled within the hook... $status->fatal('hookaborted'); $status->value = self::AS_HOOK_ERROR; return $status; } elseif ($this->hookError != '') { # ...or the hook could be expecting us to produce an error $status->fatal('hookaborted'); $status->value = self::AS_HOOK_ERROR_EXPECTED; return $status; } if ($wgUser->isBlockedFrom($this->mTitle, false)) { // Auto-block user's IP if the account was "hard" blocked $wgUser->spreadAnyEditBlock(); # Check block state against master, thus 'false'. $status->setResult(false, self::AS_BLOCKED_PAGE_FOR_USER); return $status; } $this->kblength = (int) (strlen($this->textbox1) / 1024); if ($this->kblength > $wgMaxArticleSize) { // Error will be displayed by showEditForm() $this->tooBig = true; $status->setResult(false, self::AS_CONTENT_TOO_BIG); return $status; } if (!$wgUser->isAllowed('edit')) { if ($wgUser->isAnon()) { $status->setResult(false, self::AS_READ_ONLY_PAGE_ANON); return $status; } else { $status->fatal('readonlytext'); $status->value = self::AS_READ_ONLY_PAGE_LOGGED; return $status; } } $changingContentModel = false; if ($this->contentModel !== $this->mTitle->getContentModel()) { if (!$wgContentHandlerUseDB) { $status->fatal('editpage-cannot-use-custom-model'); $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL; return $status; } elseif (!$wgUser->isAllowed('editcontentmodel')) { $status->setResult(false, self::AS_NO_CHANGE_CONTENT_MODEL); return $status; } $changingContentModel = true; $oldContentModel = $this->mTitle->getContentModel(); } if ($this->changeTags) { $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange($this->changeTags, $wgUser); if (!$changeTagsStatus->isOK()) { $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR; return $changeTagsStatus; } } if (wfReadOnly()) { $status->fatal('readonlytext'); $status->value = self::AS_READ_ONLY_PAGE; return $status; } if ($wgUser->pingLimiter() || $wgUser->pingLimiter('linkpurge', 0)) { $status->fatal('actionthrottledtext'); $status->value = self::AS_RATE_LIMITED; return $status; } # If the article has been deleted while editing, don't save it without # confirmation if ($this->wasDeletedSinceLastEdit() && !$this->recreate) { $status->setResult(false, self::AS_ARTICLE_WAS_DELETED); return $status; } # Load the page data from the master. If anything changes in the meantime, # we detect it by using page_latest like a token in a 1 try compare-and-swap. $this->mArticle->loadPageData('fromdbmaster'); $new = !$this->mArticle->exists(); if ($new) { // Late check for create permission, just in case *PARANOIA* if (!$this->mTitle->userCan('create', $wgUser)) { $status->fatal('nocreatetext'); $status->value = self::AS_NO_CREATE_PERMISSION; wfDebug(__METHOD__ . ": no create permission\n"); return $status; } // Don't save a new page if it's blank or if it's a MediaWiki: // message with content equivalent to default (allow empty pages // in this case to disable messages, see bug 50124) $defaultMessageText = $this->mTitle->getDefaultMessageText(); if ($this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false) { $defaultText = $defaultMessageText; } else { $defaultText = ''; } if (!$this->allowBlankArticle && $this->textbox1 === $defaultText) { $this->blankArticle = true; $status->fatal('blankarticle'); $status->setResult(false, self::AS_BLANK_ARTICLE); return $status; } if (!$this->runPostMergeFilters($textbox_content, $status, $wgUser)) { return $status; } $content = $textbox_content; $result['sectionanchor'] = ''; if ($this->section == 'new') { if ($this->sectiontitle !== '') { // Insert the section title above the content. $content = $content->addSectionHeader($this->sectiontitle); } elseif ($this->summary !== '') { // Insert the section title above the content. $content = $content->addSectionHeader($this->summary); } $this->summary = $this->newSectionSummary($result['sectionanchor']); } $status->value = self::AS_SUCCESS_NEW_ARTICLE; } else { # not $new # Article exists. Check for edit conflict. $this->mArticle->clear(); # Force reload of dates, etc. $timestamp = $this->mArticle->getTimestamp(); wfDebug("timestamp: {$timestamp}, edittime: {$this->edittime}\n"); if ($timestamp != $this->edittime) { $this->isConflict = true; if ($this->section == 'new') { if ($this->mArticle->getUserText() == $wgUser->getName() && $this->mArticle->getComment() == $this->newSectionSummary()) { // Probably a duplicate submission of a new comment. // This can happen when squid resends a request after // a timeout but the first one actually went through. wfDebug(__METHOD__ . ": duplicate new section submission; trigger edit conflict!\n"); } else { // New comment; suppress conflict. $this->isConflict = false; wfDebug(__METHOD__ . ": conflict suppressed; new section\n"); } } elseif ($this->section == '' && Revision::userWasLastToEdit(DB_MASTER, $this->mTitle->getArticleID(), $wgUser->getId(), $this->edittime)) { # Suppress edit conflict with self, except for section edits where merging is required. wfDebug(__METHOD__ . ": Suppressing edit conflict, same user.\n"); $this->isConflict = false; } } // If sectiontitle is set, use it, otherwise use the summary as the section title. if ($this->sectiontitle !== '') { $sectionTitle = $this->sectiontitle; } else { $sectionTitle = $this->summary; } $content = null; if ($this->isConflict) { wfDebug(__METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'" . " (article time '{$timestamp}')\n"); $content = $this->mArticle->replaceSectionContent($this->section, $textbox_content, $sectionTitle, $this->edittime); } else { wfDebug(__METHOD__ . ": getting section '{$this->section}'\n"); $content = $this->mArticle->replaceSectionContent($this->section, $textbox_content, $sectionTitle); } if (is_null($content)) { wfDebug(__METHOD__ . ": activating conflict; section replace failed.\n"); $this->isConflict = true; $content = $textbox_content; // do not try to merge here! } elseif ($this->isConflict) { # Attempt merge if ($this->mergeChangesIntoContent($content)) { // Successful merge! Maybe we should tell the user the good news? $this->isConflict = false; wfDebug(__METHOD__ . ": Suppressing edit conflict, successful merge.\n"); } else { $this->section = ''; $this->textbox1 = ContentHandler::getContentText($content); wfDebug(__METHOD__ . ": Keeping edit conflict, failed merge.\n"); } } if ($this->isConflict) { $status->setResult(false, self::AS_CONFLICT_DETECTED); return $status; } if (!$this->runPostMergeFilters($content, $status, $wgUser)) { return $status; } if ($this->section == 'new') { // Handle the user preference to force summaries here if (!$this->allowBlankSummary && trim($this->summary) == '') { $this->missingSummary = true; $status->fatal('missingsummary'); // or 'missingcommentheader' if $section == 'new'. Blegh $status->value = self::AS_SUMMARY_NEEDED; return $status; } // Do not allow the user to post an empty comment if ($this->textbox1 == '') { $this->missingComment = true; $status->fatal('missingcommenttext'); $status->value = self::AS_TEXTBOX_EMPTY; return $status; } } elseif (!$this->allowBlankSummary && !$content->equals($this->getOriginalContent($wgUser)) && !$content->isRedirect() && md5($this->summary) == $this->autoSumm) { $this->missingSummary = true; $status->fatal('missingsummary'); $status->value = self::AS_SUMMARY_NEEDED; return $status; } # All's well $sectionanchor = ''; if ($this->section == 'new') { $this->summary = $this->newSectionSummary($sectionanchor); } 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 && strlen($matches[2]) > 0) { $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText($matches[2]); } } $result['sectionanchor'] = $sectionanchor; // Save errors may fall down to the edit form, but we've now // merged the section into full text. Clear the section field // so that later submission of conflict forms won't try to // replace that into a duplicated mess. $this->textbox1 = $this->toEditText($content); $this->section = ''; $status->value = self::AS_SUCCESS_UPDATE; } if (!$this->allowSelfRedirect && $content->isRedirect() && $content->getRedirectTarget()->equals($this->getTitle())) { // If the page already redirects to itself, don't warn. $currentTarget = $this->getCurrentContent()->getRedirectTarget(); if (!$currentTarget || !$currentTarget->equals($this->getTitle())) { $this->selfRedirect = true; $status->fatal('selfredirect'); $status->value = self::AS_SELF_REDIRECT; return $status; } } // Check for length errors again now that the section is merged in $this->kblength = (int) (strlen($this->toEditText($content)) / 1024); if ($this->kblength > $wgMaxArticleSize) { $this->tooBig = true; $status->setResult(false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED); return $status; } $flags = EDIT_AUTOSUMMARY | ($new ? EDIT_NEW : EDIT_UPDATE) | ($this->minoredit && !$this->isNew ? EDIT_MINOR : 0) | ($bot ? EDIT_FORCE_BOT : 0); $doEditStatus = $this->mArticle->doEditContent($content, $this->summary, $flags, false, $wgUser, $content->getDefaultFormat()); if (!$doEditStatus->isOK()) { // Failure from doEdit() // Show the edit conflict page for certain recognized errors from doEdit(), // but don't show it for errors from extension hooks $errors = $doEditStatus->getErrorsArray(); if (in_array($errors[0][0], array('edit-gone-missing', 'edit-conflict', 'edit-already-exists'))) { $this->isConflict = true; // Destroys data doEdit() put in $status->value but who cares $doEditStatus->value = self::AS_END; } return $doEditStatus; } $result['nullEdit'] = $doEditStatus->hasMessage('edit-no-change'); if ($result['nullEdit']) { // We don't know if it was a null edit until now, so increment here $wgUser->pingLimiter('linkpurge'); } $result['redirect'] = $content->isRedirect(); $this->updateWatchlist(); if ($this->changeTags && isset($doEditStatus->value['revision'])) { // If a revision was created, apply any change tags that were requested $addTags = $this->changeTags; $revId = $doEditStatus->value['revision']->getId(); // Defer this both for performance and so that addTags() sees the rc_id // since the recentchange entry addition is deferred first (bug T100248) DeferredUpdates::addCallableUpdate(function () use($addTags, $revId) { ChangeTags::addTags($addTags, null, $revId); }); } // If the content model changed, add a log entry if ($changingContentModel) { $this->addContentModelChangeLogEntry($wgUser, $oldContentModel, $this->contentModel, $this->summary); } return $status; }
protected function saveTags() { $revId = $this->getTitle()->getLatestRevID(); $reason = $this->reason; $user = $this->getUser(); $tags = array(); // Is this a major change, or just a secondary change? Mark both for major if ($this->hasArabicLangLink()) { $tags[] = MarkMajorChanges::getSecondaryTagName(); } if (!$this->isSecondaryChange) { $tags[] = MarkMajorChanges::getMainTagName(); } // Should we use DeferredUpdates::addCallableUpdate? $status = ChangeTags::addTags($tags, null, $revId); if ($status === true) { $this->logTagAdded($tags, $revId, $user, $reason); //$this->getTitle()->isMajorChange == true; return true; } return false; }
/** * Extracts the title and reason from the request parameters and invokes * the local delete() function with these as arguments. It does not make use of * the delete function specified by Article.php. If the deletion succeeds, the * details of the article deleted and the reason for deletion are added to the * result object. */ public function execute() { $this->useTransactionalTimeLimit(); $params = $this->extractRequestParams(); $pageObj = $this->getTitleOrPageId($params, 'fromdbmaster'); if (!$pageObj->exists()) { $this->dieUsageMsg('notanarticle'); } $titleObj = $pageObj->getTitle(); $reason = $params['reason']; $user = $this->getUser(); // Check that the user is allowed to carry out the deletion $errors = $titleObj->getUserPermissionsErrors('delete', $user); if (count($errors)) { $this->dieUsageMsg($errors[0]); } // If change tagging was requested, check that the user is allowed to tag, // and the tags are valid if (count($params['tags'])) { $tagStatus = ChangeTags::canAddTagsAccompanyingChange($params['tags'], $user); if (!$tagStatus->isOK()) { $this->dieStatus($tagStatus); } } if ($titleObj->getNamespace() == NS_FILE) { $status = self::deleteFile($pageObj, $user, $params['oldimage'], $reason, false); } else { $status = self::delete($pageObj, $user, $reason); } if (is_array($status)) { $this->dieUsageMsg($status[0]); } if (!$status->isGood()) { $this->dieStatus($status); } // Deprecated parameters if ($params['watch']) { $watch = 'watch'; } elseif ($params['unwatch']) { $watch = 'unwatch'; } else { $watch = $params['watchlist']; } $this->setWatch($watch, $titleObj, 'watchdeletion'); // Apply change tags to the log entry, if requested if (count($params['tags'])) { ChangeTags::addTags($params['tags'], null, null, $status->value, null); } $r = array('title' => $titleObj->getPrefixedText(), 'reason' => $reason, 'logid' => $status->value); $this->getResult()->addValue(null, $this->getModuleName(), $r); }
/** * Called when an edit is saved * Adds 'visualeditor-switched' tag to the edit if requested * * @param $article WikiPage * @param $user User * @param $content Content * @param $summary string * @param $isMinor boolean * @param $isWatch boolean * @param $section int * @param $flags int * @param $revision Revision|null * @param $status Status * @param $baseRevId int|boolean * @return boolean true */ public static function onPageContentSaveComplete($article, $user, $content, $summary, $isMinor, $isWatch, $section, $flags, $revision, $status, $baseRevId) { $request = RequestContext::getMain()->getRequest(); if ($request->getBool('veswitched') && $revision) { ChangeTags::addTags('visualeditor-switched', null, $revision->getId()); } return true; }
* Thiswas sourced from TranslateWiki (https://github.com/wikimedia/translatewiki/blob/0b7ba4665ab3e599516d2c2774e2541495a9d7a4/TranslatewikiSettings.php#L478) */ $wgHooks['GetBetaFeaturePreferences'][] = function ($user, &$prefs) { $prefs['hhvm-beta'] = array('label-message' => 'hhvm-beta-label', 'desc-message' => 'hhvm-beta-desc', 'info-link' => 'http://www.hhvm.com/', 'discussion-link' => 'https://meta.orain.org/wiki/Forum:HHVM'); }; $wgHooks['BeforePageDisplay'][] = function (OutputPage $out) { $req = $out->getRequest(); $user = $out->getUser(); $hasCookie = $req->getCookie('hhvm', ''); $wantsCookie = BetaFeatures::isFeatureEnabled($user, 'hhvm-beta'); if (!$hasCookie && $wantsCookie) { $req->response()->setcookie('hhvm', '1', 0, array('prefix' => '')); } if ($hasCookie && !$wantsCookie) { $req->response()->setcookie('hhvm', '0', -1, array('prefix' => '')); } }; $wgHooks['RecentChange_save'][] = function (RecentChange $rc) { if (wfIsHHVM()) { ChangeTags::addTags('HHVM', $rc->getAttribute('rc_id')); } return true; }; $wgHooks['LocalisationCacheRecache'][] = function ($cache, $code, &$cachedData) { if ($code === 'en') { $cachedData['messages']['hhvm-beta-label'] = 'HHVM'; $cachedData['messages']['hhvm-beta-desc'] = 'HHVM is a new and faster PHP runtime.'; $cachedData['messages']['tag-HHVM'] = '[[m:Forum:HHVM|HHVM]]'; $cachedData['messages']['tag-HHVM-description'] = 'Changes made using HHVM. These are tagged for analysis purposes'; } };
/** * 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()); } }
/** * Adds tag to specified revision ID * * @param $revisionId integer * @param $tag string */ protected static function AddRevisionTag($revisionId, $tag) { if (!ChangeTags::addTags($tag, null, $revisionId)) { \Wikia\Logger\WikiaLogger::instance()->error('Failed to add tag to revision', ['revisionId' => $revisionId, 'tag' => $tag]); } }
/** * Called when an edit is saved * Adds 'visualeditor-switched' tag to the edit if requested * * @param RecentChange $rc * @return boolean true */ public static function onRecentChange_save(RecentChange $rc) { $request = RequestContext::getMain()->getRequest(); if ($request->getBool('veswitched') && $rc->mAttribs['rc_this_oldid']) { ChangeTags::addTags('visualeditor-switched', $rc->mAttribs['rc_id'], $rc->mAttribs['rc_this_oldid']); } return true; }
private function setTags($titlesAndTagInfo, $tag, $userId, $rlId, $comment, $setPatrolled = false) { // XXX Attach a hook to delete tags from the collabwatchlistrevisiontag table as soon as the actual tags are deleted from the change_tags table $allowedTagsAndInfo = $this->getCollabWatchlistTags($rlId); if (!array_key_exists($tag, $allowedTagsAndInfo)) { return false; } $dbw = wfGetDB(DB_MASTER); foreach ($titlesAndTagInfo as $title => $infos) { $rcIds = array(); // Add entries for the tag to the change_tags table // optionally mark edit as patrolled foreach ($infos as $infoKey => $info) { ChangeTags::addTags($tag, $info['rc_id'], $info['rev_id']); $rcIds[] = $info['rc_id']; if ($setPatrolled) { RecentChange::markPatrolled($info['rc_id']); } } // Add the tagged revisions to the collaborative watchlist $sql = 'INSERT IGNORE INTO collabwatchlistrevisiontag (ct_rc_id, ct_tag, cw_id, user_id, rrt_comment) SELECT ct_rc_id, ct_tag, ' . $dbw->strencode($rlId) . ',' . $dbw->strencode($userId) . ',' . $dbw->addQuotes($comment) . ' FROM change_tag WHERE ct_tag = ? AND ct_rc_id '; if (count($rcIds) > 1) { $sql .= 'IN (' . $dbw->makeList($rcIds) . ')'; $params = array($tag); } else { $sql .= '= ?'; $params = array($tag, $rcIds[0]); } $prepSql = $dbw->prepare($sql); $res = $dbw->execute($prepSql, $params); $dbw->freePrepared($prepSql); return true; } }
/** * Record a file upload in the upload log and the image table * @param string $oldver * @param string $comment * @param string $pageText * @param bool|array $props * @param string|bool $timestamp * @param null|User $user * @param string[] $tags * @return bool */ function recordUpload2($oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = array()) { if (is_null($user)) { global $wgUser; $user = $wgUser; } $dbw = $this->repo->getMasterDB(); # Imports or such might force a certain timestamp; otherwise we generate # it and can fudge it slightly to keep (name,timestamp) unique on re-upload. if ($timestamp === false) { $timestamp = $dbw->timestamp(); $allowTimeKludge = true; } else { $allowTimeKludge = false; } $props = $props ?: $this->repo->getFileProps($this->getVirtualUrl()); $props['description'] = $comment; $props['user'] = $user->getId(); $props['user_text'] = $user->getName(); $props['timestamp'] = wfTimestamp(TS_MW, $timestamp); // DB -> TS_MW $this->setProps($props); # Fail now if the file isn't there if (!$this->fileExists) { wfDebug(__METHOD__ . ": File " . $this->getRel() . " went missing!\n"); return false; } $dbw->startAtomic(__METHOD__); # Test to see if the row exists using INSERT IGNORE # This avoids race conditions by locking the row until the commit, and also # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition. $dbw->insert('image', array('img_name' => $this->getName(), 'img_size' => $this->size, 'img_width' => intval($this->width), 'img_height' => intval($this->height), 'img_bits' => $this->bits, 'img_media_type' => $this->media_type, 'img_major_mime' => $this->major_mime, 'img_minor_mime' => $this->minor_mime, 'img_timestamp' => $timestamp, 'img_description' => $comment, 'img_user' => $user->getId(), 'img_user_text' => $user->getName(), 'img_metadata' => $dbw->encodeBlob($this->metadata), 'img_sha1' => $this->sha1), __METHOD__, 'IGNORE'); $reupload = $dbw->affectedRows() == 0; if ($reupload) { if ($allowTimeKludge) { # Use LOCK IN SHARE MODE to ignore any transaction snapshotting $ltimestamp = $dbw->selectField('image', 'img_timestamp', array('img_name' => $this->getName()), __METHOD__, array('LOCK IN SHARE MODE')); $lUnixtime = $ltimestamp ? wfTimestamp(TS_UNIX, $ltimestamp) : false; # Avoid a timestamp that is not newer than the last version # TODO: the image/oldimage tables should be like page/revision with an ID field if ($lUnixtime && wfTimestamp(TS_UNIX, $timestamp) <= $lUnixtime) { sleep(1); // fast enough re-uploads would go far in the future otherwise $timestamp = $dbw->timestamp($lUnixtime + 1); $this->timestamp = wfTimestamp(TS_MW, $timestamp); // DB -> TS_MW } } # (bug 34993) Note: $oldver can be empty here, if the previous # version of the file was broken. Allow registration of the new # version to continue anyway, because that's better than having # an image that's not fixable by user operations. # Collision, this is an update of a file # Insert previous contents into oldimage $dbw->insertSelect('oldimage', 'image', array('oi_name' => 'img_name', 'oi_archive_name' => $dbw->addQuotes($oldver), 'oi_size' => 'img_size', 'oi_width' => 'img_width', 'oi_height' => 'img_height', 'oi_bits' => 'img_bits', 'oi_timestamp' => 'img_timestamp', 'oi_description' => 'img_description', 'oi_user' => 'img_user', 'oi_user_text' => 'img_user_text', 'oi_metadata' => 'img_metadata', 'oi_media_type' => 'img_media_type', 'oi_major_mime' => 'img_major_mime', 'oi_minor_mime' => 'img_minor_mime', 'oi_sha1' => 'img_sha1'), array('img_name' => $this->getName()), __METHOD__); # Update the current image row $dbw->update('image', array('img_size' => $this->size, 'img_width' => intval($this->width), 'img_height' => intval($this->height), 'img_bits' => $this->bits, 'img_media_type' => $this->media_type, 'img_major_mime' => $this->major_mime, 'img_minor_mime' => $this->minor_mime, 'img_timestamp' => $timestamp, 'img_description' => $comment, 'img_user' => $user->getId(), 'img_user_text' => $user->getName(), 'img_metadata' => $dbw->encodeBlob($this->metadata), 'img_sha1' => $this->sha1), array('img_name' => $this->getName()), __METHOD__); } $descTitle = $this->getTitle(); $descId = $descTitle->getArticleID(); $wikiPage = new WikiFilePage($descTitle); $wikiPage->setFile($this); // Add the log entry... $logEntry = new ManualLogEntry('upload', $reupload ? 'overwrite' : 'upload'); $logEntry->setTimestamp($this->timestamp); $logEntry->setPerformer($user); $logEntry->setComment($comment); $logEntry->setTarget($descTitle); // Allow people using the api to associate log entries with the upload. // Log has a timestamp, but sometimes different from upload timestamp. $logEntry->setParameters(array('img_sha1' => $this->sha1, 'img_timestamp' => $timestamp)); // Note we keep $logId around since during new image // creation, page doesn't exist yet, so log_page = 0 // but we want it to point to the page we're making, // so we later modify the log entry. // For a similar reason, we avoid making an RC entry // now and wait until the page exists. $logId = $logEntry->insert(); if ($descTitle->exists()) { // Use own context to get the action text in content language $formatter = LogFormatter::newFromEntry($logEntry); $formatter->setContext(RequestContext::newExtraneousContext($descTitle)); $editSummary = $formatter->getPlainActionText(); $nullRevision = Revision::newNullRevision($dbw, $descId, $editSummary, false, $user); if ($nullRevision) { $nullRevision->insertOn($dbw); Hooks::run('NewRevisionFromEditComplete', array($wikiPage, $nullRevision, $nullRevision->getParentId(), $user)); $wikiPage->updateRevisionOn($dbw, $nullRevision); // Associate null revision id $logEntry->setAssociatedRevId($nullRevision->getId()); } $newPageContent = null; } else { // Make the description page and RC log entry post-commit $newPageContent = ContentHandler::makeContent($pageText, $descTitle); } # Defer purges, page creation, and link updates in case they error out. # The most important thing is that files and the DB registry stay synced. $dbw->endAtomic(__METHOD__); # Do some cache purges after final commit so that: # a) Changes are more likely to be seen post-purge # b) They won't cause rollback of the log publish/update above $that = $this; $dbw->onTransactionIdle(function () use($that, $reupload, $wikiPage, $newPageContent, $comment, $user, $logEntry, $logId, $descId, $tags) { # Update memcache after the commit $that->invalidateCache(); $updateLogPage = false; if ($newPageContent) { # New file page; create the description page. # There's already a log entry, so don't make a second RC entry # CDN and file cache for the description page are purged by doEditContent. $status = $wikiPage->doEditContent($newPageContent, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user); if (isset($status->value['revision'])) { // Associate new page revision id $logEntry->setAssociatedRevId($status->value['revision']->getId()); } // This relies on the resetArticleID() call in WikiPage::insertOn(), // which is triggered on $descTitle by doEditContent() above. if (isset($status->value['revision'])) { /** @var $rev Revision */ $rev = $status->value['revision']; $updateLogPage = $rev->getPage(); } } else { # Existing file page: invalidate description page cache $wikiPage->getTitle()->invalidateCache(); $wikiPage->getTitle()->purgeSquid(); # Allow the new file version to be patrolled from the page footer Article::purgePatrolFooterCache($descId); } # Update associated rev id. This should be done by $logEntry->insert() earlier, # but setAssociatedRevId() wasn't called at that point yet... $logParams = $logEntry->getParameters(); $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId(); $update = array('log_params' => LogEntryBase::makeParamBlob($logParams)); if ($updateLogPage) { # Also log page, in case where we just created it above $update['log_page'] = $updateLogPage; } $that->getRepo()->getMasterDB()->update('logging', $update, array('log_id' => $logId), __METHOD__); $that->getRepo()->getMasterDB()->insert('log_search', array('ls_field' => 'associated_rev_id', 'ls_value' => $logEntry->getAssociatedRevId(), 'ls_log_id' => $logId), __METHOD__); # Now that the log entry is up-to-date, make an RC entry. $recentChange = $logEntry->publish($logId); if ($tags) { ChangeTags::addTags($tags, $recentChange ? $recentChange->getAttribute('rc_id') : null, $logEntry->getAssociatedRevId(), $logId); } # Run hook for other updates (typically more cache purging) Hooks::run('FileUpload', array($that, $reupload, !$newPageContent)); if ($reupload) { # Delete old thumbnails $that->purgeThumbnails(); # Remove the old file from the CDN cache DeferredUpdates::addUpdate(new CdnCacheUpdate(array($that->getUrl())), DeferredUpdates::PRESEND); } else { # Update backlink pages pointing to this title if created LinksUpdate::queueRecursiveJobsForTable($that->getTitle(), 'imagelinks'); } }); if (!$reupload) { # This is a new file, so update the image count DeferredUpdates::addUpdate(SiteStatsUpdate::factory(array('images' => 1))); } # Invalidate cache for all pages using this file DeferredUpdates::addUpdate(new HTMLCacheUpdate($this->getTitle(), 'imagelinks')); return true; }
/** * Publish the log entry. * * @param int $newId Id of the log entry. * @param string $to One of: rcandudp (default), rc, udp * @return RecentChange|null */ public function publish($newId, $to = 'rcandudp') { $log = new LogPage($this->getType()); if ($log->isRestricted()) { return null; } $rc = $this->getRecentChange($newId); if ($to === 'rc' || $to === 'rcandudp') { $rc->save('pleasedontudp'); } if ($to === 'udp' || $to === 'rcandudp') { $rc->notifyRCFeeds(); } // Log the autopatrol if the log entry is patrollable if ($this->getIsPatrollable() && $rc->getAttribute('rc_patrolled') === 1) { PatrolLog::record($rc, true, $this->getPerformer()); } // Add change tags to the log entry and (if applicable) the associated revision $tags = $this->getTags(); if (!is_null($tags)) { $rcId = $rc->getAttribute('rc_id'); $revId = $this->getAssociatedRevId(); // Use null if $revId is 0 ChangeTags::addTags($tags, $rcId, $revId > 0 ? $revId : null, $newId); } return $rc; }
public static function onRecentChangeSave( $recentChange ) { global $wgTorTagChanges; if ( class_exists('ChangeTags') && $wgTorTagChanges && self::isExitNode() ) { ChangeTags::addTags( 'tor', $recentChange->mAttribs['rc_id'], $recentChange->mAttribs['rc_this_oldid'], $recentChange->mAttribs['rc_logid'] ); } return true; }
/** * @param $recentChange RecentChange * @return bool */ public static function onRecentChangeSave($recentChange) { $title = Title::makeTitle($recentChange->getAttribute('rc_namespace'), $recentChange->getAttribute('rc_title')); $action = $recentChange->mAttribs['rc_log_type'] ? $recentChange->mAttribs['rc_log_type'] : 'edit'; $actionID = implode('-', array($title->getPrefixedText(), $recentChange->mAttribs['rc_user_text'], $action)); if (!empty(AbuseFilter::$tagsToSet[$actionID]) && count($tags = AbuseFilter::$tagsToSet[$actionID])) { ChangeTags::addTags($tags, $recentChange->mAttribs['rc_id'], $recentChange->mAttribs['rc_this_oldid'], $recentChange->mAttribs['rc_logid']); } return true; }
/** * RecentChange_save hook handler that tags mobile changes * @see https://www.mediawiki.org/wiki/Manual:Hooks/RecentChange_save * * @param RecentChange $rc * @return bool */ public static function onRecentChange_save(RecentChange $rc) { $context = MobileContext::singleton(); $userAgent = $context->getRequest()->getHeader("User-agent"); $logType = $rc->getAttribute('rc_log_type'); // Only log edits and uploads if ($context->shouldDisplayMobileView() && ($logType === 'upload' || is_null($logType))) { $rcId = $rc->getAttribute('rc_id'); $revId = $rc->getAttribute('rc_this_oldid'); $logId = $rc->getAttribute('rc_logid'); ChangeTags::addTags('mobile edit', $rcId, $revId, $logId); // Tag as mobile web edit specifically, if it isn't coming from the apps if (strpos($userAgent, 'WikipediaApp/') !== 0) { ChangeTags::addTags('mobile web edit', $rcId, $revId, $logId); } } return true; }
public function publish() { $params = $this->extractRequestParams(); $user = $this->getUser(); $targetTitle = ContentTranslation\SiteMapper::getTargetTitle($params['title'], $user->getName()); $title = Title::newFromText($targetTitle); if (!$title) { $this->dieUsageMsg('invalidtitle', $params['title']); } try { $wikitext = $this->convertHtmlToWikitext($title, $params['html']); } catch (MWException $e) { $this->dieUsage($e->getMessage(), 'parsoidserver'); } $saveresult = $this->saveWikitext($title, $wikitext, $params); $editStatus = $saveresult['edit']['result']; if ($editStatus === 'Success') { if (isset($saveresult['edit']['newrevid'])) { // Add the tags post-send, after RC row insertion $revId = intval($saveresult['edit']['newrevid']); DeferredUpdates::addCallableUpdate(function () use($revId, $params) { ChangeTags::addTags('contenttranslation', null, $revId, null, FormatJson::encode(array('from' => $params['from'], 'to' => $params['to']))); }); } $result = array('result' => 'success'); if (isset($saveresult['edit']['newrevid'])) { $result['newrevid'] = intval($saveresult['edit']['newrevid']); } $this->saveTranslationHistory($params); // Notify user about milestones $this->notifyTranslator(); } else { $result = array('result' => 'error', 'edit' => $saveresult['edit']); } $this->getResult()->addValue(null, $this->getModuleName(), $result); }