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 Fisharebest\Webtrees\Functions\FunctionsPrintFacts; 19use League\Glide\Urls\UrlBuilderFactory; 20 21/** 22 * A GEDCOM media (OBJE) object. 23 */ 24class Media extends GedcomRecord { 25 const RECORD_TYPE = 'OBJE'; 26 const URL_PREFIX = 'mediaviewer.php?mid='; 27 28 /** @var string The "TITL" value from the GEDCOM */ 29 private $title = ''; 30 31 /** @var string The "FILE" value from the GEDCOM */ 32 private $file = ''; 33 34 /** 35 * Create a GedcomRecord object from raw GEDCOM data. 36 * 37 * @param string $xref 38 * @param string $gedcom an empty string for new/pending records 39 * @param string|null $pending null for a record with no pending edits, 40 * empty string for records with pending deletions 41 * @param Tree $tree 42 */ 43 public function __construct($xref, $gedcom, $pending, $tree) { 44 parent::__construct($xref, $gedcom, $pending, $tree); 45 46 if (preg_match('/\n1 FILE (.+)/', $gedcom . $pending, $match)) { 47 $this->file = $match[1]; 48 } 49 if (preg_match('/\n\d TITL (.+)/', $gedcom . $pending, $match)) { 50 $this->title = $match[1]; 51 } 52 } 53 54 /** 55 * Each object type may have its own special rules, and re-implement this function. 56 * 57 * @param int $access_level 58 * 59 * @return bool 60 */ 61 protected function canShowByType($access_level) { 62 // Hide media objects if they are attached to private records 63 $linked_ids = Database::prepare( 64 "SELECT l_from FROM `##link` WHERE l_to = ? AND l_file = ?" 65 )->execute([ 66 $this->xref, $this->tree->getTreeId(), 67 ])->fetchOneColumn(); 68 foreach ($linked_ids as $linked_id) { 69 $linked_record = GedcomRecord::getInstance($linked_id, $this->tree); 70 if ($linked_record && !$linked_record->canShow($access_level)) { 71 return false; 72 } 73 } 74 75 // ... otherwise apply default behaviour 76 return parent::canShowByType($access_level); 77 } 78 79 /** 80 * Fetch data from the database 81 * 82 * @param string $xref 83 * @param int $tree_id 84 * 85 * @return null|string 86 */ 87 protected static function fetchGedcomRecord($xref, $tree_id) { 88 return Database::prepare( 89 "SELECT m_gedcom FROM `##media` WHERE m_id = :xref AND m_file = :tree_id" 90 )->execute([ 91 'xref' => $xref, 92 'tree_id' => $tree_id, 93 ])->fetchOne(); 94 } 95 96 /** 97 * Get the first note attached to this media object 98 * 99 * @return null|string 100 */ 101 public function getNote() { 102 $note = $this->getFirstFact('NOTE'); 103 if ($note) { 104 $text = $note->getValue(); 105 if (preg_match('/^@' . WT_REGEX_XREF . '@$/', $text)) { 106 $text = $note->getTarget()->getNote(); 107 } 108 109 return $text; 110 } else { 111 return ''; 112 } 113 } 114 115 /** 116 * Get the main media filename 117 * 118 * @return string 119 */ 120 public function getFilename() { 121 return $this->file; 122 } 123 124 /** 125 * Get the media's title (name) 126 * 127 * @return string 128 */ 129 public function getTitle() { 130 return $this->title; 131 } 132 133 /** 134 * Get the filename on the server - for those (very few!) functions which actually 135 * need the filename, such as mediafirewall.php and the PDF reports. 136 * 137 * @param string $which 138 * 139 * @return string 140 */ 141 public function getServerFilename($which = 'main') { 142 $MEDIA_DIRECTORY = $this->tree->getPreference('MEDIA_DIRECTORY'); 143 144 if ($this->isExternal() || !$this->file) { 145 // External image, or (in the case of corrupt GEDCOM data) no image at all 146 return $this->file; 147 } elseif ($which == 'main') { 148 // Main image 149 return WT_DATA_DIR . $MEDIA_DIRECTORY . $this->file; 150 } else { 151 // Thumbnail 152 $file = WT_DATA_DIR . $MEDIA_DIRECTORY . 'thumbs/' . $this->file; 153 // Does the thumbnail exist? 154 if (file_exists($file)) { 155 return $file; 156 } 157 // Does a user-generated thumbnail exist? 158 $user_thumb = preg_replace('/\.[a-z0-9]{3,5}$/i', '.png', $file); 159 if (file_exists($user_thumb)) { 160 return $user_thumb; 161 } 162 163 // Is there a corresponding main image? 164 $main_file = WT_DATA_DIR . $MEDIA_DIRECTORY . $this->file; 165 if (!file_exists($main_file)) { 166 Log::addMediaLog('The file ' . $main_file . ' does not exist for ' . $this->getXref()); 167 168 return $file; 169 } 170 171 return $file; 172 } 173 } 174 175 /** 176 * check if the file exists on this server 177 * 178 * @param string $which specify either 'main' or 'thumb' 179 * 180 * @return bool 181 */ 182 public function fileExists($which = 'main') { 183 return file_exists($this->getServerFilename($which)); 184 } 185 186 /** 187 * Determine if the file is an external url 188 * 189 * @return bool 190 */ 191 public function isExternal() { 192 return strpos($this->file, '://') !== false; 193 } 194 195 /** 196 * get the media file size in KB 197 * 198 * @param string $which specify either 'main' or 'thumb' 199 * 200 * @return string 201 */ 202 public function getFilesize($which = 'main') { 203 $size = $this->getFilesizeraw($which); 204 // Round up to the nearest KB. 205 $size = (int) (($size + 1023) / 1024); 206 207 return /* I18N: size of file in KB */ 208 I18N::translate('%s KB', I18N::number($size)); 209 } 210 211 /** 212 * get the media file size, unformatted 213 * 214 * @param string $which specify either 'main' or 'thumb' 215 * 216 * @return int 217 */ 218 public function getFilesizeraw($which = 'main') { 219 try { 220 return filesize($this->getServerFilename($which)); 221 } catch (\ErrorException $ex) { 222 return 0; 223 } 224 } 225 226 /** 227 * get filemtime for the media file 228 * 229 * @param string $which specify either 'main' or 'thumb' 230 * 231 * @return int 232 */ 233 public function getFiletime($which = 'main') { 234 try { 235 return filemtime($this->getServerFilename($which)); 236 } catch (\ErrorException $ex) { 237 return 0; 238 } 239 } 240 241 /** 242 * Generate an etag specific to this media item and the current user 243 * 244 * @param string $which - specify either 'main' or 'thumb' 245 * 246 * @return string 247 */ 248 public function getEtag($which = 'main') { 249 if ($this->isExternal()) { 250 // etag not really defined for external media 251 252 return ''; 253 } 254 $etag_string = basename($this->getServerFilename($which)) . $this->getFiletime($which) . $this->tree->getName() . Auth::accessLevel($this->tree) . $this->tree->getPreference('SHOW_NO_WATERMARK'); 255 $etag_string = dechex(crc32($etag_string)); 256 257 return $etag_string; 258 } 259 260 /** 261 * Deprecated? This does not need to be a function here. 262 * 263 * @return string 264 */ 265 public function getMediaType() { 266 if (preg_match('/\n\d TYPE (.+)/', $this->gedcom, $match)) { 267 return strtolower($match[1]); 268 } else { 269 return ''; 270 } 271 } 272 273 /** 274 * Is this object marked as a highlighted image? 275 * 276 * @return string 277 */ 278 public function isPrimary() { 279 if (preg_match('/\n\d _PRIM ([YN])/', $this->getGedcom(), $match)) { 280 return $match[1]; 281 } else { 282 return ''; 283 } 284 } 285 286 /** 287 * get image properties 288 * 289 * @param string $which specify either 'main' or 'thumb' 290 * @param int $addWidth amount to add to width 291 * @param int $addHeight amount to add to height 292 * 293 * @return array 294 */ 295 public function getImageAttributes($which = 'main', $addWidth = 0, $addHeight = 0) { 296 $var = $which . 'imagesize'; 297 if (!empty($this->$var)) { 298 return $this->$var; 299 } 300 $imgsize = []; 301 if ($this->fileExists($which)) { 302 303 try { 304 $imgsize = getimagesize($this->getServerFilename($which)); 305 if (is_array($imgsize) && !empty($imgsize['0'])) { 306 // this is an image 307 $imgsize[0] = $imgsize[0] + 0; 308 $imgsize[1] = $imgsize[1] + 0; 309 $imgsize['adjW'] = $imgsize[0] + $addWidth; // adjusted width 310 $imgsize['adjH'] = $imgsize[1] + $addHeight; // adjusted height 311 $imageTypes = ['', 'GIF', 'JPG', 'PNG', 'SWF', 'PSD', 'BMP', 'TIFF', 'TIFF', 'JPC', 'JP2', 'JPX', 'JB2', 'SWC', 'IFF', 'WBMP', 'XBM']; 312 $imgsize['ext'] = $imageTypes[0 + $imgsize[2]]; 313 // this is for display purposes, always show non-adjusted info 314 $imgsize['WxH'] 315 = /* I18N: image dimensions, width × height */ 316 I18N::translate('%1$s × %2$s pixels', I18N::number($imgsize['0']), I18N::number($imgsize['1'])); 317 $imgsize['imgWH'] = ' width="' . $imgsize['adjW'] . '" height="' . $imgsize['adjH'] . '" '; 318 } 319 } catch (\ErrorException $ex) { 320 // Not an image, or not a valid image? 321 $imgsize = false; 322 } 323 } 324 325 if (!is_array($imgsize) || empty($imgsize['0'])) { 326 // this is not an image, OR the file doesn’t exist OR it is a url 327 $imgsize[0] = 0; 328 $imgsize[1] = 0; 329 $imgsize['adjW'] = 0; 330 $imgsize['adjH'] = 0; 331 $imgsize['ext'] = ''; 332 $imgsize['mime'] = ''; 333 $imgsize['WxH'] = ''; 334 $imgsize['imgWH'] = ''; 335 } 336 337 if (empty($imgsize['mime'])) { 338 // this is not an image, OR the file doesn’t exist OR it is a url 339 // set file type equal to the file extension - can’t use parse_url because this may not be a full url 340 $exp = explode('?', $this->file); 341 $imgsize['ext'] = strtoupper(pathinfo($exp[0], PATHINFO_EXTENSION)); 342 // all mimetypes we wish to serve with the media firewall must be added to this array. 343 $mime = [ 344 'DOC' => 'application/msword', 345 'MOV' => 'video/quicktime', 346 'MP3' => 'audio/mpeg', 347 'PDF' => 'application/pdf', 348 'PPT' => 'application/vnd.ms-powerpoint', 349 'RTF' => 'text/rtf', 350 'SID' => 'image/x-mrsid', 351 'TXT' => 'text/plain', 352 'XLS' => 'application/vnd.ms-excel', 353 'WMV' => 'video/x-ms-wmv', 354 ]; 355 if (empty($mime[$imgsize['ext']])) { 356 // if we don’t know what the mimetype is, use something ambiguous 357 $imgsize['mime'] = 'application/octet-stream'; 358 if ($this->fileExists($which)) { 359 // alert the admin if we cannot determine the mime type of an existing file 360 // as the media firewall will be unable to serve this file properly 361 Log::addMediaLog('Media Firewall error: >Unknown Mimetype< for file >' . $this->file . '<'); 362 } 363 } else { 364 $imgsize['mime'] = $mime[$imgsize['ext']]; 365 } 366 } 367 $this->$var = $imgsize; 368 369 return $this->$var; 370 } 371 372 /** 373 * Generate a URL directly to the media file 374 * 375 * @param string $which 376 * @param bool $download 377 * 378 * @return string 379 */ 380 public function getHtmlUrlDirect($which = 'main', $download = false) { 381 return $this->imageUrl(0, 0, ''); 382 } 383 384 /** 385 * @param int $width max width in pixels 386 * @param int $height max height in pixels 387 * @param string $fit "crop" or "" 388 * 389 * @return string 390 */ 391 public function imageUrl($width, $height, $fit) { 392 // Sign the URL, to protect against mass-resize attacks. 393 $glide_key = Site::getPreference('glide-key'); 394 if (empty($glide_key)) { 395 $glide_key = bin2hex(random_bytes(128)); 396 Site::setPreference('glide-key', $glide_key); 397 } 398 399 if (Auth::accessLevel($this->getTree()) > $this->getTree()->getPreference('SHOW_NO_WATERMARK')) { 400 $mark = 'watermark.png'; 401 } else { 402 $mark = ''; 403 } 404 405 $url = UrlBuilderFactory::create(WT_BASE_URL, $glide_key) 406 ->getUrl('mediafirewall.php', [ 407 'mid' => $this->getXref(), 408 'ged' => $this->tree->getName(), 409 'w' => $width, 410 'h' => $height, 411 'fit' => $fit, 412 'mark' => $mark, 413 'markh' => '100h', 414 'markw' => '100w', 415 'markalpha' => 25, 416 'or' => 0, // Intervention uses exif_read_data() which is very buggy. 417 ]); 418 419 return $url; 420 } 421 422 /** 423 * What file extension is used by this file? 424 * 425 * @return string 426 */ 427 public function extension() { 428 if (preg_match('/\.([a-zA-Z0-9]+)$/', $this->file, $match)) { 429 return strtolower($match[1]); 430 } else { 431 return ''; 432 } 433 } 434 435 /** 436 * What is the mime-type of this object? 437 * For simplicity and efficiency, use the extension, rather than the contents. 438 * 439 * @return string 440 */ 441 public function mimeType() { 442 // Themes contain icon definitions for some/all of these mime-types 443 switch ($this->extension()) { 444 case 'bmp': 445 return 'image/bmp'; 446 case 'doc': 447 return 'application/msword'; 448 case 'docx': 449 return 'application/msword'; 450 case 'ged': 451 return 'text/x-gedcom'; 452 case 'gif': 453 return 'image/gif'; 454 case 'htm': 455 return 'text/html'; 456 case 'html': 457 return 'text/html'; 458 case 'jpeg': 459 return 'image/jpeg'; 460 case 'jpg': 461 return 'image/jpeg'; 462 case 'mov': 463 return 'video/quicktime'; 464 case 'mp3': 465 return 'audio/mpeg'; 466 case 'mp4': 467 return 'video/mp4'; 468 case 'ogv': 469 return 'video/ogg'; 470 case 'pdf': 471 return 'application/pdf'; 472 case 'png': 473 return 'image/png'; 474 case 'rar': 475 return 'application/x-rar-compressed'; 476 case 'swf': 477 return 'application/x-shockwave-flash'; 478 case 'svg': 479 return 'image/svg'; 480 case 'tif': 481 return 'image/tiff'; 482 case 'tiff': 483 return 'image/tiff'; 484 case 'xls': 485 return 'application/vnd-ms-excel'; 486 case 'xlsx': 487 return 'application/vnd-ms-excel'; 488 case 'wmv': 489 return 'video/x-ms-wmv'; 490 case 'zip': 491 return 'application/zip'; 492 default: 493 return 'application/octet-stream'; 494 } 495 } 496 497 /** 498 * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox. 499 * 500 * @param int $width Pixels 501 * @param int $height Pixels 502 * @param string $fit "crop" or "contain" 503 * @param string[] $attributes Additional HTML attributes 504 * 505 * @return string 506 */ 507 public function displayImage($width, $height, $fit, $attributes = []) { 508 // Default image for external, missing or corrupt images. 509 $image 510 = '<i' . 511 ' dir="auto"' . // For the tool-tip 512 ' class="icon-mime-' . str_replace('/', '-', $this->mimeType()) . '"' . 513 ' title="' . strip_tags($this->getFullName()) . '"' . 514 '></i>'; 515 516 // Use a thumbnail image. 517 if ($this->isExternal()) { 518 $src = $this->getFilename(); 519 $srcset = []; 520 } else { 521 // Generate multiple images for displays with higher pixel densities. 522 $src = $this->imageUrl($width, $height, $fit); 523 $srcset = []; 524 foreach ([2,3,4] as $x) { 525 $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x'; 526 } 527 } 528 529 $image = '<img ' . Html::attributes($attributes + [ 530 'dir' => 'auto', 531 'src' => $src, 532 'srcset' => implode(',', $srcset), 533 'alt' => strip_tags($this->getFullName()), 534 'title' => strip_tags($this->getFullName()), 535 ]) . '>'; 536 537 return 538 '<a' . 539 ' class="gallery"' . 540 ' href="' . $this->getHtmlUrlDirect('main') . '"' . 541 ' type="' . $this->mimeType() . '"' . 542 ' data-obje-url="' . $this->getHtmlUrl() . '"' . 543 ' data-obje-note="' . Filter::escapeHtml($this->getNote()) . '"' . 544 ' data-title="' . Filter::escapeHtml($this->getFullName()) . '"' . 545 '>' . $image . '</a>'; 546 } 547 548 /** 549 * If this object has no name, what do we call it? 550 * 551 * @return string 552 */ 553 public function getFallBackName() { 554 if ($this->canShow()) { 555 return basename($this->file); 556 } else { 557 return $this->getXref(); 558 } 559 } 560 561 /** 562 * Extract names from the GEDCOM record. 563 */ 564 public function extractNames() { 565 // Earlier gedcom versions had level 1 titles 566 // Later gedcom versions had level 2 titles 567 $this->extractNamesFromFacts(2, 'TITL', $this->getFacts('FILE')); 568 $this->extractNamesFromFacts(1, 'TITL', $this->getFacts('TITL')); 569 } 570 571 /** 572 * This function should be redefined in derived classes to show any major 573 * identifying characteristics of this record. 574 * 575 * @return string 576 */ 577 public function formatListDetails() { 578 ob_start(); 579 FunctionsPrintFacts::printMediaLinks('1 OBJE @' . $this->getXref() . '@', 1); 580 581 return ob_get_clean(); 582 } 583} 584