<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    captcha
 */

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__captcha()
{
    require_lang('captcha');
}

/**
 * Outputs and stores information for a CAPTCHA.
 */
function captcha_script()
{
    if (!function_exists('imagepng')) {
        warn_exit(do_lang_tempcode('GD_NEEDED'));
    }

    $code_needed = $GLOBALS['SITE_DB']->query_select_value_if_there('captchas', 'si_code', array('si_session_id' => get_session_id()));
    if (is_null($code_needed)) {
        $code_needed = generate_captcha();
        /*set_http_status_code('500');    This would actually be very slightly insecure, as it could be used to probe (binary) login state via rogue sites that check if CAPTCHAs had been generated

        warn_exit(do_lang_tempcode('CAPTCHA_NO_SESSION'));*/
    }
    mt_srand(crc32($code_needed)); // Important: to stop averaging out of different attempts. This makes the distortion consistent for that particular code.

    safe_ini_set('ocproducts.xss_detect', '0');

    $mode = get_param_string('mode', '');

    // Audio version
    if (($mode == 'audio') && (get_option('audio_captcha') === '1')) {
        header('Content-Type: audio/x-wav');
        header('Content-Disposition: inline; filename="captcha.wav"');
        //header('Content-Disposition: attachment; filename="captcha.wav"');  Useful for testing

        if (cms_srv('REQUEST_METHOD') == 'HEAD') {
            return;
        }

        safe_ini_set('zlib.output_compression', 'Off');

        $data = captcha_audio($code_needed);

        header('Content-Length: ' . strval(strlen($data)));

        echo $data;

        return;
    }

    list($img, $width, $height) = captcha_image($code_needed);

    // Output using CSS
    if (get_option('css_captcha') === '1') {
        echo '
        <!DOCTYPE html>
        <html xmlns="http://www.w3.org/1999/xhtml">
        <head>
            <title>' . do_lang('CONTACT_STAFF_TO_JOIN_IF_IMPAIRED') . '</title>
            <meta name="robots" content="noindex" />
        </head>
        <body style="margin: 0">
        ';
        if (get_option('js_captcha') === '1') {
            echo '<div style="display: none" id="hidden_captcha">';
        }
        echo '<div style="width: ' . strval($width) . 'px; font-size: 0; line-height: 0">';
        for ($j = 0; $j < $height; $j++) {
            for ($i = 0; $i < $width; $i++) {
                $colour = imagecolorsforindex($img, imagecolorat($img, $i, $j));
                echo '<span style="vertical-align: bottom; overflow: hidden; display: inline-block; -webkit-text-size-adjust: none; text-size-adjust: none; background: rgb(' . strval($colour['red']) . ',' . strval($colour['green']) . ',' . strval($colour['blue']) . '); width: 1px; height: 1px"></span>';
            }
            echo '<br />';
        }
        echo '</div>';
        if (get_option('js_captcha') === '1') {
            echo '</div>';
            echo '<script>document.getElementById(\'hidden_captcha\').style.display=\'block\';</script>';
        }
        echo '
        </body>
        </html>
        ';
        imagedestroy($img);
        exit();
    }

    // Output as a PNG
    header('Content-Type: image/png');
    imagepng($img);
    imagedestroy($img);
}

/**
 * Create an image CATCHA.
 *
 * @param  string $code_needed The code
 * @return array A tuple: the image CAPTCHA, the width, the height
 */
