QID authentication module




Registered users of this website can use QID authentication as an alternative login

  1. Click here to login using the QID authentication method
    • You must logout before using it.
    • If you are not a registered user, please click here to register a new account.
  2. After a successful login, click here to logout.



Release 1.0.0

The QID authentication method is implemented as an action plugin for the Wikka Wiki package.

qidlogin.php
<?php
/**
 * Display a form to login using knowledge based authentication questions about wiki activities.
 *
 * @package     actions
 * @version     $RCSfile: qidlogin.php,v $    $Revision: 1.30 $      $Date: 2008/09/22 07:18:45 $
 * @license     http://www.opensource.org/licenses/bsd-license.php
 * @author      Chi Nguyen
 * @author      {@link http://qid.cnfolio.com Chi Nguyen}
 */



//-------------------------------------------------------
//
// QID configuration settings
//
if ( !defined( 'QID_HASH_SALT' ) ) define( 'QID_HASH_SALT', "saltvalue" ); /* MUST BE CHANGED FOR EACH QID INSTALLATION */
if ( !defined( 'QID_METADATA_REFRESH_RATE' ) ) define( 'QID_METADATA_REFRESH_RATE', "7" ); /* Refresh rate specified in days */
if ( !defined( 'QID_SMALL_DATA_LIMIT' ) ) define( 'QID_SMALL_DATA_LIMIT', "10" ); /* Limit iterations when randomly selecting authentication attributes for small data sets */
if ( !defined( 'USERSETTINGS_PAGE' ) ) define( 'USERSETTINGS_PAGE', "UserSettings" ); /* Name of wiki page to manage user settings after login */


//-------------------------------------------------------
//
// Attributes used to generate challenge questions
//
if ( !defined( 'COMMENTS_ATTRIBUTE' ) ) define( 'COMMENTS_ATTRIBUTE', "1" ); /* Number of comments written */
if ( !defined( 'PAGES_OWNED_ATTRIBUTE' ) ) define( 'PAGES_OWNED_ATTRIBUTE', "2" ); /* Number of pages owned */
if ( !defined( 'PAGES_EDITED_ATTRIBUTE' ) ) define( 'PAGES_EDITED_ATTRIBUTE', "3" ); /* Number of pages edited */
if ( !defined( 'LOGIN_ATTRIBUTE' ) ) define( 'LOGIN_ATTRIBUTE', "4" ); /* Number of successful logins */
if ( !defined( 'PAGES_READ_ATTRIBUTE' ) ) define( 'PAGES_READ_ATTRIBUTE', "5" ); /* Number of pages read */


//-------------------------------------------------------
//
// Display same error message text to resist brute force attacks
//
if ( !defined( 'ERROR_NON_EXISTENT_USERNAME' ) ) define( 'ERROR_NON_EXISTENT_USERNAME', "Login attempt was not successful, please try again.");
if ( !defined( 'ERROR_USER_SUSPENDED' ) ) define( 'ERROR_USER_SUSPENDED', "Login attempt was not successful, please try again.." );
if ( !defined( 'ERROR_WRONG_ANSWERS' ) ) define( 'ERROR_WRONG_ANSWERS', "Login attempt was not successful, please try again.");
if ( !defined( 'ERROR_INVALID_FORM' ) ) define( 'ERROR_INVALID_FORM', "Login attempt was not successful, please try again.");
if ( !defined( 'ERROR_UNDEF_ATTR_VALUE' ) ) define( 'ERROR_UNDEF_ATTR_VALUE', "-99999" );


//-------------------------------------------------------
//
// Login form parameters and settings
//
if ( !defined( 'LOGIN_PARAM' ) ) define( 'LOGIN_PARAM', "name" );
if ( !defined( 'ACTION_PARAM' ) ) define( 'ACTION_PARAM', "action" );
if ( !defined( 'ACTION_STEP1' ) ) define( 'ACTION_STEP1', "qid_step_1" );
if ( !defined( 'ACTION_STEP2' ) ) define( 'ACTION_STEP2', "qid_step_2" );
if ( !defined( 'ANSWER_1' ) ) define( 'ANSWER_1', "a1" );
if ( !defined( 'ANSWER_2' ) ) define( 'ANSWER_2', "a2" );
if ( !defined( 'ANSWER_HASH_1' ) ) define( 'ANSWER_HASH_1', "a1x" );
if ( !defined( 'ANSWER_HASH_2' ) ) define( 'ANSWER_HASH_2', "a2x" );


