<?php
/**
 * This file implements the automation step class.
 *
 * This file is part of the b2evolution/evocms project - {@link http://b2evolution.net/}.
 * See also {@link https://github.com/b2evolution/b2evolution}.
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}.
*
 * @license http://b2evolution.net/about/license.html GNU General Public License (GPL)
 *
 * @package evocore
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );

load_class( '_core/model/dataobjects/_dataobject.class.php', 'DataObject' );
load_funcs( 'automations/model/_automation.funcs.php' );


/**
 * AutomationStep Class
 *
 * @package evocore
 */
class AutomationStep extends DataObject
{
	var $autm_ID;
	var $order;
	var $label;
	var $type;
	var $info;
	var $yes_next_step_ID;
	var $yes_next_step_delay;
	var $no_next_step_ID;
	var $no_next_step_delay;
	var $error_next_step_ID;
	var $error_next_step_delay;
	var $diagram;

	var $Automation = NULL;

	var $yes_next_AutomationStep = NULL;
	var $no_next_AutomationStep = NULL;
	var $error_next_AutomationStep = NULL;

	/**
	 * Constructor
	 *
	 * @param object table Database row
	 */
	function __construct( $db_row = NULL )
	{
		// Call parent constructor:
		parent::__construct( 'T_automation__step', 'step_', 'step_ID' );

		if( $db_row !== NULL )
		{
			$this->ID = $db_row->step_ID;
			$this->autm_ID = $db_row->step_autm_ID;
			$this->order = $db_row->step_order;
			$this->label = $db_row->step_label;
			$this->type = $db_row->step_type;
			$this->info = $db_row->step_info;
			$this->yes_next_step_ID = $db_row->step_yes_next_step_ID;
			$this->yes_next_step_delay = $db_row->step_yes_next_step_delay;
			$this->no_next_step_ID = $db_row->step_no_next_step_ID;
			$this->no_next_step_delay = $db_row->step_no_next_step_delay;
			$this->error_next_step_ID = $db_row->step_error_next_step_ID;
			$this->error_next_step_delay = $db_row->step_error_next_step_delay;
			$this->diagram = $db_row->step_diagram;
		}
	}


	/**
	 * Get delete restriction settings
	 *
	 * @return array
	 */
	static function get_delete_restrictions()
	{
		return array(
				array( 'table' => 'T_automation__user_state', 'fk' => 'aust_next_step_ID', 'msg' => TB_('%d states of User in Automation') ),
				array( 'table' => 'T_automation__step', 'fk' => 'step_yes_next_step_ID', 'and_condition' => 'step_yes_next_step_ID != step_ID', 'msg' => TB_('Step is used as Next Step %d times').' '.TB_('("YES" column)') ),
				array( 'table' => 'T_automation__step', 'fk' => 'step_no_next_step_ID', 'and_condition' => 'step_no_next_step_ID != step_ID', 'msg' => TB_('Step is used as Next Step %d times').' '.TB_('("NO" column)') ),
				array( 'table' => 'T_automation__step', 'fk' => 'step_error_next_step_ID', 'and_condition' => 'step_error_next_step_ID != step_ID', 'msg' => TB_('Step is used as Next Step %d times').' '.TB_('("ERROR" column)') ),
			);
	}


	/**
	 * Insert object into DB based on previously recorded changes.
	 *
	 * @param boolean TRUE to check if step can be inserted e.g. when automation is not paused
	 * @return boolean true on success
	 */
	function dbinsert( $check_restriction = true )
	{
		if( $check_restriction && ! $this->can_be_modified() )
		{	// If this step cannnot be modified
			return false;
		}

		if( $r = parent::dbinsert() )
		{
			// Update next steps with selected option "Loop" to ID of this new inserted Step:
			$next_steps = array(
				'yes_next_step_ID',
				'no_next_step_ID',
				'error_next_step_ID',
			);
			foreach( $next_steps as $next_step_ID_name )
			{
				if( get_param( 'step_'.$next_step_ID_name ) == 'loop' )
				{
					$this->set( $next_step_ID_name, $this->ID ); // Loop
				}
			}
			$this->dbupdate();
		}

		return $r;
	}


	/**
	 * Update the DB based on previously recorded changes
	 *
	 * @return boolean true on success, false on failure to update, NULL if no update necessary
	 */
	function dbupdate()
	{
		if( ! $this->can_be_modified() )
		{	// If this step cannnot be modified
			return false;
		}

		return parent::dbupdate();
	}


	/**
	 * Get a member param by its name
	 *
	 * @param mixed Name of parameter
	 * @return mixed Value of parameter
	 */
	function get( $parname )
	{
		switch( $parname )
		{
			case 'if_condition_js_object':
				// Format values(like dates) of the field "IF Condition" from MySQL DB format to current locale format:
				return param_format_condition( $this->get( 'info' ), 'js' );
		}

		return parent::get( $parname );
	}


