blob: e96ea530f1996b860df16b16f3d97118b80a52fa [file] [log] [blame]
/*
* Copyright (C) 1999 Lars Knoll (knoll@kde.org)
* (C) 1999 Antti Koivisto (koivisto@kde.org)
* (C) 2000 Simon Hausmann <hausmann@kde.org>
* Copyright (C) 2003, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
* reserved.
* (C) 2006 Graham Dennis (graham.dennis@gmail.com)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "core/html/HTMLAnchorElement.h"
#include "core/editing/EditingUtilities.h"
#include "core/events/KeyboardEvent.h"
#include "core/events/MouseEvent.h"
#include "core/frame/FrameHost.h"
#include "core/frame/Settings.h"
#include "core/frame/UseCounter.h"
#include "core/html/HTMLImageElement.h"
#include "core/html/parser/HTMLParserIdioms.h"
#include "core/layout/LayoutBox.h"
#include "core/loader/FrameLoadRequest.h"
#include "core/loader/FrameLoaderClient.h"
#include "core/loader/PingLoader.h"
#include "core/page/ChromeClient.h"
#include "platform/network/NetworkHints.h"
#include "platform/weborigin/SecurityPolicy.h"
#include "public/platform/WebNavigationHintType.h"
namespace blink {
using namespace HTMLNames;
class HTMLAnchorElement::NavigationHintSender
: public GarbageCollected<HTMLAnchorElement::NavigationHintSender> {
public:
static NavigationHintSender* create(HTMLAnchorElement* anchorElement) {
return new NavigationHintSender(anchorElement);
}
void handleEvent(Event*);
DECLARE_TRACE();
private:
explicit NavigationHintSender(HTMLAnchorElement*);
bool shouldSendNavigationHint() const;
void maybeSendNavigationHint(WebNavigationHintType);
Member<HTMLAnchorElement> m_anchorElement;
};
void HTMLAnchorElement::NavigationHintSender::handleEvent(Event* event) {
if (event->type() == EventTypeNames::mousedown && event->isMouseEvent() &&
toMouseEvent(event)->button() ==
static_cast<short>(WebPointerProperties::Button::Left))
maybeSendNavigationHint(WebNavigationHintType::LinkMouseDown);
else if (event->type() == EventTypeNames::gesturetapunconfirmed)
maybeSendNavigationHint(WebNavigationHintType::LinkTapUnconfirmed);
else if (event->type() == EventTypeNames::gestureshowpress)
maybeSendNavigationHint(WebNavigationHintType::LinkTapDown);
}
DEFINE_TRACE(HTMLAnchorElement::NavigationHintSender) {
visitor->trace(m_anchorElement);
}
HTMLAnchorElement::NavigationHintSender::NavigationHintSender(
HTMLAnchorElement* anchorElement)
: m_anchorElement(anchorElement) {}
bool HTMLAnchorElement::NavigationHintSender::shouldSendNavigationHint() const {
const KURL& url = m_anchorElement->href();
// Currently the navigation hint only supports HTTP and HTTPS.
if (!url.protocolIsInHTTPFamily())
return false;
Document& document = m_anchorElement->document();
// If the element was detached from the frame, handleClick() doesn't cause
// the navigation.
if (!document.frame())
return false;
// When the user clicks a link which is to the current document with a hash,
// the network request is not fetched. So we don't send the navigation hint
// to the browser process.
if (url.hasFragmentIdentifier() &&
equalIgnoringFragmentIdentifier(document.url(), url))
return false;
return true;
}
void HTMLAnchorElement::NavigationHintSender::maybeSendNavigationHint(
WebNavigationHintType type) {
if (!shouldSendNavigationHint())
return;
sendNavigationHint(m_anchorElement->href(), type);
}
HTMLAnchorElement::HTMLAnchorElement(const QualifiedName& tagName,
Document& document)
: HTMLElement(tagName, document),
m_linkRelations(0),
m_cachedVisitedLinkHash(0),
m_wasFocusedByMouse(false) {}
HTMLAnchorElement* HTMLAnchorElement::create(Document& document) {
return new HTMLAnchorElement(aTag, document);
}
HTMLAnchorElement::~HTMLAnchorElement() {}
DEFINE_TRACE(HTMLAnchorElement) {
visitor->trace(m_navigationHintSender);
HTMLElement::trace(visitor);
}
bool HTMLAnchorElement::supportsFocus() const {
if (hasEditableStyle(*this))
return HTMLElement::supportsFocus();
// If not a link we should still be able to focus the element if it has
// tabIndex.
return isLink() || HTMLElement::supportsFocus();
}
bool HTMLAnchorElement::matchesEnabledPseudoClass() const {
return isLink();
}
bool HTMLAnchorElement::shouldHaveFocusAppearance() const {
return !m_wasFocusedByMouse || HTMLElement::supportsFocus();
}
void HTMLAnchorElement::dispatchFocusEvent(
Element* oldFocusedElement,
WebFocusType type,
InputDeviceCapabilities* sourceCapabilities) {
if (type != WebFocusTypePage)
m_wasFocusedByMouse = type == WebFocusTypeMouse;
HTMLElement::dispatchFocusEvent(oldFocusedElement, type, sourceCapabilities);
}
void HTMLAnchorElement::dispatchBlurEvent(
Element* newFocusedElement,
WebFocusType type,
InputDeviceCapabilities* sourceCapabilities) {
if (type != WebFocusTypePage)
m_wasFocusedByMouse = false;
HTMLElement::dispatchBlurEvent(newFocusedElement, type, sourceCapabilities);
}
bool HTMLAnchorElement::isMouseFocusable() const {
if (isLink())
return supportsFocus();
return HTMLElement::isMouseFocusable();
}
bool HTMLAnchorElement::isKeyboardFocusable() const {
DCHECK(document().isActive());
if (isFocusable() && Element::supportsFocus())
return HTMLElement::isKeyboardFocusable();
if (isLink() && !document().frameHost()->chromeClient().tabsToLinks())
return false;
return HTMLElement::isKeyboardFocusable();
}
static void appendServerMapMousePosition(StringBuilder& url, Event* event) {
if (!event->isMouseEvent())
return;
DCHECK(event->target());
Node* target = event->target()->toNode();
DCHECK(target);
if (!isHTMLImageElement(*target))
return;
HTMLImageElement& imageElement = toHTMLImageElement(*target);
if (!imageElement.isServerMap())
return;
LayoutObject* layoutObject = imageElement.layoutObject();
if (!layoutObject || !layoutObject->isBox())
return;
// The coordinates sent in the query string are relative to the height and
// width of the image element, ignoring CSS transform/zoom.
LayoutPoint mapPoint(layoutObject->absoluteToLocal(
FloatPoint(toMouseEvent(event)->absoluteLocation()), UseTransforms));
// The origin (0,0) is at the upper left of the content area, inside the
// padding and border.
mapPoint -= toLayoutBox(layoutObject)->contentBoxOffset();
// CSS zoom is not reflected in the map coordinates.
float scaleFactor = 1 / layoutObject->style()->effectiveZoom();
mapPoint.scale(scaleFactor, scaleFactor);
// Negative coordinates are clamped to 0 such that clicks in the left and
// top padding/border areas receive an X or Y coordinate of 0.
IntPoint clampedPoint(roundedIntPoint(mapPoint));
clampedPoint.clampNegativeToZero();
url.append('?');
url.appendNumber(clampedPoint.x());
url.append(',');
url.appendNumber(clampedPoint.y());
}
void HTMLAnchorElement::defaultEventHandler(Event* event) {
if (isLink()) {
if (isFocused() && isEnterKeyKeydownEvent(event) && isLiveLink()) {
event->setDefaultHandled();
dispatchSimulatedClick(event);
return;
}
if (RuntimeEnabledFeatures::speculativeLaunchServiceWorkerEnabled())
ensureNavigationHintSender()->handleEvent(event);
if (isLinkClick(event) && isLiveLink()) {
handleClick(event);
return;
}
}
HTMLElement::defaultEventHandler(event);
}
void HTMLAnchorElement::setActive(bool down) {
if (hasEditableStyle(*this))
return;
ContainerNode::setActive(down);
}
void HTMLAnchorElement::parseAttribute(const QualifiedName& name,
const AtomicString& oldValue,
const AtomicString& value) {
if (name == hrefAttr) {
bool wasLink = isLink();
setIsLink(!value.isNull());
if (wasLink || isLink()) {
pseudoStateChanged(CSSSelector::PseudoLink);
pseudoStateChanged(CSSSelector::PseudoVisited);
pseudoStateChanged(CSSSelector::PseudoAnyLink);
}
if (wasLink && !isLink() && adjustedFocusedElementInTreeScope() == this) {
// We might want to call blur(), but it's dangerous to dispatch
// events here.
document().setNeedsFocusedElementCheck();
}
if (isLink()) {
String parsedURL = stripLeadingAndTrailingHTMLSpaces(value);
if (document().isDNSPrefetchEnabled()) {
if (protocolIs(parsedURL, "http") || protocolIs(parsedURL, "https") ||
parsedURL.startsWith("//"))
prefetchDNS(document().completeURL(parsedURL).host());
}
}
invalidateCachedVisitedLinkHash();
logUpdateAttributeIfIsolatedWorldAndInDocument("a", hrefAttr, oldValue,
value);
} else if (name == nameAttr || name == titleAttr) {
// Do nothing.
} else if (name == relAttr) {
setRel(value);
} else {
HTMLElement::parseAttribute(name, oldValue, value);
}
}
void HTMLAnchorElement::accessKeyAction(bool sendMouseEvents) {
dispatchSimulatedClick(
0, sendMouseEvents ? SendMouseUpDownEvents : SendNoEvents);
}
bool HTMLAnchorElement::isURLAttribute(const Attribute& attribute) const {
return attribute.name().localName() == hrefAttr ||
HTMLElement::isURLAttribute(attribute);
}
bool HTMLAnchorElement::hasLegalLinkAttribute(const QualifiedName& name) const {
return name == hrefAttr || HTMLElement::hasLegalLinkAttribute(name);
}
bool HTMLAnchorElement::canStartSelection() const {
if (!isLink())
return HTMLElement::canStartSelection();
return hasEditableStyle(*this);
}
bool HTMLAnchorElement::draggable() const {
// Should be draggable if we have an href attribute.
const AtomicString& value = getAttribute(draggableAttr);
if (equalIgnoringCase(value, "true"))
return true;
if (equalIgnoringCase(value, "false"))
return false;
return hasAttribute(hrefAttr);
}
KURL HTMLAnchorElement::href() const {
return document().completeURL(
stripLeadingAndTrailingHTMLSpaces(getAttribute(hrefAttr)));
}
void HTMLAnchorElement::setHref(const AtomicString& value) {
setAttribute(hrefAttr, value);
}
KURL HTMLAnchorElement::url() const {
return href();
}
void HTMLAnchorElement::setURL(const KURL& url) {
setHref(AtomicString(url.getString()));
}
String HTMLAnchorElement::input() const {
return getAttribute(hrefAttr);
}
void HTMLAnchorElement::setInput(const String& value) {
setHref(AtomicString(value));
}
bool HTMLAnchorElement::hasRel(uint32_t relation) const {
return m_linkRelations & relation;
}
void HTMLAnchorElement::setRel(const AtomicString& value) {
m_linkRelations = 0;
SpaceSplitString newLinkRelations(value, SpaceSplitString::ShouldFoldCase);
// FIXME: Add link relations as they are implemented
if (newLinkRelations.contains("noreferrer"))
m_linkRelations |= RelationNoReferrer;
if (newLinkRelations.contains("noopener"))
m_linkRelations |= RelationNoOpener;
}
const AtomicString& HTMLAnchorElement::name() const {
return getNameAttribute();
}
short HTMLAnchorElement::tabIndex() const {
// Skip the supportsFocus check in HTMLElement.
return Element::tabIndex();
}
bool HTMLAnchorElement::isLiveLink() const {
return isLink() && !hasEditableStyle(*this);
}
void HTMLAnchorElement::sendPings(const KURL& destinationURL) const {
const AtomicString& pingValue = getAttribute(pingAttr);
if (pingValue.isNull() || !document().settings() ||
!document().settings()->hyperlinkAuditingEnabled())
return;
UseCounter::count(document(), UseCounter::HTMLAnchorElementPingAttribute);
SpaceSplitString pingURLs(pingValue, SpaceSplitString::ShouldNotFoldCase);
for (unsigned i = 0; i < pingURLs.size(); i++)
PingLoader::sendLinkAuditPing(document().frame(),
document().completeURL(pingURLs[i]),
destinationURL);
}
void HTMLAnchorElement::handleClick(Event* event) {
event->setDefaultHandled();
LocalFrame* frame = document().frame();
if (!frame)
return;
StringBuilder url;
url.append(stripLeadingAndTrailingHTMLSpaces(fastGetAttribute(hrefAttr)));
appendServerMapMousePosition(url, event);
KURL completedURL = document().completeURL(url.toString());
// Schedule the ping before the frame load. Prerender in Chrome may kill the
// renderer as soon as the navigation is sent out.
sendPings(completedURL);
ResourceRequest request(completedURL);
request.setUIStartTime(event->platformTimeStamp());
request.setInputPerfMetricReportPolicy(
InputToLoadPerfMetricReportPolicy::ReportLink);
ReferrerPolicy policy;
if (hasAttribute(referrerpolicyAttr) &&
SecurityPolicy::referrerPolicyFromStringWithLegacyKeywords(
fastGetAttribute(referrerpolicyAttr), &policy) &&
!hasRel(RelationNoReferrer)) {
request.setHTTPReferrer(SecurityPolicy::generateReferrer(
policy, completedURL, document().outgoingReferrer()));
}
if (hasAttribute(downloadAttr)) {
request.setRequestContext(WebURLRequest::RequestContextDownload);
bool isSameOrigin =
completedURL.protocolIsData() ||
document().getSecurityOrigin()->canRequest(completedURL);
const AtomicString& suggestedName =
(isSameOrigin ? fastGetAttribute(downloadAttr) : nullAtom);
frame->loader().client()->loadURLExternally(
request, NavigationPolicyDownload, suggestedName, false);
} else {
request.setRequestContext(WebURLRequest::RequestContextHyperlink);
FrameLoadRequest frameRequest(&document(), request,
getAttribute(targetAttr));
frameRequest.setTriggeringEvent(event);
if (hasRel(RelationNoReferrer)) {
frameRequest.setShouldSendReferrer(NeverSendReferrer);
frameRequest.setShouldSetOpener(NeverSetOpener);
}
if (hasRel(RelationNoOpener))
frameRequest.setShouldSetOpener(NeverSetOpener);
// TODO(japhet): Link clicks can be emulated via JS without a user gesture.
// Why doesn't this go through NavigationScheduler?
frame->loader().load(frameRequest);
}
}
bool isEnterKeyKeydownEvent(Event* event) {
return event->type() == EventTypeNames::keydown && event->isKeyboardEvent() &&
toKeyboardEvent(event)->key() == "Enter" &&
!toKeyboardEvent(event)->repeat();
}
bool isLinkClick(Event* event) {
// Allow detail <= 1 so that synthetic clicks work. They may have detail == 0.
return (event->type() == EventTypeNames::click ||
event->type() == EventTypeNames::auxclick) &&
(!event->isMouseEvent() ||
(toMouseEvent(event)->button() !=
static_cast<short>(WebPointerProperties::Button::Right) &&
toMouseEvent(event)->detail() <= 1));
}
bool HTMLAnchorElement::willRespondToMouseClickEvents() {
return isLink() || HTMLElement::willRespondToMouseClickEvents();
}
bool HTMLAnchorElement::isInteractiveContent() const {
return isLink();
}
Node::InsertionNotificationRequest HTMLAnchorElement::insertedInto(
ContainerNode* insertionPoint) {
InsertionNotificationRequest request =
HTMLElement::insertedInto(insertionPoint);
logAddElementIfIsolatedWorldAndInDocument("a", hrefAttr);
return request;
}
HTMLAnchorElement::NavigationHintSender*
HTMLAnchorElement::ensureNavigationHintSender() {
if (!m_navigationHintSender)
m_navigationHintSender = NavigationHintSender::create(this);
return m_navigationHintSender;
}
} // namespace blink