Your IP : 3.138.174.96


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

<?php

namespace Bitrix\Disk;

use Bitrix\Disk\Integration\TransformerManager;
use Bitrix\Disk\Internals\AttachedObjectTable;
use Bitrix\Disk\Internals\EditSessionTable;
use Bitrix\Disk\Internals\Error\Error;
use Bitrix\Disk\Internals\Error\ErrorCollection;
use Bitrix\Disk\Internals\ExternalLinkTable;
use Bitrix\Disk\Internals\FileTable;
use Bitrix\Disk\Internals\ObjectTable;
use Bitrix\Disk\Internals\RightTable;
use Bitrix\Disk\Internals\SharingTable;
use Bitrix\Disk\Internals\SimpleRightTable;
use Bitrix\Disk\Internals\VersionTable;
use Bitrix\Disk\Security\SecurityContext;
use Bitrix\Disk\Uf\FileUserType;
use Bitrix\Main;
use Bitrix\Main\Application;
use Bitrix\Main\Entity\ExpressionField;
use Bitrix\Main\Entity\Query;
use Bitrix\Main\Event;
use Bitrix\Main\Loader;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\ObjectException;
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\UI\Viewer\FilePreviewTable;
use CFile;

Loc::loadMessages(__FILE__);

class File extends BaseObject
{
	const ERROR_COULD_NOT_SAVE_FILE           = 'DISK_FILE_22002';
	const ERROR_COULD_NOT_COPY_FILE           = 'DISK_FILE_22003';
	const ERROR_COULD_NOT_RESTORE_FROM_OBJECT = 'DISK_FILE_22004';
	const ERROR_COULD_NOT_GET_SAVED_FILE      = 'DISK_FILE_22005';
	const ERROR_SIZE_RESTRICTION              = 'DISK_FILE_22006';
	const ERROR_EXCLUSIVE_LOCK                = 'DISK_FILE_22007';
	const ERROR_ALREADY_LOCKED                = 'DISK_FILE_22008';
	const ERROR_INVALID_LOCK_TOKEN            = 'DISK_FILE_22009';
	const ERROR_INVALID_USER_FOR_UNLOCK       = 'DISK_FILE_22010';

	const SECONDS_TO_JOIN_VERSION = 300;

	const STATE_DO_NOTHING     = 2;
	const STATE_DELETE_PROCESS = 3;

	const CODE_RECORDED_FILE = 'RECORDED';

	/** @var int */
	protected $typeFile;
	/** @var int */
	protected $globalContentVersion;
	/** @var int */
	protected $fileId;
	/** @var int */
	protected $prevFileId;
	/** @var array */
	protected $file;
	/** @var int */
	protected $size;
	/** @var string */
	protected $externalHash;
	/** @var string */
	protected $etag;
	/** @var string */
	protected $extension;
	/** @var int */
	protected $currentState = self::STATE_DO_NOTHING;
	/** @var  int */
	protected $previewId;
	/** @var  int */
	protected $viewId;
	/** @var int */
	protected $prevViewId;
	/** @var View\Base */
	protected $view;

	/**
	 * Gets the fully qualified name of table class which belongs to current model.
	 * @throws \Bitrix\Main\NotImplementedException
	 * @return string
	 */
	public static function getTableClassName()
	{
		return FileTable::className();
	}

	/**
	 * Checks rights to start bizprocess on current object.
	 * @param SecurityContext $securityContext Security context.
	 * @return bool
	 */
	public function canStartBizProc(SecurityContext $securityContext)
	{
		return $securityContext->canStartBizProc($this->id);
	}

	/**
	 * Adds row to entity table, fills error collection and builds model.
	 * @param array           $data Data.
	 * @param ErrorCollection $errorCollection Error collection.
	 * @return \Bitrix\Disk\Internals\Model|static|null
	 * @throws \Bitrix\Main\NotImplementedException
	 * @internal
	 */
	public static function add(array $data, ErrorCollection $errorCollection)
	{
		$parent = null;
		if(isset($data['PARENT']) && $data['PARENT'] instanceof Folder)
		{
			$parent = $data['PARENT'];
			unset($data['PARENT']);
		}

		/** @var File $file */
		$file = parent::add($data, $errorCollection);
		if($file)
		{
			if($parent !== null)
			{
				$file->setAttributes(array('PARENT' => $parent));
			}

			$versionData = array(
				'ID' => $file->getFileId(),
				'FILE_SIZE' => $file->getSize(),
			);

			if(!empty($data['UPDATE_TIME']))
			{
				$versionData['UPDATE_TIME'] = $data['UPDATE_TIME'];
			}
			if(!empty($data['ETAG']))
			{
				$versionData['ETAG'] = $data['ETAG'];
			}

			$version = $file->addVersion($versionData, $file->getCreatedBy());
			if(!$version)
			{
				$errorCollection->add($file->getErrors());
				$file->delete(SystemUser::SYSTEM_USER_ID);
				return null;
			}

			$event = new Event(Driver::INTERNAL_MODULE_ID, "onAfterAddFile", array($file));
			$event->send();
		}

		return $file;
	}

	/**
	 * Returns once model by specific filter.
	 * @param array $filter Filter.
	 * @param array $with List of eager loading.
	 * @throws \Bitrix\Main\NotImplementedException
	 * @return static
	 */
	public static function load(array $filter, array $with = array())
	{
		$filter['TYPE'] = ObjectTable::TYPE_FILE;

		return parent::load($filter, $with);
	}