//-------------------------------------------------------
//
// SQL statements
//
if ( !defined( 'SQL_SAVE_NEXT_ATTR' ) ) define( 'SQL_SAVE_NEXT_ATTR', "UPDATE " . $this->config[ 'table_prefix' ] . "qid_meta SET b0 = %d WHERE a0 = '%s' " );
if ( !defined( 'SQL_GET_ATTR_PMF' ) ) define( 'SQL_GET_ATTR_PMF', "SELECT %s AS val FROM " . $this->config[ 'table_prefix' ] . "qid_meta WHERE a0 = '%s' " );
if ( !defined( 'SQL_ADD_METADATA' ) ) define( 'SQL_ADD_METADATA', "INSERT INTO " . $this->config[ 'table_prefix' ] . "qid_meta VALUES ( %d, '%s', %d, %f, %f, %f ) " );
if ( !defined( 'SQL_GET_UID' ) ) define( 'SQL_GET_UID', "SELECT uid FROM " . $this->config[ 'table_prefix' ] . "qid_attr WHERE a0 = '%s' " );
if ( !defined( 'SQL_COUNT_META' ) ) define( 'SQL_COUNT_META',
                                            "SELECT  COUNT( uid ) AS val FROM " . $this->config[ 'table_prefix' ] . "qid_attr WHERE %s = %d AND %s = %d " .
                                            "AND a0 != 'REFRESHINPROGRESS' " );
if ( !defined( 'SQL_CLEAR_METADATA' ) ) define( 'SQL_CLEAR_METADATA', "DELETE FROM " . $this->config[ 'table_prefix' ] . "qid_meta " );
if ( !defined( 'SQL_ADD_USERDATA' ) ) define( 'SQL_ADD_USERDATA', "INSERT INTO " . $this->config[ 'table_prefix' ] . "qid_attr VALUES ( NULL, '%s', %d, %d, %d, NOW() ) " );
if ( !defined( 'SQL_GET_PAGES_EDITED_ATTR' ) ) define( 'SQL_GET_PAGES_EDITED_ATTR',
                                                       "SELECT  COUNT( DISTINCT( tag ) ) AS val FROM " . $this->config[ 'table_prefix' ] . "pages WHERE user = '%s' " .
                                                       "AND TIMESTAMPDIFF( DAY, time, NOW() ) <= " . QID_METADATA_REFRESH_RATE . ' '  );
if ( !defined( 'SQL_GET_PAGES_OWNED_ATTR' ) ) define( 'SQL_GET_PAGES_OWNED_ATTR',
                                                      "SELECT COUNT(id) AS val FROM " . $this->config[ 'table_prefix' ] . "pages WHERE owner = '%s' AND latest = 'Y' " );
if ( !defined( 'SQL_GET_COMMENTS_ATTR' ) ) define( 'SQL_GET_COMMENTS_ATTR',
                                                   "SELECT COUNT(id) AS val FROM " . $this->config[ 'table_prefix' ] . "comments WHERE user = '%s' " .
                                                   "AND TIMESTAMPDIFF( DAY, time, NOW() ) <= " . QID_METADATA_REFRESH_RATE . ' '  );
