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 */ 16namespace Fisharebest\Webtrees; 17 18use ErrorException; 19use 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 */ 26class 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 * Where is the file stored on disk? 190 */ 191 private function folder(): string { 192 return WT_DATA_DIR . $this->media->getTree()->getPreference('MEDIA_DIRECTORY'); 193 } 194 195 /** 196 * Is the media file actually a URL? 197 */ 198 public function isExternal(): bool { 199 return strpos($this->multimedia_file_refn, '://') !== false; 200 } 201 202 /** 203 * Is the media file an image? 204 */ 205 public function isImage(): bool { 206 return in_array($this->extension(), ['jpeg', 'jpg', 'gif', 'png']); 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); 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 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); 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