Your IP : 18.119.132.80


Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/main/lib/ui/viewer/
Upload File :
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/main/lib/ui/viewer/previewmanager.php

<?php

namespace Bitrix\Main\UI\Viewer;

use Bitrix\Main\Application;
use Bitrix\Main;
use Bitrix\Main\Context;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use Bitrix\Main\HttpRequest;
use Bitrix\Main\Loader;
use Bitrix\Main\SystemException;
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\UI\Viewer\Transformation\TransformerManager;
use Bitrix\Main\Web\MimeType;
use Bitrix\Main\Web\Uri;
use Bitrix\Main\Security;
use Bitrix\Main\Engine\Response;

final class PreviewManager
{
	public const GET_KEY_TO_SEND_PREVIEW_CONTENT    = 'ibxPreview';
	public const GET_KEY_TO_SHOW_IMAGE              = 'ibxShowImage';
	public const HEADER_TO_SEND_PREVIEW             = 'BX-Viewer';
	public const HEADER_TO_RESIZE_IMAGE             = 'BX-Viewer-image';
	public const HEADER_TO_GET_SOURCE               = 'BX-Viewer-src';
	public const HEADER_TO_RUN_FORCE_TRANSFORMATION = 'BX-Viewer-force-transformation';
	public const HEADER_TO_CHECK_TRANSFORMATION     = 'BX-Viewer-check-transformation';

	private const SALT_FILE_ID = 'previewfile';

	private ?TransformerManager $transformer;
	private array $rendererList = [];
	private static bool $disableCatchingViewByUser = false;

	/**
	 * @var HttpRequest
	 */
	private $httpRequest;

	public function __construct(HttpRequest $httpRequest = null)
	{
		$this->httpRequest = $httpRequest? : Context::getCurrent()->getRequest();
		$this->transformer = $this->buildTransformer();
		$this->buildViewRendererList();
	}

	private function buildTransformer(): ?TransformerManager
	{
		if (!Loader::includeModule('transformer'))
		{
			return null;
		}

		return new TransformerManager();
	}

	private function buildViewRendererList(): void
	{
		$default = [
			Renderer\Pdf::class,
			Renderer\Video::class,
			Renderer\Audio::class,
			Renderer\Image::class,
			Renderer\Code::class,
		];

		$event = new Event('main', 'onPreviewRendererBuildList');
		$event->send();

		$additionalList = [];
		foreach ($event->getResults() as $result)
		{
			if ($result->getType() != EventResult::SUCCESS)
			{
				continue;
			}
			$result = $result->getParameters();
			if (!\is_array($result))
			{
				throw new SystemException('Wrong event result. Must be array.');
			}

			foreach ($result as $class)
			{
				if (!\is_string($class) || !class_exists($class))
				{
					throw new SystemException('Wrong event result. There is not a class.');
				}

				if (!is_subclass_of($class, Renderer\Renderer::class, true))
				{
					throw new SystemException("Wrong event result. {$class} is not a subclass of " . Renderer\Renderer::class);
				}

				$additionalList[] = $class;
			}
		}

		$this->rendererList = array_merge($additionalList, $default);
	}

	public static function disableCatchingViewByUser(): void
	{
		self::$disableCatchingViewByUser = true;
	}

	public static function enableCatchingViewByUser(): void
	{
		self::$disableCatchingViewByUser = false;
	}

	public static function isDisabledCatchingViewByUser(): bool
	{
		 return self::$disableCatchingViewByUser;
	}

