Passed
Branch master (380e00)
by Greg
20:17
created

MediaFile::type()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2017 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
namespace Fisharebest\Webtrees;
17
18
use ErrorException;
19
use League\Glide\Urls\UrlBuilderFactory;
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
 */
26
class MediaFile {
27
	/** @var string The filename */
28
	private $multimedia_file_refn = '';
29
30
	/** @var string The file extension; jpeg, txt, mp4, etc. */
31
	private $multimedia_format = '';
32
33
	/** @var string The type of document; newspaper, microfiche, etc. */
34
	private $source_media_type = '';
35
	/** @var string The filename */
36
37
	/** @var string The name of the document */
38
	private $descriptive_title = '';
39
40
	/** @var Media $media The media object to which this file belongs */
41
	private $media;
42
43
	/** @var string */
44
	private $fact_id;
45
46
	/**
47
	 * Create a MediaFile from raw GEDCOM data.
48
	 *
49
	 * @param string $gedcom
50
	 * @param Media  $media
51
	 */
52
	public function __construct($gedcom, Media $media) {
53
		$this->media   = $media;
54
		$this->fact_id = md5($gedcom);
55
56
		if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) {
57
			$this->multimedia_file_refn = $match[1];
58
		}
59
60
		if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) {
61
			$this->multimedia_format = $match[1];
62
		}
63
64
		if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) {
65
			$this->source_media_type = $match[1];
66
		}
67
68
		if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) {
69
			$this->descriptive_title = $match[1];
70
		}
71
	}
72
73
	/**
74
	 * Get the filename.
75
	 *
76
	 * @return string
77
	 */
78
	public function filename(): string {
79
		return $this->multimedia_file_refn;
80
	}
81
82
	/**
83
	 * Get the format.
84
	 *
85
	 * @return string
86
	 */
87
	public function format(): string {
88
		return $this->multimedia_format;
89
	}
90
91
	/**
92
	 * Get the type.
93
	 *
94
	 * @return string
95
	 */
96
	public function type(): string {
97
		return $this->source_media_type;
98
	}
99
100
	/**
101
	 * Get the title.
102
	 *
103
	 * @return string
104
	 */
105
	public function title(): string {
106
		return $this->descriptive_title;
107
	}
108
109
	/**
110
	 * Get the fact ID.
111
	 *
112
	 * @return string
113
	 */
114
	public function factId(): string {
115
		return $this->fact_id;
116
	}
117
118
	/**
119
	 * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox.
120
	 *
121
	 * @param int      $width      Pixels
122
	 * @param int      $height     Pixels
123
	 * @param string   $fit        "crop" or "contain"
124
	 * @param string[] $attributes Additional HTML attributes
125
	 *
126
	 * @return string
127
	 */
128
	public function displayImage($width, $height, $fit, $attributes = []) {
129
		if ($this->isExternal()) {
130
			$src    = $this->multimedia_file_refn;
131
			$srcset = [];
132
		} else {
133
			// Generate multiple images for displays with higher pixel densities.
134
			$src    = $this->imageUrl($width, $height, $fit);
135
			$srcset = [];
136
			foreach ([2, 3, 4] as $x) {
137
				$srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x';
138
			}
139
		}
140
141
		$image = '<img ' . Html::attributes($attributes + [
142
					'dir'    => 'auto',
143
					'src'    => $src,
144
					'srcset' => implode(',', $srcset),
145
					'alt'    => strip_tags($this->media->getFullName()),
146
				]) . '>';
147
148
		$attributes = Html::attributes([
149
			'class' => 'gallery',
150
			'type'  => $this->mimeType(),
151
			'href'  => $this->imageUrl(0, 0, ''),
152
		]);
153
154
		return '<a ' . $attributes . '>' . $image . '</a>';
155
	}
156
157
	/**
158
	 * A list of image attributes
159
	 *
160
	 * @return string[]
161
	 */
162
	public function attributes(): array {
163
		$attributes = [];
164
165
		if (!$this->isExternal() || $this->fileExists()) {
166
			$file = $this->folder() . $this->multimedia_file_refn;
167
168
			$attributes['__FILE_SIZE__'] = $this->fileSizeKB();
169
170
			$imgsize = getimagesize($file);
171
			if (is_array($imgsize) && !empty($imgsize['0'])) {
172
				$attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($imgsize['0']), I18N::number($imgsize['1']));
173
			}
174
		}
175
176
		return $attributes;
177
	}
178
179
	/**
180
	 * check if the file exists on this server
181
	 *
182
	 * @return bool
183
	 */
184
	public function fileExists() {
185
		return file_exists($this->folder() . $this->multimedia_file_refn);
186
	}
187
188
	/**
189
	 * Is the media file actually a URL?
190
	 */