	protected static function getClassNameModel(array $row)
	{
		$classNameModel = parent::getClassNameModel($row);
		if(
			$classNameModel === static::className() ||
			is_subclass_of($classNameModel, static::className()) ||
			in_array(static::className(), class_parents($classNameModel)) //5.3.9
		)
		{
			return $classNameModel;
		}

		throw new ObjectException('Could not to get non subclass of ' . static::className());
	}

	/**
	 * Returns extension.
	 * @return string
	 */
	public function getExtension()
	{
		if($this->extension === null)
		{
			$this->extension = getFileExtension($this->getName());
		}
		return $this->extension;
	}

	/**
	 * Returns external hash.
	 * @return string
	 */
	public function getExternalHash()
	{
		return $this->externalHash;
	}

	/**
	 * Returns etag.
	 * @return string
	 */
	public function getEtag()
	{
		return $this->etag;
	}

	/**
	 * Changes etag.
	 *
	 * @param string $etag Entity tag.
	 * @return bool
	 */
	public function changeEtag($etag)
	{
		return $this->update(array('ETAG' => $etag));
	}

	/**
	 * Returns global content version.
	 *
	 * Version always increments after creating new version.
	 * @return int
	 */
	public function getGlobalContentVersion()
	{
		return $this->globalContentVersion;
	}

	/**
	 * Returns id of file (table {b_file}).
	 * @return int
	 */
	public function getFileId()
	{
		return $this->fileId;
	}

	/**
	 * Returns file (@see CFile::getById());
	 * @return array|null
	 * @throws \Bitrix\Main\NotImplementedException
	 */
	public function getFile()
	{
		if(!$this->fileId)
		{
			return null;
		}

		if(isset($this->file) && $this->fileId == $this->file['ID'])
		{
			return $this->file;
		}
		/** @noinspection PhpDynamicAsStaticMethodCallInspection */
		$this->file = \CFile::getByID($this->fileId)->fetch();

		if(!$this->file)
		{
			return array();
		}

		return $this->file;
	}

	/**
	 * Returns id of preview image.
	 * @return int
	 * @deprecated
	 */
	public function getPreviewId()
	{
		return $this->previewId;
	}

	/**
	 * Set previewId, save in the database.
	 *
	 * @param int $fileId
	 * @return bool
	 * @deprecated
	 */
	public function changePreviewId($fileId)
	{
		return $this->update(array('PREVIEW_ID' => $fileId));
	}

	/**
	 * Returns id of view.
	 * @return int|null
	 * @deprecated
	 */
	public function getViewId()
	{
		return $this->viewId;
	}

	/**
	 * Set viewId, save in the database.
	 *
	 * @param int $fileId
	 * @return bool
	 * @deprecated
	 */
	public function changeViewId($fileId)
	{
		return $this->update(array('VIEW_ID' => $fileId));
	}

	/**
	 * Delete converted view file.
	 *
	 * @return bool
	 * @deprecated
	 */
	public function deleteViewId()
	{
		if($this->viewId > 0)
		{
			\CFile::Delete($this->viewId);
			return $this->update(array('VIEW_ID' => null));
		}

		return false;
	}

	/**
	 * Returns size in bytes.
	 *
	 * @param null $filter
	 *
	 * @return int
	 */
	public function getSize($filter = null)
	{
		return $this->size;
	}

	/**
	 * Returns type of file.
	 * @see TypeFile class for details.
	 * @return int
	 */
	public function getTypeFile()
	{
		return $this->typeFile;
	}

	/**
	 * Renames object.
	 * @param string $newName New name.
	 * @return bool
	 */
	public function rename($newName)
	{
		$result = parent::rename($newName);
		if($result)
		{
			$this->extension = null;
		}
		return $result;
	}

	/**
	 * Copies object to target folder.
	 * @param Folder $targetFolder Target folder.
	 * @param int    $updatedBy Id of user.
	 * @param bool   $generateUniqueName Generates unique name for object in directory.
	 * @return BaseObject|null
	 */
	public function copyTo(Folder $targetFolder, $updatedBy, $generateUniqueName = false)
	{
		$this->errorCollection->clear();

		/** @noinspection PhpDynamicAsStaticMethodCallInspection */
		$forkFileId = \CFile::copyFile($this->getFileId(), true);
		if(!$forkFileId)
		{
			$this->errorCollection->add(array(new Error(Loc::getMessage('DISK_FILE_MODEL_ERROR_COULD_NOT_COPY_FILE'), self::ERROR_COULD_NOT_COPY_FILE)));
			return null;
		}

		/** @noinspection PhpDynamicAsStaticMethodCallInspection */
		$fileArray = \CFile::getFileArray($forkFileId);
		$fileModel = $targetFolder->addFile(array(
			'NAME' => $this->getName(),
			'FILE_ID' => $forkFileId,
			'ETAG' => $this->getEtag(),
			'SIZE' => $fileArray['FILE_SIZE'],
			'CREATED_BY' => $updatedBy,
		), array(), $generateUniqueName);
		if(!$fileModel)
		{
			\CFile::delete($forkFileId);
			$this->errorCollection->add($targetFolder->getErrors());

			return null;
		}

		return $fileModel;
	}

	/**
	 * Increases global content version.
	 * @return bool
	 */
	public function increaseGlobalContentVersion()
	{
		//todo inc in DB by expression
		$success = $this->update(array(
			'GLOBAL_CONTENT_VERSION' => (int)$this->getGlobalContentVersion() + 1,
		));

		if(!$success)
		{
			return false;
		}

		$this->updateLinksAttributes(array(
			'GLOBAL_CONTENT_VERSION' => $this->getGlobalContentVersion(),
		));

		return $success;
	}

