Your IP : 3.136.20.34


Current Path : /home/bitrix/ext_www/crm.klimatlend.ua/bitrix/modules/tasks/lib/
Upload File :
Current File : /home/bitrix/ext_www/crm.klimatlend.ua/bitrix/modules/tasks/lib/dispatcher.php

<?
/**
 * Bitrix Framework
 * @package bitrix
 * @subpackage sale
 * @copyright 2001-2015 Bitrix
 *
 * @access private
 *
 * This class DOES NOT check any CSRF tokens and even for current user`s authorization, so BE CAREFUL using it.
 */

namespace Bitrix\Tasks;

use Bitrix\Main\IO\Directory;
use Bitrix\Rest\RestException;
use Bitrix\Tasks\Dispatcher\ExecutionResult;
use Bitrix\Tasks\Dispatcher\Operation;
use Bitrix\Tasks\Dispatcher\PublicAction;
use Bitrix\Tasks\Dispatcher\ToDo;
use Bitrix\Tasks\Util\Error;
use Bitrix\Tasks\Util\Error\Collection;
use Bitrix\Tasks\Util\Error\Filter;
use Bitrix\Tasks\Util\Result;

final class Dispatcher
{
	protected static $currentRestMehtod = null;
	protected static $requestData = [];

	protected $rootNamespace = 		false;
	/**
	 * @var Collection|null
	 * @deprecated
	 */
	protected $errors = 			null;
	protected $runtimeActions =     array();

	protected static $enabled =     true;

	const ERROR_TYPE_PARSE = 		'PARSE';
	/** @deprecated */
	const ERROR_TYPE_CALL = 		'CALL';

	const NAMESPACE_TO_CALLABLE = 	'\\Dispatcher\\PublicAction';
	const DIRECTORY_TO_CALLABLE = 	'/dispatcher/publicaction';

	public function __construct()
	{
		$this->rootNamespace = __NAMESPACE__.static::NAMESPACE_TO_CALLABLE;
		$this->errors = new Collection();
	}

	public static function globalDisable()
	{
		static::$enabled = false;
	}

	public static function globalEnable()
	{
		static::$enabled = false;
	}

	public static function isGloballyEnabled()
	{
		return !!static::$enabled;
	}

	public function addRuntimeActions($actions)
	{
		if(is_array($actions))
		{
			foreach($actions as $name => $callable)
			{
				if(is_callable($callable))
				{
					$this->runtimeActions[ToLower($name)] = $callable;
				}
			}
		}
	}

	/**
	 * Check if rest method exists. Return method description if exists.
	 * @param string $rest Method name.
	 * @return array|boolean
	 */
	public static function restRegister($rest)
	{
		$rest = mb_strtolower(trim((string)$rest));

		if (
			!isset($rest) || $rest == '' ||
			!preg_match('#^([a-z_]+[a-z0-9_]+)(\.[a-z_]+[a-z0-9_]+)+$#', $rest)
		)
		{
			return false;
		}

		$path = explode('.', $rest);

		$method = array_pop($path);
		$namespace = array_map('ucfirst', $path);
		$classPrefix = '\\Bitrix\\Tasks\\Dispatcher\\Publicaction\\';
		$class = $classPrefix . implode('\\', $namespace);

		if (method_exists($class, $method))
		{
			static::$currentRestMehtod = $rest;
			return array(
				'scope' => 'task',
				'callback' => array(
					'\\'.__CLASS__, 'restGateway'
				)
			);
		}
		else
		{
			return false;
		}
	}

	/**
	 * Gateway between REST and Dispatcher.
	 *
	 * @param $queryArguments
	 * @return mixed
	 * @throws RestException
	 * @throws \TasksException
	 */
	public static function restGateway($queryArguments)
	{
		if (static::$currentRestMehtod === null)
		{
			throw new RestException('Method not found!', 'ERROR_METHOD_NOT_FOUND');
		}

		// run Dispatcher
		$dispatcher = new self();
		$plan = new Dispatcher\ToDo\Plan();
		$plan->import([
			[
				'OPERATION' => static::$currentRestMehtod,
				'ARGUMENTS' => $queryArguments,
			],
		]);
		$result = $dispatcher->run($plan);

		// work with result
		if ($result->isSuccess())
		{
			$return = array_values($plan->exportResult());
			$errors = $result->getErrors();

			if ($errors && !$errors->isEmpty())
			{
				$error = $errors->first();
				throw new RestException($error->getMessage(), $error->getCode());
			}

			if (isset($return[0]['RESULT']))
			{
				return $return[0]['RESULT'];
			}

			throw new RestException('Unknown error', 'UNKNOWN_ERROR');
		}

		$errors = $result->getErrors();
		$error = $errors->first();

		throw new RestException($error->getMessage(), 'DISPATCHER_ERROR');
	}