191
	public function isExternal(): bool {
192
		return strpos($this->multimedia_file_refn, '://') !== false;
193
	}
194
195
	/**
196
	 * Is the media file an image?
197
	 */
198
	public function isImage(): bool {
199
		return in_array($this->extension(), ['jpeg', 'jpg', 'gif', 'png']);
200
	}
201
202
	/**
203
	 * Where is the file stored on disk?
204
	 */
205
	public function folder(): string {
206
		return WT_DATA_DIR . $this->media->getTree()->getPreference('MEDIA_DIRECTORY');
207
	}
208
209
	/**
210
	 * A user-friendly view of the file size
211
	 *
212
	 * @return int
213
	 */
214
	private function fileSizeBytes(): int {
215
		try {
216
			return filesize($this->folder() . $this->multimedia_file_refn);
217
		} catch (ErrorException $ex) {
218
			DebugBar::addThrowable($ex);
0 ignored issues
show
Documentation introduced by
$ex is of type object<ErrorException>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
219
220
			return 0;
221
		}
222
	}
223
224
	/**
225
	 * get the media file size in KB
226
	 *
227
	 * @return string
228
	 */
229
	public function fileSizeKB() {
230
		$size = $this->filesizeBytes();
231
		$size = (int) (($size + 1023) / 1024);
232
233
		return /* I18N: size of file in KB */ I18N::translate('%s KB', I18N::number($size));
234
	}
235
236
	///////////////////////////////////////////////////////////////////////////
237
238
	/**
239
	 * Get the filename on the server - for those (very few!) functions which actually
240
	 * need the filename, such as mediafirewall.php and the PDF reports.
241
	 *
242
	 * @return string
243
	 */
244
	public function getServerFilename() {
245
		$MEDIA_DIRECTORY = $this->media->getTree()->getPreference('MEDIA_DIRECTORY');
246
247
		if ($this->isExternal() || !$this->multimedia_file_refn) {
248
			// External image, or (in the case of corrupt GEDCOM data) no image at all
249
			return $this->multimedia_file_refn;
250
		} else {
251
			// Main image
252
			return WT_DATA_DIR . $MEDIA_DIRECTORY . $this->multimedia_file_refn;
253
		}
254
	}
255
256
	/**
257
	 * get image properties
258
	 *
259
	 * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be false|array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
260
	 */
261
	public function getImageAttributes() {
262
		$imgsize = [];
263
		if ($this->fileExists()) {
264
			try {
265
				$imgsize = getimagesize($this->getServerFilename());
266
				if (is_array($imgsize) && !empty($imgsize['0'])) {
267
					// this is an image
268
					$imageTypes     = ['', 'GIF', 'JPG', 'PNG', 'SWF', 'PSD', 'BMP', 'TIFF', 'TIFF', 'JPC', 'JP2', 'JPX', 'JB2', 'SWC', 'IFF', 'WBMP', 'XBM'];
269
					$imgsize['ext'] = $imageTypes[0 + $imgsize[2]];
270
					// this is for display purposes, always show non-adjusted info
271
					$imgsize['WxH'] = /* I18N: image dimensions, width × height */
272
						I18N::translate('%1$s × %2$s pixels', I18N::number($imgsize['0']), I18N::number($imgsize['1']));
273
				}
274
			} catch (ErrorException $ex) {
275
				DebugBar::addThrowable($ex);
0 ignored issues
show
Documentation introduced by
$ex is of type object<ErrorException>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
276
277
				// Not an image, or not a valid image?
278
				$imgsize = false;
279
			}
280
		}
281
282
		if (!is_array($imgsize) || empty($imgsize['0'])) {
283
			// this is not an image, OR the file doesn’t exist OR it is a url
284
			$imgsize[0]      = 0;
285
			$imgsize[1]      = 0;
286
			$imgsize['ext']  = '';
287
			$imgsize['mime'] = '';
288
			$imgsize['WxH']  = '';
289
		}
290
291
		if (empty($imgsize['mime'])) {
292
			// this is not an image, OR the file doesn’t exist OR it is a url
293
			// set file type equal to the file extension - can’t use parse_url because this may not be a full url
294
			$exp            = explode('?', $this->multimedia_file_refn);
295
			$imgsize['ext'] = strtoupper(pathinfo($exp[0], PATHINFO_EXTENSION));
296
			// all mimetypes we wish to serve with the media firewall must be added to this array.
297
			$mime = [
298
				'DOC' => 'application/msword',
299
				'MOV' => 'video/quicktime',
300
				'MP3' => 'audio/mpeg',
301
				'PDF' => 'application/pdf',
302
				'PPT' => 'application/vnd.ms-powerpoint',
303
				'RTF' => 'text/rtf',
304
				'SID' => 'image/x-mrsid',
305
				'TXT' => 'text/plain',
306
				'XLS' => 'application/vnd.ms-excel',
307
				'WMV' => 'video/x-ms-wmv',
308
			];
309
			if (empty($mime[$imgsize['ext']])) {
310
				// if we don’t know what the mimetype is, use something ambiguous
311
				$imgsize['mime'] = 'application/octet-stream';
312
				if ($this->fileExists()) {
313
					// alert the admin if we cannot determine the mime type of an existing file
314
					// as the media firewall will be unable to serve this file properly
315
					Log::addMediaLog('Media Firewall error: >Unknown Mimetype< for file >' . $this->multimedia_file_refn . '<');
316
				}
317
			} else {
318
				$imgsize['mime'] = $mime[$imgsize['ext']];
319
			}
320
		}
321
322
		return $imgsize;
323
	}
