Passed
Push — master ( 61bf91...85a166 )
by Greg
05:44
created

MediaFile::media()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2019 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
 */
16
declare(strict_types=1);
17
18
namespace Fisharebest\Webtrees;
19
20
use League\Flysystem\FileNotFoundException;
21
use League\Glide\Urls\UrlBuilderFactory;
22
use Psr\Http\Message\ServerRequestInterface;
23
use Throwable;
24
use function app;
25
use function getimagesize;
26
use function intdiv;
27
use function pathinfo;
28
use function strtolower;
29
use const PATHINFO_EXTENSION;
30
31
/**
32
 * A GEDCOM media file.  A media object can contain many media files,
33
 * such as scans of both sides of a document, the transcript of an audio
34
 * recording, etc.
35
 */
36
class MediaFile
37
{
38
    private const MIME_TYPES = [
39
        'bmp'  => 'image/bmp',
40
        'doc'  => 'application/msword',
41
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
42
        'ged'  => 'text/x-gedcom',
43
        'gif'  => 'image/gif',
44
        'html' => 'text/html',
45
        'htm'  => 'text/html',
46
        'jpeg' => 'image/jpeg',
47
        'jpg'  => 'image/jpeg',
48
        'mov'  => 'video/quicktime',
49
        'mp3'  => 'audio/mpeg',
50
        'mp4'  => 'video/mp4',
51
        'ogv'  => 'video/ogg',
52
        'pdf'  => 'application/pdf',
53
        'png'  => 'image/png',
54
        'rar'  => 'application/x-rar-compressed',
55
        'swf'  => 'application/x-shockwave-flash',
56
        'svg'  => 'image/svg',
57
        'tiff' => 'image/tiff',
58
        'tif'  => 'image/tiff',
59
        'xls'  => 'application/vnd-ms-excel',
60
        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
61
        'wmv'  => 'video/x-ms-wmv',
62
        'zip'  => 'application/zip',
63
    ];
64
65
    private const SUPPORTED_IMAGE_MIME_TYPES = [
66
        'image/gif',
67
        'image/jpeg',
68
        'image/png',
69
    ];
70
71
    /** @var string The filename */
72
    private $multimedia_file_refn = '';
73
74
    /** @var string The file extension; jpeg, txt, mp4, etc. */
75
    private $multimedia_format = '';
76
77
    /** @var string The type of document; newspaper, microfiche, etc. */
78
    private $source_media_type = '';
79
    /** @var string The filename */
80
81
    /** @var string The name of the document */
82
    private $descriptive_title = '';
83
84
    /** @var Media $media The media object to which this file belongs */
85
    private $media;
86
87
    /** @var string */
88
    private $fact_id;
89
90
    /**
91
     * Create a MediaFile from raw GEDCOM data.
92
     *
93
     * @param string $gedcom
94
     * @param Media  $media
95
     */
96
    public function __construct($gedcom, Media $media)
97
    {
98
        $this->media   = $media;
99
        $this->fact_id = md5($gedcom);
100
101
        if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) {
102
            $this->multimedia_file_refn = $match[1];
103
            $this->multimedia_format    = pathinfo($match[1], PATHINFO_EXTENSION);
104
        }
105
106
        if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) {
107
            $this->multimedia_format = $match[1];
108
        }
109
110
        if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) {
111
            $this->source_media_type = $match[1];
112
        }
113
114
        if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) {
115
            $this->descriptive_title = $match[1];
116
        }
117
    }
118
119
    /**
120
     * Get the format.
121
     *
122
     * @return string
123
     */
124
    public function format(): string
125
    {
126
        return $this->multimedia_format;
127
    }
128
129
    /**
130
     * Get the type.
131
     *
132
     * @return string
133
     */
134
    public function type(): string
135
    {
136
        return $this->source_media_type;
137
    }
138
139
    /**
140
     * Get the title.
141
     *
142
     * @return string
143
     */
144
    public function title(): string
145
    {
146
        return $this->descriptive_title;
147
    }
148
149
    /**
150
     * Get the fact ID.
151
     *
152
     * @return string
153
     */
