| 148 |
lars |
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 <colinodell@gmail.com>
|
|
|
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\Event\DocumentParsedEvent;
|
|
|
17 |
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
|
|
18 |
use League\Config\ConfigurationInterface;
|
|
|
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 |
/** @psalm-readonly */
|
|
|
28 |
private ConfigurationInterface $config;
|
|
|
29 |
|
|
|
30 |
public function __construct(ConfigurationInterface $config)
|
|
|
31 |
{
|
|
|
32 |
$this->config = $config;
|
|
|
33 |
}
|
|
|
34 |
|
|
|
35 |
public function __invoke(DocumentParsedEvent $e): void
|
|
|
36 |
{
|
|
|
37 |
$internalHosts = $this->config->get('external_link/internal_hosts');
|
|
|
38 |
$openInNewWindow = $this->config->get('external_link/open_in_new_window');
|
|
|
39 |
$classes = $this->config->get('external_link/html_class');
|
|
|
40 |
|
|
|
41 |
foreach ($e->getDocument()->iterator() as $link) {
|
|
|
42 |
if (! ($link instanceof Link)) {
|
|
|
43 |
continue;
|
|
|
44 |
}
|
|
|
45 |
|
|
|
46 |
$host = \parse_url($link->getUrl(), PHP_URL_HOST);
|
|
|
47 |
if (! \is_string($host)) {
|
|
|
48 |
// Something is terribly wrong with this URL
|
|
|
49 |
continue;
|
|
|
50 |
}
|
|
|
51 |
|
|
|
52 |
if (self::hostMatches($host, $internalHosts)) {
|
|
|
53 |
$link->data->set('external', false);
|
|
|
54 |
$this->applyRelAttribute($link, false);
|
|
|
55 |
continue;
|
|
|
56 |
}
|
|
|
57 |
|
|
|
58 |
// Host does not match our list
|
|
|
59 |
$this->markLinkAsExternal($link, $openInNewWindow, $classes);
|
|
|
60 |
}
|
|
|
61 |
}
|
|
|
62 |
|
|
|
63 |
private function markLinkAsExternal(Link $link, bool $openInNewWindow, string $classes): void
|
|
|
64 |
{
|
|
|
65 |
$link->data->set('external', true);
|
|
|
66 |
$this->applyRelAttribute($link, true);
|
|
|
67 |
|
|
|
68 |
if ($openInNewWindow) {
|
|
|
69 |
$link->data->set('attributes/target', '_blank');
|
|
|
70 |
}
|
|
|
71 |
|
|
|
72 |
if ($classes !== '') {
|
|
|
73 |
$link->data->append('attributes/class', $classes);
|
|
|
74 |
}
|
|
|
75 |
}
|
|
|
76 |
|
|
|
77 |
private function applyRelAttribute(Link $link, bool $isExternal): void
|
|
|
78 |
{
|
|
|
79 |
$options = [
|
|
|
80 |
'nofollow' => $this->config->get('external_link/nofollow'),
|
|
|
81 |
'noopener' => $this->config->get('external_link/noopener'),
|
|
|
82 |
'noreferrer' => $this->config->get('external_link/noreferrer'),
|
|
|
83 |
];
|
|
|
84 |
|
|
|
85 |
foreach ($options as $type => $option) {
|
|
|
86 |
switch (true) {
|
|
|
87 |
case $option === self::APPLY_ALL:
|
|
|
88 |
case $isExternal && $option === self::APPLY_EXTERNAL:
|
|
|
89 |
case ! $isExternal && $option === self::APPLY_INTERNAL:
|
|
|
90 |
$link->data->append('attributes/rel', $type);
|
|
|
91 |
}
|
|
|
92 |
}
|
|
|
93 |
}
|
|
|
94 |
|
|
|
95 |
/**
|
|
|
96 |
* @internal This method is only public so we can easily test it. DO NOT USE THIS OUTSIDE OF THIS EXTENSION!
|
|
|
97 |
*
|
|
|
98 |
* @param mixed $compareTo
|
|
|
99 |
*/
|
|
|
100 |
public static function hostMatches(string $host, $compareTo): bool
|
|
|
101 |
{
|
|
|
102 |
foreach ((array) $compareTo as $c) {
|
|
|
103 |
if (\strpos($c, '/') === 0) {
|
|
|
104 |
if (\preg_match($c, $host)) {
|
|
|
105 |
return true;
|
|
|
106 |
}
|
|
|
107 |
} elseif ($c === $host) {
|
|
|
108 |
return true;
|
|
|
109 |
}
|
|
|
110 |
}
|
|
|
111 |
|
|
|
112 |
return false;
|
|
|
113 |
}
|
|
|
114 |
}
|