Seditio Source
Root |
./othercms/b2evolution_7.2.3/plugins/_captcha_qstn.plugin.php
<?php
/**
 * This file implements the Captcha Questions plugin.
 *
 * The core functionality was provided by Francois PLANQUE.
 *
 * This file is part of the b2evolution/evocms project - {@link http://b2evolution.net/}.
 * See also {@link https://github.com/b2evolution/b2evolution}.
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2012-2016 by Francois Planque - {@link http://fplanque.com/}.
 *
 * {@internal
 * b2evolution 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.
 *
 * b2evolution 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.
 * }}
 *
 * @package plugins
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * The Captcha Questions Plugin.
 *
 * It displays an captcha question through {@link ValidateCaptcha()} and validates
 * it in {@link ValidateCaptcha()}.
 */
class captcha_qstn_plugin extends Plugin
{
    var
$version = '7.2.3';
    var
$group = 'antispam';
    var
$code = 'captcha_qstn';


   
/**
     * Init
     */
   
function PluginInit( & $params )
    {
       
$this->name = $this->T_('Captcha questions');
       
$this->short_desc = $this->T_('Use questions to tell humans and robots apart.');
    }


   
/**
     * Define the GLOBAL settings of the plugin here. These can then be edited in the backoffice in System > Plugins.
     *
     * @param array Associative array of parameters (since v1.9).
     *    'for_editing': true, if the settings get queried for editing;
     *                   false, if they get queried for instantiating {@link Plugin::$Settings}.
     * @return array see {@link Plugin::GetDefaultSettings()}.
     * The array to be returned should define the names of the settings as keys (max length is 30 chars)
     * and assign an array with the following keys to them (only 'label' is required):
     */
   
function GetDefaultSettings( & $params )
    {
        global
$Settings;

        return array(
               
'use_for_anonymous_item' => array(
                   
'label' => $this->T_('Use for anonymous item forms'),
                   
'defaultvalue' => 1,
                   
'note' => $this->T_('Should this plugin be used for anonymous users on item forms?'),
                   
'type' => 'checkbox',
                ),
               
'use_for_anonymous_comment' => array(
                   
'label' => $this->T_('Use for anonymous comment forms'),
                   
'defaultvalue' => 1,
                   
'note' => $this->T_('Should this plugin be used for anonymous users on comment forms?'),
                   
'type' => 'checkbox',
                ),
               
'use_for_registration' => array(
                   
'label' => $this->T_('Use for new registration forms'),
                   
'defaultvalue' => 1,
                   
'note' => $this->T_('Should this plugin be used on registration forms?'),
                   
'type' => 'checkbox',
                ),
               
'use_for_anonymous_message' => array(
                   
'label' => $this->T_('Use for anonymous messaging forms'),
                   
'defaultvalue' => 1,
                   
'note' => $this->T_('Should this plugin be used for anonymous users on messaging forms?'),
                   
'type' => 'checkbox',
                ),
               
'questions' => array(
                   
'label' => $this->T_('Questions'),
                   
'defaultvalue' => '',
                   
'note' => $this->T_('Type each question in one line with following format:<br />Question text? answer1|answer2|...|answerN'),
                   
'cols' => 80,
                   
'rows' => 10,
                   
'type' => 'html_textarea',
                ),
            );
    }


   
/**
     * We want a table to store our Captcha data (private key, timestamp, ..).
     *
     * @return array
     */
   
function GetDbLayout()
    {
        global
$DB;

        return array(
               
'CREATE TABLE '.$this->get_sql_table('questions').' (
                    cptq_ID       INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    cptq_question VARCHAR( 255 ) COLLATE utf8mb4_unicode_ci NOT NULL,
                    cptq_answers  VARCHAR( 255 ) COLLATE utf8mb4_unicode_ci NOT NULL,
                    PRIMARY KEY( cptq_ID )
                ) ENGINE = innodb DEFAULT CHARSET = '
.$DB->get_connection_charset(),

               
// Assign a random question ID for each IP address
               
'CREATE TABLE '.$this->get_sql_table('ip_question').' (
                    cptip_IP      INT UNSIGNED NOT NULL,
                    cptip_cptq_ID INT UNSIGNED NOT NULL,
                    KEY( cptip_IP, cptip_cptq_ID )
                ) ENGINE = innodb DEFAULT CHARSET = '
.$DB->get_connection_charset(),
            );
    }


   
/**
     * Check the available questions in DB table.
     */
   
function BeforeEnable()
    {
       
// Convert value of setting "questions" into DB table records
       
$this->PluginSettingsUpdateAction();

        if(
$error = $this->validate_questions() )
        {
            return
$error;
        }

        return
true;
    }


   