	/**
	 * Returns object lock model.
	 *
	 * @return ObjectLock
	 */
	public function getLock()
	{
		if($this->isLoadedAttribute('lock'))
		{
			return $this->lock;
		}
		$this->lock = ObjectLock::load(array('OBJECT_ID' => $this->getRealObjectId()));
		$this->setAsLoadedAttribute('lock');

		return $this->lock;
	}

	/**
	 * Locks file.
	 *
	 * @param int $lockedBy User which locks object.
	 * @param string $token Token, if is not set, then run auto generate.
	 * @param array $data Data.
	 * @return ObjectLock
	 */
	public function lock($lockedBy, $token = null, array $data = array())
	{
		if($token === null)
		{
			$token = ObjectLock::generateLockToken();
		}

		//now we work only with exclusive locks.
		$objectLock = $this->getLock();
		if($objectLock)
		{
			$this->errorCollection[] = new Error(
				Loc::getMessage('DISK_FILE_MODEL_ERROR_ALREADY_LOCKED'),
				self::ERROR_ALREADY_LOCKED
			);
			return null;
		}

		$lock = ObjectLock::add(array_merge($data, array(
			'TOKEN' => $token,
			'OBJECT_ID' => $this->getRealObjectId(),
			'CREATED_BY' => $lockedBy,
		)), $this->errorCollection);

		if($lock)
		{
			$this->update(array('SYNC_UPDATE_TIME' => new DateTime()));
			Driver::getInstance()->sendChangeStatusToSubscribers($this, 'quick');
		}

		return $lock;
	}

	/**
	 * Unlocks file.
	 *
	 * @param int $unlockedBy User which unlocks object.
	 * @param string $token Token.
	 * @return bool
	 */
	public function unlock($unlockedBy, $token = null)
	{
		$objectLock = $this->getLock();
		if(!$objectLock)
		{
			return true;
		}

		if(!$objectLock->canUnlock($unlockedBy))
		{
			$this->errorCollection[] = new Error(
				Loc::getMessage('DISK_FILE_MODEL_ERROR_INVALID_USER_FOR_UNLOCK'),
				self::ERROR_INVALID_USER_FOR_UNLOCK
			);
			return false;
		}

		if($token !== null && $objectLock->getToken() !== $token)
		{
			$this->errorCollection[] = new Error(
				Loc::getMessage('DISK_FILE_MODEL_ERROR_INVALID_LOCK_TOKEN'),
				self::ERROR_INVALID_LOCK_TOKEN
			);
			return false;
		}

		$success = $objectLock->delete($unlockedBy);
		if($success)
		{
			$this->update(array('SYNC_UPDATE_TIME' => new DateTime()));
			Driver::getInstance()->sendChangeStatusToSubscribers($this, 'quick');
		}

		return $success;
	}

	/**
	 * Updates file content.
	 *
	 * Runs index file, updates all FileLinks, sends notify to subscribers.
	 *
	 * @param array $file Structure like $_FILES.
	 * @param int $updatedBy Id of user.
	 * @return bool
	 * @throws \Bitrix\Main\ArgumentException
	 */
	public function updateContent(array $file, $updatedBy)
	{
		$this->errorCollection->clear();

		$this->checkRequiredInputParams($file, array(
			'ID', 'FILE_SIZE'
		));

		if(Configuration::isEnabledObjectLock())
		{
			$objectLock = $this->getLock();
			if($objectLock && $objectLock->isExclusive() && !$objectLock->canUnlock($updatedBy))
			{
				$this->errorCollection[] = new Error(Loc::getMessage('DISK_FILE_MODEL_ERROR_EXCLUSIVE_LOCK'), self::ERROR_EXCLUSIVE_LOCK);
				return false;
			}
		}

		//todo inc in DB by expression
		$updateData = array(
			'GLOBAL_CONTENT_VERSION' => (int)$this->getGlobalContentVersion() + 1,
			'FILE_ID' => $file['ID'],
			'ETAG' => empty($file['ETAG'])? $this->generateEtag() : $file['ETAG'], //each time we update etag field. It is random string
			'SIZE' => $file['FILE_SIZE'],
			'UPDATE_TIME' => empty($file['UPDATE_TIME'])? new DateTime() : $file['UPDATE_TIME'],
			'UPDATED_BY' => $updatedBy,
		);

		$this->prevFileId = $this->fileId;
		$success = $this->update($updateData);

		if(!$success)
		{
			$this->prevFileId = null;
			return false;
		}

		$this->changeParentUpdateTime(new DateTime(), $updatedBy);

		$this->updateLinksAttributes(array(
			'ETAG' => $this->getEtag(),
			'GLOBAL_CONTENT_VERSION' => $this->getGlobalContentVersion(),
			'SIZE' => $file['FILE_SIZE'],
			'UPDATE_TIME' => $this->getUpdateTime(),
			'SYNC_UPDATE_TIME' => $this->getSyncUpdateTime(),
			'UPDATED_BY' => $updatedBy,
		));

		$driver = Driver::getInstance();
		if ($this->getStorage()->isUseInternalRights())
		{
			$driver->getRecentlyUsedManager()->push($updatedBy, $this->getId());
		}

		if ($this->getGlobalContentVersion() <= 1)
		{
			//initial full index
			$driver->getIndexManager()->indexFile($this);
		}
		else
		{
			//just update content
			$driver->getIndexManager()->updateFileContent($this);
		}

		//todo little hack...We don't synchronize file in folder with uploaded files. And we have not to send notify by pull
		if($this->parent === null || $this->parent && $this->parent->getCode() !== Folder::CODE_FOR_UPLOADED_FILES)
		{
			$driver->sendChangeStatusToSubscribers($this, 'quick');

			$updatedBy = $this->getUpdatedBy();
			if($updatedBy)
			{
				$driver->sendEvent($updatedBy, 'live', array(
					'objectId' => $this->getId(),
					'action' => 'commit',
					'contentVersion' => (int)$this->getGlobalContentVersion(),
					'size' => (int)$this->getSize(),
					'formatSize' => (string)CFile::formatSize($this->getSize()),
				));
			}
		}

		return true;
	}