function captcha_image($code_needed)
{
    // Write basic, using multiple fonts with random Y-position offsets
    $characters = strlen($code_needed);
    $fonts = array();
    $width = 20;
    for ($i = 0; $i < max(1, $characters); $i++) {
        $font = mt_rand(4, 5); // 1 is too small
        $fonts[] = $font;
        $width += imagefontwidth($font) + 2;
        $height = imagefontheight($font) + 20;
    }
    $img = imagecreate($width, $height);
    $black = imagecolorallocate($img, 0, 0, 0);
    $off_black = imagecolorallocate($img, mt_rand(1, 45), mt_rand(1, 45), mt_rand(1, 45));
    $white = imagecolorallocate($img, 255, 255, 255);
    imagefill($img, 0, 0, $black);
    $x = 10;
    foreach ($fonts as $i => $font) {
        $y_dif = mt_rand(-15, 15);
        imagestring($img, $font, $x, 10 + $y_dif, $code_needed[strlen($code_needed) - $i - 1], $off_black);
        $x += imagefontwidth($font) + 2;
    }
    $x = 10;
    foreach ($fonts as $i => $font) {
        $y_dif = mt_rand(-5, 5);
        imagestring($img, $font, $x, 10 + $y_dif, $code_needed[$i], $white);
        if (get_option('captcha_noise') == '1') {
            imagestring($img, $font, $x + 1, 10 + mt_rand(-1, 1) + $y_dif, $code_needed[$i], $white);
        }
        $x += imagefontwidth($font) + 2;
    }

    // Add some noise
    if (get_option('captcha_noise') == '1') {
        $tricky_remap = array();
        $tricky_remap[$black] = array();
        $tricky_remap[$off_black] = array();
        $tricky_remap[$white] = array();
        for ($i = 0; $i <= 5; $i++) {
            $tricky_remap['!' . strval($black)][] = imagecolorallocate($img, 0 + mt_rand(0, 15), 0 + mt_rand(0, 15), 0 + mt_rand(0, 15));
            $tricky_remap['!' . strval($off_black)][] = $off_black;
            $tricky_remap['!' . strval($white)][] = imagecolorallocate($img, 255 - mt_rand(0, 145), 255 - mt_rand(0, 145), 255 - mt_rand(0, 145));
        }
        $noise_amount = 0.02;//0.04;
        for ($i = 0; $i < intval($width * $height * $noise_amount); $i++) {
            $x = mt_rand(0, $width);
            $y = mt_rand(0, $height);
            if (mt_rand(0, 1) == 0) {
                imagesetpixel($img, $x, $y, $white);
            } else {
                imagesetpixel($img, $x, $y, $black);
            }
        }
        for ($i = 0; $i < $width; $i++) {
            for ($j = 0; $j < $height; $j++) {
                imagesetpixel($img, $i, $j, $tricky_remap['!' . strval(imagecolorat($img, $i, $j))][mt_rand(0, 5)]);
            }
        }
    }

    return array($img, $width, $height);
}

/**
 * Create an audio CATCHA.
 *
 * @param  string $code_needed The code
 * @return string the audio CAPTCHA
 */
function captcha_audio($code_needed)
{
    $data = '';
    for ($i = 0; $i < strlen($code_needed); $i++) {
        $char = strtolower($code_needed[$i]);

        $file_path = get_file_base() . '/data_custom/sounds/' . $char . '.wav';
        if (!file_exists($file_path)) {
            $file_path = get_file_base() . '/data/sounds/' . $char . '.wav';
        }
        $myfile = fopen($file_path, 'rb');
        if ($i != 0) {
            fseek($myfile, 44);
        } else {
            $data = fread($myfile, 44);
        }
        $_data = fread($myfile, filesize($file_path));
        for ($j = 0; $j < strlen($_data); $j++) {
            if (get_option('captcha_noise') == '1') {
                $amp_mod = mt_rand(-2, 2);
                $_data[$j] = chr(min(255, max(0, ord($_data[$j]) + $amp_mod)));

                if (($j != 0) && (mt_rand(0, 10) == 1)) {
                    $data .= $_data[$j - 1];
                }
            }
            $data .= $_data[$j];
        }
        fclose($myfile);
    }

    // Fix up header
    $data = substr_replace($data, pack('V', strlen($data) - 8), 4, 4);
    $data = substr_replace($data, pack('V', strlen($data) - 44), 40, 4);

    return $data;
}

/**
 * Get a captcha (aka security code) form field.
 *
 * @return Tempcode The field
 */
function form_input_captcha()
{
    $tabindex = get_form_field_tabindex(null);

    $code_needed = $GLOBALS['SITE_DB']->query_select_value_if_there('captchas', 'si_code', array('si_session_id' => get_session_id()));
    if (is_null($code_needed)) {
        generate_captcha();
    }

    // Show template
    $input = do_template('FORM_SCREEN_INPUT_CAPTCHA', array('_GUID' => 'f7452af9b83db36685ae8a86f9762d30', 'TABINDEX' => strval($tabindex)));
    return _form_input('captcha', do_lang_tempcode('SECURITY_IMAGE'), do_lang_tempcode('DESCRIPTION_CAPTCHA'), $input, true, false);
}

/**
 * Find whether captcha (the security image) should be used if preferred (making this call assumes it is preferred).
 *
 * @return boolean Whether captcha is used
 */
function use_captcha()
{
    if (get_option('use_captchas') == '0') {
        return false;
    }

    if (!function_exists('imagetypes')) {
        return false;
    }

    if (is_guest()) {
        return true;
    }

    if (running_script('captcha')) {
        return true;
    }

    $days = get_value('captcha_member_days');
    if ((!empty($days)) && ($GLOBALS['FORUM_DRIVER']->get_member_join_timestamp(get_member()) > time() - 60 * 60 * 24 * intval($days))) {
        return true;
    }

    $posts = get_value('captcha_member_posts');
    if ((!empty($posts)) && ($GLOBALS['FORUM_DRIVER']->get_post_count(get_member()) < intval($posts))) {
        return true;
    }

    return false;
}

/**
 * Generate a CAPTCHA image.
 *
 * @return string The code.
 */
