18 global $wgCaptchaFileBackend, $wgCaptchaDirectory;
20 if ( $wgCaptchaFileBackend ) {
23 static $backend =
null;
26 'name' =>
'captcha-backend',
29 'containerPaths' => [
'captcha-render' => $wgCaptchaDirectory ],
31 'obResetFunc' =>
'wfResetOutputBuffers',
32 'streamMimeFunc' => [
'StreamFile',
'contentTypeFromPath' ]
53 $files = $backend->getFileList(
54 [
'dir' => $backend->getRootStoragePath() .
'/captcha-render' ]
57 return iterator_count( $files );
68 protected function keyMatch( $answer, $info ) {
69 global $wgCaptchaSecret;
71 $digest = $wgCaptchaSecret . $info[
'salt'] . $answer . $wgCaptchaSecret . $info[
'salt'];
72 $answerHash = substr( md5( $digest ), 0, 16 );
74 if ( $answerHash == $info[
'hash'] ) {
75 wfDebug(
"FancyCaptcha: answer hash matches expected {$info['hash']}\n" );
78 wfDebug(
"FancyCaptcha: answer hashes to $answerHash, expected {$info['hash']}\n" );
89 $resultArr[
'captcha'][
'error'] =
'Out of images';
95 $resultArr[
'captcha'][
'id'] = $index;
96 $resultArr[
'captcha'][
'url'] =
$title->getLocalURL(
'wpCaptchaId=' . urlencode( $index ) );
105 'mime' =>
'image/png',
121 $modules[] =
'ext.confirmEdit.fancyCaptcha';
123 $captchaReload = Html::element(
126 'class' =>
'confirmedit-captcha-reload fancycaptcha-reload'
128 wfMessage(
'fancycaptcha-reload-text' )->text()
131 $form = Html::openElement(
'div' ) .
132 Html::element(
'label', [
133 'for' =>
'wpCaptchaWord',
135 wfMessage(
'captcha-label' )->text() .
' ' .
wfMessage(
'fancycaptcha-captcha' )->text()
137 Html::openElement(
'div', [
'class' =>
'fancycaptcha-captcha-container' ] ) .
138 Html::openElement(
'div', [
'class' =>
'fancycaptcha-captcha-and-reload' ] ) .
139 Html::openElement(
'div', [
'class' =>
'fancycaptcha-image-container' ] ) .
140 Html::element(
'img', [
141 'class' =>
'fancycaptcha-image',
142 'src' =>
$title->getLocalURL(
'wpCaptchaId=' . urlencode( $index ) ),
145 ) . $captchaReload . Html::closeElement(
'div' ) . Html::closeElement(
'div' ) .
"\n" .
146 Html::element(
'input', [
147 'name' =>
'wpCaptchaWord',
148 'class' =>
'mw-ui-input',
149 'id' =>
'wpCaptchaWord',
153 'autocomplete' =>
'off',
154 'autocorrect' =>
'off',
155 'autocapitalize' =>
'off',
156 'required' =>
'required',
158 'tabindex' => $tabIndex,
159 'placeholder' =>
wfMessage(
'fancycaptcha-imgcaptcha-ph' )->text()
162 if ( $this->action ==
'createaccount' ) {
164 $form .= Html::rawElement(
'small', [
165 'class' =>
'mw-createacct-captcha-assisted'
166 ],
wfMessage(
'createacct-imgcaptcha-help' )->parse()
169 $form .= Html::element(
'input', [
171 'name' =>
'wpCaptchaId',
172 'id' =>
'wpCaptchaId',
175 ) . Html::closeElement(
'div' ) . Html::closeElement(
'div' ) .
"\n";
181 'modulestyles' => [
'mediawiki.ui.input',
'ext.confirmEdit.fancyCaptcha.styles' ],
190 global $wgCaptchaDirectoryLevels;
194 $baseDir = $this->
getBackend()->getRootStoragePath() .
'/captcha-render';
195 return $this->
pickImageDir( $baseDir, $wgCaptchaDirectoryLevels, $lockouts );
207 if ( $levels <= 0 ) {
214 $key =
"fancycaptcha:dirlist:{$backend->getWikiId()}:" . sha1( $directory );
217 if ( !is_array(
$dirs ) || !count(
$dirs ) ) {
221 foreach ( $backend->getTopDirectoryList( [
'dir' => $directory ] ) as $entry ) {
222 if ( ctype_xdigit( $entry ) && strlen( $entry ) == 1 ) {
226 wfDebug(
"Cache miss for $directory subdirectory listing.\n" );
227 if ( count(
$dirs ) ) {
232 if ( !count(
$dirs ) ) {
234 $backend->clean( [
'dir' => $directory ] );
240 $place = mt_rand( 0, count(
$dirs ) - 1 );
242 $fancyCount = count(
$dirs );
243 for ( $j = 0; $j < $fancyCount; $j++ ) {
244 $char =
$dirs[( $place + $j ) % count(
$dirs )];
245 $info = $this->
pickImageDir(
"$directory/$char", $levels - 1, $lockouts );
250 wfDebug(
"Could not find captcha in $directory.\n" );
270 $key =
"fancycaptcha:filelist:{$backend->getWikiId()}:" . sha1( $directory );
273 if ( !is_array( $files ) || !count( $files ) ) {
276 foreach ( $backend->getTopFileList( [
'dir' => $directory ] ) as $entry ) {
278 if ( count( $files ) >= 500 ) {
280 wfDebug(
'Skipping some captchas; $wgCaptchaDirectoryLevels set too low?.' );
284 if ( count( $files ) ) {
285 $wgMemc->set( $key, $files, 86400 );
287 wfDebug(
"Cache miss for $directory captcha listing.\n" );
290 if ( !count( $files ) ) {
292 $backend->clean( [
'dir' => $directory ] );
298 wfDebug(
"Could not find captcha in $directory.\n" );
313 global
$wgMemc, $wgCaptchaDeleteOnSolve;
315 if ( !count( $files ) ) {
322 $place = mt_rand( 0, count( $files ) - 1 );
325 $fancyImageCount = count( $files );
326 for ( $j = 0; $j < $fancyImageCount; $j++ ) {
327 $entry = $files[( $place + $j ) % count( $files )];
328 if ( preg_match(
'/^image_([0-9a-f]+)_([0-9a-f]+)\\.png$/', $entry,
$matches ) ) {
329 if ( $wgCaptchaDeleteOnSolve ) {
331 $key =
"fancycaptcha:filelock:{$backend->getWikiId()}:" . sha1( $entry );
333 if ( ++$lockouts <= 10 && !$wgMemc->add( $key,
'1', 600 ) ) {
338 if ( !$backend->fileExists( [
'src' =>
"$directory/$entry" ] ) ) {
339 if ( ++$misses >= 5 ) {
371 $info[
'viewed'] = $timestamp->getTimestamp();
374 $salt = $info[
'salt'];
375 $hash = $info[
'hash'];
378 'src' => $this->
imagePath( $salt, $hash ),
379 'headers' => [
"Cache-Control: private, s-maxage=0, max-age=3600" ]
383 wfHttpError( 400,
'Request Error',
'Requested bogus captcha image' );
393 global $wgCaptchaDirectoryLevels;
395 $file = $this->
getBackend()->getRootStoragePath() .
'/captcha-render/';
396 for ( $i = 0; $i < $wgCaptchaDirectoryLevels; $i++ ) {
397 $file .= $hash{ $i } .
'/';
399 $file .=
"image_{$salt}_{$hash}.png";
410 if ( preg_match(
'/^image_([0-9a-f]+)_([0-9a-f]+)\\.png$/', $basename,
$matches ) ) {
413 throw new Exception(
"Invalid filename '$basename'.\n" );
422 global $wgCaptchaDeleteOnSolve;
426 $pass = parent::passCaptcha( $index, $word );
428 if ( $pass && $wgCaptchaDeleteOnSolve ) {
430 'src' => $this->
imagePath( $info[
'salt'], $info[
'hash'] )
446 throw new UnderflowException(
'Ran out of captcha images' );
458 return $title->getLocalURL(
'wpCaptchaId=' . urlencode( $id ) );
468 array $requests, array $fieldInfo, array &$formDescriptor,
$action
472 AuthenticationRequest::getRequestByClass( $requests,
473 CaptchaAuthenticationRequest::class,
true );
479 unset( $formDescriptor[
'captchaInfo' ] );
481 $formDescriptor[
'captchaWord'] = [
482 'class' => HTMLFancyCaptchaField::class,
483 'imageUrl' => $this->
getCaptchaInfo( $req->captchaData, $req->captchaId ),
484 'label-message' => $this->
getMessage( $this->action ),
485 'showCreateHelp' => in_array(
$action, [
486 AuthManager::ACTION_CREATE,
487 AuthManager::ACTION_CREATE_CONTINUE
489 ] + $formDescriptor[
'captchaWord'];