	/**
	 * Adds new version to file.
	 *
	 * The method may joins version with last version.
	 *
	 * @param array $file Structure like $_FILES.
	 * @param int $createdBy Id of user.
	 * @param bool $disableJoin If set false the method attempts to join version with last version (@see \Bitrix\Disk\File::SECONDS_TO_JOIN_VERSION).
	 * @return Version|null
	 * @throws \Bitrix\Main\SystemException
	 */
	public function addVersion(array $file, $createdBy, $disableJoin = false)
	{
		$this->errorCollection->clear();

		if(Configuration::isEnabledStorageSizeRestriction())
		{
			$this->checkRequiredInputParams($file, array(
				'FILE_SIZE'
			));

			if($this->errorCollection->hasErrors())
			{
				return null;
			}

			if(!$this->getStorage()->isPossibleToUpload($file['FILE_SIZE']))
			{
				$this->errorCollection[] = new Error(
					Loc::getMessage('DISK_FILE_MODEL_ERROR_SIZE_RESTRICTION'), self::ERROR_SIZE_RESTRICTION
				);

				return null;
			}
		}

		$needToJoin = !$disableJoin && $this->isNeedToJoinVersion($createdBy);
		if(!$this->updateContent($file, $createdBy))
		{
			return null;
		}

		if($needToJoin)
		{
			$lastVersion = $this->joinVersion();
			if ($lastVersion)
			{
				$this->tryToRunBizProcAfterEdit();

				return $lastVersion;
			}
		}

		$versionModel = Version::add(array_merge(array(
			'OBJECT_ID' => $this->id,
			'FILE_ID' => $this->fileId,
			'NAME' => $this->name,
			'CREATED_BY' => $createdBy,
		), $this->getHistoricalData()), $this->errorCollection);

		if(!$versionModel)
		{
			return null;
		}

		$this->cleanVersionsOverLimit($createdBy);
		$this->commentAttachedObjects($versionModel);
		$this->resetHeadVersionToAttachedObject($versionModel);

		if ($this->getGlobalContentVersion() == 1)
		{
			$this->tryToRunBizProcAfterCreate();
		}
		else
		{
			$this->tryToRunBizProcAfterEdit();
		}

		Application::getInstance()->getTaggedCache()->clearByTag("disk_file_{$this->id}");

		return $versionModel;
	}

	private function generateEtag()
	{
		return md5(mt_rand() . mt_rand());
	}

	private function tryToRunBizProcAfterCreate()
	{
		if (!Integration\BizProcManager::isAvailable())
		{
			return;
		}

		$storage = $this->getStorage();
		if($storage && $this->getStorage()->isEnabledBizProc())
		{
			BizProcDocument::runAfterCreate($this->storageId, $this->id);
		}
	}

	private function tryToRunBizProcAfterEdit()
	{
		if (!Integration\BizProcManager::isAvailable())
		{
			return;
		}

		$storage = $this->getStorage();
		if($storage && $this->getStorage()->isEnabledBizProc())
		{
			BizProcDocument::runAfterEdit($this->storageId, $this->id);
		}
	}

	private function commentAttachedObjects(Version $version)
	{
		$createdBy = $version->getCreatedBy();
		$valueVersionUf = FileUserType::NEW_FILE_PREFIX . $version->getId();

		/** @var User $createUser */
		$createUser = User::loadById($createdBy);
		if(!$createUser)
		{
			return;
		}

		$text = $this->getTextForComment($createUser);
		$attachedObjects = $this->getAttachedObjects(array(
			'filter' => array(
				'=ALLOW_AUTO_COMMENT' => 1,
			),
		));
		foreach ($attachedObjects as $attache)
		{
			if(!$attache->getAllowAutoComment())
			{
				continue;
			}

			AttachedObject::storeDataByObjectId($this->getId(), array(
				'IS_EDITABLE' => $attache->isEditable(),
				'ALLOW_EDIT' => $attache->getAllowEdit(),
			));

			$attache->getConnector()->addComment($createdBy, array(
				'text' => $text,
				'versionId' => $valueVersionUf,
				'authorGender' => $createUser->getPersonalGender()
			));

			AttachedObject::storeDataByObjectId($this->getId(), null);
		}
		unset($attache);
	}

	private function cleanVersionsOverLimit($createdBy)
	{
		$versionLimitPerFile = Configuration::getVersionLimitPerFile();
		if ($this->getGlobalContentVersion() > 1 && $versionLimitPerFile > 0)
		{
			foreach ($this->getVersions(array('offset' => $versionLimitPerFile, 'limit' => 100)) as $oldVersion)
			{
				$oldVersion->delete($createdBy);
			}
		}
	}

	private function joinVersion()
	{
		$lastVersion = $this->getLastVersion();
		if (!$lastVersion)
		{
			return null;
		}

		$joinData = array('CREATE_TIME' => new DateTime);

		if (!$lastVersion->joinData(
			array_merge(
				$joinData,
				$this->getHistoricalData()
			)
		))
		{
			$this->errorCollection->add($lastVersion->getErrors());

			return null;
		}

		if ($this->prevFileId && $this->prevFileId != $this->fileId)
		{
			CFile::delete($this->prevFileId);
		}

		return $lastVersion;
	}

