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 Composer\Package\Link;
17use Doku_Renderer_metadata;
18use Doku_Renderer_xhtml;
19use dokuwiki\Extension\PluginTrait;
20use dokuwiki\Utf8\Conversion;
21use syntax_plugin_combo_tooltip;
22
23require_once(__DIR__ . '/PluginUtility.php');
24
25/**
26 * Class LinkUtility
27 * @package ComboStrap
28 *
29 */
30class LinkUtility
31{
32
33    /**
34     * Link pattern
35     * Found in {@link \dokuwiki\Parsing\ParserMode\Internallink}
36     */
37    const SPECIAL_PATTERN = "\[\[.*?\]\](?!\])";
38
39    /**
40     * A link may have a title or not
41     * ie
42     * [[path:page]]
43     * [[path:page|title]]
44     * are valid
45     *
46     * Get the content until one of this character is found:
47     *   * |
48     *   * or ]]
49     *   * or \n (No line break allowed, too much difficult to debug)
50     *   * and not [ (for two links on the same line)
51     */
52    const ENTRY_PATTERN_SINGLE_LINE = "\[\[[^\|\]]*(?=[^\n\[]*\]\])";
53    const EXIT_PATTERN = "\]\]";
54
55    /**
56     * Type of link
57     */
58    const TYPE_INTERWIKI = 'interwiki';
59    const TYPE_WINDOWS_SHARE = 'windowsShare';
60    const TYPE_EXTERNAL = 'external';
61
62    const TYPE_EMAIL = 'email';
63    const TYPE_LOCAL = 'local';
64    const TYPE_INTERNAL = 'internal';
65    const TYPE_INTERNAL_TEMPLATE = 'internal_template';
66
67    /**
68     * The key of the array for the handle cache
69     */
70    const ATTRIBUTE_REF = 'ref';
71    const ATTRIBUTE_NAME = 'name';
72    const ATTRIBUTE_IMAGE = 'image';
73
74
75    /**
76     * Class added to the type of link
77     * Class have styling rule conflict, they are by default not set
78     * but this configuration permits to turn it back
79     */
80    const CONF_USE_DOKUWIKI_CLASS_NAME = "useDokuwikiLinkClassName";
81    /**
82     * This configuration will set for all internal link
83     * the {@link LinkUtility::PREVIEW_ATTRIBUTE} preview attribute
84     */
85    const CONF_PREVIEW_LINK = "previewLink";
86    const CONF_PREVIEW_LINK_DEFAULT = 0;
87
88
89    const TEXT_ERROR_CLASS = "text-danger";
90
91    /**
92     * The known parameters for an email url
93     */
94    const EMAIL_VALID_PARAMETERS = ["subject"];
95
96    /**
97     * If set, it will show a page preview
98     */
99    const PREVIEW_ATTRIBUTE = "preview";
100    const PREVIEW_TOOLTIP = "preview";
101
102    /**
103     * Highlight Key
104     * Adding this property to the internal query will highlight the words
105     *
106     * See {@link html_hilight}
107     */
108    const SEARCH_HIGHLIGHT_QUERY_PROPERTY = "s";
109
110
111    /**
112     * @var mixed
113     */
114    private $type;
115    /**
116     * @var mixed
117     */
118    private $ref;
119    /**
120     * @var mixed
121     */
122    private $name;
123    /**
124     * @var Page the internal linked page if the link is an internal one
125     */
126    private $linkedPage;
127
128    /**
129     * @var string The value of the title attribute of an anchor
130     */
131    private $title;
132
133    /**
134     * @var TagAttributes|null
135     */
136    private $attributes;
137
138    /**
139     * The name of the wiki for an inter wiki link
140     * @var string
141     */
142    private $wiki;
143
144    /**
145     * @var Doku_Renderer_xhtml
146     */
147    private $renderer;
148
149    /**
150     *
151     * @var false|string
152     */
153    private $schemeUri;
154
155    /**
156     * The uri scheme that can be used inside a page
157     * @var array
158     */
159    private $authorizedSchemes;
160
161    /**
162     * @var string the query string as it was parsed
163     */
164    private $originalQueryString;
165    /**
166     * @var DokuwikiUrl
167     */
168    private $dokuwikiUrl;
169
170    /**
171     * Link constructor.
172     * @param $ref
173     * @param TagAttributes|null $tagAttributes
174     */
175    public function __construct($ref, TagAttributes &$tagAttributes = null)
176    {
177
178        if ($tagAttributes == null) {
179            $tagAttributes = TagAttributes::createEmpty(\syntax_plugin_combo_link::TAG);
180        }
181        $this->attributes = &$tagAttributes;
182
183        if ($tagAttributes->hasComponentAttribute("name")) {
184            $this->name = $tagAttributes->getValueAndRemove("name");
185        }
186
187        /**
188         * Windows share link
189         */
190        if ($this->type == null) {
191            if (preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u', $ref)) {
192                $this->type = self::TYPE_WINDOWS_SHARE;
193                $this->ref = $ref;
194                return;
195            }
196        }
197
198        /**
199         * URI like links section with query and fragment
200         */
201
202        /**
203         * Local
204         */
205        if ($this->type == null) {
206            if (preg_match('!^#.+!', $ref)) {
207                $this->type = self::TYPE_LOCAL;
208                $this->ref = $ref;
209            }
210        }
211
212        /**
213         * Email validation pattern
214         * E-Mail (pattern below is defined in inc/mail.php)
215         *
216         * Example:
217         * [[support@combostrap.com?subject=hallo]]
218         * [[support@combostrap.com]]
219         */
220        if ($this->type == null) {
221            $emailRfc2822 = "0-9a-zA-Z!#$%&'*+/=?^_`{|}~-";
222            $emailPattern = '[' . $emailRfc2822 . ']+(?:\.[' . $emailRfc2822 . ']+)*@(?i:[0-9a-z][0-9a-z-]*\.)+(?i:[a-z]{2,63})';
223            if (preg_match('<' . $emailPattern . '>', $ref)) {
224                $this->type = self::TYPE_EMAIL;
225                $this->ref = $ref;
226                // we don't return. The query part is parsed afterwards
227            }
228        }
229
230
231        /**
232         * External
233         */
234        if ($this->type == null) {
235            if (preg_match('#^([a-z0-9\-\.+]+?)://#i', $ref)) {
236                $this->type = self::TYPE_EXTERNAL;
237                $this->schemeUri = strtolower(substr($ref, 0, strpos($ref, "://")));
238                $this->ref = $ref;
239            }
240        }
241
242        /**
243         * interwiki ?
244         */
245        $refProcessing = $ref;
246        if ($this->type == null) {
247            $interwikiPosition = strpos($refProcessing, ">");
248            if ($interwikiPosition !== false) {
249                $this->wiki = strtolower(substr($refProcessing, 0, $interwikiPosition));
250                $refProcessing = substr($refProcessing, $interwikiPosition + 1);
251                $this->ref = $ref;
252                $this->type = self::TYPE_INTERWIKI;
253            }
254        }
255
256        /**
257         * Internal then
258         */
259        if ($this->type == null) {
260            /**
261             * It can be a link with a ref template
262             */
263            if (substr($ref, 0, 1) === TemplateUtility::VARIABLE_PREFIX) {
264                $this->type = self::TYPE_INTERNAL_TEMPLATE;
265            } else {
266                $this->type = self::TYPE_INTERNAL;
267            }
268            $this->ref = $ref;
269        }
270
271
272        /**
273         * Url (called ref by dokuwiki)
274         */
275        $this->dokuwikiUrl = DokuwikiUrl::createFromUrl($refProcessing);
276
277
278    }
279
280    public static function createFromPageId($id, &$tagAttributes = null): LinkUtility
281    {
282        return new LinkUtility(":$id", $tagAttributes);
283    }
284
285    public static function createFromRef(string $ref, TagAttributes $tagAttributes = null): LinkUtility
286    {
287        return new LinkUtility($ref, $tagAttributes);
288    }
289
290    /**
291     * @param $name
292     * @return $this
293     */
294    public function setName($name)
295    {
296        $this->name = $name;
297        return $this;
298    }
299
300    /**
301     * @param $type
302     * @return $this
303     */
304    public function setType($type)
305    {
306        $this->type = $type;
307        return $this;
308    }
309
310
311    /**
312     * Parse the match of a syntax {@link DokuWiki_Syntax_Plugin} handle function
313     * @param $match
314     * @return string[] - an array with the attributes constant `ATTRIBUTE_xxxx` as key
315     *
316     * Code adapted from  {@link Doku_Handler::internallink()}
317     */
318    public static function parse($match)
319    {
320
321        // Strip the opening and closing markup
322        $linkString = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match);
323
324        // Split title from URL
325        $linkArray = explode('|', $linkString, 2);
326
327        // Id
328        $attributes[self::ATTRIBUTE_REF] = trim($linkArray[0]);
329
330
331        // Text or image
332        if (!isset($linkArray[1])) {
333            $attributes[self::ATTRIBUTE_NAME] = null;
334        } else {
335            // An image in the title
336            if (preg_match('/^\{\{[^\}]+\}\}$/', $linkArray[1])) {
337                // If the title is an image, convert it to an array containing the image details
338                $attributes[self::ATTRIBUTE_IMAGE] = Doku_Handler_Parse_Media($linkArray[1]);
339            } else {
340                $attributes[self::ATTRIBUTE_NAME] = $linkArray[1];
341            }
342        }
343
344        return $attributes;
345
346    }
347
348    /**
349     * @param Doku_Renderer_xhtml|null $renderer
350     * @return mixed
351     *
352     * Derived from {@link Doku_Renderer_xhtml::internallink()}
353     * and others
354     *
355     */
356    public function renderOpenTag(Doku_Renderer_xhtml $renderer = null)
357    {
358
359        $type = $this->getType();
360
361        /**
362         * Keep a reference to the renderer
363         * The {@link LinkUtility::getUrl()} depends on it
364         * for local or interwiki link
365         */
366        if ($renderer == null) {
367            if (in_array($type, [self::TYPE_LOCAL, self::TYPE_INTERWIKI])) {
368                LogUtility::msg("The link ($this) is not ($type) link, the renderer should be not null and given", LogUtility::LVL_MSG_ERROR);
369            }
370        }
371        $this->renderer = $renderer;
372
373        /**
374         * Add the attribute from the URL
375         * if this is not a `do`
376         */
377
378        switch ($type) {
379            case self::TYPE_INTERNAL:
380                if (!$this->dokuwikiUrl->hasQueryParameter("do")) {
381                    foreach ($this->getDokuwikiUrl()->getQueryParameters() as $key => $value) {
382                        if ($key !== self::SEARCH_HIGHLIGHT_QUERY_PROPERTY) {
383                            $this->attributes->addComponentAttributeValue($key, $value);
384                        }
385                    }
386                }
387                break;
388            case
389            self::TYPE_EMAIL:
390                foreach ($this->getDokuwikiUrl()->getQueryParameters() as $key => $value) {
391                    if (!in_array($key, self::EMAIL_VALID_PARAMETERS)) {
392                        $this->attributes->addComponentAttributeValue($key, $value);
393                    }
394                }
395                break;
396        }
397
398
399        global $conf;
400
401        /**
402         * Get the url
403         */
404        $url = $this->getUrl();
405        if (!empty($url)) {
406            $this->attributes->addHtmlAttributeValue("href", $url);
407        }
408
409
410        /**
411         * Processing by type
412         */
413        switch ($this->getType()) {
414            case self::TYPE_INTERWIKI:
415
416                // normal link for the `this` wiki
417                if ($this->getWiki() != "this") {
418                    PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TYPE_INTERWIKI);
419                }
420                /**
421                 * Target
422                 */
423                $interWikiConf = $conf['target']['interwiki'];
424                if (!empty($interWikiConf)) {
425                    $this->attributes->addHtmlAttributeValue('target', $interWikiConf);
426                    $this->attributes->addHtmlAttributeValue('rel', 'noopener');
427                }
428                $this->attributes->addClassName(self::getHtmlClassInterWikiLink());
429                $wikiClass = "iw_" . preg_replace('/[^_\-a-z0-9]+/i', '_', $this->getWiki());
430                $this->attributes->addClassName($wikiClass);
431                if (!$this->wikiExists()) {
432                    $this->attributes->addClassName(self::getHtmlClassNotExist());
433                    $this->attributes->addHtmlAttributeValue("rel", 'nofollow');
434                }
435
436                break;
437            case self::TYPE_INTERNAL:
438
439                // https://www.dokuwiki.org/config:target
440                $target = $conf['target']['wiki'];
441                if (!empty($target)) {
442                    $this->attributes->addHtmlAttributeValue('target', $target);
443                }
444                /**
445                 * Internal Page
446                 */
447                $linkedPage = $this->getInternalPage();
448                $this->attributes->addHtmlAttributeValue("data-wiki-id", $linkedPage->getDokuwikiId());
449
450
451                if (!$linkedPage->exists()) {
452
453                    /**
454                     * Red color
455                     */
456                    $this->attributes->addClassName(self::getHtmlClassNotExist());
457                    $this->attributes->addHtmlAttributeValue("rel", 'nofollow');
458
459                } else {
460
461                    /**
462                     * Internal Link Class
463                     */
464                    $this->attributes->addClassName(self::getHtmlClassInternalLink());
465
466                    /**
467                     * Link Creation
468                     * Do we need to set the title or the tooltip
469                     * Processing variables
470                     */
471                    $acronym = "";
472
473                    /**
474                     * Preview tooltip
475                     */
476                    $previewConfig = PluginUtility::getConfValue(self::CONF_PREVIEW_LINK, self::CONF_PREVIEW_LINK_DEFAULT);
477                    $preview = $this->attributes->getBooleanValueAndRemove(self::PREVIEW_ATTRIBUTE, $previewConfig);
478                    if ($preview) {
479                        syntax_plugin_combo_tooltip::addToolTipSnippetIfNeeded();
480                        $tooltipHtml = <<<EOF
481<h3>{$linkedPage->getNameOrDefault()}</h3>
482<p>{$linkedPage->getDescriptionOrElseDokuWiki()}</p>
483EOF;
484                        $dataAttributeNamespace = Bootstrap::getDataNamespace();
485                        $this->attributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-toggle", "tooltip");
486                        $this->attributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-placement", "top");
487                        $this->attributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-html", "true");
488                        $this->attributes->addHtmlAttributeValue("title", $tooltipHtml);
489                    }
490
491                    /**
492                     * Low quality Page
493                     * (It has a higher priority than preview and
494                     * the code comes then after)
495                     */
496                    $pageProtectionAcronym = strtolower(PageProtection::ACRONYM);
497                    if ($linkedPage->isLowQualityPage()) {
498
499                        /**
500                         * Add a class to style it differently
501                         * (the acronym is added to the description, later)
502                         */
503                        $acronym = LowQualityPage::LOW_QUALITY_PROTECTION_ACRONYM;
504                        $lowerCaseLowQualityAcronym = strtolower(LowQualityPage::LOW_QUALITY_PROTECTION_ACRONYM);
505                        $this->attributes->addClassName(LowQualityPage::CLASS_NAME . "-combo");
506                        $snippetLowQualityPageId = $lowerCaseLowQualityAcronym;
507                        PluginUtility::getSnippetManager()->attachCssSnippetForBar($snippetLowQualityPageId);
508                        /**
509                         * Note The protection does occur on Javascript level, not on the HTML
510                         * because the created page is valid for a anonymous or logged-in user
511                         * Javascript is controlling
512                         */
513                        if (LowQualityPage::isProtectionEnabled()) {
514
515                            $linkType = LowQualityPage::getLowQualityLinkType();
516                            $this->attributes->addHtmlAttributeValue("data-$pageProtectionAcronym-link", $linkType);
517                            $this->attributes->addHtmlAttributeValue("data-$pageProtectionAcronym-source", $lowerCaseLowQualityAcronym);
518
519                            /**
520                             * Low Quality Page protection javascript is only for warning or login link
521                             */
522                            if (in_array($linkType, [PageProtection::PAGE_PROTECTION_LINK_WARNING, PageProtection::PAGE_PROTECTION_LINK_LOGIN])) {
523                                PageProtection::addPageProtectionSnippet();
524                            }
525
526                        }
527                    }
528
529                    /**
530                     * Late publication has a higher priority than
531                     * the late publication and the is therefore after
532                     * (In case this a low quality page late published)
533                     */
534                    if ($linkedPage->isLatePublication()) {
535                        /**
536                         * Add a class to style it differently if needed
537                         */
538                        $this->attributes->addClassName(PagePublicationDate::LATE_PUBLICATION_CLASS_NAME . "-combo");
539                        if (PagePublicationDate::isLatePublicationProtectionEnabled()) {
540                            $acronym = PagePublicationDate::LATE_PUBLICATION_PROTECTION_ACRONYM;
541                            $lowerCaseLatePublicationAcronym = strtolower(PagePublicationDate::LATE_PUBLICATION_PROTECTION_ACRONYM);
542                            $this->attributes->addHtmlAttributeValue("data-$pageProtectionAcronym-link", PageProtection::PAGE_PROTECTION_LINK_LOGIN);
543                            $this->attributes->addHtmlAttributeValue("data-$pageProtectionAcronym-source", $lowerCaseLatePublicationAcronym);
544                            PageProtection::addPageProtectionSnippet();
545                        }
546
547                    }
548
549                    /**
550                     * Title (ie tooltip vs title html attribute)
551                     */
552                    if (!$this->attributes->hasAttribute("title")) {
553
554                        /**
555                         * If this is not a link into the same page
556                         */
557                        if (!empty($this->getDokuwikiUrl()->getPath())) {
558                            $description = $linkedPage->getDescriptionOrElseDokuWiki();
559                            if (empty($description)) {
560                                // Rare case
561                                $description = $linkedPage->getH1OrDefault();
562                            }
563                            if (!empty($acronym)) {
564                                $description = $description . " ($acronym)";
565                            }
566                            $this->attributes->addHtmlAttributeValue("title", $description);
567                        }
568
569                    }
570
571                }
572
573                break;
574            case
575            self::TYPE_EXTERNAL:
576                if ($conf['relnofollow']) {
577                    $this->attributes->addHtmlAttributeValue("rel", 'nofollow ugc');
578                }
579                // https://www.dokuwiki.org/config:target
580                $externTarget = $conf['target']['extern'];
581                if (!empty($externTarget)) {
582                    $this->attributes->addHtmlAttributeValue('target', $externTarget);
583                    $this->attributes->addHtmlAttributeValue("rel", 'noopener');
584                }
585                $this->attributes->addClassName(self::getHtmlClassExternalLink());
586                break;
587            case self::TYPE_WINDOWS_SHARE:
588                // https://www.dokuwiki.org/config:target
589                $windowsTarget = $conf['target']['windows'];
590                if (!empty($windowsTarget)) {
591                    $this->attributes->addHtmlAttributeValue('target', $windowsTarget);
592                }
593                $this->attributes->addClassName("windows");
594                break;
595            case self::TYPE_LOCAL:
596                break;
597            case self::TYPE_EMAIL:
598                $this->attributes->addClassName(self::getHtmlClassEmailLink());
599                break;
600            default:
601                LogUtility::msg("The type (" . $this->getType() . ") is unknown", LogUtility::LVL_MSG_ERROR, \syntax_plugin_combo_link::TAG);
602
603        }
604
605        /**
606         * An email URL and title
607         * may be already encoded because of the vanguard configuration
608         *
609         * The url is not treated as an attribute
610         * because the transformation function encodes the value
611         * to mitigate XSS
612         *
613         */
614        if ($this->getType() == self::TYPE_EMAIL) {
615            $emailAddress = $this->obfuscateEmail($this->dokuwikiUrl->getPath());
616            $this->attributes->addHtmlAttributeValue("title", $emailAddress);
617        }
618
619        /**
620         * Return
621         */
622        return $this->attributes->toHtmlEnterTag("a");
623
624
625    }
626
627    /**
628     * Keep track of the backlinks ie meta['relation']['references']
629     * @param Doku_Renderer_metadata $metaDataRenderer
630     */
631    public
632    function handleMetadata($metaDataRenderer)
633    {
634
635        switch ($this->getType()) {
636            case self::TYPE_INTERNAL:
637                /**
638                 * The relative link should be passed (ie the original)
639                 */
640                $metaDataRenderer->internallink($this->ref);
641                break;
642            case self::TYPE_EXTERNAL:
643                $metaDataRenderer->externallink($this->ref, $this->name);
644                break;
645            case self::TYPE_LOCAL:
646                $metaDataRenderer->locallink($this->ref, $this->name);
647                break;
648            case self::TYPE_EMAIL:
649                $metaDataRenderer->emaillink($this->ref, $this->name);
650                break;
651            case self::TYPE_INTERWIKI:
652                $interWikiSplit = preg_split("/>/", $this->ref);
653                $metaDataRenderer->interwikilink($this->ref, $this->name, $interWikiSplit[0], $interWikiSplit[1]);
654                break;
655            case self::TYPE_WINDOWS_SHARE:
656                $metaDataRenderer->windowssharelink($this->ref, $this->name);
657                break;
658            case self::TYPE_INTERNAL_TEMPLATE:
659                // No backlinks for link template
660                break;
661            default:
662                LogUtility::msg("The link ({$this->ref}) with the type " . $this->type . " was not processed into the metadata");
663        }
664    }
665
666    /**
667     * Return the type of link from an ID
668     *
669     * @return string a `TYPE_xxx` constant
670     */
671    public
672    function getType()
673    {
674        return $this->type;
675    }
676
677
678    /**
679     * @param array $stats
680     * Calculate internal link statistics
681     */
682    public
683    function processLinkStats(array &$stats)
684    {
685
686        if ($this->getType() == self::TYPE_INTERNAL) {
687
688
689            /**
690             * Internal link count
691             */
692            if (!array_key_exists(AnalyticsDocument::INTERNAL_LINK_COUNT, $stats)) {
693                $stats[AnalyticsDocument::INTERNAL_LINK_COUNT] = 0;
694            }
695            $stats[AnalyticsDocument::INTERNAL_LINK_COUNT]++;
696
697
698            /**
699             * Broken link ?
700             */
701            $id = $this->getInternalPage()->getDokuwikiId();
702            if (!$this->getInternalPage()->exists()) {
703                $stats[AnalyticsDocument::INTERNAL_LINK_BROKEN_COUNT]++;
704                $stats[AnalyticsDocument::INFO][] = "The internal link `{$id}` does not exist";
705            }
706
707            /**
708             * Calculate link distance
709             */
710            global $ID;
711            $a = explode(':', getNS($ID));
712            $b = explode(':', getNS($id));
713            while (isset($a[0]) && $a[0] == $b[0]) {
714                array_shift($a);
715                array_shift($b);
716            }
717            $length = count($a) + count($b);
718            $stats[AnalyticsDocument::INTERNAL_LINK_DISTANCE][] = $length;
719
720        } else if ($this->getType() == self::TYPE_EXTERNAL) {
721
722            if (!array_key_exists(AnalyticsDocument::EXTERNAL_LINK_COUNT, $stats)) {
723                $stats[AnalyticsDocument::EXTERNAL_LINK_COUNT] = 0;
724            }
725            $stats[AnalyticsDocument::EXTERNAL_LINK_COUNT]++;
726
727        } else if ($this->getType() == self::TYPE_LOCAL) {
728
729            if (!array_key_exists(AnalyticsDocument::LOCAL_LINK_COUNT, $stats)) {
730                $stats[AnalyticsDocument::LOCAL_LINK_COUNT] = 0;
731            }
732            $stats[AnalyticsDocument::LOCAL_LINK_COUNT]++;
733
734        } else if ($this->getType() == self::TYPE_INTERWIKI) {
735
736            if (!array_key_exists(AnalyticsDocument::INTERWIKI_LINK_COUNT, $stats)) {
737                $stats[AnalyticsDocument::INTERWIKI_LINK_COUNT] = 0;
738            }
739            $stats[AnalyticsDocument::INTERWIKI_LINK_COUNT]++;
740
741        } else if ($this->getType() == self::TYPE_EMAIL) {
742
743            if (!array_key_exists(AnalyticsDocument::EMAIL_COUNT, $stats)) {
744                $stats[AnalyticsDocument::EMAIL_COUNT] = 0;
745            }
746            $stats[AnalyticsDocument::EMAIL_COUNT]++;
747
748        } else if ($this->getType() == self::TYPE_WINDOWS_SHARE) {
749
750            if (!array_key_exists(AnalyticsDocument::WINDOWS_SHARE_COUNT, $stats)) {
751                $stats[AnalyticsDocument::WINDOWS_SHARE_COUNT] = 0;
752            }
753            $stats[AnalyticsDocument::WINDOWS_SHARE_COUNT]++;
754
755        } else if ($this->getType() == self::TYPE_INTERNAL_TEMPLATE) {
756
757            if (!array_key_exists(AnalyticsDocument::TEMPLATE_LINK_COUNT, $stats)) {
758                $stats[AnalyticsDocument::TEMPLATE_LINK_COUNT] = 0;
759            }
760            $stats[AnalyticsDocument::TEMPLATE_LINK_COUNT]++;
761
762        } else {
763
764            LogUtility::msg("The link `{$this->ref}` with the type (" . $this->getType() . ")  is not taken into account into the statistics");
765
766        }
767
768
769    }
770
771    /**
772     * @return Page - the internal page or an error if the link is not an internal one
773     */
774    public
775    function getInternalPage()
776    {
777        if ($this->linkedPage == null) {
778            if ($this->getType() == self::TYPE_INTERNAL) {
779                // if there is no path, this is the actual page
780                $pathOrId = $this->dokuwikiUrl->getPath();
781
782                $this->linkedPage = Page::createPageFromNonQualifiedPath($pathOrId);
783
784            } else {
785                throw new \RuntimeException("You can't ask the internal page id from a link that is not an internal one");
786            }
787        }
788        return $this->linkedPage;
789    }
790
791    public
792    function getRef()
793    {
794        return $this->ref;
795    }
796
797    public
798    function getName()
799    {
800        $name = $this->name;
801
802        /**
803         * Templating
804         */
805        switch ($this->getType()) {
806            case self::TYPE_INTERNAL:
807                if (!empty($name)) {
808                    /**
809                     * With the new link syntax class, this is no more possible
810                     * because there is an enter and exit state
811                     * TODO: create a function to render on DOKU_LEXER_UNMATCHED ?
812                     */
813                    $name = TemplateUtility::renderStringTemplateForPageId($name, $this->dokuwikiUrl->getPath());
814                }
815                if (empty($name)) {
816                    $name = $this->getInternalPage()->getNameOrDefault();
817                    if (useHeading('content')) {
818                        $page = $this->getInternalPage();
819                        $h1 = $page->getH1();
820                        if (!empty($h1)) {
821                            $name = $h1;
822                        } else {
823                            /**
824                             * In dokuwiki by default, title = h1
825                             * If there is no h1, we take title
826                             * for backward compatibility
827                             */
828                            $title = $page->getTitle();
829                            if (!empty($title)) {
830                                $name = $title;
831                            }
832                        }
833                    }
834                }
835                break;
836            case self::TYPE_EMAIL:
837                if (empty($name)) {
838                    global $conf;
839                    $email = $this->dokuwikiUrl->getPath();
840                    switch ($conf['mailguard']) {
841                        case 'none' :
842                            $name = $email;
843                            break;
844                        case 'visible' :
845                        default :
846                            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
847                            $name = strtr($email, $obfuscate);
848                    }
849
850                }
851                break;
852            case self::TYPE_INTERWIKI:
853                if (empty($name)) {
854                    $name = $this->dokuwikiUrl->getPath();
855                }
856                break;
857            case self::TYPE_LOCAL:
858                if (empty($name)) {
859                    $name = $this->dokuwikiUrl->getFragment();
860                }
861                break;
862            default:
863                return $this->getRef();
864        }
865
866        return $name;
867    }
868
869    /**
870     * @param $title -the value of the title attribute of the anchor
871     */
872    public
873    function setTitle($title)
874    {
875        $this->title = $title;
876    }
877
878
879    public
880    function getUrl()
881    {
882        $url = "";
883        switch ($this->getType()) {
884            case self::TYPE_INTERNAL:
885                $page = $this->getInternalPage();
886
887                /**
888                 * Styling attribute
889                 * may be passed via parameters
890                 * for internal link
891                 * We don't want the styling attribute
892                 * in the URL
893                 *
894                 * We will not overwrite the parameters if this is an dokuwiki
895                 * action link (with the `do` property)
896                 */
897                if ($this->dokuwikiUrl->hasQueryParameter("do")) {
898
899                    $url = wl($page->getDokuwikiId(), $this->dokuwikiUrl->getQueryParameters());
900
901                } else {
902
903                    /**
904                     * No parameters by default known
905                     */
906                    $url = $page->getCanonicalUrl(
907                        [],
908                        false,
909                        DokuwikiUrl::AMPERSAND_URL_ENCODED_FOR_HTML
910                    );
911
912                    /**
913                     * The search term
914                     * Code adapted found at {@link Doku_Renderer_xhtml::internallink()}
915                     * We can't use the previous {@link wl function}
916                     * because it encode too much
917                     */
918                    $searchTerms = $this->dokuwikiUrl->getQueryParameter(self::SEARCH_HIGHLIGHT_QUERY_PROPERTY);
919                    if ($searchTerms !== null) {
920                        $url .= DokuwikiUrl::AMPERSAND_URL_ENCODED_FOR_HTML;
921                        PluginUtility::getSnippetManager()->attachCssSnippetForBar("search");
922                        if (is_array($searchTerms)) {
923                            /**
924                             * To verify, do we really need the []
925                             * to get an array in php ?
926                             */
927                            $searchTermsQuery = [];
928                            foreach ($searchTerms as $searchTerm) {
929                                $searchTermsQuery[] = "s[]=$searchTerm";
930                            }
931                            $url .= implode(DokuwikiUrl::AMPERSAND_URL_ENCODED_FOR_HTML, $searchTermsQuery);
932                        } else {
933                            $url .= "s=$searchTerms";
934                        }
935                    }
936
937
938                }
939                if ($this->dokuwikiUrl->getFragment() != null) {
940                    /**
941                     * pageutils (transform a fragment in section id)
942                     */
943                    $check = false;
944                    $url .= '#' . sectionID($this->dokuwikiUrl->getFragment(), $check);
945                }
946                break;
947            case self::TYPE_INTERWIKI:
948                $wiki = $this->wiki;
949                $extendedPath = $this->dokuwikiUrl->getPath();
950                if ($this->dokuwikiUrl->getFragment() !== null) {
951                    $extendedPath .= "#{$this->dokuwikiUrl->getFragment()}";
952                }
953                $url = $this->renderer->_resolveInterWiki($wiki, $extendedPath);
954                break;
955            case self::TYPE_WINDOWS_SHARE:
956                $url = str_replace('\\', '/', $this->getRef());
957                $url = 'file:///' . $url;
958                break;
959            case self::TYPE_EXTERNAL:
960                /**
961                 * Authorized scheme only
962                 * to not inject code
963                 */
964                if (is_null($this->authorizedSchemes)) {
965                    $this->authorizedSchemes = getSchemes();
966                }
967                if (!in_array($this->schemeUri, $this->authorizedSchemes)) {
968                    $url = '';
969                } else {
970                    $url = $this->ref;
971                }
972                break;
973            case self::TYPE_EMAIL:
974                /**
975                 * An email link is `<email>`
976                 * {@link Emaillink::connectTo()}
977                 * or
978                 * {@link PluginTrait::email()
979                 */
980                // common.php#obfsucate implements the $conf['mailguard']
981                $uri = $this->getDokuwikiUrl()->getPath();
982                $uri = $this->obfuscateEmail($uri);
983                $uri = urlencode($uri);
984                $queryParameters = $this->getDokuwikiUrl()->getQueryParameters();
985                if (sizeof($queryParameters) > 0) {
986                    $uri .= "?";
987                    foreach ($queryParameters as $key => $value) {
988                        $value = urlencode($value);
989                        $key = urlencode($key);
990                        if (in_array($key, self::EMAIL_VALID_PARAMETERS)) {
991                            $uri .= "$key=$value";
992                        }
993                    }
994                }
995                $url = 'mailto:' . $uri;
996                break;
997            case self::TYPE_LOCAL:
998                $url = '#' . $this->renderer->_headerToLink($this->ref);
999                break;
1000            default:
1001                LogUtility::log2FrontEnd("The url type (" . $this->getType() . ") was not expected to get the URL", LogUtility::LVL_MSG_ERROR, \syntax_plugin_combo_link::TAG);
1002        }
1003
1004
1005        return $url;
1006    }
1007
1008    public
1009    function getWiki()
1010    {
1011        return $this->wiki;
1012    }
1013
1014
1015    public
1016    function getScheme()
1017    {
1018        return $this->schemeUri;
1019    }
1020
1021    /**
1022     * @return bool true if the page should be protected
1023     */
1024    private
1025    function isProtectedLink()
1026    {
1027        $protectedLink = false;
1028        if ($this->getType() == self::TYPE_INTERNAL) {
1029
1030            // Low Quality Page protection
1031            $lqppEnable = PluginUtility::getConfValue(LowQualityPage::CONF_LOW_QUALITY_PAGE_PROTECTION_ENABLE);
1032            if ($lqppEnable == 1
1033                && $this->getInternalPage()->isLowQualityPage()) {
1034                $protectedLink = true;
1035            }
1036
1037            if ($protectedLink === false) {
1038                $latePublicationProtectionEnabled = PluginUtility::getConfValue(PagePublicationDate::CONF_LATE_PUBLICATION_PROTECTION_ENABLE);
1039                if ($latePublicationProtectionEnabled == 1
1040                    && $this->getInternalPage()->isLatePublication()) {
1041                    $protectedLink = true;
1042                }
1043            }
1044        }
1045        return $protectedLink;
1046    }
1047
1048    /**
1049     * @return string
1050     * @deprecated a link is a HTML anchor element (ie a), no more link with span
1051     */
1052    public
1053    function getHTMLTag()
1054    {
1055        return "a";
1056    }
1057
1058    private
1059    function wikiExists()
1060    {
1061        $wikis = getInterwiki();
1062        return key_exists($this->wiki, $wikis);
1063    }
1064
1065    private
1066    function obfuscateEmail($email, $inAttribute = true): string
1067    {
1068        /**
1069         * adapted from {@link obfuscate()} in common.php
1070         */
1071        global $conf;
1072
1073        $mailGuard = $conf['mailguard'];
1074        if ($mailGuard === "hex" && $inAttribute) {
1075            $mailGuard = "visible";
1076        }
1077        switch ($mailGuard) {
1078            case 'visible' :
1079                $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1080                return strtr($email, $obfuscate);
1081
1082            case 'hex' :
1083                return Conversion::toHtml($email, true);
1084
1085            case 'none' :
1086            default :
1087                return $email;
1088        }
1089    }
1090
1091    public
1092    function renderClosingTag(): string
1093    {
1094        $HTMLTag = $this->getHTMLTag();
1095        return "</$HTMLTag>";
1096    }
1097
1098    public
1099    function isRelative(): bool
1100    {
1101        return strpos($this->path, ':') !== 0;
1102    }
1103
1104    public
1105    function getDokuwikiUrl(): DokuwikiUrl
1106    {
1107        return $this->dokuwikiUrl;
1108    }
1109
1110
1111    public
1112    static function getHtmlClassInternalLink()
1113    {
1114        $oldClassName = PluginUtility::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME);
1115        if ($oldClassName) {
1116            return "wikilink1";
1117        } else {
1118            return "link-internal";
1119        }
1120    }
1121
1122    public
1123    static function getHtmlClassEmailLink(): string
1124    {
1125        $oldClassName = PluginUtility::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME);
1126        if ($oldClassName) {
1127            return "mail";
1128        } else {
1129            return "link-mail";
1130        }
1131    }
1132
1133    public
1134    static function getHtmlClassInterWikiLink(): string
1135    {
1136        $oldClassName = PluginUtility::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME);
1137        if ($oldClassName) {
1138            return "interwiki";
1139        } else {
1140            return "link-interwiki";
1141        }
1142    }
1143
1144    public
1145    static function getHtmlClassExternalLink(): string
1146    {
1147        $oldClassName = PluginUtility::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME);
1148        if ($oldClassName) {
1149            return "urlextern";
1150        } else {
1151            return "link-external";
1152        }
1153    }
1154
1155//FYI: exist in dokuwiki is "wikilink1 but we let the control to the user
1156    public
1157    static function getHtmlClassNotExist()
1158    {
1159        $oldClassName = PluginUtility::getConfValue(self::CONF_USE_DOKUWIKI_CLASS_NAME);
1160        if ($oldClassName) {
1161            return "wikilink2";
1162        } else {
1163            return self::TEXT_ERROR_CLASS;
1164        }
1165    }
1166
1167    public
1168    function __toString()
1169    {
1170        return $this->ref;
1171    }
1172
1173    private
1174    function getEmailObfuscationConfiguration()
1175    {
1176        global $conf;
1177        return $conf['mailguard'];
1178    }
1179
1180
1181}
1182