	/**
	 * Load data from Request form fields.
	 *
	 * @return boolean true if loaded data seems valid.
	 */
	function load_from_Request()
	{
		global $DB, $admin_url;

		if( empty( $this->ID ) )
		{	// Set Automation only for new creating Step:
			param( 'autm_ID', 'integer', true );
			$this->set_from_Request( 'autm_ID', 'autm_ID' );
		}

		if( ! $this->can_be_modified() && ! param( 'confirm_pause', 'integer' ) )
		{	// Don't allow to edit step of active automation without confirmation:
			global $Messages;
			$Messages->add( empty( $this->ID )
				? TB_('You must pause the automation before creating new step.')
				: TB_('You must pause the automation before changing step.'), 'error' );
		}

		// Order:
		$step_order = param( 'step_order', 'integer', NULL );
		if( $this->ID > 0 )
		{	// Order is required for edited step:
			param_string_not_empty( 'step_order', TB_('Please enter a step order number.') );
		}
		elseif( $step_order === NULL )
		{	// Set order for new creating step automatically:
			$max_order_SQL = new SQL( 'Get max step order for Automation #'.$this->get( 'autm_ID' ) );
			$max_order_SQL->SELECT( 'MAX( step_order )' );
			$max_order_SQL->FROM( 'T_automation__step' );
			$max_order_SQL->WHERE( 'step_autm_ID = '.$this->get( 'autm_ID' ) );
			set_param( 'step_order', $DB->get_var( $max_order_SQL ) + 1 );
		}
		$this->set_from_Request( 'order' );
		if( $this->get( 'order' ) > 0 )
		{	// Check for unique order per Automation:
			$check_order_SQL = new SQL( 'Check unique step order for Automation #'.$this->get( 'autm_ID' ) );
			$check_order_SQL->SELECT( 'step_ID' );
			$check_order_SQL->FROM( 'T_automation__step' );
			$check_order_SQL->WHERE( 'step_autm_ID = '.$this->get( 'autm_ID' ) );
			$check_order_SQL->WHERE_and( 'step_order = '.$this->get( 'order' ) );
			if( $this->ID > 0 )
			{	// Exclude this Step:
				$check_order_SQL->WHERE_and( 'step_ID != '.$this->ID );
			}
			if( $existing_step_ID = $DB->get_var( $check_order_SQL ) )
			{	// Display error because of duplicated order in the same Automation:
				global $admin_url;
				param_error( 'step_order',
					sprintf( TB_('Another step with the same order number already exists in the current automation. Do you want to <a %s>edit that step</a>?'),
						'href="'.$admin_url.'?ctrl=automations&amp;action=edit_step&amp;step_ID='.$existing_step_ID.'"' ) );
			}
		}
		param_check_range( 'step_order', -2147483646, 2147483647, sprintf( TB_('Step order must be numeric (%d - %d).'), -2147483646, 2147483647 ) );

		// Type:
		param_string_not_empty( 'step_type', 'Please select a step type.' );
		$this->set_from_Request( 'type' );
		// Save additional info depending on step type:
		switch( $this->get( 'type' ) )
		{
			case 'if_condition':
				// IF Condition:
				param_condition( 'step_if_condition' );
				param_string_not_empty( 'step_if_condition', TB_('Please set a condition.') );
				$this->set( 'info', get_param( 'step_if_condition' ) );
				break;

			case 'send_campaign':
				// Email campaign:
				param( 'step_email_campaign', 'integer', NULL );
				param_check_number( 'step_email_campaign', TB_('Please select an email campaign.'), true );
				$this->set( 'info', get_param( 'step_email_campaign' ) );
				break;

			case 'notify_owner':
				// Notify owner:
				param( 'step_notification_message', 'text' );
				param_check_not_empty( 'step_notification_message', TB_('Please enter a notification message.') );
				$this->set( 'info', get_param( 'step_notification_message' ) );
				break;

			case 'add_usertag':
			case 'remove_usertag':
				// Add/Remove usertag:
				param_string_not_empty( 'step_usertag', TB_('Please enter an user tag.') );
				if( preg_match( '/(^-|[;,])/', get_param( 'step_usertag' ) ) )
				{	// If usertag has a not allowed char:
					param_error( 'step_usertag', sprintf( TB_('Usertag cannot start with %s and contain chars %s'), '<code>-</code>', '<code>;,</code>' ) );
				}
				$this->set( 'info', get_param( 'step_usertag' ) );
				break;

			case 'subscribe':
			case 'unsubscribe':
				// Subscribe/Unsubscribe:
				param( 'step_newsletter', 'integer', true );
				param_check_not_empty( 'step_newsletter', TB_('Please select a list.') );
				$this->set( 'info', get_param( 'step_newsletter' ) );
				break;

			case 'start_automation':
				// Start new automation:
				param( 'step_automation', 'integer', true );
				param_check_not_empty( 'step_automation', TB_('Please select an automation.') );
				$this->set( 'info', get_param( 'step_automation' ) );
				break;

			case 'user_status':
				// Change user account status:
				param( 'user_status', 'string', true );
				param_check_not_empty( 'user_status', /* Do NOT translate because this error is impossible for normal form */'Please select an account status.' );
				$this->set( 'info', get_param( 'user_status' ) );
				break;

			default:
				$this->set( 'info', NULL, true );
		}

		// Next steps:
		$next_steps = array(
				'yes_next_step_ID'   => 'yes_next_step_delay',
				'no_next_step_ID'    => 'no_next_step_delay',
				'error_next_step_ID' => 'error_next_step_delay',
			);
		foreach( $next_steps as $next_step_ID_name => $next_step_delay_name )
		{
			if( ( $this->get( 'type' ) == 'notify_owner' && $next_step_ID_name == 'no_next_step_ID' ) ||
			    ( in_array( $this->get( 'type' ), array( 'add_usertag', 'remove_usertag' ) ) && $next_step_ID_name == 'error_next_step_ID' ) )
			{	// Some next steps are not used depending on step type:
				$this->set( $next_step_ID_name, NULL, true );
				$this->set( $next_step_delay_name, NULL, true );
			}
			else
			{
				$this->set( $next_step_ID_name, intval( param( 'step_'.$next_step_ID_name, 'string' ) ) );
				$this->set( $next_step_delay_name, param_duration( 'step_'.$next_step_delay_name ) );
			}
		}

		// Label:
		$this->set_label();

		return ! param_errors_detected();
	}


	/**
	 * Get Automation object of this step
	 *
	 * @return object Automation
	 */
	function & get_Automation()
	{
		if( $this->Automation === NULL )
		{	// Initialize Automation object only first time and store in cache:
			$AutomationCache = & get_AutomationCache();
			$this->Automation = & $AutomationCache->get_by_ID( $this->get( 'autm_ID' ), false, false );
		}

		return $this->Automation;
	}


