Passed
Push — master ( 602c43...6577bf )
by Greg
06:13
created

MediaFile::isPendingDeletion()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 0
dl 0
loc 9
rs 10
c 0
b 0
f 0
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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Fisharebest\Webtrees\Http\RequestHandlers\MediaFileDownload;
23
use Fisharebest\Webtrees\Http\RequestHandlers\MediaFileThumbnail;
24
use Fisharebest\Webtrees\Services\MediaFileService;
25
use League\Flysystem\Adapter\Local;
26
use League\Flysystem\FileNotFoundException;
27
use League\Flysystem\Filesystem;
28
use League\Flysystem\FilesystemInterface;
29
use League\Glide\Signatures\SignatureFactory;
30
31
use function bin2hex;
32
use function getimagesize;
33
use function http_build_query;
34
use function intdiv;
35
use function ksort;
36
use function md5;
37
use function pathinfo;
38
use function random_bytes;
39
use function str_contains;
40
use function strtolower;
41
42
use const PATHINFO_EXTENSION;
43
44
/**
45
 * A GEDCOM media file.  A media object can contain many media files,
46
 * such as scans of both sides of a document, the transcript of an audio
47
 * recording, etc.
48
 */
49
class MediaFile
50
{
51
    private const SUPPORTED_IMAGE_MIME_TYPES = [
52
        'image/gif',
53
        'image/jpeg',
54
        'image/png',
55
    ];
56
57
    /** @var string The filename */
58
    private $multimedia_file_refn = '';
59
60
    /** @var string The file extension; jpeg, txt, mp4, etc. */
61
    private $multimedia_format = '';
62
63
    /** @var string The type of document; newspaper, microfiche, etc. */
64
    private $source_media_type = '';
65
    /** @var string The filename */
66
67
    /** @var string The name of the document */
68
    private $descriptive_title = '';
69
70
    /** @var Media $media The media object to which this file belongs */
71
    private $media;
72
73
    /** @var string */
74
    private $fact_id;
75
76
    /**
77
     * Create a MediaFile from raw GEDCOM data.
78
     *
79
     * @param string $gedcom
80
     * @param Media  $media
81
     */
82
    public function __construct($gedcom, Media $media)
83
    {
84
        $this->media   = $media;
85
        $this->fact_id = md5($gedcom);
86
87
        if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) {
88
            $this->multimedia_file_refn = $match[1];
89
            $this->multimedia_format    = pathinfo($match[1], PATHINFO_EXTENSION);
90
        }
91
92
        if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) {
93
            $this->multimedia_format = $match[1];
94
        }
95
96
        if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) {
97
            $this->source_media_type = $match[1];
98
        }
99
100
        if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) {
101
            $this->descriptive_title = $match[1];
102
        }
103
    }
104
105
    /**
106
     * Get the format.
107
     *
108
     * @return string
109
     */
110
    public function format(): string
111
    {
112
        return $this->multimedia_format;
113
    }
114
115
    /**
116
     * Get the type.
117
     *
118
     * @return string
119
     */
120
    public function type(): string
121
    {
122
        return $this->source_media_type;
123
    }
124
125
    /**
126
     * Get the title.
127
     *
128
     * @return string
129
     */
130
    public function title(): string
131
    {
132
        return $this->descriptive_title;
133
    }
134
135
    /**
136
     * Get the fact ID.
137
     *
138
     * @return string
139
     */
140
    public function factId(): string
141
    {
142
        return $this->fact_id;
143
    }
144
145
    /**
146
     * @return bool
147
     */
148
    public function isPendingAddition(): bool
149
    {
150
        foreach ($this->media->facts() as $fact) {
151
            if ($fact->id() === $this->fact_id) {
152
                return $fact->isPendingAddition();
153
            }
154
        }
155
156
        return false;
157
    }
158
159
    /**
160
     * @return bool
161
     */
162
    public function isPendingDeletion(): bool
163
    {
164
        foreach ($this->media->facts() as $fact) {
165
            if ($fact->id() === $this->fact_id) {
166
                return $fact->isPendingDeletion();
167
            }
168
        }
169
170
        return false;
171
    }