if ( !defined( 'SQL_STOP_REFRESH' ) ) define( 'SQL_STOP_REFRESH', "DELETE FROM " . $this->config[ 'table_prefix' ] . "qid_attr WHERE a0 = 'REFRESHINPROGRESS' " );
if ( !defined( 'SQL_START_REFRESH' ) ) define( 'SQL_START_REFRESH', "UPDATE " . $this->config[ 'table_prefix' ] . "qid_attr SET a0 = 'REFRESHINPROGRESS' " );
if ( !defined( 'SQL_IS_REFRESH' ) ) define( 'SQL_IS_REFRESH', "SELECT uid FROM " . $this->config[ 'table_prefix' ] . "qid_attr WHERE a0 = 'REFRESHINPROGRESS' " );
if ( !defined( 'SQL_GET_ATTR_VALUE' ) ) define( 'SQL_GET_ATTR_VALUE', "SELECT %s AS val FROM " . $this->config[ 'table_prefix' ] . "qid_attr WHERE a0 = '%s' " );
if ( !defined( 'SQL_GET_ATTR_PAIR' ) ) define( 'SQL_GET_ATTR_PAIR', "SELECT b0 FROM " . $this->config[ 'table_prefix' ] . "qid_meta WHERE a0 = '%s' " );
if ( !defined( 'SQL_CHECK_REFRESH_TS' ) ) define( 'SQL_CHECK_REFRESH_TS', "SELECT TIMESTAMPDIFF(DAY,ts,NOW()) AS days FROM " . $this->config[ 'table_prefix' ] . "qid_attr " );
if ( !defined( 'SQL_CHECK_USER_EXISTS' ) ) define( 'SQL_CHECK_USER_EXISTS', "SELECT uid FROM " . $this->config[ 'table_prefix' ] . "qid_attr WHERE a0 = '%s' " );
if ( !defined( 'SQL_CREATE_META_TABLE' ) ) define( 'SQL_CREATE_META_TABLE', "CREATE TABLE " . $this->config[ 'table_prefix' ] . "qid_meta ( " .
                                                   "uid bigint(20)  NOT NULL default 0, " .
                                                   "a0  varchar(50) NOT NULL, " .
                                                   "b0  bigint(20)  NOT NULL,  " .
                                                   "a12 float       NOT NULL,  " .
                                                   "a13 float       NOT NULL,  " .
                                                   "a23 float       NOT NULL,  " .
                                                   "PRIMARY KEY (uid), " .
                                                   "KEY a0 (a0) ) TYPE=MyISAM; " );
if ( !defined( 'SQL_CREATE_ATTR_TABLE' ) ) define( 'SQL_CREATE_ATTR_TABLE', "CREATE TABLE " . $this->config[ 'table_prefix' ] . "qid_attr ( " .
                                                   "uid bigint(20)  NOT NULL auto_increment, " .
                                                   "a0  varchar(50) NOT NULL default '', " .
                                                   "a1  bigint(20)  NOT NULL default 0,  " .
                                                   "a2  bigint(20)  NOT NULL default 0,  " .
                                                   "a3  bigint(20)  NOT NULL default 0,  " .
                                                   "ts  datetime    NOT NULL default '0000-00-00 00:00:00', " .
                                                   "PRIMARY KEY (uid), " .
                                                   "KEY a0 (a0) ) TYPE=MyISAM; " );
if ( !defined( 'SQL_CHECK_FOR_TABLES' ) ) define( 'SQL_CHECK_FOR_TABLES', "SHOW TABLES LIKE '%qid_attr' " );


//-------------------------------------------------------
//
// User interface customizations which do not affect QID functionality
//
if ( !defined( 'LOGIN_HEADING1' ) ) define( 'LOGIN_HEADING1', "<h3>QID login step 1 of 2</h3>" );
if ( !defined( 'LOGIN_HEADING2' ) ) define( 'LOGIN_HEADING2', "<h3>QID login step 2 of 2</h3>" );
if ( !defined( 'STEP1_INSTRUCTIONS' ) ) define( 'STEP1_INSTRUCTIONS', "Please type your registered login name." );
if ( !defined( 'STEP2_INSTRUCTIONS' ) ) define( 'STEP2_INSTRUCTIONS', "Please answer both questions to verify your login." );
if ( !defined( 'LOGIN_BUTTON_LABEL' ) ) define( 'LOGIN_BUTTON_LABEL', "Login" );
if ( !defined( 'LOGOUT_BUTTON_LABEL') ) define( 'LOGOUT_BUTTON_LABEL', "Logout" );