	/**
	 * Get next Step object of this Step by step ID
	 *
	 * @param integer Step type: 'yes', 'no', 'error'
	 * @return object|boolean Next Automation Step OR
	 *                        FALSE - if automation should be stopped after this Step
	 *                                because either it is configured for STOP action
	 *                                            or it is the latest step of the automation
	 */
	function & get_next_AutomationStep_by_type( $next_step_type )
	{
		switch( $next_step_type )
		{
			case 'YES':
				$next_step_ID = $this->get( 'yes_next_step_ID' );
				break;
			case 'NO':
				$next_step_ID = $this->get( 'no_next_step_ID' );
				break;
			case 'ERROR':
				$next_step_ID = $this->get( 'error_next_step_ID' );
				break;
			default:
				debug_die( 'Invalid automation next step type "'.$next_step_type.'"' );
		}

		$next_AutomationStep = false;

		$possible_step_type_results = step_get_result_labels();
		if( empty( $possible_step_type_results[ $this->get( 'type' ) ][ $next_step_type ] ) )
		{	// If the requested result(YES, NO or ERROR) is not supported by current step type:
			return $next_AutomationStep;
		}

		$next_step_ID = intval( $next_step_ID );

		$AutomationStepCache = & get_AutomationStepCache();
		if( $next_step_ID > 0 )
		{	// Get a next Step by defined ID:
			$next_AutomationStep = & $AutomationStepCache->get_by_ID( $next_step_ID, false, false );
		}

		if( $next_step_ID == -1 )
		{	// Stop workflow when option is selected to "STOP":
			$next_AutomationStep = false;
		}
		elseif( $next_step_ID == 0 || ! $next_AutomationStep )
		{	// Get next ordered Step when option is selected to "Continue" OR Step cannot be found by ID in DB:
			$next_AutomationStep = & $AutomationStepCache->get_by_ID( $this->get_next_ordered_step_ID(), false, false );
			if( empty( $next_AutomationStep ) )
			{	// If it is the latest Step of the Automation:
				$next_AutomationStep = false;
			}
		}

		return $next_AutomationStep;
	}


	/**
	 * Get ID of the next ordered Step after this Step
	 *
	 * @return integer|NULL Step ID or NULL if this is the latest
	 */
	function get_next_ordered_step_ID()
	{
		if( empty( $this->ID ) )
		{	// New creating step is the latest by default:
			return NULL;
		}

		global $DB;

		$next_ordered_step_SQL = new SQL( 'Get next ordered Step after current Step #'.$this->ID );
		$next_ordered_step_SQL->SELECT( 'step_ID' );
		$next_ordered_step_SQL->FROM( 'T_automation__step' );
		$next_ordered_step_SQL->WHERE( 'step_autm_ID = '.$DB->quote( $this->get( 'autm_ID' ) ) );
		$next_ordered_step_SQL->WHERE_and( 'step_order > '.$DB->quote( $this->get( 'order' ) ) );
		$next_ordered_step_SQL->ORDER_BY( 'step_order ASC' );
		$next_ordered_step_SQL->LIMIT( 1 );

		return $DB->get_var( $next_ordered_step_SQL );
	}


	/**
	 * Get YES next Step object of this Step
	 *
	 * @return object|boolean Next Automation Step OR
	 *                        FALSE - if automation should be stopped after this Step
	 *                                because either it is configured for STOP action
	 *                                            or it is the latest step of the automation
	 */
	function & get_yes_next_AutomationStep()
	{
		if( $this->yes_next_AutomationStep === NULL )
		{	// Load next Step into cache object:
			$this->yes_next_AutomationStep = & $this->get_next_AutomationStep_by_type( 'YES' );
		}

		return $this->yes_next_AutomationStep;
	}


	/**
	 * Get NO next Step object of this Step
	 *
	 * @return object|boolean Next Automation Step OR
	 *                        FALSE - if automation should be stopped after this Step
	 *                                because either it is configured for STOP action
	 *                                            or it is the latest step of the automation
	 */
	function & get_no_next_AutomationStep()
	{
		if( $this->no_next_AutomationStep === NULL )
		{	// Load next Step into cache object:
			$this->no_next_AutomationStep = & $this->get_next_AutomationStep_by_type( 'NO' );
		}

		return $this->no_next_AutomationStep;
	}


	/**
	 * Get ERROR next Step object of this Step
	 *
	 * @return object|boolean Next Automation Step OR
	 *                        FALSE - if automation should be stopped after this Step
	 *                                because either it is configured for STOP action
	 *                                            or it is the latest step of the automation
	 */
	function & get_error_next_AutomationStep()
	{
		if( $this->error_next_AutomationStep === NULL )
		{	// Load next Step into cache object:
			$this->error_next_AutomationStep = & $this->get_next_AutomationStep_by_type( 'ERROR' );
		}

		return $this->error_next_AutomationStep;
	}


