<?php
# MantisBT - A PHP based bugtracking system

# MantisBT 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.
#
# MantisBT 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.
#
# You should have received a copy of the GNU General Public License
# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Form API
 *
 * Handles form security and validation. Security methods are targeted to
 * work with both GET and POST form types and should allow multiple
 * simultaneous edits of the form to be submitted out-of-order.
 *
 * @package CoreAPI
 * @subpackage FormAPI
 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
 * @link http://www.mantisbt.org
 *
 * @uses config_api.php
 * @uses constant_inc.php
 * @uses crypto_api.php
 * @uses gpc_api.php
 * @uses php_api.php
 * @uses session_api.php
 */

require_api( 'config_api.php' );
require_api( 'constant_inc.php' );
require_api( 'crypto_api.php' );
require_api( 'gpc_api.php' );
require_api( 'php_api.php' );
require_api( 'session_api.php' );

/**
 * Helper function to generate a form action value when forms are designed
 * to be submitted to the same url that's is currently being used, such as
 * helper_ensure_confirmed() or auth_reauthenticate().
 * @return string Form action value
 */
function form_action_self() {
	$t_self = trim( str_replace( "\0", '', $_SERVER['SCRIPT_NAME'] ) );
	return basename( $t_self );
}

/**
 * Generate a random security token, prefixed by date, store it in the
 * user's session, and then return the string to be used as a form element
 * element with the security token as the value.
 * @param string $p_form_name Form name.
 * @return string Security token string
 */
function form_security_token( $p_form_name ) {
	if( PHP_CLI == php_mode() || OFF == config_get_global( 'form_security_validation' ) ) {
		return '';
	}

	$t_tokens = session_get( 'form_security_tokens', array() );

	# Create a new array for the form name if necessary
	if( !isset( $t_tokens[$p_form_name] ) || !is_array( $t_tokens[$p_form_name] ) ) {
		$t_tokens[$p_form_name] = array();
	}

	# Generate a nonce prefixed by date.
	# With a base64 output encoded nonce length of 32 characters, we are
	# generating a 192bit nonce.
	$t_date = date( 'Ymd' );
	$t_string = $t_date . crypto_generate_uri_safe_nonce( 32 );

	# Add the token to the user's session
	if( !isset( $t_tokens[$p_form_name][$t_date] ) ) {
		$t_tokens[$p_form_name][$t_date] = array();
	}

	$t_tokens[$p_form_name][$t_date][$t_string] = true;
	session_set( 'form_security_tokens', $t_tokens );

	# The token string
	return $t_string;
}

/**
 * Get a hidden form element containing a generated form security token.
 * @param string $p_form_name      Form name.
 * @param string $p_security_token Optional security token, previously generated for the same form.
 * @return string Hidden form element to output
 */
function form_security_field( $p_form_name, $p_security_token = null ) {
	if( PHP_CLI == php_mode() || OFF == config_get_global( 'form_security_validation' ) ) {
		return '';
	}

	if( is_null( $p_security_token ) ) {
		$p_security_token = form_security_token( $p_form_name );
	}

	# Create the form element HTML string for the security token
	$t_form_token = $p_form_name . '_token';
	$t_element = sprintf(
		'<input type="hidden" name="%s" value="%s"/>',
		$t_form_token,
		$p_security_token
	);

	return $t_element;
}

/**
 * Get a URL parameter containing a generated form security token.
 * @param string $p_form_name Form name.
 * @param string $p_security_token Optional security token, previously generated for the same form.
 * @return string URL parameter containing security token
 */
function form_security_param( $p_form_name, $p_security_token = null ) {
	if( PHP_CLI == php_mode() || OFF == config_get_global( 'form_security_validation' ) ) {
		return '';
	}

	$t_string = $p_security_token === null ? form_security_token( $p_form_name ) : $p_security_token;

	# Create the GET parameter to be used in a URL for a secure link
	return sprintf(
		'&%s=%s',
		$p_form_name . '_token',
		$t_string
	);
}

/**
 * Validate the security token for the given form name based on tokens
 * stored in the user's session.  While checking stored tokens, any that
 * are more than 3 days old will be purged.
 * @param string $p_form_name Form name.
 * @return boolean Form is valid
 */
function form_security_validate( $p_form_name ) {
	if( PHP_CLI == php_mode() || OFF == config_get_global( 'form_security_validation' ) ) {
		return true;
	}

	$t_tokens = session_get( 'form_security_tokens', array() );

	# Short-circuit if we don't have any tokens for the given form name
	if( !isset( $t_tokens[$p_form_name] ) || !is_array( $t_tokens[$p_form_name] ) || count( $t_tokens[$p_form_name] ) < 1 ) {

		trigger_error( ERROR_FORM_TOKEN_INVALID, ERROR );
		return false;
	}

	# Get the form input
	$t_form_token = $p_form_name . '_token';
	$t_input = gpc_get_string( $t_form_token, '' );

	# No form input
	if( '' == $t_input ) {
		trigger_error( ERROR_FORM_TOKEN_INVALID, ERROR );
		return false;
	}

	# Get the date claimed by the token
	$t_date = mb_substr( $t_input, 0, 8 );

	# Check if the token exists
	if( isset( $t_tokens[$p_form_name][$t_date][$t_input] ) ) {
		return true;
	}

	# Token does not exist
	trigger_error( ERROR_FORM_TOKEN_INVALID, ERROR );
	return false;
}

/**
 * Purge form security tokens that are older than 3 days, or used
 * for form validation.
 * @param string $p_form_name Form name.
 * @return void
 */
function form_security_purge( $p_form_name ) {
	if( PHP_CLI == php_mode() || OFF == config_get_global( 'form_security_validation' ) ) {
		return;
	}

	$t_tokens = session_get( 'form_security_tokens', array() );

	# Short-circuit if we don't have any tokens for the given form name
	if( !isset( $t_tokens[$p_form_name] ) || !is_array( $t_tokens[$p_form_name] ) || count( $t_tokens[$p_form_name] ) < 1 ) {
		return;
	}

	# Get the form input
	$t_form_token = $p_form_name . '_token';
	$t_input = gpc_get_string( $t_form_token, '' );

	# Get the date claimed by the token
	$t_date = mb_substr( $t_input, 0, 8 );

	# Generate a date string of three days ago
	$t_purge_date = date( 'Ymd', time() - ( 3 * 24 * 60 * 60 ) );

	# Purge old token data, and the currently-used token
	unset( $t_tokens[$p_form_name][$t_date][$t_input] );

	foreach( $t_tokens as $t_form_name => $t_dates ) {
		foreach( $t_dates as $t_date => $t_date_tokens ) {
			if( $t_date < $t_purge_date ) {
				unset( $t_tokens[$t_form_name][$t_date] );
			}
		}
	}

	session_set( 'form_security_tokens', $t_tokens );

	return;
}
