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