Beginnings of a rewrite of the captcha system in a more object-oriented fashion;...
authorHappy-melon <happy-melon@users.mediawiki.org>
Sun, 24 Apr 2011 11:47:03 +0000 (11:47 +0000)
committerHappy-melon <happy-melon@users.mediawiki.org>
Sun, 24 Apr 2011 11:47:03 +0000 (11:47 +0000)
Captcha.php
ConfirmEdit.php
HTMLCaptchaField.php [new file with mode: 0644]

index ffa39ed1c7765ecb435a29bf9e077266f29d1f1b..2382845364da8995ca3216d7099dcffabe38586e 100644 (file)
@@ -1,5 +1,221 @@
 <?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 {
 
        /**
index 2785cc3168c4fcad3e9d54067fd8042e2ccc003c..0255a3ab4fe5dd58ec5889d57a48e0e93cb492e0 100644 (file)
@@ -201,11 +201,13 @@ $wgHooks['EmailUser'][] = 'ConfirmEditHooks::confirmEmailUser';
 $wgHooks['APIEditBeforeSave'][] = 'ConfirmEditHooks::confirmEditAPI';
 
 $wgAutoloadClasses['ConfirmEditHooks'] = "$wgConfirmEditIP/ConfirmEditHooks.php";
+$wgAutoloadClasses['Captcha']= "$wgConfirmEditIP/Captcha.php";
 $wgAutoloadClasses['SimpleCaptcha']= "$wgConfirmEditIP/Captcha.php";
 $wgAutoloadClasses['CaptchaStore']= "$wgConfirmEditIP/CaptchaStore.php";
 $wgAutoloadClasses['CaptchaSessionStore']= "$wgConfirmEditIP/CaptchaStore.php";
 $wgAutoloadClasses['CaptchaCacheStore']= "$wgConfirmEditIP/CaptchaStore.php";
 $wgAutoloadClasses['CaptchaSpecialPage'] = "$wgConfirmEditIP/ConfirmEditHooks.php";
+$wgAutoloadClasses['HTMLCaptchaField']= "$wgConfirmEditIP/HTMLCaptchaField.php";
 
 /**
  * Set up $wgWhitelistRead
diff --git a/HTMLCaptchaField.php b/HTMLCaptchaField.php
new file mode 100644 (file)
index 0000000..a458544
--- /dev/null
@@ -0,0 +1,84 @@
+<?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