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