	/**
	 * @param ToDo\Plan $plan
	 * @return Result
	 * @throws \TasksException
	 */
	public function run($plan)
	{
		$result = new ExecutionResult();

		$this->checkPlan($plan, $result);
		if($result->isSuccess())
		{
			// bind plan operations to the actual routines
			// it could be standard actions, runtime actions or whatever else
			$operations = $this->getOperationsByPlan($plan, $result);

			// todo: this part definitely needs for some refactoring, it is too complicated
			// todo: and especially when we will introduce operation dependency (but which entity this logic should belong to?)

			if($result->isSuccess())
			{
				// execute
				foreach($operations as $code => $op)
				{
					$callResult = $this->wrapOpCall($op);

					$opResult = new Result();
					$opResult->setData($callResult);
					$opResult->getErrors()->load($op->getErrors());

					// store execution result in the plan
					/** @var ToDo $todo */
					$todo = $plan->findOne(array('=CODE' => $code));
					if($todo)
					{
						$todo->setResult($opResult);
					}

					// duplicate errors to the dispatcher, but as warnings
					$errors = $op->getErrors();
					if(!$errors->isEmpty())
					{
						$result->getErrors()->load($op->getErrors()->transform(array('TYPE' => Error::TYPE_WARNING)));
					}
				}
			}
		}

		return $result;
	}

	/**
	 * @param ToDo\Plan $plan
	 * @param Result $result
	 * @return Operation[]
	 */
	protected function getOperationsByPlan($plan, $result)
	{
		$bindings = array();

		/** @var ToDo $op */
		foreach($plan as $op)
		{
			if($op->isProcessed())
			{
				continue;
			}

			$action = $op->getAction();
			$code = $op->getCode();

			$opClassPrefix = '\\Bitrix\\Tasks\\Dispatcher\\';
			$opClass = $opClassPrefix.'Operation';
			$opArgs = array('NAMESPACE' => $this->rootNamespace);
			if(mb_substr($action, 0, 8) == 'runtime:')
			{
				$action = mb_substr($action, 8);
				if(!array_key_exists($action, $this->runtimeActions))
				{
					$result->getErrors()->add('ILLEGAL_RUNTIME_ACTION', 'Runtime action not found: '.$action, static::ERROR_TYPE_PARSE);
				}

				$opClass = $opClassPrefix.'RunTimeOperation';
				$opArgs['CALLABLE'] = $this->runtimeActions[$action];
			}

			if($result->isSuccess())
			{
				/** @var Operation $boundOp */
				$boundOp = new $opClass($op->export(), $opArgs);
				$boundOp->parse();

				$bindings[$code] = $boundOp;

				$result->getErrors()->load($boundOp->getErrors());
			}
		}

		return $bindings;
	}

	/**
	 * @param ToDo\Plan $plan
	 * @param Result $result
	 */
	protected function checkPlan($plan, $result)
	{
		$codesUsed = array();
		/** @var ToDo $op */
		foreach($plan as $op)
		{
			$code = $op->getCode();

			if(isset($codesUsed[$code]))
			{
				$result->getErrors()->add('CODE_USED_MULTIPLE_TIMES', 'The following code is used more than once: '.$code, static::ERROR_TYPE_PARSE);
			}
			else
			{
				$codesUsed[$code] = true;
			}
		}
	}

