/** * @param $vars AbuseFilterVariableHolder * @return AFPData|array|int|mixed|null|string * @throws MWException * @throws AFPException */ function compute($vars) { $parameters = $this->mParameters; $result = null; if (!wfRunHooks('AbuseFilter-interceptVariable', array($this->mMethod, $vars, $parameters, &$result))) { return $result instanceof AFPData ? $result : AFPData::newFromPHPVar($result); } switch ($this->mMethod) { case 'diff': $text1Var = $parameters['oldtext-var']; $text2Var = $parameters['newtext-var']; $text1 = $vars->getVar($text1Var)->toString() . "\n"; $text2 = $vars->getVar($text2Var)->toString() . "\n"; $result = wfDiff($text1, $text2); break; case 'diff-split': $diff = $vars->getVar($parameters['diff-var'])->toString(); $line_prefix = $parameters['line-prefix']; $diff_lines = explode("\n", $diff); $interest_lines = array(); foreach ($diff_lines as $line) { if (substr($line, 0, 1) === $line_prefix) { $interest_lines[] = substr($line, strlen($line_prefix)); } } $result = $interest_lines; break; case 'links-from-wikitext': // This should ONLY be used when sharing a parse operation with the edit. /* @var WikiPage $article */ $article = $parameters['article']; if ($article !== null && (!defined('MW_SUPPORTS_CONTENTHANDLER') || $article->getContentModel() === CONTENT_MODEL_WIKITEXT)) { $textVar = $parameters['text-var']; // XXX: Use prepareContentForEdit. But we need a Content object for that. $new_text = $vars->getVar($textVar)->toString(); $content = ContentHandler::makeContent($new_text, $article->getTitle()); $editInfo = $article->prepareContentForEdit($content); $links = array_keys($editInfo->output->getExternalLinks()); $result = $links; break; } // Otherwise fall back to database // Otherwise fall back to database case 'links-from-wikitext-nonedit': case 'links-from-wikitext-or-database': // TODO: use Content object instead, if available! In any case, use WikiPage, not Article. $article = self::articleFromTitle($parameters['namespace'], $parameters['title']); if ($vars->getVar('context')->toString() == 'filter') { $links = $this->getLinksFromDB($article); wfDebug("AbuseFilter: loading old links from DB\n"); } elseif (!defined('MW_SUPPORTS_CONTENTHANDLER') || $article->getContentModel() === CONTENT_MODEL_WIKITEXT) { wfDebug("AbuseFilter: loading old links from Parser\n"); $textVar = $parameters['text-var']; $wikitext = $vars->getVar($textVar)->toString(); $editInfo = $this->parseNonEditWikitext($wikitext, $article); $links = array_keys($editInfo->output->getExternalLinks()); } else { // TODO: Get links from Content object. But we don't have the content object. // And for non-text content, $wikitext is usually not going to be a valid // serialization, but rather some dummy text for filtering. $links = array(); } $result = $links; break; case 'link-diff-added': case 'link-diff-removed': $oldLinkVar = $parameters['oldlink-var']; $newLinkVar = $parameters['newlink-var']; $oldLinks = $vars->getVar($oldLinkVar)->toString(); $newLinks = $vars->getVar($newLinkVar)->toString(); $oldLinks = explode("\n", $oldLinks); $newLinks = explode("\n", $newLinks); if ($this->mMethod == 'link-diff-added') { $result = array_diff($newLinks, $oldLinks); } if ($this->mMethod == 'link-diff-removed') { $result = array_diff($oldLinks, $newLinks); } break; case 'parse-wikitext': // Should ONLY be used when sharing a parse operation with the edit. $article = $parameters['article']; if ($article !== null && (!defined('MW_SUPPORTS_CONTENTHANDLER') || $article->getContentModel() === CONTENT_MODEL_WIKITEXT)) { $textVar = $parameters['wikitext-var']; // XXX: Use prepareContentForEdit. But we need a Content object for that. $new_text = $vars->getVar($textVar)->toString(); $editInfo = $article->prepareTextForEdit($new_text); if (isset($parameters['pst']) && $parameters['pst']) { $result = $editInfo->pstContent->serialize($editInfo->format); } else { $newHTML = $editInfo->output->getText(); // Kill the PP limit comments. Ideally we'd just remove these by not setting the // parser option, but then we can't share a parse operation with the edit, which is bad. $result = preg_replace('/<!--\\s*NewPP limit report[^>]*-->\\s*$/si', '', $newHTML); } break; } // Otherwise fall back to database // Otherwise fall back to database case 'parse-wikitext-nonedit': // TODO: use Content object instead, if available! In any case, use WikiPage, not Article. $article = self::articleFromTitle($parameters['namespace'], $parameters['title']); $textVar = $parameters['wikitext-var']; if (!defined('MW_SUPPORTS_CONTENTHANDLER') || $article->getContentModel() === CONTENT_MODEL_WIKITEXT) { if (isset($parameters['pst']) && $parameters['pst']) { // $textVar is already PSTed when it's not loaded from an ongoing edit. $result = $vars->getVar($textVar)->toString(); } else { $text = $vars->getVar($textVar)->toString(); $editInfo = $this->parseNonEditWikitext($text, $article); $result = $editInfo->output->getText(); } } else { // TODO: Parser Output from Content object. But we don't have the content object. // And for non-text content, $wikitext is usually not going to be a valid // serialization, but rather some dummy text for filtering. $result = ''; } break; case 'strip-html': $htmlVar = $parameters['html-var']; $html = $vars->getVar($htmlVar)->toString(); $result = StringUtils::delimiterReplace('<', '>', '', $html); break; case 'load-recent-authors': $cutOff = $parameters['cutoff']; $title = Title::makeTitle($parameters['namespace'], $parameters['title']); if (!$title->exists()) { $result = ''; break; } $dbr = wfGetDB(DB_SLAVE); $res = $dbr->select('revision', 'DISTINCT rev_user_text', array('rev_page' => $title->getArticleID(), 'rev_timestamp<' . $dbr->addQuotes($dbr->timestamp($cutOff))), __METHOD__, array('ORDER BY' => 'rev_timestamp DESC', 'LIMIT' => 10)); $users = array(); foreach ($res as $row) { $users[] = $row->rev_user_text; } $result = $users; break; case 'get-page-restrictions': $action = $parameters['action']; $title = Title::makeTitle($parameters['namespace'], $parameters['title']); $rights = $title->getRestrictions($action); $rights = count($rights) ? $rights : array(); $result = $rights; break; case 'simple-user-accessor': $user = $parameters['user']; $method = $parameters['method']; if (!$user) { throw new MWException('No user parameter given.'); } $obj = self::getUserObject($user); if (!$obj) { throw new MWException("Invalid username {$user}"); } $result = call_user_func(array($obj, $method)); break; case 'user-age': $user = $parameters['user']; $asOf = $parameters['asof']; $obj = self::getUserObject($user); if ($obj->getId() == 0) { $result = 0; break; } $registration = $obj->getRegistration(); $result = wfTimestamp(TS_UNIX, $asOf) - wfTimestampOrNull(TS_UNIX, $registration); break; case 'user-groups': // Deprecated but needed by old log entries $user = $parameters['user']; $obj = self::getUserObject($user); $result = $obj->getEffectiveGroups(); break; case 'length': $s = $vars->getVar($parameters['length-var'])->toString(); $result = strlen($s); break; case 'subtract': $v1 = $vars->getVar($parameters['val1-var'])->toFloat(); $v2 = $vars->getVar($parameters['val2-var'])->toFloat(); $result = $v1 - $v2; break; case 'revision-text-by-id': $rev = Revision::newFromId($parameters['revid']); $result = AbuseFilter::revisionToString($rev); break; case 'revision-text-by-timestamp': $timestamp = $parameters['timestamp']; $title = Title::makeTitle($parameters['namespace'], $parameters['title']); $dbr = wfGetDB(DB_SLAVE); $rev = Revision::loadFromTimestamp($dbr, $title, $timestamp); $result = AbuseFilter::revisionToString($rev); break; default: if (wfRunHooks('AbuseFilter-computeVariable', array($this->mMethod, $vars, $parameters, &$result))) { throw new AFPException('Unknown variable compute type ' . $this->mMethod); } } return $result instanceof AFPData ? $result : AFPData::newFromPHPVar($result); }
/** * Common implementation for the APIEditBeforeSave, EditFilterMerged * and EditFilterMergedContent hooks. * * @param IContextSource $context the context of the edit * @param Content|null $content the new Content generated by the edit * @param string $text new page content (subject of filtering) * @param Status $status Error message to return * @param string $summary Edit summary for page * @param bool $minoredit whether this is a minor edit according to the user. * * @return bool */ public static function filterEdit(IContextSource $context, $content, $text, Status $status, $summary, $minoredit) { // Load vars $vars = new AbuseFilterVariableHolder(); $title = $context->getTitle(); // Some edits are running through multiple hooks, but we only want to filter them once if (isset($title->editAlreadyFiltered)) { return true; } elseif ($title) { $title->editAlreadyFiltered = true; } self::$successful_action_vars = false; self::$last_edit_page = false; $user = $context->getUser(); // Check for null edits. $oldtext = ''; $oldcontent = null; if ($title instanceof Title && $title->canExist() && $title->exists()) { // Make sure we load the latest text saved in database (bug 31656) $page = $context->getWikiPage(); $revision = $page->getRevision(); if (!$revision) { return true; } if (defined('MW_SUPPORTS_CONTENTHANDLER')) { $oldcontent = $revision->getContent(Revision::RAW); $oldtext = AbuseFilter::contentToString($oldcontent); } else { $oldtext = AbuseFilter::revisionToString($revision, Revision::RAW); } // Cache article object so we can share a parse operation $articleCacheKey = $title->getNamespace() . ':' . $title->getText(); AFComputedVariable::$articleCache[$articleCacheKey] = $page; } else { $page = null; } // Don't trigger for null edits. if ($content && $oldcontent && $oldcontent->equals($content)) { // Compare Content objects if available return true; } else { if (strcmp($oldtext, $text) == 0) { // Otherwise, compare strings return true; } } $vars->addHolders(AbuseFilter::generateUserVars($user), AbuseFilter::generateTitleVars($title, 'ARTICLE')); $vars->setVar('action', 'edit'); $vars->setVar('summary', $summary); $vars->setVar('minor_edit', $minoredit); $vars->setVar('old_wikitext', $oldtext); $vars->setVar('new_wikitext', $text); // TODO: set old_content and new_content vars, use them $vars->addHolders(AbuseFilter::getEditVars($title, $page)); $filter_result = AbuseFilter::filterAction($vars, $title); if (!$filter_result->isOK()) { $status->merge($filter_result); return true; // re-show edit form } self::$successful_action_vars = $vars; self::$last_edit_page = $page; return true; }