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