<?php
/**
 * Execute cron jobs.
 *
 * Example to use CLI:
 * >c:\php4\php cron_exec.php
 * >c:\php4\php-cli cron_exec.php
 */


/**
 * Include config
 */
require_once dirname(__FILE__).'/../conf/_config.php';

/**
 * Include main initialization
 * Note: This will initialize only the required objects ( e.g. $Session or $Hit objects will NOT be initialized in cli mode ).
 */
require_once $inc_path .'_main.inc.php';

// Start timer for cron job:
// (time is printed out after each action, @see cron_log_action_end())
$Timer->start( 'cron_exec' );

if( $Settings->get( 'system_lock' ) )
{ // System is locked down for maintenance, Stop cron execution
	echo 'The site is locked for maintenance. All scheduled jobs are postponed. No job was executed.';
	exit(0);
}

/**
 * Cron support functions
 */
load_funcs( 'cron/_cron.funcs.php' );

// Register shutdown function to catch fatal errors:
register_shutdown_function( 'cron_job_shutdown' );
// Mark this script as cron job executing in order to catch function debug_die() here and store error log in cron log:
$is_cron_job_executing = true;

/**
 * @global integer Quietness.
 *         1 suppresses trivial/informative messages,
 *         2 suppresses success messages,
 *         3 suppresses errors.
 */
$quiet = 0;
if( $is_cli )
{ // called through Command Line Interface, handle args:

	// Load required functions ( we need to load here, because in CLI mode it is not loaded )
	load_funcs( '_core/_url.funcs.php' );

	if( isset( $_SERVER['argc'], $_SERVER['argv'] ) )
	{
		$argc = $_SERVER['argc'];
		$argv = $_SERVER['argv'];
	}

	if( isset($argv) )
	{ // may not be set for CGI
		foreach( array_slice($argv, 1) as $v )
		{
			switch( $v )
			{
				case '-h':
				case '--help':
					// display help:
					echo $argv[0]." - Execute cron jobs for b2evolution\n";
					echo "\n";
					echo "Options:\n";
					echo " -q --quiet: Be quiet (do not output a message, if there are no jobs).\n";
					echo "             This is especially useful, when running as a cron job.\n";
					echo "             You can use this up to three times to increase quietness.\n";
					echo "             Successful runs can be made silent with \"-q -q\".\n";
					exit(0);
					break;

				case '-q':
				case '--quiet':
					// increase quietness:
					$quiet++;
					break;

				default:
					echo 'Invalid option "'.$v.'". Use "-h" or "--help" for a list of options.'."\n";
					die(1);
			}
		}
	}

	global $default_locale, $current_charset;

	// We don't load _init_session.inc.php in CLI mode, so we set locale and DB connection charset here
	locale_overwritefromDB();
	locale_activate( $default_locale );

	// Init charset handling - this will also set the encoding for MySQL connection
	init_charsets( $current_charset );
}
elseif( ! is_admin_page() )
{ // This is a web request: (for testing purposes only. Not designed for production)

	// Make sure the response is never cached:
	header_nocache();
	header_content_type();

	// Add CSS:
	require_css( 'basic_styles.css', 'rsc_url' ); // the REAL basic styles
	require_css( 'basic.css', 'rsc_url' ); // Basic styles
	?>
	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
	<html>
	<head>
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<title>Cron exec</title>
		<?php include_headlines() /* Add javascript and css files included by plugins and skin */ ?>
	</head>
	<body>
		<h1>Cron exec</h1>
		<p>This script will execute the next task in the cron queue.
		You should normally call it with the CLI (command line interface) version of PHP
		and automate that call through a cron.</p>
	<?php
}

/*
 * The following will feel a little bloated...
 * BUT it is actually a pretty nifty design to prevent double execution of tasks without any transaction!
 * The trick is to rely on the primary key of the cron__log table.
 */

// Get next task to run in queue which has not started execution yet:
$sql = 'SELECT *
					FROM T_cron__task LEFT JOIN T_cron__log ON ctsk_ID = clog_ctsk_ID
				 WHERE clog_ctsk_ID IS NULL
					 AND ctsk_start_datetime <= '.$DB->quote( date2mysql($localtimenow) ).'
				 ORDER BY ctsk_start_datetime ASC, ctsk_ID ASC
				 LIMIT 1';