	/**
	 * Execute action for this step
	 *
	 * @param integer User ID
	 * @return string A process log
	 */
	function execute_action( $user_ID )
	{
		global $DB, $servertimenow, $mail_log_message, $executed_automation_steps;

		// Initialize array to store executed steps per user in order to avoid infinite loops:
		if( ! isset( $executed_automation_steps ) )
		{
			$executed_automation_steps = array();
		}
		if( ! isset( $executed_automation_steps[ $user_ID ] ) )
		{
			$executed_automation_steps[ $user_ID ] = array();
		}

		$Automation = & $this->get_Automation();

		$log_nl = "\n";
		$log_point = ' - ';
		$log_bold_start = '<b>';
		$log_bold_end = '</b>';

		$UserCache = & get_UserCache();
		$step_User = & $UserCache->get_by_ID( $user_ID, false, false );

		// Log:
		$process_log = 'Executing '.$log_bold_start.'Step #'.$this->get( 'order' ).$log_bold_end
			.'('.step_get_type_title( $this->get( 'type' ) ).': '.$this->get( 'label' ).')'
			.' of '.$log_bold_start.'Automation: #'.$Automation->ID.$log_bold_end.'('.$Automation->get( 'name' ).')'
			.' for '.$log_bold_start.'User #'.$user_ID.$log_bold_end.( $step_User ? '('.$step_User->get( 'login' ).')' : '' ).'...'.$log_nl;

		// Retrun ERROR result by default for all unknown cases:
		$step_result = 'ERROR';
		$additional_result_message = '';

		if( $step_User )
		{	// Allow to execute action only if User is detected in DB:
			switch( $this->get( 'type' ) )
			{
				case 'if_condition':
					if( $this->check_if_condition( $step_User, $if_condition_log ) )
					{	// The user is matched to condition of this step:
						$step_result = 'YES';
					}
					else
					{	// The user is NOT matched to condition of this step:
						$step_result = 'NO';
					}
					// Log:
					$process_log .= $log_point.'Log: '.$if_condition_log.$log_nl;
					break;

				case 'send_campaign':
					// Send email campaign
					$EmailCampaignCache = & get_EmailCampaignCache();
					if( $step_EmailCampaign = & $EmailCampaignCache->get_by_ID( $this->get( 'info' ), false, false ) )
					{
						if( in_array( $user_ID, $step_EmailCampaign->get_recipients( 'full_receive' ) ) )
						{	// If user already received this email:
							$step_result = 'NO';
							$additional_result_message = 'Email was ALREADY sent';
						}
						elseif( in_array( $user_ID, $step_EmailCampaign->get_recipients( 'full_skipped' ) ) )
						{	// If user is marked to be manually skipped:
							$step_result = 'NO';
							$additional_result_message = 'Manually skipped';
						}
						elseif( in_array( $user_ID, $step_EmailCampaign->get_recipients( 'full_skipped_tag' ) ) )
						{	// If user has user tag that should be skipped:
							$step_result = 'NO';
							$additional_result_message = 'User has skipped user tag';
						}
						elseif( ( $user_subscribed_newsletter_ID = $Automation->is_user_subscribed( $user_ID ) ) &&
						        $step_EmailCampaign->send_email( $user_ID, '', '', 'auto', $user_subscribed_newsletter_ID, $Automation->ID ) )
						{	// If user is subscribed to at least one newsletter of this Automation AND email has been sent to user successfully now:
							$step_result = 'YES';
						}
						else
						{	// Some error on sending of email to user:
							// - problem with php mail function;
							// - user cannot receive such email because of day limit;
							// - user is not activated yet.
							$step_result = 'ERROR';
							if( $user_subscribed_newsletter_ID )
							{	// If user is subscribed but some error on email sending:
								$additional_result_message = empty( $mail_log_message ) ? 'Unknown error' : $mail_log_message;
							}
							else
							{	// If user just doesn't wait this email:
								$additional_result_message = 'User #'.$step_User->ID.'('.$step_User->get( 'login' ).') is not subscribed to email lists of the Automation';
							}
						}
					}
					else
					{	// Wrong stored email campaign for this step:
						$step_result = 'ERROR';
						$additional_result_message = 'Email Campaign #'.$this->get( 'info' ).' is not found in DB.';
					}
					break;

				case 'notify_owner':
					// Notify owner of automation:
					if( ! ( $owner_User = & $Automation->get_owner_User() ) )
					{	// If owner User is not detected in DB:
						$step_result = 'ERROR';
						$additional_result_message = 'Owner User #'.$this->get( 'owner_user_ID' ).' is not found in DB.';
						break;
					}

					$notification_message = str_replace( array(
							'$step_number$',
							'$step_ID$',
							'$automation_name$',
							'$automation_ID$',
						),
						array(
							$this->get( 'order' ),
							$this->ID,
							'"'.$Automation->get( 'name' ).'"',
							$Automation->ID,
						),
						$this->get( 'info' ) );

					$step_user_login_html = $step_User->get_colored_login( array(
							'mask'      => '$avatar$ $login$',
							'use_style' => true,
							'protocol'  => 'http:',
						) );

					$email_template_params = array(
						'message_html' => nl2br( str_replace( '$login$', $step_user_login_html, $notification_message ) ),
						'message_text' => str_replace( '$login$', $step_User->get( 'login' ), $notification_message ),
					);

					if( send_mail_to_User( $owner_User->ID, sprintf( TB_('Notification of automation %s'), '"'.$Automation->get( 'name' ).'"' ), 'automation_owner_notification', $email_template_params ) )
					{	// If email has been sent to user successfully now:
						$step_result = 'YES';
					}
					else
					{	// Some error on sending of email to user:
						// - problem with php mail function;
						// - user cannot receive such email because of day limit;
						// - user is not activated yet.
						$step_result = 'ERROR';
						$additional_result_message = empty( $mail_log_message ) ? 'Unknown error' : $mail_log_message;
					}
					break;

				case 'add_usertag':
					// Add usertag:
					$usertags = $step_User->get_usertags();
					$new_usertag = $this->get( 'info' );
					if( in_array( $new_usertag, $usertags ) )
					{	// If step User was already tagged:
						$step_result = 'NO';
					}
					else
					{	// Add new usertag:
						$step_User->add_usertags( $new_usertag );
						$step_User->dbupdate();
						$step_result = 'YES';
					}
					// Display tag name in log:
					$additional_result_message = $new_usertag;
					break;

				case 'remove_usertag':
					// Remove usertag:
					$usertags = $step_User->get_usertags();
					$del_usertag = $this->get( 'info' );
					if( ! in_array( $del_usertag, $usertags ) )
					{	// if step User didn't have that tag:
						$step_result = 'NO';
					}
					else
					{	// Remove usertag:
						$step_result = 'YES';
						$step_User->remove_usertags( $del_usertag );
						$step_User->dbupdate();
					}
					// Display tag name in log:
					$additional_result_message = $del_usertag;
					break;

				case 'subscribe':
				case 'unsubscribe':
					// Subscribe/Unsubscribe User to List:
					$NewsletterCache = & get_NewsletterCache();
					if( $Newsletter = & $NewsletterCache->get_by_ID( $this->get( 'info' ), false, false ) )
					{	// If List/Newsletter exists:
						if( $this->get( 'type' ) == 'subscribe' )
						{	// Subscribe:
							$affected_subscriprions_num = $step_User->subscribe( $Newsletter->ID );

							// Send notification to owners of lists where user was subscribed:
							$step_User->send_list_owner_notifications( 'subscribe' );
						}
						else
						{	// Unsubscribe:
							$affected_subscriprions_num = $step_User->unsubscribe( $Newsletter->ID );

							// Send notification to owners of lists where user was unsubscribed:
							$step_User->send_list_owner_notifications( 'unsubscribe' );
						}
						$step_result = ( $affected_subscriprions_num ? 'YES' : 'NO' );
						// Display newsletter name in log:
						$additional_result_message = $Newsletter->get( 'name' );
					}
					else
					{	// If List/Newsletter does not exist:
						$step_result = 'ERROR';
						$additional_result_message = 'List #'.$this->get( 'info' ).' is not found in DB.';
					}
					break;

				case 'start_automation':
					// Start new Automation:
					$AutomationCache = & get_AutomationCache();
					if( $new_Automation = & $AutomationCache->get_by_ID( $this->get( 'info' ), false, false ) )
					{	// If Automation exists:
						$added_users_num = $new_Automation->add_users( array( $step_User->ID ), array(
								'users_no_subs'   => 'add',    // Add anyway users who are not subscribed to Newsletter of the Automation
								'users_automated' => 'ignore', // Ignore users who are already in the Automation
								'users_new'       => 'add',    // Add new users
							) );
						$step_result = ( $added_users_num ? 'YES' : 'NO' );
						// Display newsletter name in log:
						$additional_result_message = $new_Automation->get( 'name' );
					}
					else
					{	// If List/Newsletter does not exist:
						$step_result = 'ERROR';
						$additional_result_message = 'Automation #'.$this->get( 'info' ).' is not found in DB.';
					}
					break;

				case 'user_status':
					// Change user account status:
					$current_status = $step_User->get( 'status' );
					$new_status = $this->get( 'info' );
					if( $step_User->ID == 1 )
					{	// Don't allow to change status of the Admin user:
						$step_result = 'ERROR';
						$additional_result_message = 'Status of admin user account cannot be changed';
					}
					elseif( $current_status == $new_status )
					{	// If step User's account is already in the desired status:
						$step_result = 'NO';
						// Display status title in log:
						$user_statuses = get_user_statuses();
						$additional_result_message = ( isset( $user_statuses[ $new_status ] ) ? $user_statuses[ $new_status ] : $new_status );
					}
					elseif( $current_status == 'closed' )
					{	// Don't allow to change a closed status:
						$step_result = 'ERROR';
						$additional_result_message = 'The closed user account cannot be changed to any other status';
					}
					else
					{	// Change user account to another status:
						$step_User->set( 'status', $new_status );
						if( $step_User->dbupdate() )
						{	// Successful user updating:
							$step_result = 'YES';
							// Display status title in log:
							$user_statuses = get_user_statuses();
							$additional_result_message = ( isset( $user_statuses[ $new_status ] ) ? $user_statuses[ $new_status ] : $new_status );
						}
						else
						{	// Unknown error on user updating:
							$step_result = 'ERROR';
						}
					}
					break;

				default:
					// Log:
					$process_log .= $log_point.'No implemented action'.$log_nl;
					break;
			}
		}
		else
		{	// Wrong user:
			$additional_result_message = $log_bold_start.'User #'.$user_ID.$log_bold_end.' is not found in DB.';
		}

		// Log:
		if( $step_result == 'ERROR' && empty( $additional_result_message ) )
		{	// Set default additional error message:
			$additional_result_message = 'Unknown error';
		}
		$process_log .= $log_point.'Result: '.$this->get_result_title( $step_result, $additional_result_message ).'.'.$log_nl;

		// Get data for next step:
		switch( $step_result )
		{
			case 'YES':
				$next_AutomationStep = & $this->get_yes_next_AutomationStep();
				$next_delay = $this->get( 'yes_next_step_delay' );
				break;

			case 'NO':
				$next_AutomationStep = & $this->get_no_next_AutomationStep();
				$next_delay = $this->get( 'no_next_step_delay' );
				break;

			case 'ERROR':
				$next_AutomationStep = & $this->get_error_next_AutomationStep();
				$next_delay = $this->get( 'error_next_step_delay' );
				break;
		}

		if( $next_AutomationStep )
		{	// Use data for next step if it is defined:
			$next_step_ID = $next_AutomationStep->ID;
			if( $next_delay == 0 && in_array( $next_AutomationStep->ID, $executed_automation_steps[ $user_ID ] ) )
			{	// Force a delay of infinite loop to 4 hour:
				$next_exec_ts = date2mysql( $servertimenow + ( 3600 * 4 ) );
			}
			else
			{	// Use normal delay of next step:
				$next_exec_ts = date2mysql( $servertimenow + $next_delay );
			}
		}
		else
		{	// This was the end Step of the Automation:
			$next_step_ID = NULL;
			$next_exec_ts = NULL;
		}
		// Update data for next step or finish it:
		$DB->query( 'UPDATE T_automation__user_state
			  SET aust_next_step_ID = '.$DB->quote( $next_step_ID ).',
			      aust_next_exec_ts = '.$DB->quote( $next_exec_ts ).'
			WHERE aust_autm_ID = '.$DB->quote( $Automation->ID ).'
			  AND aust_user_ID = '.$DB->quote( $user_ID ),
			'Update data for next Step after executing Step #'.$this->ID );

		// Log:
		$process_log .= ( $next_AutomationStep
				? $log_point.'Next step: #'.$next_AutomationStep->get( 'order' )
					.'('.step_get_type_title( $next_AutomationStep->get( 'type' ) ).( $next_AutomationStep->get( 'label' ) == '' ? '' : ' "'.$next_AutomationStep->get( 'label' ).'"' ).')'
					.' delay: '.seconds_to_period( $next_delay ).', '.$next_exec_ts
				: $log_point.'There is no next step configured.' );

		if( $next_delay == 0 && $next_AutomationStep )
		{	// If delay for next step is 0 seconds then we must execute such step right now:
			if( in_array( $next_AutomationStep->ID, $executed_automation_steps[ $user_ID ] ) )
			{	// Don't run this next step because it was already executed for the user:
				$process_log .= $log_point.$log_bold_start.'Next step rescheduled with a 4 hour delay to avoid infinite loop!'.$log_bold_end.$log_nl;
			}
			else
			{	// Run next step because it is not executed yet for the user:
				$executed_automation_steps[ $user_ID ][] = $this->ID;
				$process_log .= $log_nl.$log_point.$log_bold_start.'Run next step immediately:'.$log_nl.$log_bold_end;
				$process_log .= $next_AutomationStep->execute_action( $user_ID );
			}
		}

		return $process_log;
	}


