Passed
Push — master ( 9e5d8e...29518a )
by Greg
05:10
created

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

349
                $file = $this->media()->tree()->mediaFilesystem()->getAdapter()->/** @scrutinizer ignore-call */ applyPathPrefix($this->filename());
Loading history...
350
                [$width, $height] = getimagesize($file);
351
                $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height));
352
            } catch (Throwable $ex) {
353
                // Only works for local filesystems.
354
            }
355
        }
356
357
        return $attributes;
358
    }
359
360
    /**
361
     * Read the contents of a media file.
362
     *
363
     * @return string
364
     */
365
    public function fileContents(): string
366
    {
367
        return $this->media->tree()->mediaFilesystem()->read($this->multimedia_file_refn);
368
    }
369
370
    /**
371
     * Check if the file exists on this server
372
     *
373
     * @return bool
374
     */
375
    public function fileExists(): bool
376
    {
377
        return $this->media->tree()->mediaFilesystem()->has($this->multimedia_file_refn);
378
    }
379
380
    /**
381
     * @return Media
382
     */
383
    public function media(): Media
384
    {
385
        return $this->media;
386
    }
387
388
    /**
389
     * Get the filename.
390
     *
391
     * @return string
392
     */
393
    public function filename(): string
394
    {
395
        return $this->multimedia_file_refn;
396
    }
397
398
    /**
399
     * What file extension is used by this file?
400
     *
401
     * @return string
402
     */
403
    public function extension(): string
404
    {
405
        return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION);
406
    }
407
}
408