172
173
    /**
174
     * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox.
175
     *
176
     * @param int      $width            Pixels
177
     * @param int      $height           Pixels
178
     * @param string   $fit              "crop" or "contain"
179
     * @param string[] $image_attributes Additional HTML attributes
180
     *
181
     * @return string
182
     */
183
    public function displayImage($width, $height, $fit, $image_attributes = []): string
184
    {
185
        if ($this->isExternal()) {
186
            $src    = $this->multimedia_file_refn;
187
            $srcset = [];
188
        } else {
189
            // Generate multiple images for displays with higher pixel densities.
190
            $src    = $this->imageUrl($width, $height, $fit);
191
            $srcset = [];
192
            foreach ([2, 3, 4] as $x) {
193
                $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x';
194
            }
195
        }
196
197
        if ($this->isImage()) {
198
            $image = '<img ' . Html::attributes($image_attributes + [
199
                        'dir'    => 'auto',
200
                        'src'    => $src,
201
                        'srcset' => implode(',', $srcset),
202
                        'alt'    => strip_tags($this->media->fullName()),
203
                    ]) . '>';
204
205
            $link_attributes = Html::attributes([
206
                'class'      => 'gallery',
207
                'type'       => $this->mimeType(),
208
                'href'       => $this->downloadUrl('inline'),
209
                'data-title' => strip_tags($this->media->fullName()),
210
            ]);
211
        } else {
212
            $image = view('icons/mime', ['type' => $this->mimeType()]);
213
214
            $link_attributes = Html::attributes([
215
                'type' => $this->mimeType(),
216
                'href' => $this->downloadUrl('inline'),
217
            ]);
218
        }
219
220
        return '<a ' . $link_attributes . '>' . $image . '</a>';
221
    }
222
223
    /**
224
     * Is the media file actually a URL?
225
     */
226
    public function isExternal(): bool
227
    {
228
        return str_contains($this->multimedia_file_refn, '://');
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

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

228
        return /** @scrutinizer ignore-deprecated */ str_contains($this->multimedia_file_refn, '://');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
229
    }
230
231
    /**
232
     * Generate a URL for an image.
233
     *
234
     * @param int    $width  Maximum width in pixels
235
     * @param int    $height Maximum height in pixels
236
     * @param string $fit    "crop" or "contain"
237
     *
238
     * @return string
239
     */
240
    public function imageUrl($width, $height, $fit): string
241
    {
242
        // Sign the URL, to protect against mass-resize attacks.
243
        $glide_key = Site::getPreference('glide-key');
244
245
        if ($glide_key === '') {
246
            $glide_key = bin2hex(random_bytes(128));
247
            Site::setPreference('glide-key', $glide_key);
248
        }
249
250
        // The "mark" parameter is ignored, but needed for cache-busting.
251
        $params = [
252
            'xref'      => $this->media->xref(),
253
            'tree'      => $this->media->tree()->name(),
254
            'fact_id'   => $this->fact_id,
255
            'w'         => $width,
256
            'h'         => $height,
257
            'fit'       => $fit,
258
            'mark'      => Factory::image()->thumbnailNeedsWatermark($this, Auth::user())
259
        ];
260
261
        $params['s'] = $this->signature($params);
262
263
        return route(MediaFileThumbnail::class, $params);
264
    }
265
266
    /**
267
     * Is the media file an image?
268
     */
269
    public function isImage(): bool
270
    {
271
        return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true);
272
    }
273
274
    /**
275
     * What is the mime-type of this object?
276
     * For simplicity and efficiency, use the extension, rather than the contents.
277
     *
278
     * @return string
279
     */
280
    public function mimeType(): string
281
    {
282
        $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION));
283
284
        return Mime::TYPES[$extension] ?? Mime::DEFAULT_TYPE;
285
    }
286
287
    /**
288
     * Generate a URL to download a media file.
289
     *
290
     * @param string $disposition How should the image be returned - "attachment" or "inline"
291
     *
292
     * @return string
293
     */