	private function isNeedToJoinVersion($createdBy)
	{
		if(Configuration::getFileVersionTtl() === 0 && !$this->hasAttachedObjects())
		{
			return true;
		}

		$now = new DateTime;
		if($this->updateTime && $this->updatedBy == $createdBy)
		{
			$updateTimestamp = $this->updateTime->getTimestamp();
			if($now->getTimestamp() - $updateTimestamp < self::SECONDS_TO_JOIN_VERSION)
			{
				return true;
			}
		}

		return false;
	}

	private function resetHeadVersionToAttachedObject(Version $version)
	{
		if(Configuration::isEnabledKeepVersion())
		{
			return;
		}

		AttachedObjectTable::updateBatch(
			array(
				'VERSION_ID' => $version->getId(),
			),
			array(
				'OBJECT_ID' => $version->getObjectId(),
				'!=VERSION_ID' => null,
			)
		);
	}

	private function getTextForComment(User $createUser)
	{
		if(!Configuration::isEnabledKeepVersion())
		{
			$text = Loc::getMessage('DISK_FILE_MODEL_UPLOAD_NEW_HEAD_VERSION_IN_COMMENT_M');
			if($createUser->getPersonalGender() == 'F')
			{
				$text = Loc::getMessage('DISK_FILE_MODEL_UPLOAD_NEW_HEAD_VERSION_IN_COMMENT_F');
			}
		}
		else
		{
			$text = Loc::getMessage('DISK_FILE_MODEL_UPLOAD_NEW_VERSION_IN_COMMENT_M');
			if($createUser->getPersonalGender() == 'F')
			{
				$text = Loc::getMessage('DISK_FILE_MODEL_UPLOAD_NEW_VERSION_IN_COMMENT_F');
			}
		}

		return $text;
	}

	/**
	 * Uploads new version to file.
	 * @see \Bitrix\Disk\File::addVersion().
	 * @param array $fileArray Structure like $_FILES.
	 * @param int $createdBy Id of user.
	 * @return Version|null
	 */
	public function uploadVersion(array $fileArray, $createdBy)
	{
		$this->errorCollection->clear();

		if(!isset($fileArray['MODULE_ID']))
		{
			$fileArray['MODULE_ID'] = Driver::INTERNAL_MODULE_ID;
		}

		if(empty($fileArray['type']))
		{
			$fileArray['type'] = '';
		}
		$fileArray['type'] = TypeFile::normalizeMimeType($fileArray['type'], $this->name);

		/** @noinspection PhpDynamicAsStaticMethodCallInspection */
		$fileId = CFile::saveFile($fileArray, Driver::INTERNAL_MODULE_ID, true, true);
		if(!$fileId)
		{
			$this->errorCollection->add(array(new Error(Loc::getMessage('DISK_FILE_MODEL_ERROR_COULD_NOT_SAVE_FILE'), self::ERROR_COULD_NOT_SAVE_FILE)));
			return null;
		}
		$updateTime = isset($fileArray['UPDATE_TIME'])? $fileArray['UPDATE_TIME'] : null;
		/** @var array $fileArray */
		/** @noinspection PhpDynamicAsStaticMethodCallInspection */
		$fileArray = CFile::getFileArray($fileId);
		if(!$fileArray)
		{
			CFile::delete($fileId);
			$this->errorCollection->add(array(new Error(Loc::getMessage('DISK_FILE_MODEL_ERROR_COULD_NOT_SAVE_FILE'), self::ERROR_COULD_NOT_GET_SAVED_FILE)));
			return null;
		}
		if($updateTime)
		{
			$fileArray['UPDATE_TIME'] = $updateTime;
		}
		$version = $this->addVersion($fileArray, $createdBy);
		if(!$version)
		{
			CFile::delete($fileId);
		}

		return $version;
	}

	/**
	 * Returns version of file by version id.
	 * @param int $versionId Id of version.
	 * @return static
	 */
	public function getVersion($versionId)
	{
		$version = Version::load(array(
			'ID' => $versionId,
			'OBJECT_ID' => $this->id,
		));
		if($version)
		{
			$version->setAttributes(array('OBJECT' => $this));
		}

		return $version;
	}

	/**
	 * Gets last version of the file.
	 * @return Version|null
	 */
	public function getLastVersion()
	{
		$versions = $this->getVersions(array('limit' => 1));
		return array_shift($versions)?: null;
	}

	/**
	 * Returns all versions by file.
	 * @param array $parameters Parameters.
	 * @return Version[]
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Exception
	 */
	public function getVersions(array $parameters = array())
	{
		if(!isset($parameters['filter']))
		{
			$parameters['filter'] = array();
		}
		$parameters['filter']['OBJECT_ID'] = $this->id;

		if(!isset($parameters['order']))
		{
			$parameters['order'] = array(
				'CREATE_TIME' => 'DESC',
			);
		}

		$versions = Version::getModelList($parameters);
		foreach($versions as $version)
		{
			$version->setAttributes(array('OBJECT' => $this));
		}
		unset($version);

		return $versions;
	}

