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 $parsedAttributes[MediaLink::LINKING_KEY] = $linkingKey; 297 298 /** 299 * Media Type 300 */ 301 $scheme = $dokuwikiUrl->getScheme(); 302 if ($scheme === DokuFs::SCHEME) { 303 $mediaType = MediaLink::INTERNAL_MEDIA_CALL_NAME; 304 } else { 305 $mediaType = MediaLink::EXTERNAL_MEDIA_CALL_NAME; 306 } 307 308 309 /** 310 * src in dokuwiki is the path and the anchor if any 311 */ 312 $src = $path; 313 if (isset($parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]) != null) { 314 $src = $src . "#" . $parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]; 315 } 316 317 /** 318 * To avoid clash with the combostrap component type 319 * ie this is also a ComboStrap attribute where we set the type of a SVG (icon, illustration, background) 320 * we store the media type (ie external/internal) in another key 321 * 322 * There is no need to repeat the attributes as the arrays are merged 323 * into on but this is also an informal code to show which attributes 324 * are only Dokuwiki Native 325 * 326 */ 327 $dokuwikiAttributes = array( 328 self::MEDIA_DOKUWIKI_TYPE => $mediaType, 329 self::DOKUWIKI_SRC => $src, 330 Dimension::WIDTH_KEY => $parsedAttributes[Dimension::WIDTH_KEY], 331 Dimension::HEIGHT_KEY => $parsedAttributes[Dimension::HEIGHT_KEY], 332 CacheMedia::CACHE_KEY => $parsedAttributes[CacheMedia::CACHE_KEY], 333 TagAttributes::TITLE_KEY => $description, 334 MediaLink::ALIGN_KEY => $align, 335 MediaLink::LINKING_KEY => $parsedAttributes[MediaLink::LINKING_KEY], 336 ); 337 338 /** 339 * Merge standard dokuwiki attributes and 340 * parsed attributes 341 */ 342 $mergedAttributes = PluginUtility::mergeAttributes($dokuwikiAttributes, $parsedAttributes); 343 344 /** 345 * If this is an internal media, 346 * we are using our implementation 347 * and we have a change on attribute specification 348 */ 349 if ($mediaType == MediaLink::INTERNAL_MEDIA_CALL_NAME) { 350 351 /** 352 * The align attribute on an image parse 353 * is a float right 354 * ComboStrap does a difference between a block right and a float right 355 */ 356 if ($mergedAttributes[self::ALIGN_KEY] === "right") { 357 unset($mergedAttributes[self::ALIGN_KEY]); 358 $mergedAttributes[FloatAttribute::FLOAT_KEY] = "right"; 359 } 360 361 362 } 363 364 return self::createFromCallStackArray($mergedAttributes); 365 366 } 367 368 369 public 370 function setLazyLoad($false): MediaLink 371 { 372 $this->lazyLoad = $false; 373 return $this; 374 } 375 376 public 377 function getLazyLoad() 378 { 379 return $this->lazyLoad; 380 } 381 382 383 /** 384 * Create a media link from a wiki id 385 * 386 * 387 * @param $wikiId - dokuwiki id 388 * @param TagAttributes|null $tagAttributes 389 * @param string|null $rev 390 * @return MediaLink 391 */ 392 public 393 static function createMediaLinkFromId($wikiId, ?string $rev = '', TagAttributes $tagAttributes = null) 394 { 395 if (is_object($rev)) { 396 LogUtility::msg("rev should not be an object", LogUtility::LVL_MSG_ERROR, "support"); 397 } 398 if ($tagAttributes == null) { 399 $tagAttributes = TagAttributes::createEmpty(); 400 } else { 401 if (!($tagAttributes instanceof TagAttributes)) { 402 LogUtility::msg("TagAttributes is not an instance of Tag Attributes", LogUtility::LVL_MSG_ERROR, "support"); 403 } 404 } 405 406 $dokuPath = DokuPath::createMediaPathFromId($wikiId, $rev); 407 return self::createMediaLinkFromPath($dokuPath, $tagAttributes); 408 409 } 410 411 /** 412 * @param Path $path 413 * @param TagAttributes|null $tagAttributes 414 * @return RasterImageLink|SvgImageLink|ThirdMediaLink 415 */ 416 public static function createMediaLinkFromPath(Path $path, TagAttributes $tagAttributes = null) 417 { 418 419 if ($tagAttributes === null) { 420 $tagAttributes = TagAttributes::createEmpty(); 421 } 422 423 /** 424 * Get and delete the attribute for the link 425 * (The rest is for the image) 426 */ 427 $lazyLoadMethod = $tagAttributes->getValueAndRemoveIfPresent(self::LAZY_LOAD_METHOD, self::LAZY_LOAD_METHOD_LOZAD_VALUE); 428 $linking = $tagAttributes->getValueAndRemoveIfPresent(self::LINKING_KEY); 429 $linkingClass = $tagAttributes->getValueAndRemoveIfPresent(syntax_plugin_combo_media::LINK_CLASS_ATTRIBUTE); 430 431 /** 432 * Processing 433 */ 434 $mime = $path->getMime(); 435 if ($path->getExtension() === "svg") { 436 /** 437 * The mime type is set when uploading, not when 438 * viewing. 439 * Because they are internal image, the svg was already uploaded 440 * Therefore, no authorization scheme here 441 */ 442 $mime = Mime::create(Mime::SVG); 443 } 444 445 if ($mime === null) { 446 $stringMime = self::UNKNOWN_MIME; 447 } else { 448 $stringMime = $mime->toString(); 449 } 450 451 switch ($stringMime) { 452 case self::UNKNOWN_MIME: 453 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); 454 $media = new ImageRaster($path, $tagAttributes); 455 $mediaLink = new RasterImageLink($media); 456 break; 457 case Mime::SVG: 458 $media = new ImageSvg($path, $tagAttributes); 459 $mediaLink = new SvgImageLink($media); 460 break; 461 default: 462 if (!$mime->isImage()) { 463 LogUtility::msg("The type ($mime) of media ($path) is not an image", LogUtility::LVL_MSG_DEBUG, "image"); 464 $media = new ThirdMedia($path, $tagAttributes); 465 $mediaLink = new ThirdMediaLink($media); 466 } else { 467 $media = new ImageRaster($path, $tagAttributes); 468 $mediaLink = new RasterImageLink($media); 469 } 470 break; 471 } 472 473 $mediaLink 474 ->setLazyLoadMethod($lazyLoadMethod) 475 ->setLinking($linking) 476 ->setLinkingClass($linkingClass); 477 return $mediaLink; 478 479 } 480 481 public function setLazyLoadMethod(string $lazyLoadMethod): MediaLink 482 { 483 $this->lazyLoadMethod = $lazyLoadMethod; 484 return $this; 485 } 486 487 488 /** 489 * A function to set explicitly which array format 490 * is used in the returned data of a {@link SyntaxPlugin::handle()} 491 * (which ultimately is stored in the {@link CallStack) 492 * 493 * This is to make the difference with the {@link MediaLink::createFromIndexAttributes()} 494 * that is indexed by number (ie without property name) 495 * 496 * 497 * Return the same array than with the {@link self::parse()} method 498 * that is used in the {@link CallStack} 499 * 500 * @return array of key string and value 501 */ 502 public 503 function toCallStackArray(): array 504 { 505 /** 506 * Trying to stay inline with the dokuwiki key 507 * We use the 'src' attributes as id 508 * 509 * src is a path (not an id) 510 */ 511 $array = array( 512 PagePath::PROPERTY_NAME => $this->getMedia()->getPath()->toString(), 513 self::LINKING_KEY => $this->getLinking() 514 ); 515 516 517 // Add the extra attribute 518 return array_merge($this->getMedia()->getAttributes()->toCallStackArray(), $array); 519 520 521 } 522 523 524 public 525 static function isInternalMediaSyntax($text) 526 { 527 return preg_match(' / ' . syntax_plugin_combo_media::MEDIA_PATTERN . ' / msSi', $text); 528 } 529 530 531 public 532 function __toString() 533 { 534 $media = $this->getMedia(); 535 $dokuPath = $media->getPath(); 536 if ($dokuPath !== null) { 537 return $dokuPath->getDokuwikiId(); 538 } else { 539 return $media->__toString(); 540 } 541 } 542 543 544 private 545 function getLinking() 546 { 547 return $this->linking; 548 } 549 550 private 551 function setLinking($value): MediaLink 552 { 553 $this->linking = $value; 554 return $this; 555 } 556 557 private 558 function getLinkingClass() 559 { 560 return $this->linkingClass; 561 } 562 563 private 564 function setLinkingClass($value): MediaLink 565 { 566 $this->linkingClass = $value; 567 return $this; 568 } 569 570 /** 571 * @return string - the HTML of the image inside a link if asked 572 */ 573 public 574 function renderMediaTagWithLink(): string 575 { 576 577 /** 578 * Link to the media 579 * 580 */ 581 $mediaLink = TagAttributes::createEmpty(); 582 // https://www.dokuwiki.org/config:target 583 global $conf; 584 $target = $conf['target']['media']; 585 $mediaLink->addOutputAttributeValueIfNotEmpty("target", $target); 586 if (!empty($target)) { 587 $mediaLink->addOutputAttributeValue("rel", 'noopener'); 588 } 589 590 /** 591 * Do we add a link to the image ? 592 */ 593 $media = $this->getMedia(); 594 $dokuPath = $media->getPath(); 595 if (!($dokuPath instanceof DokuPath)) { 596 LogUtility::msg("Media Link are only supported on media from the internal library ($media)", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 597 return ""; 598 } 599 $linking = $this->getLinking(); 600 if ($linking === null && $dokuPath->getMime()->isImage()) { 601 $linking = PluginUtility::getConfValue(self::CONF_DEFAULT_LINKING, self::LINKING_DIRECT_VALUE); 602 } 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