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\Extension\SyntaxPlugin; 16use dokuwiki\Parsing\ParserMode\Internallink; 17use syntax_plugin_combo_card; 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 private $lazyLoad = null; 120 121 122 /** 123 * The path of the media 124 * @var Media[] 125 */ 126 private $media; 127 128 129 /** 130 * Image constructor. 131 * @param Image $media 132 * 133 * Protected and not private 134 * to allow cascading init 135 * If private, the parent attributes are null 136 */ 137 protected function __construct(Media $media) 138 { 139 $this->media = $media; 140 } 141 142 143 /** 144 * Create an image from dokuwiki {@link Internallink internal call media attributes} 145 * 146 * Dokuwiki extracts already the width, height and align property 147 * @param array $callAttributes 148 * @return MediaLink 149 */ 150 public static function createFromIndexAttributes(array $callAttributes) 151 { 152 $src = $callAttributes[0]; 153 $title = $callAttributes[1]; 154 $align = $callAttributes[2]; 155 $width = $callAttributes[3]; 156 $height = $callAttributes[4]; 157 $cache = $callAttributes[5]; 158 $linking = $callAttributes[6]; 159 160 $tagAttributes = TagAttributes::createEmpty(); 161 $tagAttributes->addComponentAttributeValue(TagAttributes::TITLE_KEY, $title); 162 $tagAttributes->addComponentAttributeValue(self::ALIGN_KEY, $align); 163 $tagAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, $width); 164 $tagAttributes->addComponentAttributeValue(Dimension::HEIGHT_KEY, $height); 165 $tagAttributes->addComponentAttributeValue(CacheMedia::CACHE_KEY, $cache); 166 $tagAttributes->addComponentAttributeValue(self::LINKING_KEY, $linking); 167 168 return self::createMediaLinkFromId($src, $tagAttributes); 169 170 } 171 172 /** 173 * A function to explicitly create an internal media from 174 * a call stack array (ie key string and value) that we get in the {@link SyntaxPlugin::render()} 175 * from the {@link MediaLink::toCallStackArray()} 176 * 177 * @param $attributes - the attributes created by the function {@link MediaLink::getParseAttributes()} 178 * @param $rev - the mtime 179 * @return null|MediaLink 180 */ 181 public static function createFromCallStackArray($attributes, $rev = null): ?MediaLink 182 { 183 184 if (!is_array($attributes)) { 185 // Debug for the key_exist below because of the following message: 186 // `PHP Warning: key_exists() expects parameter 2 to be array, array given` 187 LogUtility::msg("The `attributes` parameter is not an array. Value ($attributes)", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 188 } 189 190 $tagAttributes = TagAttributes::createFromCallStackArray($attributes); 191 192 $src = $attributes[self::DOKUWIKI_SRC]; 193 if ($src === null) { 194 /** 195 * Dokuwiki parse already the src and create the path and the attributes 196 * The new model will not, we check if we are in the old mode 197 */ 198 $src = $attributes[PagePath::PROPERTY_NAME]; 199 if ($src === null) { 200 LogUtility::msg("src is mandatory for an image link and was not passed"); 201 return null; 202 } 203 } 204 $dokuUrl = DokuwikiUrl::createFromUrl($src); 205 $scheme = $dokuUrl->getScheme(); 206 switch ($scheme) { 207 case DokuFs::SCHEME: 208 $id = $dokuUrl->getPath(); 209 // the id is always absolute, except in a link 210 // It may be relative, transform it as absolute 211 global $ID; 212 resolve_mediaid(getNS($ID), $id, $exists); 213 $path = DokuPath::createMediaPathFromId($id, $rev); 214 return self::createMediaLinkFromPath($path, $tagAttributes); 215 case InterWikiPath::scheme: 216 $path = InterWikiPath::create($dokuUrl->getPath()); 217 return self::createMediaLinkFromPath($path, $tagAttributes); 218 case InternetPath::scheme: 219 $path = InternetPath::create($dokuUrl->getPath()); 220 return self::createMediaLinkFromPath($path, $tagAttributes); 221 default: 222 LogUtility::msg("The media with the scheme ($scheme) are not yet supported. Media Source: $src"); 223 return null; 224 225 } 226 227 228 } 229 230 /** 231 * @param $match - the match of the renderer (just a shortcut) 232 * @return MediaLink 233 */ 234 public static function createFromRenderMatch($match) 235 { 236 237 /** 238 * The parsing function {@link Doku_Handler_Parse_Media} has some flow / problem 239 * * It keeps the anchor only if there is no query string 240 * * It takes the first digit as the width (ie media.pdf?page=31 would have a width of 31) 241 * * `src` is not only the media path but may have a anchor 242 * We parse it then 243 */ 244 245 246 /** 247 * * Delete the opening and closing character 248 * * create the url and description 249 */ 250 $match = preg_replace(array('/^\{\{/', '/\}\}$/u'), '', $match); 251 $parts = explode('|', $match, 2); 252 $description = null; 253 $url = $parts[0]; 254 if (isset($parts[1])) { 255 $description = $parts[1]; 256 } 257 258 /** 259 * Media Alignment 260 */ 261 $rightAlign = (bool)preg_match('/^ /', $url); 262 $leftAlign = (bool)preg_match('/ $/', $url); 263 $url = trim($url); 264 265 // Logic = what's that ;)... 266 if ($leftAlign & $rightAlign) { 267 $align = 'center'; 268 } else if ($rightAlign) { 269 $align = 'right'; 270 } else if ($leftAlign) { 271 $align = 'left'; 272 } else { 273 $align = null; 274 } 275 276 /** 277 * The combo attributes array 278 */ 279 $dokuwikiUrl = DokuwikiUrl::createFromUrl($url); 280 $parsedAttributes = $dokuwikiUrl->toArray(); 281 $path = $dokuwikiUrl->getPath(); 282 $linkingKey = $dokuwikiUrl->getQueryParameter(MediaLink::LINKING_KEY); 283 if ($linkingKey === null) { 284 $linkingKey = PluginUtility::getConfValue(self::CONF_DEFAULT_LINKING, self::LINKING_DIRECT_VALUE); 285 } 286 $parsedAttributes[MediaLink::LINKING_KEY] = $linkingKey; 287 288 /** 289 * Media Type 290 */ 291 $scheme = $dokuwikiUrl->getScheme(); 292 if ($scheme === DokuFs::SCHEME) { 293 $mediaType = MediaLink::INTERNAL_MEDIA_CALL_NAME; 294 } else { 295 $mediaType = MediaLink::EXTERNAL_MEDIA_CALL_NAME; 296 } 297 298 299 /** 300 * src in dokuwiki is the path and the anchor if any 301 */ 302 $src = $path; 303 if (isset($parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]) != null) { 304 $src = $src . "#" . $parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]; 305 } 306 307 /** 308 * To avoid clash with the combostrap component type 309 * ie this is also a ComboStrap attribute where we set the type of a SVG (icon, illustration, background) 310 * we store the media type (ie external/internal) in another key 311 * 312 * There is no need to repeat the attributes as the arrays are merged 313 * into on but this is also an informal code to show which attributes 314 * are only Dokuwiki Native 315 * 316 */ 317 $dokuwikiAttributes = array( 318 self::MEDIA_DOKUWIKI_TYPE => $mediaType, 319 self::DOKUWIKI_SRC => $src, 320 Dimension::WIDTH_KEY => $parsedAttributes[Dimension::WIDTH_KEY], 321 Dimension::HEIGHT_KEY => $parsedAttributes[Dimension::HEIGHT_KEY], 322 CacheMedia::CACHE_KEY => $parsedAttributes[CacheMedia::CACHE_KEY], 323 TagAttributes::TITLE_KEY => $description, 324 MediaLink::ALIGN_KEY => $align, 325 MediaLink::LINKING_KEY => $parsedAttributes[MediaLink::LINKING_KEY], 326 ); 327 328 /** 329 * Merge standard dokuwiki attributes and 330 * parsed attributes 331 */ 332 $mergedAttributes = PluginUtility::mergeAttributes($dokuwikiAttributes, $parsedAttributes); 333 334 /** 335 * If this is an internal media, 336 * we are using our implementation 337 * and we have a change on attribute specification 338 */ 339 if ($mediaType == MediaLink::INTERNAL_MEDIA_CALL_NAME) { 340 341 /** 342 * The align attribute on an image parse 343 * is a float right 344 * ComboStrap does a difference between a block right and a float right 345 */ 346 if ($mergedAttributes[self::ALIGN_KEY] === "right") { 347 unset($mergedAttributes[self::ALIGN_KEY]); 348 $mergedAttributes[FloatAttribute::FLOAT_KEY] = "right"; 349 } 350 351 352 } 353 354 return self::createFromCallStackArray($mergedAttributes); 355 356 } 357 358 359 public 360 function setLazyLoad($false): MediaLink 361 { 362 $this->lazyLoad = $false; 363 return $this; 364 } 365 366 public 367 function getLazyLoad() 368 { 369 return $this->lazyLoad; 370 } 371 372 373 /** 374 * Create a media link from a wiki id 375 * 376 * 377 * @param $wikiId - dokuwiki id 378 * @param TagAttributes|null $tagAttributes 379 * @param string|null $rev 380 * @return MediaLink 381 */ 382 public 383 static function createMediaLinkFromId($wikiId, ?string $rev = '', TagAttributes $tagAttributes = null) 384 { 385 if (is_object($rev)) { 386 LogUtility::msg("rev should not be an object", LogUtility::LVL_MSG_ERROR, "support"); 387 } 388 if ($tagAttributes == null) { 389 $tagAttributes = TagAttributes::createEmpty(); 390 } else { 391 if (!($tagAttributes instanceof TagAttributes)) { 392 LogUtility::msg("TagAttributes is not an instance of Tag Attributes", LogUtility::LVL_MSG_ERROR, "support"); 393 } 394 } 395 396 $dokuPath = DokuPath::createMediaPathFromId($wikiId, $rev); 397 return self::createMediaLinkFromPath($dokuPath, $tagAttributes); 398 399 } 400 401 /** 402 * @param Path $path 403 * @param null $tagAttributes 404 * @return RasterImageLink|SvgImageLink|ThirdMediaLink 405 */ 406 public static function createMediaLinkFromPath(Path $path, $tagAttributes = null) 407 { 408 409 /** 410 * Processing 411 */ 412 $mime = $path->getMime(); 413 if ($path->getExtension() === "svg") { 414 /** 415 * The mime type is set when uploading, not when 416 * viewing. 417 * Because they are internal image, the svg was already uploaded 418 * Therefore, no authorization scheme here 419 */ 420 $mime = Mime::create(Mime::SVG); 421 } 422 423 if ($mime === null) { 424 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); 425 $media = new ImageRaster($path, $tagAttributes); 426 return new RasterImageLink($media); 427 } 428 429 if (!$mime->isImage()) { 430 LogUtility::msg("The type ($mime) of media ($path) is not an image", LogUtility::LVL_MSG_DEBUG, "image"); 431 $media = new ThirdMedia($path, $tagAttributes); 432 return new ThirdMediaLink($media); 433 } 434 435 if ($mime->toString() === Mime::SVG) { 436 $media = new ImageSvg($path, $tagAttributes); 437 return new SvgImageLink($media); 438 } 439 440 $media = new ImageRaster($path, $tagAttributes); 441 return new RasterImageLink($media); 442 443 444 } 445 446 447 /** 448 * A function to set explicitly which array format 449 * is used in the returned data of a {@link SyntaxPlugin::handle()} 450 * (which ultimately is stored in the {@link CallStack) 451 * 452 * This is to make the difference with the {@link MediaLink::createFromIndexAttributes()} 453 * that is indexed by number (ie without property name) 454 * 455 * 456 * Return the same array than with the {@link self::parse()} method 457 * that is used in the {@link CallStack} 458 * 459 * @return array of key string and value 460 */ 461 public 462 function toCallStackArray(): array 463 { 464 /** 465 * Trying to stay inline with the dokuwiki key 466 * We use the 'src' attributes as id 467 * 468 * src is a path (not an id) 469 */ 470 $array = array( 471 PagePath::PROPERTY_NAME => $this->getMedia()->getPath()->toString() 472 ); 473 474 475 // Add the extra attribute 476 return array_merge($this->getMedia()->getAttributes()->toCallStackArray(), $array); 477 478 479 } 480 481 482 public 483 static function isInternalMediaSyntax($text) 484 { 485 return preg_match(' / ' . syntax_plugin_combo_media::MEDIA_PATTERN . ' / msSi', $text); 486 } 487 488 489 public 490 function __toString() 491 { 492 $media = $this->getMedia(); 493 $dokuPath = $media->getPath(); 494 if ($dokuPath !== null) { 495 return $dokuPath->getDokuwikiId(); 496 } else { 497 return $media->__toString(); 498 } 499 } 500 501 private 502 function getAlign() 503 { 504 return $this->getMedia()->getAttributes()->getComponentAttributeValue(self::ALIGN_KEY); 505 } 506 507 private 508 function getLinking() 509 { 510 return $this->getMedia()->getAttributes()->getComponentAttributeValue(self::LINKING_KEY); 511 } 512 513 514 /** 515 * @return string - the HTML of the image inside a link if asked 516 */ 517 public 518 function renderMediaTagWithLink(): string 519 { 520 521 /** 522 * Link to the media 523 * 524 */ 525 $mediaLink = TagAttributes::createEmpty(); 526 // https://www.dokuwiki.org/config:target 527 global $conf; 528 $target = $conf['target']['media']; 529 $mediaLink->addHtmlAttributeValueIfNotEmpty("target", $target); 530 if (!empty($target)) { 531 $mediaLink->addHtmlAttributeValue("rel", 'noopener'); 532 } 533 534 /** 535 * Do we add a link to the image ? 536 */ 537 $media = $this->getMedia(); 538 $dokuPath = $media->getPath(); 539 if (!($dokuPath instanceof DokuPath)) { 540 LogUtility::msg("Media Link are only supported on media from the internal library ($media)", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 541 return ""; 542 } 543 $linking = $this->getLinking(); 544 switch ($linking) { 545 case self::LINKING_LINKONLY_VALUE: // show only a url 546 $src = ml( 547 $dokuPath->getDokuwikiId(), 548 array( 549 'id' => $dokuPath->getDokuwikiId(), 550 'cache' => $media->getCache(), 551 'rev' => $dokuPath->getRevision() 552 ) 553 ); 554 $mediaLink->addHtmlAttributeValue("href", $src); 555 $title = $media->getTitle(); 556 if (empty($title)) { 557 $title = $media->getType(); 558 } 559 return $mediaLink->toHtmlEnterTag("a") . $title . "</a>"; 560 case self::LINKING_NOLINK_VALUE: 561 return $this->renderMediaTag(); 562 default: 563 case self::LINKING_DIRECT_VALUE: 564 //directly to the image 565 $src = ml( 566 $dokuPath->getDokuwikiId(), 567 array( 568 'id' => $dokuPath->getDokuwikiId(), 569 'cache' => $media->getCache(), 570 'rev' => $dokuPath->getRevision() 571 ), 572 true 573 ); 574 $mediaLink->addHtmlAttributeValue("href", $src); 575 return $mediaLink->toHtmlEnterTag("a") . $this->renderMediaTag() . "</a>"; 576 577 case self::LINKING_DETAILS_VALUE: 578 //go to the details media viewer 579 $src = ml( 580 $dokuPath->getDokuwikiId(), 581 array( 582 'id' => $dokuPath->getDokuwikiId(), 583 'cache' => $media->getCache(), 584 'rev' => $dokuPath->getRevision() 585 ), 586 false 587 ); 588 $mediaLink->addHtmlAttributeValue("href", $src); 589 return $mediaLink->toHtmlEnterTag("a") . 590 $this->renderMediaTag() . 591 "</a>"; 592 593 } 594 595 596 } 597 598 599 /** 600 * @return string - the HTML of the image 601 */ 602 public 603 604 abstract function renderMediaTag(): string; 605 606 607 /** 608 * The file 609 * @return Media 610 */ 611 public function getMedia(): Media 612 { 613 return $this->media; 614 } 615 616 617 618} 619