/* -*- 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/ResponsiveImageSelector.h" #include "mozilla/PresShell.h" #include "mozilla/PresShellInlines.h" #include "mozilla/ServoStyleSetInlines.h" #include "mozilla/TextUtils.h" #include "nsIURI.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.h" #include "nsContentUtils.h" #include "nsPresContext.h" #include "nsCSSProps.h" using namespace mozilla; using namespace mozilla::dom; namespace mozilla::dom { NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector, mOwnerNode) static bool ParseInteger(const nsAString& aString, int32_t& aInt) { nsContentUtils::ParseHTMLIntegerResultFlags parseResult; aInt = nsContentUtils::ParseHTMLInteger(aString, &parseResult); return !(parseResult & (nsContentUtils::eParseHTMLInteger_Error | nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput | nsContentUtils::eParseHTMLInteger_NonStandard)); } static bool ParseFloat(const nsAString& aString, double& aDouble) { // Check if it is a valid floating-point number first since the result of // nsString.ToDouble() is more lenient than the spec, // https://html.spec.whatwg.org/#valid-floating-point-number nsAString::const_iterator iter, end; aString.BeginReading(iter); aString.EndReading(end); if (iter == end) { return false; } if (*iter == char16_t('-') && ++iter == end) { return false; } if (IsAsciiDigit(*iter)) { for (; iter != end && IsAsciiDigit(*iter); ++iter) ; } else if (*iter == char16_t('.')) { // Do nothing, jumps to fraction part } else { return false; } // Fraction if (*iter == char16_t('.')) { ++iter; if (iter == end || !IsAsciiDigit(*iter)) { // U+002E FULL STOP character (.) must be followed by one or more ASCII // digits return false; } for (; iter != end && IsAsciiDigit(*iter); ++iter) ; } if (iter != end && (*iter == char16_t('e') || *iter == char16_t('E'))) { ++iter; if (*iter == char16_t('-') || *iter == char16_t('+')) { ++iter; } if (iter == end || !IsAsciiDigit(*iter)) { // Should have one or more ASCII digits return false; } for (; iter != end && IsAsciiDigit(*iter); ++iter) ; } if (iter != end) { return false; } nsresult rv; aDouble = PromiseFlatString(aString).ToDouble(&rv); return NS_SUCCEEDED(rv); } ResponsiveImageSelector::ResponsiveImageSelector(nsIContent* aContent) : mOwnerNode(aContent), mSelectedCandidateIndex(-1) {} ResponsiveImageSelector::ResponsiveImageSelector(dom::Document* aDocument) : mOwnerNode(aDocument), mSelectedCandidateIndex(-1) {} ResponsiveImageSelector::~ResponsiveImageSelector() = default; void ResponsiveImageSelector::ParseSourceSet( const nsAString& aSrcSet, FunctionRef aCallback) { nsAString::const_iterator iter, end; aSrcSet.BeginReading(iter); aSrcSet.EndReading(end); // Read URL / descriptor pairs while (iter != end) { nsAString::const_iterator url, urlEnd, descriptor; // Skip whitespace and commas. // Extra commas at this point are a non-fatal syntax error. for (; iter != end && (nsContentUtils::IsHTMLWhitespace(*iter) || *iter == char16_t(',')); ++iter) ; if (iter == end) { break; } url = iter; // Find end of url for (; iter != end && !nsContentUtils::IsHTMLWhitespace(*iter); ++iter) ; // Omit trailing commas from URL. // Multiple commas are a non-fatal error. while (iter != url) { if (*(--iter) != char16_t(',')) { iter++; break; } } const nsDependentSubstring& urlStr = Substring(url, iter); MOZ_ASSERT(url != iter, "Shouldn't have empty URL at this point"); ResponsiveImageCandidate candidate; if (candidate.ConsumeDescriptors(iter, end)) { candidate.SetURLSpec(urlStr); aCallback(std::move(candidate)); } } } // http://www.whatwg.org/specs/web-apps/current-work/#processing-the-image-candidates bool ResponsiveImageSelector::SetCandidatesFromSourceSet( const nsAString& aSrcSet, nsIPrincipal* aTriggeringPrincipal) { ClearSelectedCandidate(); if (!mOwnerNode || !mOwnerNode->GetBaseURI()) { MOZ_ASSERT(false, "Should not be parsing SourceSet without a document"); return false; } mCandidates.Clear(); auto eachCandidate = [&](ResponsiveImageCandidate&& aCandidate) { aCandidate.SetTriggeringPrincipal( nsContentUtils::GetAttrTriggeringPrincipal( Content(), aCandidate.URLString(), aTriggeringPrincipal)); AppendCandidateIfUnique(std::move(aCandidate)); }; ParseSourceSet(aSrcSet, eachCandidate); bool parsedCandidates = !mCandidates.IsEmpty(); // Re-add default to end of list MaybeAppendDefaultCandidate(); return parsedCandidates; } uint32_t ResponsiveImageSelector::NumCandidates(bool aIncludeDefault) { uint32_t candidates = mCandidates.Length(); // If present, the default candidate is the last item if (!aIncludeDefault && candidates && mCandidates.LastElement().IsDefault()) { candidates--; } return candidates; } nsIContent* ResponsiveImageSelector::Content() { return mOwnerNode->IsContent() ? mOwnerNode->AsContent() : nullptr; } dom::Document* ResponsiveImageSelector::Document() { return mOwnerNode->OwnerDoc(); } void ResponsiveImageSelector::SetDefaultSource(const nsAString& aURLString, nsIPrincipal* aPrincipal) { ClearSelectedCandidate(); // Check if the last element of our candidates is a default if (!mCandidates.IsEmpty() && mCandidates.LastElement().IsDefault()) { mCandidates.RemoveLastElement(); } mDefaultSourceURL = aURLString; mDefaultSourceTriggeringPrincipal = aPrincipal; // Add new default to end of list MaybeAppendDefaultCandidate(); } void ResponsiveImageSelector::ClearSelectedCandidate() { mSelectedCandidateIndex = -1; mSelectedCandidateURL = nullptr; } bool ResponsiveImageSelector::SetSizesFromDescriptor(const nsAString& aSizes) { ClearSelectedCandidate(); NS_ConvertUTF16toUTF8 sizes(aSizes); mServoSourceSizeList.reset(Servo_SourceSizeList_Parse(&sizes)); return !!mServoSourceSizeList; } void ResponsiveImageSelector::AppendCandidateIfUnique( ResponsiveImageCandidate&& aCandidate) { int numCandidates = mCandidates.Length(); // With the exception of Default, which should not be added until we are done // building the list. if (aCandidate.IsDefault()) { return; } // Discard candidates with identical parameters, they will never match for (int i = 0; i < numCandidates; i++) { if (mCandidates[i].HasSameParameter(aCandidate)) { return; } } mCandidates.AppendElement(std::move(aCandidate)); } void ResponsiveImageSelector::MaybeAppendDefaultCandidate() { if (mDefaultSourceURL.IsEmpty()) { return; } int numCandidates = mCandidates.Length(); // https://html.spec.whatwg.org/multipage/embedded-content.html#update-the-source-set // step 4.1.3: // If child has a src attribute whose value is not the empty string and source // set does not contain an image source with a density descriptor value of 1, // and no image source with a width descriptor, append child's src attribute // value to source set. for (int i = 0; i < numCandidates; i++) { if (mCandidates[i].IsComputedFromWidth()) { return; } else if (mCandidates[i].Density(this) == 1.0) { return; } } ResponsiveImageCandidate defaultCandidate; defaultCandidate.SetParameterDefault(); defaultCandidate.SetURLSpec(mDefaultSourceURL); defaultCandidate.SetTriggeringPrincipal(mDefaultSourceTriggeringPrincipal); // We don't use MaybeAppend since we want to keep this even if it can never // match, as it may if the source set changes. mCandidates.AppendElement(std::move(defaultCandidate)); } already_AddRefed ResponsiveImageSelector::GetSelectedImageURL() { SelectImage(); nsCOMPtr url = mSelectedCandidateURL; return url.forget(); } bool ResponsiveImageSelector::GetSelectedImageURLSpec(nsAString& aResult) { SelectImage(); if (mSelectedCandidateIndex == -1) { return false; } aResult.Assign(mCandidates[mSelectedCandidateIndex].URLString()); return true; } double ResponsiveImageSelector::GetSelectedImageDensity() { int bestIndex = GetSelectedCandidateIndex(); if (bestIndex < 0) { return 1.0; } return mCandidates[bestIndex].Density(this); } nsIPrincipal* ResponsiveImageSelector::GetSelectedImageTriggeringPrincipal() { int bestIndex = GetSelectedCandidateIndex(); if (bestIndex < 0) { return nullptr; } return mCandidates[bestIndex].TriggeringPrincipal(); } bool ResponsiveImageSelector::SelectImage(bool aReselect) { if (!aReselect && mSelectedCandidateIndex != -1) { // Already have selection return false; } int oldBest = mSelectedCandidateIndex; ClearSelectedCandidate(); int numCandidates = mCandidates.Length(); if (!numCandidates) { return oldBest != -1; } dom::Document* doc = Document(); nsPresContext* pctx = doc->GetPresContext(); nsCOMPtr baseURI = mOwnerNode->GetBaseURI(); if (!pctx || !baseURI) { return oldBest != -1; } double displayDensity = pctx->CSSPixelsToDevPixels(1.0f); double overrideDPPX = pctx->GetOverrideDPPX(); if (overrideDPPX > 0) { displayDensity = overrideDPPX; } // Per spec, "In a UA-specific manner, choose one image source" // - For now, select the lowest density greater than displayDensity, otherwise // the greatest density available // If the list contains computed width candidates, compute the current // effective image width. double computedWidth = -1; for (int i = 0; i < numCandidates; i++) { if (mCandidates[i].IsComputedFromWidth()) { DebugOnly computeResult = ComputeFinalWidthForCurrentViewport(&computedWidth); MOZ_ASSERT(computeResult, "Computed candidates not allowed without sizes data"); break; } } int bestIndex = -1; double bestDensity = -1.0; for (int i = 0; i < numCandidates; i++) { double candidateDensity = (computedWidth == -1) ? mCandidates[i].Density(this) : mCandidates[i].Density(computedWidth); // - If bestIndex is below display density, pick anything larger. // - Otherwise, prefer if less dense than bestDensity but still above // displayDensity. if (bestIndex == -1 || (bestDensity < displayDensity && candidateDensity > bestDensity) || (candidateDensity >= displayDensity && candidateDensity < bestDensity)) { bestIndex = i; bestDensity = candidateDensity; } } MOZ_ASSERT(bestIndex >= 0 && bestIndex < numCandidates); // Resolve URL nsresult rv; const nsAString& urlStr = mCandidates[bestIndex].URLString(); nsCOMPtr candidateURL; rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL), urlStr, doc, baseURI); mSelectedCandidateURL = NS_SUCCEEDED(rv) ? candidateURL : nullptr; mSelectedCandidateIndex = bestIndex; return mSelectedCandidateIndex != oldBest; } int ResponsiveImageSelector::GetSelectedCandidateIndex() { SelectImage(); return mSelectedCandidateIndex; } bool ResponsiveImageSelector::ComputeFinalWidthForCurrentViewport( double* aWidth) { dom::Document* doc = Document(); PresShell* presShell = doc->GetPresShell(); nsPresContext* pctx = presShell ? presShell->GetPresContext() : nullptr; if (!pctx) { return false; } nscoord effectiveWidth = presShell->StyleSet()->EvaluateSourceSizeList(mServoSourceSizeList.get()); *aWidth = nsPresContext::AppUnitsToDoubleCSSPixels(std::max(effectiveWidth, 0)); return true; } ResponsiveImageCandidate::ResponsiveImageCandidate() { mType = CandidateType::Invalid; mValue.mDensity = 1.0; } void ResponsiveImageCandidate::SetURLSpec(const nsAString& aURLString) { mURLString = aURLString; } void ResponsiveImageCandidate::SetTriggeringPrincipal( nsIPrincipal* aPrincipal) { mTriggeringPrincipal = aPrincipal; } void ResponsiveImageCandidate::SetParameterAsComputedWidth(int32_t aWidth) { mType = CandidateType::ComputedFromWidth; mValue.mWidth = aWidth; } void ResponsiveImageCandidate::SetParameterDefault() { MOZ_ASSERT(!IsValid(), "double setting candidate type"); mType = CandidateType::Default; // mValue shouldn't actually be used for this type, but set it to default // anyway mValue.mDensity = 1.0; } void ResponsiveImageCandidate::SetParameterInvalid() { mType = CandidateType::Invalid; // mValue shouldn't actually be used for this type, but set it to default // anyway mValue.mDensity = 1.0; } void ResponsiveImageCandidate::SetParameterAsDensity(double aDensity) { MOZ_ASSERT(!IsValid(), "double setting candidate type"); mType = CandidateType::Density; mValue.mDensity = aDensity; } // Represents all supported descriptors for a ResponsiveImageCandidate, though // there is no candidate type that uses all of these. This should generally // match the mValue union of ResponsiveImageCandidate. struct ResponsiveImageDescriptors { ResponsiveImageDescriptors() : mInvalid(false){}; Maybe mDensity; Maybe mWidth; // We don't support "h" descriptors yet and they are not spec'd, but the // current spec does specify that they can be silently ignored (whereas // entirely unknown descriptors cause us to invalidate the candidate) // // If we ever start honoring them we should serialize them in // AppendDescriptors. Maybe mFutureCompatHeight; // If this descriptor set is bogus, e.g. a value was added twice (and thus // dropped) or an unknown descriptor was added. bool mInvalid; void AddDescriptor(const nsAString& aDescriptor); bool Valid(); // Use the current set of descriptors to configure a candidate void FillCandidate(ResponsiveImageCandidate& aCandidate); }; // Try to parse a single descriptor from a string. If value already set or // unknown, sets invalid flag. // This corresponds to the descriptor "Descriptor parser" step in: // https://html.spec.whatwg.org/#parse-a-srcset-attribute void ResponsiveImageDescriptors::AddDescriptor(const nsAString& aDescriptor) { if (aDescriptor.IsEmpty()) { return; } // All currently supported descriptors end with an identifying character. nsAString::const_iterator descStart, descType; aDescriptor.BeginReading(descStart); aDescriptor.EndReading(descType); descType--; const nsDependentSubstring& valueStr = Substring(descStart, descType); if (*descType == char16_t('w')) { int32_t possibleWidth; // If the value is not a valid non-negative integer, it doesn't match this // descriptor, fall through. if (ParseInteger(valueStr, possibleWidth) && possibleWidth >= 0) { if (possibleWidth != 0 && mWidth.isNothing() && mDensity.isNothing()) { mWidth.emplace(possibleWidth); } else { // Valid width descriptor, but width or density were already seen, sizes // support isn't enabled, or it parsed to 0, which is an error per spec mInvalid = true; } return; } } else if (*descType == char16_t('h')) { int32_t possibleHeight; // If the value is not a valid non-negative integer, it doesn't match this // descriptor, fall through. if (ParseInteger(valueStr, possibleHeight) && possibleHeight >= 0) { if (possibleHeight != 0 && mFutureCompatHeight.isNothing() && mDensity.isNothing()) { mFutureCompatHeight.emplace(possibleHeight); } else { // Valid height descriptor, but height or density were already seen, or // it parsed to zero, which is an error per spec mInvalid = true; } return; } } else if (*descType == char16_t('x')) { // If the value is not a valid floating point number, it doesn't match this // descriptor, fall through. double possibleDensity = 0.0; if (ParseFloat(valueStr, possibleDensity)) { if (possibleDensity >= 0.0 && mWidth.isNothing() && mDensity.isNothing() && mFutureCompatHeight.isNothing()) { mDensity.emplace(possibleDensity); } else { // Valid density descriptor, but height or width or density were already // seen, or it parsed to less than zero, which is an error per spec mInvalid = true; } return; } } // Matched no known descriptor, mark this descriptor set invalid mInvalid = true; } bool ResponsiveImageDescriptors::Valid() { return !mInvalid && !(mFutureCompatHeight.isSome() && mWidth.isNothing()); } void ResponsiveImageDescriptors::FillCandidate( ResponsiveImageCandidate& aCandidate) { if (!Valid()) { aCandidate.SetParameterInvalid(); } else if (mWidth.isSome()) { MOZ_ASSERT(mDensity.isNothing()); // Shouldn't be valid aCandidate.SetParameterAsComputedWidth(*mWidth); } else if (mDensity.isSome()) { MOZ_ASSERT(mWidth.isNothing()); // Shouldn't be valid aCandidate.SetParameterAsDensity(*mDensity); } else { // A valid set of descriptors with no density nor width (e.g. an empty set) // becomes 1.0 density, per spec aCandidate.SetParameterAsDensity(1.0); } } bool ResponsiveImageCandidate::ConsumeDescriptors( nsAString::const_iterator& aIter, const nsAString::const_iterator& aIterEnd) { nsAString::const_iterator& iter = aIter; const nsAString::const_iterator& end = aIterEnd; bool inParens = false; ResponsiveImageDescriptors descriptors; // Parse descriptor list. // This corresponds to the descriptor parsing loop from: // https://html.spec.whatwg.org/#parse-a-srcset-attribute // Skip initial whitespace for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter) ; nsAString::const_iterator currentDescriptor = iter; for (;; iter++) { if (iter == end) { descriptors.AddDescriptor(Substring(currentDescriptor, iter)); break; } else if (inParens) { if (*iter == char16_t(')')) { inParens = false; } } else { if (*iter == char16_t(',')) { // End of descriptors, flush current descriptor and advance past comma // before breaking descriptors.AddDescriptor(Substring(currentDescriptor, iter)); iter++; break; } if (nsContentUtils::IsHTMLWhitespace(*iter)) { // End of current descriptor, consume it, skip spaces // ("After descriptor" state in spec) before continuing descriptors.AddDescriptor(Substring(currentDescriptor, iter)); for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter) ; if (iter == end) { break; } currentDescriptor = iter; // Leave one whitespace so the loop advances to this position next // iteration iter--; } else if (*iter == char16_t('(')) { inParens = true; } } } descriptors.FillCandidate(*this); return IsValid(); } bool ResponsiveImageCandidate::HasSameParameter( const ResponsiveImageCandidate& aOther) const { if (aOther.mType != mType) { return false; } if (mType == CandidateType::Default) { return true; } if (mType == CandidateType::Density) { return aOther.mValue.mDensity == mValue.mDensity; } if (mType == CandidateType::Invalid) { MOZ_ASSERT_UNREACHABLE("Comparing invalid candidates?"); return true; } if (mType == CandidateType::ComputedFromWidth) { return aOther.mValue.mWidth == mValue.mWidth; } MOZ_ASSERT(false, "Somebody forgot to check for all uses of this enum"); return false; } double ResponsiveImageCandidate::Density( ResponsiveImageSelector* aSelector) const { if (mType == CandidateType::ComputedFromWidth) { double width; if (!aSelector->ComputeFinalWidthForCurrentViewport(&width)) { return 1.0; } return Density(width); } // Other types don't need matching width MOZ_ASSERT(mType == CandidateType::Default || mType == CandidateType::Density, "unhandled candidate type"); return Density(-1); } void ResponsiveImageCandidate::AppendDescriptors( nsAString& aDescriptors) const { MOZ_ASSERT(IsValid()); switch (mType) { case CandidateType::Default: case CandidateType::Invalid: return; case CandidateType::ComputedFromWidth: aDescriptors.Append(' '); aDescriptors.AppendInt(mValue.mWidth); aDescriptors.Append('w'); return; case CandidateType::Density: aDescriptors.Append(' '); aDescriptors.AppendFloat(mValue.mDensity); aDescriptors.Append('x'); return; } } double ResponsiveImageCandidate::Density(double aMatchingWidth) const { if (mType == CandidateType::Invalid) { MOZ_ASSERT(false, "Getting density for uninitialized candidate"); return 1.0; } if (mType == CandidateType::Default) { return 1.0; } if (mType == CandidateType::Density) { return mValue.mDensity; } if (mType == CandidateType::ComputedFromWidth) { if (aMatchingWidth < 0) { MOZ_ASSERT( false, "Don't expect to have a negative matching width at this point"); return 1.0; } double density = double(mValue.mWidth) / aMatchingWidth; MOZ_ASSERT(density > 0.0); return density; } MOZ_ASSERT(false, "Unknown candidate type"); return 1.0; } } // namespace mozilla::dom