<?php
-class SimpleCaptcha {
+/**
+ * Object encapsulating a captcha process. The captcha has two elements: it must be able
+ * to generate a frontend HTML representation of itself which can be presented to the user,
+ * which provides inputs for users to provide their interpretation of the captcha; and it
+ * must be able to retrieve that data from a subsequently-submitted request and validate
+ * whether the user got the data correct.
+ */
+abstract class Captcha {
+
+ /**
+ * @var String
+ */
+ protected $id;
/**
- * @var CaptchaStore
+ * Information about the captcha, in array form
+ * @var $info Array
*/
- protected $storage;
+ protected $info;
- function __construct() {
- global $wgCaptchaStorageClass;
- if( in_array( 'CaptchaStore', class_implements( $wgCaptchaStorageClass ) ) ) {
- $this->storage = new $wgCaptchaStorageClass;
+ /**
+ * Whether this captcha exists in the storage
+ * @var Bool
+ */
+ protected $exists;
+
+ /**
+ * Generate a new empty Captcha. This is guaranteed to return a Captcha object if it
+ * does not throw an exception
+ *
+ * @return Captcha subclass
+ */
+ public final static function factory() {
+ global $wgCaptchaClass;
+ $obj = new $wgCaptchaClass;
+ if ( $obj instanceof Captcha ) {
+ return $obj;
} else {
- throw new MWException( "Invalid CaptchaStore class $wgCaptchaStorageClass" );
+ throw new MWException( "Invalid Captcha class $wgCaptchaClass, must extend Captcha" );
+ }
+ }
+
+ /**
+ * Instantiate a new Captcha object for a given Id
+ *
+ * @param $id Int
+ * @return Captcha
+ */
+ public final static function newFromId( $id ){
+ $obj = self::factory();
+ $obj->setId( $id );
+ return $obj->exists()
+ ? $obj
+ : null;
+ }
+
+ /**
+ * Instantiate a brand new captcha, never seen before.
+ *
+ * @return Captcha
+ */
+ public final static function newRandom(){
+ $obj = self::factory();
+ $obj->generateNew();
+ return $obj;
+ }
+
+ /**
+ * Protected constructor - use only the factory methods above to instantiate captchas,
+ * or you may end up with the wrong type of object
+ */
+ protected function __construct(){}
+
+ /**
+ * Get the captcha Id
+ *
+ * @return String
+ */
+ public function getId(){
+ return $this->id;
+ }
+
+ /**
+ * Set the Id internally. Don't include wierd things like entities or characters that
+ * need to be HTML-escaped, you'll just be creating more work and pain for yourself...
+ *
+ * @param $id String
+ */
+ protected function setId( $id ){
+ $this->id = $id;
+ }
+
+ /**
+ * Initialise $this->info etc with information needed to make this object a new,
+ * (ideally) never-seen-before captcha. Implementations should not save the data in
+ * the store in this function, as the captcha may not ever be used.
+ *
+ * @return Array of captcha info
+ */
+ # FIXME: detail
+ protected abstract function generateNew();
+
+ /**
+ * Save a generated captcha in storage somewhere where it won't be lost between
+ * requests. A random ID is used so legit users can make edits in multiple tabs
+ * or windows without being unnecessarily hobbled by a serial order requirement.
+ */
+ protected function store() {
+ // Assign random index if we're not udpating
+ if ( !isset( $this->info['index'] ) ) {
+ if( !$this->getId() ){
+ $this->setId( strval( mt_rand() ) );
+ }
+ $this->info['index'] = $this->getId();
}
+ CaptchaStore::get()->store( $this->info['index'], $this->info );
}
+ /**
+ * Fetch the data for this captcha from the CaptchaStore. This requires $this->id
+ * to be set.
+ *
+ * @return Array|Bool: Array of info, or false if missing
+ */
+ protected function retrieve() {
+ if( $this->getId() === null ){
+ return null;
+ }
+ if( $this->info === null ){
+ $this->info = CaptchaStore::get()->retrieve( $this->getId() );
+ $this->exists = $this->info !== false;
+ }
+ return $this->info;
+ }
+
+ /**
+ * Clear the information about this captcha from the CaptchaStore, so it cannot
+ * be reused at a later date.
+ */
+ protected function delete() {
+ if( $this->getId() !== null ){
+ CaptchaStore::get()->clear( $this->getId() );
+ }
+ }
+
+ /**
+ * Whether this captcha exists. $this->setId() must have been called from some context
+ *
+ * @return Bool
+ */
+ public function exists(){
+ if( $this->exists === null ){
+ $this->retrieve();
+ }
+ return $this->exists;
+ }
+
+ /**
+ * Load some data from a WebRequest. Implementations must load all data they need
+ * from the request in this function, they must not use the global $wgRequest, as
+ * in the post-1.18 environment they may not necessarily be the same.
+ *
+ * @param $request WebRequest
+ * @param $field HTMLCaptchaField will be passed if the captcha is part of an HTMLForm
+ */
+ public abstract function loadFromRequest( WebRequest $request, HTMLCaptchaField $field = null );
+
+ /**
+ * Return the data that would be needed to pass the captcha challenge through the API.
+ * Implementations must return an array with at least the following parameters:
+ * 'type' - a unique description of the type of challenge. This could be
+ * the class name
+ * 'mime' - the MIME type of the challenge
+ * 'id' - the captcha Id produced by getId()
+ * Implementations should document how the user should use the provided data to answer
+ * the captcha.
+ *
+ * Implementations may return False to indicate that it is not possible to represent
+ * the challenge via the API. API actions protected by such a captcha will be disabled.
+ *
+ * @return Array|Bool
+ */
+ public abstract function getApiParams();
+
+ /**
+ * Return the HTML which will be placed in the 'input' table cell of an HTMLForm.
+ * Implementations must include input fields which will perpetuate the captcha Id and
+ * any special data, as well as providing a means for the user to answer the captcha.
+ * Implementations should not include any help or label text, as these will be set in
+ * the label-message and help-message attributes of the HTMLCaptchafield.
+ * Implementations should honour the options set in the HTMLFormField such as
+ * $field->mName and $field->mReadonly.
+ *
+ * @param $field HTMLCaptchaField
+ * @return String raw HTML
+ */
+ public abstract function getFormHTML( HTMLCaptchaField $field );
+
+ /**
+ * Return the HTML which will be used in legacy forms which do not implement HTMLForm
+ * Implementations must include input fields which will perpetuate the captcha Id and
+ * any other necessary data, as well as providing a means for the user to answer the
+ * captcha, and any relevant descriptions and instructions.
+ *
+ * @return String raw HTML
+ */
+ public abstract function getFreeflowHTML();
+
+ /**
+ * Using the parameters loaded from the web request, check the captcha, maybe delete
+ * it if that's desirable, do any other necessary cleanup, and return Bool
+ * @return Bool whether the captcha was successfully answered
+ */
+ public abstract function checkCaptcha();
+}
+
+class SimpleCaptcha {
+
function getCaptcha() {
$a = mt_rand( 0, 100 );
$b = mt_rand( 0, 10 );
// Assign random index if we're not udpating
$info['index'] = strval( mt_rand() );
}
- $this->storage->store( $info['index'], $info );
+ CaptchaStore::get()->store( $info['index'], $info );
return $info['index'];
}
function retrieveCaptcha() {
global $wgRequest;
$index = $wgRequest->getVal( 'wpCaptchaId' );
- return $this->storage->retrieve( $index );
+ return CaptchaStore::get()->retrieve( $index );
}
/**
* it can't be reused.
*/
function clearCaptcha( $info ) {
- $this->storage->clear( $info['index'] );
+ CaptchaStore::get()->clear( $info['index'] );
}
/**
global $wgOut;
$wgOut->setPageTitle( wfMsg( 'captchahelp-title' ) );
$wgOut->addWikiText( wfMsg( 'captchahelp-text' ) );
- if ( $this->storage->cookiesNeeded() ) {
+ if ( CaptchaStore::get()->cookiesNeeded() ) {
$wgOut->addWikiText( wfMsg( 'captchahelp-cookies-needed' ) );
}
}