154
    public function factId(): string
155
    {
156
        return $this->fact_id;
157
    }
158
159
    /**
160
     * @return bool
161
     */
162
    public function isPendingAddition(): bool
163
    {
164
        foreach ($this->media->facts() as $fact) {
165
            if ($fact->id() === $this->fact_id) {
166
                return $fact->isPendingAddition();
167
            }
168
        }
169
170
        return false;
171
    }
172
173
    /**
174
     * @return bool
175
     */
176
    public function isPendingDeletion(): bool
177
    {
178
        foreach ($this->media->facts() as $fact) {
179
            if ($fact->id() === $this->fact_id) {
180
                return $fact->isPendingDeletion();
181
            }
182
        }
183
184
        return false;
185
    }
186
187
    /**
188
     * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox.
189
     *
190
     * @param int      $width            Pixels
191
     * @param int      $height           Pixels
192
     * @param string   $fit              "crop" or "contain"
193
     * @param string[] $image_attributes Additional HTML attributes
194
     *
195
     * @return string
196
     */
197
    public function displayImage($width, $height, $fit, $image_attributes = []): string
198
    {
199
        if ($this->isExternal()) {
200
            $src    = $this->multimedia_file_refn;
201
            $srcset = [];
202
        } else {
203
            // Generate multiple images for displays with higher pixel densities.
204
            $src    = $this->imageUrl($width, $height, $fit);
205
            $srcset = [];
206
            foreach ([2, 3, 4] as $x) {
207
                $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x';
208
            }
209
        }
210
211
        if ($this->isImage()) {
212
            $image = '<img ' . Html::attributes($image_attributes + [
213
                        'dir'    => 'auto',
214
                        'src'    => $src,
215
                        'srcset' => implode(',', $srcset),
216
                        'alt'    => htmlspecialchars_decode(strip_tags($this->media->fullName())),
217
                    ]) . '>';
218
219
            $link_attributes = Html::attributes([
220
                'class'      => 'gallery',
221
                'type'       => $this->mimeType(),
222
                'href'       => $this->imageUrl(0, 0, 'contain'),
223
                'data-title' => htmlspecialchars_decode(strip_tags($this->media->fullName())),
224
            ]);
225
        } else {
226
            $image = view('icons/mime', ['type' => $this->mimeType()]);
227
228
            $link_attributes = Html::attributes([
229
                'type' => $this->mimeType(),
230
                'href' => $this->downloadUrl(),
231
            ]);
232
        }
233
234
        return '<a ' . $link_attributes . '>' . $image . '</a>';
235
    }
236
237
    /**
238
     * Is the media file actually a URL?
239
     */
240
    public function isExternal(): bool
241
    {
242
        return strpos($this->multimedia_file_refn, '://') !== false;
243
    }
244
245
    /**
246
     * Generate a URL for an image.
247
     *
248
     * @param int    $width  Maximum width in pixels
249
     * @param int    $height Maximum height in pixels
250
     * @param string $fit    "crop" or "contain"
251
     *
252
     * @return string
253
     */
254
    public function imageUrl($width, $height, $fit): string
255
    {
256
        // Sign the URL, to protect against mass-resize attacks.
257
        $glide_key = Site::getPreference('glide-key');
258
        if (empty($glide_key)) {
259
            $glide_key = bin2hex(random_bytes(128));
260
            Site::setPreference('glide-key', $glide_key);
261
        }
262
263
        if (Auth::accessLevel($this->media->tree()) > $this->media->tree()->getPreference('SHOW_NO_WATERMARK')) {
264
            $mark = 'watermark.png';
265
        } else {
266
            $mark = '';
267
        }
268
269
        $base_url = app(ServerRequestInterface::class)->getAttribute('base_url');
270
271
        $url_builder = UrlBuilderFactory::create($base_url, $glide_key);
272
273
        $url = $url_builder->getUrl('index.php', [
274
            'route'     => 'media-thumbnail',
275
            'xref'      => $this->media->xref(),
276
            'ged'       => $this->media->tree()->name(),
277
            'fact_id'   => $this->fact_id,
278
            'w'         => $width,
279
            'h'         => $height,
280
            'fit'       => $fit,
281
            'mark'      => $mark,
282
            'markh'     => '100h',
283
            'markw'     => '100w',
284
            'markalpha' => 25,
285
            'or'        => 0,
286
            // Intervention uses exif_read_data() which is very buggy.
287
        ]);
288
289
        return $url;
290
    }
