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