1<?php 2/** 3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4 * 5 * This source code is licensed under the GPL license found in the 6 * COPYING file in the root directory of this source tree. 7 * 8 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9 * @author ComboStrap <support@combostrap.com> 10 * 11 */ 12 13namespace ComboStrap; 14 15use dokuwiki\Action\Plugin; 16use dokuwiki\Extension\SyntaxPlugin; 17use dokuwiki\Parsing\ParserMode\Internallink; 18use syntax_plugin_combo_media; 19 20require_once(__DIR__ . '/PluginUtility.php'); 21 22/** 23 * Class InternalMedia 24 * Represent a media link 25 * 26 * 27 * @package ComboStrap 28 * 29 * Wrapper around {@link Doku_Handler_Parse_Media} 30 * 31 * Not that for dokuwiki the `type` key of the attributes is the `call` 32 * and therefore determine the function in an render 33 * (ie {@link \Doku_Renderer::internalmedialink()} or {@link \Doku_Renderer::externalmedialink()} 34 * 35 * This is a link to a media (pdf, image, ...). 36 * It's used to check the media type and to 37 * take over if the media type is an image 38 */ 39abstract class MediaLink 40{ 41 42 43 /** 44 * The dokuwiki type and mode name 45 * (ie call) 46 * * ie {@link MediaLink::EXTERNAL_MEDIA_CALL_NAME} 47 * or {@link MediaLink::INTERNAL_MEDIA_CALL_NAME} 48 * 49 * The dokuwiki type (internalmedia/externalmedia) 50 * is saved in a `type` key that clash with the 51 * combostrap type. To avoid the clash, we renamed it 52 */ 53 const MEDIA_DOKUWIKI_TYPE = 'dokuwiki_type'; 54 const INTERNAL_MEDIA_CALL_NAME = "internalmedia"; 55 const EXTERNAL_MEDIA_CALL_NAME = "externalmedia"; 56 57 const CANONICAL = "image"; 58 59 /** 60 * This attributes does not apply 61 * to a URL 62 * They are only for the tag (img, svg, ...) 63 * or internal 64 */ 65 const NON_URL_ATTRIBUTES = [ 66 MediaLink::ALIGN_KEY, 67 MediaLink::LINKING_KEY, 68 TagAttributes::TITLE_KEY, 69 Hover::ON_HOVER_ATTRIBUTE, 70 Animation::ON_VIEW_ATTRIBUTE, 71 MediaLink::MEDIA_DOKUWIKI_TYPE, 72 MediaLink::DOKUWIKI_SRC 73 ]; 74 75 /** 76 * This attribute applies 77 * to a image url (img, svg, ...) 78 */ 79 const URL_ATTRIBUTES = [ 80 Dimension::WIDTH_KEY, 81 Dimension::HEIGHT_KEY, 82 CacheMedia::CACHE_KEY, 83 ]; 84 85 /** 86 * Default image linking value 87 */ 88 const CONF_DEFAULT_LINKING = "defaultImageLinking"; 89 const LINKING_LINKONLY_VALUE = "linkonly"; 90 const LINKING_DETAILS_VALUE = 'details'; 91 const LINKING_NOLINK_VALUE = 'nolink'; 92 93 /** 94 * @deprecated 2021-06-12 95 */ 96 const LINK_PATTERN = "{{\s*([^|\s]*)\s*\|?.*}}"; 97 98 const LINKING_DIRECT_VALUE = 'direct'; 99 100 /** 101 * Only used by Dokuwiki 102 * Contains the path and eventually an anchor 103 * never query parameters 104 */ 105 const DOKUWIKI_SRC = "src"; 106 /** 107 * Link value: 108 * * 'nolink' 109 * * 'direct': directly to the image 110 * * 'linkonly': show only a url 111 * * 'details': go to the details media viewer 112 * 113 * @var 114 */ 115 const LINKING_KEY = 'linking'; 116 const ALIGN_KEY = 'align'; 117 118 /** 119 * The method to lazy load resources (Ie media) 120 */ 121 const LAZY_LOAD_METHOD = "lazy-method"; 122 const LAZY_LOAD_METHOD_HTML_VALUE = "html-attribute"; 123 const LAZY_LOAD_METHOD_LOZAD_VALUE = "lozad"; 124 const UNKNOWN_MIME = "unknwon"; 125 /** 126 * @var string 127 */ 128 private $lazyLoadMethod; 129 130 private $lazyLoad = null; 131 132 133 /** 134 * The path of the media 135 * @var Media[] 136 */ 137 private $media; 138 private $linking; 139 private $linkingClass; 140 141 142 /** 143 * Image constructor. 144 * @param Image $media 145 * 146 * Protected and not private 147 * to allow cascading init 148 * If private, the parent attributes are null 149 */ 150 protected function __construct(Media $media) 151 { 152 $this->media = $media; 153 } 154 155 156 /** 157 * Create an image from dokuwiki {@link Internallink internal call media attributes} 158 * 159 * Dokuwiki extracts already the width, height and align property 160 * @param array $callAttributes 161 * @return MediaLink 162 */ 163 public static function createFromIndexAttributes(array $callAttributes) 164 { 165 $src = $callAttributes[0]; 166 $title = $callAttributes[1]; 167 $align = $callAttributes[2]; 168 $width = $callAttributes[3]; 169 $height = $callAttributes[4]; 170 $cache = $callAttributes[5]; 171 $linking = $callAttributes[6]; 172 173 $tagAttributes = TagAttributes::createEmpty(); 174 $tagAttributes->addComponentAttributeValue(TagAttributes::TITLE_KEY, $title); 175 $tagAttributes->addComponentAttributeValue(self::ALIGN_KEY, $align); 176 $tagAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, $width); 177 $tagAttributes->addComponentAttributeValue(Dimension::HEIGHT_KEY, $height); 178 $tagAttributes->addComponentAttributeValue(CacheMedia::CACHE_KEY, $cache); 179 $tagAttributes->addComponentAttributeValue(self::LINKING_KEY, $linking); 180 181 return self::createMediaLinkFromId($src, $tagAttributes); 182 183 } 184 185 /** 186 * A function to explicitly create an internal media from 187 * a call stack array (ie key string and value) that we get in the {@link SyntaxPlugin::render()} 188 * from the {@link MediaLink::toCallStackArray()} 189 * 190 * @param $attributes - the attributes created by the function {@link MediaLink::getParseAttributes()} 191 * @param $rev - the mtime 192 * @return null|MediaLink 193 */ 194 public static function createFromCallStackArray($attributes, $rev = null): ?MediaLink 195 { 196 197 if (!is_array($attributes)) { 198 // Debug for the key_exist below because of the following message: 199 // `PHP Warning: key_exists() expects parameter 2 to be array, array given` 200 LogUtility::msg("The `attributes` parameter is not an array. Value ($attributes)", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 201 } 202 203 $tagAttributes = TagAttributes::createFromCallStackArray($attributes); 204 205 $src = $attributes[self::DOKUWIKI_SRC]; 206 if ($src === null) { 207 /** 208 * Dokuwiki parse already the src and create the path and the attributes 209 * The new model will not, we check if we are in the old mode 210 */ 211 $src = $attributes[PagePath::PROPERTY_NAME]; 212 if ($src === null) { 213 LogUtility::msg("src is mandatory for an image link and was not passed"); 214 return null; 215 } 216 } 217 $dokuUrl = DokuwikiUrl::createFromUrl($src); 218 $scheme = $dokuUrl->getScheme(); 219 switch ($scheme) { 220 case DokuFs::SCHEME: 221 $id = $dokuUrl->getPath(); 222 // the id is always absolute, except in a link 223 // It may be relative, transform it as absolute 224 global $ID; 225 resolve_mediaid(getNS($ID), $id, $exists); 226 $path = DokuPath::createMediaPathFromId($id, $rev); 227 return self::createMediaLinkFromPath($path, $tagAttributes); 228 case InterWikiPath::scheme: 229 $path = InterWikiPath::create($dokuUrl->getPath()); 230 return self::createMediaLinkFromPath($path, $tagAttributes); 231 case InternetPath::scheme: 232 $path = InternetPath::create($dokuUrl->getPath()); 233 return self::createMediaLinkFromPath($path, $tagAttributes); 234 default: 235 LogUtility::msg("The media with the scheme ($scheme) are not yet supported. Media Source: $src"); 236 return null; 237 238 } 239 240 241 } 242 243 /** 244 * @param $match - the match of the renderer (just a shortcut) 245 * @return MediaLink 246 */ 247 public static function createFromRenderMatch($match) 248 { 249 250 /** 251 * The parsing function {@link Doku_Handler_Parse_Media} has some flow / problem 252 * * It keeps the anchor only if there is no query string 253 * * It takes the first digit as the width (ie media.pdf?page=31 would have a width of 31) 254 * * `src` is not only the media path but may have a anchor 255 * We parse it then 256 */ 257 258 259 /** 260 * * Delete the opening and closing character 261 * * create the url and description 262 */ 263 $match = preg_replace(array('/^\{\{/', '/\}\}$/u'), '', $match); 264 $parts = explode('|', $match, 2); 265 $description = null; 266 $url = $parts[0]; 267 if (isset($parts[1])) { 268 $description = $parts[1]; 269 } 270 271 /** 272 * Media Alignment 273 */ 274 $rightAlign = (bool)preg_match('/^ /', $url); 275 $leftAlign = (bool)preg_match('/ $/', $url); 276 $url = trim($url); 277 278 // Logic = what's that ;)... 279 if ($leftAlign & $rightAlign) { 280 $align = 'center'; 281 } else if ($rightAlign) { 282 $align = 'right'; 283 } else if ($leftAlign) { 284 $align = 'left'; 285 } else { 286 $align = null; 287 } 288 289 /** 290 * The combo attributes array 291 */ 292 $dokuwikiUrl = DokuwikiUrl::createFromUrl($url); 293 $parsedAttributes = $dokuwikiUrl->toArray(); 294 $path = $dokuwikiUrl->getPath(); 295 $linkingKey = $dokuwikiUrl->getQueryParameter(MediaLink::LINKING_KEY); 296 if ($linkingKey === null) { 297 $linkingKey = PluginUtility::getConfValue(self::CONF_DEFAULT_LINKING, self::LINKING_DIRECT_VALUE); 298 } 299 $parsedAttributes[MediaLink::LINKING_KEY] = $linkingKey; 300 301 /** 302 * Media Type 303 */ 304 $scheme = $dokuwikiUrl->getScheme(); 305 if ($scheme === DokuFs::SCHEME) { 306 $mediaType = MediaLink::INTERNAL_MEDIA_CALL_NAME; 307 } else { 308 $mediaType = MediaLink::EXTERNAL_MEDIA_CALL_NAME; 309 } 310 311 312 /** 313 * src in dokuwiki is the path and the anchor if any 314 */ 315 $src = $path; 316 if (isset($parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]) != null) { 317 $src = $src . "#" . $parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]; 318 } 319 320 /** 321 * To avoid clash with the combostrap component type 322 * ie this is also a ComboStrap attribute where we set the type of a SVG (icon, illustration, background) 323 * we store the media type (ie external/internal) in another key 324 * 325 * There is no need to repeat the attributes as the arrays are merged 326 * into on but this is also an informal code to show which attributes 327 * are only Dokuwiki Native 328 * 329 */ 330 $dokuwikiAttributes = array( 331 self::MEDIA_DOKUWIKI_TYPE => $mediaType, 332 self::DOKUWIKI_SRC => $src, 333 Dimension::WIDTH_KEY => $parsedAttributes[Dimension::WIDTH_KEY], 334 Dimension::HEIGHT_KEY => $parsedAttributes[Dimension::HEIGHT_KEY], 335 CacheMedia::CACHE_KEY => $parsedAttributes[CacheMedia::CACHE_KEY], 336 TagAttributes::TITLE_KEY => $description, 337 MediaLink::ALIGN_KEY => $align, 338 MediaLink::LINKING_KEY => $parsedAttributes[MediaLink::LINKING_KEY], 339 ); 340 341 /** 342 * Merge standard dokuwiki attributes and 343 * parsed attributes 344 */ 345 $mergedAttributes = PluginUtility::mergeAttributes($dokuwikiAttributes, $parsedAttributes); 346 347 /** 348 * If this is an internal media, 349 * we are using our implementation 350 * and we have a change on attribute specification 351 */ 352 if ($mediaType == MediaLink::INTERNAL_MEDIA_CALL_NAME) { 353 354 /** 355 * The align attribute on an image parse 356 * is a float right 357 * ComboStrap does a difference between a block right and a float right 358 */ 359 if ($mergedAttributes[self::ALIGN_KEY] === "right") { 360 unset($mergedAttributes[self::ALIGN_KEY]); 361 $mergedAttributes[FloatAttribute::FLOAT_KEY] = "right"; 362 } 363 364 365 } 366 367 return self::createFromCallStackArray($mergedAttributes); 368 369 } 370 371 372 public 373 function setLazyLoad($false): MediaLink 374 { 375 $this->lazyLoad = $false; 376 return $this; 377 } 378 379 public 380 function getLazyLoad() 381 { 382 return $this->lazyLoad; 383 } 384 385 386 /** 387 * Create a media link from a wiki id 388 * 389 * 390 * @param $wikiId - dokuwiki id 391 * @param TagAttributes|null $tagAttributes 392 * @param string|null $rev 393 * @return MediaLink 394 */ 395 public 396 static function createMediaLinkFromId($wikiId, ?string $rev = '', TagAttributes $tagAttributes = null) 397 { 398 if (is_object($rev)) { 399 LogUtility::msg("rev should not be an object", LogUtility::LVL_MSG_ERROR, "support"); 400 } 401 if ($tagAttributes == null) { 402 $tagAttributes = TagAttributes::createEmpty(); 403 } else { 404 if (!($tagAttributes instanceof TagAttributes)) { 405 LogUtility::msg("TagAttributes is not an instance of Tag Attributes", LogUtility::LVL_MSG_ERROR, "support"); 406 } 407 } 408 409 $dokuPath = DokuPath::createMediaPathFromId($wikiId, $rev); 410 return self::createMediaLinkFromPath($dokuPath, $tagAttributes); 411 412 } 413 414 /** 415 * @param Path $path 416 * @param TagAttributes|null $tagAttributes 417 * @return RasterImageLink|SvgImageLink|ThirdMediaLink 418 */ 419 public static function createMediaLinkFromPath(Path $path, TagAttributes $tagAttributes = null) 420 { 421 422 if ($tagAttributes === null) { 423 $tagAttributes = TagAttributes::createEmpty(); 424 } 425 426 /** 427 * Get and delete the attribute for the link 428 * (The rest is for the image) 429 */ 430 $lazyLoadMethod = $tagAttributes->getValueAndRemoveIfPresent(self::LAZY_LOAD_METHOD, self::LAZY_LOAD_METHOD_LOZAD_VALUE); 431 $linking = $tagAttributes->getValueAndRemoveIfPresent(self::LINKING_KEY); 432 $linkingClass = $tagAttributes->getValueAndRemoveIfPresent(syntax_plugin_combo_media::LINK_CLASS_ATTRIBUTE); 433 434 /** 435 * Processing 436 */ 437 $mime = $path->getMime(); 438 if ($path->getExtension() === "svg") { 439 /** 440 * The mime type is set when uploading, not when 441 * viewing. 442 * Because they are internal image, the svg was already uploaded 443 * Therefore, no authorization scheme here 444 */ 445 $mime = Mime::create(Mime::SVG); 446 } 447 448 if ($mime === null) { 449 $stringMime = self::UNKNOWN_MIME; 450 } else { 451 $stringMime = $mime->toString(); 452 } 453 454 switch ($stringMime) { 455 case self::UNKNOWN_MIME: 456 LogUtility::msg("The mime type of the media ($path) is <a href=\"https://www.dokuwiki.org/mime\">unknown (not in the configuration file)</a>", LogUtility::LVL_MSG_ERROR); 457 $media = new ImageRaster($path, $tagAttributes); 458 $mediaLink = new RasterImageLink($media); 459 break; 460 case Mime::SVG: 461 $media = new ImageSvg($path, $tagAttributes); 462 $mediaLink = new SvgImageLink($media); 463 break; 464 default: 465 if (!$mime->isImage()) { 466 LogUtility::msg("The type ($mime) of media ($path) is not an image", LogUtility::LVL_MSG_DEBUG, "image"); 467 $media = new ThirdMedia($path, $tagAttributes); 468 $mediaLink = new ThirdMediaLink($media); 469 } else { 470 $media = new ImageRaster($path, $tagAttributes); 471 $mediaLink = new RasterImageLink($media); 472 } 473 break; 474 } 475 476 $mediaLink 477 ->setLazyLoadMethod($lazyLoadMethod) 478 ->setLinking($linking) 479 ->setLinkingClass($linkingClass); 480 return $mediaLink; 481 482 } 483 484 public function setLazyLoadMethod(string $lazyLoadMethod): MediaLink 485 { 486 $this->lazyLoadMethod = $lazyLoadMethod; 487 return $this; 488 } 489 490 491 /** 492 * A function to set explicitly which array format 493 * is used in the returned data of a {@link SyntaxPlugin::handle()} 494 * (which ultimately is stored in the {@link CallStack) 495 * 496 * This is to make the difference with the {@link MediaLink::createFromIndexAttributes()} 497 * that is indexed by number (ie without property name) 498 * 499 * 500 * Return the same array than with the {@link self::parse()} method 501 * that is used in the {@link CallStack} 502 * 503 * @return array of key string and value 504 */ 505 public 506 function toCallStackArray(): array 507 { 508 /** 509 * Trying to stay inline with the dokuwiki key 510 * We use the 'src' attributes as id 511 * 512 * src is a path (not an id) 513 */ 514 $array = array( 515 PagePath::PROPERTY_NAME => $this->getMedia()->getPath()->toString(), 516 self::LINKING_KEY => $this->getLinking() 517 ); 518 519 520 // Add the extra attribute 521 return array_merge($this->getMedia()->getAttributes()->toCallStackArray(), $array); 522 523 524 } 525 526 527 public 528 static function isInternalMediaSyntax($text) 529 { 530 return preg_match(' / ' . syntax_plugin_combo_media::MEDIA_PATTERN . ' / msSi', $text); 531 } 532 533 534 public 535 function __toString() 536 { 537 $media = $this->getMedia(); 538 $dokuPath = $media->getPath(); 539 if ($dokuPath !== null) { 540 return $dokuPath->getDokuwikiId(); 541 } else { 542 return $media->__toString(); 543 } 544 } 545 546 547 private 548 function getLinking() 549 { 550 return $this->linking; 551 } 552 553 private 554 function setLinking($value): MediaLink 555 { 556 $this->linking = $value; 557 return $this; 558 } 559 560 private 561 function getLinkingClass() 562 { 563 return $this->linkingClass; 564 } 565 566 private 567 function setLinkingClass($value): MediaLink 568 { 569 $this->linkingClass = $value; 570 return $this; 571 } 572 573 /** 574 * @return string - the HTML of the image inside a link if asked 575 */ 576 public 577 function renderMediaTagWithLink(): string 578 { 579 580 /** 581 * Link to the media 582 * 583 */ 584 $mediaLink = TagAttributes::createEmpty(); 585 // https://www.dokuwiki.org/config:target 586 global $conf; 587 $target = $conf['target']['media']; 588 $mediaLink->addOutputAttributeValueIfNotEmpty("target", $target); 589 if (!empty($target)) { 590 $mediaLink->addOutputAttributeValue("rel", 'noopener'); 591 } 592 593 /** 594 * Do we add a link to the image ? 595 */ 596 $media = $this->getMedia(); 597 $dokuPath = $media->getPath(); 598 if (!($dokuPath instanceof DokuPath)) { 599 LogUtility::msg("Media Link are only supported on media from the internal library ($media)", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 600 return ""; 601 } 602 $linking = $this->getLinking(); 603 switch ($linking) { 604 case self::LINKING_LINKONLY_VALUE: // show only a url 605 $src = ml( 606 $dokuPath->getDokuwikiId(), 607 array( 608 'id' => $dokuPath->getDokuwikiId(), 609 'cache' => $media->getCache(), 610 'rev' => $dokuPath->getRevision() 611 ) 612 ); 613 $mediaLink->addOutputAttributeValue("href", $src); 614 $title = $media->getTitle(); 615 if (empty($title)) { 616 $title = $media->getType(); 617 } 618 return $mediaLink->toHtmlEnterTag("a") . $title . "</a>"; 619 case self::LINKING_NOLINK_VALUE: 620 return $this->renderMediaTag(); 621 default: 622 case self::LINKING_DIRECT_VALUE: 623 //directly to the image 624 $src = ml( 625 $dokuPath->getDokuwikiId(), 626 array( 627 'id' => $dokuPath->getDokuwikiId(), 628 'cache' => $media->getCache(), 629 'rev' => $dokuPath->getRevision() 630 ), 631 true 632 ); 633 $mediaLink->addOutputAttributeValue("href", $src); 634 $snippetId = "lightbox"; 635 $mediaLink->addClassName("{$snippetId}-combo"); 636 $linkingClass = $this->getLinkingClass(); 637 if ($linkingClass !== null) { 638 $mediaLink->addClassName($linkingClass); 639 } 640 $snippetManager = PluginUtility::getSnippetManager(); 641 $snippetManager->attachJavascriptComboLibrary(); 642 $snippetManager->attachInternalJavascriptForSlot("lightbox"); 643 $snippetManager->attachCssInternalStyleSheetForSlot("lightbox"); 644 return $mediaLink->toHtmlEnterTag("a") . $this->renderMediaTag() . "</a>"; 645 646 case self::LINKING_DETAILS_VALUE: 647 //go to the details media viewer 648 $src = ml( 649 $dokuPath->getDokuwikiId(), 650 array( 651 'id' => $dokuPath->getDokuwikiId(), 652 'cache' => $media->getCache(), 653 'rev' => $dokuPath->getRevision() 654 ), 655 false 656 ); 657 $mediaLink->addOutputAttributeValue("href", $src); 658 return $mediaLink->toHtmlEnterTag("a") . 659 $this->renderMediaTag() . 660 "</a>"; 661 662 } 663 664 665 } 666 667 668 /** 669 * @return string - the HTML of the image 670 */ 671 public 672 673 abstract function renderMediaTag(): string; 674 675 676 /** 677 * The file 678 * @return Media 679 */ 680 public function getMedia(): Media 681 { 682 return $this->media; 683 } 684 685 protected function getLazyLoadMethod(): string 686 { 687 return $this->lazyLoadMethod; 688 } 689 690 691} 692