<?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    core
 */

/**
 * Whether CSRF-checks are active for the current zone/page.
 *
 * @return boolean Whether they are
 */
function csrf_filter_active()
{
    if (get_page_name() == 'login') {
        return false;
    }

    global $SITE_INFO;
    if ((is_guest()) && (isset($SITE_INFO['any_guest_cached_too'])) && ($SITE_INFO['any_guest_cached_too'] == '1')) {
        return false;
    }

    $security_token_exceptions = get_option('security_token_exceptions');
    $_security_token_exceptions = ($security_token_exceptions == '') ? array() : explode("\n", $security_token_exceptions);
    return !in_array(get_page_name(), $_security_token_exceptions) && !in_array(get_zone_name(), $_security_token_exceptions);
}

/**
 * Generate and save a CSRF-token.
 *
 * @return ID_TEXT Generated token
 */
function generate_csrf_token()
{
    static $token = null; // So we only have one per screen (otherwise we can get huge numbers in our table)

    $GLOBALS['STATIC_CACHE_ENABLED'] = false;

    if ($token === null) {
        require_code('crypt');
        $token = get_rand_password();

        $GLOBALS['SITE_DB']->query_insert('post_tokens', array(
            'token' => $token,
            'generation_time' => time(),
            'member_id' => get_member(),
            'session_id' => get_session_id(),
            'ip_address' => get_ip_address(),
            'usage_tally' => 0,
        ));
    }

    return $token;
}

/**
 * Check a provided CSRF-token is valid.
 *
 * @param ?ID_TEXT $token Provided token (null: none)
 */
function check_csrf_token($token)
{
    if (!csrf_filter_active()) {
        return;
    }

    if ($token === null) {
        warn_exit(do_lang_tempcode('EVIL_POSTED_FORM_NO_TOKEN_HACK'));
    }

    delete_expired_tokens();

    if ($token == get_session_id() || $token == get_session_id(true)) { // Session also works as a CSRF-token, as client-side knows it (AJAX)
        return;
    }

    $token_rows = $GLOBALS['SITE_DB']->query_select('post_tokens', array('*'), array('token' => $token), '', 1);
    if (isset($token_rows[0])) {
        $token_row = $token_rows[0];

        $member_match = false;
        if (!is_guest() && $token_row['member_id'] == get_member()) {
            $member_match = true;
        }

        $session_match = false;
        if ($token_row['session_id'] == get_session_id()) {
            $session_match = true;
        }

        $ip_match = false;
        if ($token_row['ip_address'] == get_ip_address()) {
            $ip_match = true;
        }

        if (!$member_match && !$session_match && !$ip_match) { // Multiple checks means safe if login status changed, session changed, or networks changed - but not all 3; also allows it to work well for both guests and members
            $GLOBALS['SITE_DB']->query_delete('post_tokens', array('token' => $token), '', 1); // Kill the token, in case a hacker has stolen it and this was a mis-targeting that preempts a successful future targeting

            warn_exit(do_lang_tempcode('EVIL_POSTED_FORM_MISMATCHED_TOKEN_HACK'));
        }

        if ($token_row['usage_tally'] == 0) {
            $expired = ($token_row['generation_time'] < time() - 60 * 60 * intval(get_option('csrf_token_expire_new')));
        } else {
            $expired = ($token_row['generation_time'] < time() - 60 * 60 * intval(get_option('csrf_token_expire_fresh')));
        }
        if ($expired) {
            warn_exit(do_lang_tempcode('EVIL_POSTED_FORM_EXPIRED_TOKEN_HACK'));
        }

        $GLOBALS['SITE_DB']->query_update('post_tokens', array('usage_tally' => $token_row['usage_tally'] + 1), array('token' => $token), '', 1);
    } else {
        warn_exit(do_lang_tempcode('EVIL_POSTED_FORM_UNKNOWN_TOKEN_HACK'));
    }
}

/**
 * Delete expired CSRF-tokens.
 */
function delete_expired_tokens()
{
    $hours = max(intval(get_option('csrf_token_expire_new')), intval(get_option('csrf_token_expire_fresh')));
    $sql = 'DELETE FROM ' . get_table_prefix() . 'post_tokens WHERE generation_time<' . strval(time() - 60 * 60 * $hours);
    $GLOBALS['SITE_DB']->query($sql);
}