	/**
	 * @param Operation $operation
	 * @return array
	 * @throws \TasksException
	 */
	private function wrapOpCall($operation)
	{
		$callResult = array();
		try
		{
			$callResult = $operation->call();
		}
		catch(\TasksException $e) // old-style tasks exception
		{
			$errorCode = static::getErrorCodeByTasksException($e);

			if($errorCode !== false)
			{
				$reasonsAdded = false;
				if($e->checkOfType(\TasksException::TE_FLAG_SERIALIZED_ERRORS_IN_MESSAGE) && $e->getMessage() !== false)
				{
					$errors = \Bitrix\Tasks\Util\Type::unSerializeArray($e->getMessage());
					foreach ($errors as $error)
					{
						$operation->getErrors()->add((string) $error["id"] == '' ? 'ACTION_FAILED_REASON' : $error["id"], htmlspecialcharsBack($error["text"]));
						$reasonsAdded = true;
					}
				}

				if(!$reasonsAdded)
				{
					$operation->getErrors()->add($errorCode, static::proxyExceptionMessage($e));
				}
			}
			else
			{
				throw $e; // let it log
			}
		}
		catch(\Bitrix\Tasks\AccessDeniedException $e)
		{
			// access to the entity is not allowed
			$operation->getErrors()->add('ACCESS_DENIED', static::proxyExceptionMessage($e));
		}
		catch(\Bitrix\Tasks\ActionNotAllowedException $e)
		{
			// access to the entity is generally allowed, but the certain action is forbidden to execute
			$operation->getErrors()->add('ACTION_NOT_ALLOWED', static::proxyExceptionMessage($e));
			static::addReasons($operation, $e->getErrors(), 'ACTION_NOT_ALLOWED');
		}
		catch(\Bitrix\Tasks\ActionFailedException $e)
		{
			// action was allowed, but due to some reasons execution failed
			$operation->getErrors()->add('ACTION_FAILED', static::proxyExceptionMessage($e));
			$errors = $e->getErrors();

			if(is_array($errors) && !empty($errors))
			{
				foreach($errors as $error)
				{
					$operation->getErrors()->add('ACTION_FAILED_REASON', $error);
				}
			}
		}
		catch(\Bitrix\Tasks\Exception $e)
		{
			// some general tasks error, no idea what to do
			$operation->getErrors()->add('ACTION_FAILED', static::proxyExceptionMessage($e));
			$errors = $e->getErrors();

			if(is_array($errors) && !empty($errors))
			{
				foreach($errors as $error)
				{
					$operation->getErrors()->add('ACTION_FAILED_REASON', $error);
				}
			}
		}

		return $callResult;
	}

	/**
	 * This method is deprecated, it accepts only array, returns only array and throws some exceptions, and this is not good
	 *
	 * @param $batch
	 * @return array
	 * @throws Dispatcher\BadQueryException
	 * @throws Dispatcher\Exception
	 * @throws \TasksException
	 *
	 * @deprecated
	 */
	public function execute(array $batch)
	{
		$batch = $this->parseBatchDeprecated($batch);

		$result = array();
		if($this->errors->checkHasErrorOfType(static::ERROR_TYPE_PARSE))
		{
			throw new Dispatcher\BadQueryException(false);
		}

		// executing operations
		/** @var Operation $operation */
		foreach($batch as $operation)
		{
			// todo: break chain execution or continue when exception occured?
			// todo: replace call() with execute() which will return Operation Result object, move all Task/Exception catches inside operation->execute()

			$callResult = $this->wrapOpCall($operation);

			$op = $operation->getOperation();

			// todo: an object Result with ArrayAccess, getOperation(), getArguments(), etc would be more appropriate here
			$result[$op['PARAMETERS']['CODE']] = array(
				'OPERATION' => 	$op['OPERATION'],
				'ARGUMENTS' => 	$op['ARGUMENTS'],
				'RESULT' => 	$callResult,
				'SUCCESS' => 	$operation->getErrors()->checkNoFatals(),
				'ERRORS' => 	$operation->getErrors()->getAll(true, new Filter())
			);
		}

		return $result;
	}