	public function isInternalRequest($file, $options): bool
	{
		if (self::isDisabledCatchingViewByUser())
		{
			return false;
		}

		if (!\is_array($file) || empty($file['ID']))
		{
			return false;
		}

		if (!empty($options['prevent_work_with_preview']))
		{
			return false;
		}

		if ($this->httpRequest->get(self::GET_KEY_TO_SEND_PREVIEW_CONTENT))
		{
			return true;
		}

		if ($this->httpRequest->get(self::GET_KEY_TO_SHOW_IMAGE))
		{
			return true;
		}

		if ($this->httpRequest->getHeader(self::HEADER_TO_SEND_PREVIEW))
		{
			return true;
		}

		if ($this->httpRequest->getHeader(self::HEADER_TO_RUN_FORCE_TRANSFORMATION))
		{
			return true;
		}

		if ($this->httpRequest->getHeader(self::HEADER_TO_CHECK_TRANSFORMATION))
		{
			return true;
		}

		if ($this->httpRequest->getHeader(self::HEADER_TO_RESIZE_IMAGE))
		{
			return true;
		}

		return false;
	}

	public function processViewByUserRequest($file, $options): void
	{
		self::disableCatchingViewByUser();

		$response = null;
		if ($this->httpRequest->get(self::GET_KEY_TO_SEND_PREVIEW_CONTENT))
		{
			$response = $this->sendPreviewContent($file, $options);
		}
		elseif ($this->httpRequest->get(self::GET_KEY_TO_SHOW_IMAGE))
		{
			$this->showImage($file, $options);
		}
		elseif ($this->httpRequest->getHeader(self::HEADER_TO_RUN_FORCE_TRANSFORMATION))
		{
			$response = $this->sendPreview($file, true);
		}
		elseif ($this->httpRequest->getHeader(self::HEADER_TO_CHECK_TRANSFORMATION))
		{
			$response = $this->checkTransformation($file);
		}
		elseif ($this->httpRequest->getHeader(self::HEADER_TO_SEND_PREVIEW))
		{
			$response = $this->sendPreview($file);
		}
		elseif ($this->httpRequest->getHeader(self::HEADER_TO_RESIZE_IMAGE))
		{
			$response = $this->sendResizedImage($file);
		}

		if (($response instanceof Response\BFile) && isset($options['cache_time']))
		{
			$response->setCacheTime($options['cache_time']);
		}

		if ($response instanceof Main\Response)
		{
			/** @global \CMain $APPLICATION */
			global $APPLICATION;

			$APPLICATION->RestartBuffer();

			Application::getInstance()->end(0, $response);
		}
	}

	protected function sendPreviewContent($file, $options): ?Response\BFile
	{
		$possiblePreviewFileId = $this->unsignFileId($this->httpRequest->get(self::GET_KEY_TO_SEND_PREVIEW_CONTENT));
		$filePreview = $this->getFilePreviewEntryByFileId($file['ID']);
		if (!$filePreview || empty($filePreview['PREVIEW_ID']) || $filePreview['PREVIEW_ID'] != $possiblePreviewFileId)
		{
			return null;
		}

		FilePreviewTable::update($filePreview['ID'], [
			'TOUCHED_AT' => new DateTime(),
		]);

		//get name for preview file
		return Response\BFile::createByFileId(
			$filePreview['PREVIEW_ID'],
			$file['ORIGINAL_NAME']
		);
	}

	protected function showImage($file, $options): void
	{
		if (!\is_array($options))
		{
			$options = [];
		}
		$options['force_download'] = false;
		$options['prevent_work_with_preview'] = true;
		\CFile::viewByUser($file, $options);
	}

	protected function sendResizedImage($file): ?Response\ResizedImage
	{
		$fileView = $this->getByImage($file['ID'], $this->getSourceUri());

		return $fileView?->render();
	}

	protected function prepareRenderParameters($file): array
	{
		$getContentType = function() use ($file){
			$filePreviewData = $this->getViewFileData($file);
			if (!$filePreviewData)
			{
				return null;
			}

			return $filePreviewData['CONTENT_TYPE'];
		};

		$getSourceUri = function() use ($file){
			$filePreviewData = $this->getViewFileData($file);
			if (!$filePreviewData)
			{
				return null;
			}

			return $this->getSourceUri()->addParams([
				self::GET_KEY_TO_SEND_PREVIEW_CONTENT => $this->signFileId($filePreviewData['ID']),
			]);
		};

		return [
			'alt' => [
				'contentType' => $getContentType->bindTo($this, $this),
				'sourceUri' => $getSourceUri->bindTo($this, $this),
			],
		];
	}

