From da07c3eb19e055101497090bddbf1b6e9c489b38 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 14 Oct 2005 05:07:24 +0000 Subject: [PATCH] Further testing, integrating images generated from Neil's python goody. Still experimental and incomplete, not ready for use. --- ConfirmEdit.php | 234 ++++++++++++++++++++++++++--------------------- FancyCaptcha.php | 115 +++++++++++++++++++++++ 2 files changed, 247 insertions(+), 102 deletions(-) create mode 100644 FancyCaptcha.php diff --git a/ConfirmEdit.php b/ConfirmEdit.php index 6099fb6..d82e023 100644 --- a/ConfirmEdit.php +++ b/ConfirmEdit.php @@ -3,22 +3,28 @@ # Prelim in-progress code. Proof of concept for framework, not # intended as a real production captcha system! -# Loader for spam blacklist feature +# Loader for simple captcha feature # Include this from LocalSettings.php if ( defined( 'MEDIAWIKI' ) ) { -global $wgExtensionFunctions, $wgHooks, $wgGroupPermissions; +global $wgExtensionFunctions, $wgGroupPermissions; $wgExtensionFunctions[] = 'ceSetup'; -$wgHooks['EditFilter'][] = 'ceConfirmEditLinks'; - $wgGroupPermissions['*' ]['skipcaptcha'] = false; $wgGroupPermissions['user' ]['skipcaptcha'] = false; $wgGroupPermissions['bot' ]['skipcaptcha'] = true; // registered bots $wgGroupPermissions['sysop' ]['skipcaptcha'] = true; +global $wgCaptcha, $wgCaptchaClass, $wgCaptchaTriggers; +$wgCaptcha = null; +$wgCaptchaClass = 'SimpleCaptcha'; + +$wgCaptchaTriggers = array(); +$wgCaptchaTriggers['edit'] = false; // Would check on every edit +$wgCaptchaTriggers['addurl'] = true; // Check on edits that add URLs + /** * Allow users who have confirmed their e-mail addresses to post * URL links without being harassed by the captcha. @@ -30,138 +36,162 @@ $ceAllowConfirmedEmail = false; * Set up message strings for captcha utilities. */ function ceSetup() { - global $wgMessageCache; + global $wgMessageCache, $wgHooks, $wgCaptcha, $wgCaptchaClass; $wgMessageCache->addMessage('captcha-short', "Your edit includes new URL links; as a protection against automated spam, you'll need to enter the answer to this simple arithmetic test:" ); + SpecialPage::addPage( new SpecialPage( 'Captcha', false, /*listed*/ false, /*function*/ false, /*file*/ false ) ); + + $wgCaptcha = new $wgCaptchaClass(); + $wgHooks['EditFilter'][] = array( &$wgCaptcha, 'confirmEdit' ); } /** * Entry point for Special:Captcha */ function wfSpecialCaptcha( $par = null ) { + global $wgCaptcha; switch( $par ) { case "image": - return ceShowImage(); + return $wgCaptcha->showImage(); case "help": default: - return ceShowHelp(); + return $wgCaptcha->showHelp(); } } -function ceConfirmEditLinks( &$editPage, $newtext, $section ) { - $oldtext = ceLoadText( $editPage, $section ); - - $oldLinks = ceFindLinks( $oldtext ); - $newLinks = ceFindLinks( $newtext ); - - $addedLinks = array_diff( $newLinks, $oldLinks ); - $numLinks = count( $addedLinks ); - - /* - var_dump( $oldtext ); - var_dump( $newtext ); - var_dump( $oldLinks ); - var_dump( $newLinks ); - var_dump( $addedLinks ); - die( '---' ); - */ - - if( $numLinks > 0 ) { - wfDebug( "ConfirmEdit found $numLinks new links...\n" ); - if( ceKeyMatch() ) { - wfDebug( "ConfirmEdit given proper key from form, passing.\n" ); - return true; - } else { - wfDebug( "ConfirmEdit missing form key, prompting.\n" ); - $editPage->showEditForm( 'ceFormCallback' ); +class SimpleCaptcha { + /** + * @param EditPage $editPage + * @param string $newtext + * @param string $section + * @return bool true if the captcha should run + */ + function shouldCheck( &$editPage, $newtext, $section ) { + global $wgUser; + if( $wgUser->isAllowed( 'skipcaptcha' ) ) { + wfDebug( "SimpleCaptcha: user group allows skipping captcha\n" ); return false; } - } else { - wfDebug( "ConfirmEdit: no new links.\n" ); - return true; - } -} - -function ceKeyMatch() { - global $wgUser; - if( $wgUser->isAllowed( 'skipcaptcha' ) ) { - wfDebug( "ConfirmEdit: user group allows skipping captcha\n" ); - return true; - } - - global $wgEmailAuthentication, $ceAllowConfirmedEmail; - if( $wgEmailAuthentication && $ceAllowConfirmedEmail && - $wgUser->isEmailConfirmed() ) { - wfDebug( "ConfirmEdit: user has confirmed mail, skippng captcha\n" ); - return true; - } - if( !isset( $_SESSION['ceAnswerVar'] ) ) { - wfDebug( "ConfirmEdit no session captcha key set, this is new visitor.\n" ); + global $wgEmailAuthentication, $ceAllowConfirmedEmail; + if( $wgEmailAuthentication && $ceAllowConfirmedEmail && + $wgUser->isEmailConfirmed() ) { + wfDebug( "SimpleCaptcha: user has confirmed mail, skipping captcha\n" ); + return false; + } + + global $wgCaptchaTriggers; + if( !empty( $wgCaptchaTriggers['edit'] ) ) { + // Check on all edits + wfDebug( "SimpleCaptcha: 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 ); + + $addedLinks = array_diff( $newLinks, $oldLinks ); + $numLinks = count( $addedLinks ); + + if( $numLinks > 0 ) { + wfDebug( "SimpleCaptcha: found $numLinks new links; triggered...\n" ); + return true; + } + } + return false; } - global $wgRequest; - return $wgRequest->getVal( $_SESSION['ceAnswerVar'] ) == $_SESSION['ceAnswer']; -} - -function ceFormCallback( &$out ) { - $source = 'ceSource' . mt_rand(); - $dest = 'ceConfirm' . mt_rand(); - $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); - $_SESSION['ceAnswer'] = $answer; - $_SESSION['ceAnswerVar'] = $dest; + function confirmEdit( &$editPage, $newtext, $section ) { + if( $this->shouldCheck( $editPage, $newtext, $section ) ) { + if( $this->keyMatch() ) { + wfDebug( "ConfirmEdit given proper key from form, passing.\n" ); + return true; + } else { + wfDebug( "ConfirmEdit missing form key, prompting.\n" ); + $editPage->showEditForm( array( &$this, 'formCallback' ) ); + return false; + } + } else { + wfDebug( "ConfirmEdit: no new links.\n" ); + return true; + } + } + function keyMatch() { + if( !isset( $_SESSION['ceAnswerVar'] ) ) { + wfDebug( "ConfirmEdit no session captcha key set, this is new visitor.\n" ); + return false; + } + global $wgRequest; + return $wgRequest->getVal( $_SESSION['ceAnswerVar'] ) == $_SESSION['ceAnswer']; + } - $out->addWikiText( wfMsg( "captcha-short" ) ); - $out->addHTML( <<$test =

+ function formCallback( &$out ) { + $source = 'ceSource' . mt_rand(); + $dest = 'ceConfirm' . mt_rand(); + + $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); + $_SESSION['ceAnswer'] = $answer; + $_SESSION['ceAnswerVar'] = $dest; + + + $out->addWikiText( wfMsg( "captcha-short" ) ); + $out->addHTML( <<$test =

END - ); -} - -function ceLoadText( $editPage, $section ) { - $rev = Revision::newFromTitle( $editPage->mTitle ); - if( is_null( $rev ) ) { - return ""; - } else { - $text = $rev->getText(); - if( $section != '' ) { - return Article::getSection( $text, $section ); + ); + } + + function loadText( $editPage, $section ) { + $rev = Revision::newFromTitle( $editPage->mTitle ); + if( is_null( $rev ) ) { + return ""; } else { - return $text; + $text = $rev->getText(); + if( $section != '' ) { + return Article::getSection( $text, $section ); + } else { + return $text; + } } } -} - -function ceFindLinks( $text ) { - $regex = '/((?:' . HTTP_PROTOCOLS . ')' . EXT_LINK_URL_CLASS . '+)/'; - if( preg_match_all( $regex, $text, $matches, PREG_PATTERN_ORDER ) ) { - return $matches[1]; - } else { - return array(); + function findLinks( $text ) { + $regex = '/((?:' . HTTP_PROTOCOLS . ')' . EXT_LINK_URL_CLASS . '+)/'; + + if( preg_match_all( $regex, $text, $matches, PREG_PATTERN_ORDER ) ) { + return $matches[1]; + } else { + return array(); + } } -} - -function ceShowHelp() { - global $wgOut, $ceAllowConfirmedEmail; - $wgOut->setPageTitle( 'Captcha help' ); - $wgOut->addWikiText( <<setPageTitle( 'Captcha help' ); + $wgOut->addWikiText( << diff --git a/FancyCaptcha.php b/FancyCaptcha.php new file mode 100644 index 0000000..7cacd23 --- /dev/null +++ b/FancyCaptcha.php @@ -0,0 +1,115 @@ +getVal( $var ); + $digest = $wgCaptchaSecret . $salt . $answer . $wgCaptchaSecret . $salt; + $answerHash = substr( md5( $digest ), 0, 16 ); + + if( $answerHash == $hash ) { + wfDebug( "FancyCaptcha: answer hash matches expected $hash\n" ); + return true; + } else { + wfDebug( "FancyCaptcha: answer hashes to $answerHash, expected $hash\n" ); + return false; + } + } + + function formCallback( &$out ) { + $dest = 'wpCaptchaWord' . mt_rand(); + + $img = $this->pickImage(); + if( !$img ) { + die( 'aaargh' ); + } + + $_SESSION['ceAnswerVar'] = $dest; + $_SESSION['captchaHash'] = $img['hash']; + $_SESSION['captchaSalt'] = $img['salt']; + $_SESSION['captchaViewed'] = false; + wfDebug( "Picked captcha with hash ${img['hash']}, salt ${img['salt']}.\n" ); + + $title = Title::makeTitle( NS_SPECIAL, 'Captcha/image' ); + $url = $title->getLocalUrl(); + + + $out->addWikiText( wfMsg( "captcha-short" ) ); + $out->addHTML( <<Oh noes

+

+END + ); + } + + function pickImage() { + global $wgCaptchaDirectory; + $dir = opendir( $wgCaptchaDirectory ); + + $n = mt_rand( 0, 16 ); + $count = 0; + + $entry = readdir( $dir ); + while( false !== $entry ) { + $entry = readdir( $dir ); + if( preg_match( '/^image_([0-9a-f]+)_([0-9a-f]+)\\.png$/', $entry, $matches ) ) { + if( $count++ % 16 == $n ) { + return array( + 'salt' => $matches[1], + 'hash' => $matches[2], + ); + } + } + } + return false; + } + + function showImage() { + global $wgOut; + $wgOut->disable(); + if( !empty( $_SESSION['captchaViewed'] ) ) { + wfHttpError( 403, 'Access Forbidden', "Can't view captcha image a second time." ); + return false; + } + $_SESSION['captchaViewed'] = wfTimestamp(); + + if( isset( $_SESSION['captchaSalt'] ) ) { + $salt = $_SESSION['captchaSalt']; + if( isset( $_SESSION['captchaHash'] ) ) { + $hash = $_SESSION['captchaHash']; + + global $wgCaptchaDirectory; + $file = $wgCaptchaDirectory . DIRECTORY_SEPARATOR . "image_{$salt}_{$hash}.png"; + if( file_exists( $file ) ) { + header( 'Content-type: image/png' ); + readfile( $file ); + } + } + } else { + wfHttpError( 500, 'Internal Error', 'Requested bogus captcha image' ); + } + } +} + +} # End invocation guard + +?> -- 2.39.5