	/**
	 * Get name of automation step, it is used for `<select>` with $AutomationStepCache
	 *
	 * @return string
	 */
	function get_name()
	{
		$step_label = utf8_substr( utf8_trim( $this->get( 'label' ) ), 0, 100 );
		return '#'.$this->get( 'order' ).' - '
			.( step_get_type_title( $this->get( 'type' ) ).': '.$step_label );
	}


	/**
	 * Get result title depending on step type
	 *
	 * @param string Result: YES, NO, ERROR
	 * @param string Additional message, for example: some error message
	 * @return string Result title
	 */
	function get_result_title( $result, $additional_message = '' )
	{
		$result_titles = step_get_result_titles();

		$result_title = isset( $result_titles[ $this->get( 'type' ) ][ $result ] ) ? $result_titles[ $this->get( 'type' ) ][ $result ] : $result;

		if( strpos( $result_title, '%s' ) !== false )
		{	// Replace mask with additional message like error:
			$result_title = sprintf( $result_title, '"'.$additional_message.'"' );
		}

		return $result_title;
	}


	/**
	 * Check result of "IF Condition"
	 *
	 * @param object|NULL User, NULL to get only log as scheme of current condition without checking
	 * @param string Log process into this param
	 * @return boolean TRUE if condition is matched for given user, otherwise FALSE
	 */
	function check_if_condition( $step_User, & $process_log )
	{
		if( $this->get( 'type' ) != 'if_condition' )
		{	// This is allowed only for step type "IF Condition":
			return false;
		}

		$json_object = json_decode( $this->get( 'info' ) );

		if( $json_object === NULL || ! isset( $json_object->valid ) || $json_object->valid !== true )
		{	// Wrong object, Return false:
			return false;
		}

		return $this->check_if_condition_object( $json_object, $step_User, $process_log );
	}


