1<?php 2/** 3 * Copyright (c) 2020. 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 15 16use ComboStrap\Meta\Field\PageTemplateName; 17use ComboStrap\TagAttribute\StyleAttribute; 18use Doku_Renderer_xhtml; 19use dokuwiki\Extension\PluginTrait; 20use dokuwiki\Utf8\Conversion; 21use syntax_plugin_combo_link; 22 23 24/** 25 * 26 * @package ComboStrap 27 * 28 * Parse the ref found in a markup link 29 * and return an {@link LinkMarkup::toAttributes()} array for an anchor (a) 30 * with href, style, ... attributes 31 * 32 */ 33class LinkMarkup 34{ 35 36 37 /** 38 * Class added to the type of link 39 * Class have styling rule conflict, they are by default not set 40 * but this configuration permits to turn it back 41 */ 42 const CONF_USE_DOKUWIKI_CLASS_NAME = "useDokuwikiLinkClassName"; 43 44 /** 45 * This configuration will set for all internal link 46 * the {@link LinkMarkup::PREVIEW_ATTRIBUTE} preview attribute 47 */ 48 const CONF_PREVIEW_LINK = "previewLink"; 49 const CONF_PREVIEW_LINK_DEFAULT = 0; 50 51 52 const TEXT_ERROR_CLASS = "text-danger"; 53 54 /** 55 * The known parameters for an email url 56 * The other are styling attribute :) 57 */ 58 const EMAIL_VALID_PARAMETERS = ["subject"]; 59 60 /** 61 * If set, it will show a page preview 62 */ 63 const PREVIEW_ATTRIBUTE = "preview"; 64 65 66 /** 67 * Highlight Key 68 * Adding this property to the internal query will highlight the words 69 * 70 * See {@link html_hilight} 71 */ 72 const SEARCH_HIGHLIGHT_QUERY_PROPERTY = "s"; 73 const DATA_WIKI_ID = "data-wiki-id"; 74 75 /** 76 * For styling on the anchor tag (ie a) 77 */ 78 public const ANCHOR_HTML_SNIPPET_ID = "anchor"; 79 80 /** 81 * Url properties 82 * that are not seen as styling properties 83 * for a page 84 * We could also build the {@link FetcherPage} 85 * and see which attributes were not taken ? 86 */ 87 const PROTECTED_URL_PROPERTY = [ 88 self::SEARCH_HIGHLIGHT_QUERY_PROPERTY, 89 DokuWikiId::DOKUWIKI_ID_ATTRIBUTE, 90 PageTemplateName::PROPERTY_NAME, 91 FetcherPage::PURGE 92 ]; 93 94 95 private MarkupRef $markupRef; 96 97 98 private TagAttributes $stylingAttributes; 99 100 /** 101 * Link constructor. 102 * @param String $ref 103 * @throws ExceptionBadArgument 104 * @throws ExceptionBadSyntax 105 * @throws ExceptionNotFound 106 */ 107 public function __construct(string $ref) 108 { 109 110 $this->stylingAttributes = TagAttributes::createEmpty(syntax_plugin_combo_link::TAG); 111 112 $this->markupRef = MarkupRef::createLinkFromRef($ref); 113 114 $this->collectStylingAttributeInUrl(); 115 116 117 } 118 119 public static function createFromPageIdOrPath($id): LinkMarkup 120 { 121 WikiPath::addRootSeparatorIfNotPresent($id); 122 try { 123 return new LinkMarkup($id); 124 } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) { 125 throw new ExceptionRuntime("Internal error: an id should be a good reference"); 126 } 127 } 128 129 /** 130 * @throws ExceptionBadArgument 131 * @throws ExceptionBadSyntax 132 * @throws ExceptionNotFound 133 */ 134 public static function createFromRef(string $ref): LinkMarkup 135 { 136 return new LinkMarkup($ref); 137 } 138 139 public static function getHtmlClassLocalLink(): string 140 { 141 return "link-local"; 142 } 143 144 145 /** 146 * 147 * @throws ExceptionNotFound 148 */ 149 public function toAttributes(): TagAttributes 150 { 151 152 $outputAttributes = $this->stylingAttributes; 153 154 155 $url = $this->getMarkupRef()->getUrl(); 156 $outputAttributes->addOutputAttributeValue("href", $url->toString()); 157 158 /** 159 * The search term 160 * Code adapted found at {@link Doku_Renderer_xhtml::internallink()} 161 * We can't use the previous {@link wl function} 162 * because it encode too much 163 */ 164 $snippetSystem = PluginUtility::getSnippetManager(); 165 if ($url->hasProperty(self::SEARCH_HIGHLIGHT_QUERY_PROPERTY)) { 166 $snippetSystem->attachCssInternalStyleSheet("search-hit"); 167 } 168 $snippetSystem->attachCssInternalStyleSheet(self::ANCHOR_HTML_SNIPPET_ID); 169 170 global $conf; 171 172 173 /** 174 * Processing by type 175 */ 176 switch ($this->getMarkupRef()->getSchemeType()) { 177 case MarkupRef::INTERWIKI_URI: 178 try { 179 $interWiki = $this->getMarkupRef()->getInterWiki(); 180 } catch (ExceptionNotFound $e) { 181 LogUtility::internalError("The interwiki should be available. We were unable to create the link attributes."); 182 return $outputAttributes; 183 } 184 // normal link for the `this` wiki 185 if ($interWiki->getWiki() !== "this") { 186 $snippetSystem->attachCssInternalStyleSheet(MarkupRef::INTERWIKI_URI); 187 } 188 $cssRules = $interWiki->getDefaultCssRules(); 189 $snippetSystem->attachCssInternalStyleSheet(MarkupRef::INTERWIKI_URI, $cssRules); 190 try { 191 $cssRules = $interWiki->getSpecificCssRules(); 192 $snippetSystem->attachCssInternalStyleSheet(MarkupRef::INTERWIKI_URI . "-" . $interWiki->getWiki(), $cssRules); 193 } catch (ExceptionNotFound $e) { 194 // no media find for the wiki 195 } 196 /** 197 * Target 198 */ 199 $interWikiConf = $conf['target']['interwiki']; 200 if (!empty($interWikiConf)) { 201 $outputAttributes->addOutputAttributeValue('target', $interWikiConf); 202 $outputAttributes->addOutputAttributeValue('rel', 'noopener'); 203 } 204 $outputAttributes->addClassName($interWiki->getComponentClass()); 205 $outputAttributes->addClassName($interWiki->getSubComponentClass()); 206 break; 207 case MarkupRef::WIKI_URI: 208 /** 209 * Derived from {@link Doku_Renderer_xhtml::internallink()} 210 */ 211 // https://www.dokuwiki.org/config:target 212 $target = $conf['target']['wiki']; 213 if (!empty($target)) { 214 $outputAttributes->addOutputAttributeValue('target', $target); 215 } 216 /** 217 * Internal Page 218 */ 219 try { 220 $dokuPath = $this->getMarkupRef()->getPath(); 221 } catch (ExceptionNotFound $e) { 222 throw new ExceptionNotFound("We were unable to process the internal link dokuwiki id on the link. The path was not found. Error: {$e->getMessage()}"); 223 } 224 $page = MarkupPath::createPageFromPathObject($dokuPath); 225 $outputAttributes->addOutputAttributeValue(self::DATA_WIKI_ID, $dokuPath->getWikiId()); 226 227 /** 228 * Preview, we add it here because even if the file does not exist 229 * we need to delete this attribute so that it's not in the HTML 230 */ 231 $previewConfig = SiteConfig::getConfValue(self::CONF_PREVIEW_LINK, self::CONF_PREVIEW_LINK_DEFAULT); 232 $preview = $outputAttributes->getBooleanValueAndRemoveIfPresent(self::PREVIEW_ATTRIBUTE, $previewConfig); 233 234 if (!FileSystems::exists($dokuPath)) { 235 236 /** 237 * Red color 238 * if not `do=edit` 239 */ 240 if (!$this->markupRef->getUrl()->hasProperty("do")) { 241 $outputAttributes->addClassName(self::getHtmlClassNotExist()); 242 $outputAttributes->addOutputAttributeValue("rel", 'nofollow'); 243 } 244 245 } else { 246 247 /** 248 * Internal Link Class 249 */ 250 $outputAttributes->addClassName(self::getHtmlClassInternalLink()); 251 252 /** 253 * Link Creation 254 * Do we need to set the title or the tooltip 255 * Processing variables 256 */ 257 $acronym = ""; 258 259 /** 260 * Preview tooltip 261 */ 262 if ($preview) { 263 Tooltip::addToolTipSnippetIfNeeded(); 264 // We use as heading, the name and not the title of the resource because otherwise it would be to lengthy 265 $tooltipHtml = <<<EOF 266<h3>{$page->getNameOrDefault()}</h3> 267<p>{$page->getDescriptionOrElseDokuWiki()}</p> 268EOF; 269 $dataAttributeNamespace = Bootstrap::getDataNamespace(); 270 $outputAttributes->addOutputAttributeValue("data{$dataAttributeNamespace}-toggle", "tooltip"); 271 $outputAttributes->addOutputAttributeValue("data{$dataAttributeNamespace}-placement", "top"); 272 $outputAttributes->addOutputAttributeValue("data{$dataAttributeNamespace}-html", "true"); 273 $outputAttributes->addOutputAttributeValue("title", $tooltipHtml); 274 } 275 276 /** 277 * Low quality Page 278 * (It has a higher priority than preview and 279 * the code comes then after) 280 */ 281 if ($page->isLowQualityPage()) { 282 283 /** 284 * Add a class to style it differently 285 * (the acronym is added to the description, later) 286 */ 287 $acronym = LowQualityPage::LOW_QUALITY_PROTECTION_ACRONYM; 288 $lowerCaseLowQualityAcronym = strtolower(LowQualityPage::LOW_QUALITY_PROTECTION_ACRONYM); 289 $outputAttributes->addClassName(StyleAttribute::addComboStrapSuffix(LowQualityPage::CLASS_SUFFIX)); 290 $snippetLowQualityPageId = $lowerCaseLowQualityAcronym; 291 $snippetSystem->attachCssInternalStyleSheet($snippetLowQualityPageId); 292 /** 293 * Note The protection does occur on Javascript level, not on the HTML 294 * because the created page is valid for a anonymous or logged-in user 295 * Javascript is controlling 296 */ 297 if (LowQualityPage::isProtectionEnabled()) { 298 299 $linkType = LowQualityPage::getLowQualityLinkType(); 300 $outputAttributes->addOutputAttributeValue(PageProtection::DATA_PP_LINK, $linkType); 301 $outputAttributes->addOutputAttributeValue(PageProtection::DATA_PP_SOURCE, $lowerCaseLowQualityAcronym); 302 303 /** 304 * Low Quality Page protection javascript is only for warning or login link 305 */ 306 if (in_array($linkType, [PageProtection::PAGE_PROTECTION_LINK_WARNING, PageProtection::PAGE_PROTECTION_LINK_LOGIN])) { 307 PageProtection::addPageProtectionSnippet(); 308 } 309 310 } 311 } 312 313 /** 314 * Late publication has a higher priority than 315 * the late publication and the is therefore after 316 * (In case this a low quality page late published) 317 */ 318 if ($page->isLatePublication()) { 319 /** 320 * Add a class to style it differently if needed 321 */ 322 $className = StyleAttribute::addComboStrapSuffix(PagePublicationDate::LATE_PUBLICATION_CLASS_PREFIX_NAME); 323 $outputAttributes->addClassName($className); 324 if (PagePublicationDate::isLatePublicationProtectionEnabled()) { 325 $outputAttributes->removeOutputAttributeIfPresent(PageProtection::DATA_PP_LINK); 326 $outputAttributes->removeOutputAttributeIfPresent(PageProtection::DATA_PP_SOURCE); 327 $outputAttributes->addOutputAttributeValue(PageProtection::DATA_PP_LINK, PageProtection::PAGE_PROTECTION_LINK_LOGIN); 328 $acronym = PagePublicationDate::LATE_PUBLICATION_PROTECTION_ACRONYM; 329 $lowerCaseLatePublicationAcronym = strtolower(PagePublicationDate::LATE_PUBLICATION_PROTECTION_ACRONYM); 330 $outputAttributes->addOutputAttributeValue(PageProtection::DATA_PP_SOURCE, $lowerCaseLatePublicationAcronym); 331 PageProtection::addPageProtectionSnippet(); 332 } 333 334 } 335 336 /** 337 * Title (ie tooltip vs title html attribute) 338 */ 339 if (!$outputAttributes->hasAttribute("title")) { 340 341 $description = PageDescription::createForPage($page)->getValueOrDefault(); 342 if (!empty($acronym)) { 343 $description = $description . " ($acronym)"; 344 } 345 $outputAttributes->addOutputAttributeValue("title", $description); 346 347 } 348 349 } 350 351 break; 352 353 case MarkupRef::WINDOWS_SHARE_URI: 354 // https://www.dokuwiki.org/config:target 355 $windowsTarget = $conf['target']['windows']; 356 if (!empty($windowsTarget)) { 357 $outputAttributes->addOutputAttributeValue('target', $windowsTarget); 358 } 359 $outputAttributes->addClassName("windows"); 360 break; 361 case MarkupRef::LOCAL_URI: 362 $outputAttributes->addClassName(self::getHtmlClassLocalLink()); 363 if (!$outputAttributes->hasAttribute("title")) { 364 $description = ucfirst($this->markupRef->getUrl()->getFragment()); 365 if ($description !== "") { 366 $description = str_replace("_", " ", $description); 367 $outputAttributes->addOutputAttributeValue("title", $description); 368 } 369 } 370 break; 371 case MarkupRef::EMAIL_URI: 372 $outputAttributes->addClassName(self::getHtmlClassEmailLink()); 373 /** 374 * An email link is `<email>` 375 * {@link Emaillink::connectTo()} 376 * or 377 * {@link PluginTrait::email() 378 */ 379 // common.php#obfsucate implements the $conf['mailguard'] 380 $uri = $url->getPath(); 381 $uri = $this->obfuscateEmail($uri); 382 $uri = urlencode($uri); 383 $queryParameters = $url->getQueryProperties(); 384 if (sizeof($queryParameters) > 0) { 385 $uri .= "?"; 386 foreach ($queryParameters as $key => $value) { 387 $value = urlencode($value); 388 $key = urlencode($key); 389 if (in_array($key, self::EMAIL_VALID_PARAMETERS)) { 390 $uri .= "$key=$value"; 391 } 392 } 393 } 394 // replace href 395 $outputAttributes->removeOutputAttributeIfPresent("href"); 396 $outputAttributes->addOutputAttributeValue("href", 'mailto:' . $uri); 397 break; 398 case MarkupRef::WEB_URI: 399 /** 400 * It may be a absolute url 401 * that points to the local website 402 * (case of the {@link \syntax_plugin_combo_permalink} 403 */ 404 if ($url->isExternal()) { 405 406 if ($conf['relnofollow']) { 407 $outputAttributes->addOutputAttributeValue("rel", 'nofollow ugc'); 408 } 409 // https://www.dokuwiki.org/config:target 410 $externTarget = $conf['target']['extern']; 411 if (!empty($externTarget)) { 412 $outputAttributes->addOutputAttributeValue('target', $externTarget); 413 $outputAttributes->addOutputAttributeValue("rel", 'noopener'); 414 } 415 /** 416 * Default class for default external link 417 * To not interfere with other external link style 418 * For instance, {@link \syntax_plugin_combo_share} 419 */ 420 $outputAttributes->addClassName(self::getHtmlClassExternalLink()); 421 } 422 break; 423 default: 424 /** 425 * May be any external link 426 * such as {@link \syntax_plugin_combo_share} 427 */ 428 break; 429 430 } 431 432 /** 433 * An email URL and title 434 * may be already encoded because of the vanguard configuration 435 * 436 * The url is not treated as an attribute 437 * because the transformation function encodes the value 438 * to mitigate XSS 439 * 440 */ 441 if ($this->getMarkupRef()->getSchemeType() == MarkupRef::EMAIL_URI) { 442 $emailAddress = $this->obfuscateEmail($this->markupRef->getUrl()->getPath()); 443 $outputAttributes->addOutputAttributeValue("title", $emailAddress); 444 } 445 446 447 /** 448 * Return 449 */ 450 return $outputAttributes; 451 452 453 } 454 455 456 /** 457 * The label inside the anchor tag if there is none 458 * @param false $navigation 459 * @return string 460 * @throws ExceptionNotFound|ExceptionBadArgument 461 * 462 */ 463 public 464 function getDefaultLabel(bool $navigation = false): string 465 { 466 467 switch ($this->getMarkupRef()->getSchemeType()) { 468 case MarkupRef::WIKI_URI: 469 $page = $this->getPage(); 470 if ($navigation) { 471 return ResourceName::createForResource($page)->getValueOrDefault(); 472 } else { 473 return PageTitle::createForMarkup($page)->getValueOrDefault(); 474 } 475 case MarkupRef::EMAIL_URI: 476 global $conf; 477 $email = $this->markupRef->getUrl()->getPath(); 478 switch ($conf['mailguard']) { 479 case 'none' : 480 return $email; 481 case 'visible' : 482 default : 483 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 484 return strtr($email, $obfuscate); 485 } 486 case MarkupRef::INTERWIKI_URI: 487 try { 488 $path = $this->markupRef->getInterWiki()->toUrl()->getPath(); 489 if ($path[0] === "/") { 490 return substr($path, 1); 491 } else { 492 return $path; 493 } 494 } catch (ExceptionBadSyntax|ExceptionNotFound $e) { 495 return "interwiki"; 496 } 497 case MarkupRef::LOCAL_URI: 498 return $this->markupRef->getUrl()->getFragment(); 499 default: 500 return $this->markupRef->getRef(); 501 } 502 } 503 504 505 private 506 function obfuscateEmail($email, $inAttribute = true): string 507 { 508 /** 509 * adapted from {@link obfuscate()} in common.php 510 */ 511 global $conf; 512 513 $mailGuard = $conf['mailguard']; 514 if ($mailGuard === "hex" && $inAttribute) { 515 $mailGuard = "visible"; 516 } 517 switch ($mailGuard) { 518 case 'visible' : 519 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 520 return strtr($email, $obfuscate); 521 522 case 'hex' : 523 return Conversion::toHtml($email, true); 524 525 case 'none' : 526 default : 527 return $email; 528 } 529 } 530 531 532 /** 533 * @return bool 534 * @deprecated should not be here ref does not have the notion of relative 535 */ 536 public 537 function isRelative(): bool 538 { 539 return strpos($this->getMarkupRef()->getRef(), WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) !== 0; 540 } 541 542 public 543 function getMarkupRef(): MarkupRef 544 { 545 return $this->markupRef; 546 } 547 548 549 public 550 static function getHtmlClassInternalLink(): string 551 { 552 $oldClassName = SiteConfig::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME); 553 if ($oldClassName) { 554 return "wikilink1"; 555 } else { 556 return "link-internal"; 557 } 558 } 559 560 public 561 static function getHtmlClassEmailLink(): string 562 { 563 $oldClassName = SiteConfig::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME); 564 if ($oldClassName) { 565 return "mail"; 566 } else { 567 return "link-mail"; 568 } 569 } 570 571 public static function getHtmlClassExternalLink(): string 572 { 573 $oldClassName = SiteConfig::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME); 574 if ($oldClassName) { 575 return "urlextern"; 576 } else { 577 return "link-external"; 578 } 579 } 580 581//FYI: exist in dokuwiki is "wikilink1 but we let the control to the user 582 public 583 static function getHtmlClassNotExist(): string 584 { 585 $oldClassName = SiteConfig::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME); 586 if ($oldClassName) { 587 return "wikilink2"; 588 } else { 589 return self::TEXT_ERROR_CLASS; 590 } 591 } 592 593 public 594 function __toString() 595 { 596 return $this->getMarkupRef()->getRef(); 597 } 598 599 600 /** 601 * @throws ExceptionNotFound 602 */ 603 private 604 function getPage(): MarkupPath 605 { 606 return MarkupPath::createPageFromPathObject($this->getMarkupRef()->getPath()); 607 } 608 609 /** 610 * Styling attribute 611 * may be passed via parameters 612 * for internal link 613 * We don't want the styling attribute 614 * in the URL 615 */ 616 private 617 function collectStylingAttributeInUrl() 618 { 619 620 621 /** 622 * We will not overwrite the parameters if this is an dokuwiki 623 * action link (with the `do` property) 624 */ 625 if ($this->markupRef->getUrl()->hasProperty("do")) { 626 return; 627 } 628 629 /** 630 * Add the attribute from the URL 631 * if this is not a `do` 632 */ 633 switch ($this->markupRef->getSchemeType()) { 634 case MarkupRef::WIKI_URI: 635 foreach ($this->getMarkupRef()->getUrl()->getQueryProperties() as $key => $value) { 636 if (!in_array($key, self::PROTECTED_URL_PROPERTY)) { 637 $this->getMarkupRef()->getUrl()->deleteQueryParameter($key); 638 if (!TagAttributes::isEmptyValue($value)) { 639 $this->stylingAttributes->addComponentAttributeValue($key, $value); 640 } else { 641 $this->stylingAttributes->addEmptyComponentAttributeValue($key); 642 } 643 } 644 } 645 break; 646 case 647 MarkupRef::EMAIL_URI: 648 foreach ($this->getMarkupRef()->getUrl()->getQueryProperties() as $key => $value) { 649 if (!in_array($key, self::EMAIL_VALID_PARAMETERS)) { 650 $this->stylingAttributes->addComponentAttributeValue($key, $value); 651 } 652 } 653 break; 654 } 655 656 } 657 658 /** 659 * @return TagAttributes - the unknown attributes in a url are collected as styling attributes if this not a do query 660 * by {@link LinkMarkup::collectStylingAttributeInUrl()} 661 */ 662 public 663 function getStylingAttributes(): TagAttributes 664 { 665 return $this->stylingAttributes; 666 } 667 668 669} 670