$task = $DB->get_row( $sql, OBJECT, 0, 'Get next task to run in queue which has not started execution yet' );
$error_task = NULL;

if( empty( $task ) )
{
	cron_log( 'There is no task to execute yet.', 0 );
}
else
{
	$ctsk_ID = $task->ctsk_ID;
	$ctsk_name = cron_job_name( $task->ctsk_key, $task->ctsk_name, $task->ctsk_params );

	// Initialize a var to count a number of cron job actions:
	$cron_log_actions_num = NULL;

	// Store key of currently executing cron job:
	$executing_cron_task_key = $task->ctsk_key;

	cron_log( 'Requesting lock on task #'.$ctsk_ID.' ['.$ctsk_name.']', 0 );

	$DB->halt_on_error = false;
	$DB->show_errors = false;
	$cron_timestart = $localtimenow;
	$sql = 'INSERT INTO T_cron__log( clog_ctsk_ID, clog_realstart_datetime, clog_status)
					VALUES( '.$ctsk_ID.', '.$DB->quote( date2mysql( $cron_timestart ) ).', "started" )';
	// Duplicate query for tests!
	// $DB->query( $sql, 'Request lock' );
	if( $DB->query( $sql, 'Request lock' ) != 1 )
	{ // This has no affected exactly ONE row: error! (probably locked -- duplicate key -- by a concurrent process)
		$DB->show_errors = true;
		$DB->halt_on_error = true;
		cron_log( 'Could not lock. Task is probably handled by another process.', 2 );
	}
	else
	{
		if( !empty( $task->ctsk_repeat_after ) )
		{ // This task wants to be repeated:
			// Note: we use the current time for 2 reasons: 1) prevent scheduling something in the past AND 2) introduce variety so that everyone doesn't run his repeated tasks at the same exact time, especially pings, pollings...
			if( $task->ctsk_repeat_after < 86400 )
			{	// If "Repeat every" < 1 day, then start from current timestamp:
				$new_start_datetime = $localtimenow;
			}
			else
			{	// If "Repeat every" >= 1 day, then start from TODAY at time of "Schedule date":
				$new_start_datetime = strtotime( date( 'Y-m-d', $localtimenow ).' '.substr( $task->ctsk_start_datetime, 11 ) );
			}
			$new_start_datetime += $task->ctsk_repeat_after;
			if( $task->ctsk_key == 'poll-antispam-blacklist' )
			{ // THIS IS A HACK. Guess why we need that!? :P  Please do not override or you'll kill our server :(
				$new_start_datetime += rand( 43200, 86400 ); // 12 to 24 hours
			}
			else
			{ // Normal
				if( ! empty( $task->ctsk_repeat_variation ) )
				{ // Include variation param as random +/- time value
					$new_start_datetime += rand( 0, 2 * $task->ctsk_repeat_variation ) - $task->ctsk_repeat_variation;
				}
			}
			$ctsk_name_insert = empty( $task->ctsk_name ) ? 'NULL' : $DB->quote( $task->ctsk_name );
			$sql = 'INSERT INTO T_cron__task( ctsk_start_datetime, ctsk_repeat_after, ctsk_repeat_variation, ctsk_name, ctsk_key, ctsk_params )
							VALUES( '.$DB->quote( date2mysql( $new_start_datetime ) ).', '.$DB->quote( $task->ctsk_repeat_after ).', '.$DB->quote( $task->ctsk_repeat_variation ).', '
												.$ctsk_name_insert.', '.$DB->quote( $task->ctsk_key ).', '.$DB->quote( $task->ctsk_params ).' )';
			$DB->query( $sql, 'Schedule repeated task.' );
		}

		$DB->show_errors = true;
		$DB->halt_on_error = true;
		cron_log( 'Starting task #'.$ctsk_ID.' ['.$ctsk_name.'] at '.date( 'H:i:s', $localtimenow ).'.', 1 );

		if( empty($task->ctsk_params) )
		{
			$cron_params = array();
		}
		else
		{
			$cron_params = unserialize( $task->ctsk_params );
		}

		// The job may need to know its ID and name (to set logical locks for example):
		$cron_params['ctsk_ID'] = $ctsk_ID;

		// Set max execution time for each cron job separately:
		set_max_execution_time( $Settings->get( 'cjob_timeout_'.$task->ctsk_key ) );

		// Try to execute cron job:
		set_error_handler( 'cron_job_error_handler' );
		try
		{	// EXECUTE CRON JOB:
			$error_message = call_job( $task->ctsk_key, $cron_params );
		}
		catch( Exception $ex )
		{	// Unexpected error:
			$result_status = 'error';
			$error_message = "\n".'b2evolution caught an UNEXPECTED ERROR: '
				.'<b>File:</b> '.$ex->getFile().', '
				.'<b>Line:</b> '.$ex->getLine().', '
				.'<b>Message:</b> '.$ex->getMessage();
			$result_message .= $error_message;
			echo nl2br( $result_message );
			// We must rollback any started transaction in order to proper update cron job log below:
			$DB->rollback();
		}
		restore_error_handler();

		if( ! empty( $error_message ) )
		{	// Set error task in order to report by email to admin in the function detect_timeout_cron_jobs():
			$error_task = array(
					'ID'      => $ctsk_ID,
					'name'    => $ctsk_name,
					'message' => $error_message,
				);

			if( $result_status == 'imap_error' &&
			    ( $max_consecutive_imap_errors = $Settings->get( 'cjob_imap_error_'.$task->ctsk_key ) ) > 1 )
			{	// Check if imap error task can be reported by email to admin:
				$previous_tasks_SQL = new SQL( 'Check consecutive imap error cron jobs' );
				$previous_tasks_SQL->SELECT( 'clog_status' );
				$previous_tasks_SQL->FROM( 'T_cron__log' );
				$previous_tasks_SQL->FROM_add( 'INNER JOIN T_cron__task ON clog_ctsk_ID = ctsk_ID' );
				$previous_tasks_SQL->WHERE( 'ctsk_key = '.$DB->quote( $task->ctsk_key ) );
				$previous_tasks_SQL->ORDER_BY( 'clog_realstart_datetime DESC' );
				// Skip first task because this is a currently executing task still has a status "started" in DB,
				// but after update below the status will be "imap_error":
				$previous_tasks_SQL->LIMIT( '1, '.( $max_consecutive_imap_errors - 1 ) );
				$previous_tasks = $DB->get_col( $previous_tasks_SQL );
				$previous_tasks[] = 'imap_error'; // append status of the currently executing task
				if( count( $previous_tasks ) < $max_consecutive_imap_errors ||
				    count( array_unique( $previous_tasks ) ) > 1 )
				{	// If X previous consecutive tasks have no same status "IMAP error",
					// unset error task in order to don't report by email to admin:
					$error_task = NULL;
				}
			}
		}

		// Record task as finished:
		if( empty( $timestop ) )
		{
			$timestop = time() + $time_difference;
		}

		if( is_array( $result_message ) )
		{	// If result is array we should store it as serialized data
			$result_message = serialize( $result_message );
		}

		$sql = ' UPDATE T_cron__log
								SET clog_status = '.$DB->quote( $result_status ).',
										clog_realstop_datetime = '.$DB->quote( date2mysql( $timestop ) ).',
										clog_messages = '.$DB->quote( $result_message ) /* May be NULL */.',
										clog_actions_num = '.$DB->quote( $cron_log_actions_num ).'
							WHERE clog_ctsk_ID = '.$ctsk_ID;
		$DB->query( $sql, 'Record task as finished.' );
	}

	// Unset data of the executed cron job:
	unset( $ctsk_ID );
	unset( $executing_cron_task_key );
}


//echo 'detecting timeouts...';
// Detect timed out tasks:
detect_timeout_cron_jobs( $error_task );



if( ! $is_cli && ! is_admin_page() )
{	// This is a web request:
	echo '<p><a href="cron_exec.php">Refresh Now!</a></p>';
	echo '<p>This page should refresh automatically in 15 seconds...</p>';
	echo '<!-- This is invalid HTML but it is SOOOOOO helpful! (Delay will be triggered when we reach that point -->';
	echo '<meta http-equiv="Refresh" content="15" />';

	// Add JavaScript and CSS files included by plugins and skin
	include_footerlines();
	?>
	</body>
	</html>
	<?php
}

// Stop timer of cron job:
$Timer->stop( 'cron_exec' );
?>
