| // 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; | |
| } |