Your IP : 18.117.167.132


Current Path : /var/www/axolotl/data/www/yar.axolotls.ru/bitrix/modules/tasks/lib/checklist/
Upload File :
Current File : /var/www/axolotl/data/www/yar.axolotls.ru/bitrix/modules/tasks/lib/checklist/checklistfacade.php

<?php
namespace Bitrix\Tasks\CheckList;

use Bitrix\Main\ArgumentException;
use Bitrix\Main\Db\SqlQueryException;
use Bitrix\Main\Entity\DataManager;
use Bitrix\Main\Entity\Query;
use Bitrix\Main\Entity\Query\Join;
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\ModuleManager;
use Bitrix\Main\NotImplementedException;
use Bitrix\Main\ObjectException;
use Bitrix\Main\ObjectPropertyException;
use Bitrix\Main\SystemException;
use Bitrix\Tasks\Access\ActionDictionary;
use Bitrix\Tasks\Access\Model\ChecklistModel;
use Bitrix\Tasks\CheckList\Internals\CheckList;
use Bitrix\Tasks\CheckList\Internals\CheckListTree;
use Bitrix\Tasks\Integration\Disk\Rest\Attachment;
use Bitrix\Tasks\Util\Result;
use Bitrix\Tasks\Util\User;
use Bitrix\Tasks\Util\UserField;
use Exception;

Loc::loadMessages(__FILE__);

/**
 * Class CheckListFacade
 *
 * @package Bitrix\Tasks\CheckList
 */
abstract class CheckListFacade
{
	const ACTION_ADD = 0x01;
	const ACTION_MODIFY = 0x02;
	const ACTION_REMOVE = 0x03;
	const ACTION_TOGGLE = 0x04;
	const ACTION_REORDER = 0x05;

	const ACTIONS = [
		'COMMON' => [
			self::ACTION_ADD => 'ACTION_ADD',
			self::ACTION_REORDER => 'ACTION_REORDER',
		],
		'ITEM' => [
			self::ACTION_MODIFY => 'ACTION_MODIFY',
			self::ACTION_REMOVE => 'ACTION_REMOVE',
			self::ACTION_TOGGLE => 'ACTION_TOGGLE',
		],
	];

	const MOVING_POSITION_BEFORE = 'before';
	const MOVING_POSITION_AFTER = 'after';

	const MEMBER_ACCOMPLICE = 'A';
	const MEMBER_AUDITOR = 'U';

	public static $entityIdName = '';
	public static $userFieldsEntityIdName = '';
	public static $commonAccessActions = [];
	public static $itemAccessActions = [];

	protected static $nodeId = 0;
	protected static $deferredActionsMode = false;
	protected static $locPrefix = 'TASKS_CHECKLIST_FACADE_';

	protected static $selectFields = [];
	protected static $filterFields = [];
	protected static $orderFields = [];
	protected static $memberFields = [
		'USER_ID',
		'USER_TYPE',
		'USER_NAME',
		'USER_LAST_NAME',
		'USER_SECOND_NAME',
		'USER_TITLE',
		'USER_LOGIN',
	];

	public static $oldItemsToMerge = [];

	/**
	 * Returns class that extends abstract class CheckListTree.
	 * @see CheckListTree
	 *
	 * @return string
	 * @throws NotImplementedException
	 */
	public static function getCheckListTree()
	{
		throw new NotImplementedException('Default checklist tree class doesnt exist');
	}

	/**
	 * Returns table class for checklist table.
	 *
	 * @return string
	 * @throws NotImplementedException
	 */
	public static function getCheckListDataController()
	{
		throw new NotImplementedException('Default checklist table class doesnt exist');
	}

	/**
	 * Returns table class for checklist tree table.
	 *
	 * @return string
	 * @throws NotImplementedException
	 */
	public static function getCheckListTreeDataController()
	{
		throw new NotImplementedException('Default checklist tree table class doesnt exist');
	}

	/**
	 * Returns table class for checklist member table.
	 *
	 * @return string
	 * @throws NotImplementedException
	 */
	public static function getCheckListMemberDataController()
	{
		throw new NotImplementedException('Default checklist member table class doesnt exist');
	}

	/**
	 * Returns current state of deferred actions mode.
	 *
	 * @return bool
	 */
	protected static function getDeferredActionsMode()
	{
		return static::$deferredActionsMode;
	}

	/**
	 * Sets deferred actions mode to true.
	 */
	protected static function enableDeferredActionsMode()
	{
		static::$deferredActionsMode = true;
	}

	/**
	 * Sets deferred actions mode to false.
	 */
	protected static function disableDeferredActionsMode()
	{
		static::$deferredActionsMode = false;
	}

