summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fenced-frame/resources/utils.js
blob: 4638f37cbb1576e4b85e98616cf1c159a79e3d95 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
const STORE_URL = '/fenced-frame/resources/key-value-store.py';
const BEACON_URL = '/fenced-frame/resources/automatic-beacon-store.py';
const REMOTE_EXECUTOR_URL = '/fenced-frame/resources/remote-context-executor.https.html';
const FLEDGE_BIDDING_URL = '/fenced-frame/resources/fledge-bidding-logic.js';
const FLEDGE_BIDDING_WITH_SIZE_URL = '/fenced-frame/resources/fledge-bidding-logic-with-size.js';
const FLEDGE_DECISION_URL = '/fenced-frame/resources/fledge-decision-logic.js';

// Creates a URL that includes a list of stash key UUIDs that are being used
// in the test. This allows us to generate UUIDs on the fly and let anything
// (iframes, fenced frames, pop-ups, etc...) that wouldn't have access to the
// original UUID variable know what the UUIDs are.
// @param {string} href - The base url of the page being navigated to
// @param {string list} keylist - The list of key UUIDs to be used. Note that
//                                order matters when extracting the keys
function generateURL(href, keylist) {
  const ret_url = new URL(href, location.href);
  ret_url.searchParams.append("keylist", keylist.join(','));
  return ret_url;
}

function getRemoteContextURL(origin) {
  return new URL(REMOTE_EXECUTOR_URL, origin);
}

async function runSelectRawURL(href, resolve_to_config = false) {
  try {
    await sharedStorage.worklet.addModule(
      "/shared-storage/resources/simple-module.js");
  } catch (e) {
    // Shared Storage needs to have a module added before we can operate on it.
    // It is generated on the fly with this call, and since there's no way to
    // tell through the API if a module already exists, wrap the addModule call
    // in a try/catch so that if it runs a second time in a test, it will
    // gracefully fail rather than bring the whole test down.
  }
  return await sharedStorage.selectURL(
      'test-url-selection-operation', [{url: href}], {
        data: {'mockResult': 0},
        resolveToConfig: resolve_to_config,
        keepAlive: true
      });
}

// Similar to generateURL, but creates
// 1. An urn:uuid if `resolve_to_config` is false.
// 2. A fenced frame config object if `resolve_to_config` is true.
// This relies on a mock Shared Storage auction, since it is the simplest
// WP-exposed way to turn a url into an urn:uuid or a fenced frame config.
// Note: this function, unlike generateURL, is asynchronous and needs to be
// called with an await operator.
// @param {string} href - The base url of the page being navigated to
// @param {string list} keylist - The list of key UUIDs to be used. Note that
//                                order matters when extracting the keys
// @param {boolean} [resolve_to_config = false] - Determines whether the result
//                                                of `sharedStorage.selectURL()`
//                                                is an urn:uuid or a fenced
//                                                frame config.
// Note:
// 1. There is a limit of 3 calls per origin per pageload for
// `sharedStorage.selectURL()`, so `runSelectURL()` must also respect this
// limit.
// 2. If `resolve_to_config` is true, blink feature `FencedFramesAPIChanges`
// needs to be enabled for `selectURL()` to return a fenced frame config.
// Otherwise `selectURL()` will fall back to the old behavior that returns an
// urn:uuid.
async function runSelectURL(href, keylist = [], resolve_to_config = false) {
  const full_url = generateURL(href, keylist);
  return await runSelectRawURL(full_url, resolve_to_config);
}

