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