//-------------------------------------------------------
//
// Initialize global variables
//
$error_msg = '';
$display_login_form = 1; /* Default is to display login form 1 */
$login_settings = $this->config[ 'base_url' ] . USERSETTINGS_PAGE; /* Display default user settings page after successful login */


//-------------------------------------------------------
//
// Log the user out
//
if ( isset( $_REQUEST[ ACTION_PARAM ] ) && ( $_REQUEST[ ACTION_PARAM ] == LOGOUT_BUTTON_LABEL ) )
{
    $this->LogoutUser();
    $this->Redirect( $login_form = $this->config[ 'base_url' ] . $this->tag, '' );
}


//-------------------------------------------------------
//
// Redirect in the event that login is already done
//
if ( $user = $this->GetUser() )
{
    $this->Redirect( $login_settings, '' );
}


//-------------------------------------------------------
//
// Process authentication request
//
if ( isset( $_POST[ 'submit' ] ) && ( $_POST[ 'submit' ] == LOGIN_BUTTON_LABEL ) )
{
    // Assume user name does not exist
    $error_msg = ERROR_USER_SUSPENDED;

    // Load user information
    if ( isset( $_POST[ LOGIN_PARAM ] ) && ( $existingUser = $this->LoadUser( $_POST[ LOGIN_PARAM ] ) ) )
    {
        // User name does exist, assume that account is suspended
        $error_msg = ERROR_USER_SUSPENDED;

        // Check status of user account
        $status = $existingUser[ 'status' ];
        if ( ! ( ( $status == 'deleted' ) || ( $status == 'suspended' ) || ( $status == 'banned' ) ) )
        {
            if ( isset( $_POST[ ACTION_PARAM ] ) && ( $_POST[ ACTION_PARAM ] == ACTION_STEP1 ) )
            {
                // User name exist and account status is OK, so display login form 2
                $error_msg = '';
                $display_login_form = 2;

                // Generate challenge questions
                $question1 = '';
                $question1_key = '';
                $question2 = '';
                $question2_key = '';
                generate_challenge_questions( $this, $this->GetSafeVar( LOGIN_PARAM, 'post' ), $question1, $question1_key, $question2, $question2_key );
            }
            elseif ( isset( $_POST[ ACTION_PARAM ] ) && ( $_POST[ ACTION_PARAM ] == ACTION_STEP2 ) )
            {
                // User name exist and account status is OK, assume authentication failed and reset to display login form 1
                $error_msg = ERROR_WRONG_ANSWERS;

                // Verify user answers
                if ( verify_answers( $this->GetSafeVar( LOGIN_PARAM, 'post' ),
                                     $this->GetSafeVar( ANSWER_1, 'post' ), $this->GetSafeVar( ANSWER_HASH_1, 'post' ),
                                     $this->GetSafeVar( ANSWER_2, 'post' ), $this->GetSafeVar( ANSWER_HASH_2, 'post' ) ) )
                {
                    // Successful authentication attempt
                    $this->SetUser( $existingUser );
                    $this->Redirect( $login_settings, '' );
                }
            }
            else
            {
                // Reset to display login form 1
                $error_msg = ERROR_INVALID_FORM;
            }
        }
    }
}


//-------------------------------------------------------
//
// Display QID login form 1
//
if ( $display_login_form == 1 )
{
    echo $this->FormOpen();
    echo '<input type="hidden" name="' . ACTION_PARAM . '" value="' . ACTION_STEP1 . '" />';
    echo '<table class="usersettings">';
    echo '<tr><td>' . LOGIN_HEADING1 . '</td><td>&nbsp;</td></tr>';
    echo '<tr><td>' . STEP1_INSTRUCTIONS . '</td></tr>' . "\n";
    if ( $error_msg )
        echo '<tr><td><em class="error">' . $error_msg . '</em></td></tr>';
    echo '<tr><td><input name="' . LOGIN_PARAM . '" size="40" value="' . $this->GetSafeVar( LOGIN_PARAM, 'post' ) . '" /></td></tr>';
    echo '<tr><td><input name="submit" type="submit" value="' . LOGIN_BUTTON_LABEL . '" size="40" /></td></tr>';
    echo '</table>' . $this->FormClose() . "\n";
}