async function generateURNFromFledgeRawURL(href,
  nested_urls,
  resolve_to_config = false,
  ad_with_size = false,
  requested_size = null) {
  const bidding_token = token();
  const seller_token = token();

  const ad_components_list = nested_urls.map((url) => {
    return ad_with_size ?
      { renderUrl: url, sizeGroup: "group1" } :
      { renderUrl: url }
  });

  const interestGroup = ad_with_size ?
    {
      name: 'testAd1',
      owner: location.origin,
      biddingLogicUrl: new URL(FLEDGE_BIDDING_WITH_SIZE_URL, location.origin),
      ads: [{ renderUrl: href, sizeGroup: "group1", bid: 1 }],
      userBiddingSignals: { biddingToken: bidding_token },
      trustedBiddingSignalsKeys: ['key1'],
      adComponents: ad_components_list,
      adSizes: { "size1": { width: "100px", height: "50px" } },
      sizeGroups: { "group1": ["size1"] }
    } :
    {
      name: 'testAd1',
      owner: location.origin,
      biddingLogicUrl: new URL(FLEDGE_BIDDING_URL, location.origin),
      ads: [{ renderUrl: href, bid: 1 }],
      userBiddingSignals: { biddingToken: bidding_token },
      trustedBiddingSignalsKeys: ['key1'],
      adComponents: ad_components_list,
    };

  // Pick an arbitrarily high duration to guarantee that we never leave the
  // ad interest group while the test runs.
  navigator.joinAdInterestGroup(interestGroup, /*durationSeconds=*/3000000);

  let auctionConfig = {
    seller: location.origin,
    interestGroupBuyers: [location.origin],
    decisionLogicUrl: new URL(FLEDGE_DECISION_URL, location.origin),
    auctionSignals: {biddingToken: bidding_token, sellerToken: seller_token},
    resolveToConfig: resolve_to_config
  };
  if (requested_size) {
    auctionConfig['requestedSize'] = {width: requested_size[0], height: requested_size[1]};
  }
  return navigator.runAdAuction(auctionConfig);
}

// Similar to runSelectURL, but uses FLEDGE instead of Shared Storage as the
// auctioning tool.
// Note: this function, unlike generateURL, is asynchronous and needs to be
// called with an await operator. @param {string} href - The base url of the
// page being navigated to @param {string list} keylist - The list of key UUIDs
// to be used. Note that order matters when extracting the keys
// @param {string} href - The base url of the page being navigated to
// @param {string list} keylist - The list of key UUIDs to be used. Note that
//                                order matters when extracting the keys
// @param {string list} nested_urls - A list of urls that will eventually become
//                                    the nested configs/ad components
// @param {boolean} [resolve_to_config = false] - Determines whether the result
//                                                of `navigator.runAdAuction()`
//                                                is an urn:uuid or a fenced
//                                                frame config.
// @param {boolean} [ad_with_size = false] - Determines whether the auction is
//                                           run with ad sizes specified.
async function generateURNFromFledge(href, keylist, nested_urls=[], resolve_to_config = false, ad_with_size = false, requested_size = null) {
  const full_url = generateURL(href, keylist);
  return generateURNFromFledgeRawURL(full_url, nested_urls, resolve_to_config, ad_with_size, requested_size);
}

// Extracts a list of UUIDs from the from the current page's URL.
// @returns {string list} - The list of UUIDs extracted from the page. This can
//                          be read into multiple variables using the
//                          [key1, key2, etc...] = parseKeyList(); pattern.
function parseKeylist() {
  const url = new URL(location.href);
  const keylist = url.searchParams.get("keylist");
  return keylist.split(',');
}

// Converts a same-origin URL to a cross-origin URL
// @param {URL} url - The URL object whose origin is being converted
// @param {boolean} [https=true] - Whether or not to use the HTTPS origin
//
// @returns {URL} The new cross-origin URL
function getRemoteOriginURL(url, https=true) {
  const same_origin = location.origin;
  const cross_origin = https ? get_host_info().HTTPS_REMOTE_ORIGIN
      : get_host_info().HTTP_REMOTE_ORIGIN;
  return new URL(url.toString().replace(same_origin, cross_origin));
}