	/**
	 * @param array $select
	 * @param array $filter
	 * @param array $order
	 * @return array
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws SystemException
	 */
	public static function getList(array $select = [], array $filter = [], array $order = [])
	{
		list($filteredSelect, $filteredFilter, $filteredOrder) = static::getFilteredFields($select, $filter, $order);

		/** @var DataManager $checkListDataController */
		$checkListDataController = static::getCheckListDataController();

		$query = new Query($checkListDataController::getEntity());
		$query->setSelect($filteredSelect);
		$query->setFilter($filteredFilter);
		$query->setOrder($filteredOrder);
		$query->registerRuntimeField('', new ReferenceField(
			'IT',
			static::getCheckListTreeDataController(),
			Join::on('this.ID', 'ref.CHILD_ID')->where('ref.LEVEL', 1),
			['join_type' => 'LEFT']
		));
		$query->registerRuntimeField('', new ReferenceField(
			'IM',
			static::getCheckListMemberDataController(),
			Join::on('this.ID', 'ref.ITEM_ID'),
			['join_type' => 'LEFT']
		));

		$res = $query->exec();

		$items = [];
		while ($item = $res->fetch())
		{
			$itemId = $item['ID'];

			$item = static::processItemMembers($item, $items, $select);
			$item = static::processItemAttachments($item, $select);
			$item = static::processItemCommons($item, $select);

			$items[$itemId] = $item;
		}

		return $items;
	}

	/**
	 * @param int $entityId
	 * @return array
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws SystemException
	 */
	public static function getByEntityId($entityId)
	{
		return static::getList([], [static::$entityIdName => $entityId]);
	}