//-------------------------------------------------------
//
// Display QID login form 2
//
if ( $display_login_form == 2 )
{
    echo $this->FormOpen();
    echo '<input type="hidden" name="' . ACTION_PARAM . '" value="' . ACTION_STEP2 . '" />';
    echo '<input type="hidden" name="' . LOGIN_PARAM . '" value="' . $this->GetSafeVar( LOGIN_PARAM, 'post' ) . '" />';
    echo '<input type="hidden" name="' . ANSWER_HASH_1 . '" value="' . $question1_key . '" />';
    echo '<input type="hidden" name="' . ANSWER_HASH_2 . '" value="' . $question2_key . '" />' . "\n";
    echo '<table class="usersettings">';
    echo '<tr><td>' . LOGIN_HEADING2 . '</td><td>&nbsp;</td></tr>';
    echo '<tr><td>' . STEP2_INSTRUCTIONS . '</td></tr>' . "\n";
    echo '<tr><td>&nbsp;</td></tr>';
    echo '<tr><td>' . $question1 . '</td></tr>';
    echo '<tr><td><input name="' . ANSWER_1 . '" size="20" value="" /></td></tr>' . "\n";
    echo '<tr><td>&nbsp;</td></tr>';
    echo '<tr><td>' . $question2 . '</td></tr>';
    echo '<tr><td><input name="' . ANSWER_2 . '" size="20" value="" /></td></tr>' . "\n";
    echo '<tr><td>&nbsp;</td></tr>';
    echo '<tr><td><input name="submit" type="submit" value="' . LOGIN_BUTTON_LABEL . '" size="40" /></td></tr>';
    echo '</table>' . $this->FormClose() . "\n";
}


//-------------------------------------------------------
//
// Generate challenge questions
//
function generate_challenge_questions( &$wiki, $login_user, &$login_q1, &$login_q1key, &$login_q2, &$login_q2key )
{
    init_db_once( $wiki );

    generate_metadata( $wiki, $login_user );
    $attribute_1 = 0;
    $attribute_2 = 0;
    get_current_attributes( $wiki, $login_user, $attribute_1, $attribute_2 );
    set_next_attributes( $wiki, $login_user, $attribute_1, $attribute_2 );

    $login_q1 = generate_question_text( $attribute_1 );
    $login_q2 = generate_question_text( $attribute_2 );

    $login_q1_answer = get_attribute_value( $wiki, $login_user, $attribute_1 );
    $login_q2_answer = get_attribute_value( $wiki, $login_user, $attribute_2 );

    // round the answers to the nearest 10 and encode as hash keys
    $login_q1key = sha1( QID_HASH_SALT . round_answer( $login_q1_answer ) );
    $login_q2key = sha1( QID_HASH_SALT . round_answer( $login_q2_answer ) );
}


//-------------------------------------------------------
//
// Verify answers to challenge questions
//
function verify_answers( $login_user, $login_a1, $login_a1key, $login_a2, $login_a2key )
{
    if ( isset( $login_a1key ) && ( strlen( trim( $login_a1key ) ) == 40 ) && ( trim( $login_a1key ) == sha1( QID_HASH_SALT . $login_a1 ) ) &&
         isset( $login_a2key ) && ( strlen( trim( $login_a2key ) ) == 40 ) && ( trim( $login_a2key ) == sha1( QID_HASH_SALT . $login_a2 ) ) )
    {
        // Successful authentication attempt
        return 1;
    }
    else
    {
        // Failed authentication attempt
        return 0;
    }
}


//-------------------------------------------------------
//
// Initialize the database if this is the first time the action plugin has been used
//
function init_db_once( &$wiki )
{
    // End function if tables exist
    if ( $wiki->LoadSingle( SQL_CHECK_FOR_TABLES ) )
        return;

    // Create attributes table
    $wiki->Query( SQL_CREATE_ATTR_TABLE );

    // Create meta data table
    $wiki->Query( SQL_CREATE_META_TABLE );
}


