MediaWiki  1.34.0
SimpleCaptcha.php
Go to the documentation of this file.
1 <?php
2 
5 
9 class SimpleCaptcha {
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 
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 ) {
307  $cache->delete( $this->badLoginPerUserKey( $username, $cache ) );
308  }
309  }
310 
317  public function isBadLoginTriggered() {
318  global $wgCaptchaBadLoginAttempts;
319 
322  && (int)$cache->get( $this->badLoginKey( $cache ) ) >= $wgCaptchaBadLoginAttempts;
323  }
324 
331  public function isBadLoginPerUserTriggered( $u ) {
332  global $wgCaptchaBadLoginPerUserAttempts;
333 
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()
497  ->getAttribute( CaptchaTriggers::EXT_REG_ATTRIBUTE_NAME );
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 ) {
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 ) {
765  $dbr = wfGetDB( DB_REPLICA );
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 ) ) {
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();
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 
1137  public function createAuthenticationRequest() {
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 }
ApiEditPage
A module that allows for editing and creating pages.
Definition: ApiEditPage.php:35
SimpleCaptcha\confirmEmailUser
confirmEmailUser( $from, $to, $subject, $text, &$error)
Check the captcha on Special:EmailUser.
Definition: SimpleCaptcha.php:889
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:42
CaptchaStore\get
static get()
Get somewhere to store captcha data that will persist between requests.
Definition: CaptchaStore.php:42
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:342
$wgParser
$wgParser
Definition: Setup.php:892
SimpleCaptcha\loadText
loadText( $title, $section, $flags=Revision::READ_LATEST)
Retrieve the current version of the page or section being edited...
Definition: SimpleCaptcha.php:1089
CaptchaTriggers\SENDEMAIL
const SENDEMAIL
Definition: CaptchaTriggers.php:10
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
SimpleCaptcha\passCaptchaLimitedFromRequest
passCaptchaLimitedFromRequest(WebRequest $request, User $user)
Checks, if the user reached the amount of false CAPTCHAs and give him some vacation or run self::pass...
Definition: SimpleCaptcha.php:948
IP\isInRanges
static isInRanges( $ip, $ranges)
Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
Definition: IP.php:655
SimpleCaptcha\addFormToOutput
addFormToOutput(OutputPage $out, $tabIndex=1)
Uses getFormInformation() to get the CAPTCHA form and adds it to the given OutputPage object.
Definition: SimpleCaptcha.php:160
ApiBase\PARAM_HELP_MSG
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:131
SimpleCaptcha\keyMatch
keyMatch( $answer, $info)
Check if the submitted form matches the captcha session data provided by the plugin when the form was...
Definition: SimpleCaptcha.php:465
SimpleCaptcha\editShowCaptcha
editShowCaptcha( $editPage)
Insert the captcha prompt into an edit form.
Definition: SimpleCaptcha.php:221
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:47
SimpleCaptcha\badLoginKey
badLoginKey(BagOStuff $cache)
Internal cache key for badlogin checks.
Definition: SimpleCaptcha.php:434
SimpleCaptcha\captchaTriggers
captchaTriggers( $title, $action)
Definition: SimpleCaptcha.php:477
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:63
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1264
SimpleCaptcha\$action
string $action
Used to select the right message.
Definition: SimpleCaptcha.php:20
CONTENT_MODEL_WIKITEXT
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:215
User\pingLimiter
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1911
Message
SimpleCaptcha\showHelp
showHelp()
Show a page explaining what this wacky thing is.
Definition: SimpleCaptcha.php:1125
$res
$res
Definition: testCompression.php:52
SimpleCaptcha\setAction
setAction( $action)
Definition: SimpleCaptcha.php:28
SimpleCaptcha\injectEmailUser
injectEmailUser(&$form)
Inject whazawhoo @fixme if multiple thingies insert a header, could break.
Definition: SimpleCaptcha.php:257
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:1007
SimpleCaptcha\buildRegexes
buildRegexes( $lines)
Build regex from whitelist.
Definition: SimpleCaptcha.php:691
$dbr
$dbr
Definition: testCompression.php:50
SimpleCaptcha\getCaptchaInfo
getCaptchaInfo( $captchaData, $id)
Definition: SimpleCaptcha.php:194
ExtensionRegistry\getInstance
static getInstance()
Definition: ExtensionRegistry.php:106
Revision\newFromTitle
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: Revision.php:138
SimpleCaptcha\canSkipCaptcha
canSkipCaptcha( $user, Config $config)
Check whether the user provided / IP making the request is allowed to skip captchas.
Definition: SimpleCaptcha.php:1174
Config
Interface for configuration instances.
Definition: Config.php:28
SimpleCaptcha\passCaptchaLimited
passCaptchaLimited( $index, $word, User $user)
Checks, if the user reached the amount of false CAPTCHAs and give him some vacation or run self::pass...
Definition: SimpleCaptcha.php:973
SimpleCaptcha\addCaptchaAPI
addCaptchaAPI(&$resultArr)
Definition: SimpleCaptcha.php:72
CaptchaTriggers\CREATE_ACCOUNT
const CREATE_ACCOUNT
Definition: CaptchaTriggers.php:12
SimpleCaptcha\resetBadLoginCounter
resetBadLoginCounter( $username)
Reset bad login counter after a successful login.
Definition: SimpleCaptcha.php:304
CaptchaTriggers\BAD_LOGIN
const BAD_LOGIN
Definition: CaptchaTriggers.php:13
SimpleCaptcha\getCaptcha
getCaptcha()
Returns an array with 'question' and 'answer' keys.
Definition: SimpleCaptcha.php:54
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
SimpleCaptcha\filterLink
filterLink( $url)
Filter callback function for URL whitelisting.
Definition: SimpleCaptcha.php:660
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2575
$matches
$matches
Definition: NoLocalSettings.php:24
SimpleCaptcha\findLinks
findLinks( $title, $text)
Extract a list of all recognized HTTP links in the text.
Definition: SimpleCaptcha.php:1112
SimpleCaptcha\getWikiIPWhitelist
getWikiIPWhitelist(Message $msg)
Get the on-wiki IP whitelist stored in [[MediaWiki:Captcha-ip-whitelist]] page from cache if possible...
Definition: SimpleCaptcha.php:380
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:298
MediaWiki
This class serves as a utility class for this extension.
SimpleCaptcha\getCaptchaParamsFromRequest
getCaptchaParamsFromRequest(WebRequest $request)
Definition: SimpleCaptcha.php:957
$lines
$lines
Definition: router.php:61
$title
$title
Definition: testCompression.php:34
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
SimpleCaptcha\onAuthChangeFormFields
onAuthChangeFormFields(array $requests, array $fieldInfo, array &$formDescriptor, $action)
Modify the appearance of the captcha field.
Definition: SimpleCaptcha.php:1150
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
SimpleCaptcha\apiGetAllowedParams
apiGetAllowedParams(&$module, &$params, $flags)
Definition: SimpleCaptcha.php:927
SimpleCaptcha\badLoginPerUserKey
badLoginPerUserKey( $username, BagOStuff $cache)
Cache key for badloginPerUser checks.
Definition: SimpleCaptcha.php:447
SimpleCaptcha\doConfirmEdit
doConfirmEdit(WikiPage $page, $newtext, $section, IContextSource $context)
Backend function for confirmEditMerged()
Definition: SimpleCaptcha.php:785
$line
$line
Definition: cdb.php:59
SimpleCaptcha\confirmEditMerged
confirmEditMerged( $context, $content, $status, $summary, $user, $minorEdit)
An efficient edit filter callback based on the text after section merging.
Definition: SimpleCaptcha.php:817
$content
$content
Definition: router.php:78
SimpleCaptcha\isIPWhitelisted
isIPWhitelisted()
Check if the current IP is allowed to skip captchas.
Definition: SimpleCaptcha.php:352
SimpleCaptcha\isBadLoginTriggered
isBadLoginTriggered()
Check if a bad login has already been registered for this IP address.
Definition: SimpleCaptcha.php:317
SimpleCaptcha\setTrigger
setTrigger( $trigger)
Definition: SimpleCaptcha.php:35
SimpleCaptcha\log
log( $message)
Log the status and any triggering info for debugging or statistics.
Definition: SimpleCaptcha.php:1039
SimpleCaptcha\retrieveCaptcha
retrieveCaptcha( $index)
Fetch this session's captcha info.
Definition: SimpleCaptcha.php:1068
SimpleCaptcha\createAuthenticationRequest
createAuthenticationRequest()
Definition: SimpleCaptcha.php:1137
SimpleCaptcha\needCreateAccountCaptcha
needCreateAccountCaptcha(User $creatingUser=null)
Logic to check if we need to pass a captcha for the current user to create a new account,...
Definition: SimpleCaptcha.php:866
SimpleCaptcha\$messagePrefix
static $messagePrefix
Definition: SimpleCaptcha.php:10
SimpleCaptcha\addFormInformationToOutput
addFormInformationToOutput(OutputPage $out, array $formInformation)
Processes the given $formInformation array and adds the options (see getFormInformation()) to the giv...
Definition: SimpleCaptcha.php:171
SimpleCaptcha
Demo CAPTCHA (not for production usage) and base class for real CAPTCHAs.
Definition: SimpleCaptcha.php:9
SimpleCaptcha\getError
getError()
Return the error from the last passCaptcha* call.
Definition: SimpleCaptcha.php:44
SimpleCaptcha\storeCaptcha
storeCaptcha( $info)
Generate a captcha session ID and save the info in PHP's session storage.
Definition: SimpleCaptcha.php:1054
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:431
CaptchaTriggers\EXT_REG_ATTRIBUTE_NAME
const EXT_REG_ATTRIBUTE_NAME
Definition: CaptchaTriggers.php:16
SimpleCaptcha\getMessage
getMessage( $action)
Show a message asking the user to enter a captcha on edit The result will be treated as wiki text.
Definition: SimpleCaptcha.php:241
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:53
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:42
$context
$context
Definition: load.php:45
SimpleCaptcha\describeCaptchaType
describeCaptchaType()
Describes the captcha type for API clients.
Definition: SimpleCaptcha.php:85
SimpleCaptcha\isBadLoginPerUserTriggered
isBadLoginPerUserTriggered( $u)
Is the per-user captcha triggered?
Definition: SimpleCaptcha.php:331
Content
Base interface for content objects.
Definition: Content.php:34
SimpleCaptcha\isAPICaptchaModule
isAPICaptchaModule( $module)
Definition: SimpleCaptcha.php:917
Title
Represents a title within MediaWiki.
Definition: Title.php:42
$status
return $status
Definition: SyntaxHighlight.php:347
SimpleCaptcha\$trigger
string $trigger
Used in log messages.
Definition: SimpleCaptcha.php:23
ContentHandler\getContentText
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
Definition: ContentHandler.php:85
SimpleCaptcha\shouldCheck
shouldCheck(WikiPage $page, $content, $section, $context, $oldtext=null)
Definition: SimpleCaptcha.php:525
$cache
$cache
Definition: mcc.php:33
SimpleCaptcha\increaseBadLoginCounter
increaseBadLoginCounter( $username)
Increase bad login counter after a failed login.
Definition: SimpleCaptcha.php:284
SimpleCaptcha\$captchaSolved
boolean null $captchaSolved
Was the CAPTCHA already passed and if yes, with which result?
Definition: SimpleCaptcha.php:13
WebRequest\getVal
getVal( $name, $default=null)
Fetch a scalar from the input or return $default if it's not set.
Definition: WebRequest.php:483
SimpleCaptcha\passCaptcha
passCaptcha( $index, $word)
Given a required captcha run, test form input for correct input on the open session.
Definition: SimpleCaptcha.php:1009
User\getCanonicalName
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid.
Definition: User.php:1139
SimpleCaptcha\getLinksFromTracker
getLinksFromTracker( $title)
Load external links from the externallinks table.
Definition: SimpleCaptcha.php:764
WikiPage\prepareContentForEdit
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:1991
$source
$source
Definition: mwdoc-filter.php:34
CaptchaTriggers\BAD_LOGIN_PER_USER
const BAD_LOGIN_PER_USER
Definition: CaptchaTriggers.php:14
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:752
SimpleCaptcha\triggersCaptcha
triggersCaptcha( $action, $title=null)
Checks, whether the passed action should trigger a CAPTCHA.
Definition: SimpleCaptcha.php:491
RawMessage
Variant of the Message class.
Definition: RawMessage.php:34
$wgOut
$wgOut
Definition: Setup.php:886
SimpleCaptcha\showEditFormFields
showEditFormFields(&$editPage, &$out)
Show error message for missing or incorrect captcha on EditPage.
Definition: SimpleCaptcha.php:203
SimpleCaptcha\getFormInformation
getFormInformation( $tabIndex=1)
Insert a captcha prompt into the edit form.
Definition: SimpleCaptcha.php:121
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
SimpleCaptcha\passCaptchaFromRequest
passCaptchaFromRequest(WebRequest $request, User $user)
Given a required captcha run, test form input for correct input on the open session.
Definition: SimpleCaptcha.php:997
EditPage\AS_HOOK_ERROR_EXPECTED
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: EditPage.php:70
MediaWiki\Auth\AuthenticationRequest
This is a value object for authentication requests.
Definition: AuthenticationRequest.php:37
IP\isIPAddress
static isIPAddress( $ip)
Determine if a string is as valid IP address or network (CIDR prefix).
Definition: IP.php:77
CaptchaAuthenticationRequest
Generic captcha authentication request class.
Definition: CaptchaAuthenticationRequest.php:10
SimpleCaptcha\buildValidIPs
buildValidIPs(array $input)
From a list of unvalidated input, get all the valid IP addresses and IP ranges from it.
Definition: SimpleCaptcha.php:414
SimpleCaptcha\clearCaptcha
clearCaptcha( $index)
Clear out existing captcha info from the session, to ensure it can't be reused.
Definition: SimpleCaptcha.php:1077