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