/**
     * Update the questions
     */
   
function PluginSettingsUpdateAction()
    {
        global
$DB, $Messages;

       
// Store in this array all questions IDs which are inserted or updated on currect action
       
$questions_IDs = array( -1 );
       
// Store in this array only questions with correct format
       
$questions_formated = array();

       
$questions = $this->Settings->get( 'questions' );
        if( empty(
$questions ) )
        {
// No questions
            // Delete all questions from DB:
           
$DB->query( 'DELETE FROM '.$this->get_sql_table( 'questions' ) );
            return
true;
        }

       
$questions = explode( "\n", str_replace( "\r\n", "\n", $questions ) );

       
$insert_data = array();
       
$update_data = array();
        foreach(
$questions as $question )
        {
            if(
preg_match( '/(.+\?)(.+)/i', $question, $match ) )
            {
// Check a question for correct format
               
$question = trim( $match[1] );
               
$answers = trim( $match[2] );
                if( !empty(
$question ) && !empty( $answers ) )
                {
// Save this question in DB
                   
$SQL = new SQL( 'Get captcha question' );
                   
$SQL->SELECT( 'cptq_ID' );
                   
$SQL->FROM( $this->get_sql_table( 'questions' ) );
                   
$SQL->WHERE( 'cptq_question = '.$DB->quote( $question ) );

                    if(
$question_ID = $DB->get_var( $SQL ) )
                    {
// This question already exists, we should only update the answers
                       
$update_data[ $question_ID ] = $answers;
                    }
                    else
                    {
// New question
                       
$insert_data[] = '( '.$DB->quote( $question ).', '.$DB->quote( $answers ).' )';
                    }

                   
$questions_formated[] = $question.' '.$answers;
                }
            }
            elseif( ! empty(
$question ) )
            {
// Incorrect format of question
               
$Messages->add( sprintf( T_('Invalid question format in the line %s.'), '"<b>'.$question.'</b>"' ), 'error' );
            }
        }

        if(
param_errors_detected() )
        {
// Don't save the changes on errors
           
return false;
        }
        else
        {
// Save the changes on success
           
if( count( $insert_data ) )
            {
// Insert new questions in DB:
               
foreach( $insert_data as $qstn_data )
                {
                   
$DB->query( 'INSERT INTO '.$this->get_sql_table( 'questions' ).'
                        ( cptq_question, cptq_answers ) VALUES '
.$qstn_data );
                   
$questions_IDs[] = $DB->insert_id;
                }
            }

            if(
count( $update_data ) )
            {
// Update the questions in DB:
               
foreach( $update_data as $qstn_ID => $qstn_answers )
                {
                   
$DB->query( 'UPDATE '.$this->get_sql_table( 'questions' ).'
                          SET cptq_answers = '
.$DB->quote( $qstn_answers ).'
                        WHERE cptq_ID = '
.$qstn_ID );
                   
$questions_IDs[] = $qstn_ID;
                }
            }

           
// Delete the old questions from DB:
           
$DB->query( 'DELETE FROM '.$this->get_sql_table( 'questions' ).'
                WHERE cptq_ID NOT IN ( '
.implode( ', ', $questions_IDs ).' )' );

           
// Resave the questions in order to delere all whitespaces and questions with incorrect format
           
$this->Settings->set( 'questions', implode( "\r\n", $questions_formated ) );

            return
true;
        }
    }


   
/**
     * Event handler: general event to validate a captcha which payload was added
     * through {@link RequestCaptcha()}.
     *
     * NOTE: if the action is verified/completed in total, you HAVE to cleanup its data
     *       and is not vulnerable against multiple usage of the same captcha!
     *
     * @param array Associative array of parameters
     * @return boolean true if the catcha could be validated
     */
   
function ValidateCaptcha( & $params )
    {
        if( ! empty(
$params['is_preview'] ) || ! isset( $params['form_type'] ) || ! $this->does_apply( $params['form_type'] ) )
        {    
// We should not apply captcha to the requested form:
           
return true;
        }

       
$posted_answer = utf8_strtolower( param( 'captcha_qstn_'.$this->ID.'_answer', 'string', '' ) );

        if( empty(
$posted_answer ) )
        {
           
$this->debug_log( 'captcha_qstn_'.$this->ID.'_answer' );
           
param_error( 'captcha_qstn_'.$this->ID.'_answer', $this->T_('Please enter the captcha answer.') );
            if(
$params['form_type'] == 'comment' && ( $comment_Item = & $params['Comment']->get_Item() ) )
            {
               
syslog_insert( 'Comment captcha answer is not entered', 'warning', 'item', $comment_Item->ID, 'plugin', $this->ID );
            }
            return
false;
        }

       
$question = $this->CaptchaQuestion();

       
$posted_answer_is_correct = false;

       
$answers = explode( '|', utf8_strtolower( $question->cptq_answers ) );
        foreach(
$answers as $answer )
        {
            if(
$posted_answer == $answer )
            {    
// Correct answer is found in DB
               
$posted_answer_is_correct = true;
                break;
            }
        }

        if( !
$posted_answer_is_correct )
        {
           
$this->debug_log( 'Posted ('.$posted_answer.') and saved ('.$question->cptq_answers.') answer do not match!' );
           
param_error( 'captcha_qstn_'.$this->ID.'_answer', $this->T_('The entered answer is incorrect.') );
            if(
$params['form_type'] == 'comment' && ( $comment_Item = & $params['Comment']->get_Item() ) )
            {
               
syslog_insert( 'Comment captcha answer is incorrect', 'warning', 'item', $comment_Item->ID, 'plugin', $this->ID );
            }
            return
false;
        }

       
// If answer is correct:
        //   We should clean the question ID that was assigned for current session and IP address
        //   It gives to assign new question on the next captcha event
       
$this->CaptchaQuestionCleanup();

        return
true;
    }


   
/**
     * Get question for current user
     *
     * @return object Question row from DB table "questions"
     */
   
function CaptchaQuestion()
    {
        global
$DB, $Session;

       
$question = NULL;

       
$IP = ip2int( $_SERVER['REMOTE_ADDR'] );

       
// Get question ID from Session
       
$this->question_ID = $Session->get( 'captcha_qstn_'.$this->ID.'_ID' );

        if( empty(
$this->question_ID ) )
        {    
// Get question from DB by current IP address
           
$SQL = new SQL( 'Get captcha question from DB by current IP address' );
           
$SQL->SELECT( 'cq.*' );
           
$SQL->FROM( $this->get_sql_table( 'ip_question' ) );
           
$SQL->FROM_add( 'INNER JOIN '.$this->get_sql_table( 'questions' ).' AS cq ON cptip_cptq_ID = cptq_ID' );
           
$SQL->WHERE( 'cptip_IP = '.$DB->quote( $IP ) );

            if(
$question = $DB->get_row( $SQL ) )
            {
               
$this->question_ID = $question->cptq_ID;
            }
        }

        if( empty(
$this->question_ID ) )
        {    
// Assign new random question for current IP address
           
$question = $this->CaptchaQuestionNew();
        }

        if( empty(
$question ) && !empty( $this->question_ID ) )
        {    
// Get question data
           
$SQL = new SQL( 'Get captcha question data by ID' );
           
$SQL->SELECT( '*' );
           
$SQL->FROM( $this->get_sql_table( 'questions' ) );
           
$SQL->WHERE( 'cptq_ID = '.$DB->quote( $this->question_ID ) );
           
$question = $DB->get_row( $SQL );

            if( empty(
$question ) )
            {    
// Assign random question if previous question doesn't exist in DB
                // This case may happens when admin changed the questions but user has the old question ID in the session or in DB table "ip_question"
               
$question = $this->CaptchaQuestionNew();
            }
        }

        return
$question;
    }

   
/**
     * Assign new random question for current IP address
     *
     * @return object Question row from DB table "questions"
     */
   
function CaptchaQuestionNew()
    {
        global
$DB, $Session;

       
$IP = ip2int( $_SERVER['REMOTE_ADDR'] );

       
// Get new random question from DB
       
$SQL = new SQL( 'Get new random captcha question from DB' );
       
$SQL->SELECT( '*' );
       
$SQL->FROM( $this->get_sql_table( 'questions' ) );
       
$SQL->ORDER_BY( 'RAND()' );
       
$SQL->LIMIT( 1 );
       
$question = $DB->get_row( $SQL );

       
// Insert a record for current IP address with assigned question ID
       
$DB->query( 'INSERT INTO '.$this->get_sql_table( 'ip_question' ).'
            ( cptip_IP, cptip_cptq_ID ) VALUES
            ( '
.$DB->quote( $IP ).', '.$DB->quote( $question->cptq_ID ).' )' );

       
// Save the assigned question ID in the session
       
$Session->set( 'captcha_qstn_'.$this->ID.'_ID', $question->cptq_ID );
       
$Session->dbsave();

       
$this->question_ID = $question->cptq_ID;

        return
$question;
    }


   
/**
     * Cleanup used captcha data
     */
   
function CaptchaQuestionCleanup()
    {
        global
$DB, $Session;

       
$IP = ip2int( $_SERVER['REMOTE_ADDR'] );

       
// Remove question ID from session
       
$Session->delete( 'captcha_qstn_'.$this->ID.'_ID' );

       
// Remove question ID from DB table for current IP address
       
$DB->query( 'DELETE FROM '.$this->get_sql_table( 'ip_question' ).'
            WHERE cptip_IP = '
.$DB->quote( $IP ) );
    }


   
/**
     * Event handler: Return data to display captcha html code
     *
     * @param array Associative array of parameters:
     *   - 'Form':          Form object
     *   - 'form_type':     Form type
     *   - 'form_position': Current form position where this event is called
     *   - 'captcha_info':  Default captcha info text(can be changed by this plugin)
     *   - 'captcha_template_question': Default HTML template for question text = '<span class="evo_captcha_question">$captcha_question$</span><br>'
     *   - 'captcha_template_answer':   Default HTML template for answer field = '<span class="evo_captcha_answer">$captcha_answer$</span><br>'
     * @return array Associative array of parameters:
     *   - 'captcha_position': Captcha position where current plugin must be displayed for the requested form type
     *   - 'captcha_html':     Captcha html code
     *   - 'captcha_info':     Captcha info text
     */
   
function RequestCaptcha( & $params )
    {
        if( ! isset(
$params['form_type'] ) || ! $this->does_apply( $params['form_type'] ) )
        {    
// Exit here if the form type is not defined or this captcha plugin should not be applied for the requested form:
           
return false;
        }

        switch(
$params['form_type'] )
        {    
// Set a position where we should display the captcha depending on form type:
           
case 'register':
            case
'item':
            case
'comment':
            case
'message':
               
$captcha_position = 'before_submit_button';
                break;
            default:
               
// The requested form type is not supported by this plugin
               
return false;
        }

       
$question = $this->CaptchaQuestion();

        if( empty(
$question ) )
        {    
// No the defined questions
           
return false;
        }

       
$this->debug_log( 'Question ID is: ('.$this->question_ID.')' );

       
// Question text:
       
$captcha_html = str_replace( '$captcha_question$', $question->cptq_question, $params['captcha_template_question'] );
       
// Answer input field:
       
$captcha_field_params = array(
               
'type'     => 'text',
               
'name'     => 'captcha_qstn_'.$this->ID.'_answer',
               
'value'    => param( 'captcha_qstn_'.$this->ID.'_answer', 'string', '' ),
               
'size'     => 10,
               
'required' => true,
               
'class'    => 'form_text_input form-control',
            );
        if( ! empty(
$params['use_placeholders'] ) )
        {
           
$captcha_field_params['placeholder'] = T_('Please answer the question above');
        }
       
$Form = & $params['Form'];
       
$captcha_html .= str_replace( '$captcha_answer$', $Form->get_input_element( $captcha_field_params ), $params['captcha_template_answer'] );

        return array(
               
'captcha_position' => $captcha_position,
               
'captcha_html'     => $captcha_html,
               
'captcha_info'     => ( empty( $params['use_placeholders'] ) ? $this->T_('Please answer the question above').'.<br>' : '' ).$params['captcha_info'],
            );
    }


   
/* PRIVATE methods */

    /**
     * Checks if we should captcha the current request, according to the settings made.
     *
     * @param string Form type ( comment|register|message )
     * @return boolean
     */
   
function does_apply( $form_type )
    {
        switch(
$form_type )
        {
            case
'item':
                if( !
is_logged_in() )
                {
                    return
$this->Settings->get( 'use_for_anonymous_item' );
                }
                break;

            case
'comment':
                if( !
is_logged_in() )
                {
                    return
$this->Settings->get( 'use_for_anonymous_comment' );
                }
                break;

            case
'register':
                return
$this->Settings->get( 'use_for_registration' );

            case
'message':
                if( !
is_logged_in() )
                {
                    return
$this->Settings->get( 'use_for_anonymous_message' );
                }
                break;
        }

        return
false;
    }


   
/**
     * Check if questions exist in DB.
     *
     * @access private
     */
   
function validate_questions()
    {
        global
$DB;

       
$SQL = new SQL( 'Check if captcha questions exist in DB' );
       
$SQL->SELECT( 'cptq_ID' );
       
$SQL->FROM( $this->get_sql_table( 'questions' ) );

        if( !
$DB->get_var( $SQL ) )
        {
            return
$this->T_( 'Not Enabled: You should create at least one question!' );
        }
    }

}

?>