| /* |
| * 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-2016 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 "config.h" |
| #include "HTMLAnchorElement.h" |
| |
| #include "Chrome.h" |
| #include "ChromeClient.h" |
| #include "DOMTokenList.h" |
| #include "ElementIterator.h" |
| #include "EventHandler.h" |
| #include "EventNames.h" |
| #include "Frame.h" |
| #include "FrameLoader.h" |
| #include "FrameLoaderClient.h" |
| #include "FrameLoaderTypes.h" |
| #include "FrameSelection.h" |
| #include "HTMLCanvasElement.h" |
| #include "HTMLImageElement.h" |
| #include "HTMLParserIdioms.h" |
| #include "HTMLPictureElement.h" |
| #include "KeyboardEvent.h" |
| #include "LoaderStrategy.h" |
| #include "MouseEvent.h" |
| #include "PingLoader.h" |
| #include "PlatformMouseEvent.h" |
| #include "PlatformStrategies.h" |
| #include "PrivateClickMeasurement.h" |
| #include "RegistrableDomain.h" |
| #include "RenderImage.h" |
| #include "ResourceRequest.h" |
| #include "RuntimeApplicationChecks.h" |
| #include "RuntimeEnabledFeatures.h" |
| #include "SVGImage.h" |
| #include "ScriptController.h" |
| #include "SecurityOrigin.h" |
| #include "SecurityPolicy.h" |
| #include "Settings.h" |
| #include "UserGestureIndicator.h" |
| #include <wtf/IsoMallocInlines.h> |
| #include <wtf/WeakHashMap.h> |
| #include <wtf/text/StringBuilder.h> |
| #include <wtf/text/StringConcatenateNumbers.h> |
| |
| #if PLATFORM(COCOA) |
| #include "DataDetection.h" |
| #endif |
| |
| namespace WebCore { |
| |
| WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLAnchorElement); |
| |
| using namespace HTMLNames; |
| |
| HTMLAnchorElement::HTMLAnchorElement(const QualifiedName& tagName, Document& document) |
| : HTMLElement(tagName, document) |
| { |
| } |
| |
| Ref<HTMLAnchorElement> HTMLAnchorElement::create(Document& document) |
| { |
| return adoptRef(*new HTMLAnchorElement(aTag, document)); |
| } |
| |
| Ref<HTMLAnchorElement> HTMLAnchorElement::create(const QualifiedName& tagName, Document& document) |
| { |
| return adoptRef(*new HTMLAnchorElement(tagName, document)); |
| } |
| |
| HTMLAnchorElement::~HTMLAnchorElement() |
| { |
| clearRootEditableElementForSelectionOnMouseDown(); |
| } |
| |
| bool HTMLAnchorElement::supportsFocus() const |
| { |
| if (hasEditableStyle()) |
| 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::isMouseFocusable() const |
| { |
| #if !(PLATFORM(GTK) || PLATFORM(WPE)) |
| // Only allow links with tabIndex or contentEditable to be mouse focusable. |
| if (isLink()) |
| return HTMLElement::supportsFocus(); |
| #endif |
| |
| return HTMLElement::isMouseFocusable(); |
| } |
| |
| bool HTMLAnchorElement::isInteractiveContent() const |
| { |
| return isLink(); |
| } |
| |
| static bool hasNonEmptyBox(RenderBoxModelObject* renderer) |
| { |
| if (!renderer) |
| return false; |
| |
| // Before calling absoluteRects, check for the common case where borderBoundingBox |
| // is non-empty, since this is a faster check and almost always returns true. |
| // FIXME: Why do we need to call absoluteRects at all? |
| if (!renderer->borderBoundingBox().isEmpty()) |
| return true; |
| |
| // FIXME: Since all we are checking is whether the rects are empty, could we just |
| // pass in 0,0 for the layout point instead of calling localToAbsolute? |
| Vector<IntRect> rects; |
| renderer->absoluteRects(rects, flooredLayoutPoint(renderer->localToAbsolute())); |
| for (auto& rect : rects) { |
| if (!rect.isEmpty()) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool HTMLAnchorElement::isKeyboardFocusable(KeyboardEvent* event) const |
| { |
| if (!isLink()) |
| return HTMLElement::isKeyboardFocusable(event); |
| |
| if (!isFocusable()) |
| return false; |
| |
| if (!document().frame()) |
| return false; |
| |
| if (!document().frame()->eventHandler().tabsToLinks(event)) |
| return false; |
| |
| if (!renderer() && ancestorsOfType<HTMLCanvasElement>(*this).first()) |
| return true; |
| |
| return hasNonEmptyBox(renderBoxModelObject()); |
| } |
| |
| static void appendServerMapMousePosition(StringBuilder& url, Event& event) |
| { |
| if (!is<MouseEvent>(event)) |
| return; |
| auto& mouseEvent = downcast<MouseEvent>(event); |
| |
| if (!is<HTMLImageElement>(mouseEvent.target())) |
| return; |
| |
| auto& imageElement = downcast<HTMLImageElement>(*mouseEvent.target()); |
| if (!imageElement.isServerMap()) |
| return; |
| |
| auto* renderer = imageElement.renderer(); |
| if (!is<RenderImage>(renderer)) |
| return; |
| |
| // FIXME: This should probably pass UseTransforms in the OptionSet<MapCoordinatesMode>. |
| auto absolutePosition = downcast<RenderImage>(*renderer).absoluteToLocal(FloatPoint(mouseEvent.pageX(), mouseEvent.pageY())); |
| url.append('?', std::lround(absolutePosition.x()), ',', std::lround(absolutePosition.y())); |
| } |
| |
| void HTMLAnchorElement::defaultEventHandler(Event& event) |
| { |
| if (isLink()) { |
| if (focused() && isEnterKeyKeydownEvent(event) && treatLinkAsLiveForEventType(NonMouseEvent)) { |
| event.setDefaultHandled(); |
| dispatchSimulatedClick(&event); |
| return; |
| } |
| |
| if (MouseEvent::canTriggerActivationBehavior(event) && treatLinkAsLiveForEventType(eventType(event))) { |
| handleClick(event); |
| return; |
| } |
| |
| if (hasEditableStyle()) { |
| // This keeps track of the editable block that the selection was in (if it was in one) just before the link was clicked |
| // for the LiveWhenNotFocused editable link behavior |
| auto& eventNames = WebCore::eventNames(); |
| if (event.type() == eventNames.mousedownEvent && is<MouseEvent>(event) && downcast<MouseEvent>(event).button() != RightButton && document().frame()) { |
| setRootEditableElementForSelectionOnMouseDown(document().frame()->selection().selection().rootEditableElement()); |
| m_wasShiftKeyDownOnMouseDown = downcast<MouseEvent>(event).shiftKey(); |
| } else if (event.type() == eventNames.mouseoverEvent) { |
| // These are cleared on mouseover and not mouseout because their values are needed for drag events, |
| // but drag events happen after mouse out events. |
| clearRootEditableElementForSelectionOnMouseDown(); |
| m_wasShiftKeyDownOnMouseDown = false; |
| } |
| } |
| } |
| |
| HTMLElement::defaultEventHandler(event); |
| } |
| |
| void HTMLAnchorElement::setActive(bool down, bool pause, Style::InvalidationScope invalidationScope) |
| { |
| if (down && hasEditableStyle()) { |
| switch (document().settings().editableLinkBehavior()) { |
| case EditableLinkBehavior::Default: |
| case EditableLinkBehavior::AlwaysLive: |
| break; |
| |
| // Don't set the link to be active if the current selection is in the same editable block as this link. |
| case EditableLinkBehavior::LiveWhenNotFocused: |
| if (down && document().frame() && document().frame()->selection().selection().rootEditableElement() == rootEditableElement()) |
| return; |
| break; |
| |
| case EditableLinkBehavior::NeverLive: |
| case EditableLinkBehavior::OnlyLiveWithShiftKey: |
| return; |
| |
| } |
| } |
| |
| HTMLElement::setActive(down, pause, invalidationScope); |
| } |
| |
| void HTMLAnchorElement::parseAttribute(const QualifiedName& name, const AtomString& value) |
| { |
| if (name == hrefAttr) { |
| bool wasLink = isLink(); |
| setIsLink(!value.isNull() && !shouldProhibitLinks(this)); |
| if (wasLink != isLink()) |
| invalidateStyleForSubtree(); |
| if (isLink()) { |
| String parsedURL = stripLeadingAndTrailingHTMLSpaces(value); |
| if (document().isDNSPrefetchEnabled() && document().frame()) { |
| if (protocolIsInHTTPFamily(parsedURL) || parsedURL.startsWith("//"_s)) |
| document().frame()->loader().client().prefetchDNS(document().completeURL(parsedURL).host().toString()); |
| } |
| } |
| } else if (name == nameAttr || name == titleAttr) { |
| // Do nothing. |
| } else if (name == relAttr) { |
| // Update HTMLAnchorElement::relList() if more rel attributes values are supported. |
| static MainThreadNeverDestroyed<const AtomString> noReferrer("noreferrer"_s); |
| static MainThreadNeverDestroyed<const AtomString> noOpener("noopener"_s); |
| static MainThreadNeverDestroyed<const AtomString> opener("opener"_s); |
| SpaceSplitString relValue(value, SpaceSplitString::ShouldFoldCase::Yes); |
| if (relValue.contains(noReferrer)) |
| m_linkRelations.add(Relation::NoReferrer); |
| if (relValue.contains(noOpener)) |
| m_linkRelations.add(Relation::NoOpener); |
| if (relValue.contains(opener)) |
| m_linkRelations.add(Relation::Opener); |
| if (m_relList) |
| m_relList->associatedAttributeValueChanged(value); |
| } |
| else |
| HTMLElement::parseAttribute(name, value); |
| } |
| |
| bool HTMLAnchorElement::isURLAttribute(const Attribute& attribute) const |
| { |
| return attribute.name().localName() == hrefAttr || HTMLElement::isURLAttribute(attribute); |
| } |
| |
| bool HTMLAnchorElement::canStartSelection() const |
| { |
| if (!isLink()) |
| return HTMLElement::canStartSelection(); |
| return hasEditableStyle(); |
| } |
| |
| bool HTMLAnchorElement::draggable() const |
| { |
| const AtomString& value = attributeWithoutSynchronization(draggableAttr); |
| if (equalLettersIgnoringASCIICase(value, "true"_s)) |
| return true; |
| if (equalLettersIgnoringASCIICase(value, "false"_s)) |
| return false; |
| return hasAttributeWithoutSynchronization(hrefAttr); |
| } |
| |
| URL HTMLAnchorElement::href() const |
| { |
| return document().completeURL(attributeWithoutSynchronization(hrefAttr)); |
| } |
| |
| void HTMLAnchorElement::setHref(const AtomString& value) |
| { |
| setAttributeWithoutSynchronization(hrefAttr, value); |
| } |
| |
| bool HTMLAnchorElement::hasRel(Relation relation) const |
| { |
| return m_linkRelations.contains(relation); |
| } |
| |
| DOMTokenList& HTMLAnchorElement::relList() |
| { |
| if (!m_relList) { |
| m_relList = makeUnique<DOMTokenList>(*this, HTMLNames::relAttr, [](Document& document, StringView token) { |
| #if USE(SYSTEM_PREVIEW) |
| if (equalLettersIgnoringASCIICase(token, "ar"_s)) |
| return document.settings().systemPreviewEnabled(); |
| #else |
| UNUSED_PARAM(document); |
| #endif |
| return equalLettersIgnoringASCIICase(token, "noreferrer"_s) || equalLettersIgnoringASCIICase(token, "noopener"_s) || equalLettersIgnoringASCIICase(token, "opener"_s); |
| }); |
| } |
| return *m_relList; |
| } |
| |
| const AtomString& HTMLAnchorElement::name() const |
| { |
| return getNameAttribute(); |
| } |
| |
| int HTMLAnchorElement::defaultTabIndex() const |
| { |
| return 0; |
| } |
| |
| AtomString HTMLAnchorElement::target() const |
| { |
| return attributeWithoutSynchronization(targetAttr); |
| } |
| |
| String HTMLAnchorElement::origin() const |
| { |
| return SecurityOrigin::create(href()).get().toString(); |
| } |
| |
| String HTMLAnchorElement::text() |
| { |
| return textContent(); |
| } |
| |
| void HTMLAnchorElement::setText(String&& text) |
| { |
| setTextContent(WTFMove(text)); |
| } |
| |
| bool HTMLAnchorElement::isLiveLink() const |
| { |
| return isLink() && treatLinkAsLiveForEventType(m_wasShiftKeyDownOnMouseDown ? MouseEventWithShiftKey : MouseEventWithoutShiftKey); |
| } |
| |
| void HTMLAnchorElement::sendPings(const URL& destinationURL) |
| { |
| if (!document().frame()) |
| return; |
| |
| if (!hasAttributeWithoutSynchronization(pingAttr) || !document().settings().hyperlinkAuditingEnabled()) |
| return; |
| |
| SpaceSplitString pingURLs(attributeWithoutSynchronization(pingAttr), SpaceSplitString::ShouldFoldCase::No); |
| for (unsigned i = 0; i < pingURLs.size(); i++) |
| PingLoader::sendPing(*document().frame(), document().completeURL(pingURLs[i]), destinationURL); |
| } |
| |
| #if USE(SYSTEM_PREVIEW) |
| bool HTMLAnchorElement::isSystemPreviewLink() |
| { |
| if (!document().settings().systemPreviewEnabled()) |
| return false; |
| |
| static MainThreadNeverDestroyed<const AtomString> systemPreviewRelValue("ar"_s); |
| |
| if (!relList().contains(systemPreviewRelValue)) |
| return false; |
| |
| if (auto* child = firstElementChild()) { |
| if (is<HTMLImageElement>(child) || is<HTMLPictureElement>(child)) { |
| auto numChildren = childElementCount(); |
| // FIXME: We've documented that it should be the only child, but some early demos have two children. |
| return numChildren == 1 || numChildren == 2; |
| } |
| } |
| |
| return false; |
| } |
| #endif |
| |
| std::optional<URL> HTMLAnchorElement::attributionDestinationURLForPCM() const |
| { |
| URL destinationURL { attributeWithoutSynchronization(attributiondestinationAttr) }; |
| if (destinationURL.isValid() && destinationURL.protocolIsInHTTPFamily()) |
| return destinationURL; |
| |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "attributiondestination could not be converted to a valid HTTP-family URL."_s); |
| return std::nullopt; |
| } |
| |
| std::optional<RegistrableDomain> HTMLAnchorElement::mainDocumentRegistrableDomainForPCM() const |
| { |
| if (auto frame = document().frame()) { |
| if (auto mainDocument = frame->mainFrame().document()) { |
| if (auto mainDocumentRegistrableDomain = RegistrableDomain { mainDocument->url() }; !mainDocumentRegistrableDomain.isEmpty()) |
| return mainDocumentRegistrableDomain; |
| } |
| } |
| |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "Could not find a main document to use as source site for Private Click Measurement."_s); |
| return std::nullopt; |
| } |
| |
| std::optional<PrivateClickMeasurement::EphemeralNonce> HTMLAnchorElement::attributionSourceNonceForPCM() const |
| { |
| auto attributionSourceNonceAttr = attributeWithoutSynchronization(attributionsourcenonceAttr); |
| if (attributionSourceNonceAttr.isEmpty()) |
| return std::nullopt; |
| |
| auto ephemeralNonce = PrivateClickMeasurement::EphemeralNonce { attributionSourceNonceAttr }; |
| if (!ephemeralNonce.isValid()) { |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "attributionsourcenonce was not valid."_s); |
| return std::nullopt; |
| } |
| |
| return ephemeralNonce; |
| } |
| |
| std::optional<PrivateClickMeasurement> HTMLAnchorElement::parsePrivateClickMeasurementForSKAdNetwork(const URL& hrefURL) const |
| { |
| if (!document().settings().sKAttributionEnabled()) |
| return std::nullopt; |
| |
| using SourceID = PrivateClickMeasurement::SourceID; |
| using SourceSite = PrivateClickMeasurement::SourceSite; |
| using AttributionDestinationSite = PrivateClickMeasurement::AttributionDestinationSite; |
| |
| auto adamID = PrivateClickMeasurement::appStoreURLAdamID(hrefURL); |
| if (!adamID) |
| return std::nullopt; |
| |
| auto attributionDestinationDomain = attributionDestinationURLForPCM(); |
| if (!attributionDestinationDomain) |
| return std::nullopt; |
| |
| auto mainDocumentRegistrableDomain = mainDocumentRegistrableDomainForPCM(); |
| if (!mainDocumentRegistrableDomain) |
| return std::nullopt; |
| |
| auto attributionSourceNonce = attributionSourceNonceForPCM(); |
| if (!attributionSourceNonce) |
| return std::nullopt; |
| |
| #if PLATFORM(COCOA) |
| auto bundleID = applicationBundleIdentifier(); |
| #else |
| String bundleID; |
| #endif |
| |
| auto privateClickMeasurement = PrivateClickMeasurement { |
| SourceID(0), |
| SourceSite(WTFMove(*mainDocumentRegistrableDomain)), |
| AttributionDestinationSite(*attributionDestinationDomain), |
| bundleID, |
| WallTime::now(), |
| PrivateClickMeasurement::AttributionEphemeral::No |
| }; |
| privateClickMeasurement.setEphemeralSourceNonce(WTFMove(*attributionSourceNonce)); |
| privateClickMeasurement.setAdamID(*adamID); |
| return privateClickMeasurement; |
| } |
| |
| std::optional<PrivateClickMeasurement> HTMLAnchorElement::parsePrivateClickMeasurement(const URL& hrefURL) const |
| { |
| using SourceID = PrivateClickMeasurement::SourceID; |
| using SourceSite = PrivateClickMeasurement::SourceSite; |
| using AttributionDestinationSite = PrivateClickMeasurement::AttributionDestinationSite; |
| |
| RefPtr<Frame> frame = document().frame(); |
| auto* page = document().page(); |
| if (!frame || !page || page->sessionID().isEphemeral() |
| || !document().settings().privateClickMeasurementEnabled() |
| || !UserGestureIndicator::processingUserGesture()) |
| return std::nullopt; |
| |
| if (auto pcm = parsePrivateClickMeasurementForSKAdNetwork(hrefURL)) |
| return pcm; |
| |
| auto hasAttributionSourceIDAttr = hasAttributeWithoutSynchronization(attributionsourceidAttr); |
| auto hasAttributionDestinationAttr = hasAttributeWithoutSynchronization(attributiondestinationAttr); |
| if (!hasAttributionSourceIDAttr && !hasAttributionDestinationAttr) |
| return std::nullopt; |
| |
| auto attributionSourceIDAttr = attributeWithoutSynchronization(attributionsourceidAttr); |
| auto attributionDestinationAttr = attributeWithoutSynchronization(attributiondestinationAttr); |
| |
| if (!hasAttributionSourceIDAttr || !hasAttributionDestinationAttr || attributionSourceIDAttr.isEmpty() || attributionDestinationAttr.isEmpty()) { |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "Both attributionsourceid and attributiondestination need to be set for Private Click Measurement to work."_s); |
| return std::nullopt; |
| } |
| |
| auto attributionSourceID = parseHTMLNonNegativeInteger(attributionSourceIDAttr); |
| if (!attributionSourceID) { |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "attributionsourceid is not a non-negative integer which is required for Private Click Measurement."_s); |
| return std::nullopt; |
| } |
| |
| if (attributionSourceID.value() > PrivateClickMeasurement::SourceID::MaxEntropy) { |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, makeString("attributionsourceid must have a non-negative value less than or equal to ", PrivateClickMeasurement::SourceID::MaxEntropy, " for Private Click Measurement.")); |
| return std::nullopt; |
| } |
| |
| URL destinationURL { attributionDestinationAttr }; |
| if (!destinationURL.isValid() || !destinationURL.protocolIsInHTTPFamily()) { |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "attributiondestination could not be converted to a valid HTTP-family URL."_s); |
| return std::nullopt; |
| } |
| |
| RegistrableDomain mainDocumentRegistrableDomain; |
| if (auto mainDocument = frame->mainFrame().document()) |
| mainDocumentRegistrableDomain = RegistrableDomain { mainDocument->url() }; |
| else { |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "Could not find a main document to use as source site for Private Click Measurement."_s); |
| return std::nullopt; |
| } |
| |
| if (mainDocumentRegistrableDomain.matches(destinationURL)) { |
| document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, "attributiondestination can not be the same site as the current website."_s); |
| return std::nullopt; |
| } |
| |
| #if PLATFORM(COCOA) |
| auto bundleID = applicationBundleIdentifier(); |
| #else |
| String bundleID; |
| #endif |
| auto privateClickMeasurement = PrivateClickMeasurement { SourceID(attributionSourceID.value()), SourceSite(WTFMove(mainDocumentRegistrableDomain)), AttributionDestinationSite(destinationURL), bundleID, WallTime::now(), PrivateClickMeasurement::AttributionEphemeral::No }; |
| |
| if (auto ephemeralNonce = attributionSourceNonceForPCM()) |
| privateClickMeasurement.setEphemeralSourceNonce(WTFMove(*ephemeralNonce)); |
| |
| return privateClickMeasurement; |
| } |
| |
| void HTMLAnchorElement::handleClick(Event& event) |
| { |
| event.setDefaultHandled(); |
| |
| RefPtr<Frame> frame = document().frame(); |
| if (!frame) |
| return; |
| |
| if (!hasTagName(aTag) && !isConnected()) |
| return; |
| |
| StringBuilder url; |
| url.append(stripLeadingAndTrailingHTMLSpaces(attributeWithoutSynchronization(hrefAttr))); |
| appendServerMapMousePosition(url, event); |
| URL completedURL = document().completeURL(url.toString()); |
| |
| #if ENABLE(DATA_DETECTION) && PLATFORM(IOS_FAMILY) |
| if (DataDetection::isDataDetectorLink(*this) && DataDetection::canPresentDataDetectorsUIForElement(*this)) { |
| if (auto* page = document().page()) { |
| if (page->chrome().client().showDataDetectorsUIForElement(*this, event)) |
| return; |
| } |
| } |
| #endif |
| |
| AtomString downloadAttribute; |
| #if ENABLE(DOWNLOAD_ATTRIBUTE) |
| if (document().settings().downloadAttributeEnabled()) { |
| // Ignore the download attribute completely if the href URL is cross origin. |
| bool isSameOrigin = completedURL.protocolIsData() || document().securityOrigin().canRequest(completedURL); |
| if (isSameOrigin) |
| downloadAttribute = AtomString { ResourceResponse::sanitizeSuggestedFilename(attributeWithoutSynchronization(downloadAttr)) }; |
| else if (hasAttributeWithoutSynchronization(downloadAttr)) |
| document().addConsoleMessage(MessageSource::Security, MessageLevel::Warning, "The download attribute on anchor was ignored because its href URL has a different security origin."_s); |
| } |
| #endif |
| |
| SystemPreviewInfo systemPreviewInfo; |
| #if USE(SYSTEM_PREVIEW) |
| systemPreviewInfo.isPreview = isSystemPreviewLink() && document().settings().systemPreviewEnabled(); |
| |
| if (systemPreviewInfo.isPreview) { |
| systemPreviewInfo.element.elementIdentifier = identifier(); |
| systemPreviewInfo.element.documentIdentifier = document().identifier(); |
| systemPreviewInfo.element.webPageIdentifier = valueOrDefault(document().frame()->loader().pageID()); |
| if (auto* child = firstElementChild()) |
| systemPreviewInfo.previewRect = child->boundsInRootViewSpace(); |
| } |
| #endif |
| |
| auto referrerPolicy = hasRel(Relation::NoReferrer) ? ReferrerPolicy::NoReferrer : this->referrerPolicy(); |
| |
| auto effectiveTarget = this->effectiveTarget(); |
| NewFrameOpenerPolicy newFrameOpenerPolicy = NewFrameOpenerPolicy::Allow; |
| if (hasRel(Relation::NoOpener) || hasRel(Relation::NoReferrer) || (!hasRel(Relation::Opener) && document().settings().blankAnchorTargetImpliesNoOpenerEnabled() && isBlankTargetFrameName(effectiveTarget) && !completedURL.protocolIsJavaScript())) |
| newFrameOpenerPolicy = NewFrameOpenerPolicy::Suppress; |
| |
| auto privateClickMeasurement = parsePrivateClickMeasurement(completedURL); |
| // A matching triggering event needs to happen before an attribution report can be sent. |
| // Thus, URLs should be empty for now. |
| ASSERT(!privateClickMeasurement || (privateClickMeasurement->attributionReportClickSourceURL().isNull() && privateClickMeasurement->attributionReportClickDestinationURL().isNull())); |
| |
| frame->loader().changeLocation(completedURL, effectiveTarget, &event, referrerPolicy, document().shouldOpenExternalURLsPolicyToPropagate(), newFrameOpenerPolicy, downloadAttribute, systemPreviewInfo, WTFMove(privateClickMeasurement)); |
| |
| sendPings(completedURL); |
| |
| // Preconnect to the link's target for improved page load time. |
| if (completedURL.protocolIsInHTTPFamily() && ((frame->isMainFrame() && isSelfTargetFrameName(effectiveTarget)) || isBlankTargetFrameName(effectiveTarget))) { |
| auto storageCredentialsPolicy = frame->page() && frame->page()->canUseCredentialStorage() ? StoredCredentialsPolicy::Use : StoredCredentialsPolicy::DoNotUse; |
| platformStrategies()->loaderStrategy()->preconnectTo(frame->loader(), completedURL, storageCredentialsPolicy, LoaderStrategy::ShouldPreconnectAsFirstParty::Yes, nullptr); |
| } |
| } |
| |
| // Falls back to using <base> element's target if the anchor does not have one. |
| AtomString HTMLAnchorElement::effectiveTarget() const |
| { |
| auto effectiveTarget = target(); |
| if (effectiveTarget.isEmpty()) |
| effectiveTarget = document().baseTarget(); |
| return effectiveTarget; |
| } |
| |
| HTMLAnchorElement::EventType HTMLAnchorElement::eventType(Event& event) |
| { |
| if (!is<MouseEvent>(event)) |
| return NonMouseEvent; |
| return downcast<MouseEvent>(event).shiftKey() ? MouseEventWithShiftKey : MouseEventWithoutShiftKey; |
| } |
| |
| bool HTMLAnchorElement::treatLinkAsLiveForEventType(EventType eventType) const |
| { |
| if (!hasEditableStyle()) |
| return true; |
| |
| switch (document().settings().editableLinkBehavior()) { |
| case EditableLinkBehavior::Default: |
| case EditableLinkBehavior::AlwaysLive: |
| return true; |
| |
| case EditableLinkBehavior::NeverLive: |
| return false; |
| |
| // If the selection prior to clicking on this link resided in the same editable block as this link, |
| // and the shift key isn't pressed, we don't want to follow the link. |
| case EditableLinkBehavior::LiveWhenNotFocused: |
| return eventType == MouseEventWithShiftKey || (eventType == MouseEventWithoutShiftKey && rootEditableElementForSelectionOnMouseDown() != rootEditableElement()); |
| |
| case EditableLinkBehavior::OnlyLiveWithShiftKey: |
| return eventType == MouseEventWithShiftKey; |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| |
| bool isEnterKeyKeydownEvent(Event& event) |
| { |
| return event.type() == eventNames().keydownEvent && is<KeyboardEvent>(event) && downcast<KeyboardEvent>(event).keyIdentifier() == "Enter"_s; |
| } |
| |
| bool shouldProhibitLinks(Element* element) |
| { |
| return isInSVGImage(element); |
| } |
| |
| bool HTMLAnchorElement::willRespondToMouseClickEventsWithEditability(Editability editability) const |
| { |
| return isLink() || HTMLElement::willRespondToMouseClickEventsWithEditability(editability); |
| } |
| |
| static auto& rootEditableElementMap() |
| { |
| static NeverDestroyed<WeakHashMap<HTMLAnchorElement, WeakPtr<Element>>> map; |
| return map.get(); |
| } |
| |
| Element* HTMLAnchorElement::rootEditableElementForSelectionOnMouseDown() const |
| { |
| if (!m_hasRootEditableElementForSelectionOnMouseDown) |
| return 0; |
| return rootEditableElementMap().get(*this).get(); |
| } |
| |
| void HTMLAnchorElement::clearRootEditableElementForSelectionOnMouseDown() |
| { |
| if (!m_hasRootEditableElementForSelectionOnMouseDown) |
| return; |
| rootEditableElementMap().remove(*this); |
| m_hasRootEditableElementForSelectionOnMouseDown = false; |
| } |
| |
| void HTMLAnchorElement::setRootEditableElementForSelectionOnMouseDown(Element* element) |
| { |
| if (!element) { |
| clearRootEditableElementForSelectionOnMouseDown(); |
| return; |
| } |
| |
| rootEditableElementMap().set(*this, element); |
| m_hasRootEditableElementForSelectionOnMouseDown = true; |
| } |
| |
| void HTMLAnchorElement::setReferrerPolicyForBindings(const AtomString& value) |
| { |
| setAttributeWithoutSynchronization(referrerpolicyAttr, value); |
| } |
| |
| String HTMLAnchorElement::referrerPolicyForBindings() const |
| { |
| return referrerPolicyToString(referrerPolicy()); |
| } |
| |
| ReferrerPolicy HTMLAnchorElement::referrerPolicy() const |
| { |
| if (document().settings().referrerPolicyAttributeEnabled()) |
| return parseReferrerPolicy(attributeWithoutSynchronization(referrerpolicyAttr), ReferrerPolicySource::ReferrerPolicyAttribute).value_or(ReferrerPolicy::EmptyString); |
| return ReferrerPolicy::EmptyString; |
| } |
| |
| } |