/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/HTMLImageElement.h" #include "mozilla/dom/HTMLImageElementBinding.h" #include "nsGkAtoms.h" #include "nsStyleConsts.h" #include "nsPresContext.h" #include "nsMappedAttributes.h" #include "nsSize.h" #include "nsDocument.h" #include "nsIDocument.h" #include "nsIDOMMutationEvent.h" #include "nsIScriptContext.h" #include "nsIURL.h" #include "nsIIOService.h" #include "nsIServiceManager.h" #include "nsContentUtils.h" #include "nsContainerFrame.h" #include "nsNodeInfoManager.h" #include "mozilla/MouseEvents.h" #include "nsContentPolicyUtils.h" #include "nsIDOMWindow.h" #include "nsFocusManager.h" #include "mozilla/dom/HTMLFormElement.h" #include "nsAttrValueOrString.h" #include "imgLoader.h" #include "Image.h" // Responsive images! #include "mozilla/dom/HTMLSourceElement.h" #include "mozilla/dom/ResponsiveImageSelector.h" #include "imgIContainer.h" #include "imgILoader.h" #include "imgINotificationObserver.h" #include "imgRequestProxy.h" #include "nsILoadGroup.h" #include "nsRuleData.h" #include "nsIDOMHTMLMapElement.h" #include "mozilla/EventDispatcher.h" #include "mozilla/EventStates.h" #include "mozilla/net/ReferrerPolicy.h" #include "nsLayoutUtils.h" using namespace mozilla::net; NS_IMPL_NS_NEW_HTML_ELEMENT(Image) #ifdef DEBUG // Is aSubject a previous sibling of aNode. static bool IsPreviousSibling(nsINode *aSubject, nsINode *aNode) { if (aSubject == aNode) { return false; } nsINode *parent = aSubject->GetParentNode(); if (parent && parent == aNode->GetParentNode()) { return parent->IndexOf(aSubject) < parent->IndexOf(aNode); } return false; } #endif namespace mozilla { namespace dom { // Calls LoadSelectedImage on host element unless it has been superseded or // canceled -- this is the synchronous section of "update the image data". // https://html.spec.whatwg.org/multipage/embedded-content.html#update-the-image-data class ImageLoadTask : public Runnable { public: ImageLoadTask(HTMLImageElement *aElement, bool aAlwaysLoad) : mElement(aElement) , mAlwaysLoad(aAlwaysLoad) { mDocument = aElement->OwnerDoc(); mDocument->BlockOnload(); } NS_IMETHOD Run() override { if (mElement->mPendingImageLoadTask == this) { mElement->mPendingImageLoadTask = nullptr; mElement->LoadSelectedImage(true, true, mAlwaysLoad); } mDocument->UnblockOnload(false); return NS_OK; } bool AlwaysLoad() { return mAlwaysLoad; } private: ~ImageLoadTask() {} RefPtr<HTMLImageElement> mElement; nsCOMPtr<nsIDocument> mDocument; bool mAlwaysLoad; }; HTMLImageElement::HTMLImageElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo) : nsGenericHTMLElement(aNodeInfo) , mForm(nullptr) , mInDocResponsiveContent(false) , mCurrentDensity(1.0) { // We start out broken AddStatesSilently(NS_EVENT_STATE_BROKEN); } HTMLImageElement::~HTMLImageElement() { DestroyImageLoadingContent(); } NS_IMPL_ADDREF_INHERITED(HTMLImageElement, Element) NS_IMPL_RELEASE_INHERITED(HTMLImageElement, Element) NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLImageElement, nsGenericHTMLElement, mResponsiveSelector) // QueryInterface implementation for HTMLImageElement NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLImageElement) NS_INTERFACE_TABLE_INHERITED(HTMLImageElement, nsIDOMHTMLImageElement, nsIImageLoadingContent, imgIOnloadBlocker, imgINotificationObserver) NS_INTERFACE_TABLE_TAIL_INHERITING(nsGenericHTMLElement) NS_IMPL_ELEMENT_CLONE(HTMLImageElement) NS_IMPL_STRING_ATTR(HTMLImageElement, Name, name) NS_IMPL_STRING_ATTR(HTMLImageElement, Align, align) NS_IMPL_STRING_ATTR(HTMLImageElement, Alt, alt) NS_IMPL_STRING_ATTR(HTMLImageElement, Border, border) NS_IMPL_INT_ATTR(HTMLImageElement, Hspace, hspace) NS_IMPL_BOOL_ATTR(HTMLImageElement, IsMap, ismap) NS_IMPL_URI_ATTR(HTMLImageElement, LongDesc, longdesc) NS_IMPL_STRING_ATTR(HTMLImageElement, Sizes, sizes) NS_IMPL_URI_ATTR(HTMLImageElement, Lowsrc, lowsrc) NS_IMPL_URI_ATTR(HTMLImageElement, Src, src) NS_IMPL_STRING_ATTR(HTMLImageElement, Srcset, srcset) NS_IMPL_STRING_ATTR(HTMLImageElement, UseMap, usemap) NS_IMPL_INT_ATTR(HTMLImageElement, Vspace, vspace) bool HTMLImageElement::IsInteractiveHTMLContent(bool aIgnoreTabindex) const { return HasAttr(kNameSpaceID_None, nsGkAtoms::usemap) || nsGenericHTMLElement::IsInteractiveHTMLContent(aIgnoreTabindex); } void HTMLImageElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) { nsImageLoadingContent::AsyncEventRunning(aEvent); } nsresult HTMLImageElement::GetCurrentSrc(nsAString& aValue) { nsCOMPtr<nsIURI> currentURI; GetCurrentURI(getter_AddRefs(currentURI)); if (currentURI) { nsAutoCString spec; currentURI->GetSpec(spec); CopyUTF8toUTF16(spec, aValue); } else { SetDOMStringToNull(aValue); } return NS_OK; } bool HTMLImageElement::Draggable() const { // images may be dragged unless the draggable attribute is false return !AttrValueIs(kNameSpaceID_None, nsGkAtoms::draggable, nsGkAtoms::_false, eIgnoreCase); } bool HTMLImageElement::Complete() { if (!mCurrentRequest) { return true; } if (mPendingRequest) { return false; } uint32_t status; mCurrentRequest->GetImageStatus(&status); return (status & (imgIRequest::STATUS_LOAD_COMPLETE | imgIRequest::STATUS_ERROR)) != 0; } NS_IMETHODIMP HTMLImageElement::GetComplete(bool* aComplete) { NS_PRECONDITION(aComplete, "Null out param!"); *aComplete = Complete(); return NS_OK; } CSSIntPoint HTMLImageElement::GetXY() { nsIFrame* frame = GetPrimaryFrame(Flush_Layout); if (!frame) { return CSSIntPoint(0, 0); } nsIFrame* layer = nsLayoutUtils::GetClosestLayer(frame->GetParent()); return CSSIntPoint::FromAppUnitsRounded(frame->GetOffsetTo(layer)); } int32_t HTMLImageElement::X() { return GetXY().x; } int32_t HTMLImageElement::Y() { return GetXY().y; } NS_IMETHODIMP HTMLImageElement::GetX(int32_t* aX) { *aX = X(); return NS_OK; } NS_IMETHODIMP HTMLImageElement::GetY(int32_t* aY) { *aY = Y(); return NS_OK; } NS_IMETHODIMP HTMLImageElement::GetHeight(uint32_t* aHeight) { *aHeight = Height(); return NS_OK; } NS_IMETHODIMP HTMLImageElement::SetHeight(uint32_t aHeight) { ErrorResult rv; SetHeight(aHeight, rv); return rv.StealNSResult(); } NS_IMETHODIMP HTMLImageElement::GetWidth(uint32_t* aWidth) { *aWidth = Width(); return NS_OK; } NS_IMETHODIMP HTMLImageElement::SetWidth(uint32_t aWidth) { ErrorResult rv; SetWidth(aWidth, rv); return rv.StealNSResult(); } bool HTMLImageElement::ParseAttribute(int32_t aNamespaceID, nsIAtom* aAttribute, const nsAString& aValue, nsAttrValue& aResult) { if (aNamespaceID == kNameSpaceID_None) { if (aAttribute == nsGkAtoms::align) { return ParseAlignValue(aValue, aResult); } if (aAttribute == nsGkAtoms::crossorigin) { ParseCORSValue(aValue, aResult); return true; } if (ParseImageAttribute(aAttribute, aValue, aResult)) { return true; } } return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, aResult); } void HTMLImageElement::MapAttributesIntoRule(const nsMappedAttributes* aAttributes, nsRuleData* aData) { nsGenericHTMLElement::MapImageAlignAttributeInto(aAttributes, aData); nsGenericHTMLElement::MapImageBorderAttributeInto(aAttributes, aData); nsGenericHTMLElement::MapImageMarginAttributeInto(aAttributes, aData); nsGenericHTMLElement::MapImageSizeAttributesInto(aAttributes, aData); nsGenericHTMLElement::MapCommonAttributesInto(aAttributes, aData); } nsChangeHint HTMLImageElement::GetAttributeChangeHint(const nsIAtom* aAttribute, int32_t aModType) const { nsChangeHint retval = nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType); if (aAttribute == nsGkAtoms::usemap || aAttribute == nsGkAtoms::ismap) { retval |= nsChangeHint_ReconstructFrame; } else if (aAttribute == nsGkAtoms::alt) { if (aModType == nsIDOMMutationEvent::ADDITION || aModType == nsIDOMMutationEvent::REMOVAL) { retval |= nsChangeHint_ReconstructFrame; } } return retval; } NS_IMETHODIMP_(bool) HTMLImageElement::IsAttributeMapped(const nsIAtom* aAttribute) const { static const MappedAttributeEntry* const map[] = { sCommonAttributeMap, sImageMarginSizeAttributeMap, sImageBorderAttributeMap, sImageAlignAttributeMap }; return FindAttributeDependence(aAttribute, map); } nsMapRuleToAttributesFunc HTMLImageElement::GetAttributeMappingFunction() const { return &MapAttributesIntoRule; } nsresult HTMLImageElement::BeforeSetAttr(int32_t aNameSpaceID, nsIAtom* aName, nsAttrValueOrString* aValue, bool aNotify) { if (aNameSpaceID == kNameSpaceID_None && mForm && (aName == nsGkAtoms::name || aName == nsGkAtoms::id)) { // remove the image from the hashtable as needed nsAutoString tmp; GetAttr(kNameSpaceID_None, aName, tmp); if (!tmp.IsEmpty()) { mForm->RemoveImageElementFromTable(this, tmp, HTMLFormElement::AttributeUpdated); } } return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, aNotify); } nsresult HTMLImageElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName, const nsAttrValue* aValue, bool aNotify) { if (aNameSpaceID == kNameSpaceID_None && mForm && (aName == nsGkAtoms::name || aName == nsGkAtoms::id) && aValue && !aValue->IsEmptyString()) { // add the image to the hashtable as needed MOZ_ASSERT(aValue->Type() == nsAttrValue::eAtom, "Expected atom value for name/id"); mForm->AddImageElementToTable(this, nsDependentAtomString(aValue->GetAtomValue())); } // Handle src/srcset updates. If aNotify is false, we are coming from the // parser or some such place; we'll get bound after all the attributes have // been set, so we'll do the image load from BindToTree. nsAttrValueOrString attrVal(aValue); if (aName == nsGkAtoms::src && aNameSpaceID == kNameSpaceID_None && !aValue) { // SetAttr handles setting src since it needs to catch img.src = // img.src, so we only need to handle the unset case if (InResponsiveMode()) { if (mResponsiveSelector && mResponsiveSelector->Content() == this) { mResponsiveSelector->SetDefaultSource(NullString()); } QueueImageLoadTask(true); } else { // Bug 1076583 - We still behave synchronously in the non-responsive case CancelImageRequests(aNotify); } } else if (aName == nsGkAtoms::srcset && aNameSpaceID == kNameSpaceID_None) { PictureSourceSrcsetChanged(this, attrVal.String(), aNotify); } else if (aName == nsGkAtoms::sizes && aNameSpaceID == kNameSpaceID_None) { PictureSourceSizesChanged(this, attrVal.String(), aNotify); } return nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue, aNotify); } nsresult HTMLImageElement::PreHandleEvent(EventChainPreVisitor& aVisitor) { // We handle image element with attribute ismap in its corresponding frame // element. Set mMultipleActionsPrevented here to prevent the click event // trigger the behaviors in Element::PostHandleEventForLinks WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); if (mouseEvent && mouseEvent->IsLeftClickEvent() && IsMap()) { mouseEvent->mFlags.mMultipleActionsPrevented = true; } return nsGenericHTMLElement::PreHandleEvent(aVisitor); } bool HTMLImageElement::IsHTMLFocusable(bool aWithMouse, bool *aIsFocusable, int32_t *aTabIndex) { int32_t tabIndex = TabIndex(); if (IsInUncomposedDoc()) { nsAutoString usemap; GetUseMap(usemap); // XXXbz which document should this be using? sXBL/XBL2 issue! I // think that OwnerDoc() is right, since we don't want to // assume stuff about the document we're bound to. if (OwnerDoc()->FindImageMap(usemap)) { if (aTabIndex) { // Use tab index on individual map areas *aTabIndex = (sTabFocusModel & eTabFocus_linksMask)? 0 : -1; } // Image map is not focusable itself, but flag as tabbable // so that image map areas get walked into. *aIsFocusable = false; return false; } } if (aTabIndex) { // Can be in tab order if tabindex >=0 and form controls are tabbable. *aTabIndex = (sTabFocusModel & eTabFocus_formElementsMask)? tabIndex : -1; } *aIsFocusable = #ifdef XP_MACOSX (!aWithMouse || nsFocusManager::sMouseFocusesFormControl) && #endif (tabIndex >= 0 || HasAttr(kNameSpaceID_None, nsGkAtoms::tabindex)); return false; } nsresult HTMLImageElement::SetAttr(int32_t aNameSpaceID, nsIAtom* aName, nsIAtom* aPrefix, const nsAString& aValue, bool aNotify) { bool forceReload = false; // We need to force our image to reload. This must be done here, not in // AfterSetAttr or BeforeSetAttr, because we want to do it even if the attr is // being set to its existing value, which is normally optimized away as a // no-op. // // If we are in responsive mode, we drop the forced reload behavior, // but still trigger a image load task for img.src = img.src per // spec. // // Both cases handle unsetting src in AfterSetAttr if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::src) { if (InResponsiveMode()) { if (mResponsiveSelector && mResponsiveSelector->Content() == this) { mResponsiveSelector->SetDefaultSource(aValue); } QueueImageLoadTask(true); } else if (aNotify) { // If aNotify is false, we are coming from the parser or some such place; // we'll get bound after all the attributes have been set, so we'll do the // sync image load from BindToTree. Skip the LoadImage call in that case. // Note that this sync behavior is partially removed from the spec, bug 1076583 // A hack to get animations to reset. See bug 594771. mNewRequestsWillNeedAnimationReset = true; // Force image loading here, so that we'll try to load the image from // network if it's set to be not cacheable... If we change things so that // the state gets in Element's attr-setting happen around this // LoadImage call, we could start passing false instead of aNotify // here. LoadImage(aValue, true, aNotify, eImageLoadType_Normal); mNewRequestsWillNeedAnimationReset = false; } } else if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::crossorigin && aNotify) { nsAttrValue attrValue; ParseCORSValue(aValue, attrValue); if (GetCORSMode() != AttrValueToCORSMode(&attrValue)) { // Force a new load of the image with the new cross origin policy. forceReload = true; } } else if (aName == nsGkAtoms::referrerpolicy && aNameSpaceID == kNameSpaceID_None && aNotify) { ReferrerPolicy referrerPolicy = AttributeReferrerPolicyFromString(aValue); if (!InResponsiveMode() && referrerPolicy != RP_Unset && referrerPolicy != GetImageReferrerPolicy()) { // XXX: Bug 1076583 - We still use the older synchronous algorithm // Because referrerPolicy is not treated as relevant mutations, setting // the attribute will neither trigger a reload nor update the referrer // policy of the loading channel (whether it has previously completed or // not). Force a new load of the image with the new referrerpolicy. forceReload = true; } } nsresult rv = nsGenericHTMLElement::SetAttr(aNameSpaceID, aName, aPrefix, aValue, aNotify); // Because we load image synchronously in non-responsive-mode, we need to do // reload after the attribute has been set if the reload is triggerred by // cross origin changing. if (forceReload) { if (InResponsiveMode()) { // per spec, full selection runs when this changes, even though // it doesn't directly affect the source selection QueueImageLoadTask(true); } else { // Bug 1076583 - We still use the older synchronous algorithm in // non-responsive mode. Force a new load of the image with the // new cross origin policy ForceReload(aNotify); } } return rv; } nsresult HTMLImageElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent, nsIContent* aBindingParent, bool aCompileEventHandlers) { nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent, aBindingParent, aCompileEventHandlers); NS_ENSURE_SUCCESS(rv, rv); nsImageLoadingContent::BindToTree(aDocument, aParent, aBindingParent, aCompileEventHandlers); if (aParent) { UpdateFormOwner(); } if (HaveSrcsetOrInPicture()) { if (aDocument && !mInDocResponsiveContent) { aDocument->AddResponsiveContent(this); mInDocResponsiveContent = true; } // Run selection algorithm when an img element is inserted into a document // in order to react to changes in the environment. See note of // https://html.spec.whatwg.org/multipage/embedded-content.html#img-environment-changes QueueImageLoadTask(false); } else if (!InResponsiveMode() && HasAttr(kNameSpaceID_None, nsGkAtoms::src)) { // We skip loading when our attributes were set from parser land, // so trigger a aForce=false load now to check if things changed. // This isn't necessary for responsive mode, since creating the // image load task is asynchronous we don't need to take special // care to avoid doing so when being filled by the parser. // FIXME: Bug 660963 it would be nice if we could just have // ClearBrokenState update our state and do it fast... ClearBrokenState(); RemoveStatesSilently(NS_EVENT_STATE_BROKEN); // We still act synchronously for the non-responsive case (Bug // 1076583), but still need to delay if it is unsafe to run // script. // If loading is temporarily disabled, don't even launch MaybeLoadImage. // Otherwise MaybeLoadImage may run later when someone has reenabled // loading. if (LoadingEnabled()) { nsContentUtils::AddScriptRunner( NewRunnableMethod(this, &HTMLImageElement::MaybeLoadImage)); } } return rv; } void HTMLImageElement::UnbindFromTree(bool aDeep, bool aNullParent) { if (mForm) { if (aNullParent || !FindAncestorForm(mForm)) { ClearForm(true); } else { UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT); } } if (mInDocResponsiveContent) { nsIDocument* doc = GetOurOwnerDoc(); MOZ_ASSERT(doc); if (doc) { doc->RemoveResponsiveContent(this); mInDocResponsiveContent = false; } } mLastSelectedSource = nullptr; nsImageLoadingContent::UnbindFromTree(aDeep, aNullParent); nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent); } void HTMLImageElement::UpdateFormOwner() { if (!mForm) { mForm = FindAncestorForm(); } if (mForm && !HasFlag(ADDED_TO_FORM)) { // Now we need to add ourselves to the form nsAutoString nameVal, idVal; GetAttr(kNameSpaceID_None, nsGkAtoms::name, nameVal); GetAttr(kNameSpaceID_None, nsGkAtoms::id, idVal); SetFlags(ADDED_TO_FORM); mForm->AddImageElement(this); if (!nameVal.IsEmpty()) { mForm->AddImageElementToTable(this, nameVal); } if (!idVal.IsEmpty()) { mForm->AddImageElementToTable(this, idVal); } } } void HTMLImageElement::MaybeLoadImage() { // Our base URI may have changed, or we may have had responsive parameters // change while not bound to the tree. Re-parse src/srcset and call LoadImage, // which is a no-op if it resolves to the same effective URI without aForce. // Note, check LoadingEnabled() after LoadImage call. LoadSelectedImage(false, true, false); if (!LoadingEnabled()) { CancelImageRequests(true); } } EventStates HTMLImageElement::IntrinsicState() const { return nsGenericHTMLElement::IntrinsicState() | nsImageLoadingContent::ImageState(); } void HTMLImageElement::NodeInfoChanged() { // Resetting the last selected source if adoption steps are run. mLastSelectedSource = nullptr; } // static already_AddRefed<HTMLImageElement> HTMLImageElement::Image(const GlobalObject& aGlobal, const Optional<uint32_t>& aWidth, const Optional<uint32_t>& aHeight, ErrorResult& aError) { nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports()); nsIDocument* doc; if (!win || !(doc = win->GetExtantDoc())) { aError.Throw(NS_ERROR_FAILURE); return nullptr; } already_AddRefed<mozilla::dom::NodeInfo> nodeInfo = doc->NodeInfoManager()->GetNodeInfo(nsGkAtoms::img, nullptr, kNameSpaceID_XHTML, nsIDOMNode::ELEMENT_NODE); RefPtr<HTMLImageElement> img = new HTMLImageElement(nodeInfo); if (aWidth.WasPassed()) { img->SetWidth(aWidth.Value(), aError); if (aError.Failed()) { return nullptr; } if (aHeight.WasPassed()) { img->SetHeight(aHeight.Value(), aError); if (aError.Failed()) { return nullptr; } } } return img.forget(); } uint32_t HTMLImageElement::NaturalHeight() { uint32_t height; nsresult rv = nsImageLoadingContent::GetNaturalHeight(&height); if (NS_FAILED(rv)) { MOZ_ASSERT(false, "GetNaturalHeight should not fail"); return 0; } if (mResponsiveSelector) { double density = mResponsiveSelector->GetSelectedImageDensity(); MOZ_ASSERT(density >= 0.0); height = NSToIntRound(double(height) / density); height = std::max(height, 0u); } return height; } NS_IMETHODIMP HTMLImageElement::GetNaturalHeight(uint32_t* aNaturalHeight) { *aNaturalHeight = NaturalHeight(); return NS_OK; } uint32_t HTMLImageElement::NaturalWidth() { uint32_t width; nsresult rv = nsImageLoadingContent::GetNaturalWidth(&width); if (NS_FAILED(rv)) { MOZ_ASSERT(false, "GetNaturalWidth should not fail"); return 0; } if (mResponsiveSelector) { double density = mResponsiveSelector->GetSelectedImageDensity(); MOZ_ASSERT(density >= 0.0); width = NSToIntRound(double(width) / density); width = std::max(width, 0u); } return width; } NS_IMETHODIMP HTMLImageElement::GetNaturalWidth(uint32_t* aNaturalWidth) { *aNaturalWidth = NaturalWidth(); return NS_OK; } nsresult HTMLImageElement::CopyInnerTo(Element* aDest) { bool destIsStatic = aDest->OwnerDoc()->IsStaticDocument(); auto dest = static_cast<HTMLImageElement*>(aDest); if (destIsStatic) { CreateStaticImageClone(dest); } nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); if (NS_FAILED(rv)) { return rv; } if (!destIsStatic) { // In SetAttr (called from nsGenericHTMLElement::CopyInnerTo), dest skipped // doing the image load because we passed in false for aNotify. But we // really do want it to do the load, so set it up to happen once the cloning // reaches a stable state. if (!dest->InResponsiveMode() && dest->HasAttr(kNameSpaceID_None, nsGkAtoms::src)) { nsContentUtils::AddScriptRunner( NewRunnableMethod(dest, &HTMLImageElement::MaybeLoadImage)); } } return NS_OK; } CORSMode HTMLImageElement::GetCORSMode() { return AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)); } JSObject* HTMLImageElement::WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { return HTMLImageElementBinding::Wrap(aCx, this, aGivenProto); } #ifdef DEBUG nsIDOMHTMLFormElement* HTMLImageElement::GetForm() const { return mForm; } #endif void HTMLImageElement::SetForm(nsIDOMHTMLFormElement* aForm) { NS_PRECONDITION(aForm, "Don't pass null here"); NS_ASSERTION(!mForm, "We don't support switching from one non-null form to another."); mForm = static_cast<HTMLFormElement*>(aForm); } void HTMLImageElement::ClearForm(bool aRemoveFromForm) { NS_ASSERTION((mForm != nullptr) == HasFlag(ADDED_TO_FORM), "Form control should have had flag set correctly"); if (!mForm) { return; } if (aRemoveFromForm) { nsAutoString nameVal, idVal; GetAttr(kNameSpaceID_None, nsGkAtoms::name, nameVal); GetAttr(kNameSpaceID_None, nsGkAtoms::id, idVal); mForm->RemoveImageElement(this); if (!nameVal.IsEmpty()) { mForm->RemoveImageElementFromTable(this, nameVal, HTMLFormElement::ElementRemoved); } if (!idVal.IsEmpty()) { mForm->RemoveImageElementFromTable(this, idVal, HTMLFormElement::ElementRemoved); } } UnsetFlags(ADDED_TO_FORM); mForm = nullptr; } void HTMLImageElement::QueueImageLoadTask(bool aAlwaysLoad) { // If loading is temporarily disabled, we don't want to queue tasks // that may then run when loading is re-enabled. if (!LoadingEnabled() || !this->OwnerDoc()->IsCurrentActiveDocument()) { return; } // Ensure that we don't overwrite a previous load request that requires // a complete load to occur. bool alwaysLoad = aAlwaysLoad; if (mPendingImageLoadTask) { alwaysLoad = alwaysLoad || mPendingImageLoadTask->AlwaysLoad(); } RefPtr<ImageLoadTask> task = new ImageLoadTask(this, alwaysLoad); // The task checks this to determine if it was the last // queued event, and so earlier tasks are implicitly canceled. mPendingImageLoadTask = task; nsContentUtils::RunInStableState(task.forget()); } bool HTMLImageElement::HaveSrcsetOrInPicture() { if (HasAttr(kNameSpaceID_None, nsGkAtoms::srcset)) { return true; } Element *parent = nsINode::GetParentElement(); return (parent && parent->IsHTMLElement(nsGkAtoms::picture)); } bool HTMLImageElement::InResponsiveMode() { // When we lose srcset or leave a <picture> element, the fallback to img.src // will happen from the microtask, and we should behave responsively in the // interim return mResponsiveSelector || mPendingImageLoadTask || HaveSrcsetOrInPicture(); } bool HTMLImageElement::SelectedSourceMatchesLast(nsIURI* aSelectedSource, double aSelectedDensity) { // If there was no selected source previously, we don't want to short-circuit the load. // Similarly for if there is no newly selected source. if (!mLastSelectedSource || !aSelectedSource) { return false; } bool equal = false; return NS_SUCCEEDED(mLastSelectedSource->Equals(aSelectedSource, &equal)) && equal && aSelectedDensity == mCurrentDensity; } nsresult HTMLImageElement::LoadSelectedImage(bool aForce, bool aNotify, bool aAlwaysLoad) { nsresult rv = NS_ERROR_FAILURE; if (aForce) { // In responsive mode we generally want to re-run the full // selection algorithm whenever starting a new load, per // spec. This also causes us to re-resolve the URI as appropriate. if (!UpdateResponsiveSource() && !aAlwaysLoad) { return NS_OK; } } nsCOMPtr<nsIURI> selectedSource; double currentDensity = 1.0; // default to 1.0 for the src attribute case if (mResponsiveSelector) { nsCOMPtr<nsIURI> url = mResponsiveSelector->GetSelectedImageURL(); selectedSource = url; currentDensity = mResponsiveSelector->GetSelectedImageDensity(); if (!aAlwaysLoad && SelectedSourceMatchesLast(selectedSource, currentDensity)) { return NS_OK; } if (url) { rv = LoadImage(url, aForce, aNotify, eImageLoadType_Imageset); } } else { nsAutoString src; if (!GetAttr(kNameSpaceID_None, nsGkAtoms::src, src)) { CancelImageRequests(aNotify); rv = NS_OK; } else { nsIDocument* doc = GetOurOwnerDoc(); if (doc) { StringToURI(src, doc, getter_AddRefs(selectedSource)); if (!aAlwaysLoad && SelectedSourceMatchesLast(selectedSource, currentDensity)) { return NS_OK; } } // If we have a srcset attribute or are in a <picture> element, // we always use the Imageset load type, even if we parsed no // valid responsive sources from either, per spec. rv = LoadImage(src, aForce, aNotify, HaveSrcsetOrInPicture() ? eImageLoadType_Imageset : eImageLoadType_Normal); } } mLastSelectedSource = selectedSource; mCurrentDensity = currentDensity; if (NS_FAILED(rv)) { CancelImageRequests(aNotify); } return rv; } void HTMLImageElement::PictureSourceSrcsetChanged(nsIContent *aSourceNode, const nsAString& aNewValue, bool aNotify) { MOZ_ASSERT(aSourceNode == this || IsPreviousSibling(aSourceNode, this), "Should not be getting notifications for non-previous-siblings"); nsIContent *currentSrc = mResponsiveSelector ? mResponsiveSelector->Content() : nullptr; if (aSourceNode == currentSrc) { // We're currently using this node as our responsive selector // source. mResponsiveSelector->SetCandidatesFromSourceSet(aNewValue); } if (!mInDocResponsiveContent && IsInComposedDoc()) { nsIDocument* doc = GetOurOwnerDoc(); if (doc) { doc->AddResponsiveContent(this); mInDocResponsiveContent = true; } } // This always triggers the image update steps per the spec, even if // we are not using this source. QueueImageLoadTask(true); } void HTMLImageElement::PictureSourceSizesChanged(nsIContent *aSourceNode, const nsAString& aNewValue, bool aNotify) { MOZ_ASSERT(aSourceNode == this || IsPreviousSibling(aSourceNode, this), "Should not be getting notifications for non-previous-siblings"); nsIContent *currentSrc = mResponsiveSelector ? mResponsiveSelector->Content() : nullptr; if (aSourceNode == currentSrc) { // We're currently using this node as our responsive selector // source. mResponsiveSelector->SetSizesFromDescriptor(aNewValue); } // This always triggers the image update steps per the spec, even if // we are not using this source. QueueImageLoadTask(true); } void HTMLImageElement::PictureSourceMediaOrTypeChanged(nsIContent *aSourceNode, bool aNotify) { MOZ_ASSERT(IsPreviousSibling(aSourceNode, this), "Should not be getting notifications for non-previous-siblings"); // This always triggers the image update steps per the spec, even if // we are not switching to/from this source QueueImageLoadTask(true); } void HTMLImageElement::PictureSourceAdded(nsIContent *aSourceNode) { MOZ_ASSERT(aSourceNode == this || IsPreviousSibling(aSourceNode, this), "Should not be getting notifications for non-previous-siblings"); QueueImageLoadTask(true); } void HTMLImageElement::PictureSourceRemoved(nsIContent *aSourceNode) { MOZ_ASSERT(aSourceNode == this || IsPreviousSibling(aSourceNode, this), "Should not be getting notifications for non-previous-siblings"); QueueImageLoadTask(true); } bool HTMLImageElement::UpdateResponsiveSource() { bool hadSelector = !!mResponsiveSelector; nsIContent *currentSource = mResponsiveSelector ? mResponsiveSelector->Content() : nullptr; Element *parent = nsINode::GetParentElement(); nsINode *candidateSource = nullptr; if (parent && parent->IsHTMLElement(nsGkAtoms::picture)) { // Walk source nodes previous to ourselves candidateSource = parent->GetFirstChild(); } else { candidateSource = this; } while (candidateSource) { if (candidateSource == currentSource) { // found no better source before current, re-run selection on // that and keep it if it's still usable. bool changed = mResponsiveSelector->SelectImage(true); if (mResponsiveSelector->NumCandidates()) { bool isUsableCandidate = true; // an otherwise-usable source element may still have a media query that may not // match any more. if (candidateSource->IsHTMLElement(nsGkAtoms::source) && !SourceElementMatches(candidateSource->AsContent())) { isUsableCandidate = false; } if (isUsableCandidate) { return changed; } } // no longer valid mResponsiveSelector = nullptr; if (candidateSource == this) { // No further possibilities break; } } else if (candidateSource == this) { // We are the last possible source if (!TryCreateResponsiveSelector(candidateSource->AsContent())) { // Failed to find any source mResponsiveSelector = nullptr; } break; } else if (candidateSource->IsHTMLElement(nsGkAtoms::source) && TryCreateResponsiveSelector(candidateSource->AsContent())) { // This led to a valid source, stop break; } candidateSource = candidateSource->GetNextSibling(); } if (!candidateSource) { // Ran out of siblings without finding ourself, e.g. XBL magic. mResponsiveSelector = nullptr; } // If we reach this point, either: // - there was no selector originally, and there is not one now // - there was no selector originally, and there is one now // - there was a selector, and there is a different one now // - there was a selector, and there is not one now return hadSelector || mResponsiveSelector; } /*static */ bool HTMLImageElement::SupportedPictureSourceType(const nsAString& aType) { nsAutoString type; nsAutoString params; nsContentUtils::SplitMimeType(aType, type, params); if (type.IsEmpty()) { return true; } return imgLoader::SupportImageWithMimeType(NS_ConvertUTF16toUTF8(type).get(), AcceptedMimeTypes::IMAGES_AND_DOCUMENTS); } bool HTMLImageElement::SourceElementMatches(nsIContent* aSourceNode) { MOZ_ASSERT(aSourceNode->IsHTMLElement(nsGkAtoms::source)); DebugOnly<Element *> parent(nsINode::GetParentElement()); MOZ_ASSERT(parent && parent->IsHTMLElement(nsGkAtoms::picture)); MOZ_ASSERT(IsPreviousSibling(aSourceNode, this)); // Check media and type HTMLSourceElement *src = static_cast<HTMLSourceElement*>(aSourceNode); if (!src->MatchesCurrentMedia()) { return false; } nsAutoString type; if (aSourceNode->GetAttr(kNameSpaceID_None, nsGkAtoms::type, type) && !SupportedPictureSourceType(type)) { return false; } return true; } bool HTMLImageElement::TryCreateResponsiveSelector(nsIContent *aSourceNode) { // Skip if this is not a <source> with matching media query bool isSourceTag = aSourceNode->IsHTMLElement(nsGkAtoms::source); if (isSourceTag) { if (!SourceElementMatches(aSourceNode)) { return false; } } else if (aSourceNode->IsHTMLElement(nsGkAtoms::img)) { // Otherwise this is the <img> tag itself MOZ_ASSERT(aSourceNode == this); } // Skip if has no srcset or an empty srcset nsString srcset; if (!aSourceNode->GetAttr(kNameSpaceID_None, nsGkAtoms::srcset, srcset)) { return false; } if (srcset.IsEmpty()) { return false; } // Try to parse RefPtr<ResponsiveImageSelector> sel = new ResponsiveImageSelector(aSourceNode); if (!sel->SetCandidatesFromSourceSet(srcset)) { // No possible candidates, don't need to bother parsing sizes return false; } nsAutoString sizes; aSourceNode->GetAttr(kNameSpaceID_None, nsGkAtoms::sizes, sizes); sel->SetSizesFromDescriptor(sizes); // If this is the <img> tag, also pull in src as the default source if (!isSourceTag) { MOZ_ASSERT(aSourceNode == this); nsAutoString src; if (GetAttr(kNameSpaceID_None, nsGkAtoms::src, src) && !src.IsEmpty()) { sel->SetDefaultSource(src); } } mResponsiveSelector = sel; return true; } /* static */ bool HTMLImageElement::SelectSourceForTagWithAttrs(nsIDocument *aDocument, bool aIsSourceTag, const nsAString& aSrcAttr, const nsAString& aSrcsetAttr, const nsAString& aSizesAttr, const nsAString& aTypeAttr, const nsAString& aMediaAttr, nsAString& aResult) { MOZ_ASSERT(aIsSourceTag || (aTypeAttr.IsEmpty() && aMediaAttr.IsEmpty()), "Passing type or media attrs makes no sense without aIsSourceTag"); MOZ_ASSERT(!aIsSourceTag || aSrcAttr.IsEmpty(), "Passing aSrcAttr makes no sense with aIsSourceTag set"); if (aSrcsetAttr.IsEmpty()) { if (!aIsSourceTag) { // For an <img> with no srcset, we would always select the src attr. aResult.Assign(aSrcAttr); return true; } // Otherwise, a <source> without srcset is never selected return false; } // Would not consider source tags with unsupported media or type if (aIsSourceTag && ((!aMediaAttr.IsVoid() && !HTMLSourceElement::WouldMatchMediaForDocument(aMediaAttr, aDocument)) || (!aTypeAttr.IsVoid() && !SupportedPictureSourceType(aTypeAttr)))) { return false; } // Using srcset or picture <source>, build a responsive selector for this tag. RefPtr<ResponsiveImageSelector> sel = new ResponsiveImageSelector(aDocument); sel->SetCandidatesFromSourceSet(aSrcsetAttr); if (!aSizesAttr.IsEmpty()) { sel->SetSizesFromDescriptor(aSizesAttr); } if (!aIsSourceTag) { sel->SetDefaultSource(aSrcAttr); } if (sel->GetSelectedImageURLSpec(aResult)) { return true; } if (!aIsSourceTag) { // <img> tag with no match would definitively load nothing. aResult.Truncate(); return true; } // <source> tags with no match would leave source yet-undetermined. return false; } void HTMLImageElement::DestroyContent() { mResponsiveSelector = nullptr; nsGenericHTMLElement::DestroyContent(); } void HTMLImageElement::MediaFeatureValuesChanged() { QueueImageLoadTask(false); } void HTMLImageElement::FlushUseCounters() { nsCOMPtr<imgIRequest> request; GetRequest(CURRENT_REQUEST, getter_AddRefs(request)); nsCOMPtr<imgIContainer> container; request->GetImage(getter_AddRefs(container)); static_cast<image::Image*>(container.get())->ReportUseCounters(); } } // namespace dom } // namespace mozilla