Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
previewmanager.php
1<?php
2
4
19
20final class PreviewManager
21{
22 public const GET_KEY_TO_SEND_PREVIEW_CONTENT = 'ibxPreview';
23 public const GET_KEY_TO_SHOW_IMAGE = 'ibxShowImage';
24 public const HEADER_TO_SEND_PREVIEW = 'BX-Viewer';
25 public const HEADER_TO_RESIZE_IMAGE = 'BX-Viewer-image';
26 public const HEADER_TO_GET_SOURCE = 'BX-Viewer-src';
27 public const HEADER_TO_RUN_FORCE_TRANSFORMATION = 'BX-Viewer-force-transformation';
28 public const HEADER_TO_CHECK_TRANSFORMATION = 'BX-Viewer-check-transformation';
29
30 private const SALT_FILE_ID = 'previewfile';
31
32 private ?TransformerManager $transformer;
33 private array $rendererList = [];
34 private static bool $disableCatchingViewByUser = false;
35
39 private $httpRequest;
40
41 public function __construct(HttpRequest $httpRequest = null)
42 {
43 $this->httpRequest = $httpRequest? : Context::getCurrent()->getRequest();
44 $this->transformer = $this->buildTransformer();
45 $this->buildViewRendererList();
46 }
47
48 private function buildTransformer(): ?TransformerManager
49 {
50 if (!Loader::includeModule('transformer'))
51 {
52 return null;
53 }
54
55 return new TransformerManager();
56 }
57
58 private function buildViewRendererList(): void
59 {
60 $default = [
61 Renderer\Pdf::class,
62 Renderer\Video::class,
63 Renderer\Audio::class,
64 Renderer\Image::class,
65 Renderer\Code::class,
66 ];
67
68 $event = new Event('main', 'onPreviewRendererBuildList');
69 $event->send();
70
71 $additionalList = [];
72 foreach ($event->getResults() as $result)
73 {
74 if ($result->getType() != EventResult::SUCCESS)
75 {
76 continue;
77 }
78 $result = $result->getParameters();
79 if (!\is_array($result))
80 {
81 throw new SystemException('Wrong event result. Must be array.');
82 }
83
84 foreach ($result as $class)
85 {
86 if (!\is_string($class) || !class_exists($class))
87 {
88 throw new SystemException('Wrong event result. There is not a class.');
89 }
90
91 if (!is_subclass_of($class, Renderer\Renderer::class, true))
92 {
93 throw new SystemException("Wrong event result. {$class} is not a subclass of " . Renderer\Renderer::class);
94 }
95
96 $additionalList[] = $class;
97 }
98 }
99
100 $this->rendererList = array_merge($additionalList, $default);
101 }
102
103 public static function disableCatchingViewByUser(): void
104 {
105 self::$disableCatchingViewByUser = true;
106 }
107
108 public static function enableCatchingViewByUser(): void
109 {
110 self::$disableCatchingViewByUser = false;
111 }
112
113 public static function isDisabledCatchingViewByUser(): bool
114 {
115 return self::$disableCatchingViewByUser;
116 }
117
118 public function isInternalRequest($file, $options): bool
119 {
120 if (self::isDisabledCatchingViewByUser())
121 {
122 return false;
123 }
124
125 if (!\is_array($file) || empty($file['ID']))
126 {
127 return false;
128 }
129
130 if (!empty($options['prevent_work_with_preview']))
131 {
132 return false;
133 }
134
135 if ($this->httpRequest->get(self::GET_KEY_TO_SEND_PREVIEW_CONTENT))
136 {
137 return true;
138 }
139
140 if ($this->httpRequest->get(self::GET_KEY_TO_SHOW_IMAGE))
141 {
142 return true;
143 }
144
145 if ($this->httpRequest->getHeader(self::HEADER_TO_SEND_PREVIEW))
146 {
147 return true;
148 }
149
150 if ($this->httpRequest->getHeader(self::HEADER_TO_RUN_FORCE_TRANSFORMATION))
151 {
152 return true;
153 }
154
155 if ($this->httpRequest->getHeader(self::HEADER_TO_CHECK_TRANSFORMATION))
156 {
157 return true;
158 }
159
160 if ($this->httpRequest->getHeader(self::HEADER_TO_RESIZE_IMAGE))
161 {
162 return true;
163 }
164
165 return false;
166 }
167
168 public function processViewByUserRequest($file, $options): void
169 {
171
172 $response = null;
173 if ($this->httpRequest->get(self::GET_KEY_TO_SEND_PREVIEW_CONTENT))
174 {
175 $response = $this->sendPreviewContent($file, $options);
176 }
177 elseif ($this->httpRequest->get(self::GET_KEY_TO_SHOW_IMAGE))
178 {
179 $this->showImage($file, $options);
180 }
181 elseif ($this->httpRequest->getHeader(self::HEADER_TO_RUN_FORCE_TRANSFORMATION))
182 {
183 $response = $this->sendPreview($file, true);
184 }
185 elseif ($this->httpRequest->getHeader(self::HEADER_TO_CHECK_TRANSFORMATION))
186 {
187 $response = $this->checkTransformation($file);
188 }
189 elseif ($this->httpRequest->getHeader(self::HEADER_TO_SEND_PREVIEW))
190 {
191 $response = $this->sendPreview($file);
192 }
193 elseif ($this->httpRequest->getHeader(self::HEADER_TO_RESIZE_IMAGE))
194 {
195 $response = $this->sendResizedImage($file);
196 }
197
198 if (($response instanceof Response\BFile) && isset($options['cache_time']))
199 {
200 $response->setCacheTime($options['cache_time']);
201 }
202
203 if ($response instanceof Main\Response)
204 {
206 global $APPLICATION;
207
208 $APPLICATION->RestartBuffer();
209
210 Application::getInstance()->end(0, $response);
211 }
212 }
213
214 protected function sendPreviewContent($file, $options): ?Response\BFile
215 {
216 $possiblePreviewFileId = $this->unsignFileId($this->httpRequest->get(self::GET_KEY_TO_SEND_PREVIEW_CONTENT));
217 $filePreview = $this->getFilePreviewEntryByFileId($file['ID']);
218 if (!$filePreview || empty($filePreview['PREVIEW_ID']) || $filePreview['PREVIEW_ID'] != $possiblePreviewFileId)
219 {
220 return null;
221 }
222
223 FilePreviewTable::update($filePreview['ID'], [
224 'TOUCHED_AT' => new DateTime(),
225 ]);
226
227 //get name for preview file
228 return Response\BFile::createByFileId(
229 $filePreview['PREVIEW_ID'],
230 $file['ORIGINAL_NAME']
231 );
232 }
233
234 protected function showImage($file, $options): void
235 {
236 if (!\is_array($options))
237 {
238 $options = [];
239 }
240 $options['force_download'] = false;
241 $options['prevent_work_with_preview'] = true;
242 \CFile::viewByUser($file, $options);
243 }
244
245 protected function sendResizedImage($file): ?Response\ResizedImage
246 {
247 $fileView = $this->getByImage($file['ID'], $this->getSourceUri());
248
249 return $fileView?->render();
250 }
251
252 protected function prepareRenderParameters($file): array
253 {
254 $getContentType = function() use ($file){
255 $filePreviewData = $this->getViewFileData($file);
256 if (!$filePreviewData)
257 {
258 return null;
259 }
260
261 return $filePreviewData['CONTENT_TYPE'];
262 };
263
264 $getSourceUri = function() use ($file){
265 $filePreviewData = $this->getViewFileData($file);
266 if (!$filePreviewData)
267 {
268 return null;
269 }
270
271 return $this->getSourceUri()->addParams([
272 self::GET_KEY_TO_SEND_PREVIEW_CONTENT => $this->signFileId($filePreviewData['ID']),
273 ]);
274 };
275
276 return [
277 'alt' => [
278 'contentType' => $getContentType->bindTo($this, $this),
279 'sourceUri' => $getSourceUri->bindTo($this, $this),
280 ],
281 ];
282 }
283
284 protected function checkTransformation($file): Response\AjaxJson
285 {
286 return new Response\AjaxJson([
287 'transformation' => (bool)$this->getViewFileData($file),
288 ]);
289 }
290
291 protected function sendPreview($file, bool $forceTransformation = false): Response\AjaxJson
292 {
293 $render = $this->buildRenderByFile(
294 $file['ORIGINAL_NAME'],
295 $file['CONTENT_TYPE'],
296 $this->getSourceUri(),
297 $this->prepareRenderParameters($file)
298 );
299 if ($render instanceof Renderer\Stub || $forceTransformation)
300 {
301 $filePreviewData = $this->getViewFileData($file);
302 if ($filePreviewData)
303 {
304 $sourceUri = $this->getSourceUri()->addParams([
305 self::GET_KEY_TO_SEND_PREVIEW_CONTENT => $this->signFileId($filePreviewData['ID']),
306 ]);
307
308 $render = $this->buildRenderByFile(
309 $filePreviewData['ORIGINAL_NAME'],
310 $filePreviewData['CONTENT_TYPE'],
311 $sourceUri
312 );
313 }
314 else
315 {
316 $generatePreview = $this->generatePreview($file['ID']);
317 if ($generatePreview instanceof Main\Result)
318 {
319 if ($generatePreview->isSuccess())
320 {
321 return Response\AjaxJson::createSuccess($generatePreview->getData());
322 }
323
324 return Response\AjaxJson::createError($generatePreview->getErrorCollection());
325 }
326
327 return Response\AjaxJson::createError();
328 }
329 }
330
331 if ($render)
332 {
333 return Response\AjaxJson::createSuccess([
334 'html' => $render->render(),
335 'data' => $render->getData(),
336 ]);
337 }
338
339 return Response\AjaxJson::createError();
340 }
341
342 public function attachPreviewToFileId(int $fileId, ?int $previewId, ?int $previewImageId): Main\ORM\Data\Result
343 {
344 $updatedFields = array_filter([
345 'PREVIEW_IMAGE_ID' => $previewImageId,
346 'PREVIEW_ID' => $previewId,
347 ]);
348
349 if (empty($updatedFields))
350 {
351 return new Main\ORM\Data\Result();
352 }
353
354 $alreadyPreview = $this->getFilePreviewEntryByFileId($fileId);
355 if (isset($alreadyPreview['ID']))
356 {
357 $result = FilePreviewTable::update($alreadyPreview['ID'], $updatedFields);
358 }
359 else
360 {
361 $addedFields = $updatedFields;
362 $addedFields['FILE_ID'] = $fileId;
363
364 $result = FilePreviewTable::add($addedFields);
365 }
366
367 return $result;
368 }
369
370 public function setPreviewImageId(int $fileId, int $previewImageId): Main\ORM\Data\Result
371 {
372 return $this->attachPreviewToFileId($fileId, null, $previewImageId);
373 }
374
375 public function generatePreview($fileId): ?Main\Result
376 {
377 if (!$this->transformer)
378 {
379 return null;
380 }
381
382 //todo return status (OK, WAIT, ERROR) or some result.
383
384 return $this->transformer->transform($fileId);
385 }
386
387 protected function getSourceUri(): Uri
388 {
389 $sourceSrc = $this->httpRequest->getHeader(self::HEADER_TO_GET_SOURCE);
390 if (!$sourceSrc)
391 {
392 $sourceSrc = $this->httpRequest->getRequestUri();
393 }
394
395 return new Uri($sourceSrc);
396 }
397
398 protected function signFileId($fileId): string
399 {
400 $signer = new Security\Sign\Signer();
401
402 return $signer->sign(
403 base64_encode(serialize($fileId)),
404 self::SALT_FILE_ID
405 );
406 }
407
408 protected function unsignFileId($signedString)
409 {
410 $signer = new Security\Sign\Signer();
411
412 $unsignedParameters = $signer->unsign(
413 $signedString,
414 self::SALT_FILE_ID
415 );
416
417 return unserialize(base64_decode($unsignedParameters), ['allowed_classes' => false]);
418 }
419
420 public function getByImage($fileId, Uri $sourceUri): ?Renderer\Image
421 {
422 $fileData = $this->getFileData($fileId);
423 if (!$fileData)
424 {
425 return null;
426 }
427
428 $renderer = $this->buildRenderByFile(
429 $fileData['ORIGINAL_NAME'],
430 $fileData['CONTENT_TYPE'],
431 $sourceUri,
432 [
433 'originalImage' => $fileData,
434 ]
435 );
436 if (!($renderer instanceof Renderer\Image))
437 {
438 return null;
439 }
440
441 return $renderer;
442 }
443
444 protected function getViewFileData(array $fileData)
445 {
446 static $cache = [];
447
448 if (empty($fileData['ID']))
449 {
450 return null;
451 }
452
453 if (isset($cache[$fileData['ID']]) || \array_key_exists($fileData['ID'], $cache))
454 {
455 return $cache[$fileData['ID']];
456 }
457
458 $filePreview = $this->getFilePreviewEntryByFileId($fileData['ID']);
459 if (!$filePreview || empty($filePreview['PREVIEW_ID']))
460 {
461 $cache[$fileData['ID']] = null;
462
463 return null;
464 }
465
466 $cache[$fileData['ID']] = $this->getFileData($filePreview['PREVIEW_ID']);
467
468 return $cache[$fileData['ID']];
469 }
470
471 public function getFilePreviewEntryByFileId(int $fileId): ?array
472 {
474 'filter' => [
475 '=FILE_ID' => $fileId,
476 ],
477 'limit' => 1,
478 ])->fetch();
479
480 return $row ?: null;
481 }
482
483 protected function buildRenderByFile($originalName, $contentType, Uri $sourceUri, array $options = [])
484 {
485 $options['contentType'] = $contentType;
486 $rendererClass = $this->getRenderClassByFile([
487 'contentType' => $contentType,
488 'originalName' => $originalName,
489 ]);
490
491 $reflectionClass = new \ReflectionClass($rendererClass);
493 return $reflectionClass->newInstance($originalName, $sourceUri, $options);
494 }
495
496 public function getRenderClassByFile(array $file): string
497 {
498 $contentType = $file['contentType'];
499 $originalName = $file['originalName'];
500 $rendererClass = $this->findRenderClassByContentType($contentType);
501 if (!$rendererClass)
502 {
503 $contentTypeByName = MimeType::getByFilename($originalName);
504 $rendererClass = $this->findRenderClassByContentType($contentTypeByName);
505 }
506
507 $rendererClass = $rendererClass? : Renderer\Stub::class;
508
509 if ($this->shouldRestrictBySize($file, $rendererClass))
510 {
511 return Renderer\RestrictedBySize::class;
512 }
513
514 return $rendererClass;
515 }
516
517 private function shouldRestrictBySize(array $file, $rendererClass): bool
518 {
519 if (!isset($file['size']))
520 {
521 return false;
522 }
523
524 $size = $file['size'];
525 $restriction = $rendererClass::getSizeRestriction();
526 if ($restriction !== null && $size > $restriction)
527 {
528 return true;
529 }
530
531 return false;
532 }
533
534 private function findRenderClassByContentType($contentType)
535 {
536 foreach ($this->rendererList as $rendererClass)
537 {
539 if (\in_array($contentType, $rendererClass::getAllowedContentTypes(), true))
540 {
541 return $rendererClass;
542 }
543 }
544
545 return null;
546 }
547
556 protected function getFileData(int $fileId, bool $cacheCleaned = false): ?array
557 {
558 $fileData = \CFile::GetFileArray($fileId);
559 if ($fileData === false && !$cacheCleaned)
560 {
561 global $DB;
562 $strSql = "SELECT ID FROM b_file WHERE ID={$fileId}";
563 $dbResult = $DB->Query($strSql);
564 if ($dbResult->Fetch())
565 {
566 \CFile::CleanCache($fileId);
567
568 return $this->getFileData($fileId, true);
569 }
570 }
571
572 return $fileData ?: null;
573 }
574}
static getCurrent()
Definition context.php:241
static includeModule($moduleName)
Definition loader.php:69
static getList(array $parameters=array())
static update($primary, array $data)
__construct(HttpRequest $httpRequest=null)
setPreviewImageId(int $fileId, int $previewImageId)
getFileData(int $fileId, bool $cacheCleaned=false)
buildRenderByFile($originalName, $contentType, Uri $sourceUri, array $options=[])
attachPreviewToFileId(int $fileId, ?int $previewId, ?int $previewImageId)
sendPreview($file, bool $forceTransformation=false)
getByImage($fileId, Uri $sourceUri)
static getByFilename($filename)
Definition mimetype.php:249