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