	/**
	 * Restores file from the version.
	 *
	 * The method is similar with (@see Bitrix\Disk\File::addVersion()).
	 *
	 * @param Version $version Version which need to restore.
	 * @param int $createdBy Id of user.
	 * @return bool
	 */
	public function restoreFromVersion(Version $version, $createdBy)
	{
		$this->errorCollection->clear();

		if($version->getObjectId() != $this->getRealObjectId())
		{
			$this->errorCollection->add(array(new Error(Loc::getMessage('DISK_FILE_MODEL_ERROR_COULD_NOT_RESTORE_FROM_ANOTHER_OBJECT'), self::ERROR_COULD_NOT_RESTORE_FROM_OBJECT)));
			return false;
		}
		/** @noinspection PhpDynamicAsStaticMethodCallInspection */
		$forkFileId = \CFile::copyFile($version->getFileId(), true);

		if(!$forkFileId)
		{
			$this->errorCollection->add(array(new Error(Loc::getMessage('DISK_FILE_MODEL_ERROR_COULD_NOT_COPY_FILE'), self::ERROR_COULD_NOT_COPY_FILE)));
			return false;
		}

		/** @noinspection PhpDynamicAsStaticMethodCallInspection */
		if($this->addVersion(\CFile::getFileArray($forkFileId), $createdBy, true) === null)
		{
			return false;
		}

		return true;
	}

	/**
	 * Moves object to another folder.
	 * Support cross-storage move (mark deleted + create new)
	 * @param Folder $folder             Destination folder.
	 * @param int    $movedBy            User id of user, which move file.
	 * @param bool   $generateUniqueName Generates unique name for object if in destination directory.
	 * @return BaseObject|null
	 */
	public function moveTo(Folder $folder, $movedBy, $generateUniqueName = false)
	{
		if(Configuration::isEnabledObjectLock())
		{
			$objectLock = $this->getLock();
			if($objectLock && $objectLock->isExclusive() && !$objectLock->canUnlock($movedBy))
			{
				$this->errorCollection[] = new Error(Loc::getMessage('DISK_FILE_MODEL_ERROR_EXCLUSIVE_LOCK'), self::ERROR_EXCLUSIVE_LOCK);
				return null;
			}
		}

		return parent::moveTo($folder, $movedBy, $generateUniqueName);
	}

	/**
	 * Moves file to folder from another storage.
	 * @see moveInAnotherStorage(), moveInSameStorage() to understand logic.
	 * This method is specific and can use nobody. Be careful!
	 *
	 *
	 * @internal
	 * @param Folder $folder             Destination folder.
	 * @param int    $movedBy            User id of user, which move file.
	 * @param bool   $generateUniqueName Generates unique name for object if in destination directory.
	 * @return bool
	 * @throws \Bitrix\Main\ArgumentException
	 */
	public function moveToAnotherFolder(Folder $folder, $movedBy, $generateUniqueName = false)
	{
		$realFolderId = $folder->getRealObject()->getId();
		if($this->getParentId() == $realFolderId)
		{
			return true;
		}

		$possibleNewName = $this->name;
		if($generateUniqueName)
		{
			$possibleNewName = $this->generateUniqueName($this->name, $realFolderId);
		}
		$needToRename = $possibleNewName != $this->name;

		if(!$this->isUniqueName($possibleNewName, $realFolderId))
		{
			$this->errorCollection->add(array(new Error(Loc::getMessage('DISK_OBJECT_MODEL_ERROR_NON_UNIQUE_NAME'), self::ERROR_NON_UNIQUE_NAME)));
			return false;
		}
		$this->name = $possibleNewName;

		if($needToRename)
		{
			$successUpdate = $this->update(array(
				'NAME' => $possibleNewName
			));
			if(!$successUpdate)
			{
				return false;
			}
		}

		/** @var FileTable $tableClassName */
		$tableClassName = $this->getTableClassName();

		$moveResult = $tableClassName::move($this->id, $realFolderId);
		if(!$moveResult->isSuccess())
		{
			$this->errorCollection->addFromResult($moveResult);
			return false;
		}
		$this->setAttributesFromResult($moveResult);

		Driver::getInstance()->getRightsManager()->setAfterMove($this);

		$subscribersAfterMove = Driver::getInstance()->collectSubscribers($this);
		Driver::getInstance()->sendChangeStatus($subscribersAfterMove);

		if($folder->getRealObject()->getStorageId() != $this->storageId)
		{
			$changeStorageIdResult = $tableClassName::changeStorageId($this->id, $folder->getRealObject()->getStorageId());
			if(!$changeStorageIdResult->isSuccess())
			{
				$this->errorCollection->addFromResult($changeStorageIdResult);
				return false;
			}
		}

		$success = $this->update(array(
			'UPDATE_TIME' => new DateTime(),
			'UPDATED_BY' => $movedBy,
		));
		if(!$success)
		{
			return null;
		}

		return $this;
	}

	/**
	 * Marks deleted object. It equals to move in trash can.
	 * @param int $deletedBy Id of user (or SystemUser::SYSTEM_USER_ID).
	 * @return bool
	 */
	public function markDeleted($deletedBy)
	{
		$status = $this->markDeletedInternal($deletedBy);
		if ($status)
		{
			$notifyManager = Driver::getInstance()->getDeletionNotifyManager();
			$notifyManager->put($this, $deletedBy);
			$notifyManager->send();
		}

		return $status;
	}

	/**
	 * Internal method for deleting file as child of folder.
	 * @param int $deletedBy Id of user.
	 * @param int $deletedType Type of delete (@see ObjectTable::DELETED_TYPE_ROOT, ObjectTable::DELETED_TYPE_CHILD)
	 * @return bool
	 * @internal
	 */
	public function markDeletedInternal($deletedBy, $deletedType = ObjectTable::DELETED_TYPE_ROOT)
	{
		$alreadyDeleted = $this->isDeleted();
		$success = parent::markDeletedInternal($deletedBy, $deletedType);
		if ($success && !$alreadyDeleted)
		{
			Driver::getInstance()->getDeletedLogManager()->mark($this, $deletedBy);
		}

		return $success;
	}

