18f5f5da8SGreg Roach<?php 23976b470SGreg Roach 38f5f5da8SGreg Roach/** 48f5f5da8SGreg Roach * webtrees: online genealogy 5e7f16b43SGreg Roach * Copyright (C) 2020 webtrees development team 68f5f5da8SGreg Roach * This program is free software: you can redistribute it and/or modify 78f5f5da8SGreg Roach * it under the terms of the GNU General Public License as published by 88f5f5da8SGreg Roach * the Free Software Foundation, either version 3 of the License, or 98f5f5da8SGreg Roach * (at your option) any later version. 108f5f5da8SGreg Roach * This program is distributed in the hope that it will be useful, 118f5f5da8SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 128f5f5da8SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 138f5f5da8SGreg Roach * GNU General Public License for more details. 148f5f5da8SGreg Roach * You should have received a copy of the GNU General Public License 158f5f5da8SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>. 168f5f5da8SGreg Roach */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 208f5f5da8SGreg Roachnamespace Fisharebest\Webtrees; 218f5f5da8SGreg Roach 2246b03695SGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\MediaFileDownload; 2346b03695SGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\MediaFileThumbnail; 245c98992aSGreg Roachuse League\Flysystem\Adapter\Local; 2585a166d8SGreg Roachuse League\Flysystem\FileNotFoundException; 265c98992aSGreg Roachuse League\Flysystem\Filesystem; 27a04bb9a2SGreg Roachuse League\Flysystem\FilesystemInterface; 283976b470SGreg Roach 296577bfc3SGreg Roachuse function bin2hex; 3085a166d8SGreg Roachuse function getimagesize; 316577bfc3SGreg Roachuse function http_build_query; 3285a166d8SGreg Roachuse function intdiv; 336577bfc3SGreg Roachuse function ksort; 346577bfc3SGreg Roachuse function md5; 3585a166d8SGreg Roachuse function pathinfo; 366577bfc3SGreg Roachuse function random_bytes; 37dec352c1SGreg Roachuse function str_contains; 3885a166d8SGreg Roachuse function strtolower; 393976b470SGreg Roach 4085a166d8SGreg Roachuse const PATHINFO_EXTENSION; 418f5f5da8SGreg Roach 428f5f5da8SGreg Roach/** 438f5f5da8SGreg Roach * A GEDCOM media file. A media object can contain many media files, 448f5f5da8SGreg Roach * such as scans of both sides of a document, the transcript of an audio 458f5f5da8SGreg Roach * recording, etc. 468f5f5da8SGreg Roach */ 47c1010edaSGreg Roachclass MediaFile 48c1010edaSGreg Roach{ 4985a166d8SGreg Roach private const SUPPORTED_IMAGE_MIME_TYPES = [ 5085a166d8SGreg Roach 'image/gif', 5185a166d8SGreg Roach 'image/jpeg', 5285a166d8SGreg Roach 'image/png', 53*c68bc8e2SGreg Roach 'image/webp', 5485a166d8SGreg Roach ]; 5585a166d8SGreg Roach 568f5f5da8SGreg Roach /** @var string The filename */ 578f5f5da8SGreg Roach private $multimedia_file_refn = ''; 588f5f5da8SGreg Roach 598f5f5da8SGreg Roach /** @var string The file extension; jpeg, txt, mp4, etc. */ 608f5f5da8SGreg Roach private $multimedia_format = ''; 618f5f5da8SGreg Roach 628f5f5da8SGreg Roach /** @var string The type of document; newspaper, microfiche, etc. */ 638f5f5da8SGreg Roach private $source_media_type = ''; 648f5f5da8SGreg Roach /** @var string The filename */ 658f5f5da8SGreg Roach 668f5f5da8SGreg Roach /** @var string The name of the document */ 678f5f5da8SGreg Roach private $descriptive_title = ''; 688f5f5da8SGreg Roach 698f5f5da8SGreg Roach /** @var Media $media The media object to which this file belongs */ 708f5f5da8SGreg Roach private $media; 718f5f5da8SGreg Roach 7264b90bf1SGreg Roach /** @var string */ 7364b90bf1SGreg Roach private $fact_id; 7464b90bf1SGreg Roach 758f5f5da8SGreg Roach /** 768f5f5da8SGreg Roach * Create a MediaFile from raw GEDCOM data. 778f5f5da8SGreg Roach * 788f5f5da8SGreg Roach * @param string $gedcom 798f5f5da8SGreg Roach * @param Media $media 808f5f5da8SGreg Roach */ 81c1010edaSGreg Roach public function __construct($gedcom, Media $media) 82c1010edaSGreg Roach { 838f5f5da8SGreg Roach $this->media = $media; 8464b90bf1SGreg Roach $this->fact_id = md5($gedcom); 858f5f5da8SGreg Roach 868f5f5da8SGreg Roach if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) { 878f5f5da8SGreg Roach $this->multimedia_file_refn = $match[1]; 88423c6ccdSGreg Roach $this->multimedia_format = pathinfo($match[1], PATHINFO_EXTENSION); 898f5f5da8SGreg Roach } 908f5f5da8SGreg Roach 918f5f5da8SGreg Roach if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) { 928f5f5da8SGreg Roach $this->multimedia_format = $match[1]; 938f5f5da8SGreg Roach } 948f5f5da8SGreg Roach 958f5f5da8SGreg Roach if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) { 968f5f5da8SGreg Roach $this->source_media_type = $match[1]; 978f5f5da8SGreg Roach } 988f5f5da8SGreg Roach 998f5f5da8SGreg Roach if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) { 1008f5f5da8SGreg Roach $this->descriptive_title = $match[1]; 1018f5f5da8SGreg Roach } 1028f5f5da8SGreg Roach } 1038f5f5da8SGreg Roach 1048f5f5da8SGreg Roach /** 10564b90bf1SGreg Roach * Get the format. 1068f5f5da8SGreg Roach * 1078f5f5da8SGreg Roach * @return string 1088f5f5da8SGreg Roach */ 109c1010edaSGreg Roach public function format(): string 110c1010edaSGreg Roach { 1118f5f5da8SGreg Roach return $this->multimedia_format; 1128f5f5da8SGreg Roach } 1138f5f5da8SGreg Roach 1148f5f5da8SGreg Roach /** 11564b90bf1SGreg Roach * Get the type. 1168f5f5da8SGreg Roach * 1178f5f5da8SGreg Roach * @return string 1188f5f5da8SGreg Roach */ 119c1010edaSGreg Roach public function type(): string 120c1010edaSGreg Roach { 1218f5f5da8SGreg Roach return $this->source_media_type; 1228f5f5da8SGreg Roach } 1238f5f5da8SGreg Roach 1248f5f5da8SGreg Roach /** 12564b90bf1SGreg Roach * Get the title. 1268f5f5da8SGreg Roach * 1278f5f5da8SGreg Roach * @return string 1288f5f5da8SGreg Roach */ 129c1010edaSGreg Roach public function title(): string 130c1010edaSGreg Roach { 1318f5f5da8SGreg Roach return $this->descriptive_title; 1328f5f5da8SGreg Roach } 1338f5f5da8SGreg Roach 1348f5f5da8SGreg Roach /** 13564b90bf1SGreg Roach * Get the fact ID. 13664b90bf1SGreg Roach * 13764b90bf1SGreg Roach * @return string 13864b90bf1SGreg Roach */ 139c1010edaSGreg Roach public function factId(): string 140c1010edaSGreg Roach { 14164b90bf1SGreg Roach return $this->fact_id; 14264b90bf1SGreg Roach } 14364b90bf1SGreg Roach 14464b90bf1SGreg Roach /** 145d6641c58SGreg Roach * @return bool 146d6641c58SGreg Roach */ 1478f53f488SRico Sonntag public function isPendingAddition(): bool 148c1010edaSGreg Roach { 14930158ae7SGreg Roach foreach ($this->media->facts() as $fact) { 150905ab80aSGreg Roach if ($fact->id() === $this->fact_id) { 151d6641c58SGreg Roach return $fact->isPendingAddition(); 152d6641c58SGreg Roach } 153d6641c58SGreg Roach } 154d6641c58SGreg Roach 155d6641c58SGreg Roach return false; 156d6641c58SGreg Roach } 157d6641c58SGreg Roach 158d6641c58SGreg Roach /** 159d6641c58SGreg Roach * @return bool 160d6641c58SGreg Roach */ 1618f53f488SRico Sonntag public function isPendingDeletion(): bool 162c1010edaSGreg Roach { 16330158ae7SGreg Roach foreach ($this->media->facts() as $fact) { 164905ab80aSGreg Roach if ($fact->id() === $this->fact_id) { 165d6641c58SGreg Roach return $fact->isPendingDeletion(); 166d6641c58SGreg Roach } 167d6641c58SGreg Roach } 168d6641c58SGreg Roach 169d6641c58SGreg Roach return false; 170d6641c58SGreg Roach } 171d6641c58SGreg Roach 172d6641c58SGreg Roach /** 17364b90bf1SGreg Roach * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox. 17464b90bf1SGreg Roach * 17564b90bf1SGreg Roach * @param int $width Pixels 17664b90bf1SGreg Roach * @param int $height Pixels 17764b90bf1SGreg Roach * @param string $fit "crop" or "contain" 178e364afe4SGreg Roach * @param string[] $image_attributes Additional HTML attributes 17964b90bf1SGreg Roach * 18064b90bf1SGreg Roach * @return string 18164b90bf1SGreg Roach */ 182e364afe4SGreg Roach public function displayImage($width, $height, $fit, $image_attributes = []): string 183c1010edaSGreg Roach { 18464b90bf1SGreg Roach if ($this->isExternal()) { 18564b90bf1SGreg Roach $src = $this->multimedia_file_refn; 18664b90bf1SGreg Roach $srcset = []; 18764b90bf1SGreg Roach } else { 18864b90bf1SGreg Roach // Generate multiple images for displays with higher pixel densities. 18964b90bf1SGreg Roach $src = $this->imageUrl($width, $height, $fit); 19064b90bf1SGreg Roach $srcset = []; 191bb308685SGreg Roach foreach ([2, 3, 4] as $x) { 19264b90bf1SGreg Roach $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x'; 19364b90bf1SGreg Roach } 19464b90bf1SGreg Roach } 19564b90bf1SGreg Roach 19648b53306SGreg Roach if ($this->isImage()) { 197e364afe4SGreg Roach $image = '<img ' . Html::attributes($image_attributes + [ 19864b90bf1SGreg Roach 'dir' => 'auto', 19964b90bf1SGreg Roach 'src' => $src, 20064b90bf1SGreg Roach 'srcset' => implode(',', $srcset), 201d33f6d5eSGreg Roach 'alt' => strip_tags($this->media->fullName()), 20264b90bf1SGreg Roach ]) . '>'; 20364b90bf1SGreg Roach 204e364afe4SGreg Roach $link_attributes = Html::attributes([ 20564b90bf1SGreg Roach 'class' => 'gallery', 20664b90bf1SGreg Roach 'type' => $this->mimeType(), 2076577bfc3SGreg Roach 'href' => $this->downloadUrl('inline'), 208d33f6d5eSGreg Roach 'data-title' => strip_tags($this->media->fullName()), 20964b90bf1SGreg Roach ]); 210e1d1700bSGreg Roach } else { 21148b53306SGreg Roach $image = view('icons/mime', ['type' => $this->mimeType()]); 212e364afe4SGreg Roach 213e364afe4SGreg Roach $link_attributes = Html::attributes([ 214e1d1700bSGreg Roach 'type' => $this->mimeType(), 21571625badSGreg Roach 'href' => $this->downloadUrl('inline'), 216e1d1700bSGreg Roach ]); 217e1d1700bSGreg Roach } 21864b90bf1SGreg Roach 219e364afe4SGreg Roach return '<a ' . $link_attributes . '>' . $image . '</a>'; 22064b90bf1SGreg Roach } 22164b90bf1SGreg Roach 2224a9f750fSGreg Roach /** 2234a9f750fSGreg Roach * Is the media file actually a URL? 2244a9f750fSGreg Roach */ 225c1010edaSGreg Roach public function isExternal(): bool 226c1010edaSGreg Roach { 227dec352c1SGreg Roach return str_contains($this->multimedia_file_refn, '://'); 2284a9f750fSGreg Roach } 2294a9f750fSGreg Roach 2304a9f750fSGreg Roach /** 2318f5f5da8SGreg Roach * Generate a URL for an image. 2328f5f5da8SGreg Roach * 2338f5f5da8SGreg Roach * @param int $width Maximum width in pixels 2348f5f5da8SGreg Roach * @param int $height Maximum height in pixels 2358f5f5da8SGreg Roach * @param string $fit "crop" or "contain" 2368f5f5da8SGreg Roach * 2378f5f5da8SGreg Roach * @return string 2388f5f5da8SGreg Roach */ 2398f53f488SRico Sonntag public function imageUrl($width, $height, $fit): string 240c1010edaSGreg Roach { 2418f5f5da8SGreg Roach // Sign the URL, to protect against mass-resize attacks. 2428f5f5da8SGreg Roach $glide_key = Site::getPreference('glide-key'); 24346b03695SGreg Roach 24454c1ab5eSGreg Roach if ($glide_key === '') { 2458f5f5da8SGreg Roach $glide_key = bin2hex(random_bytes(128)); 2468f5f5da8SGreg Roach Site::setPreference('glide-key', $glide_key); 2478f5f5da8SGreg Roach } 2488f5f5da8SGreg Roach 2496577bfc3SGreg Roach // The "mark" parameter is ignored, but needed for cache-busting. 250ee4364daSGreg Roach $params = [ 251c0935879SGreg Roach 'xref' => $this->media->xref(), 252d72b284aSGreg Roach 'tree' => $this->media->tree()->name(), 2534a9f750fSGreg Roach 'fact_id' => $this->fact_id, 2548f5f5da8SGreg Roach 'w' => $width, 2558f5f5da8SGreg Roach 'h' => $height, 2568f5f5da8SGreg Roach 'fit' => $fit, 2576b9cb339SGreg Roach 'mark' => Registry::imageFactory()->thumbnailNeedsWatermark($this, Auth::user()) 25846b03695SGreg Roach ]; 25946b03695SGreg Roach 2606577bfc3SGreg Roach $params['s'] = $this->signature($params); 261ee4364daSGreg Roach 26246b03695SGreg Roach return route(MediaFileThumbnail::class, $params); 2638f5f5da8SGreg Roach } 2648f5f5da8SGreg Roach 2658f5f5da8SGreg Roach /** 26685a166d8SGreg Roach * Is the media file an image? 2678f5f5da8SGreg Roach */ 26885a166d8SGreg Roach public function isImage(): bool 269c1010edaSGreg Roach { 27085a166d8SGreg Roach return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true); 2718f5f5da8SGreg Roach } 2728f5f5da8SGreg Roach 2738f5f5da8SGreg Roach /** 2748f5f5da8SGreg Roach * What is the mime-type of this object? 2758f5f5da8SGreg Roach * For simplicity and efficiency, use the extension, rather than the contents. 2768f5f5da8SGreg Roach * 2778f5f5da8SGreg Roach * @return string 2788f5f5da8SGreg Roach */ 2798f53f488SRico Sonntag public function mimeType(): string 280c1010edaSGreg Roach { 28185a166d8SGreg Roach $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION)); 28285a166d8SGreg Roach 283e7f16b43SGreg Roach return Mime::TYPES[$extension] ?? Mime::DEFAULT_TYPE; 28485a166d8SGreg Roach } 28585a166d8SGreg Roach 28685a166d8SGreg Roach /** 2876577bfc3SGreg Roach * Generate a URL to download a media file. 28885a166d8SGreg Roach * 28971625badSGreg Roach * @param string $disposition How should the image be returned - "attachment" or "inline" 29071625badSGreg Roach * 29185a166d8SGreg Roach * @return string 29285a166d8SGreg Roach */ 29371625badSGreg Roach public function downloadUrl(string $disposition): string 29485a166d8SGreg Roach { 2956577bfc3SGreg Roach // The "mark" parameter is ignored, but needed for cache-busting. 29646b03695SGreg Roach return route(MediaFileDownload::class, [ 29785a166d8SGreg Roach 'xref' => $this->media->xref(), 298d72b284aSGreg Roach 'tree' => $this->media->tree()->name(), 29985a166d8SGreg Roach 'fact_id' => $this->fact_id, 30071625badSGreg Roach 'disposition' => $disposition, 3016b9cb339SGreg Roach 'mark' => Registry::imageFactory()->fileNeedsWatermark($this, Auth::user()) 30285a166d8SGreg Roach ]); 30385a166d8SGreg Roach } 30485a166d8SGreg Roach 30585a166d8SGreg Roach /** 30685a166d8SGreg Roach * A list of image attributes 30785a166d8SGreg Roach * 3088a3784e1SGreg Roach * @param FilesystemInterface $data_filesystem 3098a3784e1SGreg Roach * 31085a166d8SGreg Roach * @return string[] 31185a166d8SGreg Roach */ 312a04bb9a2SGreg Roach public function attributes(FilesystemInterface $data_filesystem): array 31385a166d8SGreg Roach { 31485a166d8SGreg Roach $attributes = []; 31585a166d8SGreg Roach 316a04bb9a2SGreg Roach if (!$this->isExternal() || $this->fileExists($data_filesystem)) { 31785a166d8SGreg Roach try { 318a04bb9a2SGreg Roach $bytes = $this->media()->tree()->mediaFilesystem($data_filesystem)->getSize($this->filename()); 31985a166d8SGreg Roach $kb = intdiv($bytes + 1023, 1024); 32085a166d8SGreg Roach $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb)); 32185a166d8SGreg Roach } catch (FileNotFoundException $ex) { 32285a166d8SGreg Roach // External/missing files have no size. 32385a166d8SGreg Roach } 32485a166d8SGreg Roach 3255c98992aSGreg Roach // Note: getAdapter() is defined on Filesystem, but not on FilesystemInterface. 326a04bb9a2SGreg Roach $filesystem = $this->media()->tree()->mediaFilesystem($data_filesystem); 3275c98992aSGreg Roach if ($filesystem instanceof Filesystem) { 3285c98992aSGreg Roach $adapter = $filesystem->getAdapter(); 3295c98992aSGreg Roach // Only works for local filesystems. 3305c98992aSGreg Roach if ($adapter instanceof Local) { 3315c98992aSGreg Roach $file = $adapter->applyPathPrefix($this->filename()); 33285a166d8SGreg Roach [$width, $height] = getimagesize($file); 33385a166d8SGreg Roach $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height)); 3345c98992aSGreg Roach } 33585a166d8SGreg Roach } 33685a166d8SGreg Roach } 33785a166d8SGreg Roach 33885a166d8SGreg Roach return $attributes; 33985a166d8SGreg Roach } 34085a166d8SGreg Roach 34185a166d8SGreg Roach /** 34229518ad2SGreg Roach * Read the contents of a media file. 34329518ad2SGreg Roach * 344a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 345a04bb9a2SGreg Roach * 34629518ad2SGreg Roach * @return string 34729518ad2SGreg Roach */ 348a04bb9a2SGreg Roach public function fileContents(FilesystemInterface $data_filesystem): string 34929518ad2SGreg Roach { 350a04bb9a2SGreg Roach return $this->media->tree()->mediaFilesystem($data_filesystem)->read($this->multimedia_file_refn); 35129518ad2SGreg Roach } 35229518ad2SGreg Roach 35329518ad2SGreg Roach /** 35429518ad2SGreg Roach * Check if the file exists on this server 35585a166d8SGreg Roach * 356a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 357a04bb9a2SGreg Roach * 35885a166d8SGreg Roach * @return bool 35985a166d8SGreg Roach */ 360a04bb9a2SGreg Roach public function fileExists(FilesystemInterface $data_filesystem): bool 36185a166d8SGreg Roach { 362a04bb9a2SGreg Roach return $this->media->tree()->mediaFilesystem($data_filesystem)->has($this->multimedia_file_refn); 36385a166d8SGreg Roach } 36485a166d8SGreg Roach 36585a166d8SGreg Roach /** 36685a166d8SGreg Roach * @return Media 36785a166d8SGreg Roach */ 36885a166d8SGreg Roach public function media(): Media 36985a166d8SGreg Roach { 37085a166d8SGreg Roach return $this->media; 37185a166d8SGreg Roach } 37285a166d8SGreg Roach 37385a166d8SGreg Roach /** 37485a166d8SGreg Roach * Get the filename. 37585a166d8SGreg Roach * 37685a166d8SGreg Roach * @return string 37785a166d8SGreg Roach */ 37885a166d8SGreg Roach public function filename(): string 37985a166d8SGreg Roach { 38085a166d8SGreg Roach return $this->multimedia_file_refn; 38185a166d8SGreg Roach } 38285a166d8SGreg Roach 38385a166d8SGreg Roach /** 38485a166d8SGreg Roach * What file extension is used by this file? 38585a166d8SGreg Roach * 38685a166d8SGreg Roach * @return string 387e7f16b43SGreg Roach * 388e7f16b43SGreg Roach * @deprecated since 2.0.4. Will be removed in 2.1.0 38985a166d8SGreg Roach */ 39085a166d8SGreg Roach public function extension(): string 39185a166d8SGreg Roach { 39285a166d8SGreg Roach return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION); 3938f5f5da8SGreg Roach } 3946577bfc3SGreg Roach 3956577bfc3SGreg Roach /** 396fceda430SGreg Roach * Create a URL signature parameter, using the same algorithm as league/glide, 3976577bfc3SGreg Roach * for compatibility with URLs generated by older versions of webtrees. 3986577bfc3SGreg Roach * 3996577bfc3SGreg Roach * @param array<mixed> $params 4006577bfc3SGreg Roach * 4016577bfc3SGreg Roach * @return string 4026577bfc3SGreg Roach */ 4036577bfc3SGreg Roach public function signature(array $params): string 4046577bfc3SGreg Roach { 4056577bfc3SGreg Roach unset($params['s']); 4066577bfc3SGreg Roach 4076577bfc3SGreg Roach ksort($params); 4086577bfc3SGreg Roach 4096577bfc3SGreg Roach // Sign the URL, to protect against mass-resize attacks. 4106577bfc3SGreg Roach $glide_key = Site::getPreference('glide-key'); 4116577bfc3SGreg Roach 4126577bfc3SGreg Roach if ($glide_key === '') { 4136577bfc3SGreg Roach $glide_key = bin2hex(random_bytes(128)); 4146577bfc3SGreg Roach Site::setPreference('glide-key', $glide_key); 4156577bfc3SGreg Roach } 4166577bfc3SGreg Roach 4176577bfc3SGreg Roach return md5($glide_key . ':?' . http_build_query($params)); 4186577bfc3SGreg Roach } 4198f5f5da8SGreg Roach} 420