blob: 8d749df72b5d117743d462a8a1c57bdb69d70aaf [file] [log] [blame]
import 'dart:async';
import 'dart:html' as html;
import 'dart:js' as js;
import 'authentication_exception.dart';
Future<void> initializeScript(
String scriptUrl, {
String? onloadParam,
}) async {
final instance = _ScriptLoader._instances.putIfAbsent(
scriptUrl,
() => _ScriptLoader._(scriptUrl, onloadParam: onloadParam),
);
await instance._initialize();
}
/// Creates a script that will run properly when strict CSP is enforced.
///
/// More specifically, the script has the correct `nonce` value set.
final html.ScriptElement Function() _createScriptTag = (() {
final nonce = _getNonce();
if (nonce == null) return html.ScriptElement.new;
return () => html.ScriptElement()..nonce = nonce;
})();
/// Returns CSP nonce, if set for any script tag.
String? _getNonce({html.Window? window}) {
final currentWindow = window ?? html.window;
final elements = currentWindow.document.querySelectorAll('script');
for (final element in elements) {
final nonceValue =
(element as html.HtmlElement).nonce ?? element.attributes['nonce'];
if (nonceValue != null && _noncePattern.hasMatch(nonceValue)) {
return nonceValue;
}
}
return null;
}
// According to the CSP3 spec a nonce must be a valid base64 string.
// https://w3c.github.io/webappsec-csp/#grammardef-base64-value
final _noncePattern = RegExp('^[\\w+/_-]+[=]{0,2}\$');
const callbackTimeout = Duration(seconds: 20);
class _ScriptLoader {
_ScriptLoader._(
this.url, {
this.onloadParam,
});
static final _instances = <String, _ScriptLoader>{};
final String url;
final String? onloadParam;
Future<void>? _pendingInitialization;
Future<void> _initialize() {
if (_pendingInitialization != null) {
return _pendingInitialization!;
}
final completer = Completer();
final timeout = Timer(callbackTimeout, () {
_pendingInitialization = null;
completer.completeError(
AuthenticationException(
'Timed out while waiting for library to load: $url',
),
);
});
void loadComplete() {
timeout.cancel();
completer.complete();
}
final loadFunctionName = '_dartScriptLoad_${url.hashCode}';
js.context[loadFunctionName] = loadComplete;
final fullUrl =
onloadParam == null ? url : '$url?${onloadParam!}=$loadFunctionName';
final script = _createScriptTag()
..async = true
..defer = true
..src = fullUrl;
if (onloadParam == null) {
script.onLoad.first.then((event) {
loadComplete();
});
}
script.onError.first.then((errorEvent) {
timeout.cancel();
_pendingInitialization = null;
if (!completer.isCompleted) {
// script loading errors can still happen after timeouts
completer.completeError(
AuthenticationException('Failed to load library: $url'));
}
});
html.document.body!.append(script);
_pendingInitialization = completer.future;
return completer.future;
}
}