2007-11-12 07:42:25 +00:00
|
|
|
<?php
|
|
|
|
|
2022-04-08 16:53:12 +00:00
|
|
|
namespace MediaWiki\Extension\ConfirmEdit\FancyCaptcha;
|
|
|
|
|
|
|
|
use FileBackend;
|
|
|
|
use FSFileBackend;
|
2024-02-09 13:53:56 +00:00
|
|
|
use InvalidArgumentException;
|
2016-04-25 20:58:18 +00:00
|
|
|
use MediaWiki\Auth\AuthenticationRequest;
|
|
|
|
use MediaWiki\Auth\AuthManager;
|
2022-04-08 16:40:15 +00:00
|
|
|
use MediaWiki\Extension\ConfirmEdit\Auth\CaptchaAuthenticationRequest;
|
|
|
|
use MediaWiki\Extension\ConfirmEdit\SimpleCaptcha\SimpleCaptcha;
|
2023-12-10 23:07:55 +00:00
|
|
|
use MediaWiki\Html\Html;
|
2020-03-31 17:38:07 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2023-12-10 23:07:55 +00:00
|
|
|
use MediaWiki\SpecialPage\SpecialPage;
|
|
|
|
use MediaWiki\Utils\MWTimestamp;
|
2023-04-25 09:53:06 +00:00
|
|
|
use MediaWiki\WikiMap\WikiMap;
|
2022-04-08 16:53:12 +00:00
|
|
|
use NullLockManager;
|
|
|
|
use ObjectCache;
|
2023-05-15 20:32:44 +00:00
|
|
|
use RequestContext;
|
2022-04-08 16:53:12 +00:00
|
|
|
use StatusValue;
|
|
|
|
use UnderflowException;
|
2016-04-25 20:58:18 +00:00
|
|
|
|
2017-02-17 13:24:49 +00:00
|
|
|
/**
|
|
|
|
* FancyCaptcha for displaying captchas precomputed by captcha.py
|
|
|
|
*/
|
2007-11-12 07:42:25 +00:00
|
|
|
class FancyCaptcha extends SimpleCaptcha {
|
2016-04-25 20:58:18 +00:00
|
|
|
// used for fancycaptcha-edit, fancycaptcha-addurl, fancycaptcha-badlogin,
|
2016-06-16 14:06:35 +00:00
|
|
|
// fancycaptcha-accountcreate, fancycaptcha-create, fancycaptcha-sendemail via getMessage()
|
2016-04-25 20:58:18 +00:00
|
|
|
protected static $messagePrefix = 'fancycaptcha-';
|
|
|
|
|
2012-09-16 17:31:47 +00:00
|
|
|
/**
|
|
|
|
* @return FileBackend
|
|
|
|
*/
|
|
|
|
public function getBackend() {
|
|
|
|
global $wgCaptchaFileBackend, $wgCaptchaDirectory;
|
|
|
|
|
|
|
|
if ( $wgCaptchaFileBackend ) {
|
2020-03-31 17:38:07 +00:00
|
|
|
return MediaWikiServices::getInstance()->getFileBackendGroup()
|
|
|
|
->get( $wgCaptchaFileBackend );
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
2024-01-15 15:17:21 +00:00
|
|
|
|
|
|
|
static $backend = null;
|
|
|
|
if ( !$backend ) {
|
|
|
|
$backend = new FSFileBackend( [
|
|
|
|
'name' => 'captcha-backend',
|
|
|
|
'wikiId' => WikiMap::getCurrentWikiId(),
|
|
|
|
'lockManager' => new NullLockManager( [] ),
|
|
|
|
'containerPaths' => [ $this->getStorageDir() => $wgCaptchaDirectory ],
|
|
|
|
'fileMode' => 777,
|
|
|
|
'obResetFunc' => 'wfResetOutputBuffers',
|
|
|
|
'streamMimeFunc' => [ 'StreamFile', 'contentTypeFromPath' ]
|
|
|
|
] );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $backend;
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
|
|
|
|
2017-02-07 18:02:46 +00:00
|
|
|
/**
|
|
|
|
* @return int Number of captcha files
|
|
|
|
*/
|
|
|
|
public function getCaptchaCount() {
|
|
|
|
$backend = $this->getBackend();
|
|
|
|
$files = $backend->getFileList(
|
2024-01-15 15:17:21 +00:00
|
|
|
[ 'dir' => $backend->getRootStoragePath() . '/' . $this->getStorageDir() ]
|
2017-02-07 18:02:46 +00:00
|
|
|
);
|
2012-09-16 17:31:47 +00:00
|
|
|
|
2017-02-07 18:02:46 +00:00
|
|
|
return iterator_count( $files );
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
|
|
|
|
2024-01-15 15:17:21 +00:00
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getStorageDir() {
|
|
|
|
global $wgCaptchaStorageDirectory;
|
|
|
|
return $wgCaptchaStorageDirectory;
|
|
|
|
}
|
|
|
|
|
2007-11-12 07:42:25 +00:00
|
|
|
/**
|
|
|
|
* Check if the submitted form matches the captcha session data provided
|
|
|
|
* by the plugin when the form was generated.
|
|
|
|
*
|
2008-01-24 21:42:21 +00:00
|
|
|
* @param string $answer
|
2007-11-12 07:42:25 +00:00
|
|
|
* @param array $info
|
|
|
|
* @return bool
|
|
|
|
*/
|
2018-06-15 21:08:14 +00:00
|
|
|
protected function keyMatch( $answer, $info ) {
|
2007-11-12 07:42:25 +00:00
|
|
|
global $wgCaptchaSecret;
|
|
|
|
|
|
|
|
$digest = $wgCaptchaSecret . $info['salt'] . $answer . $wgCaptchaSecret . $info['salt'];
|
|
|
|
$answerHash = substr( md5( $digest ), 0, 16 );
|
|
|
|
|
2009-07-19 15:13:01 +00:00
|
|
|
if ( $answerHash == $info['hash'] ) {
|
2007-11-12 07:42:25 +00:00
|
|
|
wfDebug( "FancyCaptcha: answer hash matches expected {$info['hash']}\n" );
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
wfDebug( "FancyCaptcha: answer hashes to $answerHash, expected {$info['hash']}\n" );
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-17 13:24:49 +00:00
|
|
|
/**
|
2017-09-09 18:10:12 +00:00
|
|
|
* @param array &$resultArr
|
2017-02-17 13:24:49 +00:00
|
|
|
*/
|
2018-06-15 21:08:14 +00:00
|
|
|
protected function addCaptchaAPI( &$resultArr ) {
|
2008-02-28 17:42:23 +00:00
|
|
|
$info = $this->pickImage();
|
2009-07-19 15:13:01 +00:00
|
|
|
if ( !$info ) {
|
2008-02-28 17:42:23 +00:00
|
|
|
$resultArr['captcha']['error'] = 'Out of images';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$index = $this->storeCaptcha( $info );
|
2010-06-08 19:30:48 +00:00
|
|
|
$title = SpecialPage::getTitleFor( 'Captcha', 'image' );
|
2016-04-25 20:58:18 +00:00
|
|
|
$resultArr['captcha'] = $this->describeCaptchaType();
|
2008-02-28 17:42:23 +00:00
|
|
|
$resultArr['captcha']['id'] = $index;
|
2017-02-17 13:24:49 +00:00
|
|
|
$resultArr['captcha']['url'] = $title->getLocalURL( 'wpCaptchaId=' . urlencode( $index ) );
|
2008-02-28 17:42:23 +00:00
|
|
|
}
|
|
|
|
|
2017-02-17 13:24:49 +00:00
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
2016-04-25 20:58:18 +00:00
|
|
|
public function describeCaptchaType() {
|
|
|
|
return [
|
|
|
|
'type' => 'image',
|
|
|
|
'mime' => 'image/png',
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2017-02-17 13:24:49 +00:00
|
|
|
/**
|
|
|
|
* @param int $tabIndex
|
|
|
|
* @return array
|
|
|
|
*/
|
2018-06-15 21:08:14 +00:00
|
|
|
public function getFormInformation( $tabIndex = 1 ) {
|
2010-06-08 19:30:48 +00:00
|
|
|
$title = SpecialPage::getTitleFor( 'Captcha', 'image' );
|
2016-04-25 20:58:18 +00:00
|
|
|
$info = $this->getCaptcha();
|
|
|
|
$index = $this->storeCaptcha( $info );
|
2013-01-17 02:00:09 +00:00
|
|
|
|
2018-03-22 21:44:29 +00:00
|
|
|
$captchaReload = Html::element(
|
|
|
|
'small',
|
|
|
|
[
|
|
|
|
'class' => 'confirmedit-captcha-reload fancycaptcha-reload'
|
|
|
|
],
|
|
|
|
wfMessage( 'fancycaptcha-reload-text' )->text()
|
|
|
|
);
|
2013-01-17 02:00:09 +00:00
|
|
|
|
2015-01-14 16:34:23 +00:00
|
|
|
$form = Html::openElement( 'div' ) .
|
2016-05-09 23:41:17 +00:00
|
|
|
Html::element( 'label', [
|
2015-01-14 16:34:23 +00:00
|
|
|
'for' => 'wpCaptchaWord',
|
2016-05-09 23:41:17 +00:00
|
|
|
],
|
2016-04-25 20:58:18 +00:00
|
|
|
wfMessage( 'captcha-label' )->text() . ' ' . wfMessage( 'fancycaptcha-captcha' )->text()
|
2015-01-14 16:34:23 +00:00
|
|
|
) .
|
2016-05-09 23:41:17 +00:00
|
|
|
Html::openElement( 'div', [ 'class' => 'fancycaptcha-captcha-container' ] ) .
|
|
|
|
Html::openElement( 'div', [ 'class' => 'fancycaptcha-captcha-and-reload' ] ) .
|
|
|
|
Html::openElement( 'div', [ 'class' => 'fancycaptcha-image-container' ] ) .
|
|
|
|
Html::element( 'img', [
|
2013-04-23 19:28:44 +00:00
|
|
|
'class' => 'fancycaptcha-image',
|
2017-02-17 13:24:49 +00:00
|
|
|
'src' => $title->getLocalURL( 'wpCaptchaId=' . urlencode( $index ) ),
|
2013-04-23 19:28:44 +00:00
|
|
|
'alt' => ''
|
2016-05-09 23:41:17 +00:00
|
|
|
]
|
2015-01-14 16:34:23 +00:00
|
|
|
) . $captchaReload . Html::closeElement( 'div' ) . Html::closeElement( 'div' ) . "\n" .
|
2023-07-13 15:56:07 +00:00
|
|
|
// FIXME: This should use CodexHTMLForm rather than Html::element
|
2024-01-27 21:57:10 +00:00
|
|
|
Html::openElement( 'div', [ 'class' => 'cdx-text-input' ] ) .
|
2016-05-09 23:41:17 +00:00
|
|
|
Html::element( 'input', [
|
2013-04-23 19:28:44 +00:00
|
|
|
'name' => 'wpCaptchaWord',
|
2024-01-27 21:57:10 +00:00
|
|
|
'class' => 'cdx-text-input__input',
|
2013-04-23 19:28:44 +00:00
|
|
|
'id' => 'wpCaptchaWord',
|
|
|
|
'type' => 'text',
|
2018-06-15 21:08:14 +00:00
|
|
|
// max_length in captcha.py plus fudge factor
|
|
|
|
'size' => '12',
|
2013-05-03 01:09:51 +00:00
|
|
|
'autocomplete' => 'off',
|
2013-04-23 19:28:44 +00:00
|
|
|
'autocorrect' => 'off',
|
|
|
|
'autocapitalize' => 'off',
|
|
|
|
'required' => 'required',
|
2018-06-15 21:08:14 +00:00
|
|
|
// tab in before the edit textarea
|
2015-09-23 06:05:28 +00:00
|
|
|
'tabindex' => $tabIndex,
|
2018-08-19 11:06:26 +00:00
|
|
|
'placeholder' => wfMessage( 'fancycaptcha-imgcaptcha-ph' )->text()
|
2016-05-09 23:41:17 +00:00
|
|
|
]
|
2024-01-27 21:57:10 +00:00
|
|
|
) . Html::closeElement( 'div' );
|
2018-06-15 21:08:14 +00:00
|
|
|
if ( $this->action == 'createaccount' ) {
|
|
|
|
// use raw element, because the message can contain links or some other html
|
|
|
|
$form .= Html::rawElement( 'small', [
|
|
|
|
'class' => 'mw-createacct-captcha-assisted'
|
|
|
|
], wfMessage( 'createacct-imgcaptcha-help' )->parse()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$form .= Html::element( 'input', [
|
|
|
|
'type' => 'hidden',
|
|
|
|
'name' => 'wpCaptchaId',
|
|
|
|
'id' => 'wpCaptchaId',
|
|
|
|
'value' => $index
|
|
|
|
]
|
|
|
|
) . Html::closeElement( 'div' ) . Html::closeElement( 'div' ) . "\n";
|
2015-01-14 16:34:23 +00:00
|
|
|
|
Remove getForm() and replace by getFormInformation()
This commit removes SimpleCaptcha::getForm() and replaces it by its more informative
counterpart getFormInformation(), which returns an array, which provides some
more information about the form than only the html.
The information included in the array is:
* html: The HTML of the CAPTCHA form (this is the same as what you expected from
getForm() previously)
* modules: ResourceLoader modules, if any, that should be added to the output of the
page
* modulestyles: ResourceLoader style modules, if any, that should be added to th
output of the page
* headitems: Head items that should be added to the output (see OutputPage::addHeadItems)
Mostly you shouldn't need to handle the response of getFormInformation() anymore, as there's
a new function, addFormToOutput(), which takes an instance of OutputPage as a first parameter
and handles the response of getFormInformation for you (adds all information to the given
OutputPage instance, if they're provided).
Bug: T141300
Depends-On: I433afd124b57526caa13a540cda48ba2b99a9bde
Change-Id: I25f344538052fc18993c43185fbd97804a7cfc81
2016-07-26 16:08:42 +00:00
|
|
|
return [
|
|
|
|
'html' => $form,
|
2024-01-27 21:57:10 +00:00
|
|
|
'modules' => [ 'ext.confirmEdit.fancyCaptcha' ],
|
Remove getForm() and replace by getFormInformation()
This commit removes SimpleCaptcha::getForm() and replaces it by its more informative
counterpart getFormInformation(), which returns an array, which provides some
more information about the form than only the html.
The information included in the array is:
* html: The HTML of the CAPTCHA form (this is the same as what you expected from
getForm() previously)
* modules: ResourceLoader modules, if any, that should be added to the output of the
page
* modulestyles: ResourceLoader style modules, if any, that should be added to th
output of the page
* headitems: Head items that should be added to the output (see OutputPage::addHeadItems)
Mostly you shouldn't need to handle the response of getFormInformation() anymore, as there's
a new function, addFormToOutput(), which takes an instance of OutputPage as a first parameter
and handles the response of getFormInformation for you (adds all information to the given
OutputPage instance, if they're provided).
Bug: T141300
Depends-On: I433afd124b57526caa13a540cda48ba2b99a9bde
Change-Id: I25f344538052fc18993c43185fbd97804a7cfc81
2016-07-26 16:08:42 +00:00
|
|
|
// Uses addModuleStyles so it is loaded when JS is disabled.
|
2024-01-27 21:57:10 +00:00
|
|
|
'modulestyles' => [ 'codex-styles', 'ext.confirmEdit.fancyCaptcha.styles' ],
|
Remove getForm() and replace by getFormInformation()
This commit removes SimpleCaptcha::getForm() and replaces it by its more informative
counterpart getFormInformation(), which returns an array, which provides some
more information about the form than only the html.
The information included in the array is:
* html: The HTML of the CAPTCHA form (this is the same as what you expected from
getForm() previously)
* modules: ResourceLoader modules, if any, that should be added to the output of the
page
* modulestyles: ResourceLoader style modules, if any, that should be added to th
output of the page
* headitems: Head items that should be added to the output (see OutputPage::addHeadItems)
Mostly you shouldn't need to handle the response of getFormInformation() anymore, as there's
a new function, addFormToOutput(), which takes an instance of OutputPage as a first parameter
and handles the response of getFormInformation for you (adds all information to the given
OutputPage instance, if they're provided).
Bug: T141300
Depends-On: I433afd124b57526caa13a540cda48ba2b99a9bde
Change-Id: I25f344538052fc18993c43185fbd97804a7cfc81
2016-07-26 16:08:42 +00:00
|
|
|
];
|
2013-01-17 02:00:09 +00:00
|
|
|
}
|
|
|
|
|
2007-11-12 07:42:25 +00:00
|
|
|
/**
|
|
|
|
* Select a previously generated captcha image from the queue.
|
|
|
|
* @return mixed tuple of (salt key, text hash) or false if no image to find
|
|
|
|
*/
|
2012-09-16 17:31:47 +00:00
|
|
|
protected function pickImage() {
|
|
|
|
global $wgCaptchaDirectoryLevels;
|
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
// number of times another process claimed a file before this one
|
|
|
|
$lockouts = 0;
|
2024-01-15 15:17:21 +00:00
|
|
|
$baseDir = $this->getBackend()->getRootStoragePath() . '/' . $this->getStorageDir();
|
2012-09-16 17:31:47 +00:00
|
|
|
return $this->pickImageDir( $baseDir, $wgCaptchaDirectoryLevels, $lockouts );
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
2009-07-19 15:13:01 +00:00
|
|
|
|
2012-09-16 17:31:47 +00:00
|
|
|
/**
|
2017-09-09 18:10:12 +00:00
|
|
|
* @param string $directory
|
|
|
|
* @param int $levels
|
|
|
|
* @param int &$lockouts
|
2017-02-17 13:24:49 +00:00
|
|
|
* @return array|bool
|
2012-09-16 17:31:47 +00:00
|
|
|
*/
|
|
|
|
protected function pickImageDir( $directory, $levels, &$lockouts ) {
|
2018-06-15 21:08:14 +00:00
|
|
|
if ( $levels <= 0 ) {
|
|
|
|
// $directory has regular files
|
2012-09-16 17:31:47 +00:00
|
|
|
return $this->pickImageFromDir( $directory, $lockouts );
|
|
|
|
}
|
|
|
|
|
|
|
|
$backend = $this->getBackend();
|
2020-02-06 23:48:57 +00:00
|
|
|
$cache = ObjectCache::getLocalClusterInstance();
|
|
|
|
|
|
|
|
$key = $cache->makeGlobalKey(
|
|
|
|
'fancycaptcha-dirlist',
|
|
|
|
$backend->getDomainId(),
|
|
|
|
sha1( $directory )
|
|
|
|
);
|
2012-09-16 17:31:47 +00:00
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
// check cache
|
2020-02-06 23:48:57 +00:00
|
|
|
$dirs = $cache->get( $key );
|
2018-06-15 21:08:14 +00:00
|
|
|
if ( !is_array( $dirs ) || !count( $dirs ) ) {
|
|
|
|
// cache miss
|
|
|
|
$dirs = [];
|
|
|
|
// subdirs actually present...
|
2016-05-09 23:41:17 +00:00
|
|
|
foreach ( $backend->getTopDirectoryList( [ 'dir' => $directory ] ) as $entry ) {
|
2009-07-19 15:13:01 +00:00
|
|
|
if ( ctype_xdigit( $entry ) && strlen( $entry ) == 1 ) {
|
2007-11-12 07:42:25 +00:00
|
|
|
$dirs[] = $entry;
|
|
|
|
}
|
|
|
|
}
|
2012-09-16 17:31:47 +00:00
|
|
|
wfDebug( "Cache miss for $directory subdirectory listing.\n" );
|
2013-01-01 00:36:02 +00:00
|
|
|
if ( count( $dirs ) ) {
|
2020-02-06 23:48:57 +00:00
|
|
|
$cache->set( $key, $dirs, 86400 );
|
2013-01-01 00:36:02 +00:00
|
|
|
}
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
2009-07-19 15:13:01 +00:00
|
|
|
|
2012-09-16 17:31:47 +00:00
|
|
|
if ( !count( $dirs ) ) {
|
|
|
|
// Remove this directory if empty so callers don't keep looking here
|
2016-05-09 23:41:17 +00:00
|
|
|
$backend->clean( [ 'dir' => $directory ] );
|
2018-06-15 21:08:14 +00:00
|
|
|
// none found
|
|
|
|
return false;
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
2012-09-16 17:31:47 +00:00
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
// pick a random subdir
|
|
|
|
$place = mt_rand( 0, count( $dirs ) - 1 );
|
2012-09-16 17:31:47 +00:00
|
|
|
// In case all dirs are not filled, cycle through next digits...
|
2015-10-28 15:52:04 +00:00
|
|
|
$fancyCount = count( $dirs );
|
|
|
|
for ( $j = 0; $j < $fancyCount; $j++ ) {
|
2012-09-16 17:31:47 +00:00
|
|
|
$char = $dirs[( $place + $j ) % count( $dirs )];
|
|
|
|
$info = $this->pickImageDir( "$directory/$char", $levels - 1, $lockouts );
|
|
|
|
if ( $info ) {
|
2018-06-15 21:08:14 +00:00
|
|
|
// found a captcha
|
|
|
|
return $info;
|
2012-09-16 17:31:47 +00:00
|
|
|
} else {
|
|
|
|
wfDebug( "Could not find captcha in $directory.\n" );
|
2018-06-15 21:08:14 +00:00
|
|
|
// files changed on disk?
|
2020-02-06 23:48:57 +00:00
|
|
|
$cache->delete( $key );
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
2012-08-30 15:49:16 +00:00
|
|
|
}
|
2007-11-12 07:42:25 +00:00
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
// didn't find any images in this directory... empty?
|
|
|
|
return false;
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
2007-11-12 07:42:25 +00:00
|
|
|
|
2012-09-16 17:31:47 +00:00
|
|
|
/**
|
2017-09-09 18:10:12 +00:00
|
|
|
* @param string $directory
|
|
|
|
* @param int &$lockouts
|
2017-02-17 13:24:49 +00:00
|
|
|
* @return array|bool
|
2012-09-16 17:31:47 +00:00
|
|
|
*/
|
|
|
|
protected function pickImageFromDir( $directory, &$lockouts ) {
|
|
|
|
$backend = $this->getBackend();
|
2020-02-06 23:48:57 +00:00
|
|
|
$cache = ObjectCache::getLocalClusterInstance();
|
|
|
|
|
|
|
|
$key = $cache->makeGlobalKey(
|
|
|
|
'fancycaptcha-filelist',
|
|
|
|
$backend->getDomainId(),
|
|
|
|
sha1( $directory )
|
|
|
|
);
|
2012-09-16 17:31:47 +00:00
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
// check cache
|
2020-02-06 23:48:57 +00:00
|
|
|
$files = $cache->get( $key );
|
2018-06-15 21:08:14 +00:00
|
|
|
if ( !is_array( $files ) || !count( $files ) ) {
|
|
|
|
// cache miss
|
|
|
|
$files = [];
|
2016-05-09 23:41:17 +00:00
|
|
|
foreach ( $backend->getTopFileList( [ 'dir' => $directory ] ) as $entry ) {
|
2012-09-16 17:31:47 +00:00
|
|
|
$files[] = $entry;
|
2018-06-15 21:08:14 +00:00
|
|
|
if ( count( $files ) >= 500 ) {
|
|
|
|
// sanity
|
2012-09-16 17:31:47 +00:00
|
|
|
wfDebug( 'Skipping some captchas; $wgCaptchaDirectoryLevels set too low?.' );
|
2007-11-12 07:42:25 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2013-01-01 00:36:02 +00:00
|
|
|
if ( count( $files ) ) {
|
2020-02-06 23:48:57 +00:00
|
|
|
$cache->set( $key, $files, 86400 );
|
2013-01-01 00:36:02 +00:00
|
|
|
}
|
2012-09-16 17:31:47 +00:00
|
|
|
wfDebug( "Cache miss for $directory captcha listing.\n" );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !count( $files ) ) {
|
|
|
|
// Remove this directory if empty so callers don't keep looking here
|
2016-05-09 23:41:17 +00:00
|
|
|
$backend->clean( [ 'dir' => $directory ] );
|
2012-09-16 17:31:47 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$info = $this->pickImageFromList( $directory, $files, $lockouts );
|
|
|
|
if ( !$info ) {
|
|
|
|
wfDebug( "Could not find captcha in $directory.\n" );
|
2018-06-15 21:08:14 +00:00
|
|
|
// files changed on disk?
|
2020-02-06 23:48:57 +00:00
|
|
|
$cache->delete( $key );
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
2012-09-16 17:31:47 +00:00
|
|
|
|
|
|
|
return $info;
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-09-09 18:10:12 +00:00
|
|
|
* @param string $directory
|
|
|
|
* @param array $files
|
|
|
|
* @param int &$lockouts
|
2017-02-17 13:24:49 +00:00
|
|
|
* @return array|bool
|
2007-11-12 07:42:25 +00:00
|
|
|
*/
|
2012-09-16 17:31:47 +00:00
|
|
|
protected function pickImageFromList( $directory, array $files, &$lockouts ) {
|
2020-02-06 23:48:57 +00:00
|
|
|
global $wgCaptchaDeleteOnSolve;
|
2012-09-16 17:31:47 +00:00
|
|
|
|
|
|
|
if ( !count( $files ) ) {
|
2018-06-15 21:08:14 +00:00
|
|
|
// none found
|
|
|
|
return false;
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
$backend = $this->getBackend();
|
2020-02-06 23:48:57 +00:00
|
|
|
$cache = ObjectCache::getLocalClusterInstance();
|
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
// pick a random file
|
|
|
|
$place = mt_rand( 0, count( $files ) - 1 );
|
|
|
|
// number of files in listing that don't actually exist
|
|
|
|
$misses = 0;
|
2015-10-28 15:52:04 +00:00
|
|
|
$fancyImageCount = count( $files );
|
|
|
|
for ( $j = 0; $j < $fancyImageCount; $j++ ) {
|
2012-09-16 17:31:47 +00:00
|
|
|
$entry = $files[( $place + $j ) % count( $files )];
|
|
|
|
if ( preg_match( '/^image_([0-9a-f]+)_([0-9a-f]+)\\.png$/', $entry, $matches ) ) {
|
2018-06-15 21:08:14 +00:00
|
|
|
if ( $wgCaptchaDeleteOnSolve ) {
|
|
|
|
// captcha will be deleted when solved
|
2020-02-06 23:48:57 +00:00
|
|
|
$key = $cache->makeGlobalKey(
|
|
|
|
'fancycaptcha-filelock',
|
|
|
|
$backend->getDomainId(),
|
|
|
|
sha1( $entry )
|
|
|
|
);
|
2012-09-16 17:31:47 +00:00
|
|
|
// Try to claim this captcha for 10 minutes (for the user to solve)...
|
2020-02-06 23:48:57 +00:00
|
|
|
if ( ++$lockouts <= 10 && !$cache->add( $key, '1', 600 ) ) {
|
2018-06-15 21:08:14 +00:00
|
|
|
// could not acquire (skip it to avoid race conditions)
|
|
|
|
continue;
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
|
|
|
}
|
2016-05-09 23:41:17 +00:00
|
|
|
if ( !$backend->fileExists( [ 'src' => "$directory/$entry" ] ) ) {
|
2018-06-15 21:08:14 +00:00
|
|
|
if ( ++$misses >= 5 ) {
|
|
|
|
// too many files in the listing don't exist
|
|
|
|
// listing cache too stale? break out so it will be cleared
|
|
|
|
break;
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
2018-06-15 21:08:14 +00:00
|
|
|
// try next file
|
|
|
|
continue;
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
2016-05-09 23:41:17 +00:00
|
|
|
return [
|
2012-09-16 17:31:47 +00:00
|
|
|
'salt' => $matches[1],
|
|
|
|
'hash' => $matches[2],
|
|
|
|
'viewed' => false,
|
2016-05-09 23:41:17 +00:00
|
|
|
];
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
|
|
|
}
|
2012-09-16 17:31:47 +00:00
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
// none found
|
|
|
|
return false;
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
|
|
|
|
2017-02-17 13:24:49 +00:00
|
|
|
/**
|
|
|
|
* @return bool|StatusValue
|
|
|
|
*/
|
2018-06-15 21:08:14 +00:00
|
|
|
public function showImage() {
|
2023-05-15 20:32:44 +00:00
|
|
|
$context = RequestContext::getMain();
|
|
|
|
$context->getOutput()->disable();
|
2007-11-12 07:42:25 +00:00
|
|
|
|
2023-05-15 20:32:44 +00:00
|
|
|
$index = $context->getRequest()->getVal( 'wpCaptchaId' );
|
2016-04-25 20:58:18 +00:00
|
|
|
$info = $this->retrieveCaptcha( $index );
|
2009-07-19 15:13:01 +00:00
|
|
|
if ( $info ) {
|
2012-09-02 12:26:25 +00:00
|
|
|
$timestamp = new MWTimestamp();
|
|
|
|
$info['viewed'] = $timestamp->getTimestamp();
|
2007-11-12 07:42:25 +00:00
|
|
|
$this->storeCaptcha( $info );
|
|
|
|
|
|
|
|
$salt = $info['salt'];
|
|
|
|
$hash = $info['hash'];
|
2012-09-16 17:31:47 +00:00
|
|
|
|
2016-05-09 23:41:17 +00:00
|
|
|
return $this->getBackend()->streamFile( [
|
2012-09-16 17:31:47 +00:00
|
|
|
'src' => $this->imagePath( $salt, $hash ),
|
2016-05-09 23:41:17 +00:00
|
|
|
'headers' => [ "Cache-Control: private, s-maxage=0, max-age=3600" ]
|
|
|
|
] )->isOK();
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
2012-09-16 17:31:47 +00:00
|
|
|
|
2015-03-18 06:54:12 +00:00
|
|
|
wfHttpError( 400, 'Request Error', 'Requested bogus captcha image' );
|
2007-11-12 07:42:25 +00:00
|
|
|
return false;
|
|
|
|
}
|
2009-07-19 15:13:01 +00:00
|
|
|
|
2012-09-16 17:31:47 +00:00
|
|
|
/**
|
2017-09-09 18:10:12 +00:00
|
|
|
* @param string $salt
|
|
|
|
* @param string $hash
|
2012-09-16 17:31:47 +00:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function imagePath( $salt, $hash ) {
|
|
|
|
global $wgCaptchaDirectoryLevels;
|
|
|
|
|
2024-01-26 22:50:05 +00:00
|
|
|
$file = $this->getBackend()->getRootStoragePath() . '/' . $this->getStorageDir() . '/';
|
2009-07-19 15:13:01 +00:00
|
|
|
for ( $i = 0; $i < $wgCaptchaDirectoryLevels; $i++ ) {
|
2020-03-23 02:59:03 +00:00
|
|
|
$file .= $hash[ $i ] . '/';
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
|
|
|
$file .= "image_{$salt}_{$hash}.png";
|
2012-09-16 17:31:47 +00:00
|
|
|
|
2007-11-12 07:42:25 +00:00
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
2012-09-16 17:31:47 +00:00
|
|
|
/**
|
2017-09-09 18:10:12 +00:00
|
|
|
* @param string $basename
|
2017-02-17 13:24:49 +00:00
|
|
|
* @return array (salt, hash)
|
2012-09-16 17:31:47 +00:00
|
|
|
*/
|
|
|
|
public function hashFromImageName( $basename ) {
|
|
|
|
if ( preg_match( '/^image_([0-9a-f]+)_([0-9a-f]+)\\.png$/', $basename, $matches ) ) {
|
2016-05-09 23:41:17 +00:00
|
|
|
return [ $matches[1], $matches[2] ];
|
2012-09-16 17:31:47 +00:00
|
|
|
} else {
|
2024-02-09 13:53:56 +00:00
|
|
|
throw new InvalidArgumentException( "Invalid filename '$basename'.\n" );
|
2012-09-16 17:31:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-08-29 13:31:31 +00:00
|
|
|
/**
|
|
|
|
* Delete a solved captcha image, if $wgCaptchaDeleteOnSolve is true.
|
2017-09-24 05:22:19 +00:00
|
|
|
* @inheritDoc
|
2010-08-29 13:31:31 +00:00
|
|
|
*/
|
2016-04-25 20:58:18 +00:00
|
|
|
protected function passCaptcha( $index, $word ) {
|
2015-04-02 22:56:48 +00:00
|
|
|
global $wgCaptchaDeleteOnSolve;
|
2010-08-29 13:31:31 +00:00
|
|
|
|
2018-06-15 21:08:14 +00:00
|
|
|
// get the captcha info before it gets deleted
|
|
|
|
$info = $this->retrieveCaptcha( $index );
|
2016-04-25 20:58:18 +00:00
|
|
|
$pass = parent::passCaptcha( $index, $word );
|
2010-08-29 13:31:31 +00:00
|
|
|
|
|
|
|
if ( $pass && $wgCaptchaDeleteOnSolve ) {
|
2016-05-09 23:41:17 +00:00
|
|
|
$this->getBackend()->quickDelete( [
|
2012-09-16 17:31:47 +00:00
|
|
|
'src' => $this->imagePath( $info['salt'], $info['hash'] )
|
2016-05-09 23:41:17 +00:00
|
|
|
] );
|
2010-08-29 13:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $pass;
|
|
|
|
}
|
2016-04-25 20:58:18 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an array with 'salt' and 'hash' keys. Hash is
|
|
|
|
* md5( $wgCaptchaSecret . $salt . $answer . $wgCaptchaSecret . $salt )[0..15]
|
|
|
|
* @return array
|
2024-02-09 13:53:56 +00:00
|
|
|
* @throws UnderflowException When a captcha image cannot be produced.
|
2016-04-25 20:58:18 +00:00
|
|
|
*/
|
|
|
|
public function getCaptcha() {
|
|
|
|
$info = $this->pickImage();
|
|
|
|
if ( !$info ) {
|
|
|
|
throw new UnderflowException( 'Ran out of captcha images' );
|
|
|
|
}
|
|
|
|
return $info;
|
|
|
|
}
|
|
|
|
|
2017-02-17 13:24:49 +00:00
|
|
|
/**
|
|
|
|
* @param array $captchaData
|
|
|
|
* @param string $id
|
|
|
|
* @return string
|
|
|
|
*/
|
2016-04-25 20:58:18 +00:00
|
|
|
public function getCaptchaInfo( $captchaData, $id ) {
|
|
|
|
$title = SpecialPage::getTitleFor( 'Captcha', 'image' );
|
|
|
|
return $title->getLocalURL( 'wpCaptchaId=' . urlencode( $id ) );
|
|
|
|
}
|
|
|
|
|
2017-02-17 13:24:49 +00:00
|
|
|
/**
|
|
|
|
* @param array $requests
|
|
|
|
* @param array $fieldInfo
|
2017-09-09 18:10:12 +00:00
|
|
|
* @param array &$formDescriptor
|
2017-02-17 13:24:49 +00:00
|
|
|
* @param string $action
|
|
|
|
*/
|
2016-04-25 20:58:18 +00:00
|
|
|
public function onAuthChangeFormFields(
|
|
|
|
array $requests, array $fieldInfo, array &$formDescriptor, $action
|
|
|
|
) {
|
|
|
|
/** @var CaptchaAuthenticationRequest $req */
|
|
|
|
$req =
|
|
|
|
AuthenticationRequest::getRequestByClass( $requests,
|
2016-05-03 16:42:00 +00:00
|
|
|
CaptchaAuthenticationRequest::class, true );
|
2016-04-25 20:58:18 +00:00
|
|
|
if ( !$req ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTMLFancyCaptchaField will include this
|
|
|
|
unset( $formDescriptor['captchaInfo' ] );
|
|
|
|
|
|
|
|
$formDescriptor['captchaWord'] = [
|
|
|
|
'class' => HTMLFancyCaptchaField::class,
|
|
|
|
'imageUrl' => $this->getCaptchaInfo( $req->captchaData, $req->captchaId ),
|
|
|
|
'label-message' => $this->getMessage( $this->action ),
|
|
|
|
'showCreateHelp' => in_array( $action, [
|
|
|
|
AuthManager::ACTION_CREATE,
|
|
|
|
AuthManager::ACTION_CREATE_CONTINUE
|
|
|
|
], true ),
|
|
|
|
] + $formDescriptor['captchaWord'];
|
|
|
|
}
|
2007-11-12 07:42:25 +00:00
|
|
|
}
|
2022-04-08 16:53:12 +00:00
|
|
|
|
|
|
|
class_alias( FancyCaptcha::class, 'FancyCaptcha' );
|