blob: 985ab28e3180d128ae0dae0614b21c290d36b282 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: avoid_print
/// Checks that JavaScript API is accessed properly.
///
/// JavaScript access needs to be audited to make sure it follows security best
/// practices. To do that, all JavaScript access is consolidated into a small
/// number of libraries that change infrequently. These libraries are manually
/// audited on every change. All other code accesses JavaScript through these
/// libraries and does not require audit.
import 'dart:io';
import 'package:test/test.dart';
// Libraries that allow making arbitrary calls to JavaScript.
const List<String> _jsAccessLibraries = <String>[
'dart:js_util',
'package:js',
];
// Libraries that are allowed to make direct calls to JavaScript. These
// libraries must be reviewed carefully to make sure JavaScript APIs are used
// safely.
const List<String> _auditedLibraries = <String>[
'lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart',
'lib/web_ui/lib/src/engine/safe_browser_api.dart',
];
Future<void> main(List<String> args) async {
bool areAssertionsEnabled = false;
assert(() {
areAssertionsEnabled = true;
return true;
}());
if (!areAssertionsEnabled) {
throw ArgumentError(
'This test must run with --enable-asserts',
);
}
test('Self-test', () {
// A library that doesn't directly access JavaScript API should pass.
{
final _CheckResult result = _checkFile(
File('lib/web_ui/lib/src/engine/alarm_clock.dart'),
'''
// A comment
import 'dart:async';
import 'package:ui/ui.dart' as ui;
export 'foo.dart';
''',
);
expect(result.passed, isTrue);
expect(result.failed, isFalse);
expect(result.violations, isEmpty);
}
// Multi-line imports should fail.
{
final _CheckResult result = _checkFile(
File('lib/web_ui/lib/src/engine/alarm_clock.dart'),
'''
import 'dart:async';
import 'package:ui/ui.dart'
as ui;
''',
);
expect(result.failed, isTrue);
expect(result.violations, <String>[
"on line 2: import is broken up into multiple lines: import 'package:ui/ui.dart'",
]);
}
// A library that doesn't directly access JavaScript API should pass.
expect(
_checkFile(
File('lib/web_ui/lib/src/engine/alarm_clock.dart'),
'''
import 'dart:async';
import 'package:ui/ui.dart' as ui;
''',
).passed,
isTrue,
);
// A non-audited library that directly accesses JavaScript API should fail.
for (final String jsAccessLibrary in _jsAccessLibraries) {
final _CheckResult result = _checkFile(
File('lib/web_ui/lib/src/engine/alarm_clock.dart'),
'''
import 'dart:async';
import 'package:ui/ui.dart' as ui;
import '$jsAccessLibrary';
''',
);
expect(result.passed, isFalse);
expect(result.failed, isTrue);
expect(result.violations, <String>[
'on line 3: library accesses $jsAccessLibrary directly',
]);
}
// Audited libraries that directly accesses JavaScript API should pass.
for (final String auditedLibrary in _auditedLibraries) {
for (final String jsAccessLibrary in _jsAccessLibraries) {
expect(
_checkFile(
File(auditedLibrary),
'''
import 'dart:async';
import 'package:ui/ui.dart' as ui;
import '$jsAccessLibrary';
''',
).passed,
isTrue,
);
}
}
});
test('Check JavaScript access', () async {
final Directory webUiLibDir = Directory('lib/web_ui/lib');
final List<File> dartFiles = webUiLibDir
.listSync(recursive: true)
.whereType<File>()
.where((File file) => file.path.endsWith('.dart'))
.toList();
expect(dartFiles, isNotEmpty);
final List<_CheckResult> results = <_CheckResult>[];
for (final File dartFile in dartFiles) {
results.add(_checkFile(
dartFile,
await dartFile.readAsString(),
));
}
if (results.any((_CheckResult result) => result.failed)) {
// Sort to show failures last.
results.sort((_CheckResult a, _CheckResult b) {
final int aSortKey = a.passed ? 1 : 0;
final int bSortKey = b.passed ? 1 : 0;
return bSortKey - aSortKey;
});
int passedCount = 0;
int failedCount = 0;
for (final _CheckResult result in results) {
if (result.passed) {
passedCount += 1;
print('PASSED: ${result.file.path}');
} else {
failedCount += 1;
print('FAILED: ${result.file.path}');
for (final String violation in result.violations) {
print(' $violation');
}
}
}
expect(passedCount + failedCount, dartFiles.length);
print('$passedCount files passed. $failedCount files contain violations.');
fail('Some file contain violations. See log messages above for details.');
}
});
}
_CheckResult _checkFile(File dartFile, String code) {
final List<String> violations = <String>[];
final List<String> lines = code.split('\n');
for (int i = 0; i < lines.length; i += 1) {
final int lineNumber = i + 1;
final String line = lines[i].trim();
final bool isImport = line.startsWith('import');
if (!isImport) {
continue;
}
final bool isProperlyFormattedImport = line.endsWith(';');
if (!isProperlyFormattedImport) {
violations.add('on line $lineNumber: import is broken up into multiple lines: $line');
continue;
}
if (line.contains('"')) {
violations.add('on line $lineNumber: import is using double quotes instead of single quotes: $line');
continue;
}
final bool isAuditedLibrary = _auditedLibraries.contains(dartFile.path);
if (isAuditedLibrary) {
// This library is allowed to access JavaScript API directly.
continue;
}
for (final String jsAccessLibrary in _jsAccessLibraries) {
if (line.contains("'$jsAccessLibrary'")) {
violations.add('on line $lineNumber: library accesses $jsAccessLibrary directly');
continue;
}
}
}
if (violations.isEmpty) {
return _CheckResult.passed(dartFile);
} else {
return _CheckResult.failed(dartFile, violations);
}
}
class _CheckResult {
_CheckResult.passed(this.file) : violations = const <String>[];
_CheckResult.failed(this.file, this.violations) : assert(violations.isNotEmpty);
/// The Dart file that was checked.
final File file;
/// If the check failed, contains the descriptions of violations.
///
/// If the check passed, this is empty.
final List<String> violations;
/// Whether the file passed the check.
bool get passed => violations.isEmpty;
/// Whether the file failed the check.
bool get failed => !passed;
}