	/**
	 * Restores object from trash can.
	 * @param int $restoredBy Id of user (or SystemUser::SYSTEM_USER_ID).
	 * @return bool
	 */
	public function restore($restoredBy)
	{
		if (!$this->isDeleted())
		{
			return true;
		}

		$needRecalculate = $this->deletedType == ObjectTable::DELETED_TYPE_CHILD;
		$status = parent::restoreInternal($restoredBy);
		if($status && $needRecalculate)
		{
			$this->recalculateDeletedTypeAfterRestore($restoredBy);
		}
		if($status)
		{
			$driver = Driver::getInstance();
			if ($this->getStorage()->isUseInternalRights())
			{
				$driver->getRecentlyUsedManager()->push($restoredBy, $this->getId());
			}
			$driver->getIndexManager()->indexFileByModuleSearch($this);
			$driver->sendChangeStatusToSubscribers($this);

			//it's necessary to reset cache because in default way \Bitrix\Disk\Driver::sendChangeStatusToSubscribers
			// doesn't reset tree cache when send notification on the file. But in case when we restore file - we have to.
			//The reason is that we can restore folder when we restore the file.
			$subscribers = Driver::getInstance()->collectSubscribers($this);
			Driver::getInstance()->cleanCacheTreeBitrixDisk(array_keys($subscribers));
		}

		return $status;
	}

	/**
	 * Deletes file and all connected data and entities (@see Sharing, @see Rights, etc).
	 * @param int $deletedBy Id of user.
	 * @return bool
	 * @throws \Bitrix\Main\ArgumentNullException
	 */
	public function delete($deletedBy)
	{
		$this->errorCollection->clear();

		if(Configuration::isEnabledObjectLock())
		{
			$objectLock = $this->getLock();
			if($objectLock && $objectLock->isExclusive() && !$objectLock->canUnlock($deletedBy))
			{
				$this->errorCollection[] = new Error(
					Loc::getMessage('DISK_FILE_MODEL_ERROR_EXCLUSIVE_LOCK'), self::ERROR_EXCLUSIVE_LOCK
				);
				return false;
			}
		}

		$this->currentState = static::STATE_DELETE_PROCESS;

		$success = EditSessionTable::deleteByFilter(array(
			'OBJECT_ID' => $this->id,
		));

		if(!$success)
		{
			return false;
		}

		$success = ExternalLinkTable::deleteByFilter(array(
			'OBJECT_ID' => $this->id,
		));

		if(!$success)
		{
			return false;
		}

		foreach($this->getSharingsAsReal() as $sharing)
		{
			$sharing->delete($deletedBy);
		}

		//with status unreplied, declined (not approved)
		$success = SharingTable::deleteByFilter(array(
			'REAL_OBJECT_ID' => $this->id,
		));

		if(!$success)
		{
			return false;
		}

		foreach($this->getAttachedObjects() as $attached)
		{
			$attached->delete();
		}
		unset($attached);

		if(Integration\BizProcManager::isAvailable())
		{
			BizProcDocument::deleteWorkflowsFile($this->id);
		}

		SimpleRightTable::deleteBatch(array('OBJECT_ID' => $this->id));

		$success = RightTable::deleteByFilter(array(
			'OBJECT_ID' => $this->id,
		));

		if(!$success)
		{
			return false;
		}

		$versionQuery = Version::getList(array('filter' => array(
			'OBJECT_ID' => $this->id,
		)));
		while($versionData = $versionQuery->fetch())
		{
			$version = Version::buildFromArray($versionData);
			$version->setAttributes(array('OBJECT' => $this));
			$version->delete($deletedBy);
		}
		unset($version, $versionQuery);

		$success = VersionTable::deleteByFilter(array(
			'OBJECT_ID' => $this->id,
		));

		if(!$success)
		{
			return false;
		}

		if($this->getLock())
		{
			$this->getLock()->delete($deletedBy);
		}

		//it's possible, that object was already deleted. And we don't have to add it to deleted log. It's unnecessary.
		//we add only directly destroyed objects.
		if(!$this->isDeleted())
		{
			Driver::getInstance()->getDeletedLogManager()->mark($this, $deletedBy);
		}

		\CFile::delete($this->fileId);
		$deleteResult = FileTable::delete($this->id);
		if(!$deleteResult->isSuccess())
		{
			return false;
		}
		Driver::getInstance()->getIndexManager()->dropIndex($this);

		Application::getInstance()->getTaggedCache()->clearByTag("disk_file_{$this->id}");

		if(!$this->isLink())
		{
			//todo potential - very hard operation.
			foreach(File::getModelList(array('filter' => array('REAL_OBJECT_ID' => $this->id, '!=REAL_OBJECT_ID' => $this->id))) as $link)
			{
				$link->delete($deletedBy);
			}
			unset($link);
		}

		$event = new Event(Driver::INTERNAL_MODULE_ID, "onAfterDeleteFile", array($this->getId(), $deletedBy, array(
            'STORAGE_ID' => $this->getStorageId(),
        )));
		$event->send();

		return true;
	}

	/**
	 * Returns current state of the file.
	 *
	 * For example, STATE_DELETE_PROCESS.
	 *
	 * @return int
	 * @internal
	 */
	public function getCurrentState()
	{
		return $this->currentState;
	}

