1<?php
2
3/*
4 * This file is part of the league/commonmark package.
5 *
6 * (c) Colin O'Dell <colinodell@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace League\CommonMark\Extension\ExternalLink;
13
14use League\CommonMark\EnvironmentInterface;
15use League\CommonMark\Event\DocumentParsedEvent;
16use League\CommonMark\Inline\Element\Link;
17
18final class ExternalLinkProcessor
19{
20    public const APPLY_NONE = '';
21    public const APPLY_ALL = 'all';
22    public const APPLY_EXTERNAL = 'external';
23    public const APPLY_INTERNAL = 'internal';
24
25    /** @var EnvironmentInterface */
26    private $environment;
27
28    public function __construct(EnvironmentInterface $environment)
29    {
30        $this->environment = $environment;
31    }
32
33    /**
34     * @param DocumentParsedEvent $e
35     *
36     * @return void
37     */
38    public function __invoke(DocumentParsedEvent $e)
39    {
40        $internalHosts = $this->environment->getConfig('external_link/internal_hosts', []);
41        $openInNewWindow = $this->environment->getConfig('external_link/open_in_new_window', false);
42        $classes = $this->environment->getConfig('external_link/html_class', '');
43
44        $walker = $e->getDocument()->walker();
45        while ($event = $walker->next()) {
46            if ($event->isEntering() && $event->getNode() instanceof Link) {
47                /** @var Link $link */
48                $link = $event->getNode();
49
50                $host = parse_url($link->getUrl(), PHP_URL_HOST);
51                if (empty($host)) {
52                    // Something is terribly wrong with this URL
53                    continue;
54                }
55
56                if (self::hostMatches($host, $internalHosts)) {
57                    $link->data['external'] = false;
58                    $this->applyRelAttribute($link, false);
59                    continue;
60                }
61
62                // Host does not match our list
63                $this->markLinkAsExternal($link, $openInNewWindow, $classes);
64            }
65        }
66    }
67
68    private function markLinkAsExternal(Link $link, bool $openInNewWindow, string $classes): void
69    {
70        $link->data['external'] = true;
71        $link->data['attributes'] = $link->getData('attributes', []);
72        $this->applyRelAttribute($link, true);
73
74        if ($openInNewWindow) {
75            $link->data['attributes']['target'] = '_blank';
76        }
77
78        if (!empty($classes)) {
79            $link->data['attributes']['class'] = trim(($link->data['attributes']['class'] ?? '') . ' ' . $classes);
80        }
81    }
82
83    private function applyRelAttribute(Link $link, bool $isExternal): void
84    {
85        $rel = [];
86
87        $options = [
88            'nofollow'   => $this->environment->getConfig('external_link/nofollow', self::APPLY_NONE),
89            'noopener'   => $this->environment->getConfig('external_link/noopener', self::APPLY_EXTERNAL),
90            'noreferrer' => $this->environment->getConfig('external_link/noreferrer', self::APPLY_EXTERNAL),
91        ];
92
93        foreach ($options as $type => $option) {
94            switch (true) {
95                case $option === self::APPLY_ALL:
96                case $isExternal && $option === self::APPLY_EXTERNAL:
97                case !$isExternal && $option === self::APPLY_INTERNAL:
98                    $rel[] = $type;
99            }
100        }
101
102        if ($rel === []) {
103            return;
104        }
105
106        $link->data['attributes']['rel'] = \implode(' ', $rel);
107    }
108
109    /**
110     * @param string $host
111     * @param mixed  $compareTo
112     *
113     * @return bool
114     *
115     * @internal This method is only public so we can easily test it. DO NOT USE THIS OUTSIDE OF THIS EXTENSION!
116     */
117    public static function hostMatches(string $host, $compareTo)
118    {
119        foreach ((array) $compareTo as $c) {
120            if (strpos($c, '/') === 0) {
121                if (preg_match($c, $host)) {
122                    return true;
123                }
124            } elseif ($c === $host) {
125                return true;
126            }
127        }
128
129        return false;
130    }
131}
132