Commit aa8c1138 authored by Em Zhan's avatar Em Zhan
Browse files

Bug 1425310 - Implement modulepreload for link rel. r=yulia,smaug,jonco

Does perform the optional step of fetching descendants and linking:
https://html.spec.whatwg.org/multipage/webappapis.html#fetching-scripts:fetch-the-descendants-of-and-link-a-module-script-2

Partially implements some new destinations ("as" attribute values) for
modulepreload only (not for preload or link attribute reflection)

Differential Revision: https://phabricator.services.mozilla.com/D172368
parent d81ad62b
Loading
Loading
Loading
Loading
+66 −29
Original line number Diff line number Diff line
@@ -311,36 +311,30 @@ void HTMLLinkElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
      aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
}

static const DOMTokenListSupportedToken sSupportedRelValues[] = {
    // Keep this and the one below in sync with ToLinkMask in
    // LinkStyle.cpp.
    // "preload" must come first because it can be disabled.
    "preload", "prefetch",      "dns-prefetch", "stylesheet",
    "next",    "alternate",     "preconnect",   "icon",
    "search",  "modulepreload", nullptr};

static const DOMTokenListSupportedToken sSupportedRelValuesWithManifest[] = {
    // Keep this in sync with ToLinkMask in LinkStyle.cpp.
    // "preload" and "manifest" must come first because they can be disabled.
    "preload",   "manifest",   "prefetch", "dns-prefetch", "stylesheet", "next",
    "alternate", "preconnect", "icon",     "search",       nullptr};
// Keep this and the arrays below in sync with ToLinkMask in LinkStyle.cpp.
#define SUPPORTED_REL_VALUES_BASE                                              \
  "prefetch", "dns-prefetch", "stylesheet", "next", "alternate", "preconnect", \
      "icon", "search", nullptr

static const DOMTokenListSupportedToken sSupportedRelValueCombinations[][12] = {
    {SUPPORTED_REL_VALUES_BASE},
    {"manifest", SUPPORTED_REL_VALUES_BASE},
    {"preload", SUPPORTED_REL_VALUES_BASE},
    {"preload", "manifest", SUPPORTED_REL_VALUES_BASE},
    {"modulepreload", SUPPORTED_REL_VALUES_BASE},
    {"modulepreload", "manifest", SUPPORTED_REL_VALUES_BASE},
    {"modulepreload", "preload", SUPPORTED_REL_VALUES_BASE},
    {"modulepreload", "preload", "manifest", SUPPORTED_REL_VALUES_BASE}};
#undef SUPPORTED_REL_VALUES_BASE

nsDOMTokenList* HTMLLinkElement::RelList() {
  if (!mRelList) {
    auto preload = StaticPrefs::network_preload();
    auto manifest = StaticPrefs::dom_manifest_enabled();
    if (manifest && preload) {
      mRelList = new nsDOMTokenList(this, nsGkAtoms::rel,
                                    sSupportedRelValuesWithManifest);
    } else if (manifest && !preload) {
    int index = (StaticPrefs::dom_manifest_enabled() ? 1 : 0) |
                (StaticPrefs::network_preload() ? 2 : 0) |
                (StaticPrefs::network_modulepreload() ? 4 : 0);

    mRelList = new nsDOMTokenList(this, nsGkAtoms::rel,
                                    &sSupportedRelValuesWithManifest[1]);
    } else if (!manifest && preload) {
      mRelList = new nsDOMTokenList(this, nsGkAtoms::rel, sSupportedRelValues);
    } else {  // both false...drop preload
      mRelList =
          new nsDOMTokenList(this, nsGkAtoms::rel, &sSupportedRelValues[1]);
    }
                                  sSupportedRelValueCombinations[index]);
  }
  return mRelList;
}
@@ -490,7 +484,10 @@ void HTMLLinkElement::
  }

  if (linkTypes & eMODULE_PRELOAD) {
    if (!OwnerDoc()->ScriptLoader()->GetModuleLoader()) {
    ScriptLoader* scriptLoader = OwnerDoc()->ScriptLoader();
    ModuleLoader* moduleLoader = scriptLoader->GetModuleLoader();

    if (!moduleLoader) {
      // For the print preview documents, at this moment it doesn't have module
      // loader yet, as the (print preview) document is not attached to the
      // nsIContentViewer yet, so it doesn't have the GlobalObject.
@@ -500,9 +497,49 @@ void HTMLLinkElement::
      return;
    }

    if (!StaticPrefs::network_modulepreload()) {
      // Keep behavior from https://phabricator.services.mozilla.com/D149371,
      // prior to main implementation of modulepreload
      moduleLoader->DisallowImportMaps();
      return;
    }

    // https://html.spec.whatwg.org/multipage/semantics.html#processing-the-media-attribute
    // TODO: apply this check for all linkTypes
    nsAutoString media;
    if (GetAttr(nsGkAtoms::media, media)) {
      RefPtr<mozilla::dom::MediaList> mediaList =
          mozilla::dom::MediaList::Create(NS_ConvertUTF16toUTF8(media));
      if (!mediaList->Matches(*OwnerDoc())) {
        return;
      }
    }

    // TODO: per spec, apply this check for ePREFETCH as well
    if (!HasNonEmptyAttr(nsGkAtoms::href)) {
      return;
    }

    nsAutoString as;
    GetAttr(nsGkAtoms::as, as);

    if (!net::IsScriptLikeOrInvalid(as)) {
      RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
          this, u"error"_ns, CanBubble::eNo, ChromeOnlyDispatch::eNo);
      asyncDispatcher->PostDOMEvent();
      return;
    }

    nsCOMPtr<nsIURI> uri = GetURI();
    if (!uri) {
      return;
    }

    // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-modulepreload-module-script-graph
    // Step 1. Disallow further import maps given settings object.
    OwnerDoc()->ScriptLoader()->GetModuleLoader()->DisallowImportMaps();
    moduleLoader->DisallowImportMaps();

    StartPreload(nsIContentPolicy::TYPE_SCRIPT);
    return;
  }

