X-Git-Url: https://git.toastfreeware.priv.at/toast/cookiecaptcha.git/blobdiff_plain/ac3040e4703a0163518f673e7268158c99a30f81..c112c24f54cce653e9089fdaff2bf7e691a8a248:/ConfirmEdit.php diff --git a/ConfirmEdit.php b/ConfirmEdit.php index 1167f1f..2faac7f 100644 --- a/ConfirmEdit.php +++ b/ConfirmEdit.php @@ -1,11 +1,12 @@ + * Copyright (C) 2005-2007 Brion Vibber * http://www.mediawiki.org/ * * This program is free software; you can redistribute it and/or modify @@ -23,18 +24,23 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * http://www.gnu.org/copyleft/gpl.html * - * @package MediaWiki - * @subpackage Extensions + * @addtogroup Extensions */ -if ( defined( 'MEDIAWIKI' ) ) { +if ( !defined( 'MEDIAWIKI' ) ) { + exit; +} global $wgExtensionFunctions, $wgGroupPermissions; -$wgExtensionFunctions[] = 'ceSetup'; - -# Internationalisation file -require_once( 'ConfirmEdit.i18n.php' ); +$wgExtensionFunctions[] = 'confirmEditSetup'; +$wgExtensionCredits['other'][] = array( + 'name' => 'ConfirmEdit', + 'author' => 'Brion Vibber', + 'url' => 'http://www.mediawiki.org/wiki/Extension:ConfirmEdit', + 'description' => 'Simple captcha implementation', + 'descriptionmsg' => 'captcha-desc', +); /** * The 'skipcaptcha' permission key can be given out to @@ -50,12 +56,22 @@ $wgGroupPermissions['autoconfirmed']['skipcaptcha'] = false; $wgGroupPermissions['bot' ]['skipcaptcha'] = true; // registered bots $wgGroupPermissions['sysop' ]['skipcaptcha'] = true; +/** + * List of IP ranges to allow to skip the captcha, similar to the group setting: + * "$wgGroupPermission[...]['skipcaptcha'] = true" + * + * Specific IP addresses or CIDR-style ranges may be used, + * for instance: + * $wgCaptchaWhitelistIP = array('192.168.1.0/24', '10.1.0.0/16'); + */ +$wgCaptchaWhitelistIP = false; + global $wgCaptcha, $wgCaptchaClass, $wgCaptchaTriggers; $wgCaptcha = null; $wgCaptchaClass = 'SimpleCaptcha'; /** - * Currently the captcha works only for page edits. + * Actions which can trigger a captcha * * If the 'edit' trigger is on, *every* edit will trigger the captcha. * This may be useful for protecting against vandalbot attacks. @@ -64,12 +80,64 @@ $wgCaptchaClass = 'SimpleCaptcha'; * edits that include URLs that aren't in the current version of the page. * This should catch automated linkspammers without annoying people when * they make more typical edits. + * + * The captcha code should not use $wgCaptchaTriggers, but CaptchaTriggers() + * which also takes into account per namespace triggering. */ $wgCaptchaTriggers = array(); $wgCaptchaTriggers['edit'] = false; // Would check on every edit +$wgCaptchaTriggers['create'] = false; // Check on page creation. $wgCaptchaTriggers['addurl'] = true; // Check on edits that add URLs $wgCaptchaTriggers['createaccount'] = true; // Special:Userlogin&type=signup +$wgCaptchaTriggers['badlogin'] = true; // Special:Userlogin after failure + +/** + * You may wish to apply special rules for captcha triggering on some namespaces. + * $wgCaptchaTriggersOnNamespace[][] forces an always on / + * always off configuration with that trigger for the given namespace. + * Leave unset to use the global options ($wgCaptchaTriggers). + * + * Shall not be used with 'createaccount' (it is not checked). + */ +$wgCaptchaTriggersOnNamespace = array(); +#Example: +#$wgCaptchaTriggersOnNamespace[NS_TALK]['create'] = false; //Allow creation of talk pages without captchas. +#$wgCaptchaTriggersOnNamespace[NS_PROJECT]['edit'] = true; //Show captcha whenever editing Project pages. + +/** + * Indicate how to store per-session data required to match up the + * internal captcha data with the editor. + * + * 'CaptchaSessionStore' uses PHP's session storage, which is cookie-based + * and may fail for anons with cookies disabled. + * + * 'CaptchaCacheStore' uses $wgMemc, which avoids the cookie dependency + * but may be fragile depending on cache configuration. + */ +global $wgCaptchaStorageClass; +$wgCaptchaStorageClass = 'CaptchaSessionStore'; + +/** + * Number of seconds a captcha session should last in the data cache + * before expiring when managing through CaptchaCacheStore class. + * + * Default is a half hour. + */ +global $wgCaptchaSessionExpiration; +$wgCaptchaSessionExpiration = 30 * 60; + +/** + * Number of seconds after a bad login that a captcha will be shown to + * that client on the login form to slow down password-guessing bots. + * + * Has no effect if 'badlogin' is disabled in $wgCaptchaTriggers or + * if there is not a caching engine enabled. + * + * Default is five minutes. + */ +global $wgCaptchaBadLoginExpiration; +$wgCaptchaBadLoginExpiration = 5 * 60; /** * Allow users who have confirmed their e-mail addresses to post @@ -82,7 +150,7 @@ $ceAllowConfirmedEmail = false; * Regex to whitelist URLs to known-good sites... * For instance: * $wgCaptchaWhitelist = '#^https?://([a-z0-9-]+\\.)?(wikimedia|wikipedia)\.org/#i'; - * @fixme Use the 'spam-whitelist' thingy instead? + * Local admins can define a whitelist under [[MediaWiki:captcha-addurl-whitelist]] */ $wgCaptchaWhitelist = false; @@ -98,388 +166,46 @@ $wgCaptchaWhitelist = false; $wgCaptchaRegexes = array(); /** Register special page */ -global $wgSpecialPages; -$wgSpecialPages['Captcha'] = array( /*class*/ 'SpecialPage', /*name*/'Captcha', /*restriction*/ '', - /*listed*/ false, /*function*/ false, /*file*/ false ); +$wgSpecialPages['Captcha'] = array( /*class*/'CaptchaSpecialPage', /*name*/'Captcha' ); -/** - * Set up message strings for captcha utilities. - */ -function ceSetup() { - # Add messages - global $wgMessageCache, $wgConfirmEditMessages; - foreach( $wgConfirmEditMessages as $lang => $messages ) - $wgMessageCache->addMessages( $messages, $lang ); - - global $wgHooks, $wgCaptcha, $wgCaptchaClass, $wgSpecialPages; - $wgCaptcha = new $wgCaptchaClass(); - $wgHooks['EditFilter'][] = array( &$wgCaptcha, 'confirmEdit' ); - - $wgHooks['UserCreateForm'][] = array( &$wgCaptcha, 'injectUserCreate' ); - $wgHooks['AbortNewAccount'][] = array( &$wgCaptcha, 'confirmUserCreate' ); +$wgConfirmEditIP = dirname( __FILE__ ); +$wgExtensionMessagesFiles['ConfirmEdit'] = "$wgConfirmEditIP/ConfirmEdit.i18n.php"; + +if ( defined( 'MW_SUPPORTS_EDITFILTERMERGED' ) ) { + $wgHooks['EditFilterMerged'][] = 'ConfirmEditHooks::confirmEditMerged'; +} else { + $wgHooks['EditFilter'][] = 'ConfirmEditHooks::confirmEdit'; } +$wgHooks['UserCreateForm'][] = 'ConfirmEditHooks::injectUserCreate'; +$wgHooks['AbortNewAccount'][] = 'ConfirmEditHooks::confirmUserCreate'; +$wgHooks['LoginAuthenticateAudit'][] = 'ConfirmEditHooks::triggerUserLogin'; +$wgHooks['UserLoginForm'][] = 'ConfirmEditHooks::injectUserLogin'; +$wgHooks['AbortLogin'][] = 'ConfirmEditHooks::confirmUserLogin'; +# Register API hook +$wgHooks['APIEditBeforeSave'][] = 'ConfirmEditHooks::confirmEditAPI'; + +$wgAutoloadClasses['ConfirmEditHooks'] + = $wgAutoloadClasses['SimpleCaptcha'] + = $wgAutoloadClasses['CaptchaSessionStore'] + = $wgAutoloadClasses['CaptchaCacheStore'] + = $wgAutoloadClasses['CaptchaSpecialPage'] + = "$wgConfirmEditIP/ConfirmEdit_body.php"; /** - * Entry point for Special:Captcha + * Set up $wgWhitelistRead */ -function wfSpecialCaptcha( $par = null ) { - global $wgCaptcha; - switch( $par ) { - case "image": - return $wgCaptcha->showImage(); - case "help": - default: - return $wgCaptcha->showHelp(); - } -} - -class SimpleCaptcha { - /** - * Insert a captcha prompt into the edit form. - * This sample implementation generates a simple arithmetic operation; - * it would be easy to defeat by machine. - * - * Override this! - * - * @return string HTML - */ - function getForm() { - $a = mt_rand(0, 100); - $b = mt_rand(0, 10); - $op = mt_rand(0, 1) ? '+' : '-'; - - $test = "$a $op $b"; - $answer = ($op == '+') ? ($a + $b) : ($a - $b); - - $index = $this->storeCaptcha( array( 'answer' => $answer ) ); - - return "

