xref: /webtrees/app/MediaFile.php (revision e7f16b4369a9ba8666852184c6b908e3b2a5f760)
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