<?php
+/**
+ * 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;
+
+ /**
+ * Information about the captcha, in array form
+ * @var $info Array
+ */
+ protected $info;
+
+ /**
+ * 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 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 {
/**
--- /dev/null
+<?php
+/**
+ * HTMLFormField for inserting Captchas into a form.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @class
+ */
+class HTMLCaptchaField extends HTMLFormField {
+
+ /**
+ * @var Captcha
+ */
+ private $captcha;
+
+ public $prefix = '';
+
+ /**
+ * @var Bool|Array
+ */
+ private $validationResult;
+
+ public function __construct( $params ){
+ parent::__construct( $params );
+
+ // For differentiating the type of form, mainly
+ if( isset( $params['prefix'] ) ){
+ $this->prefix = $params['prefix'];
+ }
+ }
+
+ /**
+ * Get the captcha body. Don't include any of the surrounding table cells/rows
+ *
+ * @param $value String
+ * @return String
+ */
+ public function getInputHTML( $value ){
+ # TODO
+ }
+
+ public function validate( $data, $alldata ){
+ // We sent back the exists status of the captcha before. If it *doesn't* exist
+ // we actually want to validate this as true, because we don't want an angry red
+ // error message, just for the user to put the captcha in again
+ if( $data === false ){
+ return true;
+ }
+
+
+ }
+
+ /**
+ * @param $request WebRequest
+ * @return void
+ */
+ public function loadDataFromRequest( $request ){
+ $this->captcha = Captcha::factory();
+ $this->captcha->loadFromRequest( $request, $this );
+ if( !$this->captcha->exists() ){
+ // The captcha doesn't exist; probably because it's already been used and
+ // then deleted for security. Load the field up with a new captcha which
+ // will be shown to the user when the validation of said new object fails
+ $this->captcha = Captcha::newRandom();
+ }
+
+ // This will be useful as the difference between "the captcha doesn't exist" and
+ // "you answered the captcha wrongly"
+ return $this->captcha->exists();
+ }
+}
\ No newline at end of file