//-------------------------------------------------------
//
// Generate attributes and meta data for use with the Metropolis Hastings algorithm
//
function generate_metadata( &$wiki, $unique_id )
{
    // Wait if meta data generation is in progress
    while ( $wiki->LoadSingle( SQL_IS_REFRESH ) )
    {
        sleep( 3 ); // wait 3 seconds before checking again
    }

    // Assume meta data is up to date and does not need to be generated
    $refresh_metadata = 0;

    // Refresh meta data if the unique ID is not found
    if ( ! $wiki->LoadSingle( sprintf( SQL_CHECK_USER_EXISTS, mysql_real_escape_string( $unique_id ) ) ) )
        $refresh_metadata = 1;
    else
    {
        // Refresh meta data if it is older than the specified refresh rate
        $age_of_attributes = $wiki->LoadSingle( SQL_CHECK_REFRESH_TS );
        if ( $age_of_attributes && ( $age_of_attributes[ 'days' ] >= QID_METADATA_REFRESH_RATE ) )
            $refresh_metadata = 1;
    }

    if ( $refresh_metadata )
    {
        // Turn flag on to indicate meta data generation in progress
        $wiki->Query( SQL_START_REFRESH );

        // Refresh attribute values
        $all_users = $wiki->LoadUsers();
        foreach ( $all_users as $current_user )
        {
            $a0_value = $current_user[ 'name' ];

            $number_of_comments = $wiki->LoadSingle( sprintf( SQL_GET_COMMENTS_ATTR, mysql_real_escape_string( $a0_value ) ) );
            if ( $number_of_comments ) $a1_value = $number_of_comments[ 'val' ];
            else $a1_value = ERROR_UNDEF_ATTR_VALUE;

            $number_of_pages_owned = $wiki->LoadSingle( sprintf( SQL_GET_PAGES_OWNED_ATTR, mysql_real_escape_string( $a0_value ) ) );
            if ( $number_of_pages_owned ) $a2_value = $number_of_pages_owned[ 'val' ];
            else $a2_value = ERROR_UNDEF_ATTR_VALUE;

            $number_of_pages_edited = $wiki->LoadSingle( sprintf( SQL_GET_PAGES_EDITED_ATTR, mysql_real_escape_string( $a0_value ) ) );
            if ( $number_of_pages_edited ) $a3_value = $number_of_pages_edited[ 'val' ];
            else $a3_value = ERROR_UNDEF_ATTR_VALUE;

            $wiki->Query( sprintf( SQL_ADD_USERDATA, $a0_value, $a1_value, $a2_value, $a3_value ) );
        }

        // Clear meta data
        $wiki->Query( SQL_CLEAR_METADATA );


        // Use the total number of users to calculate the pmf value
        $max_count = count( $all_users );

        // Refresh meta data
        foreach ( $all_users as $current_user )
        {
            // Start with attribute values of current user
            $a0_value = $current_user[ 'name' ];
            $a1_value = get_attribute_value( $wiki, $a0_value, COMMENTS_ATTRIBUTE );
            $a2_value = get_attribute_value( $wiki, $a0_value, PAGES_OWNED_ATTRIBUTE );
            $a3_value = get_attribute_value( $wiki, $a0_value, PAGES_EDITED_ATTRIBUTE );

            // Count similar value combinations and calculate the inverted pmf value
            $meta_count = $wiki->LoadSingle( sprintf( SQL_COUNT_META, 'a1', $a1_value, 'a2', $a2_value ) );
            if ( $meta_count ) $a12_value = round( 1 - ( $meta_count[ 'val' ] / $max_count ), 5 );
            else $a12_value = ERROR_UNDEF_ATTR_VALUE;

            $meta_count = $wiki->LoadSingle( sprintf( SQL_COUNT_META, 'a1', $a1_value, 'a3', $a3_value ) );
            if ( $meta_count ) $a13_value = round( 1 - ( $meta_count[ 'val' ] / $max_count ), 5 );
            else $a13_value = ERROR_UNDEF_ATTR_VALUE;

            $meta_count = $wiki->LoadSingle( sprintf( SQL_COUNT_META, 'a2', $a2_value, 'a3', $a3_value ) );
            if ( $meta_count ) $a23_value = round( 1 - ( $meta_count[ 'val' ] / $max_count ), 5 );
            else $a23_value = ERROR_UNDEF_ATTR_VALUE;

            // Randomly select the first value combination to use for the next authentication request
            switch ( rand( 1, 3 ) )
            {
                case 1 :
                    $b0_value = ( COMMENTS_ATTRIBUTE * 10 ) + PAGES_OWNED_ATTRIBUTE; break;
                case 2 :
                    $b0_value = ( COMMENTS_ATTRIBUTE * 10 ) + PAGES_EDITED_ATTRIBUTE; break;
                case 3 :
                default :
                    $b0_value = ( PAGES_OWNED_ATTRIBUTE * 10 ) + PAGES_EDITED_ATTRIBUTE; break;
            }

            // Find matching uid number for current user
            $user_id_number = $wiki->LoadSingle( sprintf( SQL_GET_UID, $a0_value ) );
            if ( $user_id_number ) $uid_value = $user_id_number[ 'uid' ];
            else $uid_value = ERROR_UNDEF_ATTR_VALUE;

            // Add completed meta data record
            $wiki->Query( sprintf( SQL_ADD_METADATA, $uid_value, $a0_value, $b0_value, $a12_value, $a13_value, $a23_value ) );
        }

        // Meta data generation is done, so turn flag off
        $wiki->Query( SQL_STOP_REFRESH );
    }
}


