18f5f5da8SGreg Roach<?php 23976b470SGreg Roach 38f5f5da8SGreg Roach/** 48f5f5da8SGreg Roach * webtrees: online genealogy 58fcd0d32SGreg Roach * Copyright (C) 2019 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 2885a166d8SGreg Roachuse function getimagesize; 2985a166d8SGreg Roachuse function intdiv; 3085a166d8SGreg Roachuse function pathinfo; 3185a166d8SGreg Roachuse function strtolower; 323976b470SGreg Roach 3385a166d8SGreg Roachuse const PATHINFO_EXTENSION; 348f5f5da8SGreg Roach 358f5f5da8SGreg Roach/** 368f5f5da8SGreg Roach * A GEDCOM media file. A media object can contain many media files, 378f5f5da8SGreg Roach * such as scans of both sides of a document, the transcript of an audio 388f5f5da8SGreg Roach * recording, etc. 398f5f5da8SGreg Roach */ 40c1010edaSGreg Roachclass MediaFile 41c1010edaSGreg Roach{ 4216d6367aSGreg Roach private const MIME_TYPES = [ 435225fdfcSGreg Roach 'bmp' => 'image/bmp', 445225fdfcSGreg Roach 'doc' => 'application/msword', 455225fdfcSGreg Roach 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 465225fdfcSGreg Roach 'ged' => 'text/x-gedcom', 475225fdfcSGreg Roach 'gif' => 'image/gif', 485225fdfcSGreg Roach 'html' => 'text/html', 495225fdfcSGreg Roach 'htm' => 'text/html', 50*e5dda0aeSGreg Roach 'jpe' => 'image/jpeg', 515225fdfcSGreg Roach 'jpeg' => 'image/jpeg', 525225fdfcSGreg Roach 'jpg' => 'image/jpeg', 535225fdfcSGreg Roach 'mov' => 'video/quicktime', 545225fdfcSGreg Roach 'mp3' => 'audio/mpeg', 555225fdfcSGreg Roach 'mp4' => 'video/mp4', 565225fdfcSGreg Roach 'ogv' => 'video/ogg', 575225fdfcSGreg Roach 'pdf' => 'application/pdf', 585225fdfcSGreg Roach 'png' => 'image/png', 595225fdfcSGreg Roach 'rar' => 'application/x-rar-compressed', 605225fdfcSGreg Roach 'swf' => 'application/x-shockwave-flash', 615225fdfcSGreg Roach 'svg' => 'image/svg', 625225fdfcSGreg Roach 'tiff' => 'image/tiff', 635225fdfcSGreg Roach 'tif' => 'image/tiff', 645225fdfcSGreg Roach 'xls' => 'application/vnd-ms-excel', 655225fdfcSGreg Roach 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 665225fdfcSGreg Roach 'wmv' => 'video/x-ms-wmv', 675225fdfcSGreg Roach 'zip' => 'application/zip', 685225fdfcSGreg Roach ]; 695225fdfcSGreg Roach 7085a166d8SGreg Roach private const SUPPORTED_IMAGE_MIME_TYPES = [ 7185a166d8SGreg Roach 'image/gif', 7285a166d8SGreg Roach 'image/jpeg', 7385a166d8SGreg Roach 'image/png', 7485a166d8SGreg Roach ]; 7585a166d8SGreg Roach 768f5f5da8SGreg Roach /** @var string The filename */ 778f5f5da8SGreg Roach private $multimedia_file_refn = ''; 788f5f5da8SGreg Roach 798f5f5da8SGreg Roach /** @var string The file extension; jpeg, txt, mp4, etc. */ 808f5f5da8SGreg Roach private $multimedia_format = ''; 818f5f5da8SGreg Roach 828f5f5da8SGreg Roach /** @var string The type of document; newspaper, microfiche, etc. */ 838f5f5da8SGreg Roach private $source_media_type = ''; 848f5f5da8SGreg Roach /** @var string The filename */ 858f5f5da8SGreg Roach 868f5f5da8SGreg Roach /** @var string The name of the document */ 878f5f5da8SGreg Roach private $descriptive_title = ''; 888f5f5da8SGreg Roach 898f5f5da8SGreg Roach /** @var Media $media The media object to which this file belongs */ 908f5f5da8SGreg Roach private $media; 918f5f5da8SGreg Roach 9264b90bf1SGreg Roach /** @var string */ 9364b90bf1SGreg Roach private $fact_id; 9464b90bf1SGreg Roach 958f5f5da8SGreg Roach /** 968f5f5da8SGreg Roach * Create a MediaFile from raw GEDCOM data. 978f5f5da8SGreg Roach * 988f5f5da8SGreg Roach * @param string $gedcom 998f5f5da8SGreg Roach * @param Media $media 1008f5f5da8SGreg Roach */ 101c1010edaSGreg Roach public function __construct($gedcom, Media $media) 102c1010edaSGreg Roach { 1038f5f5da8SGreg Roach $this->media = $media; 10464b90bf1SGreg Roach $this->fact_id = md5($gedcom); 1058f5f5da8SGreg Roach 1068f5f5da8SGreg Roach if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) { 1078f5f5da8SGreg Roach $this->multimedia_file_refn = $match[1]; 108423c6ccdSGreg Roach $this->multimedia_format = pathinfo($match[1], PATHINFO_EXTENSION); 1098f5f5da8SGreg Roach } 1108f5f5da8SGreg Roach 1118f5f5da8SGreg Roach if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) { 1128f5f5da8SGreg Roach $this->multimedia_format = $match[1]; 1138f5f5da8SGreg Roach } 1148f5f5da8SGreg Roach 1158f5f5da8SGreg Roach if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) { 1168f5f5da8SGreg Roach $this->source_media_type = $match[1]; 1178f5f5da8SGreg Roach } 1188f5f5da8SGreg Roach 1198f5f5da8SGreg Roach if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) { 1208f5f5da8SGreg Roach $this->descriptive_title = $match[1]; 1218f5f5da8SGreg Roach } 1228f5f5da8SGreg Roach } 1238f5f5da8SGreg Roach 1248f5f5da8SGreg Roach /** 12564b90bf1SGreg Roach * Get the format. 1268f5f5da8SGreg Roach * 1278f5f5da8SGreg Roach * @return string 1288f5f5da8SGreg Roach */ 129c1010edaSGreg Roach public function format(): string 130c1010edaSGreg Roach { 1318f5f5da8SGreg Roach return $this->multimedia_format; 1328f5f5da8SGreg Roach } 1338f5f5da8SGreg Roach 1348f5f5da8SGreg Roach /** 13564b90bf1SGreg Roach * Get the type. 1368f5f5da8SGreg Roach * 1378f5f5da8SGreg Roach * @return string 1388f5f5da8SGreg Roach */ 139c1010edaSGreg Roach public function type(): string 140c1010edaSGreg Roach { 1418f5f5da8SGreg Roach return $this->source_media_type; 1428f5f5da8SGreg Roach } 1438f5f5da8SGreg Roach 1448f5f5da8SGreg Roach /** 14564b90bf1SGreg Roach * Get the title. 1468f5f5da8SGreg Roach * 1478f5f5da8SGreg Roach * @return string 1488f5f5da8SGreg Roach */ 149c1010edaSGreg Roach public function title(): string 150c1010edaSGreg Roach { 1518f5f5da8SGreg Roach return $this->descriptive_title; 1528f5f5da8SGreg Roach } 1538f5f5da8SGreg Roach 1548f5f5da8SGreg Roach /** 15564b90bf1SGreg Roach * Get the fact ID. 15664b90bf1SGreg Roach * 15764b90bf1SGreg Roach * @return string 15864b90bf1SGreg Roach */ 159c1010edaSGreg Roach public function factId(): string 160c1010edaSGreg Roach { 16164b90bf1SGreg Roach return $this->fact_id; 16264b90bf1SGreg Roach } 16364b90bf1SGreg Roach 16464b90bf1SGreg Roach /** 165d6641c58SGreg Roach * @return bool 166d6641c58SGreg Roach */ 1678f53f488SRico Sonntag public function isPendingAddition(): bool 168c1010edaSGreg Roach { 16930158ae7SGreg Roach foreach ($this->media->facts() as $fact) { 170905ab80aSGreg Roach if ($fact->id() === $this->fact_id) { 171d6641c58SGreg Roach return $fact->isPendingAddition(); 172d6641c58SGreg Roach } 173d6641c58SGreg Roach } 174d6641c58SGreg Roach 175d6641c58SGreg Roach return false; 176d6641c58SGreg Roach } 177d6641c58SGreg Roach 178d6641c58SGreg Roach /** 179d6641c58SGreg Roach * @return bool 180d6641c58SGreg Roach */ 1818f53f488SRico Sonntag public function isPendingDeletion(): bool 182c1010edaSGreg Roach { 18330158ae7SGreg Roach foreach ($this->media->facts() as $fact) { 184905ab80aSGreg Roach if ($fact->id() === $this->fact_id) { 185d6641c58SGreg Roach return $fact->isPendingDeletion(); 186d6641c58SGreg Roach } 187d6641c58SGreg Roach } 188d6641c58SGreg Roach 189d6641c58SGreg Roach return false; 190d6641c58SGreg Roach } 191d6641c58SGreg Roach 192d6641c58SGreg Roach /** 19364b90bf1SGreg Roach * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox. 19464b90bf1SGreg Roach * 19564b90bf1SGreg Roach * @param int $width Pixels 19664b90bf1SGreg Roach * @param int $height Pixels 19764b90bf1SGreg Roach * @param string $fit "crop" or "contain" 198e364afe4SGreg Roach * @param string[] $image_attributes Additional HTML attributes 19964b90bf1SGreg Roach * 20064b90bf1SGreg Roach * @return string 20164b90bf1SGreg Roach */ 202e364afe4SGreg Roach public function displayImage($width, $height, $fit, $image_attributes = []): string 203c1010edaSGreg Roach { 20464b90bf1SGreg Roach if ($this->isExternal()) { 20564b90bf1SGreg Roach $src = $this->multimedia_file_refn; 20664b90bf1SGreg Roach $srcset = []; 20764b90bf1SGreg Roach } else { 20864b90bf1SGreg Roach // Generate multiple images for displays with higher pixel densities. 20964b90bf1SGreg Roach $src = $this->imageUrl($width, $height, $fit); 21064b90bf1SGreg Roach $srcset = []; 211bb308685SGreg Roach foreach ([2, 3, 4] as $x) { 21264b90bf1SGreg Roach $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x'; 21364b90bf1SGreg Roach } 21464b90bf1SGreg Roach } 21564b90bf1SGreg Roach 21648b53306SGreg Roach if ($this->isImage()) { 217e364afe4SGreg Roach $image = '<img ' . Html::attributes($image_attributes + [ 21864b90bf1SGreg Roach 'dir' => 'auto', 21964b90bf1SGreg Roach 'src' => $src, 22064b90bf1SGreg Roach 'srcset' => implode(',', $srcset), 221d33f6d5eSGreg Roach 'alt' => strip_tags($this->media->fullName()), 22264b90bf1SGreg Roach ]) . '>'; 22364b90bf1SGreg Roach 224e364afe4SGreg Roach $link_attributes = Html::attributes([ 22564b90bf1SGreg Roach 'class' => 'gallery', 22664b90bf1SGreg Roach 'type' => $this->mimeType(), 22760e3c46aSGreg Roach 'href' => $this->imageUrl(0, 0, 'contain'), 228d33f6d5eSGreg Roach 'data-title' => strip_tags($this->media->fullName()), 22964b90bf1SGreg Roach ]); 230e1d1700bSGreg Roach } else { 23148b53306SGreg Roach $image = view('icons/mime', ['type' => $this->mimeType()]); 232e364afe4SGreg Roach 233e364afe4SGreg Roach $link_attributes = Html::attributes([ 234e1d1700bSGreg Roach 'type' => $this->mimeType(), 23571625badSGreg Roach 'href' => $this->downloadUrl('inline'), 236e1d1700bSGreg Roach ]); 237e1d1700bSGreg Roach } 23864b90bf1SGreg Roach 239e364afe4SGreg Roach return '<a ' . $link_attributes . '>' . $image . '</a>'; 24064b90bf1SGreg Roach } 24164b90bf1SGreg Roach 2424a9f750fSGreg Roach /** 2434a9f750fSGreg Roach * Is the media file actually a URL? 2444a9f750fSGreg Roach */ 245c1010edaSGreg Roach public function isExternal(): bool 246c1010edaSGreg Roach { 2474a9f750fSGreg Roach return strpos($this->multimedia_file_refn, '://') !== false; 2484a9f750fSGreg Roach } 2494a9f750fSGreg Roach 2504a9f750fSGreg Roach /** 2518f5f5da8SGreg Roach * Generate a URL for an image. 2528f5f5da8SGreg Roach * 2538f5f5da8SGreg Roach * @param int $width Maximum width in pixels 2548f5f5da8SGreg Roach * @param int $height Maximum height in pixels 2558f5f5da8SGreg Roach * @param string $fit "crop" or "contain" 2568f5f5da8SGreg Roach * 2578f5f5da8SGreg Roach * @return string 2588f5f5da8SGreg Roach */ 2598f53f488SRico Sonntag public function imageUrl($width, $height, $fit): string 260c1010edaSGreg Roach { 2618f5f5da8SGreg Roach // Sign the URL, to protect against mass-resize attacks. 2628f5f5da8SGreg Roach $glide_key = Site::getPreference('glide-key'); 26354c1ab5eSGreg Roach if ($glide_key === '') { 2648f5f5da8SGreg Roach $glide_key = bin2hex(random_bytes(128)); 2658f5f5da8SGreg Roach Site::setPreference('glide-key', $glide_key); 2668f5f5da8SGreg Roach } 2678f5f5da8SGreg Roach 268f4afa648SGreg Roach if (Auth::accessLevel($this->media->tree()) > $this->media->tree()->getPreference('SHOW_NO_WATERMARK')) { 2698f5f5da8SGreg Roach $mark = 'watermark.png'; 2708f5f5da8SGreg Roach } else { 2718f5f5da8SGreg Roach $mark = ''; 2728f5f5da8SGreg Roach } 2738f5f5da8SGreg Roach 274ee4364daSGreg Roach $params = [ 275c0935879SGreg Roach 'xref' => $this->media->xref(), 276d72b284aSGreg Roach 'tree' => $this->media->tree()->name(), 2774a9f750fSGreg Roach 'fact_id' => $this->fact_id, 2788f5f5da8SGreg Roach 'w' => $width, 2798f5f5da8SGreg Roach 'h' => $height, 2808f5f5da8SGreg Roach 'fit' => $fit, 2818f5f5da8SGreg Roach 'mark' => $mark, 2828f5f5da8SGreg Roach 'markh' => '100h', 2838f5f5da8SGreg Roach 'markw' => '100w', 2848f5f5da8SGreg Roach 'markalpha' => 25, 285c1010edaSGreg Roach 'or' => 0, 286ee4364daSGreg Roach ]; 2878f5f5da8SGreg Roach 288ee4364daSGreg Roach $signature = SignatureFactory::create($glide_key)->generateSignature('', $params); 289ee4364daSGreg Roach 290ee4364daSGreg Roach $params = ['route' => '/media-thumbnail', 's' => $signature] + $params; 291ee4364daSGreg Roach 292ee4364daSGreg Roach return route('media-thumbnail', $params); 2938f5f5da8SGreg Roach } 2948f5f5da8SGreg Roach 2958f5f5da8SGreg Roach /** 29685a166d8SGreg Roach * Is the media file an image? 2978f5f5da8SGreg Roach */ 29885a166d8SGreg Roach public function isImage(): bool 299c1010edaSGreg Roach { 30085a166d8SGreg Roach return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true); 3018f5f5da8SGreg Roach } 3028f5f5da8SGreg Roach 3038f5f5da8SGreg Roach /** 3048f5f5da8SGreg Roach * What is the mime-type of this object? 3058f5f5da8SGreg Roach * For simplicity and efficiency, use the extension, rather than the contents. 3068f5f5da8SGreg Roach * 3078f5f5da8SGreg Roach * @return string 3088f5f5da8SGreg Roach */ 3098f53f488SRico Sonntag public function mimeType(): string 310c1010edaSGreg Roach { 31185a166d8SGreg Roach $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION)); 31285a166d8SGreg Roach 31385a166d8SGreg Roach return self::MIME_TYPES[$extension] ?? 'application/octet-stream'; 31485a166d8SGreg Roach } 31585a166d8SGreg Roach 31685a166d8SGreg Roach /** 31785a166d8SGreg Roach * Generate a URL to download a non-image media file. 31885a166d8SGreg Roach * 31971625badSGreg Roach * @param string $disposition How should the image be returned - "attachment" or "inline" 32071625badSGreg Roach * 32185a166d8SGreg Roach * @return string 32285a166d8SGreg Roach */ 32371625badSGreg Roach public function downloadUrl(string $disposition): string 32485a166d8SGreg Roach { 32585a166d8SGreg Roach return route('media-download', [ 32685a166d8SGreg Roach 'xref' => $this->media->xref(), 327d72b284aSGreg Roach 'tree' => $this->media->tree()->name(), 32885a166d8SGreg Roach 'fact_id' => $this->fact_id, 32971625badSGreg Roach 'disposition' => $disposition, 33085a166d8SGreg Roach ]); 33185a166d8SGreg Roach } 33285a166d8SGreg Roach 33385a166d8SGreg Roach /** 33485a166d8SGreg Roach * A list of image attributes 33585a166d8SGreg Roach * 33685a166d8SGreg Roach * @return string[] 33785a166d8SGreg Roach */ 338a04bb9a2SGreg Roach public function attributes(FilesystemInterface $data_filesystem): array 33985a166d8SGreg Roach { 34085a166d8SGreg Roach $attributes = []; 34185a166d8SGreg Roach 342a04bb9a2SGreg Roach if (!$this->isExternal() || $this->fileExists($data_filesystem)) { 34385a166d8SGreg Roach try { 344a04bb9a2SGreg Roach $bytes = $this->media()->tree()->mediaFilesystem($data_filesystem)->getSize($this->filename()); 34585a166d8SGreg Roach $kb = intdiv($bytes + 1023, 1024); 34685a166d8SGreg Roach $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb)); 34785a166d8SGreg Roach } catch (FileNotFoundException $ex) { 34885a166d8SGreg Roach // External/missing files have no size. 34985a166d8SGreg Roach } 35085a166d8SGreg Roach 3515c98992aSGreg Roach // Note: getAdapter() is defined on Filesystem, but not on FilesystemInterface. 352a04bb9a2SGreg Roach $filesystem = $this->media()->tree()->mediaFilesystem($data_filesystem); 3535c98992aSGreg Roach if ($filesystem instanceof Filesystem) { 3545c98992aSGreg Roach $adapter = $filesystem->getAdapter(); 3555c98992aSGreg Roach // Only works for local filesystems. 3565c98992aSGreg Roach if ($adapter instanceof Local) { 3575c98992aSGreg Roach $file = $adapter->applyPathPrefix($this->filename()); 35885a166d8SGreg Roach [$width, $height] = getimagesize($file); 35985a166d8SGreg Roach $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height)); 3605c98992aSGreg Roach } 36185a166d8SGreg Roach } 36285a166d8SGreg Roach } 36385a166d8SGreg Roach 36485a166d8SGreg Roach return $attributes; 36585a166d8SGreg Roach } 36685a166d8SGreg Roach 36785a166d8SGreg Roach /** 36829518ad2SGreg Roach * Read the contents of a media file. 36929518ad2SGreg Roach * 370a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 371a04bb9a2SGreg Roach * 37229518ad2SGreg Roach * @return string 37329518ad2SGreg Roach */ 374a04bb9a2SGreg Roach public function fileContents(FilesystemInterface $data_filesystem): string 37529518ad2SGreg Roach { 376a04bb9a2SGreg Roach return $this->media->tree()->mediaFilesystem($data_filesystem)->read($this->multimedia_file_refn); 37729518ad2SGreg Roach } 37829518ad2SGreg Roach 37929518ad2SGreg Roach /** 38029518ad2SGreg Roach * Check if the file exists on this server 38185a166d8SGreg Roach * 382a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 383a04bb9a2SGreg Roach * 38485a166d8SGreg Roach * @return bool 38585a166d8SGreg Roach */ 386a04bb9a2SGreg Roach public function fileExists(FilesystemInterface $data_filesystem): bool 38785a166d8SGreg Roach { 388a04bb9a2SGreg Roach return $this->media->tree()->mediaFilesystem($data_filesystem)->has($this->multimedia_file_refn); 38985a166d8SGreg Roach } 39085a166d8SGreg Roach 39185a166d8SGreg Roach /** 39285a166d8SGreg Roach * @return Media 39385a166d8SGreg Roach */ 39485a166d8SGreg Roach public function media(): Media 39585a166d8SGreg Roach { 39685a166d8SGreg Roach return $this->media; 39785a166d8SGreg Roach } 39885a166d8SGreg Roach 39985a166d8SGreg Roach /** 40085a166d8SGreg Roach * Get the filename. 40185a166d8SGreg Roach * 40285a166d8SGreg Roach * @return string 40385a166d8SGreg Roach */ 40485a166d8SGreg Roach public function filename(): string 40585a166d8SGreg Roach { 40685a166d8SGreg Roach return $this->multimedia_file_refn; 40785a166d8SGreg Roach } 40885a166d8SGreg Roach 40985a166d8SGreg Roach /** 41085a166d8SGreg Roach * What file extension is used by this file? 41185a166d8SGreg Roach * 41285a166d8SGreg Roach * @return string 41385a166d8SGreg Roach */ 41485a166d8SGreg Roach public function extension(): string 41585a166d8SGreg Roach { 41685a166d8SGreg Roach return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION); 4178f5f5da8SGreg Roach } 4188f5f5da8SGreg Roach} 419