291
292
    /**
293
     * Is the media file an image?
294
     */
295
    public function isImage(): bool
296
    {
297
        return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true);
298
    }
299
300
    /**
301
     * What is the mime-type of this object?
302
     * For simplicity and efficiency, use the extension, rather than the contents.
303
     *
304
     * @return string
305
     */
306
    public function mimeType(): string
307
    {
308
        $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION));
309
310
        return self::MIME_TYPES[$extension] ?? 'application/octet-stream';
311
    }
312
313
    /**
314
     * Generate a URL to download a non-image media file.
315
     *
316
     * @return string
317
     */
318
    public function downloadUrl(): string
319
    {
320
        return route('media-download', [
321
            'xref'    => $this->media->xref(),
322
            'ged'     => $this->media->tree()->name(),
323
            'fact_id' => $this->fact_id,
324
        ]);
325
    }
326
327
    /**
328
     * A list of image attributes
329
     *
330
     * @return string[]
331
     */
332
    public function attributes(): array
333
    {
334
        $attributes = [];
335
336
        if (!$this->isExternal() || $this->fileExists()) {
337
            try {
338
                $bytes                       = $this->media()->tree()->mediaFilesystem()->getSize($this->filename());
339
                $kb                          = intdiv($bytes + 1023, 1024);
340
                $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb));
341
            } catch (FileNotFoundException $ex) {
342
                // External/missing files have no size.
343
            }
344
345
            try {
346
                $file = $this->media()->tree()->mediaFilesystem()->getAdapter()->applyPathPrefix($this->filename());
0 ignored issues
show
Bug introduced by
The method applyPathPrefix() does not exist on League\Flysystem\AdapterInterface. It seems like you code against a sub-type of League\Flysystem\AdapterInterface such as League\Flysystem\Adapter\AbstractAdapter. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

346
                $file = $this->media()->tree()->mediaFilesystem()->getAdapter()->/** @scrutinizer ignore-call */ applyPathPrefix($this->filename());
Loading history...
347
                [$width, $height] = getimagesize($file);
348
                $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height));
349
            } catch (Throwable $ex) {
350
                // Only works for local filesystems.
351
            }
352
        }
353
354
        return $attributes;
355
    }
356
357
    /**
358
     * check if the file exists on this server
359
     *
360
     * @return bool
361
     */
362
    public function fileExists(): bool
363
    {
364
        return $this->media->tree()->mediaFilesystem()->has($this->multimedia_file_refn);
365
    }
366
367
    /**
368
     * @return Media
369
     */
370
    public function media(): Media
371
    {
372
        return $this->media;
373
    }
374
375
    /**
376
     * Get the filename.
377
     *
378
     * @return string
379
     */
380
    public function filename(): string
381
    {
382
        return $this->multimedia_file_refn;
383
    }
384
385
    /**
386
     * Get the filename on the server - for those (very few!) functions which actually
387
     * need the filename, such as the PDF reports.
388
     *
389
     * @return string
390
     */
391
    public function getServerFilename(): string
392
    {
393
        $MEDIA_DIRECTORY = $this->media->tree()->getPreference('MEDIA_DIRECTORY');
394
395
        if ($this->multimedia_file_refn === '' || $this->isExternal()) {
396
            // External image, or (in the case of corrupt GEDCOM data) no image at all
397
            return $this->multimedia_file_refn;
398
        }
399
400
        // Main image
401
        return WT_DATA_DIR . $MEDIA_DIRECTORY . $this->multimedia_file_refn;
402
    }
403
404
    /**
405
     * What file extension is used by this file?
406
     *
407
     * @return string
408
     */
409
    public function extension(): string
410
    {
411
        return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION);
412
    }
413
}
414