	/**
	 * @param int $entityId
	 * @param int $userId
	 * @param array $fields
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws ObjectPropertyException
	 * @throws SqlQueryException
	 * @throws SystemException
	 */
	public static function add($entityId, $userId, $fields)
	{
		$addResult = new Result();

		if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_ADD, $fields))
		{
			$code = 'ACTION_NOT_ALLOWED';
			$addResult = static::addErrorToResult($addResult, $code, self::ACTION_ADD);
			return $addResult;
		}

		$fieldsChecking = static::checkFieldsForAdd($fields);
		if (!$fieldsChecking->isSuccess())
		{
			$addResult->loadErrors($fieldsChecking->getErrors());
			return $addResult;
		}

		/** @var static $facade */
		$facade = static::class;
		$fields['ENTITY_ID'] = $entityId;

		$newCheckList = new CheckList(0, $userId, $facade, $fields);
		$addResult = $newCheckList->save();

		return $addResult;
	}

	/**
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @param $fields
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws ObjectPropertyException
	 * @throws SqlQueryException
	 * @throws SystemException
	 */
	public static function update($entityId, $userId, CheckList $checkList, $fields)
	{
		$updateResult = new Result();

		$code = 'ACTION_NOT_ALLOWED';

		if (
			array_key_exists('IS_COMPLETE', $fields)
			&& count($fields) === 1
		)
		{
			if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_TOGGLE, $checkList))
			{
				$updateResult = static::addErrorToResult($updateResult, $code, self::ACTION_TOGGLE);
				return $updateResult;
			}
		}
		else if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $checkList))
		{
			$updateResult = static::addErrorToResult($updateResult, $code, self::ACTION_MODIFY);
			return $updateResult;
		}

		$fieldsChecking = static::checkFieldsForUpdate($fields);
		if (!$fieldsChecking->isSuccess())
		{
			$updateResult->loadErrors($fieldsChecking->getErrors());
			return $updateResult;
		}

		$checkList->setFields($fields);
		$updateResult = $checkList->save();

		return $updateResult;
	}

	/**
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @return Result|bool
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws SystemException
	 */
	public static function delete($entityId, $userId, $checkList)
	{
		$deleteResult = new Result();

		$items = static::getByEntityId($entityId);
		$itemsTree = static::getObjectStructuredRoots($items, $entityId, $userId);

		$id = $checkList->getFields()['ID'];
		$checkListWithSubTree = null;

		/** @var CheckList $item */
		foreach ($itemsTree as $item)
		{
			if ($checkListWithSubTree = $item->findById($id))
			{
				break;
			}
		}

		if ($checkListWithSubTree !== null)
		{
			$deleteCompositeResult = static::deleteComposite($entityId, $userId, $checkListWithSubTree);
			if (!$deleteCompositeResult->isSuccess())
			{
				$deleteResult->loadErrors($deleteCompositeResult->getErrors());
				return $deleteResult;
			}
		}

		return $deleteResult;
	}

	/**
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectPropertyException
	 * @throws SystemException
	 */
	public static function deleteComposite($entityId, $userId, $checkList)
	{
		$deleteCompositeResult = new Result();

		foreach ($checkList->getDescendants() as $descendant)
		{
			$deleteCompositeResult->loadErrors(static::deleteComposite($entityId, $userId, $descendant)->getErrors());
			if (!$deleteCompositeResult->isSuccess())
			{
				return $deleteCompositeResult;
			}
		}

		$deleteLeafResult = static::deleteLeaf($entityId, $userId, $checkList);
		if (!$deleteLeafResult->isSuccess())
		{
			$deleteCompositeResult->loadErrors($deleteLeafResult->getErrors());
		}

		return $deleteCompositeResult;
	}

	/**
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws SystemException
	 * @throws ObjectPropertyException
	 * @throws Exception
	 */
	public static function deleteLeaf($entityId, $userId, $checkList)
	{
		/** @noinspection PhpVariableNamingConventionInspection */
		global $USER_FIELD_MANAGER;

		$deleteLeafResult = new Result();

		if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $checkList))
		{
			$code = 'ACTION_NOT_ALLOWED';
			$deleteLeafResult = static::addErrorToResult($deleteLeafResult, $code, self::ACTION_REMOVE);
			return $deleteLeafResult;
		}

		$id = $checkList->getFields()['ID'];

		try
		{
			$USER_FIELD_MANAGER->Delete(static::$userFieldsEntityIdName, $id);
		}
		catch (Exception $exception)
		{
			$deleteLeafResult = static::addErrorToResult($deleteLeafResult, 'USER_FIELD_DELETE_FAILED');
			static::logError($exception->getMessage());
		}

		/** @var DataManager $memberDataController */
		$memberDataController = static::getCheckListMemberDataController();
		$members = $memberDataController::getList(['select' => ['ID'], 'filter' => ['ITEM_ID' => $id]])->fetchAll();
		foreach ($members as $member)
		{
			$memberDeleteResult = $memberDataController::delete($member['ID']);
			if (!$memberDeleteResult->isSuccess())
			{
				$deleteLeafResult = static::addErrorToResult($deleteLeafResult, 'MEMBER_DELETE_FAILED');
				static::logError($memberDeleteResult->getErrorMessages()[0]);
			}
		}

		/** @var CheckListTree $checkListTree */
		$checkListTree = static::getCheckListTree();
		$treeDeleteResult = $checkListTree::delete($id, ['DELETE_SUBTREE' => true]);
		if (!$treeDeleteResult->isSuccess())
		{
			$deleteLeafResult->loadErrors($treeDeleteResult->getErrors());
		}

		/** @var DataManager $checkListDataController */
		$checkListDataController = static::getCheckListDataController();
		$tableDeleteResult = $checkListDataController::delete($id);
		if (!$tableDeleteResult->isSuccess())
		{
			$deleteLeafResult = static::addErrorToResult($deleteLeafResult, 'CHECKLIST_DELETE_FAILED');
			static::logError($tableDeleteResult->getErrorMessages()[0]);
		}

		static::doDeletePostActions($entityId, $userId, ['CHECKLIST' => $checkList]);

		return $deleteLeafResult;
	}

	/**
	 * Deletes all checklists of entity.
	 * Works much slower than deleteByEntityIdOnLowLevel because of recursion,
	 * but checks rights and does post actions.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws SystemException
	 */
	public static function deleteByEntityId($entityId, $userId)
	{
		$checkListItems = static::getList(['ID', 'PARENT_ID'], [static::$entityIdName => $entityId]);
		$checkListItemsTree = static::getObjectStructuredRoots($checkListItems, $entityId, $userId);

		foreach ($checkListItemsTree as $item)
		{
			/** @var CheckList $item */
			static::deleteComposite($entityId, $userId, $item);
		}
	}

	/**
	 * Deletes all checklists of entity.
	 * Works much faster than deleteByEntityId.
	 * This function doesn't check rights and doesn't do post actions.
	 *
	 * @param int $entityId
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws SystemException
	 */
	public static function deleteByEntityIdOnLowLevel($entityId)
	{
		$checkLists = array_keys(static::getList(['ID', 'TITLE'], [static::$entityIdName => $entityId]));
		static::deleteByCheckListsIds($checkLists);
	}

	/**
	 * @param array $checkLists
	 * @throws NotImplementedException
	 */
	private static function deleteByCheckListsIds($checkLists)
	{
		/** @noinspection PhpVariableNamingConventionInspection */
		global $USER_FIELD_MANAGER;

		if (!$checkLists)
		{
			return;
		}

		$checkListsIds = '('.implode(',', $checkLists).')';

		$dataControllers = [
			static::getCheckListDataController(),
			static::getCheckListTreeDataController(),
			static::getCheckListMemberDataController(),
		];

		foreach ($dataControllers as $controller)
		{
			$controller::deleteByCheckListsIds($checkListsIds);
		}

		foreach ($checkLists as $id)
		{
			$USER_FIELD_MANAGER->Delete(static::$userFieldsEntityIdName, $id);
		}
	}

	/**
	 * Sets new checklists for entity by merging with old ones
	 * (runs through $newItems, adds completely new to entity, detects differences and changes inner data of
	 * changed checklists, deletes all old checklists it did not traverse through).
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param array $newItems
	 * @param array $parameters
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws SystemException
	 */
	public static function merge($entityId, $userId, $newItems, $parameters = [])
	{
		$mergeResult = new Result();

		static::enableDeferredActionsMode();

		static::$oldItemsToMerge = static::getList([], [static::$entityIdName => $entityId]);
		$oldItemsKeys = array_keys(static::$oldItemsToMerge);

		if (empty($newItems))
		{
			static::deleteByCheckListsIds($oldItemsKeys);
			static::doDeletePostActions($entityId, $userId, ['ITEMS' => static::$oldItemsToMerge]);

			static::doMergePostActions($entityId, $userId, ['PARAMETERS' => $parameters]);

			return $mergeResult;
		}

		$traversedItems = [];
		$newItemsTree = static::getObjectStructuredRoots($newItems, $entityId, $userId, 'PARENT_NODE_ID');

		foreach ($newItemsTree as $item)
		{
			/** @var CheckList $item */
			$saveResult = $item->save();
			if (!$saveResult->isSuccess())
			{
				$mergeResult->loadErrors($saveResult->getErrors());
				return $mergeResult;
			}

			/** @var CheckList $savedItem */
			$savedItem = $saveResult->getData()['ITEM'];
			$traversedItems[] = $savedItem->toItemsArray();
		}

		$traversedItems = array_merge(...$traversedItems);
		$mergeResult->setData(['TRAVERSED_ITEMS' => $traversedItems]);

		$itemsToRemoveKeys = array_diff($oldItemsKeys, array_column($traversedItems, 'ID'));
		$itemsToRemove = array_filter(
			static::$oldItemsToMerge,
			static function ($item) use ($itemsToRemoveKeys)
			{
				return in_array((int)$item['ID'], $itemsToRemoveKeys, true);
			}
		);

		static::deleteByCheckListsIds($itemsToRemoveKeys);
		static::doDeletePostActions($entityId, $userId, ['ITEMS' => $itemsToRemove]);

		static::doMergePostActions($entityId, $userId, ['ITEMS' => $traversedItems, 'PARAMETERS' => $parameters]);

		static::disableDeferredActionsMode();

		return $mergeResult;
	}

	/**
	 * Moves item before or after another item depending on position.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $itemToMove
	 * @param int $relatedItemId
	 * @param string $position
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws SystemException
	 */
	public static function moveItem(
		$entityId,
		$userId,
		$itemToMove,
		$relatedItemId,
		$position = self::MOVING_POSITION_AFTER
	)
	{
		$moveResult = new Result();

		if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $itemToMove))
		{
			$code = 'ACTION_NOT_ALLOWED';
			$moveResult = static::addErrorToResult($moveResult, $code, self::ACTION_REORDER);
			return $moveResult;
		}

		/** @var CheckListFacade $facade */
		$items = static::getByEntityId($entityId);
		$facade = static::class;
		$relatedItemFields = static::getList([], ['ID' => $relatedItemId])[$relatedItemId];
		$relatedItem = new CheckList(0, $userId, $facade, $relatedItemFields);

		$itemToMoveFields = $itemToMove->getFields();
		$relatedItemFields = $relatedItem->getFields();

		$sortIndex = $relatedItemFields['SORT_INDEX'];
		$newParentId = $relatedItemFields['PARENT_ID'];

		$previousParentId = $newParentId;

		while ($previousParentId !== 0)
		{
			if ($previousParentId === $itemToMoveFields['ID'])
			{
				$moveResult = static::addErrorToResult($moveResult, 'NO_LOOPS_AVAILABLE');
				return $moveResult;
			}

			$previousParentId = $items[$previousParentId]['PARENT_ID'];
		}

		$newFields = [
			'SORT_INDEX' => ($position === static::MOVING_POSITION_BEFORE? $sortIndex : $sortIndex + 1)
		];

		if ($itemToMoveFields['PARENT_ID'] !== $newParentId)
		{
			$newFields['PARENT_ID'] = $newParentId;
		}

		$itemToMove->setFields($newFields);
		$moveResult = $itemToMove->save();

		return $moveResult;
	}

	/**
	 * Completes checklist.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws ObjectPropertyException
	 * @throws SqlQueryException
	 * @throws SystemException
	 */
	public static function complete($entityId, $userId, $checkList)
	{
		$completeResult = static::update($entityId, $userId, $checkList, ['IS_COMPLETE' => true]);
		return $completeResult;
	}

	/**
	 * Renews checklist.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws ObjectPropertyException
	 * @throws SqlQueryException
	 * @throws SystemException
	 */
	public static function renew($entityId, $userId, $checkList)
	{
		$renewResult = static::update($entityId, $userId, $checkList, ['IS_COMPLETE' => false]);
		return $renewResult;
	}

	/**
	 * Adds members to checklist.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @param $members
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws ObjectPropertyException
	 * @throws SqlQueryException
	 * @throws SystemException
	 */
	public static function addMembers($entityId, $userId, $checkList, $members)
	{
		$addMembersResult = new Result();

		if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $checkList))
		{
			$code = 'ACTION_NOT_ALLOWED';
			$addMembersResult = static::addErrorToResult($addMembersResult, $code, self::ACTION_MODIFY);
			return $addMembersResult;
		}

		$fieldsChecking = static::checkFields(['MEMBERS' => $members]);
		if (!$fieldsChecking->isSuccess())
		{
			$addMembersResult->loadErrors($fieldsChecking->getErrors());
			return $addMembersResult;
		}

		$members = array_map(
			static function($data)
			{
				return (!is_array($data)? ['TYPE' => $data] : $data);
			},
			$members
		);

		$checkList->addMembers($members);
		$addMembersResult = $checkList->save();

		return $addMembersResult;
	}

	/**
	 * Removes members from checklist.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @param $membersIds
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws ObjectPropertyException
	 * @throws SqlQueryException
	 * @throws SystemException
	 */
	public static function removeMembers($entityId, $userId, $checkList, $membersIds)
	{
		$removeMembersResult = new Result();

		if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $checkList))
		{
			$code = 'ACTION_NOT_ALLOWED';
			$removeMembersResult = static::addErrorToResult($removeMembersResult, $code, self::ACTION_MODIFY);
			return $removeMembersResult;
		}

		$checkList->removeMembers($membersIds);
		$removeMembersResult = $checkList->save();

		return $removeMembersResult;
	}

	/**
	 * Attaches file represented by base64 content to checklist.
	 * This function uploads file on bitrix24 disk before attaching.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @param array $attachmentParameters - consists of NAME => (string)$name and CONTENT => (base64)$content
	 * @return Result
	 */
	public static function addAttachmentByContent($entityId, $userId, $checkList, $attachmentParameters)
	{
		$addAttachmentResult = new Result();

		if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $checkList))
		{
			$code = 'ACTION_NOT_ALLOWED';
			$addAttachmentResult = static::addErrorToResult($addAttachmentResult, $code, self::ACTION_MODIFY);
			return $addAttachmentResult;
		}

		try
		{
			Attachment::add($checkList->getFields()['ID'], $attachmentParameters, [
				'USER_ID' => $userId,
				'ENTITY_ID' => static::$userFieldsEntityIdName,
				'FIELD_NAME' => 'UF_CHECKLIST_FILES',
			]);
		}
		catch (Exception $exception)
		{
			$addAttachmentResult = static::addErrorToResult($addAttachmentResult, 'ATTACHMENT_ADDING_FAILED');
			return $addAttachmentResult;
		}

		$addAttachmentResult->setData(['ITEM' => $checkList]);

		return $addAttachmentResult;
	}

	/**
	 * Attaches file from bitrix24 disk to checklist.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @param array $filesIds
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws ObjectPropertyException
	 * @throws SqlQueryException
	 * @throws SystemException
	 */
	public static function addAttachmentsFromDisk($entityId, $userId, $checkList, $filesIds)
	{
		$addAttachmentsResult = new Result();

		if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $checkList))
		{
			$code = 'ACTION_NOT_ALLOWED';
			$addAttachmentsResult = static::addErrorToResult($addAttachmentsResult, $code, self::ACTION_MODIFY);
			return $addAttachmentsResult;
		}

		$checkList->addAttachments($filesIds);
		$addAttachmentsResult = $checkList->save();

		return $addAttachmentsResult;
	}

	/**
	 * Removes attachments from checklist.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 * @param array $attachmentsIds
	 * @return Result
	 * @throws ArgumentException
	 * @throws NotImplementedException
	 * @throws ObjectException
	 * @throws ObjectPropertyException
	 * @throws SqlQueryException
	 * @throws SystemException
	 */
	public static function removeAttachments($entityId, $userId, $checkList, $attachmentsIds)
	{
		$removeAttachmentsResult = new Result();

		if (!static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $checkList))
		{
			$code = 'ACTION_NOT_ALLOWED';
			$removeAttachmentsResult = static::addErrorToResult($removeAttachmentsResult, $code, self::ACTION_MODIFY);
			return $removeAttachmentsResult;
		}

		$checkList->removeAttachments($attachmentsIds);
		$removeAttachmentsResult = $checkList->save();

		return $removeAttachmentsResult;
	}

	public static function checkAccess($entityId, $userId, $action, $params = null)
	{
		$accessController = static::getAccessControllerClass();
		return $accessController::can($userId, $action, $entityId, $params);
	}

	abstract protected static function getAccessControllerClass(): string;

	/**
	 * Checks if action is allowed for user and entity.
	 *
	 * @param int $entityId
	 * @param CheckList $checkList
	 * @param int $userId
	 * @param $actionId
	 * @return bool
	 */
	public static function isActionAllowed($entityId, $checkList, $userId, $actionId)
	{
		$action = ActionDictionary::ACTION_CHECKLIST_EDIT;

		if ($actionId == self::ACTION_ADD)
		{
			$action = ActionDictionary::ACTION_CHECKLIST_ADD;
		}
		elseif ($actionId == self::ACTION_TOGGLE)
		{
			$action = ActionDictionary::ACTION_CHECKLIST_TOGGLE;
		}

		return static::checkAccess($entityId, $userId, $action, $checkList);
	}

	/**
	 * Returns array of roots with subtrees as array structures.
	 *
	 * @param array $sourceArray
	 * @param string $keyToParent
	 * @return array
	 */
	public static function getArrayStructuredRoots(array $sourceArray, $keyToParent = 'PARENT_ID')
	{
		$roots = [];
		$result = [];

		foreach ($sourceArray as $id => $item)
		{
			if (!isset($sourceArray[$id]['SUB_TREE']))
			{
				$sourceArray[$id]['SUB_TREE'] = [];
			}

			if ($item[$keyToParent] && isset($sourceArray[$item[$keyToParent]]))
			{
				$sourceArray[$item[$keyToParent]]['SUB_TREE'][$id] =& $sourceArray[$id];
			}
			else
			{
				$roots[] = $id;
			}
		}

		foreach ($roots as $root)
		{
			$result[$root] = $sourceArray[$root];
		}

		return $result;
	}

	/**
	 * Returns array of roots with subtrees as object structures.
	 *
	 * @param array $items
	 * @param int $entityId
	 * @param int $userId
	 * @param string $keyToParent
	 * @return array
	 * @throws NotImplementedException
	 */
	public static function getObjectStructuredRoots($items, $entityId, $userId, $keyToParent = 'PARENT_ID')
	{
		$result = [];

		$arrayStructuredRoots = static::getArrayStructuredRoots($items, $keyToParent);
		foreach ($arrayStructuredRoots as $root)
		{
			$checkList = static::makeCheckListItem($root, $entityId, $userId);
			$checkList->setFields(['PARENT_ID' => 0]);

			$result[] = $checkList;
		}

		return $result;
	}

	/**
	 * @param array $root
	 * @param int $entityId
	 * @param int $userId
	 * @return CheckList
	 * @throws NotImplementedException
	 */
	private static function makeCheckListItem($root, $entityId, $userId)
	{
		static::$nodeId++;

		$nodeId = (isset($root['NODE_ID'])? $root['NODE_ID'] : static::$nodeId);
		$fields = $root;
		$fields['ENTITY_ID'] = $entityId;
		/** @var CheckListFacade $facade */
		$facade = static::class;

		$tree = new CheckList($nodeId, $userId, $facade, $fields);

		foreach ($root['SUB_TREE'] as $item)
		{
			$tree->add(static::makeCheckListItem($item, $entityId, $userId));
		}

		return $tree;
	}

	/**
	 * Checks fields before adding checklist.
	 *
	 * @param array $fields
	 * @return Result
	 */
	public static function checkFieldsForAdd(array $fields)
	{
		return static::checkFields($fields, ['MODE' => 'add']);
	}

	/**
	 * Checks fields before updating checklist.
	 *
	 * @param array $fields
	 * @return Result
	 */
	public static function checkFieldsForUpdate(array $fields)
	{
		return static::checkFields($fields, ['MODE' => 'update']);
	}

	/**
	 * @param array $fields
	 * @param array $parameters
	 * @return Result
	 */
	private static function checkFields(array $fields, array $parameters = [])
	{
		$checkResult = new Result();

		if (!array_key_exists('TITLE', $fields) && $parameters['MODE'] === 'add')
		{
			$checkResult = static::addErrorToResult($checkResult, 'EMPTY_TITLE');
		}
		else if (empty($fields) && $parameters['MODE'] === 'update')
		{
			$checkResult = static::addErrorToResult($checkResult, 'EMPTY_FIELDS');
		}

		$allowedFields = ['TITLE', 'PARENT_ID', 'SORT_INDEX', 'IS_COMPLETE', 'IS_IMPORTANT', 'MEMBERS', 'ATTACHMENTS'];
		foreach (array_keys($fields) as $fieldName)
		{
			if (!in_array($fieldName, $allowedFields, true))
			{
				$checkResult = static::addErrorToResult($checkResult, 'NOT_ALLOWED_FIELD', $fieldName);
			}
		}

		if (array_key_exists('MEMBERS', $fields))
		{
			if (!$fields['MEMBERS'])
			{
				$fields['MEMBERS'] = [];
			}

			foreach ($fields['MEMBERS'] as $id => $data)
			{
				$type = $data;

				if (is_array($data))
				{
					$type = $data['TYPE'];
				}

				if (!in_array($type, [self::MEMBER_ACCOMPLICE, self::MEMBER_AUDITOR], true))
				{
					$checkResult = static::addErrorToResult($checkResult, 'WRONG_MEMBER_TYPE', $type);
				}
			}
		}

		return $checkResult;
	}

	/**
	 * @param array $select
	 * @param array $filter
	 * @param array $order
	 * @return array
	 */
	private static function getFilteredFields($select, $filter, $order)
	{
		return [
			static::getFilteredSelect($select),
			static::getFilteredFilter($filter),
			static::getFilteredOrder($order),
		];
	}

	/**
	 * @param array $select
	 * @return array
	 */
	private static function getFilteredSelect($select)
	{
		$filteredSelect = [];

		if (empty($select))
		{
			$select = static::$selectFields;
		}

		foreach (array_values($select) as $field)
		{
			if (in_array($field, static::$selectFields, true))
			{
				if ($field === 'MEMBERS')
				{
					foreach (static::$memberFields as $userField)
					{
						if ($userField === 'USER_ID')
						{
							$value = 'IM.USER_ID';
						}
						else if ($userField === 'USER_TYPE')
						{
							$value = 'IM.TYPE';
						}
						else
						{
							$value = 'IM.USER.'.str_replace('USER_', '', $userField);
						}

						$filteredSelect[$userField] = $value;
					}
					continue;
				}

				if ($field === 'PARENT_ID')
				{
					$filteredSelect[$field] = 'IT.PARENT_ID';
					continue;
				}

				if ($field === 'ATTACHMENTS')
				{
					if (ModuleManager::isModuleInstalled('disk'))
					{
						$filteredSelect[] = 'UF_CHECKLIST_FILES';
					}
					continue;
				}

				$filteredSelect[] = $field;
			}
		}

		if (!in_array('ID', $filteredSelect, true))
		{
			$filteredSelect[] = 'ID';
		}

		return $filteredSelect;
	}

	/**
	 * @param array $filter
	 * @return array
	 */
	private static function getFilteredFilter($filter)
	{
		$filteredFilter = [];

		foreach ($filter as $field => $value)
		{
			if (in_array($field, static::$filterFields, true))
			{
				if ($field === 'PARENT_ID')
				{
					$filteredFilter['IT.PARENT_ID'] = $value;
					continue;
				}

				$filteredFilter[$field] = $value;
			}
		}

		return $filteredFilter;
	}

	/**
	 * @param array $order
	 * @return array
	 */
	private static function getFilteredOrder($order)
	{
		$filteredOrder = [];
		$availableSorts = ['asc', 'desc', 'ASC', 'DESC'];

		if (empty($order))
		{
			$order = [static::$entityIdName => 'DESC', 'SORT_INDEX' => 'ASC', 'ID' => 'DESC'];
		}

		foreach ($order as $field => $sort)
		{
			if (in_array($field, static::$orderFields, true) && in_array($sort, $availableSorts, true))
			{
				if ($field === 'PARENT_ID')
				{
					$filteredOrder['IT.PARENT_ID'] = $sort;
					continue;
				}

				$filteredOrder[$field] = $sort;
			}
		}

		return $filteredOrder;
	}

	/**
	 * @param array $item
	 * @param array $items
	 * @param array $select
	 * @return array
	 */
	private static function processItemMembers($item, $items, $select)
	{
		$processedItem = $item;

		if (empty($select) || in_array('MEMBERS', $select, true))
		{
			$processedItem['MEMBERS'] = [];

			$id = $processedItem['ID'];
			$userId = $processedItem['USER_ID'];

			if (isset($userId) || $items[$id])
			{
				$userFields = [];

				foreach (static::$memberFields as $field)
				{
					$userFields[str_replace('USER_', '', $field)] = $processedItem[$field];
				}

				$member = [
					'TYPE' => $processedItem['USER_TYPE'],
					'NAME' => User::formatName($userFields),
				];

				if ($items[$id])
				{
					$items[$id]['MEMBERS'][$userId] = $member;
					return $items[$id];
				}

				$processedItem['MEMBERS'][$userId] = $member;
			}

			foreach (static::$memberFields as $field)
			{
				unset($processedItem[$field]);
			}
		}

		return $processedItem;
	}

	/**
	 * @param array $item
	 * @param array $select
	 * @return array
	 */
	private static function processItemAttachments($item, $select)
	{
		$processedItem = $item;

		if (
			(empty($select) || in_array('ATTACHMENTS', $select, true))
			&& ModuleManager::isModuleInstalled('disk')
		)
		{
			/** @noinspection PhpVariableNamingConventionInspection */
			global $USER_FIELD_MANAGER;

			$processedItem['ATTACHMENTS'] = [];

			if ($processedItem['UF_CHECKLIST_FILES'])
			{
				$userFields = $USER_FIELD_MANAGER->GetUserFields(static::$userFieldsEntityIdName, $item['ID'], LANGUAGE_ID);
				$value = $userFields['UF_CHECKLIST_FILES']['VALUE'];

				if (!UserField::isValueEmpty($value))
				{
					foreach ($value as $attachmentId)
					{
						$processedItem['ATTACHMENTS'][$attachmentId] = Attachment::getById($attachmentId);
					}
					$processedItem['UF_CHECKLIST_FILES'] = $userFields['UF_CHECKLIST_FILES'];
				}
			}
		}

		return $processedItem;
	}

	/**
	 * @param array $item
	 * @param array $select
	 * @return array
	 */
	private static function processItemCommons($item, $select)
	{
		$processedItem = $item;

		if (array_key_exists('PARENT_ID', $processedItem) && $processedItem['PARENT_ID'] === null)
		{
			$processedItem['PARENT_ID'] = 0;
		}

		if (array_key_exists(static::$entityIdName, $processedItem))
		{
			$processedItem['ENTITY_ID'] = $processedItem[static::$entityIdName];
		}

		if (!empty($select) && !in_array('ID', $select, true))
		{
			unset($processedItem['ID']);
		}

		return $processedItem;
	}

	/**
	 * @param int $entityId
	 * @param int $userId
	 * @param array $items
	 * @return mixed
	 */
	protected static function fillActionsForItems($entityId, $userId, $items)
	{
		if (empty($items))
		{
			return $items;
		}

		$items = array_map(
			static function($item) use ($entityId, $userId)
			{
				$item['ACTION'] = [
					'MODIFY' => static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $item),
					'REMOVE' => static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_EDIT, $item),
					'TOGGLE' => static::checkAccess($entityId, $userId, ActionDictionary::ACTION_CHECKLIST_TOGGLE, $item),
				];

				return $item;
			},
			$items
		);

		return $items;
	}

	/**
	 * @param Result $result
	 * @param string $code
	 * @param mixed $argument
	 * @return Result
	 */
	private static function addErrorToResult($result, $code, $argument = null)
	{
		$actions = static::ACTIONS['COMMON'] + static::ACTIONS['ITEM'];
		$replaces = [
			'NOT_ALLOWED_FIELD' => '#FIELD_NAME#',
			'WRONG_MEMBER_TYPE' => '#TYPE#',
		];

		if ($code === 'ACTION_NOT_ALLOWED' && isset($actions[$argument]))
		{
			$actionName = Loc::getMessage(static::$locPrefix.$actions[$argument]);
			$message = str_replace('#ACTION_NAME#', $actionName, Loc::getMessage(static::$locPrefix.$code));
		}
		else if (array_key_exists($code, $replaces))
		{
			$message = str_replace($replaces[$code], $argument, Loc::getMessage(static::$locPrefix.$code));
		}
		else
		{
			$message = Loc::getMessage(static::$locPrefix.$code);
		}

		$result->addError($code, $message);

		return $result;
	}

	/**
	 * Returns checklists with actions for entity if entity is accessible for reading.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @return array
	 */
	public static function getItemsForEntity($entityId, $userId)
	{
		return [];
	}

	/**
	 * Does some actions after adding checklist.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $checkList
	 */
	public static function doAddPostActions($entityId, $userId, $checkList)
	{

	}

	/**
	 * Does some actions after updating checklist.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param CheckList $oldCheckList
	 * @param CheckList $newCheckList
	 */
	public static function doUpdatePostActions($entityId, $userId, $oldCheckList, $newCheckList)
	{

	}

	/**
	 * Does some actions after deleting checklists.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param array $data
	 */
	public static function doDeletePostActions($entityId, $userId, $data = [])
	{

	}

	/**
	 * Does some actions after merging checklists.
	 *
	 * @param int $entityId
	 * @param int $userId
	 * @param array $data
	 */
	public static function doMergePostActions($entityId, $userId, $data = [])
	{

	}

	/**
	 * Returns array of fields suitable for table data adding or updating.
	 *
	 * @param array $fields
	 * @return array
	 */
	public static function getFieldsForTable($fields)
	{
		return [];
	}

	/**
	 * Logs error message.
	 *
	 * @param string $message
	 */
	public static function logError($message)
	{

	}

	/**
	 * @param int $entityId
	 * @param int $userId
	 * @return array
	 */
	protected static function fillCommonAccessActions($entityId, $userId)
	{
		return [];
	}

	/**
	 * @param int $entityId
	 * @param CheckList $checkList
	 * @param int $userId
	 * @return array
	 */
	protected static function fillItemAccessActions($entityId, $checkList, $userId)
	{
		return [];
	}
}