= " . - wfElement( 'input', array( - 'name' => 'wpCaptchaWord', - 'id' => 'wpCaptchaWord', - 'tabindex' => 1 ) ) . // tab in before the edit textarea - "

\n" . - wfElement( 'input', array( - 'type' => 'hidden', - 'name' => 'wpCaptchaId', - 'id' => 'wpCaptchaId', - 'value' => $index ) ); - } - - /** - * Insert the captcha prompt into an edit form. - * @param OutputPage $out - */ - function editCallback( &$out ) { - $out->addWikiText( $this->getMessage( 'edit' ) ); - $out->addHTML( $this->getForm() ); - } - - /** - * Show a message asking the user to enter a captcha on edit - * The result will be treated as wiki text - * - * @param $action Action being performed - * @return string - */ - function getMessage( $action ) { - $name = 'captcha-' . $action; - $text = wfMsg( $name ); - # Obtain a more tailored message, if possible, otherwise, fall back to - # the default for edits - return wfEmptyMsg( $name, $text ) ? wfMsg( 'captcha-edit' ) : $text; - } - - /** - * Inject whazawhoo - * @fixme if multiple thingies insert a header, could break - * @param SimpleTemplate $template - * @return bool true to keep running callbacks - */ - function injectUserCreate( &$template ) { - global $wgCaptchaTriggers, $wgOut; - if( $wgCaptchaTriggers['createaccount'] ) { - $template->set( 'header', - "
" . - $wgOut->parse( $this->getMessage( 'createaccount' ) ) . - $this->getForm() . - "
\n" ); - } - return true; - } - - /** - * Check if the submitted form matches the captcha session data provided - * by the plugin when the form was generated. - * - * Override this! - * - * @param WebRequest $request - * @param array $info - * @return bool - */ - function keyMatch( $request, $info ) { - return $request->getVal( 'wpCaptchaWord' ) == $info['answer']; - } - - // ---------------------------------- - - /** - * @param EditPage $editPage - * @param string $newtext - * @param string $section - * @return bool true if the captcha should run - */ - function shouldCheck( &$editPage, $newtext, $section ) { - $this->trigger = ''; - - global $wgUser; - if( $wgUser->isAllowed( 'skipcaptcha' ) ) { - wfDebug( "ConfirmEdit: user group allows skipping captcha\n" ); - return false; - } - - global $wgEmailAuthentication, $ceAllowConfirmedEmail; - if( $wgEmailAuthentication && $ceAllowConfirmedEmail && - $wgUser->isEmailConfirmed() ) { - wfDebug( "ConfirmEdit: user has confirmed mail, skipping captcha\n" ); - return false; - } - - global $wgCaptchaTriggers; - if( !empty( $wgCaptchaTriggers['edit'] ) ) { - // Check on all edits - global $wgUser, $wgTitle; - $this->trigger = sprintf( "edit trigger by '%s' at [[%s]]", - $wgUser->getName(), - $wgTitle->getPrefixedText() ); - wfDebug( "ConfirmEdit: checking all edits...\n" ); - return true; - } - - if( !empty( $wgCaptchaTriggers['addurl'] ) ) { - // Only check edits that add URLs - $oldtext = $this->loadText( $editPage, $section ); - - $oldLinks = $this->findLinks( $oldtext ); - $newLinks = $this->findLinks( $newtext ); - $unknownLinks = array_filter( $newLinks, array( &$this, 'filterLink' ) ); - - $addedLinks = array_diff( $unknownLinks, $oldLinks ); - $numLinks = count( $addedLinks ); - - if( $numLinks > 0 ) { - global $wgUser, $wgTitle; - $this->trigger = sprintf( "%dx url trigger by '%s' at [[%s]]: %s", - $numLinks, - $wgUser->getName(), - $wgTitle->getPrefixedText(), - implode( ", ", $addedLinks ) ); - return true; - } - } - - global $wgCaptchaRegexes; - if( !empty( $wgCaptchaRegexes ) ) { - // Custom regex checks - $oldtext = $this->loadText( $editPage, $section ); - - foreach( $wgCaptchaRegexes as $regex ) { - $newMatches = array(); - if( preg_match_all( $regex, $newtext, $newMatches ) ) { - $oldMatches = array(); - preg_match_all( $regex, $oldtext, $oldMatches ); - - $addedMatches = array_diff( $newMatches[0], $oldMatches[0] ); - - $numHits = count( $addedMatches ); - if( $numHits > 0 ) { - global $wgUser, $wgTitle; - $this->trigger = sprintf( "%dx %s at [[%s]]: %s", - $numHits, - $regex, - $wgUser->getName(), - $wgTitle->getPrefixedText(), - implode( ", ", $addedMatches ) ); - return true; - } - } - } - } - - return false; - } - - /** - * Filter callback function for URL whitelisting - * @return bool true if unknown, false if whitelisted - * @access private - */ - function filterLink( $url ) { - global $wgCaptchaWhitelist; - return !( $wgCaptchaWhitelist && preg_match( $wgCaptchaWhitelist, $url ) ); - } - - /** - * The main callback run on edit attempts. - * @param EditPage $editPage - * @param string $newtext - * @param string $section - * @param bool true to continue saving, false to abort and show a captcha form - */ - function confirmEdit( &$editPage, $newtext, $section ) { - if( $this->shouldCheck( $editPage, $newtext, $section ) ) { - if( $this->passCaptcha() ) { - return true; - } else { - $editPage->showEditForm( array( &$this, 'editCallback' ) ); - return false; - } - } else { - wfDebug( "ConfirmEdit: no new links.\n" ); - return true; - } - } - - /** - * Hook for user creation form submissions. - * @param User $u - * @param string $message - * @return bool true to continue, false to abort user creation - */ - function confirmUserCreate( $u, &$message ) { - global $wgCaptchaTriggers; - if( $wgCaptchaTriggers['createaccount'] ) { - $this->trigger = "new account '" . $u->getName() . "'"; - if( !$this->passCaptcha() ) { - $message = wfMsg( 'captcha-createaccount-fail' ); - return false; - } - } - return true; - } - - /** - * Given a required captcha run, test form input for correct - * input on the open session. - * @return bool if passed, false if failed or new session - */ - function passCaptcha() { - $info = $this->retrieveCaptcha(); - if( $info ) { - global $wgRequest; - if( $this->keyMatch( $wgRequest, $info ) ) { - $this->log( "passed" ); - $this->clearCaptcha( $info ); - return true; - } else { - $this->clearCaptcha( $info ); - $this->log( "bad form input" ); - return false; - } - } else { - $this->log( "new captcha session" ); - return false; - } - } - - /** - * Log the status and any triggering info for debugging or statistics - * @param string $message - */ - function log( $message ) { - wfDebugLog( 'captcha', 'ConfirmEdit: ' . $message . '; ' . $this->trigger ); - } - - /** - * Generate a captcha session ID and save the info in PHP's session storage. - * (Requires the user to have cookies enabled to get through the captcha.) - * - * A random ID is used so legit users can make edits in multiple tabs or - * windows without being unnecessarily hobbled by a serial order requirement. - * Pass the returned id value into the edit form as wpCaptchaId. - * - * @param array $info data to store - * @return string captcha ID key - */ - function storeCaptcha( $info ) { - if( !isset( $info['index'] ) ) { - // Assign random index if we're not udpating - $info['index'] = strval( mt_rand() ); - } - $_SESSION['captcha' . $info['index']] = $info; - return $info['index']; - } - - /** - * Fetch this session's captcha info. - * @return mixed array of info, or false if missing - */ - function retrieveCaptcha() { - global $wgRequest; - $index = $wgRequest->getVal( 'wpCaptchaId' ); - if( isset( $_SESSION['captcha' . $index] ) ) { - return $_SESSION['captcha' . $index]; - } else { - return false; - } - } - - /** - * Clear out existing captcha info from the session, to ensure - * it can't be reused. - */ - function clearCaptcha( $info ) { - unset( $_SESSION['captcha' . $info['index']] ); - } - - /** - * Retrieve the current version of the page or section being edited... - * @param EditPage $editPage - * @param string $section - * @return string - * @access private - */ - function loadText( $editPage, $section ) { - $rev = Revision::newFromTitle( $editPage->mTitle ); - if( is_null( $rev ) ) { - return ""; - } else { - $text = $rev->getText(); - if( $section != '' ) { - return Article::getSection( $text, $section ); - } else { - return $text; - } - } - } - - /** - * Extract a list of all recognized HTTP links in the text. - * @param string $text - * @return array of strings - */ - function findLinks( $text ) { - global $wgParser, $wgTitle, $wgUser; - - $options = new ParserOptions(); - $text = $wgParser->preSaveTransform( $text, $wgTitle, $wgUser, $options ); - $out = $wgParser->parse( $text, $wgTitle, $options ); - - return array_keys( $out->getExternalLinks() ); - } - - /** - * Show a page explaining what this wacky thing is. - */ - function showHelp() { - global $wgOut, $ceAllowConfirmedEmail; - $wgOut->setPageTitle( wfMsg( 'captchahelp-title' ) ); - $wgOut->addWikiText( wfMsg( 'captchahelp-text' ) ); +function confirmEditSetup() { + global $wgGroupPermissions, $wgCaptchaTriggers; + if( !$wgGroupPermissions['*']['read'] && $wgCaptchaTriggers['badlogin'] ) { + // We need to ensure that the captcha interface is accessible + // so that unauthenticated users can actually get in after a + // mistaken password typing. + global $wgWhitelistRead; + $image = Title::makeTitle( NS_SPECIAL, 'Captcha/image' ); + $help = Title::makeTitle( NS_SPECIAL, 'Captcha/help' ); + $wgWhitelistRead[] = $image->getPrefixedText(); + $wgWhitelistRead[] = $help->getPrefixedText(); } - } -} # End invocation guard -?>