1 | /* |
2 | * Copyright (C) 2011 Google Inc. All rights reserved. |
3 | * Copyright (C) 2016-2017 Apple Inc. All rights reserved. |
4 | * |
5 | * Redistribution and use in source and binary forms, with or without |
6 | * modification, are permitted provided that the following conditions are |
7 | * met: |
8 | * |
9 | * * Redistributions of source code must retain the above copyright |
10 | * notice, this list of conditions and the following disclaimer. |
11 | * * Redistributions in binary form must reproduce the above |
12 | * copyright notice, this list of conditions and the following disclaimer |
13 | * in the documentation and/or other materials provided with the |
14 | * distribution. |
15 | * * Neither the name of Google Inc. nor the names of its |
16 | * contributors may be used to endorse or promote products derived from |
17 | * this software without specific prior written permission. |
18 | * |
19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
20 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
21 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
22 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
23 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
24 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
25 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
26 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
27 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
28 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
29 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
30 | * |
31 | */ |
32 | |
33 | #include "config.h" |
34 | #include "LinkLoader.h" |
35 | |
36 | #include "CSSStyleSheet.h" |
37 | #include "CachedCSSStyleSheet.h" |
38 | #include "CachedResourceLoader.h" |
39 | #include "CachedResourceRequest.h" |
40 | #include "ContainerNode.h" |
41 | #include "CrossOriginAccessControl.h" |
42 | #include "Document.h" |
43 | #include "Frame.h" |
44 | #include "FrameLoader.h" |
45 | #include "FrameLoaderClient.h" |
46 | #include "FrameView.h" |
47 | #include "HTMLSrcsetParser.h" |
48 | #include "LinkHeader.h" |
49 | #include "LinkPreloadResourceClients.h" |
50 | #include "LinkRelAttribute.h" |
51 | #include "LoaderStrategy.h" |
52 | #include "MIMETypeRegistry.h" |
53 | #include "MediaList.h" |
54 | #include "MediaQueryEvaluator.h" |
55 | #include "PlatformStrategies.h" |
56 | #include "ResourceError.h" |
57 | #include "RuntimeEnabledFeatures.h" |
58 | #include "Settings.h" |
59 | #include "SizesAttributeParser.h" |
60 | #include "StyleResolver.h" |
61 | |
62 | namespace WebCore { |
63 | |
64 | LinkLoader::LinkLoader(LinkLoaderClient& client) |
65 | : m_client(client) |
66 | { |
67 | } |
68 | |
69 | LinkLoader::~LinkLoader() |
70 | { |
71 | if (m_cachedLinkResource) |
72 | m_cachedLinkResource->removeClient(*this); |
73 | if (m_preloadResourceClient) |
74 | m_preloadResourceClient->clear(); |
75 | } |
76 | |
77 | void LinkLoader::triggerEvents(const CachedResource& resource) |
78 | { |
79 | if (resource.errorOccurred()) |
80 | m_client.linkLoadingErrored(); |
81 | else |
82 | m_client.linkLoaded(); |
83 | } |
84 | |
85 | void LinkLoader::notifyFinished(CachedResource& resource) |
86 | { |
87 | ASSERT_UNUSED(resource, m_cachedLinkResource.get() == &resource); |
88 | |
89 | triggerEvents(*m_cachedLinkResource); |
90 | |
91 | m_cachedLinkResource->removeClient(*this); |
92 | m_cachedLinkResource = nullptr; |
93 | } |
94 | |
95 | void LinkLoader::(const String& , const URL& baseURL, Document& document, MediaAttributeCheck mediaAttributeCheck) |
96 | { |
97 | if (headerValue.isEmpty()) |
98 | return; |
99 | LinkHeaderSet (headerValue); |
100 | for (auto& : headerSet) { |
101 | if (!header.valid() || header.url().isEmpty() || header.rel().isEmpty()) |
102 | continue; |
103 | if ((mediaAttributeCheck == MediaAttributeCheck::MediaAttributeNotEmpty && !header.isViewportDependent()) |
104 | || (mediaAttributeCheck == MediaAttributeCheck::MediaAttributeEmpty && header.isViewportDependent())) { |
105 | continue; |
106 | } |
107 | |
108 | LinkRelAttribute relAttribute(document, header.rel()); |
109 | URL url(baseURL, header.url()); |
110 | // Sanity check to avoid re-entrancy here. |
111 | if (equalIgnoringFragmentIdentifier(url, baseURL)) |
112 | continue; |
113 | preconnectIfNeeded(relAttribute, url, document, header.crossOrigin()); |
114 | preloadIfNeeded(relAttribute, url, document, header.as(), header.media(), header.mimeType(), header.crossOrigin(), header.imageSrcSet(), header.imageSizes(), nullptr); |
115 | } |
116 | } |
117 | |
118 | Optional<CachedResource::Type> LinkLoader::resourceTypeFromAsAttribute(const String& as) |
119 | { |
120 | if (equalLettersIgnoringASCIICase(as, "fetch" )) |
121 | return CachedResource::Type::RawResource; |
122 | if (equalLettersIgnoringASCIICase(as, "image" )) |
123 | return CachedResource::Type::ImageResource; |
124 | if (equalLettersIgnoringASCIICase(as, "script" )) |
125 | return CachedResource::Type::Script; |
126 | if (equalLettersIgnoringASCIICase(as, "style" )) |
127 | return CachedResource::Type::CSSStyleSheet; |
128 | if (RuntimeEnabledFeatures::sharedFeatures().mediaPreloadingEnabled() && (equalLettersIgnoringASCIICase(as, "video" ) || equalLettersIgnoringASCIICase(as, "audio" ))) |
129 | return CachedResource::Type::MediaResource; |
130 | if (equalLettersIgnoringASCIICase(as, "font" )) |
131 | return CachedResource::Type::FontResource; |
132 | #if ENABLE(VIDEO_TRACK) |
133 | if (equalLettersIgnoringASCIICase(as, "track" )) |
134 | return CachedResource::Type::TextTrackResource; |
135 | #endif |
136 | return WTF::nullopt; |
137 | } |
138 | |
139 | static std::unique_ptr<LinkPreloadResourceClient> createLinkPreloadResourceClient(CachedResource& resource, LinkLoader& loader) |
140 | { |
141 | switch (resource.type()) { |
142 | case CachedResource::Type::ImageResource: |
143 | return std::make_unique<LinkPreloadImageResourceClient>(loader, downcast<CachedImage>(resource)); |
144 | case CachedResource::Type::Script: |
145 | return std::make_unique<LinkPreloadDefaultResourceClient>(loader, downcast<CachedScript>(resource)); |
146 | case CachedResource::Type::CSSStyleSheet: |
147 | return std::make_unique<LinkPreloadStyleResourceClient>(loader, downcast<CachedCSSStyleSheet>(resource)); |
148 | case CachedResource::Type::FontResource: |
149 | return std::make_unique<LinkPreloadFontResourceClient>(loader, downcast<CachedFont>(resource)); |
150 | #if ENABLE(VIDEO_TRACK) |
151 | case CachedResource::Type::TextTrackResource: |
152 | return std::make_unique<LinkPreloadDefaultResourceClient>(loader, downcast<CachedTextTrack>(resource)); |
153 | #endif |
154 | case CachedResource::Type::MediaResource: |
155 | ASSERT(RuntimeEnabledFeatures::sharedFeatures().mediaPreloadingEnabled()); |
156 | FALLTHROUGH; |
157 | case CachedResource::Type::RawResource: |
158 | return std::make_unique<LinkPreloadRawResourceClient>(loader, downcast<CachedRawResource>(resource)); |
159 | case CachedResource::Type::MainResource: |
160 | case CachedResource::Type::Icon: |
161 | #if ENABLE(SVG_FONTS) |
162 | case CachedResource::Type::SVGFontResource: |
163 | #endif |
164 | case CachedResource::Type::SVGDocumentResource: |
165 | #if ENABLE(XSLT) |
166 | case CachedResource::Type::XSLStyleSheet: |
167 | #endif |
168 | case CachedResource::Type::Beacon: |
169 | case CachedResource::Type::Ping: |
170 | case CachedResource::Type::LinkPrefetch: |
171 | #if ENABLE(APPLICATION_MANIFEST) |
172 | case CachedResource::Type::ApplicationManifest: |
173 | #endif |
174 | // None of these values is currently supported as an `as` value. |
175 | ASSERT_NOT_REACHED(); |
176 | } |
177 | return nullptr; |
178 | } |
179 | |
180 | bool LinkLoader::isSupportedType(CachedResource::Type resourceType, const String& mimeType) |
181 | { |
182 | if (mimeType.isEmpty()) |
183 | return true; |
184 | switch (resourceType) { |
185 | case CachedResource::Type::ImageResource: |
186 | return MIMETypeRegistry::isSupportedImageVideoOrSVGMIMEType(mimeType); |
187 | case CachedResource::Type::Script: |
188 | return MIMETypeRegistry::isSupportedJavaScriptMIMEType(mimeType); |
189 | case CachedResource::Type::CSSStyleSheet: |
190 | return MIMETypeRegistry::isSupportedStyleSheetMIMEType(mimeType); |
191 | case CachedResource::Type::FontResource: |
192 | return MIMETypeRegistry::isSupportedFontMIMEType(mimeType); |
193 | case CachedResource::Type::MediaResource: |
194 | if (!RuntimeEnabledFeatures::sharedFeatures().mediaPreloadingEnabled()) |
195 | ASSERT_NOT_REACHED(); |
196 | return MIMETypeRegistry::isSupportedMediaMIMEType(mimeType); |
197 | |
198 | #if ENABLE(VIDEO_TRACK) |
199 | case CachedResource::Type::TextTrackResource: |
200 | return MIMETypeRegistry::isSupportedTextTrackMIMEType(mimeType); |
201 | #endif |
202 | case CachedResource::Type::RawResource: |
203 | #if ENABLE(APPLICATION_MANIFEST) |
204 | case CachedResource::Type::ApplicationManifest: |
205 | #endif |
206 | return true; |
207 | default: |
208 | ASSERT_NOT_REACHED(); |
209 | } |
210 | return false; |
211 | } |
212 | |
213 | void LinkLoader::preconnectIfNeeded(const LinkRelAttribute& relAttribute, const URL& href, Document& document, const String& crossOrigin) |
214 | { |
215 | if (!relAttribute.isLinkPreconnect || !href.isValid() || !href.protocolIsInHTTPFamily() || !document.frame()) |
216 | return; |
217 | ASSERT(document.settings().linkPreconnectEnabled()); |
218 | StoredCredentialsPolicy storageCredentialsPolicy = StoredCredentialsPolicy::Use; |
219 | if (equalIgnoringASCIICase(crossOrigin, "anonymous" ) && document.securityOrigin().canAccess(SecurityOrigin::create(href))) |
220 | storageCredentialsPolicy = StoredCredentialsPolicy::DoNotUse; |
221 | ASSERT(document.frame()->loader().networkingContext()); |
222 | platformStrategies()->loaderStrategy()->preconnectTo(document.frame()->loader(), href, storageCredentialsPolicy, [weakDocument = makeWeakPtr(document), href](ResourceError error) { |
223 | if (!weakDocument) |
224 | return; |
225 | |
226 | if (!error.isNull()) |
227 | weakDocument->addConsoleMessage(MessageSource::Network, MessageLevel::Error, makeString("Failed to preconnect to "_s , href.string(), ". Error: "_s , error.localizedDescription())); |
228 | else |
229 | weakDocument->addConsoleMessage(MessageSource::Network, MessageLevel::Info, makeString("Successfuly preconnected to "_s , href.string())); |
230 | }); |
231 | } |
232 | |
233 | std::unique_ptr<LinkPreloadResourceClient> LinkLoader::preloadIfNeeded(const LinkRelAttribute& relAttribute, const URL& href, Document& document, const String& as, const String& media, const String& mimeType, const String& crossOriginMode, const String& imageSrcSet, const String& imageSizes, LinkLoader* loader) |
234 | { |
235 | if (!document.loader() || !relAttribute.isLinkPreload) |
236 | return nullptr; |
237 | |
238 | ASSERT(RuntimeEnabledFeatures::sharedFeatures().linkPreloadEnabled()); |
239 | auto type = LinkLoader::resourceTypeFromAsAttribute(as); |
240 | if (!type) { |
241 | document.addConsoleMessage(MessageSource::Other, MessageLevel::Error, "<link rel=preload> must have a valid `as` value"_s ); |
242 | return nullptr; |
243 | } |
244 | URL url; |
245 | if (type == CachedResource::Type::ImageResource && !imageSrcSet.isEmpty()) { |
246 | auto sourceSize = SizesAttributeParser(imageSizes, document).length(); |
247 | auto candidate = bestFitSourceForImageAttributes(document.deviceScaleFactor(), href.string(), imageSrcSet, sourceSize); |
248 | url = document.completeURL(URL({ }, candidate.string.toString())); |
249 | } else |
250 | url = document.completeURL(href); |
251 | |
252 | if (!url.isValid()) { |
253 | if (imageSrcSet.isEmpty()) |
254 | document.addConsoleMessage(MessageSource::Other, MessageLevel::Error, "<link rel=preload> has an invalid `href` value"_s ); |
255 | else |
256 | document.addConsoleMessage(MessageSource::Other, MessageLevel::Error, "<link rel=preload> has an invalid `imagesrcset` value"_s ); |
257 | return nullptr; |
258 | } |
259 | if (!MediaQueryEvaluator::mediaAttributeMatches(document, media)) |
260 | return nullptr; |
261 | if (!isSupportedType(type.value(), mimeType)) |
262 | return nullptr; |
263 | |
264 | auto options = CachedResourceLoader::defaultCachedResourceOptions(); |
265 | auto linkRequest = createPotentialAccessControlRequest(url, document, crossOriginMode, WTFMove(options)); |
266 | linkRequest.setPriority(CachedResource::defaultPriorityForResourceType(type.value())); |
267 | linkRequest.setInitiator("link" ); |
268 | linkRequest.setIgnoreForRequestCount(true); |
269 | linkRequest.setIsLinkPreload(); |
270 | |
271 | auto cachedLinkResource = document.cachedResourceLoader().preload(type.value(), WTFMove(linkRequest)).value_or(nullptr); |
272 | |
273 | if (cachedLinkResource && cachedLinkResource->type() != *type) |
274 | return nullptr; |
275 | |
276 | if (cachedLinkResource && loader) |
277 | return createLinkPreloadResourceClient(*cachedLinkResource, *loader); |
278 | return nullptr; |
279 | } |
280 | |
281 | void LinkLoader::prefetchIfNeeded(const LinkRelAttribute& relAttribute, const URL& href, Document& document) |
282 | { |
283 | if (!relAttribute.isLinkPrefetch || !href.isValid() || !document.frame() || !m_client.shouldLoadLink()) |
284 | return; |
285 | |
286 | ASSERT(RuntimeEnabledFeatures::sharedFeatures().linkPrefetchEnabled()); |
287 | Optional<ResourceLoadPriority> priority; |
288 | CachedResource::Type type = CachedResource::Type::LinkPrefetch; |
289 | |
290 | if (m_cachedLinkResource) { |
291 | m_cachedLinkResource->removeClient(*this); |
292 | m_cachedLinkResource = nullptr; |
293 | } |
294 | // FIXME: Add further prefetch restrictions/limitations: |
295 | // - third-party iframes cannot trigger prefetches |
296 | // - Number of prefetches of a given page is limited (to 1 maybe?) |
297 | ResourceLoaderOptions options = CachedResourceLoader::defaultCachedResourceOptions(); |
298 | options.contentSecurityPolicyImposition = ContentSecurityPolicyImposition::SkipPolicyCheck; |
299 | options.certificateInfoPolicy = CertificateInfoPolicy::IncludeCertificateInfo; |
300 | options.credentials = FetchOptions::Credentials::SameOrigin; |
301 | options.redirect = FetchOptions::Redirect::Manual; |
302 | options.mode = FetchOptions::Mode::Navigate; |
303 | options.serviceWorkersMode = ServiceWorkersMode::None; |
304 | options.cachingPolicy = CachingPolicy::DisallowCaching; |
305 | m_cachedLinkResource = document.cachedResourceLoader().requestLinkResource(type, CachedResourceRequest(ResourceRequest(document.completeURL(href)), options, priority)).value_or(nullptr); |
306 | if (m_cachedLinkResource) |
307 | m_cachedLinkResource->addClient(*this); |
308 | } |
309 | |
310 | void LinkLoader::cancelLoad() |
311 | { |
312 | if (m_preloadResourceClient) |
313 | m_preloadResourceClient->clear(); |
314 | } |
315 | |
316 | bool LinkLoader::loadLink(const LinkRelAttribute& relAttribute, const URL& href, const String& as, const String& media, const String& mimeType, const String& crossOrigin, const String& imageSrcSet, const String& imageSizes, Document& document) |
317 | { |
318 | if (relAttribute.isDNSPrefetch) { |
319 | // FIXME: The href attribute of the link element can be in "//hostname" form, and we shouldn't attempt |
320 | // to complete that as URL <https://bugs.webkit.org/show_bug.cgi?id=48857>. |
321 | if (document.settings().dnsPrefetchingEnabled() && href.isValid() && !href.isEmpty() && document.frame()) |
322 | document.frame()->loader().client().prefetchDNS(href.host().toString()); |
323 | } |
324 | |
325 | preconnectIfNeeded(relAttribute, href, document, crossOrigin); |
326 | |
327 | if (m_client.shouldLoadLink()) { |
328 | auto resourceClient = preloadIfNeeded(relAttribute, href, document, as, media, mimeType, crossOrigin, imageSrcSet, imageSizes, this); |
329 | if (m_preloadResourceClient) |
330 | m_preloadResourceClient->clear(); |
331 | if (resourceClient) |
332 | m_preloadResourceClient = WTFMove(resourceClient); |
333 | } |
334 | |
335 | prefetchIfNeeded(relAttribute, href, document); |
336 | |
337 | return true; |
338 | } |
339 | |
340 | } |
341 | |