Skip to content

Commit 8501740

Browse files
authored
Add cross-origin window and location wrappers (#291)
Closes dart-lang/sdk#54443 Closes #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.
1 parent f1acb17 commit 8501740

File tree

5 files changed

+291
-0
lines changed

5 files changed

+291
-0
lines changed

web/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
`switch`ed over.
1313
- Add an extension `responseHeaders` to `XMLHttpRequest`.
1414
- Correctly namespace `WebAssembly` types.
15+
- Added `CrossOriginWindow` and `CrossOriginLocation` wrappers for cross-origin
16+
windows and locations, respectively, that can be accessed through
17+
`HTMLIFrameElement.contentWindowCrossOrigin`, `Window.openCrossOrigin`,
18+
`Window.openerCrossOrigin`, `Window.topCrossOrigin`,
19+
and `Window.parentCrossOrigin`.
1520

1621
## 1.0.0
1722

web/lib/src/helpers.dart

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import 'dart:js_interop_unsafe';
2727
import 'dom.dart';
2828
import 'helpers/lists.dart';
2929

30+
export 'helpers/cross_origin.dart' show CrossOriginLocation, CrossOriginWindow;
3031
export 'helpers/enums.dart';
3132
export 'helpers/events/events.dart';
3233
export 'helpers/events/providers.dart';

web/lib/src/helpers/cross_origin.dart

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:js_interop';
6+
7+
import '../dom.dart' show HTMLIFrameElement, Location, Window;
8+
9+
// The Dart runtime does not allow this to be typed as any better than `JSAny?`.
10+
extension type _CrossOriginWindow(JSAny? any) {
11+
external bool get closed;
12+
external int get length;
13+
// While you can set the location to a string value, this is the same as
14+
// `location.href`, so we only allow the getter to avoid a
15+
// `getter_not_subtype_setter_types` error.
16+
external JSAny? get location;
17+
external JSAny? get opener;
18+
external JSAny? get parent;
19+
external JSAny? get top;
20+
// `frames`, `self`, and `window` are all supported for cross-origin windows,
21+
// but simply return the calling window, so there's no use in supporting them
22+
// for interop.
23+
external void blur();
24+
external void close();
25+
external void focus();
26+
external void postMessage(
27+
JSAny? message, [
28+
JSAny optionsOrTargetOrigin,
29+
JSArray<JSObject> transfer,
30+
]);
31+
}
32+
33+
// The Dart runtime does not allow this to be typed as any better than `JSAny?`.
34+
extension type _CrossOriginLocation(JSAny? any) {
35+
external void replace(String url);
36+
external set href(String value);
37+
}
38+
39+
/// A safe wrapper for a cross-origin window.
40+
///
41+
/// Since cross-origin access is limited by the browser, the Dart runtime can't
42+
/// provide a type for or null-assert the cross-origin window. To safely
43+
/// interact with the cross-origin window, use this wrapper instead.
44+
///
45+
/// The `dart:html` equivalent is `_DOMWindowCrossFrame`.
46+
///
47+
/// Only includes allowed APIs from the W3 spec located here:
48+
/// https://html.spec.whatwg.org/multipage/nav-history-apis.html#crossoriginproperties-(-o-)
49+
/// Some browsers may provide more access.
50+
class CrossOriginWindow {
51+
CrossOriginWindow._(JSAny? o) : _window = _CrossOriginWindow(o);
52+
53+
static CrossOriginWindow? _create(JSAny? o) {
54+
if (o == null) return null;
55+
return CrossOriginWindow._(o);
56+
}
57+
58+
final _CrossOriginWindow _window;
59+
60+
/// The [Window.closed] value of this cross-origin window.
61+
bool get closed => _window.closed;
62+
63+
/// The [Window.length] value of this cross-origin window.
64+
int get length => _window.length;
65+
66+
/// A [CrossOriginLocation] wrapper of the [Window.location] value of this
67+
/// cross-origin window.
68+
CrossOriginLocation? get location =>
69+
CrossOriginLocation._create(_window.location);
70+
71+
/// A [CrossOriginWindow] wrapper of the [Window.opener] value of this
72+
/// cross-origin window.
73+
CrossOriginWindow? get opener => _create(_window.opener);
74+
75+
/// A [CrossOriginWindow] wrapper of the [Window.top] value of this
76+
/// cross-origin window.
77+
CrossOriginWindow? get parent => _create(_window.parent);
78+
79+
/// A [CrossOriginWindow] wrapper of the [Window.parent] value of this
80+
/// cross-origin window.
81+
CrossOriginWindow? get top => _create(_window.top);
82+
83+
/// Calls [Window.blur] on this cross-origin window.
84+
void blur() => _window.blur();
85+
86+
/// Calls [Window.close] on this cross-origin window.
87+
void close() => _window.close();
88+
89+
/// Calls [Window.focus] on this cross-origin window.
90+
void focus() => _window.focus();
91+
92+
/// Calls [Window.postMessage] on this cross-origin window with the given
93+
/// [message], [optionsOrTargetOrigin] if not `null`, and [transfer] if not
94+
/// `null`.
95+
void postMessage(
96+
JSAny? message, [
97+
JSAny? optionsOrTargetOrigin,
98+
JSArray<JSObject>? transfer,
99+
]) {
100+
if (optionsOrTargetOrigin == null) {
101+
_window.postMessage(message);
102+
} else if (transfer == null) {
103+
_window.postMessage(message, optionsOrTargetOrigin);
104+
} else {
105+
_window.postMessage(message, optionsOrTargetOrigin, transfer);
106+
}
107+
}
108+
109+
/// The unsafe window value that this wrapper wraps that should only ever be
110+
/// typed as <code>[JSAny]?</code>.
111+
///
112+
/// > [!NOTE]
113+
/// > This is only intended to be passed to an interop member that expects a
114+
/// > <code>[JSAny]?</code>. Safety for any other operations is not
115+
/// > guaranteed.
116+
JSAny? get unsafeWindow => _window.any;
117+
}
118+
119+
/// A safe wrapper for a cross-origin location obtained through a cross-origin
120+
/// window.
121+
///
122+
/// Since cross-origin access is limited by the browser, the Dart runtime can't
123+
/// provide a type for or null-assert the cross-origin location. To safely
124+
/// interact with the cross-origin location, use this wrapper instead.
125+
///
126+
/// The `dart:html` equivalent is `_LocationCrossFrame`.
127+
///
128+
/// Only includes allowed APIs from the W3 spec located here:
129+
/// https://html.spec.whatwg.org/multipage/nav-history-apis.html#crossoriginproperties-(-o-)
130+
/// Some browsers may provide more access.
131+
class CrossOriginLocation {
132+
CrossOriginLocation._(JSAny? o) : _location = _CrossOriginLocation(o);
133+
134+
static CrossOriginLocation? _create(JSAny? o) {
135+
if (o == null) return null;
136+
return CrossOriginLocation._(o);
137+
}
138+
139+
final _CrossOriginLocation _location;
140+
141+
/// Sets the [Location.href] value of this cross-origin location to [value].
142+
set href(String value) => _location.href = value;
143+
144+
/// Calls [Location.replace] on this cross-origin location with the given
145+
/// [url].
146+
void replace(String url) => _location.replace(url);
147+
148+
/// The unsafe location value that this wrapper wraps that should only ever be
149+
/// typed as <code>[JSAny]?</code>.
150+
///
151+
/// > [!NOTE]
152+
/// > This is only intended to be passed to an interop member that expects a
153+
/// > <code>[JSAny]?</code>. Safety for any other operations is not
154+
/// > guaranteed.
155+
JSAny? get unsafeLocation => _location.any;
156+
}
157+
158+
extension CrossOriginContentWindowExtension on HTMLIFrameElement {
159+
@JS('contentWindow')
160+
external JSAny? get _contentWindow;
161+
162+
/// A [CrossOriginWindow] wrapper of the [HTMLIFrameElement.contentWindow]
163+
/// value of this `iframe`.
164+
CrossOriginWindow? get contentWindowCrossOrigin =>
165+
CrossOriginWindow._create(_contentWindow);
166+
}
167+
168+
/// Safe alternatives to common [Window] members that can return cross-origin
169+
/// windows.
170+
///
171+
/// By default, the Dart web compilers are not sensitive to cross-origin
172+
/// objects, and therefore same-origin policy errors may be triggered when
173+
/// type-checking. Use these members instead to safely interact with such
174+
/// objects.
175+
extension CrossOriginWindowExtension on Window {
176+
@JS('open')
177+
external JSAny? _open(String url);
178+
179+
/// A [CrossOriginWindow] wrapper of the value returned from calling
180+
/// [Window.open] with [url].
181+
CrossOriginWindow? openCrossOrigin(String url) =>
182+
CrossOriginWindow._create(_open(url));
183+
@JS('opener')
184+
external JSAny? get _opener;
185+
186+
/// A [CrossOriginWindow] wrapper of the [Window.opener] value of this
187+
/// cross-origin window.
188+
CrossOriginWindow? get openerCrossOrigin =>
189+
CrossOriginWindow._create(_opener);
190+
@JS('parent')
191+
external JSAny? get _parent;
192+
193+
/// A [CrossOriginWindow] wrapper of the [Window.parent] value of this
194+
/// cross-origin window.
195+
CrossOriginWindow? get parentCrossOrigin =>
196+
CrossOriginWindow._create(_parent);
197+
@JS('top')
198+
external JSAny? get _top;
199+
200+
/// A [CrossOriginWindow] wrapper of the [Window.top] value of this
201+
/// cross-origin window.
202+
CrossOriginWindow? get topCrossOrigin => CrossOriginWindow._create(_top);
203+
}

web/lib/src/helpers/extensions.dart

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import 'dart:math' show Point;
2828
import '../dom.dart';
2929
import 'lists.dart';
3030

31+
export 'cross_origin.dart'
32+
show CrossOriginContentWindowExtension, CrossOriginWindowExtension;
33+
3134
extension HTMLCanvasElementGlue on HTMLCanvasElement {
3235
CanvasRenderingContext2D get context2D =>
3336
getContext('2d') as CanvasRenderingContext2D;

web/test/helpers_test.dart

+79
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import 'dart:js_interop';
1010
import 'package:test/test.dart';
1111
import 'package:web/web.dart';
1212

13+
@JS('Object.is')
14+
external bool _is(JSAny? a, JSAny? b);
15+
1316
void main() {
1417
test('instanceOfString works with package:web types', () {
1518
final div = document.createElement('div') as JSObject;
@@ -55,4 +58,80 @@ void main() {
5558
),
5659
);
5760
});
61+
62+
test('cross-origin windows and locations can be accessed safely', () {
63+
// TODO(https://github.com/dart-lang/test/issues/2282): For some reason,
64+
// running `dart test` doesn't flag violations of same-origin policy,
65+
// allowing any unsafe accesses. When tested with `--pause-after-load` and
66+
// single stepped, however, the test correctly flags violations. Figure out
67+
// why and make this test always respect same-origin policy. Add some tests
68+
// to ensure that violations are being handled properly.
69+
const url = 'https://www.google.com';
70+
const url2 = 'https://www.example.org';
71+
72+
void testCommon(CrossOriginWindow crossOriginWindow) {
73+
expect(crossOriginWindow.length, 0);
74+
expect(crossOriginWindow.closed, false);
75+
// We can't add an event listener on a cross-origin window, so just test
76+
// that a message can be sent without any errors.
77+
crossOriginWindow.postMessage('hello world'.toJS);
78+
crossOriginWindow.postMessage('hello world'.toJS, url.toJS);
79+
crossOriginWindow.postMessage('hello world'.toJS, url.toJS, JSArray());
80+
crossOriginWindow.location!.replace(url2);
81+
crossOriginWindow.location!.href = url;
82+
crossOriginWindow.blur();
83+
crossOriginWindow.focus();
84+
crossOriginWindow.close();
85+
}
86+
87+
final openedWindow = window.openCrossOrigin(url)!;
88+
// Use `Object.is` to test that values can be passed to interop.
89+
expect(_is(openedWindow.opener!.unsafeWindow, window), true);
90+
expect(
91+
_is(openedWindow.top!.unsafeWindow, openedWindow.unsafeWindow), true);
92+
expect(_is(openedWindow.parent!.unsafeWindow, openedWindow.unsafeWindow),
93+
true);
94+
expect(_is(openedWindow.opener!.location!.unsafeLocation, window.location),
95+
true);
96+
expect(
97+
_is(openedWindow.opener!.parent?.unsafeWindow,
98+
window.parentCrossOrigin?.unsafeWindow),
99+
true);
100+
expect(
101+
_is(openedWindow.opener!.top?.unsafeWindow,
102+
window.topCrossOrigin?.unsafeWindow),
103+
true);
104+
expect(openedWindow.opener!.opener?.unsafeWindow,
105+
window.openerCrossOrigin?.unsafeWindow);
106+
testCommon(openedWindow);
107+
expect(openedWindow.closed, true);
108+
109+
final iframe = HTMLIFrameElement();
110+
iframe.src = url;
111+
document.body!.append(iframe);
112+
final contentWindow = iframe.contentWindowCrossOrigin!;
113+
expect(contentWindow.opener, null);
114+
expect(
115+
_is(contentWindow.top?.unsafeWindow,
116+
window.topCrossOrigin?.unsafeWindow),
117+
true);
118+
expect(_is(contentWindow.parent!.unsafeWindow, window), true);
119+
expect(_is(contentWindow.parent!.location!.unsafeLocation, window.location),
120+
true);
121+
expect(
122+
_is(contentWindow.parent!.parent?.unsafeWindow,
123+
window.parentCrossOrigin?.unsafeWindow),
124+
true);
125+
expect(
126+
_is(contentWindow.parent!.top?.unsafeWindow,
127+
window.topCrossOrigin?.unsafeWindow),
128+
true);
129+
expect(
130+
_is(contentWindow.parent!.opener?.unsafeWindow,
131+
window.openerCrossOrigin?.unsafeWindow),
132+
true);
133+
testCommon(contentWindow);
134+
// `close` on a `contentWindow` does nothing.
135+
expect(contentWindow.closed, false);
136+
});
58137
}

0 commit comments

Comments
 (0)