	/**
	 * Check result of "IF Condition" object(one group of rules)
	 * Used recursively to find all sub grouped conditions
	 *
	 * @param object JSON object of step type "IF Condition"
	 * @param object|NULL User, NULL to get only log as scheme of current condition without checking
	 * @param string Log process into this param
	 * @return boolean TRUE if condition is matched for given user, otherwise FALSE
	 */
	function check_if_condition_object( $json_object, $step_User, & $process_log )
	{
		if( ! isset( $json_object->condition ) || ! in_array( $json_object->condition, array( 'AND', 'OR' ) ) || empty( $json_object->rules ) )
		{	// Wrong json object params, Skip it:
			return false;
		}

		// If user is not given we cannot do a checking,
		// Used to autogenerate step label:
		$check_result = ( $step_User !== NULL );

		// Log:
		$process_log .= ' ('.( $check_result ? $json_object->condition : ' ' );
		// Array to convert operator names to log format:
		$log_operators = array(
				'equal'            => '=',
				'not_equal'        => '&#8800;',
				'less'             => '<',
				'less_or_equal'    => '&#8804;',
				'greater'          => '>',
				'greater_or_equal' => '&#8805;',
				'between'          => array( '&#8805;', 'AND &#8804;' ),
				'not_between'      => array( '<', 'OR >' ),
			);
		$log_fields = array(
				'user_tag'    => 'User tag',
				'user_status' => 'User Account status',
				'date'        => 'Current date',
				'time'        => 'Current time',
				'day'         => 'Current day of the week',
				'month'       => 'Current month',
				'days_before_birthday'          => 'Days before birthday',
				'listsend_last_sent_to_user'    => 'Last sent list to user',
				'listsend_last_opened_by_user'  => 'Last opened list by user',
				'listsend_last_clicked_by_user' => 'Last clicked list by user',
			);
		$log_values = array(
				'day' => array(
					1 => 'Monday',
					2 => 'Tuesday',
					3 => 'Wednesday',
					4 => 'Thursday',
					5 => 'Friday',
					6 => 'Saturday',
					7 => 'Sunday'
				),
				'month' => array(
					1 => 'January',
					2 => 'February',
					3 => 'March',
					4 => 'April',
					5 => 'May',
					6 => 'June',
					7 => 'July',
					8 => 'August',
					9 => 'September',
					10 => 'October',
					11 => 'November',
					12 => 'December'
				),
				'user_status' => get_user_statuses(),
			);
		$log_bold_start = '<b>';
		$log_bold_end = '</b>';
		$log_rule_separator = ', ';

		if( $json_object->condition == 'AND' )
		{	// Default result for group with operator 'AND':
			$conditions_result = true;
			$stop_result = false;
		}
		else
		{	// Default result for group with operator 'OR':
			$conditions_result = false;
			$stop_result = true;
		}

		foreach( $json_object->rules as $r => $rule )
		{
			if( ! $check_result )
			{
				$log_rule_separator = $r > 0 ? ' '.$json_object->condition.' ' : '';
			}

			if( $check_result && $conditions_result == $stop_result )
			{	// Skip this rule because previous rules already returned the end result for current condition(AND|OR):
				$process_log .= $log_rule_separator.$log_bold_start.'ignored'.$log_bold_end;
				continue;
			}

			if( isset( $rule->rules ) && is_array( $rule->rules ) )
			{	// This is a group of conditions, Run this function recursively:
				$process_log .= $log_rule_separator;
				$rule_result = $this->check_if_condition_object( $rule, $step_User, $process_log );
			}
			else
			{	// This is a single field:
				if( $check_result )
				{
					$rule_result = $this->check_if_condition_rule( $rule, $step_User, $process_log );
				}
				// Log:
				$process_log .= $log_rule_separator.( isset( $log_fields[ $rule->field ] ) ? $log_fields[ $rule->field ] : $rule->field );
				if( in_array( $rule->field, array( 'listsend_last_sent_to_user', 'listsend_last_opened_by_user', 'listsend_last_clicked_by_user' ) ) )
				{	// Special value for list send fields:
					$value = explode( ':', $rule->value );
					$period = ( isset( $value[0] ) ? intval( $value[0] ) : '0' )
						.( isset( $value[1] ) ? ' '.$value[1].'s' : '' ).' ago';
					$rule_newsletter_ID = isset( $value[2] ) ? intval( $value[2] ) : 0;
					$newsletter = ' for ';
					if( $rule_newsletter_ID > 0 )
					{	// Specific newsletter is selected:
						$NewsletterCache = & get_NewsletterCache();
						if( $rule_Newsletter = & $NewsletterCache->get_by_ID( $rule_newsletter_ID, false, false ) )
						{	// Display a name of the selected newsletter:
							$newsletter .= 'List: '.$rule_Newsletter->get( 'name' ).'';
						}
						else
						{	// If newsletter was deleted from DB:
							$newsletter .= 'List: Error: NOT FOUND IN DB!';
						}
					}
					elseif( $rule_newsletter_ID == -1 )
					{	// Any newsletter should be used for this condition rule:
						$newsletter .= 'any list';
					}
					else
					{	// Any tied newsletter should be used for this condition rule:
						$newsletter .= 'any list tied to step automation';
					}
					$process_log .= ' '.$log_operators[ $rule->operator ].' "'.$period.$newsletter.'"';
				}
				elseif( is_array( $log_operators[ $rule->operator ] ) )
				{	// Multiple operator and values:
					foreach( $log_operators[ $rule->operator ] as $o => $operator )
					{
						$process_log .= ' '.$operator.' "'.( isset( $log_values[ $rule->field ][ $rule->value[ $o ] ] ) ? $log_values[ $rule->field ][ $rule->value[ $o ] ] : $rule->value[ $o ] ).'"';
					}
				}
				else
				{	// Single operator and value:
					$process_log .= ' '.$log_operators[ $rule->operator ].' "'.( isset( $log_values[ $rule->field ][ $rule->value ] ) ? $log_values[ $rule->field ][ $rule->value ] : $rule->value ).'"';
				}
				if( $check_result )
				{
					$process_log .= ': '.$log_bold_start.( $rule_result ? 'TRUE' : 'FALSE' ).$log_bold_end;
				}
			}

			// Append current result with previous results:
			if( $json_object->condition == 'AND' )
			{	// AND condition:
				$conditions_result = $check_result ? ( $conditions_result && $rule_result ) : true;
			}
			else
			{	// OR condition:
				$conditions_result = $check_result ? ( $conditions_result || $rule_result ) : false;
			}
		}

		// Log:
		$process_log .= ( $check_result ? '' : ' ' ).')';
		if( $check_result )
		{
			$process_log .= ' : '.$log_bold_start.( $conditions_result ? 'TRUE' : 'FALSE' ).$log_bold_end;
		}

		return $conditions_result;
	}