// Builds a URL to be used as a remote context executor.
function generateRemoteContextURL(headers, origin) {
  // Generate the unique id for the parent/child channel.
  const uuid = token();

  // Use the absolute path of the remote context executor source file, so that
  // nested contexts will work.
  const url = getRemoteContextURL(origin ? origin : location.origin);
  url.searchParams.append('uuid', uuid);

  // Add the header to allow loading in a fenced frame.
  headers.push(["Supports-Loading-Mode", "fenced-frame"]);

  // Transform the headers into the expected format.
  // https://web-platform-tests.org/writing-tests/server-pipes.html#headers
  function escape(s) {
    return s.replace('(', '\\(').replace(')', '\\)');
  }
  const formatted_headers = headers.map((header) => {
    return `header(${escape(header[0])}, ${escape(header[1])})`;
  });
  url.searchParams.append('pipe', formatted_headers.join('|'));

  return [uuid, url];
}

function buildRemoteContextForObject(object, uuid, html) {
  // https://github.com/web-platform-tests/wpt/blob/master/common/dispatcher/README.md
  const context = new RemoteContext(uuid);
  if (html) {
    context.execute_script(
      (html_source) => {
        document.body.insertAdjacentHTML('beforebegin', html_source);
      },
    [html]);
  }

  // We need a little bit of boilerplate in the handlers because Proxy doesn't
  // work so nicely with HTML elements.
  const handler = {
    get: (target, key) => {
      if (key == "execute") {
        return context.execute_script;
      }
      if (key == "element") {
        return object;
      }
      if (key in target) {
        return target[key];
      }
      return context[key];
    },
    set: (target, key, value) => {
      target[key] = value;
      return value;
    }
  };

  const proxy = new Proxy(object, handler);
  return proxy;
}

// Attaches an object that waits for scripts to execute from RemoteContext.
// (In practice, this is either a frame or a window.)
// Returns a proxy for the object that first resolves to the object itself,
// then resolves to the RemoteContext if the property isn't found.
// The proxy also has an extra attribute `execute`, which is an alias for the
// remote context's `execute_script(fn, args=[])`.
function attachContext(object_constructor, html, headers, origin) {
  const [uuid, url] = generateRemoteContextURL(headers, origin);
  const object = object_constructor(url);
  return buildRemoteContextForObject(object, uuid, html);
}

// TODO(crbug.com/1347953): Update this function to also test
// `sharedStorage.selectURL()` that returns a fenced frame config object.
// This should be done after fixing the following flaky tests that use this
// function.
// 1. crbug.com/1372536: resize-lock-input.https.html
// 2. crbug.com/1394559: unfenced-top.https.html
async function attachOpaqueContext(generator_api, resolve_to_config, ad_with_size, requested_size, object_constructor, html, headers, origin) {
  const [uuid, url] = generateRemoteContextURL(headers, origin);
  const id = await (generator_api == 'fledge' ? generateURNFromFledge(url, [], [], resolve_to_config, ad_with_size, requested_size) : runSelectURL(url, [], resolve_to_config));
  const object = object_constructor(id);
  return buildRemoteContextForObject(object, uuid, html);
}

function attachPotentiallyOpaqueContext(generator_api, resolve_to_config, ad_with_size, requested_size, frame_constructor, html, headers, origin) {
  generator_api = generator_api.toLowerCase();
  if (generator_api == 'fledge' || generator_api == 'sharedstorage') {
    return attachOpaqueContext(generator_api, resolve_to_config, ad_with_size, requested_size, frame_constructor, html, headers, origin);
  } else {
    return attachContext(frame_constructor, html, headers, origin);
  }
}

function attachFrameContext(element_name, generator_api, resolve_to_config, ad_with_size, requested_size, html, headers, attributes, origin) {
  frame_constructor = (id) => {
    frame = document.createElement(element_name);
    attributes.forEach(attribute => {
      frame.setAttribute(attribute[0], attribute[1]);
    });
    if (element_name == "iframe") {
      frame.src = id;
    } else if (id instanceof FencedFrameConfig) {
      frame.config = id;
    } else {
      const config = new FencedFrameConfig(id);
      frame.config = config;
    }
    document.body.append(frame);
    return frame;
  };
  return attachPotentiallyOpaqueContext(generator_api, resolve_to_config, ad_with_size, requested_size, frame_constructor, html, headers, origin);
}

