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