xref: /webtrees/app/MediaFile.php (revision c1010eda29c0909ed4d5d463f32d32bfefdd4dfe)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2018 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16namespace Fisharebest\Webtrees;
17
18use League\Glide\Urls\UrlBuilderFactory;
19use Throwable;
20
21/**
22 * A GEDCOM media file.  A media object can contain many media files,
23 * such as scans of both sides of a document, the transcript of an audio
24 * recording, etc.
25 */
26class MediaFile
27{
28    const MIME_TYPES = [
29        'bmp'  => 'image/bmp',
30        'doc'  => 'application/msword',
31        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
32        'ged'  => 'text/x-gedcom',
33        'gif'  => 'image/gif',
34        'html' => 'text/html',
35        'htm'  => 'text/html',
36        'jpeg' => 'image/jpeg',
37        'jpg'  => 'image/jpeg',
38        'mov'  => 'video/quicktime',
39        'mp3'  => 'audio/mpeg',
40        'mp4'  => 'video/mp4',
41        'ogv'  => 'video/ogg',
42        'pdf'  => 'application/pdf',
43        'png'  => 'image/png',
44        'rar'  => 'application/x-rar-compressed',
45        'swf'  => 'application/x-shockwave-flash',
46        'svg'  => 'image/svg',
47        'tiff' => 'image/tiff',
48        'tif'  => 'image/tiff',
49        'xls'  => 'application/vnd-ms-excel',
50        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
51        'wmv'  => 'video/x-ms-wmv',
52        'zip'  => 'application/zip',
53    ];
54
55    /** @var string The filename */
56    private $multimedia_file_refn = '';
57
58    /** @var string The file extension; jpeg, txt, mp4, etc. */
59    private $multimedia_format = '';
60
61    /** @var string The type of document; newspaper, microfiche, etc. */
62    private $source_media_type = '';
63    /** @var string The filename */
64
65    /** @var string The name of the document */
66    private $descriptive_title = '';
67
68    /** @var Media $media The media object to which this file belongs */
69    private $media;
70
71    /** @var string */
72    private $fact_id;
73
74    /**
75     * Create a MediaFile from raw GEDCOM data.
76     *
77     * @param string $gedcom
78     * @param Media  $media
79     */
80    public function __construct($gedcom, Media $media)
81    {
82        $this->media   = $media;
83        $this->fact_id = md5($gedcom);
84
85        if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) {
86            $this->multimedia_file_refn = $match[1];
87        }
88
89        if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) {
90            $this->multimedia_format = $match[1];
91        }
92
93        if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) {
94            $this->source_media_type = $match[1];
95        }
96
97        if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) {
98            $this->descriptive_title = $match[1];
99        }
100    }
101
102    /**
103     * Get the filename.
104     *
105     * @return string
106     */
107    public function filename(): string
108    {
109        return $this->multimedia_file_refn;
110    }
111
112    /**
113     * Get the base part of the filename.
114     *
115     * @return string
116     */
117    public function basename(): string
118    {
119        return basename($this->multimedia_file_refn);
120    }
121
122    /**
123     * Get the folder part of the filename.
124     *
125     * @return string
126     */
127    public function dirname(): string
128    {
129        $dirname = dirname($this->multimedia_file_refn);
130
131        if ($dirname === '.') {
132            return '';
133        } else {
134            return $dirname;
135        }
136    }
137
138    /**
139     * Get the format.
140     *
141     * @return string
142     */
143    public function format(): string
144    {
145        return $this->multimedia_format;
146    }
147
148    /**
149     * Get the type.
150     *
151     * @return string
152     */
153    public function type(): string
154    {
155        return $this->source_media_type;
156    }
157
158    /**
159     * Get the title.
160     *
161     * @return string
162     */
163    public function title(): string
164    {
165        return $this->descriptive_title;
166    }
167
168    /**
169     * Get the fact ID.
170     *
171     * @return string
172     */
173    public function factId(): string
174    {
175        return $this->fact_id;
176    }
177
178    /**
179     * @return bool
180     */
181    public function isPendingAddition()
182    {
183        foreach ($this->media->getFacts() as $fact) {
184            if ($fact->getFactId() === $this->fact_id) {
185                return $fact->isPendingAddition();
186            }
187        }
188
189        return false;
190    }
191
192    /**
193     * @return bool
194     */
195    public function isPendingDeletion()
196    {
197        foreach ($this->media->getFacts() as $fact) {
198            if ($fact->getFactId() === $this->fact_id) {
199                return $fact->isPendingDeletion();
200            }
201        }
202
203        return false;
204    }
205
206    /**
207     * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox.
208     *
209     * @param int      $width      Pixels
210     * @param int      $height     Pixels
211     * @param string   $fit        "crop" or "contain"
212     * @param string[] $attributes Additional HTML attributes
213     *
214     * @return string
215     */
216    public function displayImage($width, $height, $fit, $attributes = [])
217    {
218        if ($this->isExternal()) {
219            $src    = $this->multimedia_file_refn;
220            $srcset = [];
221        } else {
222            // Generate multiple images for displays with higher pixel densities.
223            $src    = $this->imageUrl($width, $height, $fit);
224            $srcset = [];
225            foreach ([
226                         2,
227                         3,
228                         4,
229                     ] as $x) {
230                $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x';
231            }
232        }
233
234        $image = '<img ' . Html::attributes($attributes + [
235                    'dir'    => 'auto',
236                    'src'    => $src,
237                    'srcset' => implode(',', $srcset),
238                    'alt'    => htmlspecialchars_decode(strip_tags($this->media->getFullName())),
239                ]) . '>';
240
241        if ($this->isImage()) {
242            $attributes = Html::attributes([
243                'class'      => 'gallery',
244                'type'       => $this->mimeType(),
245                'href'       => $this->imageUrl(0, 0, 'contain'),
246                'data-title' => htmlspecialchars_decode(strip_tags($this->media->getFullName())),
247            ]);
248        } else {
249            $attributes = Html::attributes([
250                'type' => $this->mimeType(),
251                'href' => $this->downloadUrl(),
252            ]);
253        }
254
255        return '<a ' . $attributes . '>' . $image . '</a>';
256    }
257
258    /**
259     * A list of image attributes
260     *
261     * @return string[]
262     */
263    public function attributes(): array
264    {
265        $attributes = [];
266
267        if (!$this->isExternal() || $this->fileExists()) {
268            $file = $this->folder() . $this->multimedia_file_refn;
269
270            $attributes['__FILE_SIZE__'] = $this->fileSizeKB();
271
272            $imgsize = getimagesize($file);
273            if (is_array($imgsize) && !empty($imgsize['0'])) {
274                $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($imgsize['0']), I18N::number($imgsize['1']));
275            }
276        }
277
278        return $attributes;
279    }
280
281    /**
282     * check if the file exists on this server
283     *
284     * @return bool
285     */
286    public function fileExists()
287    {
288        return !$this->isExternal() && file_exists($this->folder() . $this->multimedia_file_refn);
289    }
290
291    /**
292     * Is the media file actually a URL?
293     */
294    public function isExternal(): bool
295    {
296        return strpos($this->multimedia_file_refn, '://') !== false;
297    }
298
299    /**
300     * Is the media file an image?
301     */
302    public function isImage(): bool
303    {
304        return in_array($this->extension(), [
305            'jpeg',
306            'jpg',
307            'gif',
308            'png',
309        ]);
310    }
311
312    /**
313     * Where is the file stored on disk?
314     */
315    public function folder(): string
316    {
317        return WT_DATA_DIR . $this->media->getTree()->getPreference('MEDIA_DIRECTORY');
318    }
319
320    /**
321     * A user-friendly view of the file size
322     *
323     * @return int
324     */
325    private function fileSizeBytes(): int
326    {
327        try {
328            return filesize($this->folder() . $this->multimedia_file_refn);
329        } catch (Throwable $ex) {
330            DebugBar::addThrowable($ex);
331
332            return 0;
333        }
334    }
335
336    /**
337     * get the media file size in KB
338     *
339     * @return string
340     */
341    public function fileSizeKB()
342    {
343        $size = $this->filesizeBytes();
344        $size = (int)(($size + 1023) / 1024);
345
346        return /* I18N: size of file in KB */
347            I18N::translate('%s KB', I18N::number($size));
348    }
349
350    /**
351     * Get the filename on the server - for those (very few!) functions which actually
352     * need the filename, such as the PDF reports.
353     *
354     * @return string
355     */
356    public function getServerFilename()
357    {
358        $MEDIA_DIRECTORY = $this->media->getTree()->getPreference('MEDIA_DIRECTORY');
359
360        if ($this->isExternal() || !$this->multimedia_file_refn) {
361            // External image, or (in the case of corrupt GEDCOM data) no image at all
362            return $this->multimedia_file_refn;
363        } else {
364            // Main image
365            return WT_DATA_DIR . $MEDIA_DIRECTORY . $this->multimedia_file_refn;
366        }
367    }
368
369    /**
370     * Generate a URL to download a non-image media file.
371     *
372     * @return string
373     */
374    public function downloadUrl()
375    {
376        return route('media-download', [
377            'xref'    => $this->media->getXref(),
378            'ged'     => $this->media->getTree()->getName(),
379            'fact_id' => $this->fact_id,
380        ]);
381    }
382
383    /**
384     * Generate a URL for an image.
385     *
386     * @param int    $width  Maximum width in pixels
387     * @param int    $height Maximum height in pixels
388     * @param string $fit    "crop" or "contain"
389     *
390     * @return string
391     */
392    public function imageUrl($width, $height, $fit)
393    {
394        // Sign the URL, to protect against mass-resize attacks.
395        $glide_key = Site::getPreference('glide-key');
396        if (empty($glide_key)) {
397            $glide_key = bin2hex(random_bytes(128));
398            Site::setPreference('glide-key', $glide_key);
399        }
400
401        if (Auth::accessLevel($this->media->getTree()) > $this->media->getTree()->getPreference('SHOW_NO_WATERMARK')) {
402            $mark = 'watermark.png';
403        } else {
404            $mark = '';
405        }
406
407        $url_builder = UrlBuilderFactory::create(WT_BASE_URL, $glide_key);
408
409        $url = $url_builder->getUrl('index.php', [
410            'route'     => 'media-thumbnail',
411            'xref'      => $this->media->getXref(),
412            'ged'       => $this->media->getTree()->getName(),
413            'fact_id'   => $this->fact_id,
414            'w'         => $width,
415            'h'         => $height,
416            'fit'       => $fit,
417            'mark'      => $mark,
418            'markh'     => '100h',
419            'markw'     => '100w',
420            'markalpha' => 25,
421            'or'        => 0,
422            // Intervention uses exif_read_data() which is very buggy.
423        ]);
424
425        return $url;
426    }
427
428    /**
429     * What file extension is used by this file?
430     *
431     * @return string
432     */
433    public function extension()
434    {
435        if (preg_match('/\.([a-zA-Z0-9]+)$/', $this->multimedia_file_refn, $match)) {
436            return strtolower($match[1]);
437        } else {
438            return '';
439        }
440    }
441
442    /**
443     * What is the mime-type of this object?
444     * For simplicity and efficiency, use the extension, rather than the contents.
445     *
446     * @return string
447     */
448    public function mimeType()
449    {
450        return self::MIME_TYPES[$this->extension()] ?? 'application/octet-stream';
451    }
452}
453