MediaWiki REL1_34
SimpleCaptcha.php
Go to the documentation of this file.
1<?php
2
5
10 protected static $messagePrefix = 'captcha-';
11
13 private $captchaSolved = null;
14
20 protected $action;
21
23 protected $trigger;
24
28 public function setAction( $action ) {
29 $this->action = $action;
30 }
31
35 public function setTrigger( $trigger ) {
36 $this->trigger = $trigger;
37 }
38
44 public function getError() {
45 return null;
46 }
47
54 public function getCaptcha() {
55 $a = mt_rand( 0, 100 );
56 $b = mt_rand( 0, 10 );
57
58 /* Minus sign is used in the question. UTF-8,
59 since the api uses text/plain, not text/html */
60 $op = mt_rand( 0, 1 ) ? '+' : '−';
61
62 // No space before and after $op, to ensure correct
63 // directionality.
64 $test = "$a$op$b";
65 $answer = ( $op == '+' ) ? ( $a + $b ) : ( $a - $b );
66 return [ 'question' => $test, 'answer' => $answer ];
67 }
68
72 protected function addCaptchaAPI( &$resultArr ) {
73 $captcha = $this->getCaptcha();
74 $index = $this->storeCaptcha( $captcha );
75 $resultArr['captcha'] = $this->describeCaptchaType();
76 $resultArr['captcha']['id'] = $index;
77 $resultArr['captcha']['question'] = $captcha['question'];
78 }
79
85 public function describeCaptchaType() {
86 return [
87 'type' => 'simple',
88 'mime' => 'text/plain',
89 ];
90 }
91
121 public function getFormInformation( $tabIndex = 1 ) {
122 $captcha = $this->getCaptcha();
123 $index = $this->storeCaptcha( $captcha );
124
125 return [
126 'html' =>
127 new OOUI\FieldLayout(
128 new OOUI\NumberInputWidget( [
129 'name' => 'wpCaptchaWord',
130 'classes' => [ 'simplecaptcha-answer' ],
131 'id' => 'wpCaptchaWord',
132 'autocomplete' => 'off',
133 // tab in before the edit textarea
134 'tabIndex' => $tabIndex
135 ] ),
136 [
137 'align' => 'left',
138 'label' => $captcha['question'] . ' = ',
139 'classes' => [ 'simplecaptcha-field' ],
140 ]
141 ) .
142 new OOUI\HiddenInputWidget( [
143 'name' => 'wpCaptchaId',
144 'id' => 'wpCaptchaId',
145 'value' => $index
146 ] ),
147 'modulestyles' => [
148 'ext.confirmEdit.simpleCaptcha'
149 ]
150 ];
151 }
152
160 public function addFormToOutput( OutputPage $out, $tabIndex = 1 ) {
161 $this->addFormInformationToOutput( $out, $this->getFormInformation( $tabIndex ) );
162 }
163
171 public function addFormInformationToOutput( OutputPage $out, array $formInformation ) {
172 if ( !$formInformation ) {
173 return;
174 }
175 if ( isset( $formInformation['html'] ) ) {
176 $out->addHTML( $formInformation['html'] );
177 }
178 if ( isset( $formInformation['modules'] ) ) {
179 $out->addModules( $formInformation['modules'] );
180 }
181 if ( isset( $formInformation['modulestyles'] ) ) {
182 $out->addModuleStyles( $formInformation['modulestyles'] );
183 }
184 if ( isset( $formInformation['headitems'] ) ) {
185 $out->addHeadItems( $formInformation['headitems'] );
186 }
187 }
188
194 public function getCaptchaInfo( $captchaData, $id ) {
195 return $captchaData['question'] . ' =';
196 }
197
203 public function showEditFormFields( &$editPage, &$out ) {
204 $out->enableOOUI();
205 $page = $editPage->getArticle()->getPage();
206 if ( !isset( $page->ConfirmEdit_ActivateCaptcha ) ) {
207 return;
208 }
209
210 if ( $this->action !== 'edit' ) {
211 unset( $page->ConfirmEdit_ActivateCaptcha );
212 $out->addHTML( $this->getMessage( $this->action )->parseAsBlock() );
213 $this->addFormToOutput( $out );
214 }
215 }
216
221 public function editShowCaptcha( $editPage ) {
222 $context = $editPage->getArticle()->getContext();
223 $page = $editPage->getArticle()->getPage();
224 $out = $context->getOutput();
225 if ( isset( $page->ConfirmEdit_ActivateCaptcha ) ||
226 $this->shouldCheck( $page, '', '', $context )
227 ) {
228 $out->addHTML( $this->getMessage( $this->action )->parseAsBlock() );
229 $this->addFormToOutput( $out );
230 }
231 unset( $page->ConfirmEdit_ActivateCaptcha );
232 }
233
241 public function getMessage( $action ) {
242 // one of captcha-edit, captcha-addurl, captcha-badlogin, captcha-createaccount,
243 // captcha-create, captcha-sendemail
244 $name = static::$messagePrefix . $action;
245 $msg = wfMessage( $name );
246 // obtain a more tailored message, if possible, otherwise, fall back to
247 // the default for edits
248 return $msg->isDisabled() ? wfMessage( static::$messagePrefix . 'edit' ) : $msg;
249 }
250
257 public function injectEmailUser( &$form ) {
258 $out = $form->getOutput();
259 $user = $form->getUser();
261 $this->action = 'sendemail';
262 if ( $this->canSkipCaptcha( $user, $form->getConfig() ) ) {
263 return true;
264 }
265 $formInformation = $this->getFormInformation();
266 $formMetainfo = $formInformation;
267 unset( $formMetainfo['html'] );
268 $this->addFormInformationToOutput( $out, $formMetainfo );
269 $form->addFooterText(
270 "<div class='captcha'>" .
271 $this->getMessage( 'sendemail' )->parseAsBlock() .
272 $formInformation['html'] .
273 "</div>\n" );
274 }
275 return true;
276 }
277
284 public function increaseBadLoginCounter( $username ) {
285 global $wgCaptchaBadLoginExpiration, $wgCaptchaBadLoginPerUserExpiration;
286
287 $cache = ObjectCache::getLocalClusterInstance();
288
290 $key = $this->badLoginKey( $cache );
291 $cache->incrWithInit( $key, $wgCaptchaBadLoginExpiration );
292 }
293
294 if ( $this->triggersCaptcha( CaptchaTriggers::BAD_LOGIN_PER_USER ) && $username ) {
295 $key = $this->badLoginPerUserKey( $username, $cache );
296 $cache->incrWithInit( $key, $wgCaptchaBadLoginPerUserExpiration );
297 }
298 }
299
304 public function resetBadLoginCounter( $username ) {
305 if ( $this->triggersCaptcha( CaptchaTriggers::BAD_LOGIN_PER_USER ) && $username ) {
306 $cache = ObjectCache::getLocalClusterInstance();
307 $cache->delete( $this->badLoginPerUserKey( $username, $cache ) );
308 }
309 }
310
317 public function isBadLoginTriggered() {
318 global $wgCaptchaBadLoginAttempts;
319
320 $cache = ObjectCache::getLocalClusterInstance();
322 && (int)$cache->get( $this->badLoginKey( $cache ) ) >= $wgCaptchaBadLoginAttempts;
323 }
324
331 public function isBadLoginPerUserTriggered( $u ) {
332 global $wgCaptchaBadLoginPerUserAttempts;
333
334 $cache = ObjectCache::getLocalClusterInstance();
335
336 if ( is_object( $u ) ) {
337 $u = $u->getName();
338 }
339 $badLoginPerUserKey = $this->badLoginPerUserKey( $u, $cache );
341 && (int)$cache->get( $badLoginPerUserKey ) >= $wgCaptchaBadLoginPerUserAttempts;
342 }
343
352 private function isIPWhitelisted() {
353 global $wgCaptchaWhitelistIP, $wgRequest;
354 $ip = $wgRequest->getIP();
355
356 if ( $wgCaptchaWhitelistIP ) {
357 if ( IP::isInRanges( $ip, $wgCaptchaWhitelistIP ) ) {
358 return true;
359 }
360 }
361
362 $whitelistMsg = wfMessage( 'captcha-ip-whitelist' )->inContentLanguage();
363 if ( !$whitelistMsg->isDisabled() ) {
364 $whitelistedIPs = $this->getWikiIPWhitelist( $whitelistMsg );
365 if ( IP::isInRanges( $ip, $whitelistedIPs ) ) {
366 return true;
367 }
368 }
369
370 return false;
371 }
372
380 private function getWikiIPWhitelist( Message $msg ) {
381 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
382 $cacheKey = $cache->makeKey( 'confirmedit', 'ipwhitelist' );
383
384 $cachedWhitelist = $cache->get( $cacheKey );
385 if ( $cachedWhitelist === false ) {
386 // Could not retrieve from cache so build the whitelist directly
387 // from the wikipage
388 $whitelist = $this->buildValidIPs(
389 explode( "\n", $msg->plain() )
390 );
391 // And then store it in cache for one day. This cache is cleared on
392 // modifications to the whitelist page.
393 // @see ConfirmEditHooks::onPageContentSaveComplete()
394 $cache->set( $cacheKey, $whitelist, 86400 );
395 } else {
396 // Whitelist from the cache
397 $whitelist = $cachedWhitelist;
398 }
399
400 return $whitelist;
401 }
402
414 private function buildValidIPs( array $input ) {
415 // Remove whitespace and blank lines first
416 $ips = array_map( 'trim', $input );
417 $ips = array_filter( $ips );
418
419 $validIPs = [];
420 foreach ( $ips as $ip ) {
421 if ( IP::isIPAddress( $ip ) ) {
422 $validIPs[] = $ip;
423 }
424 }
425
426 return $validIPs;
427 }
428
434 private function badLoginKey( BagOStuff $cache ) {
435 global $wgRequest;
436 $ip = $wgRequest->getIP();
437
438 return $cache->makeGlobalKey( 'captcha', 'badlogin', 'ip', $ip );
439 }
440
447 private function badLoginPerUserKey( $username, BagOStuff $cache ) {
448 $username = User::getCanonicalName( $username, 'usable' ) ?: $username;
449
450 return $cache->makeGlobalKey(
451 'captcha', 'badlogin', 'user', md5( $username )
452 );
453 }
454
465 protected function keyMatch( $answer, $info ) {
466 return $answer == $info['answer'];
467 }
468
469 // ----------------------------------
470
477 public function captchaTriggers( $title, $action ) {
478 return $this->triggersCaptcha( $action, $title );
479 }
480
491 public function triggersCaptcha( $action, $title = null ) {
492 global $wgCaptchaTriggers, $wgCaptchaTriggersOnNamespace;
493
494 $result = false;
495 $triggers = $wgCaptchaTriggers;
496 $attributeCaptchaTriggers = ExtensionRegistry::getInstance()
498 if ( is_array( $attributeCaptchaTriggers ) ) {
499 $triggers += $attributeCaptchaTriggers;
500 }
501
502 if ( isset( $triggers[$action] ) ) {
503 $result = $triggers[$action];
504 }
505
506 if (
507 $title !== null &&
508 isset( $wgCaptchaTriggersOnNamespace[$title->getNamespace()][$action] )
509 ) {
510 $result = $wgCaptchaTriggersOnNamespace[$title->getNamespace()][$action];
511 }
512
513 return $result;
514 }
515
525 public function shouldCheck( WikiPage $page, $content, $section, $context, $oldtext = null ) {
526 if ( !$context instanceof IContextSource ) {
527 $context = RequestContext::getMain();
528 }
529
530 $request = $context->getRequest();
531 $user = $context->getUser();
532
533 if ( $this->canSkipCaptcha( $user, $context->getConfig() ) ) {
534 return false;
535 }
536
537 $title = $page->getTitle();
538 $this->trigger = '';
539
540 if ( $content instanceof Content ) {
541 if ( $content->getModel() == CONTENT_MODEL_WIKITEXT ) {
542 $newtext = $content->getNativeData();
543 } else {
544 $newtext = null;
545 }
546 $isEmpty = $content->isEmpty();
547 } else {
548 $newtext = $content;
549 $isEmpty = $content === '';
550 }
551
552 if ( $this->triggersCaptcha( 'edit', $title ) ) {
553 // Check on all edits
554 $this->trigger = sprintf( "edit trigger by '%s' at [[%s]]",
555 $user->getName(),
556 $title->getPrefixedText() );
557 $this->action = 'edit';
558 wfDebug( "ConfirmEdit: checking all edits...\n" );
559 return true;
560 }
561
562 if ( $this->triggersCaptcha( 'create', $title ) && !$title->exists() ) {
563 // Check if creating a page
564 $this->trigger = sprintf( "Create trigger by '%s' at [[%s]]",
565 $user->getName(),
566 $title->getPrefixedText() );
567 $this->action = 'create';
568 wfDebug( "ConfirmEdit: checking on page creation...\n" );
569 return true;
570 }
571
572 // The following checks are expensive and should be done only,
573 // if we can assume, that the edit will be saved
574 if ( !$request->wasPosted() ) {
575 wfDebug(
576 "ConfirmEdit: request not posted, assuming that no content will be saved -> no CAPTCHA check"
577 );
578 return false;
579 }
580
581 if ( !$isEmpty && $this->triggersCaptcha( 'addurl', $title ) ) {
582 // Only check edits that add URLs
583 if ( $content instanceof Content ) {
584 // Get links from the database
585 $oldLinks = $this->getLinksFromTracker( $title );
586 // Share a parse operation with Article::doEdit()
587 $editInfo = $page->prepareContentForEdit( $content );
588 if ( $editInfo->output ) {
589 $newLinks = array_keys( $editInfo->output->getExternalLinks() );
590 } else {
591 $newLinks = [];
592 }
593 } else {
594 // Get link changes in the slowest way known to man
595 if ( $oldtext === null ) {
596 $oldtext = $this->loadText( $title, $section );
597 }
598 $oldLinks = $this->findLinks( $title, $oldtext );
599 $newLinks = $this->findLinks( $title, $newtext );
600 }
601
602 $unknownLinks = array_filter( $newLinks, [ $this, 'filterLink' ] );
603 $addedLinks = array_diff( $unknownLinks, $oldLinks );
604 $numLinks = count( $addedLinks );
605
606 if ( $numLinks > 0 ) {
607 $this->trigger = sprintf( "%dx url trigger by '%s' at [[%s]]: %s",
608 $numLinks,
609 $user->getName(),
610 $title->getPrefixedText(),
611 implode( ", ", $addedLinks ) );
612 $this->action = 'addurl';
613 return true;
614 }
615 }
616
617 global $wgCaptchaRegexes;
618 if ( $newtext !== null && $wgCaptchaRegexes ) {
619 if ( !is_array( $wgCaptchaRegexes ) ) {
620 throw new UnexpectedValueException(
621 '$wgCaptchaRegexes is required to be an array, ' . gettype( $wgCaptchaRegexes ) . ' given.'
622 );
623 }
624 // Custom regex checks. Reuse $oldtext if set above.
625 if ( $oldtext === null ) {
626 $oldtext = $this->loadText( $title, $section );
627 }
628
629 foreach ( $wgCaptchaRegexes as $regex ) {
630 $newMatches = [];
631 if ( preg_match_all( $regex, $newtext, $newMatches ) ) {
632 $oldMatches = [];
633 preg_match_all( $regex, $oldtext, $oldMatches );
634
635 $addedMatches = array_diff( $newMatches[0], $oldMatches[0] );
636
637 $numHits = count( $addedMatches );
638 if ( $numHits > 0 ) {
639 $this->trigger = sprintf( "%dx %s at [[%s]]: %s",
640 $numHits,
641 $regex,
642 $user->getName(),
643 $title->getPrefixedText(),
644 implode( ", ", $addedMatches ) );
645 $this->action = 'edit';
646 return true;
647 }
648 }
649 }
650 }
651
652 return false;
653 }
654
660 private function filterLink( $url ) {
661 global $wgCaptchaWhitelist;
662 static $regexes = null;
663
664 if ( $regexes === null ) {
665 $source = wfMessage( 'captcha-addurl-whitelist' )->inContentLanguage();
666
667 $regexes = $source->isDisabled()
668 ? []
669 : $this->buildRegexes( explode( "\n", $source->plain() ) );
670
671 if ( $wgCaptchaWhitelist !== false ) {
672 array_unshift( $regexes, $wgCaptchaWhitelist );
673 }
674 }
675
676 foreach ( $regexes as $regex ) {
677 if ( preg_match( $regex, $url ) ) {
678 return false;
679 }
680 }
681
682 return true;
683 }
684
691 private function buildRegexes( $lines ) {
692 # Code duplicated from the SpamBlacklist extension (r19197)
693 # and later modified.
694
695 # Strip comments and whitespace, then remove blanks
696 $lines = array_filter( array_map( 'trim', preg_replace( '/#.*$/', '', $lines ) ) );
697
698 # No lines, don't make a regex which will match everything
699 if ( count( $lines ) == 0 ) {
700 wfDebug( "No lines\n" );
701 return [];
702 } else {
703 # Make regex
704 # It's faster using the S modifier even though it will usually only be run once
705 // $regex = 'http://+[a-z0-9_\-.]*(' . implode( '|', $lines ) . ')';
706 // return '/' . str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $regex) ) . '/Si';
707 $regexes = [];
708 $regexStart = [
709 'normal' => '/^(?:https?:)?\/\/+[a-z0-9_\-.]*(?:',
710 'noprotocol' => '/^(?:',
711 ];
712 $regexEnd = [
713 'normal' => ')/Si',
714 'noprotocol' => ')/Si',
715 ];
716 $regexMax = 4096;
717 $build = [];
718 foreach ( $lines as $line ) {
719 # Extract flags from the line
720 $options = [];
721 if ( preg_match( '/^(.*?)\s*<([^<>]*)>$/', $line, $matches ) ) {
722 if ( $matches[1] === '' ) {
723 wfDebug( "Line with empty regex\n" );
724 continue;
725 }
726 $line = $matches[1];
727 $opts = preg_split( '/\s*\|\s*/', trim( $matches[2] ) );
728 foreach ( $opts as $opt ) {
729 $opt = strtolower( $opt );
730 if ( $opt == 'noprotocol' ) {
731 $options['noprotocol'] = true;
732 }
733 }
734 }
735
736 $key = isset( $options['noprotocol'] ) ? 'noprotocol' : 'normal';
737
738 // FIXME: not very robust size check, but should work. :)
739 if ( !isset( $build[$key] ) ) {
740 $build[$key] = $line;
741 } elseif ( strlen( $build[$key] ) + strlen( $line ) > $regexMax ) {
742 $regexes[] = $regexStart[$key] .
743 str_replace( '/', '\/', preg_replace( '|\\\*/|', '/', $build[$key] ) ) .
744 $regexEnd[$key];
745 $build[$key] = $line;
746 } else {
747 $build[$key] .= '|' . $line;
748 }
749 }
750 foreach ( $build as $key => $value ) {
751 $regexes[] = $regexStart[$key] .
752 str_replace( '/', '\/', preg_replace( '|\\\*/|', '/', $build[$key] ) ) .
753 $regexEnd[$key];
754 }
755 return $regexes;
756 }
757 }
758
764 private function getLinksFromTracker( $title ) {
766 // should be zero queries
767 $id = $title->getArticleID();
768 $res = $dbr->select( 'externallinks', [ 'el_to' ],
769 [ 'el_from' => $id ], __METHOD__ );
770 $links = [];
771 foreach ( $res as $row ) {
772 $links[] = $row->el_to;
773 }
774 return $links;
775 }
776
785 private function doConfirmEdit( WikiPage $page, $newtext, $section, IContextSource $context ) {
786 global $wgUser, $wgRequest;
787 $request = $context->getRequest();
788
789 // FIXME: Stop using wgRequest in other parts of ConfirmEdit so we can
790 // stop having to duplicate code for it.
791 if ( $request->getVal( 'captchaid' ) ) {
792 $request->setVal( 'wpCaptchaId', $request->getVal( 'captchaid' ) );
793 $wgRequest->setVal( 'wpCaptchaId', $request->getVal( 'captchaid' ) );
794 }
795 if ( $request->getVal( 'captchaword' ) ) {
796 $request->setVal( 'wpCaptchaWord', $request->getVal( 'captchaword' ) );
797 $wgRequest->setVal( 'wpCaptchaWord', $request->getVal( 'captchaword' ) );
798 }
799 if ( $this->shouldCheck( $page, $newtext, $section, $context ) ) {
800 return $this->passCaptchaLimitedFromRequest( $wgRequest, $wgUser );
801 } else {
802 wfDebug( "ConfirmEdit: no need to show captcha.\n" );
803 return true;
804 }
805 }
806
817 public function confirmEditMerged( $context, $content, $status, $summary, $user, $minorEdit ) {
818 if ( !$context->canUseWikiPage() ) {
819 // we check WikiPage only
820 // try to get an appropriate title for this page
821 $title = $context->getTitle();
822 if ( $title instanceof Title ) {
823 $title = $title->getFullText();
824 } else {
825 // otherwise it's an unknown page where this function is called from
826 $title = 'unknown';
827 }
828 // log this error, it could be a problem in another extension,
829 // edits should always have a WikiPage if
830 // they go through EditFilterMergedContent.
831 wfDebug( __METHOD__ . ': Skipped ConfirmEdit check: No WikiPage for title ' . $title );
832 return true;
833 }
834 $page = $context->getWikiPage();
835 if ( !$this->doConfirmEdit( $page, $content, '', $context ) ) {
836 $status->value = EditPage::AS_HOOK_ERROR_EXPECTED;
837 $status->apiHookResult = [];
838 // give an error message for the user to know, what goes wrong here.
839 // this can't be done for addurl trigger, because this requires one "free" save
840 // for the user, which we don't know, when he did it.
841 if ( $this->action === 'edit' ) {
842 $status->fatal(
843 new RawMessage(
844 Html::element(
845 'div',
846 [ 'class' => 'errorbox' ],
847 $context->msg( 'captcha-edit-fail' )->text()
848 )
849 )
850 );
851 }
852 $this->addCaptchaAPI( $status->apiHookResult );
853 $page->ConfirmEdit_ActivateCaptcha = true;
854 return false;
855 }
856 return true;
857 }
858
866 public function needCreateAccountCaptcha( User $creatingUser = null ) {
867 global $wgUser;
868 $creatingUser = $creatingUser ?: $wgUser;
869
871 if ( $this->canSkipCaptcha( $creatingUser,
872 \MediaWiki\MediaWikiServices::getInstance()->getMainConfig() ) ) {
873 return false;
874 }
875 return true;
876 }
877 return false;
878 }
879
889 public function confirmEmailUser( $from, $to, $subject, $text, &$error ) {
890 global $wgUser, $wgRequest;
891
893 if ( $this->canSkipCaptcha( $wgUser,
894 \MediaWiki\MediaWikiServices::getInstance()->getMainConfig() ) ) {
895 return true;
896 }
897
898 if ( defined( 'MW_API' ) ) {
899 # API mode
900 # Asking for captchas in the API is really silly
901 $error = Status::newFatal( 'captcha-disabledinapi' );
902 return false;
903 }
904 $this->trigger = "{$wgUser->getName()} sending email";
905 if ( !$this->passCaptchaLimitedFromRequest( $wgRequest, $wgUser ) ) {
906 $error = Status::newFatal( 'captcha-sendemail-fail' );
907 return false;
908 }
909 }
910 return true;
911 }
912
917 protected function isAPICaptchaModule( $module ) {
918 return $module instanceof ApiEditPage;
919 }
920
927 public function apiGetAllowedParams( &$module, &$params, $flags ) {
928 if ( $this->isAPICaptchaModule( $module ) ) {
929 $params['captchaword'] = [
930 ApiBase::PARAM_HELP_MSG => 'captcha-apihelp-param-captchaword',
931 ];
932 $params['captchaid'] = [
933 ApiBase::PARAM_HELP_MSG => 'captcha-apihelp-param-captchaid',
934 ];
935 }
936
937 return true;
938 }
939
948 public function passCaptchaLimitedFromRequest( WebRequest $request, User $user ) {
949 list( $index, $word ) = $this->getCaptchaParamsFromRequest( $request );
950 return $this->passCaptchaLimited( $index, $word, $user );
951 }
952
957 protected function getCaptchaParamsFromRequest( WebRequest $request ) {
958 $index = $request->getVal( 'wpCaptchaId' );
959 $word = $request->getVal( 'wpCaptchaWord' );
960 return [ $index, $word ];
961 }
962
973 public function passCaptchaLimited( $index, $word, User $user ) {
974 // don't increase pingLimiter here, just check, if CAPTCHA limit exceeded
975 if ( $user->pingLimiter( 'badcaptcha', 0 ) ) {
976 // for debugging add an proper error message, the user just see an false captcha error message
977 $this->log( 'User reached RateLimit, preventing action' );
978 return false;
979 }
980
981 if ( $this->passCaptcha( $index, $word ) ) {
982 return true;
983 }
984
985 // captcha was not solved: increase limit and return false
986 $user->pingLimiter( 'badcaptcha' );
987 return false;
988 }
989
997 public function passCaptchaFromRequest( WebRequest $request, User $user ) {
998 list( $index, $word ) = $this->getCaptchaParamsFromRequest( $request );
999 return $this->passCaptcha( $index, $word );
1000 }
1001
1009 protected function passCaptcha( $index, $word ) {
1010 // Don't check the same CAPTCHA twice in one session,
1011 // if the CAPTCHA was already checked - Bug T94276
1012 if ( isset( $this->captchaSolved ) ) {
1013 return $this->captchaSolved;
1014 }
1015
1016 $info = $this->retrieveCaptcha( $index );
1017 if ( $info ) {
1018 if ( $this->keyMatch( $word, $info ) ) {
1019 $this->log( "passed" );
1020 $this->clearCaptcha( $index );
1021 $this->captchaSolved = true;
1022 return true;
1023 } else {
1024 $this->clearCaptcha( $index );
1025 $this->log( "bad form input" );
1026 $this->captchaSolved = false;
1027 return false;
1028 }
1029 } else {
1030 $this->log( "new captcha session" );
1031 return false;
1032 }
1033 }
1034
1039 protected function log( $message ) {
1040 wfDebugLog( 'captcha', 'ConfirmEdit: ' . $message . '; ' . $this->trigger );
1041 }
1042
1054 public function storeCaptcha( $info ) {
1055 if ( !isset( $info['index'] ) ) {
1056 // Assign random index if we're not udpating
1057 $info['index'] = strval( mt_rand() );
1058 }
1059 CaptchaStore::get()->store( $info['index'], $info );
1060 return $info['index'];
1061 }
1062
1068 public function retrieveCaptcha( $index ) {
1069 return CaptchaStore::get()->retrieve( $index );
1070 }
1071
1077 public function clearCaptcha( $index ) {
1078 CaptchaStore::get()->clear( $index );
1079 }
1080
1089 private function loadText( $title, $section, $flags = Revision::READ_LATEST ) {
1090 global $wgParser;
1091
1092 $rev = Revision::newFromTitle( $title, false, $flags );
1093 if ( is_null( $rev ) ) {
1094 return "";
1095 }
1096
1097 $content = $rev->getContent();
1098 $text = ContentHandler::getContentText( $content );
1099 if ( $section !== '' ) {
1100 return $wgParser->getSection( $text, $section );
1101 }
1102
1103 return $text;
1104 }
1105
1112 private function findLinks( $title, $text ) {
1113 global $wgParser, $wgUser;
1114
1115 $options = new ParserOptions();
1116 $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
1117 $out = $wgParser->parse( $text, $title, $options );
1118
1119 return array_keys( $out->getExternalLinks() );
1120 }
1121
1125 public function showHelp() {
1126 global $wgOut;
1127 $wgOut->setPageTitle( wfMessage( 'captchahelp-title' )->text() );
1128 $wgOut->addWikiMsg( 'captchahelp-text' );
1129 if ( CaptchaStore::get()->cookiesNeeded() ) {
1130 $wgOut->addWikiMsg( 'captchahelp-cookies-needed' );
1131 }
1132 }
1133
1138 $captchaData = $this->getCaptcha();
1139 $id = $this->storeCaptcha( $captchaData );
1140 return new CaptchaAuthenticationRequest( $id, $captchaData );
1141 }
1142
1150 public function onAuthChangeFormFields(
1151 array $requests, array $fieldInfo, array &$formDescriptor, $action
1152 ) {
1153 $req = AuthenticationRequest::getRequestByClass( $requests,
1154 CaptchaAuthenticationRequest::class );
1155 if ( !$req ) {
1156 return;
1157 }
1158
1159 $formDescriptor['captchaWord'] = [
1160 'label-message' => null,
1161 'autocomplete' => false,
1162 'persistent' => false,
1163 'required' => true,
1164 ] + $formDescriptor['captchaWord'];
1165 }
1166
1174 public function canSkipCaptcha( $user, Config $config ) {
1175 $allowConfirmEmail = $config->get( 'AllowConfirmedEmail' );
1176
1177 if ( $user->isAllowed( 'skipcaptcha' ) ) {
1178 wfDebug( "ConfirmEdit: user group allows skipping captcha\n" );
1179 return true;
1180 }
1181
1182 if ( $this->isIPWhitelisted() ) {
1183 wfDebug( "ConfirmEdit: user IP is whitelisted" );
1184 return true;
1185 }
1186
1187 if ( $allowConfirmEmail && $user->isEmailConfirmed() ) {
1188 wfDebug( "ConfirmEdit: user has confirmed mail, skipping captcha\n" );
1189 return true;
1190 }
1191
1192 return false;
1193 }
1194}
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$wgOut
Definition Setup.php:885
$wgParser
Definition Setup.php:891
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:751
$line
Definition cdb.php:59
A module that allows for editing and creating pages.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:63
Generic captcha authentication request class.
static get()
Get somewhere to store captcha data that will persist between requests.
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition EditPage.php:70
This is a value object for authentication requests.
MediaWikiServices is the service locator for the application scope of MediaWiki.
The Message class provides methods which fulfil two basic services:
Definition Message.php:162
plain()
Returns the message text as-is, only parameters are substituted.
Definition Message.php:954
This is one of the Core classes and should be read at least once by any new developers.
addModuleStyles( $modules)
Load the styles of one or more ResourceLoader modules on this page.
addHTML( $text)
Append $text to the body HTML.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
addHeadItems( $values)
Add one or more head items to the output.
Set options of the Parser.
Variant of the Message class.
Demo CAPTCHA (not for production usage) and base class for real CAPTCHAs.
getMessage( $action)
Show a message asking the user to enter a captcha on edit The result will be treated as wiki text.
buildRegexes( $lines)
Build regex from whitelist.
findLinks( $title, $text)
Extract a list of all recognized HTTP links in the text.
isIPWhitelisted()
Check if the current IP is allowed to skip captchas.
isBadLoginTriggered()
Check if a bad login has already been registered for this IP address.
addFormToOutput(OutputPage $out, $tabIndex=1)
Uses getFormInformation() to get the CAPTCHA form and adds it to the given OutputPage object.
retrieveCaptcha( $index)
Fetch this session's captcha info.
static $messagePrefix
log( $message)
Log the status and any triggering info for debugging or statistics.
isAPICaptchaModule( $module)
loadText( $title, $section, $flags=Revision::READ_LATEST)
Retrieve the current version of the page or section being edited...
triggersCaptcha( $action, $title=null)
Checks, whether the passed action should trigger a CAPTCHA.
keyMatch( $answer, $info)
Check if the submitted form matches the captcha session data provided by the plugin when the form was...
passCaptchaFromRequest(WebRequest $request, User $user)
Given a required captcha run, test form input for correct input on the open session.
getCaptchaInfo( $captchaData, $id)
showEditFormFields(&$editPage, &$out)
Show error message for missing or incorrect captcha on EditPage.
getError()
Return the error from the last passCaptcha* call.
editShowCaptcha( $editPage)
Insert the captcha prompt into an edit form.
needCreateAccountCaptcha(User $creatingUser=null)
Logic to check if we need to pass a captcha for the current user to create a new account,...
showHelp()
Show a page explaining what this wacky thing is.
filterLink( $url)
Filter callback function for URL whitelisting.
injectEmailUser(&$form)
Inject whazawhoo @fixme if multiple thingies insert a header, could break.
getLinksFromTracker( $title)
Load external links from the externallinks table.
string $trigger
Used in log messages.
getFormInformation( $tabIndex=1)
Insert a captcha prompt into the edit form.
string $action
Used to select the right message.
confirmEditMerged( $context, $content, $status, $summary, $user, $minorEdit)
An efficient edit filter callback based on the text after section merging.
buildValidIPs(array $input)
From a list of unvalidated input, get all the valid IP addresses and IP ranges from it.
apiGetAllowedParams(&$module, &$params, $flags)
isBadLoginPerUserTriggered( $u)
Is the per-user captcha triggered?
confirmEmailUser( $from, $to, $subject, $text, &$error)
Check the captcha on Special:EmailUser.
passCaptchaLimited( $index, $word, User $user)
Checks, if the user reached the amount of false CAPTCHAs and give him some vacation or run self::pass...
passCaptchaLimitedFromRequest(WebRequest $request, User $user)
Checks, if the user reached the amount of false CAPTCHAs and give him some vacation or run self::pass...
addFormInformationToOutput(OutputPage $out, array $formInformation)
Processes the given $formInformation array and adds the options (see getFormInformation()) to the giv...
setAction( $action)
getWikiIPWhitelist(Message $msg)
Get the on-wiki IP whitelist stored in [[MediaWiki:Captcha-ip-whitelist]] page from cache if possible...
badLoginPerUserKey( $username, BagOStuff $cache)
Cache key for badloginPerUser checks.
storeCaptcha( $info)
Generate a captcha session ID and save the info in PHP's session storage.
canSkipCaptcha( $user, Config $config)
Check whether the user provided / IP making the request is allowed to skip captchas.
badLoginKey(BagOStuff $cache)
Internal cache key for badlogin checks.
increaseBadLoginCounter( $username)
Increase bad login counter after a failed login.
captchaTriggers( $title, $action)
clearCaptcha( $index)
Clear out existing captcha info from the session, to ensure it can't be reused.
passCaptcha( $index, $word)
Given a required captcha run, test form input for correct input on the open session.
getCaptchaParamsFromRequest(WebRequest $request)
shouldCheck(WikiPage $page, $content, $section, $context, $oldtext=null)
addCaptchaAPI(&$resultArr)
doConfirmEdit(WikiPage $page, $newtext, $section, IContextSource $context)
Backend function for confirmEditMerged()
setTrigger( $trigger)
getCaptcha()
Returns an array with 'question' and 'answer' keys.
describeCaptchaType()
Describes the captcha type for API clients.
boolean null $captchaSolved
Was the CAPTCHA already passed and if yes, with which result?
onAuthChangeFormFields(array $requests, array $fieldInfo, array &$formDescriptor, $action)
Modify the appearance of the captcha field.
resetBadLoginCounter( $username)
Reset bad login counter after a successful login.
Represents a title within MediaWiki.
Definition Title.php:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition User.php:1954
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
getVal( $name, $default=null)
Fetch a scalar from the input or return $default if it's not set.
Class representing a MediaWiki article and history.
Definition WikiPage.php:47
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
getTitle()
Get the title object of the article.
Definition WikiPage.php:298
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:224
Interface for configuration instances.
Definition Config.php:28
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Base interface for content objects.
Definition Content.php:34
Interface for objects which can provide a MediaWiki context on request.
$context
Definition load.php:45
$cache
Definition mcc.php:33
$source
This class serves as a utility class for this extension.
const DB_REPLICA
Definition defines.php:25
$lines
Definition router.php:61
$content
Definition router.php:78