6821427059d87ccf7d0b3a5e87154477889e0c37
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / html / output / node / HtmlOutputNodeA.class.php
1 <?php
2
3 namespace wcf\system\html\output\node;
4
5 use GuzzleHttp\Psr7\Exception\MalformedUriException;
6 use GuzzleHttp\Psr7\Uri;
7 use GuzzleHttp\Psr7\UriComparator;
8 use Psr\Http\Message\UriInterface;
9 use wcf\system\application\ApplicationHandler;
10 use wcf\system\html\node\AbstractHtmlNodeProcessor;
11 use wcf\system\request\RouteHandler;
12 use wcf\util\DOMUtil;
13 use wcf\util\FileUtil;
14 use wcf\util\StringUtil;
15
16 /**
17 * Processes links.
18 *
19 * @author Alexander Ebert
20 * @copyright 2001-2019 WoltLab GmbH
21 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
22 * @since 3.0
23 */
24 class HtmlOutputNodeA extends AbstractHtmlOutputNode
25 {
26 /**
27 * @inheritDoc
28 */
29 protected $tagName = 'a';
30
31 /**
32 * @inheritDoc
33 */
34 public function process(array $elements, AbstractHtmlNodeProcessor $htmlNodeProcessor)
35 {
36 /** @var \DOMElement $element */
37 foreach ($elements as $element) {
38 try {
39 $href = new Uri($element->getAttribute('href'));
40 } catch (MalformedUriException) {
41 // If the link href is not a valid URI we drop the entire link.
42 DOMUtil::removeNode($element, true);
43
44 continue;
45 }
46
47 if (ApplicationHandler::getInstance()->isInternalURL($href->__toString())) {
48 $href = $href->withScheme(RouteHandler::secureConnection() ? 'https' : 'http');
49
50 $element->setAttribute(
51 'href',
52 $href->__toString(),
53 );
54 } else {
55 /** @var HtmlOutputNodeProcessor $htmlNodeProcessor */
56 self::markLinkAsExternal($element, $htmlNodeProcessor->getHtmlProcessor()->enableUgc);
57 }
58
59 $value = StringUtil::trim($element->textContent);
60 if ($value === '') {
61 if ($element->childElementCount === 0) {
62 // Discard empty links, these were sometimes created by the
63 // previous editor when editing links.
64 DOMUtil::removeNode($element);
65
66 continue;
67 }
68 } else if ($this->isSuspiciousValue($value, $href)) {
69 $value = $href->__toString();
70 }
71
72 if ($this->outputType === 'text/html' || $this->outputType === 'text/simplified-html') {
73 if ($value === $href->__toString()) {
74 while ($element->childNodes->length) {
75 DOMUtil::removeNode($element->childNodes->item(0));
76 }
77
78 $newValue = $value;
79 if (\mb_strlen($newValue) > 60) {
80 try {
81 // The value returned by `Uri::__toString()` can be malformed.
82 // https://github.com/guzzle/psr7/issues/583
83 $uri = new Uri($newValue);
84 } catch (MalformedUriException) {
85 $uri = clone $href;
86 }
87
88 $schemeHost = Uri::composeComponents(
89 $uri->getScheme(),
90 $uri->getAuthority(),
91 '',
92 null,
93 null,
94 );
95 $pathQueryFragment = Uri::composeComponents(
96 null,
97 null,
98 $uri->getPath(),
99 $uri->getQuery(),
100 $uri->getFragment(),
101 );
102 if (\mb_strlen($pathQueryFragment) > 35) {
103 $pathQueryFragment = \mb_substr($pathQueryFragment, 0, 15) . StringUtil::HELLIP . \mb_substr($pathQueryFragment, -15);
104 }
105 $newValue = $schemeHost . $pathQueryFragment;
106 }
107
108 $element->appendChild(
109 $element->ownerDocument->createTextNode($newValue)
110 );
111 }
112 } elseif ($this->outputType === 'text/plain') {
113 if ($value !== $href->__toString()) {
114 $text = $value . ' [URL:' . $href->__toString() . ']';
115 } else {
116 $text = $href->__toString();
117 }
118
119 $htmlNodeProcessor->replaceElementWithText($element, $text, false);
120 }
121 }
122 }
123
124 /**
125 * Returns whether the given link value is suspicious with regard
126 * to the actual link target.
127 *
128 * A value is considered suspicious if it is a cross-origin URI (i.e.
129 * if one of host, port or scheme differs).
130 *
131 * @see \GuzzleHttp\Psr7\UriComparator::isCrossOrigin()
132 */
133 private function isSuspiciousValue(string $value, UriInterface $href): bool
134 {
135 if (!\preg_match(FileUtil::LINK_REGEX, $value)) {
136 return false;
137 }
138
139 try {
140 $value = new Uri($value);
141 } catch (MalformedUriException) {
142 return false;
143 }
144
145 return UriComparator::isCrossOrigin($href, $value);
146 }
147
148 /**
149 * Marks an element as external.
150 *
151 * @param \DOMElement $element
152 * @param bool $isUgc
153 */
154 public static function markLinkAsExternal(\DOMElement $element, $isUgc = false)
155 {
156 $element->setAttribute('class', 'externalURL');
157
158 $rel = 'nofollow';
159 if (EXTERNAL_LINK_TARGET_BLANK) {
160 $rel .= ' noopener';
161
162 $element->setAttribute('target', '_blank');
163 }
164 if ($isUgc) {
165 $rel .= ' ugc';
166 }
167
168 $element->setAttribute('rel', $rel);
169
170 // If the link contains only a single image that is floated to the right,
171 // then the external link marker is misaligned. Inheriting the CSS class
172 // will cause the link marker to behave properly.
173 if ($element->childNodes->length === 1) {
174 $child = $element->childNodes->item(0);
175 if ($child instanceof \DOMElement && $child->nodeName === 'img') {
176 if (
177 \preg_match(
178 '~\b(?P<className>messageFloatObject(?:Left|Right))\b~',
179 $child->getAttribute('class'),
180 $match
181 )
182 ) {
183 $element->setAttribute('class', $element->getAttribute('class') . ' ' . $match['className']);
184 }
185 }
186 }
187 }
188 }