	/**
	 * @param array $batch
	 * @return array
	 *
	 * @deprecated
	 */
	private function parseBatchDeprecated(array $batch)
	{
		// parse code and sort first
		$i = 0;
		$codesUsed = array();
		foreach($batch as &$operation)
		{
			if(is_array($operation['PARAMETERS']))
			{
				$operation['PARAMETERS'] = array_change_key_case($operation['PARAMETERS'], CASE_UPPER);
			}
			else
			{
				$operation['PARAMETERS'] = array();
			}

			if((string) $operation['PARAMETERS']['CODE'] === '')
			{
				$operation['PARAMETERS']['CODE'] = 'op_'.$i;
			}

			if(isset($codesUsed[$operation['PARAMETERS']['CODE']]))
			{
				$this->errors->add('CODE_USED_MULTIPLE_TIMES', 'The following code is used more than once: '.$operation['PARAMETERS']['CODE'], static::ERROR_TYPE_PARSE);
			}
			else
			{
				$codesUsed[$operation['PARAMETERS']['CODE']] = true;
			}

			$i++;
		}
		unset($operation);

		$batchParsed = array();
		foreach($batch as $operation)
		{
			$op = new Dispatcher\Operation($operation, array('NAMESPACE' => $this->rootNamespace));
			$op->parse();
			$batchParsed[] = $op;
			$this->errors->load($op->getErrors());
		}

		return $batchParsed;
	}

	public function getErrors()
	{
		return $this->errors;
	}

	public static function getErrorCodeByTasksException($e)
	{
		$result = false;

		if($e instanceof \TasksException)
		{
			if($e->checkOfType(\TasksException::TE_ACTION_FAILED_TO_BE_PROCESSED))
			{
				$result = 'ACTION_FAILED';
			}
			elseif($e->checkOfType(\TasksException::TE_ACTION_NOT_ALLOWED)) // DO NOT relocate this ...
			{
				$result = 'ACTION_NOT_ALLOWED';
			}
			elseif($e->checkOfType(\TasksException::TE_ACCESS_DENIED)) // ... after this
			{
				$result = 'ACCESS_DENIED';
			}
			elseif($e->checkOfType(\TasksException::TE_TASK_NOT_FOUND_OR_NOT_ACCESSIBLE))
			{
				$result = 'ACCESS_DENIED.NO_TASK';
			}
		}

		return $result;
	}

	/**
	 * There may be a policy of preventing users from seeing exception message due to security reasons
	 * @param \Bitrix\Main\SystemException $e
	 * @return string
	 */
	public static function proxyExceptionMessage($e)
	{
		if(method_exists($e, 'getMessageFriendly'))
		{
			return $e->getMessageFriendly();
		}
		else
		{
			return $e->getMessage();
		}
	}

	/**
	 * Use this to get info about methods supported.
	 * This is just a reference generator for developers. Proper work is not guaranteed. Also untested on Windows.
	 *
	 * @access private
	 */
	public function getDescription()
	{
		$list = $this->getClasses();

		$result = array();
		foreach($list as $item)
		{
			$methods = get_class_methods($item['CLASS']);
			/** @var PublicAction $class */
			$class = $item['CLASS'];
			$forbiddenMethods = array_flip(array_map('ToLower', $class::getForbiddenMethods()));

			if(is_array($methods))
			{
				foreach($methods as $method)
				{
					$method = ToLower($method);

					if(!isset($forbiddenMethods[$method]))
					{
						if(is_callable(array($item['CLASS'], $method)))
						{
							$info = static::getMethodInfo($item['CLASS'], $method);

							$query = $item['ENTITY'].'.'.$method;
							$info['QUERY'] = $query;

							$result[$query] = $info;
						}
					}
				}
			}
		}

		//ksort($result);

		return $result;
	}

	public function getDescriptionFormatted()
	{
		$formatted = '';

		$desc = $this->getDescription();

		foreach($desc as $method)
		{
			$argsFormatted = array();
			if(is_array($method['ARGUMENTS']))
			{
				foreach($method['ARGUMENTS'] as $arg)
				{
					$argsFormatted[] = $arg['TYPE'].' '.$arg['NAME'].($arg['REQUIRED'] ? '*' : '');
				}
			}

			$formatted[] = $method['QUERY'].'('.implode(', ', $argsFormatted).')'.($method['DOC'] !== '' ? ' - '.$method['DOC'] : '');
		}

		return implode(PHP_EOL, $formatted);
	}

	public static function isA($instance)
	{
		return is_a($instance, get_called_class());
	}

