18f5f5da8SGreg Roach<?php 23976b470SGreg Roach 38f5f5da8SGreg Roach/** 48f5f5da8SGreg Roach * webtrees: online genealogy 5*e7f16b43SGreg 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 225c98992aSGreg Roachuse League\Flysystem\Adapter\Local; 2385a166d8SGreg Roachuse League\Flysystem\FileNotFoundException; 245c98992aSGreg Roachuse League\Flysystem\Filesystem; 25a04bb9a2SGreg Roachuse League\Flysystem\FilesystemInterface; 26ee4364daSGreg Roachuse League\Glide\Signatures\SignatureFactory; 273976b470SGreg Roach 28565f3f17SGreg Roachuse function extension_loaded; 2985a166d8SGreg Roachuse function getimagesize; 3085a166d8SGreg Roachuse function intdiv; 3185a166d8SGreg Roachuse function pathinfo; 3285a166d8SGreg Roachuse function strtolower; 333976b470SGreg Roach 3485a166d8SGreg Roachuse const PATHINFO_EXTENSION; 358f5f5da8SGreg Roach 368f5f5da8SGreg Roach/** 378f5f5da8SGreg Roach * A GEDCOM media file. A media object can contain many media files, 388f5f5da8SGreg Roach * such as scans of both sides of a document, the transcript of an audio 398f5f5da8SGreg Roach * recording, etc. 408f5f5da8SGreg Roach */ 41c1010edaSGreg Roachclass MediaFile 42c1010edaSGreg Roach{ 4385a166d8SGreg Roach private const SUPPORTED_IMAGE_MIME_TYPES = [ 4485a166d8SGreg Roach 'image/gif', 4585a166d8SGreg Roach 'image/jpeg', 4685a166d8SGreg Roach 'image/png', 4785a166d8SGreg Roach ]; 4885a166d8SGreg Roach 498f5f5da8SGreg Roach /** @var string The filename */ 508f5f5da8SGreg Roach private $multimedia_file_refn = ''; 518f5f5da8SGreg Roach 528f5f5da8SGreg Roach /** @var string The file extension; jpeg, txt, mp4, etc. */ 538f5f5da8SGreg Roach private $multimedia_format = ''; 548f5f5da8SGreg Roach 558f5f5da8SGreg Roach /** @var string The type of document; newspaper, microfiche, etc. */ 568f5f5da8SGreg Roach private $source_media_type = ''; 578f5f5da8SGreg Roach /** @var string The filename */ 588f5f5da8SGreg Roach 598f5f5da8SGreg Roach /** @var string The name of the document */ 608f5f5da8SGreg Roach private $descriptive_title = ''; 618f5f5da8SGreg Roach 628f5f5da8SGreg Roach /** @var Media $media The media object to which this file belongs */ 638f5f5da8SGreg Roach private $media; 648f5f5da8SGreg Roach 6564b90bf1SGreg Roach /** @var string */ 6664b90bf1SGreg Roach private $fact_id; 6764b90bf1SGreg Roach 688f5f5da8SGreg Roach /** 698f5f5da8SGreg Roach * Create a MediaFile from raw GEDCOM data. 708f5f5da8SGreg Roach * 718f5f5da8SGreg Roach * @param string $gedcom 728f5f5da8SGreg Roach * @param Media $media 738f5f5da8SGreg Roach */ 74c1010edaSGreg Roach public function __construct($gedcom, Media $media) 75c1010edaSGreg Roach { 768f5f5da8SGreg Roach $this->media = $media; 7764b90bf1SGreg Roach $this->fact_id = md5($gedcom); 788f5f5da8SGreg Roach 798f5f5da8SGreg Roach if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) { 808f5f5da8SGreg Roach $this->multimedia_file_refn = $match[1]; 81423c6ccdSGreg Roach $this->multimedia_format = pathinfo($match[1], PATHINFO_EXTENSION); 828f5f5da8SGreg Roach } 838f5f5da8SGreg Roach 848f5f5da8SGreg Roach if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) { 858f5f5da8SGreg Roach $this->multimedia_format = $match[1]; 868f5f5da8SGreg Roach } 878f5f5da8SGreg Roach 888f5f5da8SGreg Roach if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) { 898f5f5da8SGreg Roach $this->source_media_type = $match[1]; 908f5f5da8SGreg Roach } 918f5f5da8SGreg Roach 928f5f5da8SGreg Roach if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) { 938f5f5da8SGreg Roach $this->descriptive_title = $match[1]; 948f5f5da8SGreg Roach } 958f5f5da8SGreg Roach } 968f5f5da8SGreg Roach 978f5f5da8SGreg Roach /** 9864b90bf1SGreg Roach * Get the format. 998f5f5da8SGreg Roach * 1008f5f5da8SGreg Roach * @return string 1018f5f5da8SGreg Roach */ 102c1010edaSGreg Roach public function format(): string 103c1010edaSGreg Roach { 1048f5f5da8SGreg Roach return $this->multimedia_format; 1058f5f5da8SGreg Roach } 1068f5f5da8SGreg Roach 1078f5f5da8SGreg Roach /** 10864b90bf1SGreg Roach * Get the type. 1098f5f5da8SGreg Roach * 1108f5f5da8SGreg Roach * @return string 1118f5f5da8SGreg Roach */ 112c1010edaSGreg Roach public function type(): string 113c1010edaSGreg Roach { 1148f5f5da8SGreg Roach return $this->source_media_type; 1158f5f5da8SGreg Roach } 1168f5f5da8SGreg Roach 1178f5f5da8SGreg Roach /** 11864b90bf1SGreg Roach * Get the title. 1198f5f5da8SGreg Roach * 1208f5f5da8SGreg Roach * @return string 1218f5f5da8SGreg Roach */ 122c1010edaSGreg Roach public function title(): string 123c1010edaSGreg Roach { 1248f5f5da8SGreg Roach return $this->descriptive_title; 1258f5f5da8SGreg Roach } 1268f5f5da8SGreg Roach 1278f5f5da8SGreg Roach /** 12864b90bf1SGreg Roach * Get the fact ID. 12964b90bf1SGreg Roach * 13064b90bf1SGreg Roach * @return string 13164b90bf1SGreg Roach */ 132c1010edaSGreg Roach public function factId(): string 133c1010edaSGreg Roach { 13464b90bf1SGreg Roach return $this->fact_id; 13564b90bf1SGreg Roach } 13664b90bf1SGreg Roach 13764b90bf1SGreg Roach /** 138d6641c58SGreg Roach * @return bool 139d6641c58SGreg Roach */ 1408f53f488SRico Sonntag public function isPendingAddition(): bool 141c1010edaSGreg Roach { 14230158ae7SGreg Roach foreach ($this->media->facts() as $fact) { 143905ab80aSGreg Roach if ($fact->id() === $this->fact_id) { 144d6641c58SGreg Roach return $fact->isPendingAddition(); 145d6641c58SGreg Roach } 146d6641c58SGreg Roach } 147d6641c58SGreg Roach 148d6641c58SGreg Roach return false; 149d6641c58SGreg Roach } 150d6641c58SGreg Roach 151d6641c58SGreg Roach /** 152d6641c58SGreg Roach * @return bool 153d6641c58SGreg Roach */ 1548f53f488SRico Sonntag public function isPendingDeletion(): bool 155c1010edaSGreg Roach { 15630158ae7SGreg Roach foreach ($this->media->facts() as $fact) { 157905ab80aSGreg Roach if ($fact->id() === $this->fact_id) { 158d6641c58SGreg Roach return $fact->isPendingDeletion(); 159d6641c58SGreg Roach } 160d6641c58SGreg Roach } 161d6641c58SGreg Roach 162d6641c58SGreg Roach return false; 163d6641c58SGreg Roach } 164d6641c58SGreg Roach 165d6641c58SGreg Roach /** 16664b90bf1SGreg Roach * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox. 16764b90bf1SGreg Roach * 16864b90bf1SGreg Roach * @param int $width Pixels 16964b90bf1SGreg Roach * @param int $height Pixels 17064b90bf1SGreg Roach * @param string $fit "crop" or "contain" 171e364afe4SGreg Roach * @param string[] $image_attributes Additional HTML attributes 17264b90bf1SGreg Roach * 17364b90bf1SGreg Roach * @return string 17464b90bf1SGreg Roach */ 175e364afe4SGreg Roach public function displayImage($width, $height, $fit, $image_attributes = []): string 176c1010edaSGreg Roach { 17764b90bf1SGreg Roach if ($this->isExternal()) { 17864b90bf1SGreg Roach $src = $this->multimedia_file_refn; 17964b90bf1SGreg Roach $srcset = []; 18064b90bf1SGreg Roach } else { 18164b90bf1SGreg Roach // Generate multiple images for displays with higher pixel densities. 18264b90bf1SGreg Roach $src = $this->imageUrl($width, $height, $fit); 18364b90bf1SGreg Roach $srcset = []; 184bb308685SGreg Roach foreach ([2, 3, 4] as $x) { 18564b90bf1SGreg Roach $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x'; 18664b90bf1SGreg Roach } 18764b90bf1SGreg Roach } 18864b90bf1SGreg Roach 18948b53306SGreg Roach if ($this->isImage()) { 190e364afe4SGreg Roach $image = '<img ' . Html::attributes($image_attributes + [ 19164b90bf1SGreg Roach 'dir' => 'auto', 19264b90bf1SGreg Roach 'src' => $src, 19364b90bf1SGreg Roach 'srcset' => implode(',', $srcset), 194d33f6d5eSGreg Roach 'alt' => strip_tags($this->media->fullName()), 19564b90bf1SGreg Roach ]) . '>'; 19664b90bf1SGreg Roach 197e364afe4SGreg Roach $link_attributes = Html::attributes([ 19864b90bf1SGreg Roach 'class' => 'gallery', 19964b90bf1SGreg Roach 'type' => $this->mimeType(), 20060e3c46aSGreg Roach 'href' => $this->imageUrl(0, 0, 'contain'), 201d33f6d5eSGreg Roach 'data-title' => strip_tags($this->media->fullName()), 20264b90bf1SGreg Roach ]); 203e1d1700bSGreg Roach } else { 20448b53306SGreg Roach $image = view('icons/mime', ['type' => $this->mimeType()]); 205e364afe4SGreg Roach 206e364afe4SGreg Roach $link_attributes = Html::attributes([ 207e1d1700bSGreg Roach 'type' => $this->mimeType(), 20871625badSGreg Roach 'href' => $this->downloadUrl('inline'), 209e1d1700bSGreg Roach ]); 210e1d1700bSGreg Roach } 21164b90bf1SGreg Roach 212e364afe4SGreg Roach return '<a ' . $link_attributes . '>' . $image . '</a>'; 21364b90bf1SGreg Roach } 21464b90bf1SGreg Roach 2154a9f750fSGreg Roach /** 2164a9f750fSGreg Roach * Is the media file actually a URL? 2174a9f750fSGreg Roach */ 218c1010edaSGreg Roach public function isExternal(): bool 219c1010edaSGreg Roach { 2204a9f750fSGreg Roach return strpos($this->multimedia_file_refn, '://') !== false; 2214a9f750fSGreg Roach } 2224a9f750fSGreg Roach 2234a9f750fSGreg Roach /** 2248f5f5da8SGreg Roach * Generate a URL for an image. 2258f5f5da8SGreg Roach * 2268f5f5da8SGreg Roach * @param int $width Maximum width in pixels 2278f5f5da8SGreg Roach * @param int $height Maximum height in pixels 2288f5f5da8SGreg Roach * @param string $fit "crop" or "contain" 2298f5f5da8SGreg Roach * 2308f5f5da8SGreg Roach * @return string 2318f5f5da8SGreg Roach */ 2328f53f488SRico Sonntag public function imageUrl($width, $height, $fit): string 233c1010edaSGreg Roach { 2348f5f5da8SGreg Roach // Sign the URL, to protect against mass-resize attacks. 2358f5f5da8SGreg Roach $glide_key = Site::getPreference('glide-key'); 23654c1ab5eSGreg Roach if ($glide_key === '') { 2378f5f5da8SGreg Roach $glide_key = bin2hex(random_bytes(128)); 2388f5f5da8SGreg Roach Site::setPreference('glide-key', $glide_key); 2398f5f5da8SGreg Roach } 2408f5f5da8SGreg Roach 241f4afa648SGreg Roach if (Auth::accessLevel($this->media->tree()) > $this->media->tree()->getPreference('SHOW_NO_WATERMARK')) { 2428f5f5da8SGreg Roach $mark = 'watermark.png'; 2438f5f5da8SGreg Roach } else { 2448f5f5da8SGreg Roach $mark = ''; 2458f5f5da8SGreg Roach } 2468f5f5da8SGreg Roach 247565f3f17SGreg Roach // Automatic rotation only works when the php-exif library is loaded. 248565f3f17SGreg Roach $orientation = extension_loaded('exif') ? 'or' : 0; 249565f3f17SGreg Roach 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, 2578f5f5da8SGreg Roach 'mark' => $mark, 2588f5f5da8SGreg Roach 'markh' => '100h', 2598f5f5da8SGreg Roach 'markw' => '100w', 260d2ebda77SGreg Roach 'markpos' => 'center', 2618f5f5da8SGreg Roach 'markalpha' => 25, 262565f3f17SGreg Roach 'or' => $orientation, 263ee4364daSGreg Roach ]; 2648f5f5da8SGreg Roach 265ee4364daSGreg Roach $signature = SignatureFactory::create($glide_key)->generateSignature('', $params); 266ee4364daSGreg Roach 267ee4364daSGreg Roach $params = ['route' => '/media-thumbnail', 's' => $signature] + $params; 268ee4364daSGreg Roach 269ee4364daSGreg Roach return route('media-thumbnail', $params); 2708f5f5da8SGreg Roach } 2718f5f5da8SGreg Roach 2728f5f5da8SGreg Roach /** 27385a166d8SGreg Roach * Is the media file an image? 2748f5f5da8SGreg Roach */ 27585a166d8SGreg Roach public function isImage(): bool 276c1010edaSGreg Roach { 27785a166d8SGreg Roach return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true); 2788f5f5da8SGreg Roach } 2798f5f5da8SGreg Roach 2808f5f5da8SGreg Roach /** 2818f5f5da8SGreg Roach * What is the mime-type of this object? 2828f5f5da8SGreg Roach * For simplicity and efficiency, use the extension, rather than the contents. 2838f5f5da8SGreg Roach * 2848f5f5da8SGreg Roach * @return string 2858f5f5da8SGreg Roach */ 2868f53f488SRico Sonntag public function mimeType(): string 287c1010edaSGreg Roach { 28885a166d8SGreg Roach $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION)); 28985a166d8SGreg Roach 290*e7f16b43SGreg Roach return Mime::TYPES[$extension] ?? Mime::DEFAULT_TYPE; 29185a166d8SGreg Roach } 29285a166d8SGreg Roach 29385a166d8SGreg Roach /** 29485a166d8SGreg Roach * Generate a URL to download a non-image media file. 29585a166d8SGreg Roach * 29671625badSGreg Roach * @param string $disposition How should the image be returned - "attachment" or "inline" 29771625badSGreg Roach * 29885a166d8SGreg Roach * @return string 29985a166d8SGreg Roach */ 30071625badSGreg Roach public function downloadUrl(string $disposition): string 30185a166d8SGreg Roach { 30285a166d8SGreg Roach return route('media-download', [ 30385a166d8SGreg Roach 'xref' => $this->media->xref(), 304d72b284aSGreg Roach 'tree' => $this->media->tree()->name(), 30585a166d8SGreg Roach 'fact_id' => $this->fact_id, 30671625badSGreg Roach 'disposition' => $disposition, 30785a166d8SGreg Roach ]); 30885a166d8SGreg Roach } 30985a166d8SGreg Roach 31085a166d8SGreg Roach /** 31185a166d8SGreg Roach * A list of image attributes 31285a166d8SGreg Roach * 3138a3784e1SGreg Roach * @param FilesystemInterface $data_filesystem 3148a3784e1SGreg Roach * 31585a166d8SGreg Roach * @return string[] 31685a166d8SGreg Roach */ 317a04bb9a2SGreg Roach public function attributes(FilesystemInterface $data_filesystem): array 31885a166d8SGreg Roach { 31985a166d8SGreg Roach $attributes = []; 32085a166d8SGreg Roach 321a04bb9a2SGreg Roach if (!$this->isExternal() || $this->fileExists($data_filesystem)) { 32285a166d8SGreg Roach try { 323a04bb9a2SGreg Roach $bytes = $this->media()->tree()->mediaFilesystem($data_filesystem)->getSize($this->filename()); 32485a166d8SGreg Roach $kb = intdiv($bytes + 1023, 1024); 32585a166d8SGreg Roach $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb)); 32685a166d8SGreg Roach } catch (FileNotFoundException $ex) { 32785a166d8SGreg Roach // External/missing files have no size. 32885a166d8SGreg Roach } 32985a166d8SGreg Roach 3305c98992aSGreg Roach // Note: getAdapter() is defined on Filesystem, but not on FilesystemInterface. 331a04bb9a2SGreg Roach $filesystem = $this->media()->tree()->mediaFilesystem($data_filesystem); 3325c98992aSGreg Roach if ($filesystem instanceof Filesystem) { 3335c98992aSGreg Roach $adapter = $filesystem->getAdapter(); 3345c98992aSGreg Roach // Only works for local filesystems. 3355c98992aSGreg Roach if ($adapter instanceof Local) { 3365c98992aSGreg Roach $file = $adapter->applyPathPrefix($this->filename()); 33785a166d8SGreg Roach [$width, $height] = getimagesize($file); 33885a166d8SGreg Roach $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height)); 3395c98992aSGreg Roach } 34085a166d8SGreg Roach } 34185a166d8SGreg Roach } 34285a166d8SGreg Roach 34385a166d8SGreg Roach return $attributes; 34485a166d8SGreg Roach } 34585a166d8SGreg Roach 34685a166d8SGreg Roach /** 34729518ad2SGreg Roach * Read the contents of a media file. 34829518ad2SGreg Roach * 349a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 350a04bb9a2SGreg Roach * 35129518ad2SGreg Roach * @return string 35229518ad2SGreg Roach */ 353a04bb9a2SGreg Roach public function fileContents(FilesystemInterface $data_filesystem): string 35429518ad2SGreg Roach { 355a04bb9a2SGreg Roach return $this->media->tree()->mediaFilesystem($data_filesystem)->read($this->multimedia_file_refn); 35629518ad2SGreg Roach } 35729518ad2SGreg Roach 35829518ad2SGreg Roach /** 35929518ad2SGreg Roach * Check if the file exists on this server 36085a166d8SGreg Roach * 361a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 362a04bb9a2SGreg Roach * 36385a166d8SGreg Roach * @return bool 36485a166d8SGreg Roach */ 365a04bb9a2SGreg Roach public function fileExists(FilesystemInterface $data_filesystem): bool 36685a166d8SGreg Roach { 367a04bb9a2SGreg Roach return $this->media->tree()->mediaFilesystem($data_filesystem)->has($this->multimedia_file_refn); 36885a166d8SGreg Roach } 36985a166d8SGreg Roach 37085a166d8SGreg Roach /** 37185a166d8SGreg Roach * @return Media 37285a166d8SGreg Roach */ 37385a166d8SGreg Roach public function media(): Media 37485a166d8SGreg Roach { 37585a166d8SGreg Roach return $this->media; 37685a166d8SGreg Roach } 37785a166d8SGreg Roach 37885a166d8SGreg Roach /** 37985a166d8SGreg Roach * Get the filename. 38085a166d8SGreg Roach * 38185a166d8SGreg Roach * @return string 38285a166d8SGreg Roach */ 38385a166d8SGreg Roach public function filename(): string 38485a166d8SGreg Roach { 38585a166d8SGreg Roach return $this->multimedia_file_refn; 38685a166d8SGreg Roach } 38785a166d8SGreg Roach 38885a166d8SGreg Roach /** 38985a166d8SGreg Roach * What file extension is used by this file? 39085a166d8SGreg Roach * 39185a166d8SGreg Roach * @return string 392*e7f16b43SGreg Roach * 393*e7f16b43SGreg Roach * @deprecated since 2.0.4. Will be removed in 2.1.0 39485a166d8SGreg Roach */ 39585a166d8SGreg Roach public function extension(): string 39685a166d8SGreg Roach { 39785a166d8SGreg Roach return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION); 3988f5f5da8SGreg Roach } 3998f5f5da8SGreg Roach} 400