* (bug 32609) API: Move captchaid/captchaword of action=edit from core to Captcha...
[toast/cookiecaptcha.git] / Captcha.php
index 6bbb60715f0e1c351373bc5651c479990490abe9..a67870c8e61e444cc834753bb415bff98b53d961 100644 (file)
 <?php
 
-class SimpleCaptcha {
-       function __construct() {
-               global $wgCaptchaStorageClass;
-               $this->storage = new $wgCaptchaStorageClass;
+/**
+ * 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 {
+
        function getCaptcha() {
                $a = mt_rand( 0, 100 );
                $b = mt_rand( 0, 10 );
@@ -41,7 +253,9 @@ class SimpleCaptcha {
                $captcha = $this->getCaptcha();
                $index = $this->storeCaptcha( $captcha );
 
-               return "<p><label for=\"wpCaptchaWord\">{$captcha['question']}</label> = " .
+               // dir="ltr" is needed because otherwise it may say
+               // "5 - 20" instead of "20 - 5" and that would be wrong.
+               return "<p><label dir=\"ltr\" for=\"wpCaptchaWord\">{$captcha['question']}</label> = " .
                        Xml::element( 'input', array(
                                'name' => 'wpCaptchaWord',
                                'id'   => 'wpCaptchaWord',
@@ -81,7 +295,7 @@ class SimpleCaptcha {
        /**
         * Inject whazawhoo
         * @fixme if multiple thingies insert a header, could break
-        * @param HTMLForm
+        * @param $form HTMLForm
         * @return bool true to keep running callbacks
         */
        function injectEmailUser( &$form ) {
@@ -91,7 +305,7 @@ class SimpleCaptcha {
                                wfDebug( "ConfirmEdit: user group allows skipping captcha on email sending\n" );
                                return true;
                        }
-                       $form->addFooterText( 
+                       $form->addFooterText(
                                "<div class='captcha'>" .
                                $wgOut->parse( $this->getMessage( 'sendemail' ) ) .
                                $this->getForm() .
@@ -103,7 +317,7 @@ class SimpleCaptcha {
        /**
         * Inject whazawhoo
         * @fixme if multiple thingies insert a header, could break
-        * @param SimpleTemplate $template
+        * @param QuickTemplate $template
         * @return bool true to keep running callbacks
         */
        function injectUserCreate( &$template ) {
@@ -126,7 +340,7 @@ class SimpleCaptcha {
         * Inject a captcha into the user login form after a failed
         * password attempt as a speedbump for mass attacks.
         * @fixme if multiple thingies insert a header, could break
-        * @param SimpleTemplate $template
+        * @param $template QuickTemplate
         * @return bool true to keep running callbacks
         */
        function injectUserLogin( &$template ) {
@@ -410,6 +624,8 @@ class SimpleCaptcha {
 
        /**
         * Load external links from the externallinks table
+        * @param $title Title
+        * @return Array
         */
        function getLinksFromTracker( $title ) {
                $dbr = wfGetDB( DB_SLAVE );
@@ -476,6 +692,7 @@ class SimpleCaptcha {
                        $this->addCaptchaAPI( $resultArr );
                        return false;
                }
+
                return true;
        }
 
@@ -526,7 +743,7 @@ class SimpleCaptcha {
        }
 
        /**
-        * Check the captcha on Special:EmailUser 
+        * Check the captcha on Special:EmailUser
         * @param $from MailAddress
         * @param $to MailAddress
         * @param $subject String
@@ -543,7 +760,7 @@ class SimpleCaptcha {
                        }
                        if ( $this->isIPWhitelisted() )
                                return true;
-               
+
                        if ( defined( 'MW_API' ) ) {
                                # API mode
                                # Asking for captchas in the API is really silly
@@ -559,6 +776,36 @@ class SimpleCaptcha {
                return true;
        }
 
+       /**
+        * @param $module ApiBase
+        * @param $params array
+        * @return bool
+        */
+       public function APIGetAllowedParams( &$module, &$params ) {
+               if ( !$module instanceof ApiEditPage ) {
+                       return true;
+               }
+               $params['captchaword'] = null;
+               $params['captchaid'] = null;
+
+               return true;
+       }
+
+       /**
+        * @param $module ApiBae
+        * @param $desc array
+        * @return bool
+        */
+       public function APIGetParamDescription( &$module, &$desc ) {
+               if ( !$module instanceof ApiEditPage ) {
+                       return true;
+               }
+               $desc['captchaid'] = 'CAPTCHA ID from previous request';
+               $desc['captchaword'] = 'Answer to the CAPTCHA';
+
+               return true;
+       }
+
        /**
         * Given a required captcha run, test form input for correct
         * input on the open session.
@@ -607,7 +854,7 @@ class SimpleCaptcha {
                        // 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'];
        }
 
@@ -618,7 +865,7 @@ class SimpleCaptcha {
        function retrieveCaptcha() {
                global $wgRequest;
                $index = $wgRequest->getVal( 'wpCaptchaId' );
-               return $this->storage->retrieve( $index );
+               return CaptchaStore::get()->retrieve( $index );
        }
 
        /**
@@ -626,7 +873,7 @@ class SimpleCaptcha {
         * it can't be reused.
         */
        function clearCaptcha( $info ) {
-               $this->storage->clear( $info['index'] );
+               CaptchaStore::get()->clear( $info['index'] );
        }
 
        /**
@@ -673,7 +920,7 @@ class SimpleCaptcha {
                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' ) );
                }
        }