+4 −3
Original line number Diff line number Diff line
@@ -62,9 +62,10 @@ class Element;
 *        the script does not need to be fetched first.
 *    * mIsXSLT
 *        Set if we are in an XSLT request.
 *    * TODO: mIsPreload (will be moved from ScriptFetchOptions)
 *    * mIsPreload
 *        Set for scripts that are preloaded in a
 *        <link rel="preload" as="script"> element.
 *        <link rel="preload" as="script"> or <link rel="modulepreload">
 *        element.
 *
 * In addition to describing how the ScriptLoadRequest will be loaded by the
 * DOM ScriptLoader, the ScriptLoadContext contains fields that facilitate
@@ -113,7 +114,7 @@ class ScriptLoadContext : public JS::loader::LoadContextBase,
    eDeferred,
    eAsync,
    eLinkPreload  // this is a load initiated by <link rel="preload"
                  // as="script"> tag
                  // as="script"> or <link rel="modulepreload"> tag
  };

  void SetScriptMode(bool aDeferAttr, bool aAsyncAttr, bool aLinkPreload);
+8 −7
Original line number Diff line number Diff line
@@ -675,12 +675,12 @@ nsresult ScriptLoader::StartLoadInternal(
       aRequest->GetScriptLoadContext()->IsTracking()));

  if (aRequest->GetScriptLoadContext()->IsLinkPreloadScript()) {
    // This is <link rel="preload" as="script"> initiated speculative load,
    // put it to the group that is not blocked by leaders and doesn't block
    // follower at the same time. Giving it a much higher priority will make
    // this request be processed ahead of other Unblocked requests, but with
    // the same weight as Leaders.  This will make us behave similar way for
    // both http2 and http1.
    // This is <link rel="preload" as="script"> or <link rel="modulepreload">
    // initiated speculative load, put it to the group that is not blocked by
    // leaders and doesn't block follower at the same time. Giving it a much
    // higher priority will make this request be processed ahead of other
    // Unblocked requests, but with the same weight as Leaders. This will make
    // us behave similar way for both http2 and http1.
    ScriptLoadContext::PrioritizeAsPreload(channel);
    ScriptLoadContext::AddLoadBackgroundFlag(channel);
  } else if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(channel)) {
@@ -786,7 +786,8 @@ nsresult ScriptLoader::StartLoadInternal(
      aRequest->mURI, aRequest->CORSMode(), aRequest->mKind);
  aRequest->GetScriptLoadContext()->NotifyOpen(
      key, channel, mDocument,
      aRequest->GetScriptLoadContext()->IsLinkPreloadScript());
      aRequest->GetScriptLoadContext()->IsLinkPreloadScript(),
      aRequest->IsModuleRequest());

  if (aEarlyHintPreloaderId) {
    nsCOMPtr<nsIHttpChannelInternal> channelInternal =
+10 −1
Original line number Diff line number Diff line
@@ -11673,12 +11673,21 @@
  value: false
  mirror: always

# Enables `<link rel="preload">` tag and `Link: rel=preload` response header handling.
# Enables `<link rel="preload">` tag and `Link: rel=preload` response header
# handling.
- name: network.preload
  type: RelaxedAtomicBool
  value: true
  mirror: always

# Enables `<link rel="modulepreload">` tag and `Link: rel=modulepreload`
# response header handling. The latter is not yet implemented, see:
# https://bugzilla.mozilla.org/show_bug.cgi?id=1773056.
- name: network.modulepreload
  type: RelaxedAtomicBool
  value: true
  mirror: always

# Enable 103 Early Hint status code (RFC 8297)
- name: network.early-hints.enabled
  type: RelaxedAtomicBool
+18 −0
Original line number Diff line number Diff line
@@ -3756,6 +3756,24 @@ nsContentPolicyType AsValueToContentPolicy(const nsAttrValue& aValue) {
  return nsIContentPolicy::TYPE_INVALID;
}

// TODO: implement this using nsAttrValue's destination enums when support for
// the new destinations is added; see this diff for a possible start:
// https://phabricator.services.mozilla.com/D172368?vs=705114&id=708720
bool IsScriptLikeOrInvalid(const nsAString& aAs) {
  return !(
      aAs.LowerCaseEqualsASCII("fetch") || aAs.LowerCaseEqualsASCII("audio") ||
      aAs.LowerCaseEqualsASCII("document") ||
      aAs.LowerCaseEqualsASCII("embed") || aAs.LowerCaseEqualsASCII("font") ||
      aAs.LowerCaseEqualsASCII("frame") || aAs.LowerCaseEqualsASCII("iframe") ||
      aAs.LowerCaseEqualsASCII("image") ||
      aAs.LowerCaseEqualsASCII("manifest") ||
      aAs.LowerCaseEqualsASCII("object") ||
      aAs.LowerCaseEqualsASCII("report") || aAs.LowerCaseEqualsASCII("style") ||
      aAs.LowerCaseEqualsASCII("track") || aAs.LowerCaseEqualsASCII("video") ||
      aAs.LowerCaseEqualsASCII("webidentity") ||
      aAs.LowerCaseEqualsASCII("xslt"));
}

bool CheckPreloadAttrs(const nsAttrValue& aAs, const nsAString& aType,
                       const nsAString& aMedia,
                       mozilla::dom::Document* aDocument) {
Loading