	protected function checkTransformation($file): Response\AjaxJson
	{
		return new Response\AjaxJson([
			'transformation' => (bool)$this->getViewFileData($file),
		]);
	}

	protected function sendPreview($file, bool $forceTransformation = false): Response\AjaxJson
	{
		$render = $this->buildRenderByFile(
			$file['ORIGINAL_NAME'],
			$file['CONTENT_TYPE'],
			$this->getSourceUri(),
			$this->prepareRenderParameters($file)
		);
		if ($render instanceof Renderer\Stub || $forceTransformation)
		{
			$filePreviewData = $this->getViewFileData($file);
			if ($filePreviewData)
			{
				$sourceUri = $this->getSourceUri()->addParams([
					self::GET_KEY_TO_SEND_PREVIEW_CONTENT => $this->signFileId($filePreviewData['ID']),
				]);

				$render = $this->buildRenderByFile(
					$filePreviewData['ORIGINAL_NAME'],
					$filePreviewData['CONTENT_TYPE'],
					$sourceUri
				);
			}
			else
			{
				$generatePreview = $this->generatePreview($file['ID']);
				if ($generatePreview instanceof Main\Result)
				{
					if ($generatePreview->isSuccess())
					{
						return Response\AjaxJson::createSuccess($generatePreview->getData());
					}

					return Response\AjaxJson::createError($generatePreview->getErrorCollection());
				}

				return Response\AjaxJson::createError();
			}
		}

		if ($render)
		{
			return Response\AjaxJson::createSuccess([
				'html' => $render->render(),
				'data' => $render->getData(),
			]);
		}

		return Response\AjaxJson::createError();
	}

	public function attachPreviewToFileId(int $fileId, ?int $previewId, ?int $previewImageId): Main\ORM\Data\Result
	{
		$updatedFields = array_filter([
			'PREVIEW_IMAGE_ID' => $previewImageId,
			'PREVIEW_ID' => $previewId,
		]);

		if (empty($updatedFields))
		{
			return new Main\ORM\Data\Result();
		}

		$alreadyPreview = $this->getFilePreviewEntryByFileId($fileId);
		if (isset($alreadyPreview['ID']))
		{
			$result = FilePreviewTable::update($alreadyPreview['ID'], $updatedFields);
		}
		else
		{
			$addedFields = $updatedFields;
			$addedFields['FILE_ID'] = $fileId;

			$result = FilePreviewTable::add($addedFields);
		}

		return $result;
	}

	public function setPreviewImageId(int $fileId, int $previewImageId): Main\ORM\Data\Result
	{
		return $this->attachPreviewToFileId($fileId, null, $previewImageId);
	}

	public function generatePreview($fileId): ?Main\Result
	{
		if (!$this->transformer)
		{
			return null;
		}

		//todo return status (OK, WAIT, ERROR) or some result.

		return $this->transformer->transform($fileId);
	}

	protected function getSourceUri(): Uri
	{
		$sourceSrc = $this->httpRequest->getHeader(self::HEADER_TO_GET_SOURCE);
		if (!$sourceSrc)
		{
			$sourceSrc = $this->httpRequest->getRequestUri();
		}

		return new Uri($sourceSrc);
	}

	protected function signFileId($fileId): string
	{
		$signer = new Security\Sign\Signer();

		return $signer->sign(
			base64_encode(serialize($fileId)),
			self::SALT_FILE_ID
		);
	}

	protected function unsignFileId($signedString)
	{
		$signer = new Security\Sign\Signer();

		$unsignedParameters = $signer->unsign(
			$signedString,
			self::SALT_FILE_ID
		);

		return unserialize(base64_decode($unsignedParameters), ['allowed_classes' => false]);
	}

