xref: /webtrees/app/MediaFile.php (revision e7f16b4369a9ba8666852184c6b908e3b2a5f760)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2020 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees;
21
22use League\Flysystem\Adapter\Local;
23use League\Flysystem\FileNotFoundException;
24use League\Flysystem\Filesystem;
25use League\Flysystem\FilesystemInterface;
26use League\Glide\Signatures\SignatureFactory;
27
28use function extension_loaded;
29use function getimagesize;
30use function intdiv;
31use function pathinfo;
32use function strtolower;
33
34use const PATHINFO_EXTENSION;
35
36/**
37 * A GEDCOM media file.  A media object can contain many media files,
38 * such as scans of both sides of a document, the transcript of an audio
39 * recording, etc.
40 */
41class MediaFile
42{
43    private const SUPPORTED_IMAGE_MIME_TYPES = [
44        'image/gif',
45        'image/jpeg',
46        'image/png',
47    ];
48
49    /** @var string The filename */
50    private $multimedia_file_refn = '';
51
52    /** @var string The file extension; jpeg, txt, mp4, etc. */
53    private $multimedia_format = '';
54
55    /** @var string The type of document; newspaper, microfiche, etc. */
56    private $source_media_type = '';
57    /** @var string The filename */
58
59    /** @var string The name of the document */
60    private $descriptive_title = '';
61
62    /** @var Media $media The media object to which this file belongs */
63    private $media;
64
65    /** @var string */
66    private $fact_id;
67
68    /**
69     * Create a MediaFile from raw GEDCOM data.
70     *
71     * @param string $gedcom
72     * @param Media  $media
73     */
74    public function __construct($gedcom, Media $media)
75    {
76        $this->media   = $media;
77        $this->fact_id = md5($gedcom);
78
79        if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) {
80            $this->multimedia_file_refn = $match[1];
81            $this->multimedia_format    = pathinfo($match[1], PATHINFO_EXTENSION);
82        }
83
84        if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) {
85            $this->multimedia_format = $match[1];
86        }
87
88        if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) {
89            $this->source_media_type = $match[1];
90        }
91
92        if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) {
93            $this->descriptive_title = $match[1];
94        }
95    }
96
97    /**
98     * Get the format.
99     *
100     * @return string
101     */
102    public function format(): string
103    {
104        return $this->multimedia_format;
105    }
106
107    /**
108     * Get the type.
109     *
110     * @return string
111     */
112    public function type(): string
113    {
114        return $this->source_media_type;
115    }
116
117    /**
118     * Get the title.
119     *
120     * @return string
121     */
122    public function title(): string
123    {
124        return $this->descriptive_title;
125    }
126
127    /**
128     * Get the fact ID.
129     *
130     * @return string
131     */
132    public function factId(): string
133    {
134        return $this->fact_id;
135    }
136
137    /**
138     * @return bool
139     */
140    public function isPendingAddition(): bool
141    {
142        foreach ($this->media->facts() as $fact) {
143            if ($fact->id() === $this->fact_id) {
144                return $fact->isPendingAddition();
145            }
146        }
147
148        return false;
149    }
150
151    /**
152     * @return bool
153     */
154    public function isPendingDeletion(): bool
155    {
156        foreach ($this->media->facts() as $fact) {
157            if ($fact->id() === $this->fact_id) {
158                return $fact->isPendingDeletion();
159            }
160        }
161
162        return false;
163    }
164
165    /**
166     * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox.
167     *
168     * @param int      $width            Pixels
169     * @param int      $height           Pixels
170     * @param string   $fit              "crop" or "contain"
171     * @param string[] $image_attributes Additional HTML attributes
172     *
173     * @return string
174     */
175    public function displayImage($width, $height, $fit, $image_attributes = []): string
176    {
177        if ($this->isExternal()) {
178            $src    = $this->multimedia_file_refn;
179            $srcset = [];
180        } else {
181            // Generate multiple images for displays with higher pixel densities.
182            $src    = $this->imageUrl($width, $height, $fit);
183            $srcset = [];
184            foreach ([2, 3, 4] as $x) {
185                $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x';
186            }
187        }
188
189        if ($this->isImage()) {
190            $image = '<img ' . Html::attributes($image_attributes + [
191                        'dir'    => 'auto',
192                        'src'    => $src,
193                        'srcset' => implode(',', $srcset),
194                        'alt'    => strip_tags($this->media->fullName()),
195                    ]) . '>';
196
197            $link_attributes = Html::attributes([
198                'class'      => 'gallery',
199                'type'       => $this->mimeType(),
200                'href'       => $this->imageUrl(0, 0, 'contain'),
201                'data-title' => strip_tags($this->media->fullName()),
202            ]);
203        } else {
204            $image = view('icons/mime', ['type' => $this->mimeType()]);
205
206            $link_attributes = Html::attributes([
207                'type' => $this->mimeType(),
208                'href' => $this->downloadUrl('inline'),
209            ]);
210        }
211
212        return '<a ' . $link_attributes . '>' . $image . '</a>';
213    }
214
215    /**
216     * Is the media file actually a URL?
217     */
218    public function isExternal(): bool
219    {
220        return strpos($this->multimedia_file_refn, '://') !== false;
221    }
222
223    /**
224     * Generate a URL for an image.
225     *
226     * @param int    $width  Maximum width in pixels
227     * @param int    $height Maximum height in pixels
228     * @param string $fit    "crop" or "contain"
229     *
230     * @return string
231     */
232    public function imageUrl($width, $height, $fit): string
233    {
234        // Sign the URL, to protect against mass-resize attacks.
235        $glide_key = Site::getPreference('glide-key');
236        if ($glide_key === '') {
237            $glide_key = bin2hex(random_bytes(128));
238            Site::setPreference('glide-key', $glide_key);
239        }
240
241        if (Auth::accessLevel($this->media->tree()) > $this->media->tree()->getPreference('SHOW_NO_WATERMARK')) {
242            $mark = 'watermark.png';
243        } else {
244            $mark = '';
245        }
246
247        // Automatic rotation only works when the php-exif library is loaded.
248        $orientation = extension_loaded('exif') ? 'or' : 0;
249
250        $params = [
251            'xref'      => $this->media->xref(),
252            'tree'      => $this->media->tree()->name(),
253            'fact_id'   => $this->fact_id,
254            'w'         => $width,
255            'h'         => $height,
256            'fit'       => $fit,
257            'mark'      => $mark,
258            'markh'     => '100h',
259            'markw'     => '100w',
260            'markpos'   => 'center',
261            'markalpha' => 25,
262            'or'        => $orientation,
263        ];
264
265        $signature = SignatureFactory::create($glide_key)->generateSignature('', $params);
266
267        $params = ['route' => '/media-thumbnail', 's' => $signature] + $params;
268
269        return route('media-thumbnail', $params);
270    }
271
272    /**
273     * Is the media file an image?
274     */
275    public function isImage(): bool
276    {
277        return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true);
278    }
279
280    /**
281     * What is the mime-type of this object?
282     * For simplicity and efficiency, use the extension, rather than the contents.
283     *
284     * @return string
285     */
286    public function mimeType(): string
287    {
288        $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION));
289
290        return Mime::TYPES[$extension] ?? Mime::DEFAULT_TYPE;
291    }
292
293    /**
294     * Generate a URL to download a non-image media file.
295     *
296     * @param string $disposition How should the image be returned - "attachment" or "inline"
297     *
298     * @return string
299     */
300    public function downloadUrl(string $disposition): string
301    {
302        return route('media-download', [
303            'xref'        => $this->media->xref(),
304            'tree'        => $this->media->tree()->name(),
305            'fact_id'     => $this->fact_id,
306            'disposition' => $disposition,
307        ]);
308    }
309
310    /**
311     * A list of image attributes
312     *
313     * @param FilesystemInterface $data_filesystem
314     *
315     * @return string[]
316     */
317    public function attributes(FilesystemInterface $data_filesystem): array
318    {
319        $attributes = [];
320
321        if (!$this->isExternal() || $this->fileExists($data_filesystem)) {
322            try {
323                $bytes                       = $this->media()->tree()->mediaFilesystem($data_filesystem)->getSize($this->filename());
324                $kb                          = intdiv($bytes + 1023, 1024);
325                $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb));
326            } catch (FileNotFoundException $ex) {
327                // External/missing files have no size.
328            }
329
330            // Note: getAdapter() is defined on Filesystem, but not on FilesystemInterface.
331            $filesystem = $this->media()->tree()->mediaFilesystem($data_filesystem);
332            if ($filesystem instanceof Filesystem) {
333                $adapter = $filesystem->getAdapter();
334                // Only works for local filesystems.
335                if ($adapter instanceof Local) {
336                    $file = $adapter->applyPathPrefix($this->filename());
337                    [$width, $height] = getimagesize($file);
338                    $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height));
339                }
340            }
341        }
342
343        return $attributes;
344    }
345
346    /**
347     * Read the contents of a media file.
348     *
349     * @param FilesystemInterface $data_filesystem
350     *
351     * @return string
352     */
353    public function fileContents(FilesystemInterface $data_filesystem): string
354    {
355        return $this->media->tree()->mediaFilesystem($data_filesystem)->read($this->multimedia_file_refn);
356    }
357
358    /**
359     * Check if the file exists on this server
360     *
361     * @param FilesystemInterface $data_filesystem
362     *
363     * @return bool
364     */
365    public function fileExists(FilesystemInterface $data_filesystem): bool
366    {
367        return $this->media->tree()->mediaFilesystem($data_filesystem)->has($this->multimedia_file_refn);
368    }
369
370    /**
371     * @return Media
372     */
373    public function media(): Media
374    {
375        return $this->media;
376    }
377
378    /**
379     * Get the filename.
380     *
381     * @return string
382     */
383    public function filename(): string
384    {
385        return $this->multimedia_file_refn;
386    }
387
388    /**
389     * What file extension is used by this file?
390     *
391     * @return string
392     *
393     * @deprecated since 2.0.4.  Will be removed in 2.1.0
394     */
395    public function extension(): string
396    {
397        return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION);
398    }
399}
400