	/**
	 * Check rule of "IF Condition" for given User
	 *
	 * @param object Rule, object with properties: field, value, operator
	 * @param object User
	 * @return boolean TRUE if condition is matched for given user, otherwise FALSE
	 */
	function check_if_condition_rule( $rule, $step_User )
	{
		switch( $rule->field )
		{
			case 'user_tag':
				// Check if User has a tag:
				$user_tags = $step_User->get_usertags();
				switch( $rule->operator )
				{
					case 'equal':
						return in_array( $rule->value, $user_tags );
					case 'not_equal':
						return ! in_array( $rule->value, $user_tags );
				}
				break;

			case 'user_status':
				// Check User status:
				switch( $rule->operator )
				{
					case 'equal':
						return $step_User->get( 'status' ) == $rule->value;
					case 'not_equal':
						return $step_User->get( 'status' ) != $rule->value;
				}
				break;

			case 'date':
				// Check current date:
				return $this->check_if_condition_rule_date_value( $rule, 'Y-m-d' );

			case 'time':
				// Check current time:
				return $this->check_if_condition_rule_date_value( $rule, 'H:i' );

			case 'day':
				// Check current day of week:
				return $this->check_if_condition_rule_date_value( $rule, 'w' );

			case 'month':
				// Check current month:
				return $this->check_if_condition_rule_date_value( $rule, 'm' );

			case 'days_before_birthday':
				// Check number of days before birthday:
				global $localtimenow;

				$localdatenow   = strtotime( date( 'Y-m-d', $localtimenow ) );
				$birthday_month = $step_User->get( 'birthday_month' );
				$birthday_day   = $step_User->get( 'birthday_day' );

				if( $birthday_month && $birthday_day )
				{
					$birthday = strtotime( date( 'Y', $localtimenow ).'-'.$birthday_month.'-'.$birthday_day );
					if( $birthday < $localdatenow )
					{	// Birthday for current year has already passed, use birthday next year:
						$birthday = strtotime( ( (int) date( 'Y', $localtimenow ) + 1 ).'-'.$birthday_month.'-'.$birthday_day );
					}
					$datediff = $birthday - $localdatenow;
					$days     = (int) round( $datediff / ( 60 * 60 * 24 ) );

					switch( $rule->operator )
					{
						case 'equal':
							return $days == $rule->value;
						case 'not_equal':
							return $days != $rule->value;
						case 'less':
							return $days < $rule->value;
						case 'less_or_equal':
							return $days <= $rule->value;
						case 'greater':
							return $days > $rule->value;
						case 'greater_or_equal':
							return $days >= $rule->value;
						case 'between':
							return $days >= $rule->value[0] && $days <= $rule->value[1];
						case 'not_between':
							return $days < $rule->value[0] || $days > $rule->value[1];
					}
				}
				
				return false;

			case 'listsend_last_sent_to_user':
				// Check last sent list to user:
				return $this->check_if_condition_rule_listsend_value( $rule, $step_User->ID, 'enls_last_sent_manual_ts' ) ||
							 $this->check_if_condition_rule_listsend_value( $rule, $step_User->ID, 'enls_last_sent_auto_ts' );

			case 'listsend_last_opened_by_user':
				// Check last opened list by user:
				return $this->check_if_condition_rule_listsend_value( $rule, $step_User->ID, 'enls_last_open_ts' );

			case 'listsend_last_clicked_by_user':
				// Check last clicked list by user:
				return $this->check_if_condition_rule_listsend_value( $rule, $step_User->ID, 'enls_last_click_ts' );
		}

		// Unknown field or operator:
		return false;
	}


	/**
	 * Check rule of "IF Condition" for date value
	 *
	 * @param object Rule, object with properties: field, value, operator
	 * @param string Date format like Y-m-d, H:i, w, m
	 * @return boolean TRUE if condition is matched for current date, otherwise FALSE
	 */
	function check_if_condition_rule_date_value( $rule, $date_format )
	{
		global $localtimenow;

		$date_value = date( $date_format, $localtimenow );
		if( $date_format == 'w' && $date_value === '0' )
		{	// Use 7 for Sunday:
			$date_value = '7';
		}

		switch( $rule->operator )
		{
			case 'equal':
				return $date_value == $rule->value;
			case 'not_equal':
				return $date_value != $rule->value;
			case 'less':
				return $date_value < $rule->value;
			case 'less_or_equal':
				return $date_value <= $rule->value;
			case 'greater':
				return $date_value > $rule->value;
			case 'greater_or_equal':
				return $date_value >= $rule->value;
			case 'between':
				return $date_value >= $rule->value[0] && $date_value <= $rule->value[1];
			case 'not_between':
				return $date_value < $rule->value[0] || $date_value > $rule->value[1];
		}

		return false;
	}


