Follow-ups to r56684: add a member function for extensions to add header text cleanly...
[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->addFormHeader( 
155                                 "<div class='captcha'>" .
156                                 $wgOut->parse( $this->getMessage( 'createaccount' ) ) .
157                                 $this->getForm() .
158                                 "</div>\n"
159                         );
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          * @param SimpleTemplate $template
168          * @return bool true to keep running callbacks
169          */
170         function injectUserLogin( &$sp ) {
171                 if ( $this->isBadLoginTriggered() ) {
172                         global $wgOut;
173                         $sp->addFormHeader(
174                                 "<div class='captcha'>" .
175                                 $wgOut->parse( $this->getMessage( 'badlogin' ) ) .
176                                 $this->getForm() .
177                                 "</div>\n"
178                         );
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 == Login::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 ( $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          * @param EditPage $editPage
273          * @param string $newtext
274          * @param string $section
275          * @return bool true if the captcha should run
276          */
277         function shouldCheck( &$editPage, $newtext, $section, $merged = false ) {
278                 $this->trigger = '';
279                 $title = $editPage->mArticle->getTitle();
280
281                 global $wgUser;
282                 if ( $wgUser->isAllowed( 'skipcaptcha' ) ) {
283                         wfDebug( "ConfirmEdit: user group allows skipping captcha\n" );
284                         return false;
285                 }
286                 if ( $this->isIPWhitelisted() )
287                         return false;
288
289
290                 global $wgEmailAuthentication, $ceAllowConfirmedEmail;
291                 if ( $wgEmailAuthentication && $ceAllowConfirmedEmail &&
292                         $wgUser->isEmailConfirmed() ) {
293                         wfDebug( "ConfirmEdit: user has confirmed mail, skipping captcha\n" );
294                         return false;
295                 }
296
297                 if ( $this->captchaTriggers( $editPage, 'edit' ) ) {
298                         // Check on all edits
299                         global $wgUser;
300                         $this->trigger = sprintf( "edit trigger by '%s' at [[%s]]",
301                                 $wgUser->getName(),
302                                 $title->getPrefixedText() );
303                         $this->action = 'edit';
304                         wfDebug( "ConfirmEdit: checking all edits...\n" );
305                         return true;
306                 }
307
308                 if ( $this->captchaTriggers( $editPage, 'create' )  && !$editPage->mTitle->exists() ) {
309                         // Check if creating a page
310                         global $wgUser;
311                         $this->trigger = sprintf( "Create trigger by '%s' at [[%s]]",
312                                 $wgUser->getName(),
313                                 $title->getPrefixedText() );
314                         $this->action = 'create';
315                         wfDebug( "ConfirmEdit: checking on page creation...\n" );
316                         return true;
317                 }
318
319                 if ( $this->captchaTriggers( $editPage, 'addurl' ) ) {
320                         // Only check edits that add URLs
321                         if ( $merged ) {
322                                 // Get links from the database
323                                 $oldLinks = $this->getLinksFromTracker( $title );
324                                 // Share a parse operation with Article::doEdit()
325                                 $editInfo = $editPage->mArticle->prepareTextForEdit( $newtext );
326                                 $newLinks = array_keys( $editInfo->output->getExternalLinks() );
327                         } else {
328                                 // Get link changes in the slowest way known to man
329                                 $oldtext = $this->loadText( $editPage, $section );
330                                 $oldLinks = $this->findLinks( $editPage, $oldtext );
331                                 $newLinks = $this->findLinks( $editPage, $newtext );
332                         }
333
334                         $unknownLinks = array_filter( $newLinks, array( &$this, 'filterLink' ) );
335                         $addedLinks = array_diff( $unknownLinks, $oldLinks );
336                         $numLinks = count( $addedLinks );
337
338                         if ( $numLinks > 0 ) {
339                                 global $wgUser;
340                                 $this->trigger = sprintf( "%dx url trigger by '%s' at [[%s]]: %s",
341                                         $numLinks,
342                                         $wgUser->getName(),
343                                         $title->getPrefixedText(),
344                                         implode( ", ", $addedLinks ) );
345                                 $this->action = 'addurl';
346                                 return true;
347                         }
348                 }
349
350                 global $wgCaptchaRegexes;
351                 if ( $wgCaptchaRegexes ) {
352                         // Custom regex checks
353                         $oldtext = $this->loadText( $editPage, $section );
354
355                         foreach ( $wgCaptchaRegexes as $regex ) {
356                                 $newMatches = array();
357                                 if ( preg_match_all( $regex, $newtext, $newMatches ) ) {
358                                         $oldMatches = array();
359                                         preg_match_all( $regex, $oldtext, $oldMatches );
360
361                                         $addedMatches = array_diff( $newMatches[0], $oldMatches[0] );
362
363                                         $numHits = count( $addedMatches );
364                                         if ( $numHits > 0 ) {
365                                                 global $wgUser;
366                                                 $this->trigger = sprintf( "%dx %s at [[%s]]: %s",
367                                                         $numHits,
368                                                         $regex,
369                                                         $wgUser->getName(),
370                                                         $title->getPrefixedText(),
371                                                         implode( ", ", $addedMatches ) );
372                                                 $this->action = 'edit';
373                                                 return true;
374                                         }
375                                 }
376                         }
377                 }
378
379                 return false;
380         }
381
382         /**
383          * Filter callback function for URL whitelisting
384          * @param string url to check
385          * @return bool true if unknown, false if whitelisted
386          * @access private
387          */
388         function filterLink( $url ) {
389                 global $wgCaptchaWhitelist;
390                 $source = wfMsgForContent( 'captcha-addurl-whitelist' );
391
392                 $whitelist = wfEmptyMsg( 'captcha-addurl-whitelist', $source )
393                         ? false
394                         : $this->buildRegexes( explode( "\n", $source ) );
395
396                 $cwl = $wgCaptchaWhitelist !== false ? preg_match( $wgCaptchaWhitelist, $url ) : false;
397                 $wl  = $whitelist          !== false ? preg_match( $whitelist, $url )          : false;
398
399                 return !( $cwl || $wl );
400         }
401
402         /**
403          * Build regex from whitelist
404          * @param string lines from [[MediaWiki:Captcha-addurl-whitelist]]
405          * @return string Regex or bool false if whitelist is empty
406          * @access private
407          */
408         function buildRegexes( $lines ) {
409                 # Code duplicated from the SpamBlacklist extension (r19197)
410
411                 # Strip comments and whitespace, then remove blanks
412                 $lines = array_filter( array_map( 'trim', preg_replace( '/#.*$/', '', $lines ) ) );
413
414                 # No lines, don't make a regex which will match everything
415                 if ( count( $lines ) == 0 ) {
416                         wfDebug( "No lines\n" );
417                         return false;
418                 } else {
419                         # Make regex
420                         # It's faster using the S modifier even though it will usually only be run once
421                         // $regex = 'http://+[a-z0-9_\-.]*(' . implode( '|', $lines ) . ')';
422                         // return '/' . str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $regex) ) . '/Si';
423                         $regexes = '';
424                         $regexStart = '/^https?:\/\/+[a-z0-9_\-.]*(';
425                         $regexEnd = ')/Si';
426                         $regexMax = 4096;
427                         $build = false;
428                         foreach ( $lines as $line ) {
429                                 // FIXME: not very robust size check, but should work. :)
430                                 if ( $build === false ) {
431                                         $build = $line;
432                                 } elseif ( strlen( $build ) + strlen( $line ) > $regexMax ) {
433                                         $regexes .= $regexStart .
434                                                 str_replace( '/', '\/', preg_replace( '|\\\*/|', '/', $build ) ) .
435                                                 $regexEnd;
436                                         $build = $line;
437                                 } else {
438                                         $build .= '|' . $line;
439                                 }
440                         }
441                         if ( $build !== false ) {
442                                 $regexes .= $regexStart .
443                                         str_replace( '/', '\/', preg_replace( '|\\\*/|', '/', $build ) ) .
444                                         $regexEnd;
445                         }
446                         return $regexes;
447                 }
448         }
449
450         /**
451          * Load external links from the externallinks table
452          */
453         function getLinksFromTracker( $title ) {
454                 $dbr =& wfGetDB( DB_SLAVE );
455                 $id = $title->getArticleId(); // should be zero queries
456                 $res = $dbr->select( 'externallinks', array( 'el_to' ),
457                         array( 'el_from' => $id ), __METHOD__ );
458                 $links = array();
459                 while ( $row = $dbr->fetchObject( $res ) ) {
460                         $links[] = $row->el_to;
461                 }
462                 return $links;
463         }
464
465         /**
466          * Backend function for confirmEdit() and confirmEditAPI()
467          * @return bool false if the CAPTCHA is rejected, true otherwise
468          */
469         private function doConfirmEdit( $editPage, $newtext, $section, $merged = false ) {
470                 if ( $this->shouldCheck( $editPage, $newtext, $section, $merged ) ) {
471                         if ( $this->passCaptcha() ) {
472                                 return true;
473                         } else {
474                                 return false;
475                         }
476                 } else {
477                         wfDebug( "ConfirmEdit: no need to show captcha.\n" );
478                         return true;
479                 }
480         }
481
482         /**
483          * The main callback run on edit attempts.
484          * @param EditPage $editPage
485          * @param string $newtext
486          * @param string $section
487          * @param bool $merged
488          * @return bool true to continue saving, false to abort and show a captcha form
489          */
490         function confirmEdit( $editPage, $newtext, $section, $merged = false ) {
491                 if ( defined( 'MW_API' ) ) {
492                         # API mode
493                         # The CAPTCHA was already checked and approved
494                         return true;
495                 }
496                 if ( !$this->doConfirmEdit( $editPage, $newtext, $section, $merged ) ) {
497                         $editPage->showEditForm( array( &$this, 'editCallback' ) );
498                         return false;
499                 }
500                 return true;
501         }
502
503         /**
504          * A more efficient edit filter callback based on the text after section merging
505          * @param EditPage $editPage
506          * @param string $newtext
507          */
508         function confirmEditMerged( $editPage, $newtext ) {
509                 return $this->confirmEdit( $editPage, $newtext, false, true );
510         }
511
512
513         function confirmEditAPI( $editPage, $newtext, &$resultArr ) {
514                 if ( !$this->doConfirmEdit( $editPage, $newtext, false, false ) ) {
515                         $this->addCaptchaAPI( $resultArr );
516                         return false;
517                 }
518                 return true;
519         }
520
521         /**
522          * Hook for user creation form submissions.
523          * @param User $u
524          * @param string $message
525          * @return bool true to continue, false to abort user creation
526          */
527         function confirmUserCreate( $u, &$message ) {
528                 global $wgCaptchaTriggers, $wgUser;
529                 if ( $wgCaptchaTriggers['createaccount'] ) {
530                         if ( $wgUser->isAllowed( 'skipcaptcha' ) ) {
531                                 wfDebug( "ConfirmEdit: user group allows skipping captcha on account creation\n" );
532                                 return true;
533                         }
534                         if ( $this->isIPWhitelisted() )
535                                 return true;
536
537                         $this->trigger = "new account '" . $u->getName() . "'";
538                         if ( !$this->passCaptcha() ) {
539                                 $message = wfMsg( 'captcha-createaccount-fail' );
540                                 return false;
541                         }
542                 }
543                 return true;
544         }
545
546         /**
547          * Hook for user login form submissions.
548          * @param User $u
549          * @param string $message
550          * @return bool true to continue, false to abort user creation
551          */
552         function confirmUserLogin( $u, $pass, &$retval ) {
553                 if ( $this->isBadLoginTriggered() ) {
554                         if ( $this->isIPWhitelisted() )
555                                 return true;
556
557                         $this->trigger = "post-badlogin login '" . $u->getName() . "'";
558                         if ( !$this->passCaptcha() ) {
559                                 $message = wfMsg( 'captcha-badlogin-fail' );
560                                 // Emulate a bad-password return to confuse the shit out of attackers
561                                 $retval = Login::WRONG_PASS;
562                                 return false;
563                         }
564                 }
565                 return true;
566         }
567
568         /**
569          * Given a required captcha run, test form input for correct
570          * input on the open session.
571          * @return bool if passed, false if failed or new session
572          */
573         function passCaptcha() {
574                 $info = $this->retrieveCaptcha();
575                 if ( $info ) {
576                         global $wgRequest;
577                         if ( $this->keyMatch( $wgRequest->getVal( 'wpCaptchaWord' ), $info ) ) {
578                                 $this->log( "passed" );
579                                 $this->clearCaptcha( $info );
580                                 return true;
581                         } else {
582                                 $this->clearCaptcha( $info );
583                                 $this->log( "bad form input" );
584                                 return false;
585                         }
586                 } else {
587                         $this->log( "new captcha session" );
588                         return false;
589                 }
590         }
591
592         /**
593          * Log the status and any triggering info for debugging or statistics
594          * @param string $message
595          */
596         function log( $message ) {
597                 wfDebugLog( 'captcha', 'ConfirmEdit: ' . $message . '; ' .  $this->trigger );
598         }
599
600         /**
601          * Generate a captcha session ID and save the info in PHP's session storage.
602          * (Requires the user to have cookies enabled to get through the captcha.)
603          *
604          * A random ID is used so legit users can make edits in multiple tabs or
605          * windows without being unnecessarily hobbled by a serial order requirement.
606          * Pass the returned id value into the edit form as wpCaptchaId.
607          *
608          * @param array $info data to store
609          * @return string captcha ID key
610          */
611         function storeCaptcha( $info ) {
612                 if ( !isset( $info['index'] ) ) {
613                         // Assign random index if we're not udpating
614                         $info['index'] = strval( mt_rand() );
615                 }
616                 $this->storage->store( $info['index'], $info );
617                 return $info['index'];
618         }
619
620         /**
621          * Fetch this session's captcha info.
622          * @return mixed array of info, or false if missing
623          */
624         function retrieveCaptcha() {
625                 global $wgRequest;
626                 $index = $wgRequest->getVal( 'wpCaptchaId' );
627                 return $this->storage->retrieve( $index );
628         }
629
630         /**
631          * Clear out existing captcha info from the session, to ensure
632          * it can't be reused.
633          */
634         function clearCaptcha( $info ) {
635                 $this->storage->clear( $info['index'] );
636         }
637
638         /**
639          * Retrieve the current version of the page or section being edited...
640          * @param EditPage $editPage
641          * @param string $section
642          * @return string
643          * @access private
644          */
645         function loadText( $editPage, $section ) {
646                 $rev = Revision::newFromTitle( $editPage->mTitle );
647                 if ( is_null( $rev ) ) {
648                         return "";
649                 } else {
650                         $text = $rev->getText();
651                         if ( $section != '' ) {
652                                 return Article::getSection( $text, $section );
653                         } else {
654                                 return $text;
655                         }
656                 }
657         }
658
659         /**
660          * Extract a list of all recognized HTTP links in the text.
661          * @param string $text
662          * @return array of strings
663          */
664         function findLinks( &$editpage, $text ) {
665                 global $wgParser, $wgUser;
666
667                 $options = new ParserOptions();
668                 $text = $wgParser->preSaveTransform( $text, $editpage->mTitle, $wgUser, $options );
669                 $out = $wgParser->parse( $text, $editpage->mTitle, $options );
670
671                 return array_keys( $out->getExternalLinks() );
672         }
673
674         /**
675          * Show a page explaining what this wacky thing is.
676          */
677         function showHelp() {
678                 global $wgOut, $ceAllowConfirmedEmail;
679                 $wgOut->setPageTitle( wfMsg( 'captchahelp-title' ) );
680                 $wgOut->addWikiText( wfMsg( 'captchahelp-text' ) );
681                 if ( $this->storage->cookiesNeeded() ) {
682                         $wgOut->addWikiText( wfMsg( 'captchahelp-cookies-needed' ) );
683                 }
684         }
685 }
686
687 class CaptchaSessionStore {
688         function store( $index, $info ) {
689                 $_SESSION['captcha' . $info['index']] = $info;
690         }
691
692         function retrieve( $index ) {
693                 if ( isset( $_SESSION['captcha' . $index] ) ) {
694                         return $_SESSION['captcha' . $index];
695                 } else {
696                         return false;
697                 }
698         }
699
700         function clear( $index ) {
701                 unset( $_SESSION['captcha' . $index] );
702         }
703
704         function cookiesNeeded() {
705                 return true;
706         }
707 }
708
709 class CaptchaCacheStore {
710         function store( $index, $info ) {
711                 global $wgMemc, $wgCaptchaSessionExpiration;
712                 $wgMemc->set( wfMemcKey( 'captcha', $index ), $info,
713                         $wgCaptchaSessionExpiration );
714         }
715
716         function retrieve( $index ) {
717                 global $wgMemc;
718                 $info = $wgMemc->get( wfMemcKey( 'captcha', $index ) );
719                 if ( $info ) {
720                         return $info;
721                 } else {
722                         return false;
723                 }
724         }
725
726         function clear( $index ) {
727                 global $wgMemc;
728                 $wgMemc->delete( wfMemcKey( 'captcha', $index ) );
729         }
730
731         function cookiesNeeded() {
732                 return false;
733         }
734 }