//-------------------------------------------------------
//
// Use meta table to identify attributes to use for the current authentication request
//
function get_current_attributes( &$wiki, $uid, &$attribute_number_1, &$attribute_number_2 )
{
    // Read attribute pair from meta table
    $attribute_pair = $wiki->LoadSingle( sprintf( SQL_GET_ATTR_PAIR, mysql_real_escape_string( $uid ) ) );

    // Convert to actual attributes
    $attribute_combo = $attribute_pair[ 'b0' ];

    // Index of attribute 1 is the quotient after division by 10
    $attribute_number_1 = floor( $attribute_combo / 10 );

    // Index of attribute 2 is the remainder after division by 10
    $attribute_number_2 = $attribute_combo % 10;
}


//-------------------------------------------------------
//
// Calculate and save the attributes to use in the next authentication request
//
function set_next_attributes( &$wiki, $uid, $attribute_number_1, $attribute_number_2 )
{
    // Accomodate small amounts of data by limiting the number of iterations
    $max_iterations = QID_SMALL_DATA_LIMIT;

    // Modified Metropolis Hastings algorithm
    $current_attributes = ( $attribute_number_1 * 10 ) + $attribute_number_2;
    do
    {
        $next_attributes = get_random_attribute_pair( $current_attributes );

        $mh_ratio = get_attributes_pmf( $wiki, $uid, $next_attributes ) / get_attributes_pmf( $wiki, $uid, $current_attributes );

        $std_random_variable = rand( 1, 100 ) / 100;
    } while ( ( $mh_ratio <= 1 ) && ( $mh_ratio <= $std_random_variable ) && ( $max_iterations-- > 0 ) );

    // Save attributes for next authentication request
    $wiki->Query( sprintf( SQL_SAVE_NEXT_ATTR, $next_attributes, mysql_real_escape_string( $uid ) ) );
}


//-------------------------------------------------------
//
// Select random attributes pair which is not the same as the current attributes paire
//
function get_random_attribute_pair( $current_attributes )
{
    // Accomodate small amounts of data by limiting the number of iterations
    $max_iterations = QID_SMALL_DATA_LIMIT;

    // Number of available attribute pairs
    $max_pairs_available = 3;

    // Randomly generate number until random number is not the same as the current attributes pair
    do
    {
        switch( rand( 1, $max_pairs_available ) )
        {
            case 1 :
                $next_attributes = ( COMMENTS_ATTRIBUTE * 10 ) + PAGES_OWNED_ATTRIBUTE; break;
            case 2 :
                $next_attributes = ( COMMENTS_ATTRIBUTE * 10 ) + PAGES_EDITED_ATTRIBUTE; break;
            case 3 :
            default :
                $next_attributes = ( PAGES_OWNED_ATTRIBUTE * 10 ) + PAGES_EDITED_ATTRIBUTE; break;
               
        }
    } while ( ( $current_attributes == $next_attributes ) && ( $max_iterations-- > 0 ) );

    // Return next attributes
    return $next_attributes;
}


