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