Detect system preview links
[WebKit-https.git] / Source / WebCore / html / HTMLAnchorElement.cpp
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 "DOMTokenList.h"
28 #include "ElementIterator.h"
29 #include "EventHandler.h"
30 #include "EventNames.h"
31 #include "Frame.h"
32 #include "FrameLoader.h"
33 #include "FrameLoaderClient.h"
34 #include "FrameLoaderTypes.h"
35 #include "FrameSelection.h"
36 #include "HTMLCanvasElement.h"
37 #include "HTMLImageElement.h"
38 #include "HTMLParserIdioms.h"
39 #include "HTMLPictureElement.h"
40 #include "KeyboardEvent.h"
41 #include "MouseEvent.h"
42 #include "PingLoader.h"
43 #include "PlatformMouseEvent.h"
44 #include "RenderImage.h"
45 #include "ResourceRequest.h"
46 #include "RuntimeEnabledFeatures.h"
47 #include "SVGImage.h"
48 #include "ScriptController.h"
49 #include "SecurityOrigin.h"
50 #include "SecurityPolicy.h"
51 #include "Settings.h"
52 #include "URLUtils.h"
53 #include <wtf/IsoMallocInlines.h>
54 #include <wtf/text/StringBuilder.h>
55
56 #if USE(APPLE_INTERNAL_SDK)
57 #import <WebKitAdditions/SystemPreviewDetection.cpp>
58 #endif
59
60 namespace WebCore {
61
62 WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLAnchorElement);
63
64 using namespace HTMLNames;
65
66 HTMLAnchorElement::HTMLAnchorElement(const QualifiedName& tagName, Document& document)
67     : HTMLElement(tagName, document)
68     , m_hasRootEditableElementForSelectionOnMouseDown(false)
69     , m_wasShiftKeyDownOnMouseDown(false)
70     , m_cachedVisitedLinkHash(0)
71 {
72 }
73
74 Ref<HTMLAnchorElement> HTMLAnchorElement::create(Document& document)
75 {
76     return adoptRef(*new HTMLAnchorElement(aTag, document));
77 }
78
79 Ref<HTMLAnchorElement> HTMLAnchorElement::create(const QualifiedName& tagName, Document& document)
80 {
81     return adoptRef(*new HTMLAnchorElement(tagName, document));
82 }
83
84 HTMLAnchorElement::~HTMLAnchorElement()
85 {
86     clearRootEditableElementForSelectionOnMouseDown();
87 }
88
89 bool 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
97 bool 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
106 static 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
129 bool 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
149 static 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
174 void 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
206 void 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
236 void 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());
248             }
249         }
250         invalidateCachedVisitedLinkHash();
251     } else if (name == nameAttr || name == titleAttr) {
252         // Do nothing.
253     } else if (name == relAttr) {
254         // Update HTMLAnchorElement::relList() if more rel attributes values are supported.
255         static NeverDestroyed<AtomicString> noReferrer("noreferrer", AtomicString::ConstructFromLiteral);
256         static NeverDestroyed<AtomicString> noOpener("noopener", AtomicString::ConstructFromLiteral);
257         const bool shouldFoldCase = true;
258         SpaceSplitString relValue(value, shouldFoldCase);
259         if (relValue.contains(noReferrer))
260             m_linkRelations |= Relation::NoReferrer;
261         if (relValue.contains(noOpener))
262             m_linkRelations |= Relation::NoOpener;
263         if (m_relList)
264             m_relList->associatedAttributeValueChanged(value);
265     }
266     else
267         HTMLElement::parseAttribute(name, value);
268 }
269
270 void HTMLAnchorElement::accessKeyAction(bool sendMouseEvents)
271 {
272     dispatchSimulatedClick(0, sendMouseEvents ? SendMouseUpDownEvents : SendNoEvents);
273 }
274
275 bool HTMLAnchorElement::isURLAttribute(const Attribute& attribute) const
276 {
277     return attribute.name().localName() == hrefAttr || HTMLElement::isURLAttribute(attribute);
278 }
279
280 bool HTMLAnchorElement::canStartSelection() const
281 {
282     if (!isLink())
283         return HTMLElement::canStartSelection();
284     return hasEditableStyle();
285 }
286
287 bool HTMLAnchorElement::draggable() const
288 {
289     const AtomicString& value = attributeWithoutSynchronization(draggableAttr);
290     if (equalLettersIgnoringASCIICase(value, "true"))
291         return true;
292     if (equalLettersIgnoringASCIICase(value, "false"))
293         return false;
294     return hasAttributeWithoutSynchronization(hrefAttr);
295 }
296
297 URL HTMLAnchorElement::href() const
298 {
299     return document().completeURL(stripLeadingAndTrailingHTMLSpaces(attributeWithoutSynchronization(hrefAttr)));
300 }
301
302 void HTMLAnchorElement::setHref(const AtomicString& value)
303 {
304     setAttributeWithoutSynchronization(hrefAttr, value);
305 }
306
307 bool HTMLAnchorElement::hasRel(Relation relation) const
308 {
309     return m_linkRelations.contains(relation);
310 }
311
312 DOMTokenList& HTMLAnchorElement::relList() const
313 {
314     if (!m_relList) {
315         m_relList = std::make_unique<DOMTokenList>(const_cast<HTMLAnchorElement&>(*this), HTMLNames::relAttr, [](Document&, StringView token) {
316             return equalIgnoringASCIICase(token, "noreferrer") || equalIgnoringASCIICase(token, "noopener");
317         });
318     }
319     return *m_relList;
320 }
321
322 const AtomicString& HTMLAnchorElement::name() const
323 {
324     return getNameAttribute();
325 }
326
327 int HTMLAnchorElement::tabIndex() const
328 {
329     // Skip the supportsFocus check in HTMLElement.
330     return Element::tabIndex();
331 }
332
333 String HTMLAnchorElement::target() const
334 {
335     return attributeWithoutSynchronization(targetAttr);
336 }
337
338 String HTMLAnchorElement::origin() const
339 {
340     return SecurityOrigin::create(href()).get().toString();
341 }
342
343 String HTMLAnchorElement::text()
344 {
345     return textContent();
346 }
347
348 void HTMLAnchorElement::setText(const String& text)
349 {
350     setTextContent(text);
351 }
352
353 bool HTMLAnchorElement::isLiveLink() const
354 {
355     return isLink() && treatLinkAsLiveForEventType(m_wasShiftKeyDownOnMouseDown ? MouseEventWithShiftKey : MouseEventWithoutShiftKey);
356 }
357
358 void HTMLAnchorElement::sendPings(const URL& destinationURL)
359 {
360     if (!document().frame())
361         return;
362
363     if (!hasAttributeWithoutSynchronization(pingAttr) || !document().settings().hyperlinkAuditingEnabled())
364         return;
365
366     SpaceSplitString pingURLs(attributeWithoutSynchronization(pingAttr), false);
367     for (unsigned i = 0; i < pingURLs.size(); i++)
368         PingLoader::sendPing(*document().frame(), document().completeURL(pingURLs[i]), destinationURL);
369 }
370
371 bool HTMLAnchorElement::isSystemPreviewLink() const
372 {
373 #if USE(APPLE_INTERNAL_SDK)
374     auto systemPreviewRelValue = getSystemPreviewRelValue();
375 #else
376     auto systemPreviewRelValue = String { ASCIILiteral("system-preview") };
377 #endif
378
379     // The relList is created on demand, which means that calling relList()
380     // is only available in a non-const method.
381     if (!relList().contains(systemPreviewRelValue))
382         return false;
383
384     if (auto* child = firstElementChild()) {
385         if (is<HTMLImageElement>(child) || is<HTMLPictureElement>(child)) {
386             auto numChildren = childElementCount();
387             // FIXME: Should only be 1.
388             return numChildren == 1 || numChildren == 2;
389         }
390     }
391
392     return false;
393 }
394
395 void HTMLAnchorElement::handleClick(Event& event)
396 {
397     event.setDefaultHandled();
398
399     RefPtr<Frame> frame = document().frame();
400     if (!frame)
401         return;
402
403     StringBuilder url;
404     url.append(stripLeadingAndTrailingHTMLSpaces(attributeWithoutSynchronization(hrefAttr)));
405     appendServerMapMousePosition(url, event);
406     URL completedURL = document().completeURL(url.toString());
407
408     String downloadAttribute;
409 #if ENABLE(DOWNLOAD_ATTRIBUTE)
410     if (RuntimeEnabledFeatures::sharedFeatures().downloadAttributeEnabled()) {
411         // Ignore the download attribute completely if the href URL is cross origin.
412         bool isSameOrigin = completedURL.protocolIsData() || document().securityOrigin().canRequest(completedURL);
413         if (isSameOrigin)
414             downloadAttribute = ResourceResponse::sanitizeSuggestedFilename(attributeWithoutSynchronization(downloadAttr));
415         else if (hasAttributeWithoutSynchronization(downloadAttr))
416             document().addConsoleMessage(MessageSource::Security, MessageLevel::Warning, "The download attribute on anchor was ignored because its href URL has a different security origin.");
417     }
418 #endif
419
420     ShouldSendReferrer shouldSendReferrer = hasRel(Relation::NoReferrer) ? NeverSendReferrer : MaybeSendReferrer;
421     auto newFrameOpenerPolicy = hasRel(Relation::NoOpener) ? std::make_optional(NewFrameOpenerPolicy::Suppress) : std::nullopt;
422     frame->loader().urlSelected(completedURL, target(), &event, LockHistory::No, LockBackForwardList::No, shouldSendReferrer, document().shouldOpenExternalURLsPolicyToPropagate(), newFrameOpenerPolicy, downloadAttribute);
423
424     sendPings(completedURL);
425 }
426
427 HTMLAnchorElement::EventType HTMLAnchorElement::eventType(Event& event)
428 {
429     if (!is<MouseEvent>(event))
430         return NonMouseEvent;
431     return downcast<MouseEvent>(event).shiftKey() ? MouseEventWithShiftKey : MouseEventWithoutShiftKey;
432 }
433
434 bool HTMLAnchorElement::treatLinkAsLiveForEventType(EventType eventType) const
435 {
436     if (!hasEditableStyle())
437         return true;
438
439     switch (document().settings().editableLinkBehavior()) {
440     case EditableLinkDefaultBehavior:
441     case EditableLinkAlwaysLive:
442         return true;
443
444     case EditableLinkNeverLive:
445         return false;
446
447     // If the selection prior to clicking on this link resided in the same editable block as this link,
448     // and the shift key isn't pressed, we don't want to follow the link.
449     case EditableLinkLiveWhenNotFocused:
450         return eventType == MouseEventWithShiftKey || (eventType == MouseEventWithoutShiftKey && rootEditableElementForSelectionOnMouseDown() != rootEditableElement());
451
452     case EditableLinkOnlyLiveWithShiftKey:
453         return eventType == MouseEventWithShiftKey;
454     }
455
456     ASSERT_NOT_REACHED();
457     return false;
458 }
459
460 bool isEnterKeyKeydownEvent(Event& event)
461 {
462     return event.type() == eventNames().keydownEvent && is<KeyboardEvent>(event) && downcast<KeyboardEvent>(event).keyIdentifier() == "Enter";
463 }
464
465 bool shouldProhibitLinks(Element* element)
466 {
467     return isInSVGImage(element);
468 }
469
470 bool HTMLAnchorElement::willRespondToMouseClickEvents()
471 {
472     return isLink() || HTMLElement::willRespondToMouseClickEvents();
473 }
474
475 typedef HashMap<const HTMLAnchorElement*, RefPtr<Element>> RootEditableElementMap;
476
477 static RootEditableElementMap& rootEditableElementMap()
478 {
479     static NeverDestroyed<RootEditableElementMap> map;
480     return map;
481 }
482
483 Element* HTMLAnchorElement::rootEditableElementForSelectionOnMouseDown() const
484 {
485     if (!m_hasRootEditableElementForSelectionOnMouseDown)
486         return 0;
487     return rootEditableElementMap().get(this);
488 }
489
490 void HTMLAnchorElement::clearRootEditableElementForSelectionOnMouseDown()
491 {
492     if (!m_hasRootEditableElementForSelectionOnMouseDown)
493         return;
494     rootEditableElementMap().remove(this);
495     m_hasRootEditableElementForSelectionOnMouseDown = false;
496 }
497
498 void HTMLAnchorElement::setRootEditableElementForSelectionOnMouseDown(Element* element)
499 {
500     if (!element) {
501         clearRootEditableElementForSelectionOnMouseDown();
502         return;
503     }
504
505     rootEditableElementMap().set(this, element);
506     m_hasRootEditableElementForSelectionOnMouseDown = true;
507 }
508
509 }