/** * Utilities for initiating prefetch via speculation rules. */ // Resolved URL to find this script. const SR_PREFETCH_UTILS_URL = new URL(document.currentScript.src, document.baseURI); // Hostname for cross origin urls. const PREFETCH_PROXY_BYPASS_HOST = "{{hosts[alt][]}}"; class PrefetchAgent extends RemoteContext { constructor(uuid, t) { super(uuid); this.t = t; } getExecutorURL(options = {}) { let {hostname, username, password, protocol, executor, ...extra} = options; let params = new URLSearchParams({uuid: this.context_id, ...extra}); if(executor === undefined) { executor = "executor.sub.html"; } let url = new URL(`${executor}?${params}`, SR_PREFETCH_UTILS_URL); if(hostname !== undefined) { url.hostname = hostname; } if(username !== undefined) { url.username = username; } if(password !== undefined) { url.password = password; } if(protocol !== undefined) { url.protocol = protocol; url.port = protocol === "https" ? "{{ports[https][0]}}" : "{{ports[http][0]}}"; } return url; } // Requests prefetch via speculation rules. // // In the future, this should also use browser hooks to force the prefetch to // occur despite heuristic matching, etc., and await the completion of the // prefetch. async forceSinglePrefetch(url, extra = {}, wait_for_completion = true) { await this.execute_script((url, extra) => { insertSpeculationRules({ prefetch: [{source: 'list', urls: [url], ...extra}] }); }, [url, extra]); if (!wait_for_completion) { return Promise.resolve(); } return new Promise(resolve => this.t.step_timeout(resolve, 2000)); } // `url` is the URL to navigate. // // `expectedDestinationUrl` is the expected URL after navigation. // When omitted, `url` is used. async navigate(url, {expectedDestinationUrl} = {}) { await this.execute_script((url) => { window.executor.suspend(() => { location.href = url; }); }, [url]); if (!expectedDestinationUrl) { expectedDestinationUrl = url; } expectedDestinationUrl.username = ''; expectedDestinationUrl.password = ''; assert_equals( await this.execute_script(() => location.href), expectedDestinationUrl.toString(), "expected navigation to reach destination URL"); await this.execute_script(() => {}); } async getRequestHeaders() { return this.execute_script(() => requestHeaders); } async getResponseCookies() { return this.execute_script(() => { let cookie = {}; document.cookie.split(/\s*;\s*/).forEach((kv)=>{ let [key, value] = kv.split(/\s*=\s*/); cookie[key] = value; }); return cookie; }); } async getRequestCookies() { return this.execute_script(() => window.requestCookies); } async getRequestCredentials() { return this.execute_script(() => window.requestCredentials); } async setReferrerPolicy(referrerPolicy) { return this.execute_script(referrerPolicy => { const meta = document.createElement("meta"); meta.name = "referrer"; meta.content = referrerPolicy; document.head.append(meta); }, [referrerPolicy]); } async getDeliveryType(){ return this.execute_script(() => { return performance.getEntriesByType("navigation")[0].deliveryType; }); } } // Produces a URL with a UUID which will record when it's prefetched. // |extra_params| can be specified to add extra search params to the generated // URL. function getPrefetchUrl(extra_params={}) { let params = new URLSearchParams({ uuid: token(), ...extra_params }); return new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL); } // Produces n URLs with unique UUIDs which will record when they are prefetched. function getPrefetchUrlList(n) { return Array.from({ length: n }, () => getPrefetchUrl()); } async function isUrlPrefetched(url) { let response = await fetch(url, {redirect: 'follow'}); assert_true(response.ok); return response.json(); } // Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this. async function spawnWindowWithReference(t, options = {}, uuid = token()) { let agent = new PrefetchAgent(uuid, t); let w = window.open(agent.getExecutorURL(options), '_blank', options); t.add_cleanup(() => w.close()); return {"agent":agent, "window":w}; } // Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this. async function spawnWindow(t, options = {}, uuid = token()) { let agent_window_pair = await spawnWindowWithReference(t, options, uuid); return agent_window_pair.agent; } function insertSpeculationRules(body) { let script = document.createElement('script'); script.type = 'speculationrules'; script.textContent = JSON.stringify(body); document.head.appendChild(script); } // Creates and appends to |insertion point|. If // |insertion_point| is not specified, document.body is used. function addLink(href, insertion_point=document.body) { const a = document.createElement('a'); a.href = href; insertion_point.appendChild(a); return a; } // Inserts a prefetch document rule with |predicate|. |predicate| can be // undefined, in which case the default predicate will be used (i.e. all links // in document will match). function insertDocumentRule(predicate, extra_options={}) { insertSpeculationRules({ prefetch: [{ source: 'document', eagerness: 'eager', where: predicate, ...extra_options }] }); } function assert_prefetched (requestHeaders, description) { assert_in_array(requestHeaders.purpose, ["", "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'."); assert_in_array(requestHeaders.sec_purpose, ["prefetch", "prefetch;anonymous-client-ip"], description); } function assert_not_prefetched (requestHeaders, description){ assert_equals(requestHeaders.purpose, "", description); assert_equals(requestHeaders.sec_purpose, "", description); } // Use nvs_header query parameter to ask the wpt server // to populate No-Vary-Search response header. function addNoVarySearchHeaderUsingQueryParam(url, value){ if(value){ url.searchParams.append("nvs_header", value); } }