Verify IP whitelisting on login and account creation as well.
[toast/cookiecaptcha.git] / ConfirmEdit_body.php
1 <?php
2
3 class ConfirmEditHooks {
4         static function getInstance() {
5                 global $wgCaptcha, $wgCaptchaClass, $wgExtensionMessagesFiles;
6                 static $done = false;
7                 if ( !$done ) {
8                         $done = true;
9                         wfLoadExtensionMessages( 'ConfirmEdit' );
10                         if ( isset( $wgExtensionMessagesFiles[$wgCaptchaClass] ) ) {
11                                 wfLoadExtensionMessages( $wgCaptchaClass );
12                         }
13                         $wgCaptcha = new $wgCaptchaClass;
14                 }
15                 return $wgCaptcha;
16         }
17
18         static function confirmEdit( &$editPage, $newtext, $section ) {
19                 return self::getInstance()->confirmEdit( $editPage, $newtext, $section );
20         }
21
22         static function confirmEditMerged( &$editPage, $newtext ) {
23                 return self::getInstance()->confirmEditMerged( $editPage, $newtext );
24         }
25         
26         static function confirmEditAPI( &$editPage, $newtext, &$resultArr ) {
27                 return self::getInstance()->confirmEditAPI( $editPage, $newtext, $resultArr );
28         }
29
30         static function injectUserCreate( &$template ) {
31                 return self::getInstance()->injectUserCreate( $template );
32         }
33
34         static function confirmUserCreate( $u, &$message ) {
35                 return self::getInstance()->confirmUserCreate( $u, $message );
36         }
37
38         static function triggerUserLogin( $user, $password, $retval ) {
39                 return self::getInstance()->triggerUserLogin( $user, $password, $retval );
40         }
41
42         static function injectUserLogin( &$template ) {
43                 return self::getInstance()->injectUserLogin( $template );
44         }
45
46         static function confirmUserLogin( $u, $pass, &$retval ) {
47                 return self::getInstance()->confirmUserLogin( $u, $pass, $retval );
48         }
49 }
50
51 class CaptchaSpecialPage extends UnlistedSpecialPage {
52         function execute( $par ) {
53                 $this->setHeaders();
54                 $instance = ConfirmEditHooks::getInstance();
55                 switch( $par ) {
56                 case "image":
57                         if( method_exists($instance,'showImage') )
58                                 return $instance->showImage();
59                 case "help":
60                 default:
61                         return $instance->showHelp();
62                 }
63         }
64 }
65
66
67 class SimpleCaptcha {
68         function SimpleCaptcha() {
69                 global $wgCaptchaStorageClass;
70                 $this->storage = new $wgCaptchaStorageClass;
71         }
72         
73         function getCaptcha() {
74                 $a = mt_rand(0, 100);
75                 $b = mt_rand(0, 10);
76                 $op = mt_rand(0, 1) ? '+' : '-';
77
78                 $test = "$a $op $b";
79                 $answer = ($op == '+') ? ($a + $b) : ($a - $b);
80                 return array('question' => $test, 'answer' => $answer);
81         }
82         
83         function addCaptchaAPI(&$resultArr) {
84                 $captcha = $this->getCaptcha();
85                 $index = $this->storeCaptcha( $captcha );
86                 $resultArr['captcha']['type'] = 'simple';
87                 $resultArr['captcha']['mime'] = 'text/plain';
88                 $resultArr['captcha']['id'] = $index;
89                 $resultArr['captcha']['question'] = $captcha['question'];
90         }
91         
92         /**
93          * Insert a captcha prompt into the edit form.
94          * This sample implementation generates a simple arithmetic operation;
95          * it would be easy to defeat by machine.
96          *
97          * Override this!
98          *
99          * @return string HTML
100          */
101         function getForm() {
102                 $captcha = $this->getCaptcha();
103                 $index = $this->storeCaptcha( $captcha );
104
105                 return "<p><label for=\"wpCaptchaWord\">{$captcha['question']}</label> = " .
106                         Xml::element( 'input', array(
107                                 'name' => 'wpCaptchaWord',
108                                 'id'   => 'wpCaptchaWord',
109                                 'tabindex' => 1 ) ) . // tab in before the edit textarea
110                         "</p>\n" .
111                         Xml::element( 'input', array(
112                                 'type'  => 'hidden',
113                                 'name'  => 'wpCaptchaId',
114                                 'id'    => 'wpCaptchaId',
115                                 'value' => $index ) );
116         }
117
118         /**
119          * Insert the captcha prompt into an edit form.
120          * @param OutputPage $out
121          */
122         function editCallback( &$out ) {
123                 $out->addWikiText( $this->getMessage( $this->action ) );
124                 $out->addHTML( $this->getForm() );
125         }
126
127         /**
128          * Show a message asking the user to enter a captcha on edit
129          * The result will be treated as wiki text
130          *
131          * @param $action Action being performed
132          * @return string
133          */
134         function getMessage( $action ) {
135                 $name = 'captcha-' . $action;
136                 $text = wfMsg( $name );
137                 # Obtain a more tailored message, if possible, otherwise, fall back to
138                 # the default for edits
139                 return wfEmptyMsg( $name, $text ) ? wfMsg( 'captcha-edit' ) : $text;
140         }
141
142         /**
143          * Inject whazawhoo
144          * @fixme if multiple thingies insert a header, could break
145          * @param SimpleTemplate $template
146          * @return bool true to keep running callbacks
147          */
148         function injectUserCreate( &$template ) {
149                 global $wgCaptchaTriggers, $wgOut, $wgUser;
150                 if( $wgCaptchaTriggers['createaccount'] ) {
151                         if( $wgUser->isAllowed( 'skipcaptcha' ) ) {
152                                 wfDebug( "ConfirmEdit: user group allows skipping captcha on account creation\n" );
153                                 return true;
154                         }
155                         $template->set( 'header',
156                                 "<div class='captcha'>" .
157                                 $wgOut->parse( $this->getMessage( 'createaccount' ) ) .
158                                 $this->getForm() .
159                                 "</div>\n" );
160                 }
161                 return true;
162         }
163
164         /**
165          * Inject a captcha into the user login form after a failed
166          * password attempt as a speedbump for mass attacks.
167          * @fixme if multiple thingies insert a header, could break
168          * @param SimpleTemplate $template
169          * @return bool true to keep running callbacks
170          */
171         function injectUserLogin( &$template ) {
172                 if( $this->isBadLoginTriggered() ) {
173                         global $wgOut;
174                         $template->set( 'header',
175                                 "<div class='captcha'>" .
176                                 $wgOut->parse( $this->getMessage( 'badlogin' ) ) .
177                                 $this->getForm() .
178                                 "</div>\n" );
179                 }
180                 return true;
181         }
182         
183         /**
184          * When a bad login attempt is made, increment an expiring counter
185          * in the memcache cloud. Later checks for this may trigger a
186          * captcha display to prevent too many hits from the same place.
187          * @param User $user
188          * @param string $password
189          * @param int $retval authentication return value
190          * @return bool true to keep running callbacks
191          */
192         function triggerUserLogin( $user, $password, $retval ) {
193                 global $wgCaptchaTriggers, $wgCaptchaBadLoginExpiration, $wgMemc;
194                 if( $retval == LoginForm::WRONG_PASS && $wgCaptchaTriggers['badlogin'] ) {
195                         $key = $this->badLoginKey();
196                         $count = $wgMemc->get( $key );
197                         if( !$count ) {
198                                 $wgMemc->add( $key, 0, $wgCaptchaBadLoginExpiration );
199                         }
200                         $count = $wgMemc->incr( $key );
201                 }
202                 return true;
203         }
204         
205         /**
206          * Check if a bad login has already been registered for this
207          * IP address. If so, require a captcha.
208          * @return bool
209          * @access private
210          */
211         function isBadLoginTriggered() {
212                 global $wgMemc, $wgCaptchaBadLoginAttempts;
213                 return intval( $wgMemc->get( $this->badLoginKey() ) ) >= $wgCaptchaBadLoginAttempts;
214         }
215         
216         /**
217          * Check if the IP is allowed to skip captchas
218          */
219         function isIPWhitelisted() {
220                 global $wgCaptchaWhitelistIP;
221                 if( !empty( $wgCaptchaWhitelistIP ) ) {
222                         $ip = wfGetIp();
223                         foreach ( $wgCaptchaWhitelistIP as $range ) {
224                                 if ( IP::isInRange( $ip, $range ) ) {
225                                         return true;
226                                 }
227                         }
228                 }
229                 return false;
230         }
231         
232         /**
233          * Internal cache key for badlogin checks.
234          * @return string
235          * @access private
236          */
237         function badLoginKey() {
238                 return wfMemcKey( 'captcha', 'badlogin', 'ip', wfGetIP() );
239         }
240         
241         /**
242          * Check if the submitted form matches the captcha session data provided
243          * by the plugin when the form was generated.
244          *
245          * Override this!
246          *
247          * @param string $answer
248          * @param array $info
249          * @return bool
250          */
251         function keyMatch( $answer, $info ) {
252                 return $answer == $info['answer'];
253         }
254
255         // ----------------------------------
256
257         /**
258          * @param EditPage $editPage
259          * @param string $action (edit/create/addurl...)
260          * @return bool true if action triggers captcha on editPage's namespace
261          */
262         function captchaTriggers( &$editPage, $action) {
263                 global $wgCaptchaTriggers, $wgCaptchaTriggersOnNamespace;       
264                 //Special config for this NS?
265                 if (isset( $wgCaptchaTriggersOnNamespace[$editPage->mTitle->getNamespace()][$action] ) )
266                         return $wgCaptchaTriggersOnNamespace[$editPage->mTitle->getNamespace()][$action];
267
268                 return ( !empty( $wgCaptchaTriggers[$action] ) ); //Default
269         }
270
271
272         /**
273          * @param EditPage $editPage
274          * @param string $newtext
275          * @param string $section
276          * @return bool true if the captcha should run
277          */
278         function shouldCheck( &$editPage, $newtext, $section, $merged = false ) {
279                 $this->trigger = '';
280                 $title = $editPage->mArticle->getTitle();
281
282                 global $wgUser;
283                 if( $wgUser->isAllowed( 'skipcaptcha' ) ) {
284                         wfDebug( "ConfirmEdit: user group allows skipping captcha\n" );
285                         return false;
286                 }
287                 if( $this->isIPWhitelisted() )
288                         return false;
289
290
291                 global $wgEmailAuthentication, $ceAllowConfirmedEmail;
292                 if( $wgEmailAuthentication && $ceAllowConfirmedEmail &&
293                         $wgUser->isEmailConfirmed() ) {
294                         wfDebug( "ConfirmEdit: user has confirmed mail, skipping captcha\n" );
295                         return false;
296                 }
297
298                 if( $this->captchaTriggers( $editPage, 'edit' ) ) {
299                         // Check on all edits
300                         global $wgUser;
301                         $this->trigger = sprintf( "edit trigger by '%s' at [[%s]]",
302                                 $wgUser->getName(),
303                                 $title->getPrefixedText() );
304                         $this->action = 'edit';
305                         wfDebug( "ConfirmEdit: checking all edits...\n" );
306                         return true;
307                 }
308
309                 if( $this->captchaTriggers( $editPage, 'create' )  && !$editPage->mTitle->exists() ) {
310                         //Check if creating a page
311                         global $wgUser;
312                         $this->trigger = sprintf( "Create trigger by '%s' at [[%s]]",
313                                 $wgUser->getName(),
314                                 $title->getPrefixedText() );
315                         $this->action = 'create';
316                         wfDebug( "ConfirmEdit: checking on page creation...\n" );
317                         return true;
318                 }
319
320                 if( $this->captchaTriggers( $editPage, 'addurl' ) ) {
321                         // Only check edits that add URLs
322                         if ( $merged ) {
323                                 // Get links from the database
324                                 $oldLinks = $this->getLinksFromTracker( $title );
325                                 // Share a parse operation with Article::doEdit()
326                                 $editInfo = $editPage->mArticle->prepareTextForEdit( $newtext );
327                                 $newLinks = array_keys( $editInfo->output->getExternalLinks() );
328                         } else {
329                                 // Get link changes in the slowest way known to man
330                                 $oldtext = $this->loadText( $editPage, $section );
331                                 $oldLinks = $this->findLinks( $editPage, $oldtext );
332                                 $newLinks = $this->findLinks( $editPage, $newtext );
333                         }
334
335                         $unknownLinks = array_filter( $newLinks, array( &$this, 'filterLink' ) );
336                         $addedLinks = array_diff( $unknownLinks, $oldLinks );
337                         $numLinks = count( $addedLinks );
338
339                         if( $numLinks > 0 ) {
340                                 global $wgUser;
341                                 $this->trigger = sprintf( "%dx url trigger by '%s' at [[%s]]: %s",
342                                         $numLinks,
343                                         $wgUser->getName(),
344                                         $title->getPrefixedText(),
345                                         implode( ", ", $addedLinks ) );
346                                 $this->action = 'addurl';
347                                 return true;
348                         }
349                 }
350
351                 global $wgCaptchaRegexes;
352                 if( !empty( $wgCaptchaRegexes ) ) {
353                         // Custom regex checks
354                         $oldtext = $this->loadText( $editPage, $section );
355
356                         foreach( $wgCaptchaRegexes as $regex ) {
357                                 $newMatches = array();
358                                 if( preg_match_all( $regex, $newtext, $newMatches ) ) {
359                                         $oldMatches = array();
360                                         preg_match_all( $regex, $oldtext, $oldMatches );
361
362                                         $addedMatches = array_diff( $newMatches[0], $oldMatches[0] );
363
364                                         $numHits = count( $addedMatches );
365                                         if( $numHits > 0 ) {
366                                                 global $wgUser;
367                                                 $this->trigger = sprintf( "%dx %s at [[%s]]: %s",
368                                                         $numHits,
369                                                         $regex,
370                                                         $wgUser->getName(),
371                                                         $title->getPrefixedText(),
372                                                         implode( ", ", $addedMatches ) );
373                                                 $this->action = 'edit';
374                                                 return true;
375                                         }
376                                 }
377                         }
378                 }
379
380                 return false;
381         }
382
383         /**
384          * Filter callback function for URL whitelisting
385          * @param string url to check
386          * @return bool true if unknown, false if whitelisted
387          * @access private
388          */
389         function filterLink( $url ) {
390                 global $wgCaptchaWhitelist;
391                 $source = wfMsgForContent( 'captcha-addurl-whitelist' );
392
393                 $whitelist = wfEmptyMsg( 'captcha-addurl-whitelist', $source ) 
394                         ? false
395                         : $this->buildRegexes( explode( "\n", $source ) );
396
397                 $cwl = $wgCaptchaWhitelist !== false ? preg_match( $wgCaptchaWhitelist, $url ) : false;
398                 $wl  = $whitelist          !== false ? preg_match( $whitelist, $url )          : false;
399
400                 return !( $cwl || $wl );
401         }
402
403         /**
404          * Build regex from whitelist
405          * @param string lines from [[MediaWiki:Captcha-addurl-whitelist]]
406          * @return string Regex or bool false if whitelist is empty
407          * @access private
408          */
409         function buildRegexes( $lines ) {
410                 # Code duplicated from the SpamBlacklist extension (r19197)
411
412                 # Strip comments and whitespace, then remove blanks
413                 $lines = array_filter( array_map( 'trim', preg_replace( '/#.*$/', '', $lines ) ) );
414
415                 # No lines, don't make a regex which will match everything
416                 if ( count( $lines ) == 0 ) {
417                         wfDebug( "No lines\n" );
418                         return false;
419                 } else {
420                         # Make regex
421                         # It's faster using the S modifier even though it will usually only be run once
422                         //$regex = 'http://+[a-z0-9_\-.]*(' . implode( '|', $lines ) . ')';
423                         //return '/' . str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $regex) ) . '/Si';
424                         $regexes = '';
425                         $regexStart = '/^https?:\/\/+[a-z0-9_\-.]*(';
426                         $regexEnd = ')/Si';
427                         $regexMax = 4096;
428                         $build = false;
429                         foreach( $lines as $line ) {
430                                 // FIXME: not very robust size check, but should work. :)
431                                 if( $build === false ) {
432                                         $build = $line;
433                                 } elseif( strlen( $build ) + strlen( $line ) > $regexMax ) {
434                                         $regexes .= $regexStart .
435                                                 str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $build) ) .
436                                                 $regexEnd;
437                                         $build = $line;
438                                 } else {
439                                         $build .= '|' . $line;
440                                 }
441                         }
442                         if( $build !== false ) {
443                                 $regexes .= $regexStart .
444                                         str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $build) ) .
445                                         $regexEnd;
446                         }
447                         return $regexes;
448                 }
449         }
450
451         /**
452          * Load external links from the externallinks table
453          */
454         function getLinksFromTracker( $title ) {
455                 $dbr =& wfGetDB( DB_SLAVE );
456                 $id = $title->getArticleId(); // should be zero queries
457                 $res = $dbr->select( 'externallinks', array( 'el_to' ), 
458                         array( 'el_from' => $id ), __METHOD__ );
459                 $links = array();
460                 while ( $row = $dbr->fetchObject( $res ) ) {
461                         $links[] = $row->el_to;
462                 }
463                 return $links;
464         }
465         
466         /**
467          * Backend function for confirmEdit() and confirmEditAPI()
468          * @return bool false if the CAPTCHA is rejected, true otherwise
469          */
470         private function doConfirmEdit( &$editPage, $newtext, $section, $merged = false ) {
471                 if( $this->shouldCheck( $editPage, $newtext, $section, $merged ) ) {
472                         if( $this->passCaptcha() ) {
473                                 return true;
474                         } else {
475                                 return false;
476                         }
477                 } else {
478                         wfDebug( "ConfirmEdit: no need to show captcha.\n" );
479                         return true;
480                 }
481         }
482
483         /**
484          * The main callback run on edit attempts.
485          * @param EditPage $editPage
486          * @param string $newtext
487          * @param string $section
488          * @param bool $merged
489          * @return bool true to continue saving, false to abort and show a captcha form
490          */
491         function confirmEdit( &$editPage, $newtext, $section, $merged = false ) {
492                 if( defined('MW_API') ) {
493                         # API mode
494                         # The CAPTCHA was already checked and approved 
495                         return true;
496                 }
497                 if( !$this->doConfirmEdit( $editPage, $newtext, $section, $merged ) ) {
498                         $editPage->showEditForm( array( &$this, 'editCallback' ) );
499                         return false;
500                 }
501                 return true;
502         }
503
504         /**
505          * A more efficient edit filter callback based on the text after section merging
506          * @param EditPage $editPage
507          * @param string $newtext
508          */
509         function confirmEditMerged( &$editPage, $newtext ) {
510                 return $this->confirmEdit( $editPage, $newtext, false, true );
511         }
512         
513         
514         function confirmEditAPI( &$editPage, $newtext, &$resultArr) {
515                 if( !$this->doConfirmEdit( $editPage, $newtext, false, false ) ) {
516                         $this->addCaptchaAPI($resultArr);
517                         return false;
518                 }
519                 return true;
520         }
521
522         /**
523          * Hook for user creation form submissions.
524          * @param User $u
525          * @param string $message
526          * @return bool true to continue, false to abort user creation
527          */
528         function confirmUserCreate( $u, &$message ) {
529                 global $wgCaptchaTriggers, $wgUser;
530                 if( $wgCaptchaTriggers['createaccount'] ) {
531                         if( $wgUser->isAllowed( 'skipcaptcha' ) ) {
532                                 wfDebug( "ConfirmEdit: user group allows skipping captcha on account creation\n" );
533                                 return true;
534                         }
535                         if( $this->isIPWhitelisted() )
536                                 return true;
537                                 
538                         $this->trigger = "new account '" . $u->getName() . "'";
539                         if( !$this->passCaptcha() ) {
540                                 $message = wfMsg( 'captcha-createaccount-fail' );
541                                 return false;
542                         }
543                 }
544                 return true;
545         }
546         
547         /**
548          * Hook for user login form submissions.
549          * @param User $u
550          * @param string $message
551          * @return bool true to continue, false to abort user creation
552          */
553         function confirmUserLogin( $u, $pass, &$retval ) {
554                 if( $this->isBadLoginTriggered() ) {
555                         if( $this->isIPWhitelisted() )
556                                 return true;
557                         
558                         $this->trigger = "post-badlogin login '" . $u->getName() . "'";
559                         if( !$this->passCaptcha() ) {
560                                 $message = wfMsg( 'captcha-badlogin-fail' );
561                                 // Emulate a bad-password return to confuse the shit out of attackers
562                                 $retval = LoginForm::WRONG_PASS;
563                                 return false;
564                         }
565                 }
566                 return true;
567         }
568
569         /**
570          * Given a required captcha run, test form input for correct
571          * input on the open session.
572          * @return bool if passed, false if failed or new session
573          */
574         function passCaptcha() {
575                 $info = $this->retrieveCaptcha();
576                 if( $info ) {
577                         global $wgRequest;
578                         if( $this->keyMatch( $wgRequest->getVal('wpCaptchaWord'), $info ) ) {
579                                 $this->log( "passed" );
580                                 $this->clearCaptcha( $info );
581                                 return true;
582                         } else {
583                                 $this->clearCaptcha( $info );
584                                 $this->log( "bad form input" );
585                                 return false;
586                         }
587                 } else {
588                         $this->log( "new captcha session" );
589                         return false;
590                 }
591         }
592
593         /**
594          * Log the status and any triggering info for debugging or statistics
595          * @param string $message
596          */
597         function log( $message ) {
598                 wfDebugLog( 'captcha', 'ConfirmEdit: ' . $message . '; ' .  $this->trigger );
599         }
600
601         /**
602          * Generate a captcha session ID and save the info in PHP's session storage.
603          * (Requires the user to have cookies enabled to get through the captcha.)
604          *
605          * A random ID is used so legit users can make edits in multiple tabs or
606          * windows without being unnecessarily hobbled by a serial order requirement.
607          * Pass the returned id value into the edit form as wpCaptchaId.
608          *
609          * @param array $info data to store
610          * @return string captcha ID key
611          */
612         function storeCaptcha( $info ) {
613                 if( !isset( $info['index'] ) ) {
614                         // Assign random index if we're not udpating
615                         $info['index'] = strval( mt_rand() );
616                 }
617                 $this->storage->store( $info['index'], $info );
618                 return $info['index'];
619         }
620
621         /**
622          * Fetch this session's captcha info.
623          * @return mixed array of info, or false if missing
624          */
625         function retrieveCaptcha() {
626                 global $wgRequest;
627                 $index = $wgRequest->getVal( 'wpCaptchaId' );
628                 return $this->storage->retrieve( $index );
629         }
630
631         /**
632          * Clear out existing captcha info from the session, to ensure
633          * it can't be reused.
634          */
635         function clearCaptcha( $info ) {
636                 $this->storage->clear( $info['index'] );
637         }
638
639         /**
640          * Retrieve the current version of the page or section being edited...
641          * @param EditPage $editPage
642          * @param string $section
643          * @return string
644          * @access private
645          */
646         function loadText( $editPage, $section ) {
647                 $rev = Revision::newFromTitle( $editPage->mTitle );
648                 if( is_null( $rev ) ) {
649                         return "";
650                 } else {
651                         $text = $rev->getText();
652                         if( $section != '' ) {
653                                 return Article::getSection( $text, $section );
654                         } else {
655                                 return $text;
656                         }
657                 }
658         }
659
660         /**
661          * Extract a list of all recognized HTTP links in the text.
662          * @param string $text
663          * @return array of strings
664          */
665         function findLinks( &$editpage, $text ) {
666                 global $wgParser, $wgUser;
667
668                 $options = new ParserOptions();
669                 $text = $wgParser->preSaveTransform( $text, $editpage->mTitle, $wgUser, $options );
670                 $out = $wgParser->parse( $text, $editpage->mTitle, $options );
671
672                 return array_keys( $out->getExternalLinks() );
673         }
674
675         /**
676          * Show a page explaining what this wacky thing is.
677          */
678         function showHelp() {
679                 global $wgOut, $ceAllowConfirmedEmail;
680                 $wgOut->setPageTitle( wfMsg( 'captchahelp-title' ) );
681                 $wgOut->addWikiText( wfMsg( 'captchahelp-text' ) );
682                 if ( $this->storage->cookiesNeeded() ) {
683                         $wgOut->addWikiText( wfMsg( 'captchahelp-cookies-needed' ) );
684                 }
685         }
686
687 }
688
689 class CaptchaSessionStore {
690         function store( $index, $info ) {
691                 $_SESSION['captcha' . $info['index']] = $info;
692         }
693         
694         function retrieve( $index ) {
695                 if( isset( $_SESSION['captcha' . $index] ) ) {
696                         return $_SESSION['captcha' . $index];
697                 } else {
698                         return false;
699                 }
700         }
701         
702         function clear( $index ) {
703                 unset( $_SESSION['captcha' . $index] );
704         }
705
706         function cookiesNeeded() {
707                 return true;
708         }
709 }
710
711 class CaptchaCacheStore {
712         function store( $index, $info ) {
713                 global $wgMemc, $wgCaptchaSessionExpiration;
714                 $wgMemc->set( wfMemcKey( 'captcha', $index ), $info,
715                         $wgCaptchaSessionExpiration );
716         }
717
718         function retrieve( $index ) {
719                 global $wgMemc;
720                 $info = $wgMemc->get( wfMemcKey( 'captcha', $index ) );
721                 if( $info ) {
722                         return $info;
723                 } else {
724                         return false;
725                 }
726         }
727         
728         function clear( $index ) {
729                 global $wgMemc;
730                 $wgMemc->delete( wfMemcKey( 'captcha', $index ) );
731         }
732
733         function cookiesNeeded() {
734                 return false;
735         }
736 }
737