Use the shared parse on API edit

ConfirmEdit was tripling the API save time, because it was parsing the
entire content twice to evaluate whether the addurl trigger is hit.

While I was here, I stopped using the deprecated non-Content hooks. The
new hook, EditEditFilterMergedContent, does not pass an EditPage object,
which means that Title or WikiPage objects need to be passed around
instead. Also, since EditPage::showEditForm() cannot be called with no
EditPage object, use a EditPage::showEditForm:fields hook instead.

If non-wikitext content is edited, assume that the regex trigger is not
hit.

For further architectural details, see the associated core change:
I4b4270dd868a . MW_EDITFILTERMERGED_SUPPORTS_API is a constant
introduced to detect the presence of the associated core change.

Also, in APIGetAllowedParams, set the allowed parameters even if we are
not on the help screen. This allows API users to submit their CAPTCHA
answer without it failing with an "unrecognized parameter" error.

Compatibility with MediaWiki 1.21 is retained, compatibility before that
is dropped.

Change-Id: I9529b7e8d3fc9301c754b28fda185aa3ab36f13e
This commit is contained in:
Tim Starling 2014-12-05 15:48:13 +11:00
parent d3f89805e7
commit 40c7e9c9aa
3 changed files with 94 additions and 62 deletions

View file

@ -56,9 +56,15 @@ class SimpleCaptcha {
/**
* Insert the captcha prompt into an edit form.
* @param EditPage $editPage
* @param OutputPage $out
*/
function editCallback( &$out ) {
function showEditFormFields( &$editPage, &$out ) {
$page = $editPage->getArticle()->getPage();
if ( !isset( $page->ConfirmEdit_ActivateCaptcha ) ) {
return;
}
unset( $page->ConfirmEdit_ActivateCaptcha );
$out->addWikiText( $this->getMessage( $this->action ) );
$out->addHTML( $this->getForm() );
}
@ -223,29 +229,38 @@ class SimpleCaptcha {
// ----------------------------------
/**
* @param EditPage $editPage
* @param Title $title
* @param string $action (edit/create/addurl...)
* @return bool true if action triggers captcha on editPage's namespace
* @return bool true if action triggers captcha on $title's namespace
*/
function captchaTriggers( &$editPage, $action ) {
function captchaTriggers( $title, $action ) {
global $wgCaptchaTriggers, $wgCaptchaTriggersOnNamespace;
// Special config for this NS?
if ( isset( $wgCaptchaTriggersOnNamespace[$editPage->mTitle->getNamespace()][$action] ) )
return $wgCaptchaTriggersOnNamespace[$editPage->mTitle->getNamespace()][$action];
if ( isset( $wgCaptchaTriggersOnNamespace[$title->getNamespace()][$action] ) )
return $wgCaptchaTriggersOnNamespace[$title->getNamespace()][$action];
return ( !empty( $wgCaptchaTriggers[$action] ) ); // Default
}
/**
* @param $editPage EditPage
* @param $newtext string
* @param WikiPage $page
* @param $content Content|string
* @param $section string
* @param $merged bool
* @param $isContent bool If true, $content is a Content object
* @return bool true if the captcha should run
*/
function shouldCheck( &$editPage, $newtext, $section, $merged = false ) {
function shouldCheck( WikiPage $page, $content, $section, $isContent = false ) {
$title = $page->getTitle();
$this->trigger = '';
$title = $editPage->mArticle->getTitle();
if ( $isContent ) {
if ( $content->getModel() == CONTENT_MODEL_WIKITEXT ) {
$newtext = $content->getNativeData();
} else {
$newtext = null;
}
} else {
$newtext = $content;
}
global $wgUser;
if ( $wgUser->isAllowed( 'skipcaptcha' ) ) {
@ -263,7 +278,7 @@ class SimpleCaptcha {
return false;
}
if ( $this->captchaTriggers( $editPage, 'edit' ) ) {
if ( $this->captchaTriggers( $title, 'edit' ) ) {
// Check on all edits
global $wgUser;
$this->trigger = sprintf( "edit trigger by '%s' at [[%s]]",
@ -274,7 +289,7 @@ class SimpleCaptcha {
return true;
}
if ( $this->captchaTriggers( $editPage, 'create' ) && !$editPage->mTitle->exists() ) {
if ( $this->captchaTriggers( $title, 'create' ) && !$title->exists() ) {
// Check if creating a page
global $wgUser;
$this->trigger = sprintf( "Create trigger by '%s' at [[%s]]",
@ -285,19 +300,23 @@ class SimpleCaptcha {
return true;
}
if ( $this->captchaTriggers( $editPage, 'addurl' ) ) {
if ( $this->captchaTriggers( $title, 'addurl' ) ) {
// Only check edits that add URLs
if ( $merged ) {
if ( $isContent ) {
// Get links from the database
$oldLinks = $this->getLinksFromTracker( $title );
// Share a parse operation with Article::doEdit()
$editInfo = $editPage->mArticle->prepareTextForEdit( $newtext );
$newLinks = array_keys( $editInfo->output->getExternalLinks() );
$editInfo = $page->prepareContentForEdit( $content );
if ( $editInfo->output ) {
$newLinks = array_keys( $editInfo->output->getExternalLinks() );
} else {
$newLinks = array();
}
} else {
// Get link changes in the slowest way known to man
$oldtext = $this->loadText( $editPage, $section );
$oldLinks = $this->findLinks( $editPage, $oldtext );
$newLinks = $this->findLinks( $editPage, $newtext );
$oldtext = $this->loadText( $title, $section );
$oldLinks = $this->findLinks( $title, $oldtext );
$newLinks = $this->findLinks( $title, $newtext );
}
$unknownLinks = array_filter( $newLinks, array( &$this, 'filterLink' ) );
@ -317,9 +336,9 @@ class SimpleCaptcha {
}
global $wgCaptchaRegexes;
if ( $wgCaptchaRegexes ) {
if ( $newtext !== null && $wgCaptchaRegexes ) {
// Custom regex checks. Reuse $oldtext if set above.
$oldtext = isset( $oldtext ) ? $oldtext : $this->loadText( $editPage, $section );
$oldtext = isset( $oldtext ) ? $oldtext : $this->loadText( $title, $section );
foreach ( $wgCaptchaRegexes as $regex ) {
$newMatches = array();
@ -472,13 +491,13 @@ class SimpleCaptcha {
/**
* Backend function for confirmEdit() and confirmEditAPI()
* @param $editPage EditPage
* @param WikiPage $page
* @param $newtext string
* @param $section
* @param $merged bool
* @param $isContent bool
* @return bool false if the CAPTCHA is rejected, true otherwise
*/
private function doConfirmEdit( $editPage, $newtext, $section, $merged = false ) {
private function doConfirmEdit( WikiPage $page, $newtext, $section, $isContent = false ) {
global $wgRequest;
if ( $wgRequest->getVal( 'captchaid' ) ) {
$wgRequest->setVal( 'wpCaptchaId', $wgRequest->getVal( 'captchaid' ) );
@ -486,7 +505,7 @@ class SimpleCaptcha {
if ( $wgRequest->getVal( 'captchaword' ) ) {
$wgRequest->setVal( 'wpCaptchaWord', $wgRequest->getVal( 'captchaword' ) );
}
if ( $this->shouldCheck( $editPage, $newtext, $section, $merged ) ) {
if ( $this->shouldCheck( $page, $newtext, $section, $isContent ) ) {
return $this->passCaptcha();
} else {
wfDebug( "ConfirmEdit: no need to show captcha.\n" );
@ -495,38 +514,39 @@ class SimpleCaptcha {
}
/**
* The main callback run on edit attempts.
* @param EditPage $editPage
* @param string $newtext
* @param string $section
* @param bool $merged
* @return bool true to continue saving, false to abort and show a captcha form
* An efficient edit filter callback based on the text after section merging
* @param RequestContext $context
* @param Content $content
* @param Status $status
* @param $summary
* @param $user
* @param $minorEdit
* @return bool
*/
function confirmEdit( $editPage, $newtext, $section, $merged = false ) {
if ( defined( 'MW_API' ) ) {
function confirmEditMerged( $context, $content, $status, $summary, $user, $minorEdit ) {
$legacyMode = !defined( 'MW_EDITFILTERMERGED_SUPPORTS_API' );
if ( defined( 'MW_API' ) && $legacyMode ) {
# API mode
# The CAPTCHA was already checked and approved
return true;
}
if ( !$this->doConfirmEdit( $editPage, $newtext, $section, $merged ) ) {
$editPage->showEditForm( array( &$this, 'editCallback' ) );
return false;
$page = $context->getWikiPage();
if ( !$this->doConfirmEdit( $page, $content, false, true ) ) {
if ( $legacyMode ) {
$status->fatal( 'hookaborted' );
}
$status->value = EditPage::AS_HOOK_ERROR_EXPECTED;
$status->apiHookResult = array();
$this->addCaptchaAPI( $status->apiHookResult );
$page->ConfirmEdit_ActivateCaptcha = true;
return $legacyMode;
}
return true;
}
/**
* A more efficient edit filter callback based on the text after section merging
* @param EditPage $editPage
* @param string $newtext
* @return bool
*/
function confirmEditMerged( $editPage, $newtext ) {
return $this->confirmEdit( $editPage, $newtext, false, true );
}
function confirmEditAPI( $editPage, $newtext, &$resultArr ) {
if ( !$this->doConfirmEdit( $editPage, $newtext, false, false ) ) {
function confirmEditAPI( $editPage, $newText, &$resultArr ) {
$page = $editPage->getArticle()->getPage();
if ( !$this->doConfirmEdit( $page, $newText, false, false ) ) {
$this->addCaptchaAPI( $resultArr );
return false;
}
@ -652,7 +672,7 @@ class SimpleCaptcha {
* @return bool
*/
public function APIGetAllowedParams( &$module, &$params, $flags ) {
if ( $flags && $this->isAPICaptchaModule( $module ) ) {
if ( $this->isAPICaptchaModule( $module ) ) {
$params['captchaword'] = null;
$params['captchaid'] = null;
}
@ -746,13 +766,13 @@ class SimpleCaptcha {
/**
* Retrieve the current version of the page or section being edited...
* @param EditPage $editPage
* @param Title $title
* @param string $section
* @return string
* @access private
*/
function loadText( $editPage, $section ) {
$rev = Revision::newFromTitle( $editPage->mTitle, false, Revision::READ_LATEST );
function loadText( $title, $section ) {
$rev = Revision::newFromTitle( $title, false, Revision::READ_LATEST );
if ( is_null( $rev ) ) {
return "";
} else {
@ -768,16 +788,16 @@ class SimpleCaptcha {
/**
* Extract a list of all recognized HTTP links in the text.
* @param $editpage EditPage
* @param $title Title
* @param $text string
* @return array of strings
*/
function findLinks( &$editpage, $text ) {
function findLinks( $title, $text ) {
global $wgParser, $wgUser;
$options = new ParserOptions();
$text = $wgParser->preSaveTransform( $text, $editpage->mTitle, $wgUser, $options );
$out = $wgParser->parse( $text, $editpage->mTitle, $options );
$text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
$out = $wgParser->parse( $text, $title, $options );
return array_keys( $out->getExternalLinks() );
}

View file

@ -33,6 +33,9 @@
if ( !defined( 'MEDIAWIKI' ) ) {
exit;
}
if ( !defined( 'MW_SUPPORTS_CONTENTHANDLER' ) ) {
throw Exception( 'This version of ConfirmEdit requires MediaWiki 1.21 or later' );
}
$wgExtensionFunctions[] = 'confirmEditSetup';
$wgExtensionCredits['antispam'][] = array(
@ -178,7 +181,6 @@ $wgMessagesDirs['ConfirmEdit'] = __DIR__ . '/i18n/core';
$wgExtensionMessagesFiles['ConfirmEdit'] = "$wgConfirmEditIP/ConfirmEdit.i18n.php";
$wgExtensionMessagesFiles['ConfirmEditAlias'] = "$wgConfirmEditIP/ConfirmEdit.alias.php";
$wgHooks['EditFilterMerged'][] = 'ConfirmEditHooks::confirmEditMerged';
$wgHooks['UserCreateForm'][] = 'ConfirmEditHooks::injectUserCreate';
$wgHooks['AbortNewAccount'][] = 'ConfirmEditHooks::confirmUserCreate';
$wgHooks['LoginAuthenticateAudit'][] = 'ConfirmEditHooks::triggerUserLogin';
@ -186,8 +188,13 @@ $wgHooks['UserLoginForm'][] = 'ConfirmEditHooks::injectUserLogin';
$wgHooks['AbortLogin'][] = 'ConfirmEditHooks::confirmUserLogin';
$wgHooks['EmailUserForm'][] = 'ConfirmEditHooks::injectEmailUser';
$wgHooks['EmailUser'][] = 'ConfirmEditHooks::confirmEmailUser';
# Register API hook
$wgHooks['APIEditBeforeSave'][] = 'ConfirmEditHooks::confirmEditAPI';
$wgHooks['EditPage::showEditForm:fields'][] = 'ConfirmEditHooks::showEditFormFields';
$wgHooks['EditFilterMergedContent'][] = 'ConfirmEditHooks::confirmEditMerged';
if ( !defined( 'MW_EDITFILTERMERGED_SUPPORTS_API' ) ) {
$wgHooks['APIEditBeforeSave'][] = 'ConfirmEditHooks::confirmEditAPI';
}
$wgHooks['APIGetAllowedParams'][] = 'ConfirmEditHooks::APIGetAllowedParams';
$wgHooks['APIGetParamDescription'][] = 'ConfirmEditHooks::APIGetParamDescription';
$wgHooks['AddNewAccountApiForm'][] = 'ConfirmEditHooks::addNewAccountApiForm';

View file

@ -19,13 +19,18 @@ class ConfirmEditHooks {
return $wgCaptcha;
}
static function confirmEditMerged( $editPage, $newtext ) {
return self::getInstance()->confirmEditMerged( $editPage, $newtext );
static function confirmEditMerged( $context, $content, $status, $summary, $user, $minorEdit ) {
return self::getInstance()->confirmEditMerged( $context, $content, $status, $summary,
$user, $minorEdit );
}
static function confirmEditAPI( $editPage, $newtext, &$resultArr ) {
return self::getInstance()->confirmEditAPI( $editPage, $newtext, $resultArr );
}
static function showEditFormFields( &$editPage, &$out ) {
return self::getInstance()->showEditFormFields( $editPage, $out );
}
static function addNewAccountApiForm( $apiModule, $loginForm ) {
return self::getInstance()->addNewAccountApiForm( $apiModule, $loginForm );