function generate_captcha()
{
    $session = get_session_id();

    // Clear out old codes
    $where = 'si_time<' . strval(time() - 60 * 30) . ' OR ' . db_string_equal_to('si_session_id', $session);
    $rows = $GLOBALS['SITE_DB']->query('SELECT si_session_id FROM ' . get_table_prefix() . 'captchas WHERE ' . $where);
    foreach ($rows as $row) {
        @unlink(get_custom_file_base() . '/uploads/auto_thumbs/' . $row['si_session_id'] . '.wav');
    }
    $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'captchas WHERE ' . $where);

    // Create code
    $choices = array('3', '4', '6', '7', '9', 'A', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'R', 'T', 'W', 'X', 'Y');
    $si_code = '';
    for ($i = 0; $i < 6; $i++) {
        $choice = mt_rand(0, count($choices) - 1);
        $si_code .= $choices[$choice]; // NB: In ASCII code all the chars in $choices are 10-99 (i.e. 2 digit)
    }

    // Store code
    $GLOBALS['SITE_DB']->query_insert('captchas', array('si_session_id' => $session, 'si_time' => time(), 'si_code' => $si_code), false, true);

    require_code('files');
    cms_file_put_contents_safe(get_custom_file_base() . '/uploads/auto_thumbs/' . $session . '.wav', captcha_audio($si_code));

    require_javascript('ajax');

    return $si_code;
}

/**
 * Calling this assumes captcha was needed. Checks that it was done correctly.
 *
 * @param  boolean $regenerate_on_error Whether to possibly regenerate upon error.
 */
function enforce_captcha($regenerate_on_error = true)
{
    if (use_captcha()) {
        $code_entered = post_param_string('captcha');
        if (!check_captcha($code_entered, $regenerate_on_error)) {
            set_http_status_code('500');

            warn_exit(do_lang_tempcode('INVALID_SECURITY_CODE_ENTERED'));
        }
    }
}

/**
 * Checks a CAPTCHA.
 *
 * @param  string $code_entered CAPTCHA entered.
 * @param  boolean $regenerate_on_error Whether to possibly regenerate upon error.
 * @return boolean Whether it is valid for the current session.
 */
function check_captcha($code_entered, $regenerate_on_error = true)
{
    if (use_captcha()) {
        $code_needed = $GLOBALS['SITE_DB']->query_select_value_if_there('captchas', 'si_code', array('si_session_id' => get_session_id()));
        if (is_null($code_needed)) {
            if (get_option('captcha_single_guess') == '1') {
                generate_captcha();
            }
            attach_message(do_lang_tempcode('NO_SESSION_SECURITY_CODE'), 'warn');
            return false;
        }
        $passes = (strtolower($code_needed) == strtolower($code_entered));
        if ($regenerate_on_error) {
            if (get_option('captcha_single_guess') == '1') {
                if ($passes) {
                    register_shutdown_function('_cleanout_captcha');
                } else {
                    generate_captcha();
                }
            }
        }
        if (!$passes) {
            $data = serialize($_POST);

            if (
                (strpos($data, '[url=http://') !== false) ||
                (strpos($data, '[link=') !== false) ||
                ((strpos($data, ' href="') !== false) && (strpos($data, '[html') === false) && (strpos($data, '[semihtml') === false) && (strpos($data, '__is_wysiwyg') === false))
            ) {
                log_hack_attack_and_exit('CAPTCHAFAIL_HACK', '', '', true); // This is done to stop spammers hogging server resources via repeatedly re-trying CAPTCHAs
            }
        }
        return $passes;
    }
    return true;
}

/**
 * Delete current CAPTCHA.
 *
 * @ignore
 */
function _cleanout_captcha()
{
    if (!running_script('snippet')) {
        $GLOBALS['SITE_DB']->query_delete('captchas', array('si_session_id' => get_session_id())); // Only allowed to check once
    }
}

/**
 * Get code to do an AJAX check of the CAPTCHA.
 *
 * @return string JavaScript code.
 */
function captcha_ajax_check()
{
    if (!use_captcha()) {
        return '';
    }

    require_javascript('ajax');
    $script = find_script('snippet');
    return "
        var form=document.getElementById('main_form');
        if (!form) form=document.getElementById('posting_form');
        form.old_submit_b=form.onsubmit;
        form.onsubmit=function() {
            document.getElementById('submit_button').disabled=true;
            var url='" . addslashes($script) . "?snippet=captcha_wrong&name='+window.encodeURIComponent(form.elements['captcha'].value);
            if (!do_ajax_field_test(url))
            {
                refresh_captcha(document.getElementById('captcha_readable'),document.getElementById('captcha_audio'),captcha_sound);
                document.getElementById('submit_button').disabled=false;
                return false;
            }
            document.getElementById('submit_button').disabled=false;
            if (typeof form.old_submit_b!='undefined' && form.old_submit_b) return form.old_submit_b();
            return true;
        };
    ";
}
