Skip to content

Commit

Permalink
Add cross-origin window and location wrappers
Browse files Browse the repository at this point in the history
Closes dart-lang/sdk#54443
Closes dart-lang#247
Closes dart-lang/sdk#54938

Since cross-origin objects have limitations around
access, wrappers are introduced to do the only safe
operations. Extension methods are added to get instances
of these wrappers.
  • Loading branch information
srujzs committed Aug 24, 2024
1 parent 49f3e6d commit 1e17c5a
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 0 deletions.
5 changes: 5 additions & 0 deletions web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
`dart:io` and `dart:html`.
- Added `JSImmutableListWrapper` which helps create a dart list from a JS list.
- Deprecated `TouchListWrapper` and `TouchListConvert` in favor of `JSImmutableListWrapper`.
- Added `CrossOriginWindow` and `CrossOriginLocation` wrappers for cross-origin
windows and locations, respectively, that can be accessed through
`HTMLIFrameElement.contentWindowCrossOrigin`, `Window.openCrossOrigin`,
`Window.openerCrossOrigin`, `Window.topCrossOrigin`,
and `Window.parentCrossOrigin`.

## 1.0.0

Expand Down
1 change: 1 addition & 0 deletions web/lib/src/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'dart:js_interop_unsafe';
import 'dom.dart';
import 'helpers/lists.dart';

export 'helpers/cross_origin.dart' show CrossOriginLocation, CrossOriginWindow;
export 'helpers/enums.dart';
export 'helpers/events/events.dart';
export 'helpers/events/providers.dart';
Expand Down
184 changes: 184 additions & 0 deletions web/lib/src/helpers/cross_origin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:js_interop';

import '../dom.dart' show HTMLIFrameElement, Location, Window;

// Includes all the allowed and necessary APIs from the W3 spec located here:
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#cross-origin-objects
// Some browsers may provide more access. These APIs are exposed as
//`_DOMWindowCrossFrame` and `_LocationCrossFrame` in `dart:html`.

// The Dart runtime does not allow this to be typed as any better than `JSAny?`.
extension type _CrossOriginWindow(JSAny? any) {
external bool get closed;
external int get length;
// While you can set the location to a string value, this is the same as
// `location.href`, so we only allow the getter to avoid a
// `getter_not_subtype_setter_types` error.
external JSAny? get location;
external JSAny? get opener;
external JSAny? get parent;
external JSAny? get top;
// `frames`, `self`, and `window` are all supported for cross-origin windows,
// but simply return the calling window, so there's no use in supporting them
// for interop.
external void blur();
external void close();
external void focus();
external void postMessage(
JSAny? message, [
JSAny optionsOrTargetOrigin,
JSArray<JSObject> transfer,
]);
}

// The Dart runtime does not allow this to be typed as any better than `JSAny?`.
extension type _CrossOriginLocation(JSAny? any) {
external void replace(String url);
external set href(String value);
}

class CrossOriginWindow {
CrossOriginWindow._(JSAny? o) : _window = _CrossOriginWindow(o);

static CrossOriginWindow? _create(JSAny? o) {
if (o == null) return null;
return CrossOriginWindow._(o);
}

final _CrossOriginWindow _window;

/// The [Window.closed] value of this cross-origin window.
bool get closed => _window.closed;

/// The [Window.length] value of this cross-origin window.
int get length => _window.length;

/// A [CrossOriginLocation] wrapper of the [Window.location] value of this
/// cross-origin window.
CrossOriginLocation? get location =>
CrossOriginLocation._create(_window.location);

/// A [CrossOriginWindow] wrapper of the [Window.opener] value of this
/// cross-origin window.
CrossOriginWindow? get opener => _create(_window.opener);

/// A [CrossOriginWindow] wrapper of the [Window.top] value of this
/// cross-origin window.
CrossOriginWindow? get parent => _create(_window.parent);

/// A [CrossOriginWindow] wrapper of the [Window.parent] value of this
/// cross-origin window.
CrossOriginWindow? get top => _create(_window.top);

/// Calls [Window.blur] on this cross-origin window.
void blur() => _window.blur();

/// Calls [Window.close] on this cross-origin window.
void close() => _window.close();

/// Calls [Window.focus] on this cross-origin window.
void focus() => _window.focus();

/// Calls [Window.postMessage] on this cross-origin window with the given
/// [message], [optionsOrTargetOrigin] if not `null`, and [transfer] if not
/// `null`.
void postMessage(
JSAny? message, [
JSAny? optionsOrTargetOrigin,
JSArray<JSObject>? transfer,
]) {
if (optionsOrTargetOrigin == null) {
_window.postMessage(message);
} else if (transfer == null) {
_window.postMessage(message, optionsOrTargetOrigin);
} else {
_window.postMessage(message, optionsOrTargetOrigin, transfer);
}
}

/// The unsafe window value that this wrapper wraps that should only ever be
/// typed as <code>[JSAny]?</code>.
///
/// > [!NOTE]
/// > This is only intended to be passed to an interop member that expects a
/// > <code>[JSAny]?</code>. Safety for any other operations is not
/// > guaranteed.
JSAny? get window => _window.any;
}

