Completed
Push — master ( fb47ed...8a4ab4 )
by Colin
01:43
created

ExternalLinkProcessor::__invoke()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6

Importance

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