* */ namespace ComboStrap; use ComboStrap\Meta\Field\PageTemplateName; use ComboStrap\TagAttribute\StyleAttribute; use Doku_Renderer_xhtml; use dokuwiki\Extension\PluginTrait; use dokuwiki\Utf8\Conversion; use syntax_plugin_combo_link; /** * * @package ComboStrap * * Parse the ref found in a markup link * and return an {@link LinkMarkup::toAttributes()} array for an anchor (a) * with href, style, ... attributes * */ class LinkMarkup { /** * Class added to the type of link * Class have styling rule conflict, they are by default not set * but this configuration permits to turn it back */ const CONF_USE_DOKUWIKI_CLASS_NAME = "useDokuwikiLinkClassName"; /** * This configuration will set for all internal link * the {@link LinkMarkup::PREVIEW_ATTRIBUTE} preview attribute */ const CONF_PREVIEW_LINK = "previewLink"; const CONF_PREVIEW_LINK_DEFAULT = 0; const TEXT_ERROR_CLASS = "text-danger"; /** * The known parameters for an email url * The other are styling attribute :) */ const EMAIL_VALID_PARAMETERS = ["subject"]; /** * If set, it will show a page preview */ const PREVIEW_ATTRIBUTE = "preview"; /** * Highlight Key * Adding this property to the internal query will highlight the words * * See {@link html_hilight} */ const SEARCH_HIGHLIGHT_QUERY_PROPERTY = "s"; const DATA_WIKI_ID = "data-wiki-id"; /** * For styling on the anchor tag (ie a) */ public const ANCHOR_HTML_SNIPPET_ID = "anchor"; /** * Url properties * that are not seen as styling properties * for a page * We could also build the {@link FetcherPage} * and see which attributes were not taken ? */ const PROTECTED_URL_PROPERTY = [ self::SEARCH_HIGHLIGHT_QUERY_PROPERTY, DokuWikiId::DOKUWIKI_ID_ATTRIBUTE, PageTemplateName::PROPERTY_NAME, FetcherPage::PURGE ]; private MarkupRef $markupRef; private TagAttributes $stylingAttributes; /** * Link constructor. * @param String $ref * @throws ExceptionBadArgument * @throws ExceptionBadSyntax * @throws ExceptionNotFound */ public function __construct(string $ref) { $this->stylingAttributes = TagAttributes::createEmpty(syntax_plugin_combo_link::TAG); $this->markupRef = MarkupRef::createLinkFromRef($ref); $this->collectStylingAttributeInUrl(); } public static function createFromPageIdOrPath($id): LinkMarkup { WikiPath::addRootSeparatorIfNotPresent($id); try { return new LinkMarkup($id); } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) { throw new ExceptionRuntime("Internal error: an id should be a good reference"); } } /** * @throws ExceptionBadArgument * @throws ExceptionBadSyntax * @throws ExceptionNotFound */ public static function createFromRef(string $ref): LinkMarkup { return new LinkMarkup($ref); } public static function getHtmlClassLocalLink(): string { return "link-local"; } /** * * @throws ExceptionNotFound */ public function toAttributes(): TagAttributes { $outputAttributes = $this->stylingAttributes; $url = $this->getMarkupRef()->getUrl(); $outputAttributes->addOutputAttributeValue("href", $url->toString()); /** * The search term * Code adapted found at {@link Doku_Renderer_xhtml::internallink()} * We can't use the previous {@link wl function} * because it encode too much */ $snippetSystem = PluginUtility::getSnippetManager(); if ($url->hasProperty(self::SEARCH_HIGHLIGHT_QUERY_PROPERTY)) { $snippetSystem->attachCssInternalStyleSheet("search-hit"); } $snippetSystem->attachCssInternalStyleSheet(self::ANCHOR_HTML_SNIPPET_ID); global $conf; /** * Processing by type */ switch ($this->getMarkupRef()->getSchemeType()) { case MarkupRef::INTERWIKI_URI: try { $interWiki = $this->getMarkupRef()->getInterWiki(); } catch (ExceptionNotFound $e) { LogUtility::internalError("The interwiki should be available. We were unable to create the link attributes."); return $outputAttributes; } // normal link for the `this` wiki if ($interWiki->getWiki() !== "this") { $snippetSystem->attachCssInternalStyleSheet(MarkupRef::INTERWIKI_URI); } $cssRules = $interWiki->getDefaultCssRules(); $snippetSystem->attachCssInternalStyleSheet(MarkupRef::INTERWIKI_URI, $cssRules); try { $cssRules = $interWiki->getSpecificCssRules(); $snippetSystem->attachCssInternalStyleSheet(MarkupRef::INTERWIKI_URI . "-" . $interWiki->getWiki(), $cssRules); } catch (ExceptionNotFound $e) { // no media find for the wiki } /** * Target */ $interWikiConf = $conf['target']['interwiki']; if (!empty($interWikiConf)) { $outputAttributes->addOutputAttributeValue('target', $interWikiConf); $outputAttributes->addOutputAttributeValue('rel', 'noopener'); } $outputAttributes->addClassName($interWiki->getComponentClass()); $outputAttributes->addClassName($interWiki->getSubComponentClass()); break; case MarkupRef::WIKI_URI: /** * Derived from {@link Doku_Renderer_xhtml::internallink()} */ // https://www.dokuwiki.org/config:target $target = $conf['target']['wiki']; if (!empty($target)) { $outputAttributes->addOutputAttributeValue('target', $target); } /** * Internal Page */ try { $dokuPath = $this->getMarkupRef()->getPath(); } catch (ExceptionNotFound $e) { throw new ExceptionNotFound("We were unable to process the internal link dokuwiki id on the link. The path was not found. Error: {$e->getMessage()}"); } $page = MarkupPath::createPageFromPathObject($dokuPath); $outputAttributes->addOutputAttributeValue(self::DATA_WIKI_ID, $dokuPath->getWikiId()); /** * Preview, we add it here because even if the file does not exist * we need to delete this attribute so that it's not in the HTML */ $previewConfig = SiteConfig::getConfValue(self::CONF_PREVIEW_LINK, self::CONF_PREVIEW_LINK_DEFAULT); $preview = $outputAttributes->getBooleanValueAndRemoveIfPresent(self::PREVIEW_ATTRIBUTE, $previewConfig); if (!FileSystems::exists($dokuPath)) { /** * Red color * if not `do=edit` */ if (!$this->markupRef->getUrl()->hasProperty("do")) { $outputAttributes->addClassName(self::getHtmlClassNotExist()); $outputAttributes->addOutputAttributeValue("rel", 'nofollow'); } } else { /** * Internal Link Class */ $outputAttributes->addClassName(self::getHtmlClassInternalLink()); /** * Link Creation * Do we need to set the title or the tooltip * Processing variables */ $acronym = ""; /** * Preview tooltip */ if ($preview) { Tooltip::addToolTipSnippetIfNeeded(); // We use as heading, the name and not the title of the resource because otherwise it would be to lengthy $tooltipHtml = <<{$page->getNameOrDefault()}

{$page->getDescriptionOrElseDokuWiki()}

EOF; $dataAttributeNamespace = Bootstrap::getDataNamespace(); $outputAttributes->addOutputAttributeValue("data{$dataAttributeNamespace}-toggle", "tooltip"); $outputAttributes->addOutputAttributeValue("data{$dataAttributeNamespace}-placement", "top"); $outputAttributes->addOutputAttributeValue("data{$dataAttributeNamespace}-html", "true"); $outputAttributes->addOutputAttributeValue("title", $tooltipHtml); } /** * Low quality Page * (It has a higher priority than preview and * the code comes then after) */ if ($page->isLowQualityPage()) { /** * Add a class to style it differently * (the acronym is added to the description, later) */ $acronym = LowQualityPage::LOW_QUALITY_PROTECTION_ACRONYM; $lowerCaseLowQualityAcronym = strtolower(LowQualityPage::LOW_QUALITY_PROTECTION_ACRONYM); $outputAttributes->addClassName(StyleAttribute::addComboStrapSuffix(LowQualityPage::CLASS_SUFFIX)); $snippetLowQualityPageId = $lowerCaseLowQualityAcronym; $snippetSystem->attachCssInternalStyleSheet($snippetLowQualityPageId); /** * Note The protection does occur on Javascript level, not on the HTML * because the created page is valid for a anonymous or logged-in user * Javascript is controlling */ if (LowQualityPage::isProtectionEnabled()) { $linkType = LowQualityPage::getLowQualityLinkType(); $outputAttributes->addOutputAttributeValue(PageProtection::DATA_PP_LINK, $linkType); $outputAttributes->addOutputAttributeValue(PageProtection::DATA_PP_SOURCE, $lowerCaseLowQualityAcronym); /** * Low Quality Page protection javascript is only for warning or login link */ if (in_array($linkType, [PageProtection::PAGE_PROTECTION_LINK_WARNING, PageProtection::PAGE_PROTECTION_LINK_LOGIN])) { PageProtection::addPageProtectionSnippet(); } } } /** * Late publication has a higher priority than * the late publication and the is therefore after * (In case this a low quality page late published) */ if ($page->isLatePublication()) { /** * Add a class to style it differently if needed */ $className = StyleAttribute::addComboStrapSuffix(PagePublicationDate::LATE_PUBLICATION_CLASS_PREFIX_NAME); $outputAttributes->addClassName($className); if (PagePublicationDate::isLatePublicationProtectionEnabled()) { $outputAttributes->removeOutputAttributeIfPresent(PageProtection::DATA_PP_LINK); $outputAttributes->removeOutputAttributeIfPresent(PageProtection::DATA_PP_SOURCE); $outputAttributes->addOutputAttributeValue(PageProtection::DATA_PP_LINK, PageProtection::PAGE_PROTECTION_LINK_LOGIN); $acronym = PagePublicationDate::LATE_PUBLICATION_PROTECTION_ACRONYM; $lowerCaseLatePublicationAcronym = strtolower(PagePublicationDate::LATE_PUBLICATION_PROTECTION_ACRONYM); $outputAttributes->addOutputAttributeValue(PageProtection::DATA_PP_SOURCE, $lowerCaseLatePublicationAcronym); PageProtection::addPageProtectionSnippet(); } } /** * Title (ie tooltip vs title html attribute) */ if (!$outputAttributes->hasAttribute("title")) { $description = PageDescription::createForPage($page)->getValueOrDefault(); if (!empty($acronym)) { $description = $description . " ($acronym)"; } $outputAttributes->addOutputAttributeValue("title", $description); } } break; case MarkupRef::WINDOWS_SHARE_URI: // https://www.dokuwiki.org/config:target $windowsTarget = $conf['target']['windows']; if (!empty($windowsTarget)) { $outputAttributes->addOutputAttributeValue('target', $windowsTarget); } $outputAttributes->addClassName("windows"); break; case MarkupRef::LOCAL_URI: $outputAttributes->addClassName(self::getHtmlClassLocalLink()); if (!$outputAttributes->hasAttribute("title")) { $description = ucfirst($this->markupRef->getUrl()->getFragment()); if ($description !== "") { $description = str_replace("_", " ", $description); $outputAttributes->addOutputAttributeValue("title", $description); } } break; case MarkupRef::EMAIL_URI: $outputAttributes->addClassName(self::getHtmlClassEmailLink()); /** * An email link is `` * {@link Emaillink::connectTo()} * or * {@link PluginTrait::email() */ // common.php#obfsucate implements the $conf['mailguard'] $uri = $url->getPath(); $uri = $this->obfuscateEmail($uri); $uri = urlencode($uri); $queryParameters = $url->getQueryProperties(); if (sizeof($queryParameters) > 0) { $uri .= "?"; foreach ($queryParameters as $key => $value) { $value = urlencode($value); $key = urlencode($key); if (in_array($key, self::EMAIL_VALID_PARAMETERS)) { $uri .= "$key=$value"; } } } // replace href $outputAttributes->removeOutputAttributeIfPresent("href"); $outputAttributes->addOutputAttributeValue("href", 'mailto:' . $uri); break; case MarkupRef::WEB_URI: /** * It may be a absolute url * that points to the local website * (case of the {@link \syntax_plugin_combo_permalink} */ if ($url->isExternal()) { if ($conf['relnofollow']) { $outputAttributes->addOutputAttributeValue("rel", 'nofollow ugc'); } // https://www.dokuwiki.org/config:target $externTarget = $conf['target']['extern']; if (!empty($externTarget)) { $outputAttributes->addOutputAttributeValue('target', $externTarget); $outputAttributes->addOutputAttributeValue("rel", 'noopener'); } /** * Default class for default external link * To not interfere with other external link style * For instance, {@link \syntax_plugin_combo_share} */ $outputAttributes->addClassName(self::getHtmlClassExternalLink()); } break; default: /** * May be any external link * such as {@link \syntax_plugin_combo_share} */ break; } /** * An email URL and title * may be already encoded because of the vanguard configuration * * The url is not treated as an attribute * because the transformation function encodes the value * to mitigate XSS * */ if ($this->getMarkupRef()->getSchemeType() == MarkupRef::EMAIL_URI) { $emailAddress = $this->obfuscateEmail($this->markupRef->getUrl()->getPath()); $outputAttributes->addOutputAttributeValue("title", $emailAddress); } /** * Return */ return $outputAttributes; } /** * The label inside the anchor tag if there is none * @param false $navigation * @return string * @throws ExceptionNotFound|ExceptionBadArgument * */ public function getDefaultLabel(bool $navigation = false): string { switch ($this->getMarkupRef()->getSchemeType()) { case MarkupRef::WIKI_URI: $page = $this->getPage(); if ($navigation) { return ResourceName::createForResource($page)->getValueOrDefault(); } else { return PageTitle::createForMarkup($page)->getValueOrDefault(); } case MarkupRef::EMAIL_URI: global $conf; $email = $this->markupRef->getUrl()->getPath(); switch ($conf['mailguard']) { case 'none' : return $email; case 'visible' : default : $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); return strtr($email, $obfuscate); } case MarkupRef::INTERWIKI_URI: try { $path = $this->markupRef->getInterWiki()->toUrl()->getPath(); if ($path[0] === "/") { return substr($path, 1); } else { return $path; } } catch (ExceptionBadSyntax|ExceptionNotFound $e) { return "interwiki"; } case MarkupRef::LOCAL_URI: return $this->markupRef->getUrl()->getFragment(); default: return $this->markupRef->getRef(); } } private function obfuscateEmail($email, $inAttribute = true): string { /** * adapted from {@link obfuscate()} in common.php */ global $conf; $mailGuard = $conf['mailguard']; if ($mailGuard === "hex" && $inAttribute) { $mailGuard = "visible"; } switch ($mailGuard) { case 'visible' : $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); return strtr($email, $obfuscate); case 'hex' : return Conversion::toHtml($email, true); case 'none' : default : return $email; } } /** * @return bool * @deprecated should not be here ref does not have the notion of relative */ public function isRelative(): bool { return strpos($this->getMarkupRef()->getRef(), WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) !== 0; } public function getMarkupRef(): MarkupRef { return $this->markupRef; } public static function getHtmlClassInternalLink(): string { $oldClassName = SiteConfig::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME); if ($oldClassName) { return "wikilink1"; } else { return "link-internal"; } } public static function getHtmlClassEmailLink(): string { $oldClassName = SiteConfig::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME); if ($oldClassName) { return "mail"; } else { return "link-mail"; } } public static function getHtmlClassExternalLink(): string { $oldClassName = SiteConfig::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME); if ($oldClassName) { return "urlextern"; } else { return "link-external"; } } //FYI: exist in dokuwiki is "wikilink1 but we let the control to the user public static function getHtmlClassNotExist(): string { $oldClassName = SiteConfig::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME); if ($oldClassName) { return "wikilink2"; } else { return self::TEXT_ERROR_CLASS; } } public function __toString() { return $this->getMarkupRef()->getRef(); } /** * @throws ExceptionNotFound */ private function getPage(): MarkupPath { return MarkupPath::createPageFromPathObject($this->getMarkupRef()->getPath()); } /** * Styling attribute * may be passed via parameters * for internal link * We don't want the styling attribute * in the URL */ private function collectStylingAttributeInUrl() { /** * We will not overwrite the parameters if this is an dokuwiki * action link (with the `do` property) */ if ($this->markupRef->getUrl()->hasProperty("do")) { return; } /** * Add the attribute from the URL * if this is not a `do` */ switch ($this->markupRef->getSchemeType()) { case MarkupRef::WIKI_URI: foreach ($this->getMarkupRef()->getUrl()->getQueryProperties() as $key => $value) { if (!in_array($key, self::PROTECTED_URL_PROPERTY)) { $this->getMarkupRef()->getUrl()->deleteQueryParameter($key); if (!TagAttributes::isEmptyValue($value)) { $this->stylingAttributes->addComponentAttributeValue($key, $value); } else { $this->stylingAttributes->addEmptyComponentAttributeValue($key); } } } break; case MarkupRef::EMAIL_URI: foreach ($this->getMarkupRef()->getUrl()->getQueryProperties() as $key => $value) { if (!in_array($key, self::EMAIL_VALID_PARAMETERS)) { $this->stylingAttributes->addComponentAttributeValue($key, $value); } } break; } } /** * @return TagAttributes - the unknown attributes in a url are collected as styling attributes if this not a do query * by {@link LinkMarkup::collectStylingAttributeInUrl()} */ public function getStylingAttributes(): TagAttributes { return $this->stylingAttributes; } }