	protected function getMethodInfo($class, $method)
	{
		$info = new \ReflectionMethod($class, $method);

		$doc = '';
		$comment = $info->getDocComment();
		if((string) $comment !== '')
		{
			$found = array();
			preg_match('#/\*\*\s+\*([^\*]+)#', $comment, $found);

			if($found[1] !== '')
			{
				$doc = trim($found[1]);
			}
		}

		$args = array();
		$arguments = $info->getParameters();
		if(is_array($arguments))
		{
			foreach($arguments as $arg)
			{
				$argName = ToLower($arg->getName());
				$args[] = array(
					'NAME' => 		$argName,
					'TYPE' => 		$arg->isArray() ? 'array' : 'string',
					'REQUIRED' => 	!$arg->isOptional(),
				);
			}
		}

		return array(
			'DOC' => $doc,
			'ARGUMENTS' => $args
		);
	}

	protected function getClasses()
	{
		if($this->rootNamespace == false)
		{
			throw new Dispatcher\Exception('Root namespace incorrect'); // paranoid disorder
		}

		$dir = dirname(__FILE__).static::DIRECTORY_TO_CALLABLE;

		$result = array();

		if(Directory::isDirectoryExists($dir))
		{
			$index = array();
			static::walkDirectory($dir, $index, '');

			if(is_array($index['FILE']))
			{
				foreach($index['FILE'] as $fileName)
				{
					$fileName = str_replace($dir, '', $fileName);
					$fileName = explode('/', $fileName);
					$query = array();
					if(is_array($fileName))
					{
						foreach($fileName as $part)
						{
							if((string) $part !== '' || preg_match('#\.php$#', $part))
							{
								$query[] = preg_replace('#\.php$#', '', $part);
							}
						}
					}

					$result[] = array(
						'ENTITY' => implode('.', $query),
						'CLASS' => $this->rootNamespace.'\\'.implode('\\', array_map('ucfirst', $query))
					);
				}
			}
		}

		return $result;
	}

	// todo: rewirite this on \Bitrix\Main\IO functions
	protected static function walkDirectory($dir, &$index, $rootDir)
	{
		$fullDir = $rootDir.$dir;

		if(!is_readable($fullDir))
			return;

		if(is_file($fullDir))
		{
			$index['FILE'][] = $dir;
			return;
		}
		elseif(is_dir($fullDir) && (string) $dir != '')
		{
			$index['DIR'][] = $dir;
			sort($index['DIR'], SORT_STRING);
		}

		foreach(new \DirectoryIterator($fullDir) as $entry)
		{
			if($entry->isDot())
			{
				continue;
			}

			$file = $dir.'/'.$entry->getFilename();
			static::walkDirectory($file, $index, $rootDir);
		}
	}

	/**
	 * @param $args
	 * @return mixed
	 */
	private static function tryParseBatchArguments($args)
	{
		if (count($args) == 1 && isset($args['cmd']))
		{
			foreach ($args['cmd'] as $key => $url)
			{
				if (!array_key_exists($key, static::$requestData))
				{
					$parsedUrl = parse_url($url);

					if ($parsedUrl['path'] == static::$currentRestMehtod)
					{
						static::$requestData[] = $key;
						parse_str($parsedUrl['query'], $args);
						break;
					}
				}
			}
		}

		return $args;
	}

	private static function addReasons(Dispatcher\Operation $operation, array $reasons, $reasonPrefix = '')
	{
		$errors = $operation->getErrors();

		if((string) $reasonPrefix != '')
		{
			$reasonPrefix = '_'.$reasonPrefix;
		}

		foreach($reasons as $reason)
		{
			if(is_string($reason))
			{
				$errors->add($reasonPrefix.'REASON', $reason);
			}
			else
			{
				if((string) $reason['MESSAGE'] != '')
				{
					$code = ((string) $reason['CODE'] != '' ? $reason['CODE'] : $reasonPrefix.'REASON');
					$errors->add($code, $reason['MESSAGE']);
				}
			}
		}
	}

	/**
	 * @return Collection|null
	 *
	 * @deprecated Bad name
	 */
	public function getErrorCollection()
	{
		return $this->errors;
	}
}