	/**
	 * Check rule of "IF Condition" for list send value
	 *
	 * @param object Rule, object with properties: field, value, operator
	 * @param integer Step User ID
	 * @param string DB field name for checking: 'enls_last_sent_manual_ts', 'enls_last_open_ts', 'enls_last_click_ts'
	 * @return boolean TRUE if condition is matched for current date, otherwise FALSE
	 */
	function check_if_condition_rule_listsend_value( $rule, $step_user_ID, $check_db_field_name )
	{
		$value = explode( ':', $rule->value );
		$period_value = ( isset( $value[0] ) ? intval( $value[0] ) : 0 );

		if( $period_value > 0 )
		{	// Check this condition only if period > 0 seconds:
			global $DB, $servertimenow;

			// Calculate a time ago depending on period:
			$periods = array(
				'second' => 1,        // 1 second
				'minute' => 60,       // 60 seconds
				'hour'   => 3600,     // 60 minutes
				'day'    => 86400,    // 24 hours
				'month'  => 2592000,  // 30 days
				'year'   => 31536000, // 365 days
			);
			$period_name = ( isset( $value[1] ) ? $value[1] : false );
			if( $period_name && isset( $periods[ $period_name ] ) )
			{
				$period_value *= $periods[ $period_name ];
			}
			$rule_value_time = $servertimenow - $period_value;

			$rule_newsletter_ID = ( isset( $value[2] ) ? intval( $value[2] ) : 0 );
			if( $rule_newsletter_ID > 0 )
			{	// Check for a selected list:
				$NewsletterCache = & get_NewsletterCache();
				if( $rule_Newsletter = & $NewsletterCache->get_by_ID( $rule_newsletter_ID, false, false ) )
				{	// Check only for a selected list:
					$rule_newsletters = $rule_Newsletter->ID;
				}
				else
				{	// If a selected list has been removed from DB:
					$rule_newsletters = -1;
				}
			}
			elseif( $rule_newsletter_ID == -1 )
			{	// Check for ALL lists:
				$rule_newsletters = false;
			}
			else
			{	// Check any list tied to step automation:
				$step_Automation = & $this->get_Automation();
				$rule_newsletters = $step_Automation->get_newsletter_IDs();
			}

			$SQL = new SQL( 'Get last time for IF Condition "Last sent/opened/clicked list" ('.$check_db_field_name.')' );
			$SQL->SELECT( $check_db_field_name );
			$SQL->FROM( 'T_email__newsletter_subscription' );
			$SQL->WHERE( 'enls_user_ID = '.$DB->quote( $step_user_ID ) );
			if( $rule_newsletters !== false )
			{	// Check only for the selected rule lists:
				$SQL->WHERE_and( 'enls_enlt_ID IN ( '.$DB->quote( $rule_newsletters ).' )' );
			}
			$SQL->ORDER_BY( $check_db_field_name );
			$SQL->LIMIT( 1 );
			$last_time = strtotime( $DB->get_var( $SQL ) );

			switch( $rule->operator )
			{
				case 'less':
					return $rule_value_time < $last_time;
				case 'less_or_equal':
					return $rule_value_time <= $last_time;
				case 'greater':
					return $rule_value_time > $last_time;
				case 'greater_or_equal':
					return $rule_value_time >= $last_time;
			}
		}
		else
		{	// No reason to check if period is 0 seconds:
			return true;
		}
	}


	/**
	 * Set label generated automatically
	 *
	 * @param string Label
	 */
	function set_label()
	{
		$label = '';

		switch( $this->get( 'type' ) )
		{
			case 'if_condition':
				// Get log of conditions without results:
				$this->check_if_condition( NULL, $label );
				break;

			case 'send_campaign':
				$EmailCampaignCache = & get_EmailCampaignCache();
				if( $EmailCampaign = & $EmailCampaignCache->get_by_ID( $this->get( 'info' ), false, false ) )
				{	// Use name of Email Campaign:
					$label = $EmailCampaign->get( 'name' );
				}
				break;

			case 'notify_owner':
				if( ( $step_Automation = & $this->get_Automation() ) &&
				    ( $automation_owner_User = & $step_Automation->get_owner_User() ) )
				{	// User login of owner:
					$label = $automation_owner_User->get( 'login' );
				}
				break;

			case 'subscribe':
			case 'unsubscribe':
				$NewsletterCache = & get_NewsletterCache();
				if( $Newsletter = & $NewsletterCache->get_by_ID( $this->get( 'info' ), false, false ) )
				{	// Use name of Newsletter/List:
					$label = $Newsletter->get( 'name' );
				}
				break;

			case 'start_automation':
				$AutomationCache = & get_AutomationCache();
				if( $Automation = & $AutomationCache->get_by_ID( $this->get( 'info' ), false, false ) )
				{	// Use name of Automation:
					$label = $Automation->get( 'name' );
				}
				break;

			case 'user_status':
				$user_statuses = get_user_statuses();
				if( isset( $user_statuses[ $this->get( 'info' ) ] ) )
				{	// Get status title from status key:
					$label = $user_statuses[ $this->get( 'info' ) ];
				}
				break;

			case 'add_usertag':
			case 'remove_usertag':
			default:
				$label = $this->get( 'info' );
				break;
		}

		$this->set( 'label', utf8_substr( utf8_trim( $label ), 0, 500 ) );
	}


	/**
	 * Check if this automation step can be modified(added/edited/deleted) currently
	 *
	 * @param boolean
	 */
	function can_be_modified()
	{
		if( ( $step_Automation = & $this->get_Automation() ) &&
		    $step_Automation->get( 'status' ) == 'paused' )
		{	// Automation of this step must be paused in order to edit steps:
			return true;
		}

		return false;
	}


	/**
	 * Pause automation by confirmation from request
	 *
	 * @return boolean
	 */
	function pause_automation()
	{
		if( $this->can_be_modified() )
		{	// If step automation is already paused
			return true;
		}

		if( ! param( 'confirm_pause', 'integer' ) )
		{	// If action is not confirmed
			return false;
		}

		// Try to pause the step's automation:
		$step_Automation = & $this->get_Automation();
		$step_Automation->set( 'status', 'paused' );

		if( $step_Automation->dbupdate() )
		{	// Display a message if automation has been paused:
			global $Messages;
			$Messages->add( TB_('Automation has been paused.'), 'success' );
			return true;
		}
		else
		{	// If automation could not paused
			return false;
		}
	}
}

?>