	public function getByImage($fileId, Uri $sourceUri): ?Renderer\Image
	{
		$fileData = $this->getFileData($fileId);
		if (!$fileData)
		{
			return null;
		}

		$renderer = $this->buildRenderByFile(
			$fileData['ORIGINAL_NAME'],
			$fileData['CONTENT_TYPE'],
			$sourceUri,
			[
				'originalImage' => $fileData,
			]
		);
		if (!($renderer instanceof Renderer\Image))
		{
			return null;
		}

		return $renderer;
	}

	protected function getViewFileData(array $fileData)
	{
		static $cache = [];

		if (empty($fileData['ID']))
		{
			return null;
		}

		if (isset($cache[$fileData['ID']]) || \array_key_exists($fileData['ID'], $cache))
		{
			return $cache[$fileData['ID']];
		}

		$filePreview = $this->getFilePreviewEntryByFileId($fileData['ID']);
		if (!$filePreview || empty($filePreview['PREVIEW_ID']))
		{
			$cache[$fileData['ID']] = null;

			return null;
		}

		$cache[$fileData['ID']] = $this->getFileData($filePreview['PREVIEW_ID']);

		return $cache[$fileData['ID']];
	}

	public function getFilePreviewEntryByFileId(int $fileId): ?array
	{
		$row = FilePreviewTable::getList([
			'filter' => [
				'=FILE_ID' => $fileId,
			],
			'limit' => 1,
		])->fetch();

		return $row ?: null;
	}

	protected function buildRenderByFile($originalName, $contentType, Uri $sourceUri, array $options = [])
	{
		$options['contentType'] = $contentType;
		$rendererClass = $this->getRenderClassByFile([
			'contentType' => $contentType,
			'originalName' => $originalName,
		]);

		$reflectionClass = new \ReflectionClass($rendererClass);
		/** @see \Bitrix\Main\UI\Viewer\Renderer\Renderer::__construct */
		return $reflectionClass->newInstance($originalName, $sourceUri, $options);
	}

	public function getRenderClassByFile(array $file): string
	{
		$contentType = $file['contentType'];
		$originalName = $file['originalName'];
		$rendererClass = $this->findRenderClassByContentType($contentType);
		if (!$rendererClass)
		{
			$contentTypeByName = MimeType::getByFilename($originalName);
			$rendererClass = $this->findRenderClassByContentType($contentTypeByName);
		}

		$rendererClass = $rendererClass? : Renderer\Stub::class;

		if ($this->shouldRestrictBySize($file, $rendererClass))
		{
			return Renderer\RestrictedBySize::class;
		}

		return $rendererClass;
	}

	private function shouldRestrictBySize(array $file, $rendererClass): bool
	{
		if (!isset($file['size']))
		{
			return false;
		}

		$size = $file['size'];
		$restriction = $rendererClass::getSizeRestriction();
		if ($restriction !== null && $size > $restriction)
		{
			return true;
		}

		return false;
	}

	private function findRenderClassByContentType($contentType)
	{
		foreach ($this->rendererList as $rendererClass)
		{
			/** @var Renderer\Renderer $rendererClass */
			if (\in_array($contentType, $rendererClass::getAllowedContentTypes(), true))
			{
				return $rendererClass;
			}
		}

		return null;
	}

	/**
	 * Temporary dirty hack. It is here while we do not solve the problem of race conditions in b_file managed cache.
	 * Get file data from b_file on $fileId.
	 *
	 * @param int $fileId
	 * @param bool $cacheCleaned
	 * @return array|null
	 */
	protected function getFileData(int $fileId, bool $cacheCleaned = false): ?array
	{
		$fileData = \CFile::GetFileArray($fileId);
		if ($fileData === false && !$cacheCleaned)
		{
			global $DB;
			$strSql = "SELECT ID FROM b_file WHERE ID={$fileId}";
			$dbResult = $DB->Query($strSql);
			if ($dbResult->Fetch())
			{
				\CFile::CleanCache($fileId);

				return $this->getFileData($fileId, true);
			}
		}

		return $fileData ?: null;
	}
}