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