1/*
2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3 * (C) 1999 Antti Koivisto (koivisto@kde.org)
4 * (C) 2000 Simon Hausmann <hausmann@kde.org>
5 * Copyright (C) 2003-2016 Apple Inc. All rights reserved.
6 * (C) 2006 Graham Dennis (graham.dennis@gmail.com)
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Library General Public License for more details.
17 *
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24#include "config.h"
25#include "HTMLAnchorElement.h"
26
27#include "AdClickAttribution.h"
28#include "DOMTokenList.h"
29#include "ElementIterator.h"
30#include "EventHandler.h"
31#include "EventNames.h"
32#include "Frame.h"
33#include "FrameLoader.h"
34#include "FrameLoaderClient.h"
35#include "FrameLoaderTypes.h"
36#include "FrameSelection.h"
37#include "HTMLCanvasElement.h"
38#include "HTMLImageElement.h"
39#include "HTMLParserIdioms.h"
40#include "HTMLPictureElement.h"
41#include "KeyboardEvent.h"
42#include "MouseEvent.h"
43#include "PingLoader.h"
44#include "PlatformMouseEvent.h"
45#include "RegistrableDomain.h"
46#include "RenderImage.h"
47#include "ResourceRequest.h"
48#include "RuntimeEnabledFeatures.h"
49#include "SVGImage.h"
50#include "ScriptController.h"
51#include "SecurityOrigin.h"
52#include "SecurityPolicy.h"
53#include "Settings.h"
54#include "URLUtils.h"
55#include "UserGestureIndicator.h"
56#include <wtf/IsoMallocInlines.h>
57#include <wtf/Optional.h>
58#include <wtf/text/StringBuilder.h>
59#include <wtf/text/StringConcatenateNumbers.h>
60
61namespace WebCore {
62
63WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLAnchorElement);
64
65using namespace HTMLNames;
66
67HTMLAnchorElement::HTMLAnchorElement(const QualifiedName& tagName, Document& document)
68 : HTMLElement(tagName, document)
69 , m_hasRootEditableElementForSelectionOnMouseDown(false)
70 , m_wasShiftKeyDownOnMouseDown(false)
71{
72}
73
74Ref<HTMLAnchorElement> HTMLAnchorElement::create(Document& document)
75{
76 return adoptRef(*new HTMLAnchorElement(aTag, document));
77}
78
79Ref<HTMLAnchorElement> HTMLAnchorElement::create(const QualifiedName& tagName, Document& document)
80{
81 return adoptRef(*new HTMLAnchorElement(tagName, document));
82}
83
84HTMLAnchorElement::~HTMLAnchorElement()
85{
86 clearRootEditableElementForSelectionOnMouseDown();
87}
88
89bool HTMLAnchorElement::supportsFocus() const
90{
91 if (hasEditableStyle())
92 return HTMLElement::supportsFocus();
93 // If not a link we should still be able to focus the element if it has tabIndex.
94 return isLink() || HTMLElement::supportsFocus();
95}
96
97bool HTMLAnchorElement::isMouseFocusable() const
98{
99 // Only allow links with tabIndex or contentEditable to be mouse focusable.
100 if (isLink())
101 return HTMLElement::supportsFocus();
102
103 return HTMLElement::isMouseFocusable();
104}
105
106static bool hasNonEmptyBox(RenderBoxModelObject* renderer)
107{
108 if (!renderer)
109 return false;
110
111 // Before calling absoluteRects, check for the common case where borderBoundingBox
112 // is non-empty, since this is a faster check and almost always returns true.
113 // FIXME: Why do we need to call absoluteRects at all?
114 if (!renderer->borderBoundingBox().isEmpty())
115 return true;
116
117 // FIXME: Since all we are checking is whether the rects are empty, could we just
118 // pass in 0,0 for the layout point instead of calling localToAbsolute?
119 Vector<IntRect> rects;
120 renderer->absoluteRects(rects, flooredLayoutPoint(renderer->localToAbsolute()));
121 for (auto& rect : rects) {
122 if (!rect.isEmpty())
123 return true;
124 }
125
126 return false;
127}
128
129bool HTMLAnchorElement::isKeyboardFocusable(KeyboardEvent* event) const
130{
131 if (!isLink())
132 return HTMLElement::isKeyboardFocusable(event);
133
134 if (!isFocusable())
135 return false;
136
137 if (!document().frame())
138 return false;
139
140 if (!document().frame()->eventHandler().tabsToLinks(event))
141 return false;
142
143 if (!renderer() && ancestorsOfType<HTMLCanvasElement>(*this).first())
144 return true;
145
146 return hasNonEmptyBox(renderBoxModelObject());
147}
148
149static void appendServerMapMousePosition(StringBuilder& url, Event& event)
150{
151 if (!is<MouseEvent>(event))
152 return;
153 auto& mouseEvent = downcast<MouseEvent>(event);
154
155 if (!is<HTMLImageElement>(mouseEvent.target()))
156 return;
157
158 auto& imageElement = downcast<HTMLImageElement>(*mouseEvent.target());
159 if (!imageElement.isServerMap())
160 return;
161
162 auto* renderer = imageElement.renderer();
163 if (!is<RenderImage>(renderer))
164 return;
165
166 // FIXME: This should probably pass UseTransforms in the MapCoordinatesFlags.
167 auto absolutePosition = downcast<RenderImage>(*renderer).absoluteToLocal(FloatPoint(mouseEvent.pageX(), mouseEvent.pageY()));
168 url.append('?');
169 url.appendNumber(std::lround(absolutePosition.x()));
170 url.append(',');
171 url.appendNumber(std::lround(absolutePosition.y()));
172}
173
174void HTMLAnchorElement::defaultEventHandler(Event& event)
175{
176 if (isLink()) {
177 if (focused() && isEnterKeyKeydownEvent(event) && treatLinkAsLiveForEventType(NonMouseEvent)) {
178 event.setDefaultHandled();
179 dispatchSimulatedClick(&event);
180 return;
181 }
182
183 if (MouseEvent::canTriggerActivationBehavior(event) && treatLinkAsLiveForEventType(eventType(event))) {
184 handleClick(event);
185 return;
186 }
187
188 if (hasEditableStyle()) {
189 // This keeps track of the editable block that the selection was in (if it was in one) just before the link was clicked
190 // for the LiveWhenNotFocused editable link behavior
191 if (event.type() == eventNames().mousedownEvent && is<MouseEvent>(event) && downcast<MouseEvent>(event).button() != RightButton && document().frame()) {
192 setRootEditableElementForSelectionOnMouseDown(document().frame()->selection().selection().rootEditableElement());
193 m_wasShiftKeyDownOnMouseDown = downcast<MouseEvent>(event).shiftKey();
194 } else if (event.type() == eventNames().mouseoverEvent) {
195 // These are cleared on mouseover and not mouseout because their values are needed for drag events,
196 // but drag events happen after mouse out events.
197 clearRootEditableElementForSelectionOnMouseDown();
198 m_wasShiftKeyDownOnMouseDown = false;
199 }
200 }
201 }
202
203 HTMLElement::defaultEventHandler(event);
204}
205
206void HTMLAnchorElement::setActive(bool down, bool pause)
207{
208 if (hasEditableStyle()) {
209 EditableLinkBehavior editableLinkBehavior = document().settings().editableLinkBehavior();
210
211 switch (editableLinkBehavior) {
212 default:
213 case EditableLinkDefaultBehavior:
214 case EditableLinkAlwaysLive:
215 break;
216
217 case EditableLinkNeverLive:
218 return;
219
220 // Don't set the link to be active if the current selection is in the same editable block as
221 // this link
222 case EditableLinkLiveWhenNotFocused:
223 if (down && document().frame() && document().frame()->selection().selection().rootEditableElement() == rootEditableElement())
224 return;
225 break;
226
227 case EditableLinkOnlyLiveWithShiftKey:
228 return;
229 }
230
231 }
232
233 HTMLElement::setActive(down, pause);
234}
235
236void HTMLAnchorElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
237{
238 if (name == hrefAttr) {
239 bool wasLink = isLink();
240 setIsLink(!value.isNull() && !shouldProhibitLinks(this));
241 if (wasLink != isLink())
242 invalidateStyleForSubtree();
243 if (isLink()) {
244 String parsedURL = stripLeadingAndTrailingHTMLSpaces(value);
245 if (document().isDNSPrefetchEnabled() && document().frame()) {
246 if (protocolIsInHTTPFamily(parsedURL) || parsedURL.startsWith("//"))
247 document().frame()->loader().client().prefetchDNS(document().completeURL(parsedURL).host().toString());
248 }
249 }
250 } else if (name == nameAttr || name == titleAttr) {
251 // Do nothing.
252 } else if (name == relAttr) {
253 // Update HTMLAnchorElement::relList() if more rel attributes values are supported.
254 static NeverDestroyed<AtomicString> noReferrer("noreferrer", AtomicString::ConstructFromLiteral);
255 static NeverDestroyed<AtomicString> noOpener("noopener", AtomicString::ConstructFromLiteral);
256 static NeverDestroyed<AtomicString> opener("opener", AtomicString::ConstructFromLiteral);
257 const bool shouldFoldCase = true;
258 SpaceSplitString relValue(value, shouldFoldCase);
259 if (relValue.contains(noReferrer))
260 m_linkRelations.add(Relation::NoReferrer);
261 if (relValue.contains(noOpener))
262 m_linkRelations.add(Relation::NoOpener);
263 if (relValue.contains(opener))
264 m_linkRelations.add(Relation::Opener);
265 if (m_relList)
266 m_relList->associatedAttributeValueChanged(value);
267 }
268 else
269 HTMLElement::parseAttribute(name, value);
270}
271
272void HTMLAnchorElement::accessKeyAction(bool sendMouseEvents)
273{
274 dispatchSimulatedClick(0, sendMouseEvents ? SendMouseUpDownEvents : SendNoEvents);
275}
276
277bool HTMLAnchorElement::isURLAttribute(const Attribute& attribute) const
278{
279 return attribute.name().localName() == hrefAttr || HTMLElement::isURLAttribute(attribute);
280}
281
282bool HTMLAnchorElement::canStartSelection() const
283{
284 if (!isLink())
285 return HTMLElement::canStartSelection();
286 return hasEditableStyle();
287}
288
289bool HTMLAnchorElement::draggable() const
290{
291 const AtomicString& value = attributeWithoutSynchronization(draggableAttr);
292 if (equalLettersIgnoringASCIICase(value, "true"))
293 return true;
294 if (equalLettersIgnoringASCIICase(value, "false"))
295 return false;
296 return hasAttributeWithoutSynchronization(hrefAttr);
297}
298
299URL HTMLAnchorElement::href() const
300{
301 return document().completeURL(stripLeadingAndTrailingHTMLSpaces(attributeWithoutSynchronization(hrefAttr)));
302}
303
304void HTMLAnchorElement::setHref(const AtomicString& value)
305{
306 setAttributeWithoutSynchronization(hrefAttr, value);
307}
308
309bool HTMLAnchorElement::hasRel(Relation relation) const
310{
311 return m_linkRelations.contains(relation);
312}
313
314DOMTokenList& HTMLAnchorElement::relList() const
315{
316 if (!m_relList) {
317 m_relList = std::make_unique<DOMTokenList>(const_cast<HTMLAnchorElement&>(*this), HTMLNames::relAttr, [](Document&, StringView token) {
318#if USE(SYSTEM_PREVIEW)
319 return equalIgnoringASCIICase(token, "noreferrer") || equalIgnoringASCIICase(token, "noopener") || equalIgnoringASCIICase(token, "ar");
320#else
321 return equalIgnoringASCIICase(token, "noreferrer") || equalIgnoringASCIICase(token, "noopener");
322#endif
323 });
324 }
325 return *m_relList;
326}
327
328const AtomicString& HTMLAnchorElement::name() const
329{
330 return getNameAttribute();
331}
332
333int HTMLAnchorElement::tabIndex() const
334{
335 // Skip the supportsFocus check in HTMLElement.
336 return Element::tabIndex();
337}
338
339String HTMLAnchorElement::target() const
340{
341 return attributeWithoutSynchronization(targetAttr);
342}
343
344String HTMLAnchorElement::origin() const
345{
346 return SecurityOrigin::create(href()).get().toString();
347}
348
349String HTMLAnchorElement::text()
350{
351 return textContent();
352}
353
354void HTMLAnchorElement::setText(const String& text)
355{
356 setTextContent(text);
357}
358
359bool HTMLAnchorElement::isLiveLink() const
360{
361 return isLink() && treatLinkAsLiveForEventType(m_wasShiftKeyDownOnMouseDown ? MouseEventWithShiftKey : MouseEventWithoutShiftKey);
362}
363
364void HTMLAnchorElement::sendPings(const URL& destinationURL)
365{
366 if (!document().frame())
367 return;
368
369 if (!hasAttributeWithoutSynchronization(pingAttr) || !document().settings().hyperlinkAuditingEnabled())
370 return;
371
372 SpaceSplitString pingURLs(attributeWithoutSynchronization(pingAttr), false);
373 for (unsigned i = 0; i < pingURLs.size(); i++)
374 PingLoader::sendPing(*document().frame(), document().completeURL(pingURLs[i]), destinationURL);
375}
376
377#if USE(SYSTEM_PREVIEW)
378bool HTMLAnchorElement::isSystemPreviewLink() const
379{
380 if (!RuntimeEnabledFeatures::sharedFeatures().systemPreviewEnabled())
381 return false;
382
383 static NeverDestroyed<AtomicString> systemPreviewRelValue("ar", AtomicString::ConstructFromLiteral);
384
385 if (!relList().contains(systemPreviewRelValue))
386 return false;
387
388 if (auto* child = firstElementChild()) {
389 if (is<HTMLImageElement>(child) || is<HTMLPictureElement>(child)) {
390 auto numChildren = childElementCount();
391 // FIXME: We've documented that it should be the only child, but some early demos have two children.
392 return numChildren == 1 || numChildren == 2;
393 }
394 }
395
396 return false;
397}
398#endif
399
400Optional<AdClickAttribution> HTMLAnchorElement::parseAdClickAttribution() const
401{
402 using Campaign = AdClickAttribution::Campaign;
403 using Source = AdClickAttribution::Source;
404 using Destination = AdClickAttribution::Destination;
405
406 if (document().sessionID().isEphemeral()
407 || !RuntimeEnabledFeatures::sharedFeatures().adClickAttributionEnabled()
408 || !UserGestureIndicator::processingUserGesture())
409 return WTF::nullopt;
410
411 if (!hasAttributeWithoutSynchronization(adcampaignidAttr) && !hasAttributeWithoutSynchronization(addestinationAttr))
412 return WTF::nullopt;
413
414 auto adCampaignIDAttr = attributeWithoutSynchronization(adcampaignidAttr);
415 auto adDestinationAttr = attributeWithoutSynchronization(addestinationAttr);
416
417 if (adCampaignIDAttr.isEmpty() || adDestinationAttr.isEmpty()) {
418 document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "Both adcampaignid and addestination need to be set for Ad Click Attribution to work."_s);
419 return WTF::nullopt;
420 }
421
422 RefPtr<Frame> frame = document().frame();
423 if (!frame || !frame->isMainFrame()) {
424 document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "Ad Click Attribution is only supported in the main frame."_s);
425 return WTF::nullopt;
426 }
427
428 auto adCampaignID = parseHTMLNonNegativeInteger(adCampaignIDAttr);
429 if (!adCampaignID) {
430 document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution."_s);
431 return WTF::nullopt;
432 }
433
434 if (adCampaignID.value() > AdClickAttribution::MaxEntropy) {
435 document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, makeString("adcampaignid must have a non-negative value less than or equal to ", AdClickAttribution::MaxEntropy, " for Ad Click Attribution."));
436 return WTF::nullopt;
437 }
438
439 URL adDestinationURL { URL(), adDestinationAttr };
440 if (!adDestinationURL.isValid() || !adDestinationURL.protocolIsInHTTPFamily()) {
441 document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "addestination could not be converted to a valid HTTP-family URL."_s);
442 return WTF::nullopt;
443 }
444
445 RegistrableDomain documentRegistrableDomain { document().url() };
446 if (documentRegistrableDomain.matches(adDestinationURL)) {
447 document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "addestination can not be the same site as the current website."_s);
448 return WTF::nullopt;
449 }
450
451 return AdClickAttribution { Campaign(adCampaignID.value()), Source(documentRegistrableDomain), Destination(adDestinationURL) };
452}
453
454void HTMLAnchorElement::handleClick(Event& event)
455{
456 event.setDefaultHandled();
457
458 RefPtr<Frame> frame = document().frame();
459 if (!frame)
460 return;
461
462 StringBuilder url;
463 url.append(stripLeadingAndTrailingHTMLSpaces(attributeWithoutSynchronization(hrefAttr)));
464 appendServerMapMousePosition(url, event);
465 URL completedURL = document().completeURL(url.toString());
466
467 String downloadAttribute;
468#if ENABLE(DOWNLOAD_ATTRIBUTE)
469 if (RuntimeEnabledFeatures::sharedFeatures().downloadAttributeEnabled()) {
470 // Ignore the download attribute completely if the href URL is cross origin.
471 bool isSameOrigin = completedURL.protocolIsData() || document().securityOrigin().canRequest(completedURL);
472 if (isSameOrigin)
473 downloadAttribute = ResourceResponse::sanitizeSuggestedFilename(attributeWithoutSynchronization(downloadAttr));
474 else if (hasAttributeWithoutSynchronization(downloadAttr))
475 document().addConsoleMessage(MessageSource::Security, MessageLevel::Warning, "The download attribute on anchor was ignored because its href URL has a different security origin.");
476 }
477#endif
478
479 SystemPreviewInfo systemPreviewInfo;
480#if USE(SYSTEM_PREVIEW)
481 systemPreviewInfo.isSystemPreview = isSystemPreviewLink() && RuntimeEnabledFeatures::sharedFeatures().systemPreviewEnabled();
482
483 if (systemPreviewInfo.isSystemPreview) {
484 if (auto* child = firstElementChild())
485 systemPreviewInfo.systemPreviewRect = child->boundsInRootViewSpace();
486 }
487#endif
488
489 ShouldSendReferrer shouldSendReferrer = hasRel(Relation::NoReferrer) ? NeverSendReferrer : MaybeSendReferrer;
490
491 auto effectiveTarget = this->effectiveTarget();
492 Optional<NewFrameOpenerPolicy> newFrameOpenerPolicy;
493 if (hasRel(Relation::Opener))
494 newFrameOpenerPolicy = NewFrameOpenerPolicy::Allow;
495 else if (hasRel(Relation::NoOpener) || (RuntimeEnabledFeatures::sharedFeatures().blankAnchorTargetImpliesNoOpenerEnabled() && equalIgnoringASCIICase(effectiveTarget, "_blank")))
496 newFrameOpenerPolicy = NewFrameOpenerPolicy::Suppress;
497
498 auto adClickAttribution = parseAdClickAttribution();
499 // A matching conversion event needs to happen before the complete ad click attributionURL can be
500 // created. Thus, it should be empty for now.
501 ASSERT(!adClickAttribution || adClickAttribution->url().isNull());
502
503 frame->loader().urlSelected(completedURL, effectiveTarget, &event, LockHistory::No, LockBackForwardList::No, shouldSendReferrer, document().shouldOpenExternalURLsPolicyToPropagate(), newFrameOpenerPolicy, downloadAttribute, systemPreviewInfo, WTFMove(adClickAttribution));
504
505 sendPings(completedURL);
506}
507
508// Falls back to using <base> element's target if the anchor does not have one.
509String HTMLAnchorElement::effectiveTarget() const
510{
511 auto effectiveTarget = target();
512 if (effectiveTarget.isEmpty())
513 effectiveTarget = document().baseTarget();
514 return effectiveTarget;
515}
516
517HTMLAnchorElement::EventType HTMLAnchorElement::eventType(Event& event)
518{
519 if (!is<MouseEvent>(event))
520 return NonMouseEvent;
521 return downcast<MouseEvent>(event).shiftKey() ? MouseEventWithShiftKey : MouseEventWithoutShiftKey;
522}
523
524bool HTMLAnchorElement::treatLinkAsLiveForEventType(EventType eventType) const
525{
526 if (!hasEditableStyle())
527 return true;
528
529 switch (document().settings().editableLinkBehavior()) {
530 case EditableLinkDefaultBehavior:
531 case EditableLinkAlwaysLive:
532 return true;
533
534 case EditableLinkNeverLive:
535 return false;
536
537 // If the selection prior to clicking on this link resided in the same editable block as this link,
538 // and the shift key isn't pressed, we don't want to follow the link.
539 case EditableLinkLiveWhenNotFocused:
540 return eventType == MouseEventWithShiftKey || (eventType == MouseEventWithoutShiftKey && rootEditableElementForSelectionOnMouseDown() != rootEditableElement());
541
542 case EditableLinkOnlyLiveWithShiftKey:
543 return eventType == MouseEventWithShiftKey;
544 }
545
546 ASSERT_NOT_REACHED();
547 return false;
548}
549
550bool isEnterKeyKeydownEvent(Event& event)
551{
552 return event.type() == eventNames().keydownEvent && is<KeyboardEvent>(event) && downcast<KeyboardEvent>(event).keyIdentifier() == "Enter";
553}
554
555bool shouldProhibitLinks(Element* element)
556{
557 return isInSVGImage(element);
558}
559
560bool HTMLAnchorElement::willRespondToMouseClickEvents()
561{
562 return isLink() || HTMLElement::willRespondToMouseClickEvents();
563}
564
565typedef HashMap<const HTMLAnchorElement*, RefPtr<Element>> RootEditableElementMap;
566
567static RootEditableElementMap& rootEditableElementMap()
568{
569 static NeverDestroyed<RootEditableElementMap> map;
570 return map;
571}
572
573Element* HTMLAnchorElement::rootEditableElementForSelectionOnMouseDown() const
574{
575 if (!m_hasRootEditableElementForSelectionOnMouseDown)
576 return 0;
577 return rootEditableElementMap().get(this);
578}
579
580void HTMLAnchorElement::clearRootEditableElementForSelectionOnMouseDown()
581{
582 if (!m_hasRootEditableElementForSelectionOnMouseDown)
583 return;
584 rootEditableElementMap().remove(this);
585 m_hasRootEditableElementForSelectionOnMouseDown = false;
586}
587
588void HTMLAnchorElement::setRootEditableElementForSelectionOnMouseDown(Element* element)
589{
590 if (!element) {
591 clearRootEditableElementForSelectionOnMouseDown();
592 return;
593 }
594
595 rootEditableElementMap().set(this, element);
596 m_hasRootEditableElementForSelectionOnMouseDown = true;
597}
598
599}
600