//-------------------------------------------------------
//
// Retrieve the inverted pmf value for particular attributes
//
function get_attributes_pmf( &$wiki, $uid, $attributes )
{
    $a12_equivalent = ( COMMENTS_ATTRIBUTE * 10 ) + PAGES_OWNED_ATTRIBUTE;
    $a13_equivalent = ( COMMENTS_ATTRIBUTE * 10 ) + PAGES_EDITED_ATTRIBUTE;
    $a23_equivalent = ( PAGES_OWNED_ATTRIBUTE * 10 ) + PAGES_EDITED_ATTRIBUTE;

    switch( $attributes )
    {
        case $a12_equivalent :
            $field_name = 'a12'; break;
        case $a13_equivalent :
            $field_name = 'a13'; break;
        case $a23_equivalent :
        default :
            $field_name = 'a23'; break;
    }

    // Read from table
    $attribute_pmf = $wiki->LoadSingle( sprintf( SQL_GET_ATTR_PMF, $field_name, mysql_real_escape_string( $uid ) ) );

    if ( $attribute_pmf )
        return $attribute_pmf[ 'val' ];
    else
        return ERROR_UNDEF_ATTR_VALUE;
}


//-------------------------------------------------------
//
// Generate challenge question
//
function generate_question_text( $attribute_id )
{
    $question_text = "Estimate the number of ";
    $append_refresh_range = 1;
    switch ( $attribute_id )
    {
        case PAGES_READ_ATTRIBUTE :
            $question_text .= "pages you've <strong>read</strong> on this website"; break;
        case LOGIN_ATTRIBUTE :
            $question_text .= "successful logins you've had"; break;
        case PAGES_EDITED_ATTRIBUTE :
            $question_text .= "pages you've <strong>edited</strong> on this website"; break;
        case PAGES_OWNED_ATTRIBUTE :
            $question_text .= "pages you <strong>own</strong> on this website. "; $append_refresh_range = 0; break;
        case COMMENTS_ATTRIBUTE :
        default :
            $question_text .= "comments you've written"; break;
    }

    if ( $append_refresh_range )
        $question_text .= ' during the past ' . QID_METADATA_REFRESH_RATE . ' days. ';

    $question_text .= '<strong>Round to the nearest 10</strong>. For example, 0, 10 or 20.';
    return $question_text;
}


//-------------------------------------------------------
//
// Find the value (answer) for a selected attribute
//
function get_attribute_value( &$wiki, $uid, $attribute_number )
{
    // Select relevant table column
    switch ( $attribute_number )
    {
        case COMMENTS_ATTRIBUTE :
            $attribute_field = 'a1'; break;
        case PAGES_OWNED_ATTRIBUTE :
            $attribute_field = 'a2'; break;
        case PAGES_EDITED_ATTRIBUTE :
        default :
            $attribute_field = 'a3'; break;
    }

    // Read from table
    $attribute = $wiki->LoadSingle( sprintf( SQL_GET_ATTR_VALUE, $attribute_field, mysql_real_escape_string( $uid ) ) );

    if ( $attribute )
        return $attribute[ 'val' ];
    else
        return ERROR_UNDEF_ATTR_VALUE;
}


//-------------------------------------------------------
//
// Round answer to nearest multiple of 10
//
function round_answer( $answer_value )
{
    if ( ( $answer_value < 0 ) && ( $answer_value >= -5 ) )
    {
        // Manually round to zero because PHP rounding function returns -0 instead of plain zero
        return 0;
    }
    else
        return round( $answer_value, -1 );
}
?>