/// A safe wrapper for a cross-origin location obtained through a cross-origin
/// window.
///
/// Since cross-origin access is limited by the browser, the Dart runtime can't
/// provide a type for or null-assert this value. To safely interact with this
/// value, use this wrapper instead.
class CrossOriginLocation {
CrossOriginLocation._(JSAny? o) : _location = _CrossOriginLocation(o);

static CrossOriginLocation? _create(JSAny? o) {
if (o == null) return null;
return CrossOriginLocation._(o);
}

final _CrossOriginLocation _location;

/// Sets the [Location.href] value of this cross-origin location to [value].
set href(String value) => _location.href = value;

/// Calls [Location.replace] on this cross-origin location with the given
/// [url].
void replace(String url) => _location.replace(url);

/// The unsafe location value that this wrapper wraps that should only ever be
/// typed as <code>[JSAny]?</code>.
///
/// > [!NOTE]
/// > This is only intended to be passed to an interop member that expects a
/// > <code>[JSAny]?</code>. Safety for any other operations is not
/// > guaranteed.
JSAny? get location => _location.any;
}

extension CrossOriginContentWindowExtension on HTMLIFrameElement {
@JS('contentWindow')
external JSAny? get _contentWindow;

/// A [CrossOriginWindow] wrapper of the [HTMLIFrameElement.contentWindow]
/// value of this `iframe`.
CrossOriginWindow? get contentWindowCrossOrigin =>
CrossOriginWindow._create(_contentWindow);
}

extension CrossOriginWindowExtension on Window {
@JS('open')
external JSAny? _open(String url);

/// A [CrossOriginWindow] wrapper of the value returned from calling
/// [Window.open] with [url].
CrossOriginWindow? openCrossOrigin(String url) =>
CrossOriginWindow._create(_open(url));
@JS('opener')
external JSAny? get _opener;

/// A [CrossOriginWindow] wrapper of the [Window.opener] value of this
/// cross-origin window.
CrossOriginWindow? get openerCrossOrigin =>
CrossOriginWindow._create(_opener);
@JS('parent')
external JSAny? get _parent;

/// A [CrossOriginWindow] wrapper of the [Window.parent] value of this
/// cross-origin window.
CrossOriginWindow? get parentCrossOrigin =>
CrossOriginWindow._create(_parent);
@JS('top')
external JSAny? get _top;

/// A [CrossOriginWindow] wrapper of the [Window.top] value of this
/// cross-origin window.
CrossOriginWindow? get topCrossOrigin => CrossOriginWindow._create(_top);
}
3 changes: 3 additions & 0 deletions web/lib/src/helpers/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import 'dart:math' show Point;
import '../dom.dart';
import 'lists.dart';

export 'cross_origin.dart'
show CrossOriginContentWindowExtension, CrossOriginWindowExtension;

extension HTMLCanvasElementGlue on HTMLCanvasElement {
CanvasRenderingContext2D get context2D =>
getContext('2d') as CanvasRenderingContext2D;
Expand Down
64 changes: 64 additions & 0 deletions web/test/helpers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import 'dart:js_interop';
import 'package:test/test.dart';
import 'package:web/web.dart';

@JS('Object.is')
external bool _is(JSAny? a, JSAny? b);

void main() {
test('instanceOfString works with package:web types', () {
final div = document.createElement('div') as JSObject;
Expand All @@ -36,4 +39,65 @@ void main() {
// Ensure accessing any arbitrary item in the list does not throw.
expect(() => dartList[0], returnsNormally);
});

test('cross-origin windows and locations can be accessed safely', () {
const url = 'https://www.google.com';
const url2 = 'https://www.example.org';

void testCommon(CrossOriginWindow crossOriginWindow) {
expect(crossOriginWindow.length, 0);
expect(crossOriginWindow.closed, false);
// We can't add an event listener on a cross-origin window, so just test
// that a message can be sent without any errors.
crossOriginWindow.postMessage('hello world'.toJS);
crossOriginWindow.postMessage('hello world'.toJS, url.toJS);
crossOriginWindow.postMessage('hello world'.toJS, url.toJS, JSArray());
crossOriginWindow.location!.replace(url2);
crossOriginWindow.location!.href = url;
crossOriginWindow.blur();
crossOriginWindow.focus();
crossOriginWindow.close();
}

final openedWindow = window.openCrossOrigin(url)!;
// Use `Object.is` to test that values can be passed to interop.
expect(_is(openedWindow.opener!.window, window), true);
expect(_is(openedWindow.top!.window, openedWindow.window), true);
expect(_is(openedWindow.parent!.window, openedWindow.window), true);
expect(_is(openedWindow.opener!.location!.location, window.location), true);
expect(
_is(openedWindow.opener!.parent?.window,
window.parentCrossOrigin?.window),
true);
expect(_is(openedWindow.opener!.top?.window, window.topCrossOrigin?.window),
true);
expect(
openedWindow.opener!.opener?.window, window.openerCrossOrigin?.window);
testCommon(openedWindow);
expect(openedWindow.closed, true);

final iframe = HTMLIFrameElement();
iframe.src = url;
document.body!.append(iframe);
final contentWindow = iframe.contentWindowCrossOrigin!;
expect(contentWindow.opener, null);
expect(_is(contentWindow.top?.window, window.topCrossOrigin?.window), true);
expect(_is(contentWindow.parent!.window, window), true);
expect(
_is(contentWindow.parent!.location!.location, window.location), true);
expect(
_is(contentWindow.parent!.parent?.window,
window.parentCrossOrigin?.window),
true);
expect(
_is(contentWindow.parent!.top?.window, window.topCrossOrigin?.window),
true);
expect(
_is(contentWindow.parent!.opener?.window,
window.openerCrossOrigin?.window),
true);
testCommon(contentWindow);
// `close` on a `contentWindow` does nothing.
expect(contentWindow.closed, false);
});
}

0 comments on commit 1e17c5a

Please sign in to comment.