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