function replaceFrameContext(frame_proxy, {generator_api="", resolve_to_config=false, ad_with_size=false, requested_size=null, html="", headers=[], origin=""}={}) {
  frame_constructor = (id) => {
    if (frame_proxy.element.nodeName == "IFRAME") {
      frame_proxy.element.src = id;
    } else if (id instanceof FencedFrameConfig) {
      frame_proxy.element.config = id;
    } else {
      const config = new FencedFrameConfig(id);
      frame_proxy.element.config = config;
    }
    return frame_proxy.element;
  };
  return attachPotentiallyOpaqueContext(generator_api, resolve_to_config, ad_with_size, requested_size, frame_constructor, html, headers, origin);
}

// Attach a fenced frame that waits for scripts to execute.
// Takes as input a(n optional) dictionary of configs:
// - generator_api: the name of the API that should generate the urn/config.
//    Supports (case-insensitive) "fledge" and "sharedstorage", or any other
//    value as a default.
//    If you generate a urn, then you need to await the result of this function.
// - resolve_to_config: whether a config should be used. (currently only works
//    for FLEDGE and sharedStorage generator_api)
// - ad_with_size: whether an ad auction is run with size specified for the ads
//    and ad components. (currently only works for FLEDGE)
// - requested_size: A 2-element list with the width and height for
//    requestedSize in the FLEDGE auction config.
// - html: extra HTML source code to inject into the loaded frame
// - headers: an array of header pairs [[key, value], ...]
// - attributes: an array of attribute pairs to set on the frame [[key, value], ...]
// - origin: origin of the url, default to location.origin if not set
// Returns a proxy that acts like the frame HTML element, but with an extra
// function `execute`. See `attachFrameContext` or the README for more details.
function attachFencedFrameContext({generator_api="", resolve_to_config=false, ad_with_size=false, requested_size=null, html = "", headers=[], attributes=[], origin=""}={}) {
  return attachFrameContext('fencedframe', generator_api, resolve_to_config, ad_with_size, requested_size, html, headers, attributes, origin);
}

// Attach an iframe that waits for scripts to execute.
// See `attachFencedFrameContext` for more details.
function attachIFrameContext({generator_api="", html="", headers=[], attributes=[], origin=""}={}) {
  return attachFrameContext('iframe', generator_api, resolve_to_config=false, ad_with_size=false, requested_size=null, html, headers, attributes, origin);
}

// Open a window that waits for scripts to execute.
// Returns a proxy that acts like the window object, but with an extra
// function `execute`. See `attachContext` for more details.
function attachWindowContext({target="_blank", html="", headers=[], origin=""}={}) {
  window_constructor = (url) => {
    return window.open(url, target);
  }

  return attachContext(window_constructor, html, headers, origin);
}

// Converts a key string into a key uuid using a cryptographic hash function.
// This function only works in secure contexts (HTTPS).
async function stringToStashKey(string) {
  // Compute a SHA-256 hash of the input string, and convert it to hex.
  const data = new TextEncoder().encode(string);
  const digest = await crypto.subtle.digest('SHA-256', data);
  const digest_array = Array.from(new Uint8Array(digest));
  const digest_as_hex = digest_array.map(b => b.toString(16).padStart(2, '0')).join('');

  // UUIDs are structured as 8X-4X-4X-4X-12X.
  // Use the first 32 hex digits and ignore the rest.
  const digest_slices = [digest_as_hex.slice(0,8),
                         digest_as_hex.slice(8,12),
                         digest_as_hex.slice(12,16),
                         digest_as_hex.slice(16,20),
                         digest_as_hex.slice(20,32)];
  return digest_slices.join('-');
}

