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