294
    public function downloadUrl(string $disposition): string
295
    {
296
        // The "mark" parameter is ignored, but needed for cache-busting.
297
        return route(MediaFileDownload::class, [
298
            'xref'        => $this->media->xref(),
299
            'tree'        => $this->media->tree()->name(),
300
            'fact_id'     => $this->fact_id,
301
            'disposition' => $disposition,
302
            'mark'        => Factory::image()->fileNeedsWatermark($this, Auth::user())
303
        ]);
304
    }
305
306
    /**
307
     * A list of image attributes
308
     *
309
     * @param FilesystemInterface $data_filesystem
310
     *
311
     * @return string[]
312
     */
313
    public function attributes(FilesystemInterface $data_filesystem): array
314
    {
315
        $attributes = [];
316
317
        if (!$this->isExternal() || $this->fileExists($data_filesystem)) {
318
            try {
319
                $bytes                       = $this->media()->tree()->mediaFilesystem($data_filesystem)->getSize($this->filename());
320
                $kb                          = intdiv($bytes + 1023, 1024);
321
                $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb));
322
            } catch (FileNotFoundException $ex) {
323
                // External/missing files have no size.
324
            }
325
326
            // Note: getAdapter() is defined on Filesystem, but not on FilesystemInterface.
327
            $filesystem = $this->media()->tree()->mediaFilesystem($data_filesystem);
328
            if ($filesystem instanceof Filesystem) {
0 ignored issues
show
introduced by
$filesystem is always a sub-type of League\Flysystem\Filesystem.
Loading history...
329
                $adapter = $filesystem->getAdapter();
330
                // Only works for local filesystems.
331
                if ($adapter instanceof Local) {
332
                    $file = $adapter->applyPathPrefix($this->filename());
333
                    [$width, $height] = getimagesize($file);
334
                    $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height));
335
                }
336
            }
337
        }
338
339
        return $attributes;
340
    }
341
342
    /**
343
     * Read the contents of a media file.
344
     *
345
     * @param FilesystemInterface $data_filesystem
346
     *
347
     * @return string
348
     */
349
    public function fileContents(FilesystemInterface $data_filesystem): string
350
    {
351
        return $this->media->tree()->mediaFilesystem($data_filesystem)->read($this->multimedia_file_refn);
352
    }
353
354
    /**
355
     * Check if the file exists on this server
356
     *
357
     * @param FilesystemInterface $data_filesystem
358
     *
359
     * @return bool
360
     */
361
    public function fileExists(FilesystemInterface $data_filesystem): bool
362
    {
363
        return $this->media->tree()->mediaFilesystem($data_filesystem)->has($this->multimedia_file_refn);
364
    }
365
366
    /**
367
     * @return Media
368
     */
369
    public function media(): Media
370
    {
371
        return $this->media;
372
    }
373
374
    /**
375
     * Get the filename.
376
     *
377
     * @return string
378
     */
379
    public function filename(): string
380
    {
381
        return $this->multimedia_file_refn;
382
    }
383
384
    /**
385
     * What file extension is used by this file?
386
     *
387
     * @return string
388
     *
389
     * @deprecated since 2.0.4.  Will be removed in 2.1.0
390
     */
391
    public function extension(): string
392
    {
393
        return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION);
394
    }
395
396
    /**
397
     * Create a URL signature paramete, using the same algorithm as league/glide,
398
     * for compatibility with URLs generated by older versions of webtrees.
399
     *
400
     * @param array<mixed> $params
401
     *
402
     * @return string
403
     */
404
    public function signature(array $params): string
405
    {
406
        unset($params['s']);
407
408
        ksort($params);
409
410
        // Sign the URL, to protect against mass-resize attacks.
411
        $glide_key = Site::getPreference('glide-key');
412
413
        if ($glide_key === '') {
414
            $glide_key = bin2hex(random_bytes(128));
415
            Site::setPreference('glide-key', $glide_key);
416
        }
417
418
        return md5($glide_key . ':?' . http_build_query($params));
419
    }
420
}
421