324
325
	/**
326
	 * Generate a URL for an image.
327
	 *
328
	 * @param int    $width  Maximum width in pixels
329
	 * @param int    $height Maximum height in pixels
330
	 * @param string $fit    "crop" or "contain"
331
	 *
332
	 * @return string
333
	 */
334
	public function imageUrl($width, $height, $fit) {
335
		// Sign the URL, to protect against mass-resize attacks.
336
		$glide_key = Site::getPreference('glide-key');
337
		if (empty($glide_key)) {
338
			$glide_key = bin2hex(random_bytes(128));
339
			Site::setPreference('glide-key', $glide_key);
340
		}
341
342
		if (Auth::accessLevel($this->media->getTree()) > $this->media->getTree()->getPreference('SHOW_NO_WATERMARK')) {
343
			$mark = 'watermark.png';
344
		} else {
345
			$mark = '';
346
		}
347
348
		$url_builder = UrlBuilderFactory::create(WT_BASE_URL, $glide_key);
349
350
		$url = $url_builder->getUrl('index.php', [
351
			'route'     => 'media-thumbnail',
352
			'xref'      => $this->media->getXref(),
353
			'ged'       => $this->media->getTree()->getName(),
354
			'fact_id'   => $this->fact_id,
355
			'w'         => $width,
356
			'h'         => $height,
357
			'fit'       => $fit,
358
			'mark'      => $mark,
359
			'markh'     => '100h',
360
			'markw'     => '100w',
361
			'markalpha' => 25,
362
			'or'        => 0, // Intervention uses exif_read_data() which is very buggy.
363
		]);
364
365
		return $url;
366
	}
367
368
	/**
369
	 * What file extension is used by this file?
370
	 *
371
	 * @return string
372
	 */
373
	public function extension() {
374
		if (preg_match('/\.([a-zA-Z0-9]+)$/', $this->multimedia_file_refn, $match)) {
375
			return strtolower($match[1]);
376
		} else {
377
			return '';
378
		}
379
	}
380
381
	/**
382
	 * What is the mime-type of this object?
383
	 * For simplicity and efficiency, use the extension, rather than the contents.
384
	 *
385
	 * @return string
386
	 */
387
	public function mimeType() {
388
		// Themes contain icon definitions for some/all of these mime-types
389
		switch ($this->extension()) {
390
			case 'bmp':
391
				return 'image/bmp';
392
			case 'doc':
393
				return 'application/msword';
394
			case 'docx':
395
				return 'application/msword';
396
			case 'ged':
397
				return 'text/x-gedcom';
398
			case 'gif':
399
				return 'image/gif';
400
			case 'htm':
401
				return 'text/html';
402
			case 'html':
403
				return 'text/html';
404
			case 'jpeg':
405
				return 'image/jpeg';
406
			case 'jpg':
407
				return 'image/jpeg';
408
			case 'mov':
409
				return 'video/quicktime';
410
			case 'mp3':
411
				return 'audio/mpeg';
412
			case 'mp4':
413
				return 'video/mp4';
414
			case 'ogv':
415
				return 'video/ogg';
416
			case 'pdf':
417
				return 'application/pdf';
418
			case 'png':
419
				return 'image/png';
420
			case 'rar':
421
				return 'application/x-rar-compressed';
422
			case 'swf':
423
				return 'application/x-shockwave-flash';
424
			case 'svg':
425
				return 'image/svg';
426
			case 'tif':
427
				return 'image/tiff';
428
			case 'tiff':
429
				return 'image/tiff';
430
			case 'xls':
431
				return 'application/vnd-ms-excel';
432
			case 'xlsx':
433
				return 'application/vnd-ms-excel';
434
			case 'wmv':
435
				return 'video/x-ms-wmv';
436
			case 'zip':
437
				return 'application/zip';
438
			default:
439
				return 'application/octet-stream';
440
		}
441
	}
442
}
443