diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b017785..9c637e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -269,6 +269,12 @@ jobs: channel: stable cache: true + - name: Test native download policies + run: | + cmake -S webview_all_linux/linux/test -B build/linux-native-tests + cmake --build build/linux-native-tests + xvfb-run -a ctest --test-dir build/linux-native-tests --output-on-failure + - name: Build Linux example working-directory: examples/platform run: | diff --git a/docs/src/content/docs/guides/debugging.md b/docs/src/content/docs/guides/debugging.md index 8ba39d3..abd1f35 100644 --- a/docs/src/content/docs/guides/debugging.md +++ b/docs/src/content/docs/guides/debugging.md @@ -35,6 +35,23 @@ final windows = controller.platform as WindowsWebViewController; await windows.openDevTools(); ``` +The plugin disables user access to DevTools by default in all build modes. +Enable the menu and keyboard entry points (including F12 and Ctrl+Shift+I) +per controller when needed: + +```dart +await windows.setDevToolsEnabled(true); +``` + +Apply this setting before loading content; runtime changes take effect on the +next top-level navigation. It does not close existing DevTools or prevent the +app from calling `openDevTools()` explicitly. + +To allow user access only in debug builds, pass `devToolsEnabled: kDebugMode` +to `WindowsWebViewControllerCreationParams`. Import `kDebugMode` from +`package:flutter/foundation.dart`. Browser shortcuts must also be enabled for +the DevTools shortcuts to work. + Check runtime availability: ```dart diff --git a/docs/src/content/docs/platforms/linux.md b/docs/src/content/docs/platforms/linux.md index e6dcc94..e465ef9 100644 --- a/docs/src/content/docs/platforms/linux.md +++ b/docs/src/content/docs/platforms/linux.md @@ -29,6 +29,7 @@ runner realizes `FlView`; applications do not need to modify their runner. ```dart final params = const LinuxWebViewControllerCreationParams( developerExtrasEnabled: true, + downloadsEnabled: false, javascriptCanOpenWindowsAutomatically: true, mediaPlaybackRequiresUserGesture: false, mediaPlaybackAllowsInline: true, @@ -45,12 +46,18 @@ final params = const LinuxWebViewControllerCreationParams( Each field is nullable. `null` leaves the WebKitGTK default unchanged. +Downloads are allowed by default. Set `downloadsEnabled: false` at creation, or +await `setDownloadsEnabled(false)` before loading content, to cancel new +downloads before a file is saved. Ongoing downloads and other WebViews are not +affected. Allowed downloads use WebKitGTK's default download destination. + ## Controller API | API | Purpose | | --- | --- | | `setDeveloperExtrasEnabled(bool enabled)` | Enables WebKitGTK developer extras. | | `openDevTools()` | Opens the Web Inspector. | +| `setDownloadsEnabled(bool enabled)` | Allows or cancels new downloads for this WebView. Enabled by default. | | `setJavaScriptCanOpenWindowsAutomatically(bool enabled)` | Controls JavaScript popups. | | `setMediaPlaybackRequiresUserGesture(bool require)` | Controls media autoplay policy. | | `setMediaPlaybackAllowsInline(bool allow)` | Controls inline media playback. | diff --git a/docs/src/content/docs/platforms/web.md b/docs/src/content/docs/platforms/web.md index 40d7dda..a2af50d 100644 --- a/docs/src/content/docs/platforms/web.md +++ b/docs/src/content/docs/platforms/web.md @@ -35,7 +35,7 @@ final controller = WebViewController.fromPlatformCreationParams(params); | Param | Meaning | | --- | --- | | `iFrameAllow` | Sets iframe `allow`. | -| `iFrameSandbox` | Sets iframe `sandbox` when JavaScript is unrestricted. | +| `iFrameSandbox` | Sets iframe sandbox permissions. Disabling JavaScript removes `allow-scripts` without adding permissions. | | `iFrameReferrerPolicy` | Sets iframe `referrerpolicy`. | | `iFrameAttributes` | Additional iframe attributes. `null` removes an attribute. | | `httpRequestFactory` | Test hook for fetch-backed loads. | @@ -44,6 +44,11 @@ Attribute names are validated before they reach the DOM. Empty or malformed names are rejected, and `id`, `src`, and `srcdoc` are reserved because the controller owns iframe identity and document loading. +An explicit sandbox is preserved when JavaScript is disabled, including an empty +sandbox (all restrictions). Only `allow-scripts` is removed; switching back to +unrestricted mode restores the configured value. Sandbox changes apply to the +next document load, not to a document already loaded in the iframe. + ## Controller API | API | Purpose | diff --git a/docs/src/content/docs/platforms/windows.md b/docs/src/content/docs/platforms/windows.md index 4d9dad3..14c5690 100644 --- a/docs/src/content/docs/platforms/windows.md +++ b/docs/src/content/docs/platforms/windows.md @@ -82,11 +82,26 @@ renderer if hardware device creation fails. ```dart final params = const WindowsWebViewControllerCreationParams( popupWindowPolicy: WindowsPopupWindowPolicy.sameWindow, + devToolsEnabled: false, + browserAcceleratorKeysEnabled: false, + downloadsEnabled: false, ); final controller = WebViewController.fromPlatformCreationParams(params); ``` +DevTools user access is disabled by default in all build modes. Set +`devToolsEnabled: true` to enable it, or use `kDebugMode` from +`package:flutter/foundation.dart` to enable it only in debug builds. +Browser shortcuts and downloads keep their native defaults (enabled) when +omitted. The DevTools default and explicit settings are applied before the first navigation and retained +when initialization is retried. A failed setting is reported to the caller; +navigation remains blocked until the setting is successfully applied or changed. +Disabling browser shortcuts requires `ICoreWebView2Settings3`; disabling downloads +requires a successfully registered `ICoreWebView2_4` download handler. Unsupported +restrictions report `not_supported`; native failures report `method_failed` with +the setting name and HRESULT. + `WindowsPopupWindowPolicy`: | Value | Behavior | @@ -119,10 +134,18 @@ final widget = WebViewWidget.fromPlatformCreationParams( | `setPopupWindowPolicy(policy)` | Changes popup handling after creation. | | `setZoomFactor(double zoomFactor)` | Sets WebView2 zoom factor. | | `setCacheDisabled(bool disabled)` | Toggles cache bypass behavior. | +| `setDevToolsEnabled(bool enabled)` | Controls the menu and keyboard shortcuts for opening DevTools. Disabled by default; does not block `openDevTools()`. | +| `setBrowserAcceleratorKeysEnabled(bool enabled)` | Enables or disables browser accelerator keys such as F5, Ctrl+P, and Ctrl+F. Enabled by default. | +| `setDownloadsEnabled(bool enabled)` | Allows new downloads or cancels them without saving the downloaded file. Enabled by default; ongoing downloads are unchanged. | | `dispose()` | Permanently releases this controller and its WebView2 resources. | Common APIs implemented on Windows include request loading with method, headers, and body; JavaScript execution; JavaScript channels; console messages; JavaScript dialogs; permission requests; HTTP errors; HTTP auth; SSL auth; scroll position; scrollbars; background color; user agent override; and overscroll styling. +DevTools and browser-shortcut changes take effect on the next top-level +navigation. Editing shortcuts such as copy and paste remain available; disabling +browser shortcuts does not forward them to Flutter. These switches control +WebView behavior and do not constitute a sandbox for untrusted content. + `onNavigationRequest` covers controller loads and WebView2 main-frame navigations initiated by page content, including redirects and popups opened with `sameWindow`. Controller loads are approved before native dispatch so custom methods, headers, and bodies are preserved. Page-initiated navigations wait for the asynchronous Dart policy through cancel-and-replay, and the intentional cancellation is suppressed from `onWebResourceError`. Local files and Flutter assets use private randomized HTTPS hosts per controller. Paths are canonicalized, asset traversal and symlink escapes are rejected, cross-origin access is denied, and mappings are cleared before unrelated remote or inline navigation. diff --git a/docs/src/content/docs/reference/platform-specific-api.md b/docs/src/content/docs/reference/platform-specific-api.md index 3a2fd1e..e7a888c 100644 --- a/docs/src/content/docs/reference/platform-specific-api.md +++ b/docs/src/content/docs/reference/platform-specific-api.md @@ -35,7 +35,7 @@ Package: `webview_all_windows`. Main types: `WindowsWebViewController`, `WindowsWebViewWidget`, `WindowsNavigationDelegate`, `WindowsWebViewCookieManager`, `WindowsWebViewDataManager`, `WindowsWebViewCookie`, `WindowsPlatformSslAuthError`, `WindowsWebResourceRequest`, `WindowsWebResourceResponse`, `WindowsWebResourceError`. -Important APIs: idempotent `ensureEnvironment`, strict `initializeEnvironment`, `getWebViewVersion`, `openDevTools`, `suspend`, `resume`, `setPopupWindowPolicy`, `setZoomFactor`, `setCacheDisabled`, Windows-specific deterministic `dispose`, full cookie set/query/delete. `WindowsWebViewDataManagerCreationParams` carries the same environment options into controller-independent data cleanup. Removing the widget does not dispose a reusable controller; call `dispose` only when its owner will never use it again. +Important APIs: idempotent `ensureEnvironment`, strict `initializeEnvironment`, `getWebViewVersion`, `openDevTools`, `setDevToolsEnabled`, `setBrowserAcceleratorKeysEnabled`, `setDownloadsEnabled`, `suspend`, `resume`, `setPopupWindowPolicy`, `setZoomFactor`, `setCacheDisabled`, Windows-specific deterministic `dispose`, full cookie set/query/delete. `WindowsWebViewDataManagerCreationParams` carries the same environment options into controller-independent data cleanup. Removing the widget does not dispose a reusable controller; call `dispose` only when its owner will never use it again. ## Linux @@ -43,7 +43,7 @@ Package: `webview_all_linux`. Main types: `LinuxWebViewController`, `LinuxWebViewWidget`, `LinuxNavigationDelegate`, `LinuxWebViewCookieManager`, `LinuxWebViewDataManager`, `LinuxWebResourceRequest`, `LinuxWebResourceResponse`, `LinuxWebResourceError`, `LinuxPlatformWebViewPermissionRequest`, `LinuxPlatformSslAuthError`. -Important APIs: WebKitGTK developer extras, Inspector, JS popup, media settings, page cache, file URL access, font size, zoom factor, and the pre-existing Linux-specific `dispose()` for optional early release. Normal cleanup is automatic through its finalizer; no common controller lifecycle API is added. +Important APIs: WebKitGTK developer extras, Inspector, `setDownloadsEnabled`, JS popup, media settings, page cache, file URL access, font size, zoom factor, and the pre-existing Linux-specific `dispose()` for optional early release. Normal cleanup is automatic through its finalizer; no common controller lifecycle API is added. ## OHOS diff --git a/docs/src/content/docs/zh/guides/debugging.md b/docs/src/content/docs/zh/guides/debugging.md index 24ce9af..f40b56b 100644 --- a/docs/src/content/docs/zh/guides/debugging.md +++ b/docs/src/content/docs/zh/guides/debugging.md @@ -31,6 +31,20 @@ final version = await WindowsWebViewController.getWebViewVersion(); `getWebViewVersion()` 可用于启动时确认 WebView2 Runtime 是否存在。 +插件在所有构建模式下默认关闭 DevTools 的用户入口,需要时可按 controller +开启菜单和快捷键入口(包括 F12 和 Ctrl+Shift+I): + +```dart +await windows.setDevToolsEnabled(true); +``` + +建议在加载内容前设置;运行时修改会在下次顶层导航时生效。该开关不会关闭已经 +打开的 DevTools,也不影响应用主动调用 `openDevTools()`。 + +如需仅在调试构建中开放用户入口,可在 `WindowsWebViewControllerCreationParams` +中设置 `devToolsEnabled: kDebugMode`,并从 `package:flutter/foundation.dart` +导入 `kDebugMode`。使用 DevTools 快捷键还需要保持浏览器快捷键启用。 + ## Linux ```dart diff --git a/docs/src/content/docs/zh/platforms/linux.md b/docs/src/content/docs/zh/platforms/linux.md index e009992..46a4298 100644 --- a/docs/src/content/docs/zh/platforms/linux.md +++ b/docs/src/content/docs/zh/platforms/linux.md @@ -24,6 +24,7 @@ Linux WebView 是 native GTK widget。插件会在标准 Flutter runner realize ```dart final params = const LinuxWebViewControllerCreationParams( developerExtrasEnabled: true, + downloadsEnabled: false, mediaPlaybackRequiresUserGesture: false, pageCacheEnabled: true, allowFileAccessFromFileUrls: false, @@ -34,12 +35,17 @@ final params = const LinuxWebViewControllerCreationParams( 所有字段都可为 `null`,表示保留 WebKitGTK 默认值。 +下载默认允许。可在创建时设置 `downloadsEnabled: false`,或在加载内容前等待 +`setDownloadsEnabled(false)` 完成,取消新下载并阻止保存文件。已开始的下载和 +其他 WebView 不受影响。允许下载时沿用 WebKitGTK 的默认保存位置。 + ## 主要 API | API | 作用 | | --- | --- | | `setDeveloperExtrasEnabled` | 启用开发者功能。 | | `openDevTools` | 打开 Web Inspector。 | +| `setDownloadsEnabled` | 控制此 WebView 是否允许新下载,默认允许。 | | `setJavaScriptCanOpenWindowsAutomatically` | 控制 JS popup。 | | `setMediaPlaybackRequiresUserGesture` | 控制媒体自动播放。 | | `setMediaPlaybackAllowsInline` | 控制内联媒体播放。 | diff --git a/docs/src/content/docs/zh/platforms/web.md b/docs/src/content/docs/zh/platforms/web.md index ee6be9d..0428059 100644 --- a/docs/src/content/docs/zh/platforms/web.md +++ b/docs/src/content/docs/zh/platforms/web.md @@ -29,6 +29,10 @@ final params = WebWebViewControllerCreationParams( iframe 属性名会在写入 DOM 前校验。空名称、非法名称以及由控制器管理的 `id`、`src`、`srcdoc` 都会被拒绝,避免破坏实例标识和加载状态。 +关闭 JavaScript 时会保留自定义 sandbox 的其他限制,仅移除 `allow-scripts`; +空字符串仍表示启用全部 sandbox 限制。恢复 JavaScript 后会还原用户配置。 +sandbox 的修改在下次文档加载时生效,不会改变已经加载的文档。 + ## 主要 API | API | 作用 | diff --git a/docs/src/content/docs/zh/platforms/windows.md b/docs/src/content/docs/zh/platforms/windows.md index 5d12512..65256b4 100644 --- a/docs/src/content/docs/zh/platforms/windows.md +++ b/docs/src/content/docs/zh/platforms/windows.md @@ -58,14 +58,26 @@ controller 初始化失败时,组件中心会显示错误和恢复操作: 帧捕获及其回调统一使用该 UI 线程队列,Direct3D 硬件设备创建失败时会回退到 WARP 软件渲染器。 -## Popup 策略 +## 创建参数 ```dart final params = const WindowsWebViewControllerCreationParams( popupWindowPolicy: WindowsPopupWindowPolicy.sameWindow, + devToolsEnabled: false, + browserAcceleratorKeysEnabled: false, + downloadsEnabled: false, ); ``` +DevTools 用户入口在所有构建模式下默认关闭,可设置 `devToolsEnabled: true` +开启;如需仅在调试构建中开启,可使用 `package:flutter/foundation.dart` 的 +`kDebugMode`。浏览器快捷键和下载省略配置时保持原生默认值(启用)。 +DevTools 默认配置及显式配置均会在首次导航前应用,初始化 +重试时也会保留。设置失败会返回错误,后续导航需等待该配置成功应用或被修改。 +禁用浏览器快捷键需要 `ICoreWebView2Settings3`,禁用下载需要成功注册 +`ICoreWebView2_4` 下载监听。不支持的限制返回 `not_supported`;原生调用失败 +返回 `method_failed`,错误详情包含设置名称和 HRESULT。 + | 值 | 行为 | | --- | --- | | `allow` | 允许 popup 新窗口。 | @@ -82,8 +94,15 @@ final params = const WindowsWebViewControllerCreationParams( | `setPopupWindowPolicy` | 运行时修改 popup 策略。 | | `setZoomFactor` | 设置 WebView2 缩放因子。 | | `setCacheDisabled` | 控制请求是否绕过 cache。 | +| `setDevToolsEnabled` | 控制通过菜单和快捷键打开 DevTools。默认关闭,不影响 `openDevTools()`。 | +| `setBrowserAcceleratorKeysEnabled` | 启用或禁用浏览器快捷键,例如 F5、Ctrl+P 和 Ctrl+F。默认启用。 | +| `setDownloadsEnabled` | 允许新下载,或在开始时取消并阻止保存下载文件。默认允许,不影响已开始的下载。 | | `dispose` | 永久释放此 controller 及其 WebView2 资源。 | +开发者工具和浏览器快捷键设置会在下次顶层导航时生效。复制、粘贴等编辑快捷键 +不受影响;禁用浏览器快捷键也不会自动将按键交给 Flutter。这些开关用于控制 +WebView 行为,不是隔离不可信内容的完整安全沙箱。 + `onNavigationRequest` 会覆盖 controller 加载以及页面内容触发的 WebView2 主 frame 导航,包括 redirect 和 `sameWindow` popup。controller 请求在原生分发前完成判断,因此会保留自定义 method、headers 和 body;页面导航通过取消后等待异步 Dart 决策、放行后重放来实现,策略性取消不会触发 `onWebResourceError`。 本地文件和 Flutter asset 会使用每个 controller 独立的随机 HTTPS host。路径会先规范化,asset 路径穿越和逃出 bundle 的符号链接会被拒绝,跨 origin 访问默认禁止,切换到无关的远程或 inline 内容前会清理映射。 diff --git a/docs/src/content/docs/zh/reference/platform-specific-api.md b/docs/src/content/docs/zh/reference/platform-specific-api.md index c69df10..7155d17 100644 --- a/docs/src/content/docs/zh/reference/platform-specific-api.md +++ b/docs/src/content/docs/zh/reference/platform-specific-api.md @@ -35,7 +35,7 @@ if (controller.platform is WindowsWebViewController) { 主要类型:`WindowsWebViewController`、`WindowsWebViewWidget`、`WindowsNavigationDelegate`、`WindowsWebViewCookieManager`、`WindowsWebViewDataManager`、`WindowsWebViewCookie`、`WindowsPlatformSslAuthError`、`WindowsWebResourceRequest`、`WindowsWebResourceResponse`、`WindowsWebResourceError`。 -重要 API:幂等的 `ensureEnvironment`、严格的 `initializeEnvironment`、`getWebViewVersion`、`openDevTools`、`suspend`、`resume`、`setPopupWindowPolicy`、`setZoomFactor`、`setCacheDisabled`、Windows 专属的确定性 `dispose`、完整 cookie 设置/查询/删除。`WindowsWebViewDataManagerCreationParams` 可把同一套环境参数传给无需 controller 的数据清理。移除组件不会销毁仍可复用的 controller;仅在其所有者确认不再使用时调用 `dispose`。 +重要 API:幂等的 `ensureEnvironment`、严格的 `initializeEnvironment`、`getWebViewVersion`、`openDevTools`、`setDevToolsEnabled`、`setBrowserAcceleratorKeysEnabled`、`setDownloadsEnabled`、`suspend`、`resume`、`setPopupWindowPolicy`、`setZoomFactor`、`setCacheDisabled`、Windows 专属的确定性 `dispose`、完整 cookie 设置/查询/删除。`WindowsWebViewDataManagerCreationParams` 可把同一套环境参数传给无需 controller 的数据清理。移除组件不会销毁仍可复用的 controller;仅在其所有者确认不再使用时调用 `dispose`。 ## Linux @@ -43,7 +43,7 @@ if (controller.platform is WindowsWebViewController) { 主要类型:`LinuxWebViewController`、`LinuxWebViewWidget`、`LinuxNavigationDelegate`、`LinuxWebViewCookieManager`、`LinuxWebViewDataManager`、`LinuxWebResourceRequest`、`LinuxWebResourceResponse`、`LinuxWebResourceError`、`LinuxPlatformWebViewPermissionRequest`、`LinuxPlatformSslAuthError`。 -重要 API:WebKitGTK developer extras、Inspector、JS popup、media settings、page cache、file URL access、font size、zoom factor,以及可选提前释放资源的既有 Linux 专属 `dispose()`。正常生命周期由 finalizer 自动清理,没有给公共 controller 增加生命周期 API。 +重要 API:WebKitGTK developer extras、Inspector、`setDownloadsEnabled`、JS popup、media settings、page cache、file URL access、font size、zoom factor,以及可选提前释放资源的既有 Linux 专属 `dispose()`。正常生命周期由 finalizer 自动清理,没有给公共 controller 增加生命周期 API。 ## OHOS diff --git a/examples/platform/integration_test/webview_flutter_test.dart b/examples/platform/integration_test/webview_flutter_test.dart index e49b80b..5d9c275 100644 --- a/examples/platform/integration_test/webview_flutter_test.dart +++ b/examples/platform/integration_test/webview_flutter_test.dart @@ -20,6 +20,10 @@ import 'package:webview_all_android/webview_all_android.dart'; import 'package:webview_all_linux/webview_all_linux.dart'; import 'package:webview_all_wkwebview/webview_all_wkwebview.dart'; import 'package:webview_all_windows/webview_all_windows.dart'; +import 'package:webview_all_windows/src/windows_webview_native.dart' + as native_windows; +import 'package:webview_all_windows/src/windows_webview_types.dart' + as windows_types; Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -38,6 +42,13 @@ Future main() async { request.response.writeln('${request.headers}'); } else if (request.uri.path == '/favicon.ico') { request.response.statusCode = HttpStatus.notFound; + } else if (request.uri.path.startsWith('/webview_all_download_')) { + request.response.headers.contentType = ContentType.text; + request.response.headers.set( + 'Content-Disposition', + 'attachment; filename="${request.uri.pathSegments.last}"', + ); + request.response.write('webview_all'); } else if (request.uri.path == '/http-basic-authentication') { final List? authHeader = request.headers[HttpHeaders.authorizationHeader]; @@ -228,6 +239,125 @@ return { ); }); + testWidgets('Windows controller can refuse downloads', ( + WidgetTester tester, + ) async { + if (!Platform.isWindows) { + return; + } + + final String stamp = DateTime.now().microsecondsSinceEpoch.toString(); + final files = {}; + final controllers = List.generate( + 2, + (_) => WebViewController.fromPlatformCreationParams( + const WindowsWebViewControllerCreationParams( + devToolsEnabled: false, + browserAcceleratorKeysEnabled: false, + ), + ), + ); + final windows = controllers + .map((controller) => controller.platform as WindowsWebViewController) + .toList(); + final subscriptions = + >[]; + addTearDown(() async { + await tester.pumpWidget(const SizedBox.shrink()); + for (final controller in windows) { + await controller.dispose(); + } + for (final subscription in subscriptions) { + await subscription.cancel(); + } + for (final String path in files) { + final file = File(path); + if (file.existsSync()) { + file.deleteSync(); + } + } + }); + + for (final controller in controllers) { + await controller.currentUrl(); + } + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: controllers + .map( + (controller) => + Expanded(child: WebViewWidget(controller: controller)), + ) + .toList(), + ), + ), + ); + await tester.pumpAndSettle(); + final native = tester + .widgetList(find.byType(native_windows.Webview)) + .map((widget) => widget.controller) + .toList(); + expect(native, hasLength(2)); + for (final controller in native) { + subscriptions.add( + controller.onDownloadEvent.listen((event) { + if (event.resultFilePath.isNotEmpty && + File(event.resultFilePath).uri.pathSegments.last.startsWith( + 'webview_all_download_$stamp', + )) { + files.add(event.resultFilePath); + } + }), + ); + } + + Future download({ + required int index, + required bool enabled, + required String suffix, + }) async { + final name = 'webview_all_download_${stamp}_$suffix.txt'; + final url = '$prefixUrl/$name'; + final result = native[index].onDownloadEvent + .firstWhere( + (event) => + event.url == url && + (event.kind == + windows_types + .WebviewDownloadEventKind + .downloadCompleted || + event.kind == + windows_types + .WebviewDownloadEventKind + .downloadCancelled), + ) + .timeout(const Duration(seconds: 20)); + await controllers[index].loadRequest(Uri.parse(url)); + final event = await result; + expect( + event.kind, + enabled + ? windows_types.WebviewDownloadEventKind.downloadCompleted + : windows_types.WebviewDownloadEventKind.downloadCancelled, + ); + final file = File(event.resultFilePath); + expect(file.uri.pathSegments.last, name); + expect(file.existsSync(), enabled); + if (enabled) { + expect(file.readAsStringSync(), 'webview_all'); + } + } + + await download(index: 0, enabled: true, suffix: 'default'); + await windows[0].setDownloadsEnabled(false); + await download(index: 0, enabled: false, suffix: 'disabled'); + await download(index: 1, enabled: true, suffix: 'independent'); + await windows[0].setDownloadsEnabled(true); + await download(index: 0, enabled: true, suffix: 'enabled'); + }); + testWidgets('Windows controller releases its renderer process', ( WidgetTester tester, ) async { diff --git a/webview_all/CHANGELOG-ZH.md b/webview_all/CHANGELOG-ZH.md index 12a3f60..f361e91 100644 --- a/webview_all/CHANGELOG-ZH.md +++ b/webview_all/CHANGELOG-ZH.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Windows 新增开发者工具、浏览器快捷键和下载开关,支持创建时配置及运行时调整。 +* Windows 默认关闭开发者工具的菜单和快捷键入口,可按需开启。 +* Linux 新增下载开关,可按 WebView 单独控制是否允许下载。 +* 修复 Web 关闭 JavaScript 时覆盖自定义 iframe sandbox 限制的问题。 + ## 1.4.1 * 修复 Windows 应用退出时的崩溃。 diff --git a/webview_all/CHANGELOG.md b/webview_all/CHANGELOG.md index dbead51..cfe6648 100644 --- a/webview_all/CHANGELOG.md +++ b/webview_all/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Add Windows controls for DevTools, browser shortcuts, and downloads, configurable at creation and at runtime. +* Disable Windows DevTools menu and keyboard entry points by default; enable them explicitly when needed. +* Add per-WebView download controls on Linux. +* Preserve custom iframe sandbox restrictions when disabling JavaScript on Web. + ## 1.4.1 * Fix a crash when exiting Windows applications. diff --git a/webview_all_android/CHANGELOG-ZH.md b/webview_all_android/CHANGELOG-ZH.md index 12a3f60..f361e91 100644 --- a/webview_all_android/CHANGELOG-ZH.md +++ b/webview_all_android/CHANGELOG-ZH.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Windows 新增开发者工具、浏览器快捷键和下载开关,支持创建时配置及运行时调整。 +* Windows 默认关闭开发者工具的菜单和快捷键入口,可按需开启。 +* Linux 新增下载开关,可按 WebView 单独控制是否允许下载。 +* 修复 Web 关闭 JavaScript 时覆盖自定义 iframe sandbox 限制的问题。 + ## 1.4.1 * 修复 Windows 应用退出时的崩溃。 diff --git a/webview_all_android/CHANGELOG.md b/webview_all_android/CHANGELOG.md index dbead51..cfe6648 100644 --- a/webview_all_android/CHANGELOG.md +++ b/webview_all_android/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Add Windows controls for DevTools, browser shortcuts, and downloads, configurable at creation and at runtime. +* Disable Windows DevTools menu and keyboard entry points by default; enable them explicitly when needed. +* Add per-WebView download controls on Linux. +* Preserve custom iframe sandbox restrictions when disabling JavaScript on Web. + ## 1.4.1 * Fix a crash when exiting Windows applications. diff --git a/webview_all_linux/CHANGELOG-ZH.md b/webview_all_linux/CHANGELOG-ZH.md index 12a3f60..f361e91 100644 --- a/webview_all_linux/CHANGELOG-ZH.md +++ b/webview_all_linux/CHANGELOG-ZH.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Windows 新增开发者工具、浏览器快捷键和下载开关,支持创建时配置及运行时调整。 +* Windows 默认关闭开发者工具的菜单和快捷键入口,可按需开启。 +* Linux 新增下载开关,可按 WebView 单独控制是否允许下载。 +* 修复 Web 关闭 JavaScript 时覆盖自定义 iframe sandbox 限制的问题。 + ## 1.4.1 * 修复 Windows 应用退出时的崩溃。 diff --git a/webview_all_linux/CHANGELOG.md b/webview_all_linux/CHANGELOG.md index dbead51..cfe6648 100644 --- a/webview_all_linux/CHANGELOG.md +++ b/webview_all_linux/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Add Windows controls for DevTools, browser shortcuts, and downloads, configurable at creation and at runtime. +* Disable Windows DevTools menu and keyboard entry points by default; enable them explicitly when needed. +* Add per-WebView download controls on Linux. +* Preserve custom iframe sandbox restrictions when disabling JavaScript on Web. + ## 1.4.1 * Fix a crash when exiting Windows applications. diff --git a/webview_all_linux/lib/src/linux_webview_controller.dart b/webview_all_linux/lib/src/linux_webview_controller.dart index ea388d0..463a94a 100644 --- a/webview_all_linux/lib/src/linux_webview_controller.dart +++ b/webview_all_linux/lib/src/linux_webview_controller.dart @@ -151,6 +151,8 @@ class LinuxWebViewController extends PlatformWebViewController { final Map settings = { if (_linuxParams.developerExtrasEnabled case final bool value) 'developerExtrasEnabled': value, + if (_linuxParams.downloadsEnabled case final bool value) + 'downloadsEnabled': value, if (_linuxParams.javascriptCanOpenWindowsAutomatically case final bool value) 'javascriptCanOpenWindowsAutomatically': value, @@ -670,6 +672,16 @@ class LinuxWebViewController extends PlatformWebViewController { /// Opens the WebKitGTK web inspector for this WebView. Future openDevTools() => _invoke('openDevTools'); + /// Sets whether this WebView can start downloads. Enabled by default. + /// + /// When disabled, new downloads are cancelled before saving a file. Downloads + /// already in progress are not cancelled. Other WebViews are not affected. + Future setDownloadsEnabled(bool enabled) { + return _invoke('setDownloadsEnabled', { + 'enabled': enabled, + }); + } + /// Sets whether JavaScript may open windows automatically. Future setJavaScriptCanOpenWindowsAutomatically(bool enabled) { return _invoke( diff --git a/webview_all_linux/lib/src/linux_webview_creation_params.dart b/webview_all_linux/lib/src/linux_webview_creation_params.dart index 668e3c5..0895199 100644 --- a/webview_all_linux/lib/src/linux_webview_creation_params.dart +++ b/webview_all_linux/lib/src/linux_webview_creation_params.dart @@ -6,6 +6,7 @@ class LinuxWebViewControllerCreationParams extends PlatformWebViewControllerCreationParams { const LinuxWebViewControllerCreationParams({ this.developerExtrasEnabled, + this.downloadsEnabled, this.javascriptCanOpenWindowsAutomatically, this.mediaPlaybackRequiresUserGesture, this.mediaPlaybackAllowsInline, @@ -22,6 +23,7 @@ class LinuxWebViewControllerCreationParams const LinuxWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( PlatformWebViewControllerCreationParams params, { this.developerExtrasEnabled, + this.downloadsEnabled, this.javascriptCanOpenWindowsAutomatically, this.mediaPlaybackRequiresUserGesture, this.mediaPlaybackAllowsInline, @@ -38,6 +40,10 @@ class LinuxWebViewControllerCreationParams /// Whether WebKitGTK developer extras are enabled for this WebView. final bool? developerExtrasEnabled; + /// Whether this WebView can start downloads. Null preserves the WebKitGTK + /// default (enabled). Disabled downloads are cancelled before saving a file. + final bool? downloadsEnabled; + /// Whether JavaScript is allowed to open windows automatically. final bool? javascriptCanOpenWindowsAutomatically; diff --git a/webview_all_linux/linux/CMakeLists.txt b/webview_all_linux/linux/CMakeLists.txt index 9d6a5bd..46af8f4 100644 --- a/webview_all_linux/linux/CMakeLists.txt +++ b/webview_all_linux/linux/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(${PLUGIN_NAME} SHARED "src/plugin/webview_all_linux_plugin.cc" "src/plugin/root_channel.cc" "src/webview/linux_webview.cc" + "src/webview/download_policy.cc" "src/webview/webview_javascript.cc" "src/webview/webview_method_handler.cc" ) diff --git a/webview_all_linux/linux/src/plugin/webview_all_linux_plugin_private.h b/webview_all_linux/linux/src/plugin/webview_all_linux_plugin_private.h index 8371259..edfa2fc 100644 --- a/webview_all_linux/linux/src/plugin/webview_all_linux_plugin_private.h +++ b/webview_all_linux/linux/src/plugin/webview_all_linux_plugin_private.h @@ -9,6 +9,8 @@ constexpr const gchar *kLinuxWebViewInstanceKey = "webview_all_linux_instance"; +class DownloadPolicy; + struct _WebviewAllLinuxPlugin { GObject parent_instance; @@ -51,6 +53,7 @@ typedef struct { gboolean vertical_scrollbar_enabled; gboolean horizontal_scrollbar_enabled; gboolean zoom_enabled; + DownloadPolicy *download_policy; gint media_playback_requires_user_gesture; const gchar *over_scroll_behavior; gint frame_x; diff --git a/webview_all_linux/linux/src/webview/download_policy.cc b/webview_all_linux/linux/src/webview/download_policy.cc new file mode 100644 index 0000000..73fb9fe --- /dev/null +++ b/webview_all_linux/linux/src/webview/download_policy.cc @@ -0,0 +1,23 @@ +#include "webview/download_policy.h" + +DownloadPolicy::DownloadPolicy(WebKitWebView *web_view) : web_view_(web_view) { + signal_id_ = + g_signal_connect(webkit_web_view_get_context(web_view_), + "download-started", G_CALLBACK(OnDownloadStarted), this); +} + +DownloadPolicy::~DownloadPolicy() { + g_signal_handler_disconnect(webkit_web_view_get_context(web_view_), + signal_id_); +} + +void DownloadPolicy::OnDownloadStarted(WebKitWebContext *context, + WebKitDownload *download, + gpointer user_data) { + auto *policy = static_cast(user_data); + // A shared context must not let one view cancel another view's downloads. + if (!policy->enabled_ && + webkit_download_get_web_view(download) == policy->web_view_) { + webkit_download_cancel(download); + } +} diff --git a/webview_all_linux/linux/src/webview/download_policy.h b/webview_all_linux/linux/src/webview/download_policy.h new file mode 100644 index 0000000..1b8c374 --- /dev/null +++ b/webview_all_linux/linux/src/webview/download_policy.h @@ -0,0 +1,24 @@ +#ifndef WEBVIEW_ALL_LINUX_DOWNLOAD_POLICY_H_ +#define WEBVIEW_ALL_LINUX_DOWNLOAD_POLICY_H_ + +#include + +// Owned by a single WebView and destroyed before that WebView is released. +class DownloadPolicy { +public: + explicit DownloadPolicy(WebKitWebView *web_view); + ~DownloadPolicy(); + DownloadPolicy(const DownloadPolicy &) = delete; + DownloadPolicy &operator=(const DownloadPolicy &) = delete; + + void SetEnabled(bool enabled) { enabled_ = enabled; } + +private: + static void OnDownloadStarted(WebKitWebContext *context, + WebKitDownload *download, gpointer user_data); + WebKitWebView *web_view_; + gulong signal_id_; + bool enabled_ = true; +}; + +#endif // WEBVIEW_ALL_LINUX_DOWNLOAD_POLICY_H_ diff --git a/webview_all_linux/linux/src/webview/linux_webview.cc b/webview_all_linux/linux/src/webview/linux_webview.cc index e50c313..5c134eb 100644 --- a/webview_all_linux/linux/src/webview/linux_webview.cc +++ b/webview_all_linux/linux/src/webview/linux_webview.cc @@ -1,5 +1,6 @@ #include "common/method_channel_utils.h" #include "plugin/webview_all_linux_plugin_private.h" +#include "webview/download_policy.h" #include "webview/webview_internal.h" #include @@ -726,6 +727,8 @@ void destroy_linux_webview(gpointer data) { } if (webview->web_view != nullptr) { + delete webview->download_policy; + webview->download_policy = nullptr; webview->visible = FALSE; release_linux_webview_focus(webview); if (webview->plugin != nullptr && !webview->plugin->disposing) { @@ -804,6 +807,7 @@ LinuxWebView *create_linux_webview(WebviewAllLinuxPlugin *self) { webview->vertical_scrollbar_enabled = TRUE; webview->horizontal_scrollbar_enabled = TRUE; webview->zoom_enabled = TRUE; + webview->download_policy = new DownloadPolicy(webview->web_view); webview->media_playback_requires_user_gesture = -1; webview->frame_sequence = 0; webview->over_scroll_behavior = ""; diff --git a/webview_all_linux/linux/src/webview/webview_method_handler.cc b/webview_all_linux/linux/src/webview/webview_method_handler.cc index 85cca48..f6fe4a1 100644 --- a/webview_all_linux/linux/src/webview/webview_method_handler.cc +++ b/webview_all_linux/linux/src/webview/webview_method_handler.cc @@ -1,4 +1,5 @@ #include "common/method_channel_utils.h" +#include "webview/download_policy.h" #include "webview/webview_internal.h" #include @@ -135,6 +136,11 @@ void apply_webkit_settings(LinuxWebView *webview, FlValue *args) { WebKitSettings *settings = webkit_web_view_get_settings(webview->web_view); FlValue *value = nullptr; + value = map_lookup(args, "downloadsEnabled"); + if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_BOOL) { + webview->download_policy->SetEnabled(fl_value_get_bool(value)); + } + value = map_lookup(args, "developerExtrasEnabled"); if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_BOOL) { webkit_settings_set_enable_developer_extras(settings, @@ -768,6 +774,18 @@ void instance_method_call_cb(FlMethodChannel *channel, return; } + if (strcmp(method, "setDownloadsEnabled") == 0) { + FlValue *enabled = map_lookup(args, "enabled"); + if (enabled == nullptr || fl_value_get_type(enabled) != FL_VALUE_TYPE_BOOL) { + respond(method_call, error_response("invalid_arguments", + "enabled must be a boolean")); + return; + } + webview->download_policy->SetEnabled(fl_value_get_bool(enabled)); + respond(method_call, success_response()); + return; + } + if (strcmp(method, "openDevTools") == 0) { WebKitSettings *settings = webkit_web_view_get_settings(webview->web_view); webkit_settings_set_enable_developer_extras(settings, TRUE); diff --git a/webview_all_linux/linux/test/CMakeLists.txt b/webview_all_linux/linux/test/CMakeLists.txt new file mode 100644 index 0000000..003c985 --- /dev/null +++ b/webview_all_linux/linux/test/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.10) +project(webview_all_linux_native_tests LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(WEBKIT REQUIRED IMPORTED_TARGET webkit2gtk-4.1) + +add_executable(download_policy_test + download_policy_test.cc + ../src/webview/download_policy.cc +) +target_include_directories(download_policy_test PRIVATE ../src) +target_link_libraries(download_policy_test PRIVATE PkgConfig::WEBKIT) +target_compile_options(download_policy_test PRIVATE -Wall -Wextra -Werror + -Wno-unused-parameter) + +enable_testing() +add_test(NAME download_policy COMMAND download_policy_test) +# Download policy tests do not use the desktop accessibility service. +set_tests_properties(download_policy PROPERTIES + TIMEOUT 60 + ENVIRONMENT "NO_AT_BRIDGE=1" +) diff --git a/webview_all_linux/linux/test/download_policy_test.cc b/webview_all_linux/linux/test/download_policy_test.cc new file mode 100644 index 0000000..69fe320 --- /dev/null +++ b/webview_all_linux/linux/test/download_policy_test.cc @@ -0,0 +1,176 @@ +#include "webview/download_policy.h" + +#include +#include + +#include +#include + +namespace { + +struct DownloadResult { + GMainLoop *loop; + std::string destination; + bool destination_requested = false; + bool cancelled = false; + bool finished = false; + bool timed_out = false; +}; + +class Fixture { +public: + Fixture() { + GError *error = nullptr; + directory_ = g_dir_make_tmp("webview-all-download-test-XXXXXX", &error); + g_assert_no_error(error); + server_ = soup_server_new(nullptr, nullptr); + soup_server_add_handler( + server_, nullptr, + [](SoupServer *, SoupServerMessage *message, const char *, GHashTable *, + gpointer) { + soup_server_message_set_status(message, SOUP_STATUS_OK, nullptr); + soup_message_headers_append( + soup_server_message_get_response_headers(message), + "Content-Disposition", "attachment; filename=test.txt"); + soup_server_message_set_response( + message, "text/plain", SOUP_MEMORY_STATIC, "download test", 13); + }, + nullptr, nullptr); + soup_server_listen_local(server_, 0, SOUP_SERVER_LISTEN_IPV4_ONLY, &error); + g_assert_no_error(error); + GSList *uris = soup_server_get_uris(server_); + gchar *uri = g_uri_to_string(static_cast(uris->data)); + url_ = uri; + g_free(uri); + g_slist_free_full(uris, reinterpret_cast(g_uri_unref)); + context_ = webkit_web_context_new_ephemeral(); + first_ = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(context_)); + second_ = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(context_)); + g_object_ref_sink(first_); + g_object_ref_sink(second_); + first_policy_ = std::make_unique(first_); + second_policy_ = std::make_unique(second_); + } + + ~Fixture() { + first_policy_.reset(); + second_policy_.reset(); + gtk_widget_destroy(GTK_WIDGET(first_)); + gtk_widget_destroy(GTK_WIDGET(second_)); + g_object_unref(first_); + g_object_unref(second_); + g_object_unref(context_); + soup_server_disconnect(server_); + g_object_unref(server_); + g_assert_cmpint(g_rmdir(directory_), ==, 0); + g_free(directory_); + } + + void Check(WebKitWebView *view, bool allowed) { + const std::string path = std::string(directory_) + "/download.txt"; + gchar *destination = g_filename_to_uri(path.c_str(), nullptr, nullptr); + DownloadResult result{g_main_loop_new(nullptr, FALSE), destination}; + g_free(destination); + WebKitDownload *download = + view != nullptr + ? webkit_web_view_download_uri(view, url_.c_str()) + : webkit_web_context_download_uri(context_, url_.c_str()); + g_signal_connect(download, "decide-destination", + G_CALLBACK(+[](WebKitDownload *download, const gchar *, + gpointer data) -> gboolean { + auto *result = static_cast(data); + result->destination_requested = true; + webkit_download_set_destination( + download, result->destination.c_str()); + return TRUE; + }), + &result); + g_signal_connect( + download, "failed", + G_CALLBACK(+[](WebKitDownload *, GError *error, gpointer data) { + auto *result = static_cast(data); + g_assert_error(error, WEBKIT_DOWNLOAD_ERROR, + WEBKIT_DOWNLOAD_ERROR_CANCELLED_BY_USER); + result->cancelled = true; + }), + &result); + g_signal_connect(download, "finished", + G_CALLBACK(+[](WebKitDownload *, gpointer data) { + auto *result = static_cast(data); + result->finished = true; + g_main_loop_quit(result->loop); + }), + &result); + const guint timeout = g_timeout_add_seconds( + 10, + +[](gpointer data) -> gboolean { + auto *result = static_cast(data); + result->timed_out = true; + g_main_loop_quit(result->loop); + return G_SOURCE_REMOVE; + }, + &result); + g_main_loop_run(result.loop); + g_assert_false(result.timed_out); + g_source_remove(timeout); + g_assert_true(result.finished); + g_assert_cmpint(result.cancelled, ==, !allowed); + g_assert_cmpint(result.destination_requested, ==, allowed); + g_assert_cmpint(g_file_test(path.c_str(), G_FILE_TEST_EXISTS), ==, allowed); + if (allowed) { + gchar *contents = nullptr; + gsize length = 0; + g_assert_true( + g_file_get_contents(path.c_str(), &contents, &length, nullptr)); + g_assert_cmpstr(contents, ==, "download test"); + g_free(contents); + g_assert_cmpint(g_remove(path.c_str()), ==, 0); + } + g_object_unref(download); + g_main_loop_unref(result.loop); + } + + WebKitWebView *first_; + WebKitWebView *second_; + std::unique_ptr first_policy_; + std::unique_ptr second_policy_; + +private: + WebKitWebContext *context_; + SoupServer *server_; + gchar *directory_; + std::string url_; +}; + +void TestScopeAndRuntimeChanges() { + Fixture fixture; + fixture.Check(fixture.first_, true); + fixture.first_policy_->SetEnabled(false); + fixture.Check(fixture.first_, false); + fixture.Check(fixture.second_, true); + fixture.first_policy_->SetEnabled(true); + fixture.Check(fixture.first_, true); + fixture.first_policy_->SetEnabled(false); + fixture.Check(fixture.first_, false); +} + +void TestCleanupAndContextDownloads() { + Fixture fixture; + fixture.first_policy_->SetEnabled(false); + fixture.second_policy_->SetEnabled(false); + fixture.Check(nullptr, true); + fixture.first_policy_.reset(); + fixture.Check(fixture.first_, true); + fixture.Check(fixture.second_, false); +} + +} // namespace + +int main(int argc, char **argv) { + gtk_test_init(&argc, &argv, nullptr); + g_test_add_func("/downloads/scope-and-runtime-changes", + TestScopeAndRuntimeChanges); + g_test_add_func("/downloads/cleanup-and-context-downloads", + TestCleanupAndContextDownloads); + return g_test_run(); +} diff --git a/webview_all_linux/test/linux_webview_platform_test.dart b/webview_all_linux/test/linux_webview_platform_test.dart index a90bff1..81562bb 100644 --- a/webview_all_linux/test/linux_webview_platform_test.dart +++ b/webview_all_linux/test/linux_webview_platform_test.dart @@ -861,6 +861,7 @@ void main() { final LinuxWebViewController controller = LinuxWebViewController( const LinuxWebViewControllerCreationParams( developerExtrasEnabled: true, + downloadsEnabled: false, javascriptCanOpenWindowsAutomatically: true, mediaPlaybackRequiresUserGesture: false, mediaPlaybackAllowsInline: true, @@ -880,6 +881,7 @@ void main() { expect(calls.first.method, 'applySettings'); expect(calls.first.arguments, { 'developerExtrasEnabled': true, + 'downloadsEnabled': false, 'javascriptCanOpenWindowsAutomatically': true, 'mediaPlaybackRequiresUserGesture': false, 'mediaPlaybackAllowsInline': true, @@ -946,6 +948,35 @@ void main() { expect(calls[12].arguments, {'zoomFactor': 1.25}); }); + test( + 'leaves downloads enabled by default and forwards runtime changes', + () async { + final calls = []; + _mockLinuxWebViewCreation(onInstanceCall: calls.add); + final controller = LinuxWebViewController( + const LinuxWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( + PlatformWebViewControllerCreationParams(), + ), + ); + addTearDown(controller.dispose); + await controller.setDownloadsEnabled(false); + await controller.setDownloadsEnabled(true); + expect(calls.map((call) => call.method), [ + 'setDownloadsEnabled', + 'setDownloadsEnabled', + ]); + expect(calls.map((call) => call.arguments), [ + {'enabled': false}, + {'enabled': true}, + ]); + await controller.dispose(); + await expectLater( + controller.setDownloadsEnabled(false), + throwsStateError, + ); + }, + ); + test('loads requests with method headers and body', () async { final List calls = []; _mockLinuxWebViewCreation(onInstanceCall: calls.add); diff --git a/webview_all_ohos/CHANGELOG-ZH.md b/webview_all_ohos/CHANGELOG-ZH.md index 12a3f60..f361e91 100644 --- a/webview_all_ohos/CHANGELOG-ZH.md +++ b/webview_all_ohos/CHANGELOG-ZH.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Windows 新增开发者工具、浏览器快捷键和下载开关,支持创建时配置及运行时调整。 +* Windows 默认关闭开发者工具的菜单和快捷键入口,可按需开启。 +* Linux 新增下载开关,可按 WebView 单独控制是否允许下载。 +* 修复 Web 关闭 JavaScript 时覆盖自定义 iframe sandbox 限制的问题。 + ## 1.4.1 * 修复 Windows 应用退出时的崩溃。 diff --git a/webview_all_ohos/CHANGELOG.md b/webview_all_ohos/CHANGELOG.md index dbead51..cfe6648 100644 --- a/webview_all_ohos/CHANGELOG.md +++ b/webview_all_ohos/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Add Windows controls for DevTools, browser shortcuts, and downloads, configurable at creation and at runtime. +* Disable Windows DevTools menu and keyboard entry points by default; enable them explicitly when needed. +* Add per-WebView download controls on Linux. +* Preserve custom iframe sandbox restrictions when disabling JavaScript on Web. + ## 1.4.1 * Fix a crash when exiting Windows applications. diff --git a/webview_all_web/CHANGELOG-ZH.md b/webview_all_web/CHANGELOG-ZH.md index 12a3f60..f361e91 100644 --- a/webview_all_web/CHANGELOG-ZH.md +++ b/webview_all_web/CHANGELOG-ZH.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Windows 新增开发者工具、浏览器快捷键和下载开关,支持创建时配置及运行时调整。 +* Windows 默认关闭开发者工具的菜单和快捷键入口,可按需开启。 +* Linux 新增下载开关,可按 WebView 单独控制是否允许下载。 +* 修复 Web 关闭 JavaScript 时覆盖自定义 iframe sandbox 限制的问题。 + ## 1.4.1 * 修复 Windows 应用退出时的崩溃。 diff --git a/webview_all_web/CHANGELOG.md b/webview_all_web/CHANGELOG.md index dbead51..cfe6648 100644 --- a/webview_all_web/CHANGELOG.md +++ b/webview_all_web/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Add Windows controls for DevTools, browser shortcuts, and downloads, configurable at creation and at runtime. +* Disable Windows DevTools menu and keyboard entry points by default; enable them explicitly when needed. +* Add per-WebView download controls on Linux. +* Preserve custom iframe sandbox restrictions when disabling JavaScript on Web. + ## 1.4.1 * Fix a crash when exiting Windows applications. diff --git a/webview_all_web/lib/src/web_webview_controller.dart b/webview_all_web/lib/src/web_webview_controller.dart index 483446a..f8f062e 100644 --- a/webview_all_web/lib/src/web_webview_controller.dart +++ b/webview_all_web/lib/src/web_webview_controller.dart @@ -136,7 +136,8 @@ class WebWebViewControllerCreationParams /// Value for the iframe `allow` attribute. final String? iFrameAllow; - /// Value for the iframe `sandbox` attribute while JavaScript is unrestricted. + /// Permissions for the iframe `sandbox` attribute. Disabling JavaScript + /// removes `allow-scripts` without adding permissions to an explicit sandbox. final String? iFrameSandbox; /// Value for the iframe `referrerpolicy` attribute. @@ -865,7 +866,14 @@ class WebWebViewController extends PlatformWebViewController { if (_javaScriptMode == JavaScriptMode.disabled) { _webWebViewParams.iFrame.setAttribute( 'sandbox', - _javaScriptDisabledSandbox, + _customSandbox == null + ? _javaScriptDisabledSandbox + : _customSandbox! + .split(RegExp(r'[\t\n\f\r ]+')) + .where( + (String token) => token.toLowerCase() != 'allow-scripts', + ) + .join(' '), ); } else { _setIFrameAttribute(_webWebViewParams.iFrame, 'sandbox', _customSandbox); diff --git a/webview_all_web/test/web_webview_controller_test.dart b/webview_all_web/test/web_webview_controller_test.dart index f687cb8..9ce0ee3 100644 --- a/webview_all_web/test/web_webview_controller_test.dart +++ b/webview_all_web/test/web_webview_controller_test.dart @@ -1299,18 +1299,11 @@ void main() { await controller.setJavaScriptMode(JavaScriptMode.disabled); - expect(params.iFrame.getAttribute('sandbox'), isNotNull); - expect( - params.iFrame.getAttribute('sandbox'), - isNot(contains('allow-scripts')), - ); + expect(params.iFrame.getAttribute('sandbox'), 'allow-forms'); await controller.setIFrameSandbox('allow-popups'); - expect( - params.iFrame.getAttribute('sandbox'), - isNot(contains('allow-scripts')), - ); + expect(params.iFrame.getAttribute('sandbox'), 'allow-popups'); await controller.setJavaScriptMode(JavaScriptMode.unrestricted); @@ -1322,6 +1315,38 @@ void main() { }, ); + test( + 'keeps explicit sandbox permissions when scripts are disabled', + () async { + final params = WebWebViewControllerCreationParams(iFrameSandbox: ''); + final controller = WebWebViewController(params); + await controller.setJavaScriptMode(JavaScriptMode.disabled); + expect(params.iFrame.getAttribute('sandbox'), ''); + + await controller.setIFrameSandbox( + 'ALLOW-SCRIPTS\tallow-downloads\nallow-forms', + ); + expect( + params.iFrame.getAttribute('sandbox'), + 'allow-downloads allow-forms', + ); + await controller.setJavaScriptMode(JavaScriptMode.unrestricted); + expect( + params.iFrame.getAttribute('sandbox'), + 'ALLOW-SCRIPTS\tallow-downloads\nallow-forms', + ); + + await controller.setJavaScriptMode(JavaScriptMode.disabled); + await controller.setIFrameSandbox(null); + expect( + params.iFrame.getAttribute('sandbox'), + isNot(contains('allow-scripts')), + ); + await controller.setJavaScriptMode(JavaScriptMode.unrestricted); + expect(params.iFrame.hasAttribute('sandbox'), isFalse); + }, + ); + test('sets and removes custom iframe attributes at runtime', () async { final params = WebWebViewControllerCreationParams(); final controller = WebWebViewController(params); diff --git a/webview_all_windows/CHANGELOG-ZH.md b/webview_all_windows/CHANGELOG-ZH.md index 12a3f60..f361e91 100644 --- a/webview_all_windows/CHANGELOG-ZH.md +++ b/webview_all_windows/CHANGELOG-ZH.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Windows 新增开发者工具、浏览器快捷键和下载开关,支持创建时配置及运行时调整。 +* Windows 默认关闭开发者工具的菜单和快捷键入口,可按需开启。 +* Linux 新增下载开关,可按 WebView 单独控制是否允许下载。 +* 修复 Web 关闭 JavaScript 时覆盖自定义 iframe sandbox 限制的问题。 + ## 1.4.1 * 修复 Windows 应用退出时的崩溃。 diff --git a/webview_all_windows/CHANGELOG.md b/webview_all_windows/CHANGELOG.md index dbead51..cfe6648 100644 --- a/webview_all_windows/CHANGELOG.md +++ b/webview_all_windows/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Add Windows controls for DevTools, browser shortcuts, and downloads, configurable at creation and at runtime. +* Disable Windows DevTools menu and keyboard entry points by default; enable them explicitly when needed. +* Add per-WebView download controls on Linux. +* Preserve custom iframe sandbox restrictions when disabling JavaScript on Web. + ## 1.4.1 * Fix a crash when exiting Windows applications. diff --git a/webview_all_windows/lib/src/windows_webview_api.g.dart b/webview_all_windows/lib/src/windows_webview_api.g.dart index fb74fec..471d8a3 100644 --- a/webview_all_windows/lib/src/windows_webview_api.g.dart +++ b/webview_all_windows/lib/src/windows_webview_api.g.dart @@ -1396,6 +1396,69 @@ class WindowsWebViewHostApi { ); } + Future setDevToolsEnabled(int textureId, bool enabled) async { + final pigeonVar_channelName = + 'com.abandoft.pigeon.webview_all_windows.WindowsWebViewHostApi.setDevToolsEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [textureId, enabled], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future setBrowserAcceleratorKeysEnabled( + int textureId, + bool enabled, + ) async { + final pigeonVar_channelName = + 'com.abandoft.pigeon.webview_all_windows.WindowsWebViewHostApi.setBrowserAcceleratorKeysEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [textureId, enabled], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + + Future setDownloadsEnabled(int textureId, bool enabled) async { + final pigeonVar_channelName = + 'com.abandoft.pigeon.webview_all_windows.WindowsWebViewHostApi.setDownloadsEnabled$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [textureId, enabled], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + Future setZoomFactor(int textureId, double zoomFactor) async { final pigeonVar_channelName = 'com.abandoft.pigeon.webview_all_windows.WindowsWebViewHostApi.setZoomFactor$pigeonVar_messageChannelSuffix'; diff --git a/webview_all_windows/lib/src/windows_webview_controller.dart b/webview_all_windows/lib/src/windows_webview_controller.dart index a2824c8..4dd6b53 100644 --- a/webview_all_windows/lib/src/windows_webview_controller.dart +++ b/webview_all_windows/lib/src/windows_webview_controller.dart @@ -61,16 +61,39 @@ class WindowsWebViewControllerCreationParams /// Creates a new [WindowsWebViewControllerCreationParams]. const WindowsWebViewControllerCreationParams({ this.popupWindowPolicy = WindowsPopupWindowPolicy.sameWindow, + this.devToolsEnabled = false, + this.browserAcceleratorKeysEnabled, + this.downloadsEnabled, }); /// Creates a [WindowsWebViewControllerCreationParams] from generic params. const WindowsWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( PlatformWebViewControllerCreationParams params, { this.popupWindowPolicy = WindowsPopupWindowPolicy.sameWindow, + this.devToolsEnabled = false, + this.browserAcceleratorKeysEnabled, + this.downloadsEnabled, }); /// How popup windows should be handled. final WindowsPopupWindowPolicy popupWindowPolicy; + + /// Whether users can open DevTools through menus and keyboard shortcuts. + /// + /// Disabled by default in all build modes. Set to true to enable user access. + /// Does not prevent the app from explicitly calling + /// [WindowsWebViewController.openDevTools]. + final bool devToolsEnabled; + + /// Whether browser shortcuts such as reload, print and find are enabled. + /// + /// Null preserves the WebView2 default (enabled). Editing shortcuts remain + /// available. Disabling requires ICoreWebView2Settings3. + final bool? browserAcceleratorKeysEnabled; + + /// Whether this WebView can start downloads. Null preserves the default + /// (enabled). Disabling downloads requires ICoreWebView2_4. + final bool? downloadsEnabled; } /// Windows-specific creation parameters for [WindowsWebViewWidget]. @@ -168,6 +191,29 @@ class WindowsWebViewController extends PlatformWebViewController { String? _pageStartedUrl; String? _title; String? _userAgent; + late final _WebView2Setting _devToolsEnabled = _WebView2Setting(( + bool enabled, + ) { + _throwIfDisposed(); + return _webviewController.setDevToolsEnabled(enabled); + }, _windowsParams.devToolsEnabled); + late final _WebView2Setting _browserAcceleratorKeysEnabled = + _WebView2Setting((bool enabled) { + _throwIfDisposed(); + return _webviewController.setBrowserAcceleratorKeysEnabled(enabled); + }, _windowsParams.browserAcceleratorKeysEnabled); + late final _WebView2Setting _downloadsEnabled = _WebView2Setting(( + bool enabled, + ) { + _throwIfDisposed(); + return _webviewController.setDownloadsEnabled(enabled); + }, _windowsParams.downloadsEnabled); + + Iterable<_WebView2Setting> get _settings => <_WebView2Setting>[ + _devToolsEnabled, + _browserAcceleratorKeysEnabled, + _downloadsEnabled, + ]; bool _canGoBack = false; bool _canGoForward = false; bool _verticalScrollBarEnabled = true; @@ -313,7 +359,6 @@ class WindowsWebViewController extends PlatformWebViewController { }, ); _throwIfDisposed(); - _subscriptions.addAll(>[ _webviewController.url.listen((String url) { weakThis.target?._handleUrlChanged(url); @@ -351,15 +396,31 @@ class WindowsWebViewController extends PlatformWebViewController { _throwIfDisposed(); await _initializationFuture!; _throwIfDisposed(); + await _applySettings(); } - Future _retryInitialization() { - if (_isDisposed) { - return Future.error(_disposedStateError()); + Future _applySettings() async { + for (final _WebView2Setting setting in _settings) { + _throwIfDisposed(); + await setting.apply(); } - final Future initializationFuture = _prepareRetry(); - _setInitializationFuture(initializationFuture); - return initializationFuture; + _throwIfDisposed(); + } + + Future _retryInitialization() async { + _throwIfDisposed(); + final Future previous = _initializationFuture!; + try { + await previous; + } catch (_) { + _throwIfDisposed(); + if (identical(_initializationFuture, previous)) { + _setInitializationFuture(_prepareRetry()); + } + await _initializationFuture!; + } + // A setting failure leaves the native view and its subscriptions intact. + await _applySettings(); } void _setInitializationFuture(Future future) { @@ -432,6 +493,10 @@ class WindowsWebViewController extends PlatformWebViewController { // Continue with cleanup. } + await Future.wait( + _settings.map((_WebView2Setting setting) => setting.settled), + ); + Object? cleanupError; StackTrace? cleanupStackTrace; final List> subscriptions = @@ -1338,6 +1403,42 @@ ${params.functionBody} await _webviewController.setZoomControlEnabled(enabled); } + /// Sets whether users can open DevTools through menus and keyboard shortcuts. + /// + /// Disabled by default. Does not close existing DevTools or prevent the app + /// from calling [openDevTools]. Changes apply on the next top-level navigation. + Future setDevToolsEnabled(bool enabled) async { + _throwIfDisposed(); + _devToolsEnabled.request(enabled); + await _ensureInitialized(); + } + + /// Sets whether browser-specific accelerator keys are enabled. + /// + /// When disabled, WebView2 stops acting on browser-specific keys such as + /// F5 (reload), Ctrl+P (print), Ctrl+F (find) and Ctrl+Shift+I (DevTools). + /// Enabled by default. Disabling requires `ICoreWebView2Settings3`; on a + /// Runtime without it this throws a [PlatformException]. + /// Changes apply on the next top-level navigation. Text-editing shortcuts + /// remain enabled; disabled browser shortcuts are not forwarded to Flutter. + Future setBrowserAcceleratorKeysEnabled(bool enabled) async { + _throwIfDisposed(); + _browserAcceleratorKeysEnabled.request(enabled); + await _ensureInitialized(); + } + + /// Sets whether the page may start downloads. + /// + /// When disabled, new downloads are cancelled without saving the downloaded + /// file or showing the download UI. Existing downloads are not cancelled. + /// Enabled by default. Disabling requires `ICoreWebView2_4`; an unavailable + /// download handler causes a [PlatformException], not a silent success. + Future setDownloadsEnabled(bool enabled) async { + _throwIfDisposed(); + _downloadsEnabled.request(enabled); + await _ensureInitialized(); + } + @override Future setBackgroundColor(Color color) async { await _ensureInitialized(); @@ -2193,3 +2294,50 @@ class _WindowsWebViewPermissionRequest } } } + +/// A WebView2 setting owned by [WindowsWebViewController]. +/// +/// Retains requested values across initialization failures. Native creation and +/// settings application have separate futures so a failed setting can be +/// corrected without leaving the controller's initialization future failed. +final class _WebView2Setting { + _WebView2Setting(this._push, this._requested); + + final Future Function(T value) _push; + T? _requested; + T? _applied; + Future? _tail; + + Future get settled async { + await _tail; + } + + void request(T value) => _requested = value; + + /// Sends the requested value unless the native WebView already has it. + Future apply() { + if (_tail == null && _requested == _applied) { + return Future.value(); + } + final Future? previous = _tail; + final Future result = () async { + await previous; + while (_requested != null && _requested != _applied) { + final T value = _requested!; + await _push(value); + _applied = value; + } + }(); + // Report failures to this caller without poisoning subsequent requests. + final Future tail = result.catchError((Object _) {}); + _tail = tail; + unawaited( + tail.then((_) { + if (identical(_tail, tail)) { + _tail = null; + } + }), + ); + return result; + } +} diff --git a/webview_all_windows/lib/src/windows_webview_native.dart b/webview_all_windows/lib/src/windows_webview_native.dart index 02e46f8..2cec76b 100644 --- a/webview_all_windows/lib/src/windows_webview_native.dart +++ b/webview_all_windows/lib/src/windows_webview_native.dart @@ -1029,6 +1029,34 @@ class WebviewController extends ValueNotifier { return _hostApi.setZoomControlEnabled(_textureId, enabled); } + /// Sets whether users can open DevTools through menus and keyboard shortcuts. + /// Does not prevent explicit calls to [openDevTools]. + Future setDevToolsEnabled(bool enabled) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _hostApi.setDevToolsEnabled(_textureId, enabled); + } + + /// Sets whether browser-specific accelerator keys (F5, Ctrl+P, Ctrl+F, ...) are enabled. + Future setBrowserAcceleratorKeysEnabled(bool enabled) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _hostApi.setBrowserAcceleratorKeysEnabled(_textureId, enabled); + } + + /// Sets whether new downloads may save files. Existing downloads are unchanged. + Future setDownloadsEnabled(bool enabled) async { + if (_isDisposed) { + return; + } + assert(value.isInitialized); + return _hostApi.setDownloadsEnabled(_textureId, enabled); + } + /// Sets the zoom factor. Future setZoomFactor(double zoomFactor) async { if (_isDisposed) { diff --git a/webview_all_windows/lib/src/windows_webview_types.dart b/webview_all_windows/lib/src/windows_webview_types.dart index 50c8097..74bc3a6 100644 --- a/webview_all_windows/lib/src/windows_webview_types.dart +++ b/webview_all_windows/lib/src/windows_webview_types.dart @@ -10,6 +10,7 @@ enum WebviewDownloadEventKind { downloadStarted, downloadCompleted, downloadProgress, + downloadCancelled, } /// Pointer Event kind diff --git a/webview_all_windows/pigeons/windows_webview_api.dart b/webview_all_windows/pigeons/windows_webview_api.dart index be993c8..4b77cf7 100644 --- a/webview_all_windows/pigeons/windows_webview_api.dart +++ b/webview_all_windows/pigeons/windows_webview_api.dart @@ -206,6 +206,12 @@ abstract class WindowsWebViewHostApi { void setZoomControlEnabled(int textureId, bool enabled); + void setDevToolsEnabled(int textureId, bool enabled); + + void setBrowserAcceleratorKeysEnabled(int textureId, bool enabled); + + void setDownloadsEnabled(int textureId, bool enabled); + void setZoomFactor(int textureId, double zoomFactor); void setPopupWindowPolicy(int textureId, int policy); diff --git a/webview_all_windows/test/webview_windows_test.dart b/webview_all_windows/test/webview_windows_test.dart index e66c914..9a1030f 100644 --- a/webview_all_windows/test/webview_windows_test.dart +++ b/webview_all_windows/test/webview_windows_test.dart @@ -23,8 +23,12 @@ void main() { _mockWindowsWebViewCreation(); }); - tearDown(() { - _clearWindowsWebViewCreationMock(); + tearDown(() async { + try { + await _disposeTestControllers(); + } finally { + _clearWindowsWebViewCreationMock(); + } }); test('registerWith sets the Windows WebView platform implementation', () { @@ -42,13 +46,11 @@ void main() { test('creates Windows platform implementation objects', () { final platform = WindowsWebViewPlatform(); - - expect( - platform.createPlatformWebViewController( - const PlatformWebViewControllerCreationParams(), - ), - isA(), + final controller = platform.createPlatformWebViewController( + const PlatformWebViewControllerCreationParams(), ); + expect(controller, isA()); + _controllerDisposals.add(controller.dispose); expect( platform.createPlatformNavigationDelegate( const PlatformNavigationDelegateCreationParams(), @@ -58,7 +60,7 @@ void main() { expect( platform.createPlatformWebViewWidget( PlatformWebViewWidgetCreationParams( - controller: WindowsWebViewController( + controller: _createWindowsController( const PlatformWebViewControllerCreationParams(), ), ), @@ -198,7 +200,7 @@ void main() { return 'null'; }, ); - final WindowsWebViewController controller = WindowsWebViewController( + final WindowsWebViewController controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -245,7 +247,7 @@ void main() { }, onRemoveScript: removedScripts.add, ); - final WindowsWebViewController controller = WindowsWebViewController( + final WindowsWebViewController controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -277,7 +279,7 @@ void main() { }, ); final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); await controller.initialize(); await controller.dispose(); @@ -293,7 +295,7 @@ void main() { disposeCallCount++; }, ); - final WindowsWebViewController controller = WindowsWebViewController( + final WindowsWebViewController controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -314,7 +316,7 @@ void main() { disposeCallCount++; }, ); - final WindowsWebViewController controller = WindowsWebViewController( + final WindowsWebViewController controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -328,7 +330,7 @@ void main() { test('native controller unregisters callbacks and closes streams', () async { final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); final List> streamDoneFutures = >[ controller.url.drain(), controller.loadingState.drain(), @@ -360,7 +362,7 @@ void main() { test('native controller can retry after an initialization failure', () async { _mockWindowsWebViewCreation(creationFailureCount: 1); final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); await expectLater( controller.initialize(), @@ -376,13 +378,13 @@ void main() { await controller.dispose(); }); - testWidgets('native surface attachment follows the widget lifecycle', ( + _widgetTest('native surface attachment follows the widget lifecycle', ( WidgetTester tester, ) async { final List attachmentStates = []; _mockWindowsWebViewCreation(onSetSurfaceAttached: attachmentStates.add); final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); await controller.initialize(); await tester.pumpWidget( @@ -404,13 +406,13 @@ void main() { expect(attachmentStates, [true, false]); }); - testWidgets('native surface detaches when an ancestor stops painting', ( + _widgetTest('native surface detaches when an ancestor stops painting', ( WidgetTester tester, ) async { final List attachmentStates = []; _mockWindowsWebViewCreation(onSetSurfaceAttached: attachmentStates.add); final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); await controller.initialize(); double opacity = 1; @@ -443,13 +445,13 @@ void main() { expect(attachmentStates, [true, false, true]); }); - testWidgets('native surface follows the application lifecycle', ( + _widgetTest('native surface follows the application lifecycle', ( WidgetTester tester, ) async { final List attachmentStates = []; _mockWindowsWebViewCreation(onSetSurfaceAttached: attachmentStates.add); final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); await controller.initialize(); addTearDown(() { tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); @@ -478,13 +480,13 @@ void main() { expect(attachmentStates, [true, false, true]); }); - testWidgets('native surface updates an explicit scale factor', ( + _widgetTest('native surface updates an explicit scale factor', ( WidgetTester tester, ) async { final List sizes = []; _mockWindowsWebViewCreation(onSetSize: sizes.add); final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); await controller.initialize(); Widget buildWebView(double scaleFactor) { @@ -511,16 +513,15 @@ void main() { await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(); - await tester.runAsync(controller.dispose); }); - testWidgets('native surface follows device-pixel-ratio changes', ( + _widgetTest('native surface follows device-pixel-ratio changes', ( WidgetTester tester, ) async { final List sizes = []; _mockWindowsWebViewCreation(onSetSize: sizes.add); final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); await controller.initialize(); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetDevicePixelRatio); @@ -548,10 +549,9 @@ void main() { await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(); - await tester.runAsync(controller.dispose); }); - testWidgets('native rendering errors are latched and can be retried', ( + _widgetTest('native rendering errors are latched and can be retried', ( WidgetTester tester, ) async { var sizeCalls = 0; @@ -560,7 +560,7 @@ void main() { onSetSize: (WindowsSizeData size) => sizeCalls += 1, ); final native_webview.WebviewController controller = - native_webview.WebviewController(); + _createNativeController(); await controller.initialize(); await tester.pumpWidget( @@ -595,10 +595,9 @@ void main() { await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(); - await tester.runAsync(controller.dispose); }); - testWidgets('initialization error offers install and refresh actions', ( + _widgetTest('initialization error offers install and refresh actions', ( WidgetTester tester, ) async { var openDownloadPageCount = 0; @@ -609,7 +608,7 @@ void main() { openDownloadPageCount += 1; }, ); - final WindowsWebViewController controller = WindowsWebViewController( + final WindowsWebViewController controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final WindowsWebViewWidget platformWidget = WindowsWebViewWidget( @@ -643,7 +642,7 @@ void main() { expect(find.byType(Texture), findsOneWidget); }); - testWidgets( + _widgetTest( 'rendering error offers refresh without a runtime install action', (WidgetTester tester) async { _mockWindowsWebViewCreation( @@ -656,7 +655,7 @@ void main() { 'webView2RuntimeVersion': '1.0.0.0', }, ); - final WindowsWebViewController controller = WindowsWebViewController( + final WindowsWebViewController controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final WindowsWebViewWidget platformWidget = WindowsWebViewWidget( @@ -766,7 +765,7 @@ void main() { }, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final alertRequests = []; @@ -845,7 +844,7 @@ void main() { test( 'dispatches HTTP auth requests through the navigation delegate', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -881,7 +880,7 @@ void main() { ); test('cancels HTTP auth requests without a handler', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); await controller.currentUrl(); @@ -898,7 +897,7 @@ void main() { }); test('dispatches SSL auth errors through the navigation delegate', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -934,7 +933,7 @@ void main() { }); test('cancels SSL auth errors without a handler', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); await controller.currentUrl(); @@ -954,7 +953,7 @@ void main() { final loadRequests = []; _mockWindowsWebViewCreation(onLoadRequest: loadRequests.add); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final body = Uint8List.fromList([1, 2, 3]); @@ -997,7 +996,7 @@ void main() { final File file = File('${tempDir.path}/index.html') ..writeAsStringSync(''); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1022,7 +1021,7 @@ void main() { final loadRequests = []; _mockWindowsWebViewCreation(onLoadRequest: loadRequests.add); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1039,7 +1038,7 @@ void main() { }); test('dispatches HTTP response errors from Windows events', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -1087,7 +1086,7 @@ void main() { }); test('dispatches web resource errors from Windows events', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -1132,7 +1131,7 @@ void main() { }, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final List consoleMessages = @@ -1205,7 +1204,7 @@ void main() { }, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1244,7 +1243,7 @@ void main() { onSetJavaScriptEnabled: javaScriptEnabledValues.add, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1263,7 +1262,7 @@ void main() { onGetUserAgent: () => 'DefaultWebView2/1.0', ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1289,7 +1288,7 @@ void main() { }, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1305,7 +1304,7 @@ void main() { onSetZoomControlEnabled: zoomControlEnabledValues.add, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1315,10 +1314,357 @@ void main() { expect(zoomControlEnabledValues, [false, true]); }); + test( + 'sets DevTools, accelerator keys and downloads through WebView2', + () async { + final devToolsValues = []; + final acceleratorKeysValues = []; + final downloadsValues = []; + _mockWindowsWebViewCreation( + onSetDevToolsEnabled: devToolsValues.add, + onSetBrowserAcceleratorKeysEnabled: acceleratorKeysValues.add, + onSetDownloadsEnabled: downloadsValues.add, + ); + + final controller = _createWindowsController( + const PlatformWebViewControllerCreationParams(), + ); + + await controller.setDevToolsEnabled(false); + await controller.setDevToolsEnabled(true); + await controller.setDevToolsEnabled(false); + await controller.setBrowserAcceleratorKeysEnabled(false); + await controller.setDownloadsEnabled(false); + await controller.setDownloadsEnabled(true); + + expect(devToolsValues, [false, true, false]); + expect(acceleratorKeysValues, [false]); + expect(downloadsValues, [false, true]); + }, + ); + + _widgetTest( + 'WebView2 settings are re-applied after a retried initialization', + (WidgetTester tester) async { + final devToolsValues = []; + final acceleratorKeysValues = []; + final downloadsValues = []; + _mockWindowsWebViewCreation( + creationFailureCount: 1, + onSetDevToolsEnabled: devToolsValues.add, + onSetBrowserAcceleratorKeysEnabled: acceleratorKeysValues.add, + onSetDownloadsEnabled: downloadsValues.add, + ); + final WindowsWebViewController controller = _createWindowsController( + const PlatformWebViewControllerCreationParams(), + ); + + // The first environment fails, so the settings cannot reach WebView2 yet; + // they must still hold once the widget's refresh creates a new one. + for (final Future Function() apply in Function()>[ + () => controller.setDevToolsEnabled(false), + () => controller.setBrowserAcceleratorKeysEnabled(false), + () => controller.setDownloadsEnabled(false), + ]) { + await expectLater(apply(), throwsA(isA())); + } + expect(devToolsValues, isEmpty); + + final WindowsWebViewWidget platformWidget = WindowsWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 800, + height: 600, + child: Builder(builder: platformWidget.build), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Refresh')); + await tester.pumpAndSettle(); + + expect(devToolsValues, [false]); + expect(acceleratorKeysValues, [false]); + expect(downloadsValues, [false]); + }, + ); + + _widgetTest( + 'refresh recovers a setting failure without replacing native subscriptions', + (WidgetTester tester) async { + var creations = 0; + _mockWindowsWebViewCreation( + beforeCreateWebView: () async { + creations++; + }, + onSetDownloadsEnabled: (bool enabled) { + if (!enabled) throw PlatformException(code: 'not_supported'); + }, + ); + final controller = _createWindowsController( + const WindowsWebViewControllerCreationParams(downloadsEnabled: false), + ); + final platformWidget = WindowsWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 800, + height: 600, + child: Builder(builder: platformWidget.build), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Refresh'), findsOneWidget); + expect(find.text('Install Webview2'), findsNothing); + await controller.setDownloadsEnabled(true); + await tester.tap(find.text('Refresh')); + await tester.pumpAndSettle(); + expect(find.text('Refresh'), findsNothing); + expect(find.byType(native_webview.Webview), findsOneWidget); + expect(creations, 1); + }, + ); + + for (final entry in { + 'generic': const PlatformWebViewControllerCreationParams(), + 'Windows': const WindowsWebViewControllerCreationParams(), + 'converted': + const WindowsWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( + PlatformWebViewControllerCreationParams(), + ), + }.entries) { + test( + '${entry.key} params disable DevTools before loading without changing other defaults', + () async { + final events = []; + _mockWindowsWebViewCreation( + onSetDevToolsEnabled: (bool value) => events.add('devTools:$value'), + onSetBrowserAcceleratorKeysEnabled: (bool value) => + events.add('keys:$value'), + onSetDownloadsEnabled: (bool value) => events.add('downloads:$value'), + onLoadRequest: (_) => events.add('load'), + ); + final controller = _createWindowsController(entry.value); + final request = LoadRequestParams( + uri: Uri.parse('https://example.test'), + ); + await controller.loadRequest(request); + await controller.loadRequest(request); + expect(events, ['devTools:false', 'load', 'load']); + }, + ); + } + + for (final entry in { + 'Windows': const WindowsWebViewControllerCreationParams( + devToolsEnabled: true, + ), + 'converted': + const WindowsWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( + PlatformWebViewControllerCreationParams(), + devToolsEnabled: true, + ), + }.entries) { + test( + '${entry.key} params explicitly enable DevTools before loading', + () async { + final events = []; + _mockWindowsWebViewCreation( + onSetDevToolsEnabled: (bool value) => events.add('devTools:$value'), + onLoadRequest: (_) => events.add('load'), + ); + final controller = _createWindowsController(entry.value); + await controller.loadRequest( + LoadRequestParams(uri: Uri.parse('https://example.test')), + ); + expect(events, ['devTools:true', 'load']); + }, + ); + } + + test('opens DevTools explicitly without enabling user shortcuts', () async { + final events = []; + _mockWindowsWebViewCreation( + onSetDevToolsEnabled: (bool value) => events.add('devTools:$value'), + onOpenDevTools: () => events.add('open'), + ); + final controller = _createWindowsController( + const WindowsWebViewControllerCreationParams(), + ); + await controller.openDevTools(); + expect(events, ['devTools:false', 'open']); + }); + + test('applies creation settings before the first navigation', () async { + final events = []; + _mockWindowsWebViewCreation( + onSetDevToolsEnabled: (bool value) => events.add('devTools:$value'), + onSetBrowserAcceleratorKeysEnabled: (bool value) => + events.add('keys:$value'), + onSetDownloadsEnabled: (bool value) => events.add('downloads:$value'), + onLoadRequest: (_) => events.add('load'), + ); + final controller = _createWindowsController( + const WindowsWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( + PlatformWebViewControllerCreationParams(), + devToolsEnabled: false, + browserAcceleratorKeysEnabled: false, + downloadsEnabled: false, + ), + ); + await controller.loadRequest( + LoadRequestParams(uri: Uri.parse('https://example.test')), + ); + expect(events, [ + 'devTools:false', + 'keys:false', + 'downloads:false', + 'load', + ]); + }); + + test('serializes changes while a native setting is in flight', () async { + final values = []; + final started = Completer(); + final release = Completer(); + _mockWindowsWebViewCreation( + onSetDownloadsEnabled: (bool value) async { + values.add(value); + if (value) { + started.complete(); + await release.future; + } + }, + ); + final controller = _createWindowsController( + const WindowsWebViewControllerCreationParams(), + ); + await controller.setDownloadsEnabled(false); + final enabling = controller.setDownloadsEnabled(true); + await started.future; + final disabling = controller.setDownloadsEnabled(false); + release.complete(); + await Future.wait(>[enabling, disabling]); + expect(values, [false, true, false]); + }); + + test( + 'failed settings block navigation but can be corrected without recreating the WebView', + () async { + var creations = 0; + var loads = 0; + _mockWindowsWebViewCreation( + beforeCreateWebView: () async { + creations++; + }, + onSetDownloadsEnabled: (bool enabled) { + if (!enabled) { + throw PlatformException( + code: 'not_supported', + details: {'hresult': '0x80004002'}, + ); + } + }, + onLoadRequest: (_) { + loads++; + }, + ); + final controller = _createWindowsController( + const WindowsWebViewControllerCreationParams(downloadsEnabled: false), + ); + final request = LoadRequestParams(uri: Uri.parse('https://example.test')); + await expectLater( + controller.loadRequest(request), + throwsA( + isA() + .having((error) => error.code, 'code', 'not_supported') + .having((error) => error.details, 'details', { + 'hresult': '0x80004002', + }), + ), + ); + expect(loads, 0); + await controller.setDownloadsEnabled(true); + await controller.loadRequest(request); + expect(loads, 1); + expect(creations, 1); + }, + ); + + test('a transient setting failure can be retried', () async { + var attempts = 0; + _mockWindowsWebViewCreation( + onSetDevToolsEnabled: (_) { + if (attempts++ == 0) { + throw PlatformException(code: 'method_failed'); + } + }, + ); + final controller = _createWindowsController( + const WindowsWebViewControllerCreationParams(), + ); + await expectLater( + controller.setDevToolsEnabled(false), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'method_failed', + ), + ), + ); + await controller.setDevToolsEnabled(false); + expect(attempts, 2); + }); + + test( + 'disposing drains in-flight settings and rejects further changes', + () async { + final started = Completer(); + final release = Completer(); + var disposed = false; + _mockWindowsWebViewCreation( + onSetDownloadsEnabled: (_) async { + started.complete(); + await release.future; + }, + onDisposeWebView: () { + disposed = true; + }, + ); + final controller = _createWindowsController( + const WindowsWebViewControllerCreationParams(), + ); + final setting = controller.setDownloadsEnabled(false); + final failure = expectLater(setting, throwsStateError); + await started.future; + final disposal = controller.dispose(); + expect(disposed, isFalse); + release.complete(); + await failure; + await disposal; + expect(disposed, isTrue); + await expectLater(controller.setDownloadsEnabled(true), throwsStateError); + await expectLater(controller.setDevToolsEnabled(true), throwsStateError); + await expectLater( + controller.setBrowserAcceleratorKeysEnabled(true), + throwsStateError, + ); + }, + ); + test( 'dispatches Windows permission requests and returns decisions', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final requests = []; @@ -1364,7 +1710,7 @@ void main() { test( 'uses the default Windows decision for unsupported permissions', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); var callbackCalled = false; @@ -1397,10 +1743,10 @@ void main() { }, ); - testWidgets('uses safe defaults when application decisions do not complete', ( + _widgetTest('uses safe defaults when application decisions do not complete', ( WidgetTester tester, ) async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -1476,7 +1822,7 @@ void main() { }); test('uses safe defaults when decision callbacks throw', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -1550,7 +1896,7 @@ void main() { test( 'contains callback failures and preserves completed decisions', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -1659,7 +2005,7 @@ void main() { }, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1687,7 +2033,7 @@ void main() { _mockWindowsWebViewCreation( onSetNavigationRequestCallbacksEnabled: callbackStates.add, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -1753,7 +2099,7 @@ void main() { debugPrint = previousDebugPrint; }); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); final delegate = WindowsNavigationDelegate( @@ -1798,7 +2144,7 @@ void main() { return 'null'; }, ); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); const String channelName = 'channel-name"\\line'; @@ -1825,7 +2171,7 @@ void main() { test('escapes base URLs inserted into HTML', () async { final contents = []; _mockWindowsWebViewCreation(onLoadStringContent: contents.add); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1856,7 +2202,7 @@ void main() { addTearDown(() => tempDir.deleteSync(recursive: true)); final File file = File('${tempDir.path}/index.html') ..writeAsStringSync(''); - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1870,7 +2216,7 @@ void main() { }); test('rejects relative file paths and traversing asset keys', () async { - final controller = WindowsWebViewController( + final controller = _createWindowsController( const PlatformWebViewControllerCreationParams(), ); @@ -1885,6 +2231,70 @@ void main() { }); } +final _controllerDisposals = Function()>[]; + +void _widgetTest( + String description, + Future Function(WidgetTester tester) callback, +) { + testWidgets(description, (WidgetTester tester) async { + try { + await callback(tester); + } finally { + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pumpAndSettle(); + // Cancellation crosses the fake clock and the real EventChannel queue. + // Keep both moving until cleanup completes, before removing the mocks. + var completed = false; + Object? cleanupError; + StackTrace? cleanupStack; + final cleanup = _disposeTestControllers().then( + (_) => completed = true, + onError: (Object error, StackTrace stack) { + cleanupError = error; + cleanupStack = stack; + completed = true; + return true; + }, + ); + final elapsed = Stopwatch()..start(); + while (!completed && elapsed.elapsed < const Duration(seconds: 5)) { + await tester.pump(); + await tester.runAsync( + () => Future.delayed(const Duration(milliseconds: 1)), + ); + } + expect(completed, isTrue, reason: 'Controller cleanup did not complete.'); + await cleanup; + if (cleanupError != null) { + Error.throwWithStackTrace(cleanupError!, cleanupStack!); + } + } + }); +} + +Future _disposeTestControllers() async { + final disposals = _controllerDisposals.reversed.toList(); + _controllerDisposals.clear(); + await Future.wait( + disposals.map((dispose) => Future.sync(dispose)), + ); +} + +WindowsWebViewController _createWindowsController( + PlatformWebViewControllerCreationParams params, +) { + final controller = WindowsWebViewController(params); + _controllerDisposals.add(controller.dispose); + return controller; +} + +native_webview.WebviewController _createNativeController() { + final controller = native_webview.WebviewController(); + _controllerDisposals.add(controller.dispose); + return controller; +} + void _mockWindowsWebViewCreation({ int creationFailureCount = 0, String creationFailureCode = 'environment_creation_failed', @@ -1908,6 +2318,10 @@ void _mockWindowsWebViewCreation({ String? clearAllWebsiteDataFailureCode, void Function(bool enabled)? onSetJavaScriptEnabled, void Function(bool enabled)? onSetZoomControlEnabled, + FutureOr Function(bool enabled)? onSetDevToolsEnabled, + void Function()? onOpenDevTools, + FutureOr Function(bool enabled)? onSetBrowserAcceleratorKeysEnabled, + FutureOr Function(bool enabled)? onSetDownloadsEnabled, void Function(bool enabled)? onSetNavigationRequestCallbacksEnabled, void Function(WindowsSizeData size)? onSetSize, int setSizeFailureCount = 0, @@ -2088,6 +2502,46 @@ void _mockWindowsWebViewCreation({ onSetZoomControlEnabled?.call(args[1]! as bool); return _encodePigeonSuccess(); }); + messenger.setMockMessageHandler(_hostApiChannel('openDevTools'), ( + ByteData? message, + ) async { + onOpenDevTools?.call(); + return _encodePigeonSuccess(); + }); + messenger.setMockMessageHandler(_hostApiChannel('setDevToolsEnabled'), ( + ByteData? message, + ) async { + final args = _decodePigeonArgs(message); + try { + await onSetDevToolsEnabled?.call(args[1]! as bool); + } on PlatformException catch (error) { + return _encodePigeonError(error); + } + return _encodePigeonSuccess(); + }); + messenger.setMockMessageHandler( + _hostApiChannel('setBrowserAcceleratorKeysEnabled'), + (ByteData? message) async { + final args = _decodePigeonArgs(message); + try { + await onSetBrowserAcceleratorKeysEnabled?.call(args[1]! as bool); + } on PlatformException catch (error) { + return _encodePigeonError(error); + } + return _encodePigeonSuccess(); + }, + ); + messenger.setMockMessageHandler(_hostApiChannel('setDownloadsEnabled'), ( + ByteData? message, + ) async { + final args = _decodePigeonArgs(message); + try { + await onSetDownloadsEnabled?.call(args[1]! as bool); + } on PlatformException catch (error) { + return _encodePigeonError(error); + } + return _encodePigeonSuccess(); + }); messenger.setMockMessageHandler( _hostApiChannel('setJavaScriptDialogCallbacksEnabled'), (ByteData? message) async { @@ -2194,6 +2648,13 @@ void _clearWindowsWebViewCreationMock() { _hostApiChannel('setZoomControlEnabled'), null, ); + messenger.setMockMessageHandler(_hostApiChannel('setDevToolsEnabled'), null); + messenger.setMockMessageHandler(_hostApiChannel('openDevTools'), null); + messenger.setMockMessageHandler( + _hostApiChannel('setBrowserAcceleratorKeysEnabled'), + null, + ); + messenger.setMockMessageHandler(_hostApiChannel('setDownloadsEnabled'), null); messenger.setMockMessageHandler( _hostApiChannel('setJavaScriptDialogCallbacksEnabled'), null, @@ -2216,6 +2677,14 @@ ByteData? _encodePigeonSuccess() { ]); } +ByteData? _encodePigeonError(PlatformException error) { + return WindowsWebViewHostApi.pigeonChannelCodec.encodeMessage([ + error.code, + error.message, + error.details, + ]); +} + List _decodePigeonArgs(ByteData? message) { return WindowsWebViewHostApi.pigeonChannelCodec.decodeMessage(message!) as List; diff --git a/webview_all_windows/windows/generated/windows_webview_api.g.cpp b/webview_all_windows/windows/generated/windows_webview_api.g.cpp index ef882be..506d3a5 100644 --- a/webview_all_windows/windows/generated/windows_webview_api.g.cpp +++ b/webview_all_windows/windows/generated/windows_webview_api.g.cpp @@ -2742,6 +2742,134 @@ void WindowsWebViewHostApi::SetUp(::flutter::BinaryMessenger *binary_messenger, channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel(binary_messenger, + "com.abandoft.pigeon.webview_all_windows." + "WindowsWebViewHostApi.setDevToolsEnabled" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue &message, + const ::flutter::MessageReply &reply) { + try { + const auto &args = std::get(message); + const auto &encodable_texture_id_arg = args.at(0); + if (encodable_texture_id_arg.IsNull()) { + reply(WrapError("texture_id_arg unexpectedly null.")); + return; + } + const int64_t texture_id_arg = + encodable_texture_id_arg.LongValue(); + const auto &encodable_enabled_arg = args.at(1); + if (encodable_enabled_arg.IsNull()) { + reply(WrapError("enabled_arg unexpectedly null.")); + return; + } + const auto &enabled_arg = std::get(encodable_enabled_arg); + std::optional output = + api->SetDevToolsEnabled(texture_id_arg, enabled_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception &exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "com.abandoft.pigeon.webview_all_windows.WindowsWebViewHostApi." + "setBrowserAcceleratorKeysEnabled" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue &message, + const ::flutter::MessageReply &reply) { + try { + const auto &args = std::get(message); + const auto &encodable_texture_id_arg = args.at(0); + if (encodable_texture_id_arg.IsNull()) { + reply(WrapError("texture_id_arg unexpectedly null.")); + return; + } + const int64_t texture_id_arg = + encodable_texture_id_arg.LongValue(); + const auto &encodable_enabled_arg = args.at(1); + if (encodable_enabled_arg.IsNull()) { + reply(WrapError("enabled_arg unexpectedly null.")); + return; + } + const auto &enabled_arg = std::get(encodable_enabled_arg); + std::optional output = + api->SetBrowserAcceleratorKeysEnabled(texture_id_arg, + enabled_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception &exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, + "com.abandoft.pigeon.webview_all_windows." + "WindowsWebViewHostApi.setDownloadsEnabled" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue &message, + const ::flutter::MessageReply &reply) { + try { + const auto &args = std::get(message); + const auto &encodable_texture_id_arg = args.at(0); + if (encodable_texture_id_arg.IsNull()) { + reply(WrapError("texture_id_arg unexpectedly null.")); + return; + } + const int64_t texture_id_arg = + encodable_texture_id_arg.LongValue(); + const auto &encodable_enabled_arg = args.at(1); + if (encodable_enabled_arg.IsNull()) { + reply(WrapError("enabled_arg unexpectedly null.")); + return; + } + const auto &enabled_arg = std::get(encodable_enabled_arg); + std::optional output = + api->SetDownloadsEnabled(texture_id_arg, enabled_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception &exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } { BasicMessageChannel<> channel(binary_messenger, "com.abandoft.pigeon.webview_all_windows." diff --git a/webview_all_windows/windows/generated/windows_webview_api.g.h b/webview_all_windows/windows/generated/windows_webview_api.g.h index af1a6f3..d1eb4fb 100644 --- a/webview_all_windows/windows/generated/windows_webview_api.g.h +++ b/webview_all_windows/windows/generated/windows_webview_api.g.h @@ -524,6 +524,12 @@ class WindowsWebViewHostApi { int64_t color) = 0; virtual std::optional SetZoomControlEnabled(int64_t texture_id, bool enabled) = 0; + virtual std::optional SetDevToolsEnabled(int64_t texture_id, + bool enabled) = 0; + virtual std::optional + SetBrowserAcceleratorKeysEnabled(int64_t texture_id, bool enabled) = 0; + virtual std::optional SetDownloadsEnabled(int64_t texture_id, + bool enabled) = 0; virtual std::optional SetZoomFactor(int64_t texture_id, double zoom_factor) = 0; virtual std::optional SetPopupWindowPolicy(int64_t texture_id, diff --git a/webview_all_windows/windows/src/plugin/windows_host_api.cc b/webview_all_windows/windows/src/plugin/windows_host_api.cc index c0305b0..fbc6ac6 100644 --- a/webview_all_windows/windows/src/plugin/windows_host_api.cc +++ b/webview_all_windows/windows/src/plugin/windows_host_api.cc @@ -43,6 +43,21 @@ std::string FormatHresult(HRESULT result) { return value.str(); } +std::optional SettingResult(const char *setting, HRESULT result) { + if (SUCCEEDED(result)) { + return std::nullopt; + } + return FlutterError( + result == E_NOINTERFACE ? kErrorNotSupported : kErrorMethodFailed, + std::string("Applying ") + setting + " failed (HRESULT: " + + FormatHresult(result) + ").", + flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue("setting"), flutter::EncodableValue(setting)}, + {flutter::EncodableValue("hresult"), + flutter::EncodableValue(FormatHresult(result))}, + })); +} + FlutterError CreateInitializationError( const std::string &code, const std::string &stage, const std::string &message, HRESULT result, @@ -916,6 +931,35 @@ WindowsHostApi::SetZoomControlEnabled(int64_t texture_id, bool enabled) { return std::nullopt; } +std::optional +WindowsHostApi::SetDevToolsEnabled(int64_t texture_id, bool enabled) { + auto bridge = FindBridge(texture_id); + if (!bridge) { + return InvalidIdError(); + } + return SettingResult("devToolsEnabled", bridge->SetDevToolsEnabled(enabled)); +} + +std::optional +WindowsHostApi::SetBrowserAcceleratorKeysEnabled(int64_t texture_id, + bool enabled) { + auto bridge = FindBridge(texture_id); + if (!bridge) { + return InvalidIdError(); + } + return SettingResult("browserAcceleratorKeysEnabled", + bridge->SetBrowserAcceleratorKeysEnabled(enabled)); +} + +std::optional +WindowsHostApi::SetDownloadsEnabled(int64_t texture_id, bool enabled) { + auto bridge = FindBridge(texture_id); + if (!bridge) { + return InvalidIdError(); + } + return SettingResult("downloadsEnabled", bridge->SetDownloadsEnabled(enabled)); +} + std::optional WindowsHostApi::SetZoomFactor(int64_t texture_id, double zoom_factor) { auto bridge = FindBridge(texture_id); diff --git a/webview_all_windows/windows/src/plugin/windows_host_api.h b/webview_all_windows/windows/src/plugin/windows_host_api.h index 4ed2725..4f3716e 100644 --- a/webview_all_windows/windows/src/plugin/windows_host_api.h +++ b/webview_all_windows/windows/src/plugin/windows_host_api.h @@ -159,6 +159,12 @@ class WindowsHostApi : public flutter::Plugin, public WindowsWebViewHostApi { int64_t color) override; std::optional SetZoomControlEnabled(int64_t texture_id, bool enabled) override; + std::optional SetDevToolsEnabled(int64_t texture_id, + bool enabled) override; + std::optional + SetBrowserAcceleratorKeysEnabled(int64_t texture_id, bool enabled) override; + std::optional SetDownloadsEnabled(int64_t texture_id, + bool enabled) override; std::optional SetZoomFactor(int64_t texture_id, double zoom_factor) override; std::optional SetPopupWindowPolicy(int64_t texture_id, diff --git a/webview_all_windows/windows/src/webview/webview.cc b/webview_all_windows/windows/src/webview/webview.cc index 9809965..2be8645 100644 --- a/webview_all_windows/windows/src/webview/webview.cc +++ b/webview_all_windows/windows/src/webview/webview.cc @@ -231,6 +231,7 @@ Webview::Webview( if (SUCCEEDED(webview_->get_Settings(settings.put()))) { settings_ = settings; settings2_ = settings.try_query(); + settings3_ = settings.try_query(); if (settings2_) { wil::unique_cotaskmem_string default_user_agent; if (SUCCEEDED(settings2_->get_UserAgent(&default_user_agent)) && @@ -901,10 +902,29 @@ void Webview::RegisterEventHandlers() { auto webview24 = webview_.try_query(); if (webview24) { - webview24->add_DownloadStarting( + download_handler_result_ = webview24->add_DownloadStarting( Callback( [this](ICoreWebView2 *sender, ICoreWebView2DownloadStartingEventArgs *args) -> HRESULT { + if (!downloads_enabled_) { + const HRESULT result = args->put_Cancel(TRUE); + if (FAILED(result)) { + util::LogWarning("Cancelling a WebView2 download failed."); + } else if (download_event_callback_) { + wil::com_ptr download; + wil::unique_cotaskmem_string uri; + wil::unique_cotaskmem_string path; + if (SUCCEEDED(args->get_DownloadOperation(download.put())) && + download && SUCCEEDED(download->get_Uri(&uri)) && + SUCCEEDED(args->get_ResultFilePath(&path))) { + download_event_callback_( + {WebviewDownloadEventKind::DownloadCancelled, + util::Utf8FromUtf16(uri.get()), + util::Utf8FromUtf16(path.get()), 0, 0}); + } + } + return result; + } args->put_Handled(TRUE); wil::com_ptr download; @@ -1418,6 +1438,30 @@ bool Webview::SetZoomControlEnabled(bool enabled) { return false; } +HRESULT Webview::SetDevToolsEnabled(bool enabled) { + if (settings_) { + return settings_->put_AreDevToolsEnabled(enabled ? TRUE : FALSE); + } + return E_NOINTERFACE; +} + +HRESULT Webview::SetBrowserAcceleratorKeysEnabled(bool enabled) { + if (settings3_) { + return settings3_->put_AreBrowserAcceleratorKeysEnabled( + enabled ? TRUE : FALSE); + } + // Older runtimes always allow browser accelerators. + return enabled ? S_OK : E_NOINTERFACE; +} + +HRESULT Webview::SetDownloadsEnabled(bool enabled) { + if (!enabled && FAILED(download_handler_result_)) { + return download_handler_result_; + } + downloads_enabled_ = enabled; + return S_OK; +} + bool Webview::SetBackgroundColor(int32_t color) { if (!IsValid()) { return false; diff --git a/webview_all_windows/windows/src/webview/webview.h b/webview_all_windows/windows/src/webview/webview.h index 559e9b4..8a1b71a 100644 --- a/webview_all_windows/windows/src/webview/webview.h +++ b/webview_all_windows/windows/src/webview/webview.h @@ -26,7 +26,8 @@ enum class WebviewPointerEventKind { Activate, Down, Enter, Leave, Up, Update }; enum class WebviewDownloadEventKind { DownloadStarted, DownloadCompleted, - DownloadProgress + DownloadProgress, + DownloadCancelled }; enum class WebviewPermissionKind { @@ -265,6 +266,9 @@ class Webview { std::optional GetUserAgent(); bool SetJavaScriptEnabled(bool enabled); bool SetZoomControlEnabled(bool enabled); + HRESULT SetDevToolsEnabled(bool enabled); + HRESULT SetBrowserAcceleratorKeysEnabled(bool enabled); + HRESULT SetDownloadsEnabled(bool enabled); bool OpenDevTools(); bool SetBackgroundColor(int32_t color); bool SetZoomFactor(double factor); @@ -366,6 +370,7 @@ class Webview { devtools_protocol_event_receiver_; wil::com_ptr settings_; wil::com_ptr settings2_; + wil::com_ptr settings3_; std::string default_user_agent_; POINT last_cursor_pos_ = {0, 0}; VirtualKeyState virtual_keys_; @@ -375,6 +380,8 @@ class Webview { bool java_script_confirm_dialog_enabled_ = false; bool java_script_prompt_dialog_enabled_ = false; bool navigation_request_callbacks_enabled_ = false; + bool downloads_enabled_ = true; + HRESULT download_handler_result_ = E_NOINTERFACE; uint64_t latest_navigation_request_id_ = 0; size_t bypass_next_navigation_count_ = 0; std::unordered_multiset approved_navigation_urls_; diff --git a/webview_all_windows/windows/src/webview/webview_bridge.cc b/webview_all_windows/windows/src/webview/webview_bridge.cc index ba629ae..296e998 100644 --- a/webview_all_windows/windows/src/webview/webview_bridge.cc +++ b/webview_all_windows/windows/src/webview/webview_bridge.cc @@ -735,6 +735,18 @@ bool WebviewBridge::SetZoomControlEnabled(bool enabled) { return webview_->SetZoomControlEnabled(enabled); } +HRESULT WebviewBridge::SetDevToolsEnabled(bool enabled) { + return webview_->SetDevToolsEnabled(enabled); +} + +HRESULT WebviewBridge::SetBrowserAcceleratorKeysEnabled(bool enabled) { + return webview_->SetBrowserAcceleratorKeysEnabled(enabled); +} + +HRESULT WebviewBridge::SetDownloadsEnabled(bool enabled) { + return webview_->SetDownloadsEnabled(enabled); +} + bool WebviewBridge::SetBackgroundColor(int64_t color) { return webview_->SetBackgroundColor(static_cast(color)); } diff --git a/webview_all_windows/windows/src/webview/webview_bridge.h b/webview_all_windows/windows/src/webview/webview_bridge.h index e4f6bac..bcb4745 100644 --- a/webview_all_windows/windows/src/webview/webview_bridge.h +++ b/webview_all_windows/windows/src/webview/webview_bridge.h @@ -71,6 +71,9 @@ class WebviewBridge { std::optional GetUserAgent(); bool SetJavaScriptEnabled(bool enabled); bool SetZoomControlEnabled(bool enabled); + HRESULT SetDevToolsEnabled(bool enabled); + HRESULT SetBrowserAcceleratorKeysEnabled(bool enabled); + HRESULT SetDownloadsEnabled(bool enabled); bool SetBackgroundColor(int64_t color); bool SetZoomFactor(double zoom_factor); bool OpenDevTools(); diff --git a/webview_all_wkwebview/CHANGELOG-ZH.md b/webview_all_wkwebview/CHANGELOG-ZH.md index 12a3f60..f361e91 100644 --- a/webview_all_wkwebview/CHANGELOG-ZH.md +++ b/webview_all_wkwebview/CHANGELOG-ZH.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Windows 新增开发者工具、浏览器快捷键和下载开关,支持创建时配置及运行时调整。 +* Windows 默认关闭开发者工具的菜单和快捷键入口,可按需开启。 +* Linux 新增下载开关,可按 WebView 单独控制是否允许下载。 +* 修复 Web 关闭 JavaScript 时覆盖自定义 iframe sandbox 限制的问题。 + ## 1.4.1 * 修复 Windows 应用退出时的崩溃。 diff --git a/webview_all_wkwebview/CHANGELOG.md b/webview_all_wkwebview/CHANGELOG.md index dbead51..cfe6648 100644 --- a/webview_all_wkwebview/CHANGELOG.md +++ b/webview_all_wkwebview/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Add Windows controls for DevTools, browser shortcuts, and downloads, configurable at creation and at runtime. +* Disable Windows DevTools menu and keyboard entry points by default; enable them explicitly when needed. +* Add per-WebView download controls on Linux. +* Preserve custom iframe sandbox restrictions when disabling JavaScript on Web. + ## 1.4.1 * Fix a crash when exiting Windows applications. diff --git a/webview_platform_interface/CHANGELOG-ZH.md b/webview_platform_interface/CHANGELOG-ZH.md index 12a3f60..f361e91 100644 --- a/webview_platform_interface/CHANGELOG-ZH.md +++ b/webview_platform_interface/CHANGELOG-ZH.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Windows 新增开发者工具、浏览器快捷键和下载开关,支持创建时配置及运行时调整。 +* Windows 默认关闭开发者工具的菜单和快捷键入口,可按需开启。 +* Linux 新增下载开关,可按 WebView 单独控制是否允许下载。 +* 修复 Web 关闭 JavaScript 时覆盖自定义 iframe sandbox 限制的问题。 + ## 1.4.1 * 修复 Windows 应用退出时的崩溃。 diff --git a/webview_platform_interface/CHANGELOG.md b/webview_platform_interface/CHANGELOG.md index dbead51..cfe6648 100644 --- a/webview_platform_interface/CHANGELOG.md +++ b/webview_platform_interface/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.2 + +* Add Windows controls for DevTools, browser shortcuts, and downloads, configurable at creation and at runtime. +* Disable Windows DevTools menu and keyboard entry points by default; enable them explicitly when needed. +* Add per-WebView download controls on Linux. +* Preserve custom iframe sandbox restrictions when disabling JavaScript on Web. + ## 1.4.1 * Fix a crash when exiting Windows applications.