	protected function getHistoricalData()
	{
		return array(
			'FILE_ID' => $this->fileId,
			'SIZE' => $this->size,

			'GLOBAL_CONTENT_VERSION' => $this->globalContentVersion,

			'OBJECT_CREATED_BY' => $this->createdBy,
			'OBJECT_UPDATED_BY' => $this->updatedBy,

			'OBJECT_CREATE_TIME'=> $this->createTime,
			'OBJECT_UPDATE_TIME'=> $this->updateTime,
		);
	}

	/**
	 * Returns all attached objects by the file.
	 * @param array $parameters Parameters.
	 * @return AttachedObject[]
	 */
	public function getAttachedObjects(array $parameters = array())
	{
		if(!isset($parameters['filter']))
		{
			$parameters['filter'] = array();
		}
		$parameters['filter']['=OBJECT_ID'] = $this->id;
		$parameters['filter']['=VERSION_ID'] = null;

		return AttachedObject::getModelList($parameters);
	}

	/**
	 * Returns count of attached objects of file.
	 * @return int
	 */
	public function countAttachedObjects()
	{
		$countQuery = new Query(AttachedObjectTable::getEntity());
		$countQuery
			->addSelect(new ExpressionField('CNT', 'COUNT(1)'))
			->setFilter(array(
				'=OBJECT_ID' => $this->id,
				'=VERSION_ID' => null,
			))
		;

		$totalData = $countQuery->setLimit(null)->setOffset(null)->exec()->fetch();

		return $totalData['CNT'];
	}

	public function hasAttachedObjects(): bool
	{
		$query = new Query(AttachedObjectTable::getEntity());
		$query
			->addSelect('ID')
			->setFilter([
				'=OBJECT_ID' => $this->id,
			])
			->setLimit(1)
		;

		$data = $query->exec()->fetch();

		return !empty($data['ID']);
	}

	protected function updateLinksAttributes(array $attr)
	{
		$possibleToUpdate = array(
			'GLOBAL_CONTENT_VERSION' => 'globalContentVersion',
			'TYPE_FILE' => 'typeFile',
			'SIZE' => 'size',
			'EXTERNAL_HASH' => 'externalHash',
			'ETAG' => 'etag',
			'UPDATE_TIME' => 'updateTime',
			'SYNC_UPDATE_TIME' => 'syncUpdateTime',
			'UPDATED_BY' => 'updatedBy',
			'UPDATE_USER' => 'updateUser',
		);
		$attr = array_intersect_key($attr, $possibleToUpdate);
		if($attr)
		{
			parent::updateLinksAttributes($attr);
		}
	}

	/**
	 * Returns the list of pair for mapping.
	 * Key is field in DataManager, value is object property.
	 * @return array
	 */
	public static function getMapAttributes()
	{
		static $shelve = null;
		if($shelve !== null)
		{
			return $shelve;
		}

		$shelve = array_merge(parent::getMapAttributes(), array(
			'TYPE_FILE' => 'typeFile',
			'GLOBAL_CONTENT_VERSION' => 'globalContentVersion',
			'FILE_ID' => 'fileId',
			'SIZE' => 'size',
			'EXTERNAL_HASH' => 'externalHash',
			'PREVIEW_ID' => 'previewId',
			'VIEW_ID' => 'viewId',
			'ETAG' => 'etag',
		));

		return $shelve;
	}

	/**
	 * Return instance of View for current file.
	 *
	 * @return View\Base
	 */
	public function getView()
	{
		if (!$this->view)
		{
			$isTransformationEnabledInStorage = true;
			$storage = $this->getStorage();
			if ($storage)
			{
				$isTransformationEnabledInStorage = $storage->isEnabledTransformation();
			}

			$previewData = FilePreviewTable::getList(['filter' => ['FILE_ID' => $this->getFileId(),],])->fetch();
			$viewId = isset($previewData['PREVIEW_ID'])? $previewData['PREVIEW_ID'] : null;
			$imageId = isset($previewData['PREVIEW_IMAGE_ID'])? $previewData['PREVIEW_IMAGE_ID'] : null;

			if (TypeFile::isDocument($this))
			{
				$this->view = new View\Document($this->getName(), $this->getFileId(), $viewId, $imageId, $isTransformationEnabledInStorage);
			}
			elseif (TypeFile::isVideo($this))
			{
				$this->view = new View\Video($this->getName(), $this->getFileId(), $viewId, $imageId, $isTransformationEnabledInStorage);
			}
			else
			{
				$this->view = new View\Base($this->getName(), $this->getFileId(), $viewId, $imageId, $isTransformationEnabledInStorage);
			}
		}

		return $this->view;
	}

	/**
	 * Specify data which should be serialized to JSON
	 * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
	 * @return mixed data which can be serialized by <b>json_encode</b>,
	 * which is a value of any type other than a resource.
	 * @since 5.4.0
	 */
	public function jsonSerialize()
	{
		$urlShowObjectInGrid = Driver::getInstance()->getUrlManager()->getUrlFocusController('showObjectInGrid', [
			'objectId' => $this->getId(),
		]);
		$urlShowObjectInGrid = new Main\Web\Uri($urlShowObjectInGrid);

		return array_merge(parent::jsonSerialize(), [
			'typeFile' => (int)$this->getTypeFile(),
			'globalContentVersion' => (int)$this->getGlobalContentVersion(),
			'fileId' => (int)$this->getFileId(),
			'size' => (int)$this->getSize(),
			'etag' => $this->getEtag(),
			'links' => [
				/** @see \Bitrix\Disk\Controller\File::downloadAction() */
				'download' => Main\Engine\UrlManager::getInstance()->create('disk.file.download', ['fileId' => $this->getId()]),
				'showInGrid' => $urlShowObjectInGrid,
			],
		]);
	}
}