// Create a fenced frame. Then navigate it using the given `target`, which can
// be either an urn:uuid or a fenced frame config object.
function attachFencedFrame(target) {
  assert_implements(
      window.HTMLFencedFrameElement,
      'The HTMLFencedFrameElement should be exposed on the window object');

  const fenced_frame = document.createElement('fencedframe');

  if (target instanceof FencedFrameConfig) {
    fenced_frame.config = target;
  } else {
    const config = new FencedFrameConfig(target);
    fenced_frame.config = config;
  }

  document.body.append(fenced_frame);
  return fenced_frame;
}

function attachIFrame(url) {
  const iframe = document.createElement('iframe');
  iframe.src = url;
  document.body.append(iframe);
  return iframe;
}

// Reads the value specified by `key` from the key-value store on the server.
async function readValueFromServer(key) {
  // Resolve the key if it is a Promise.
  key = await key;

  const serverUrl = `${STORE_URL}?key=${key}`;
  const response = await fetch(serverUrl);
  if (!response.ok)
    throw new Error('An error happened in the server');
  const value = await response.text();

  // The value is not stored in the server.
  if (value === "<Not set>")
    return { status: false };

  return { status: true, value: value };
}

// Convenience wrapper around the above getter that will wait until a value is
// available on the server.
async function nextValueFromServer(key) {
  // Resolve the key if it is a Promise.
  key = await key;

  while (true) {
    // Fetches the test result from the server.
    const { status, value } = await readValueFromServer(key);
    if (!status) {
      // The test result has not been stored yet. Retry after a while.
      await new Promise(resolve => setTimeout(resolve, 20));
      continue;
    }

    return value;
  }
}

// Reads the data from the latest automatic beacon sent to the server.
async function readAutomaticBeaconDataFromServer() {
  const serverUrl = `${BEACON_URL}`;
  const response = await fetch(serverUrl);
  if (!response.ok)
    throw new Error('An error happened in the server');
  const value = await response.text();

  // The value is not stored in the server.
  if (value === "<Not set>")
    return { status: false };

  return { status: true, value: value };
}

// Convenience wrapper around the above getter that will wait until a value is
// available on the server.
async function nextAutomaticBeacon() {
  while (true) {
    // Fetches the test result from the server.
    const { status, value } = await readAutomaticBeaconDataFromServer();
    if (!status) {
      // The test result has not been stored yet. Retry after a while.
      await new Promise(resolve => setTimeout(resolve, 20));
      continue;
    }

    return value;
  }
}

// Writes `value` for `key` in the key-value store on the server.
async function writeValueToServer(key, value, origin = '') {
  // Resolve the key if it is a Promise.
  key = await key;

  const serverUrl = `${origin}${STORE_URL}?key=${key}&value=${value}`;
  await fetch(serverUrl, {"mode": "no-cors"});
}

// Simulates a user gesture.
async function simulateGesture() {
  // Wait until the window size is initialized.
  while (window.innerWidth == 0) {
    await new Promise(resolve => requestAnimationFrame(resolve));
  }
  await test_driver.bless('simulate gesture');
}

// Fenced frames are always put in the public IP address space which is the
// least privileged. In case a navigation to a local data: URL or blob: URL
// resource is allowed, they would only be able to fetch things that are *also*
// in the public IP address space. So for the document described by these local
// URLs, we'll set them up to only communicate back to the outer page via
// resources obtained in the public address space.
function createLocalSource(key, url) {
  return `
    <head>
      <script src="${url}"><\/script>
    </head>
    <body>
      <script>
        writeValueToServer("${key}", "LOADED", /*origin=*/"${url.origin}");
      <\/script>
    </body>
  `;
}

function setupCSP(csp, second_csp=null) {
  let meta = document.createElement('meta');
  meta.httpEquiv = "Content-Security-Policy";
  meta.content = "fenced-frame-src " + csp;
  document.head.appendChild(meta);

  if (second_csp != null) {
    let second_meta = document.createElement('meta');
    second_meta.httpEquiv = "Content-Security-Policy";
    second_meta.content = "frame-src " + second_csp;
    document.head.appendChild(second_meta);
  }
}