diff --git a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json index 8ede6453e..b084f0e97 100644 --- a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json +++ b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json @@ -976,6 +976,13 @@ "expectations": ["SKIP"], "comment": "We can't connect to a browser started with pipe:true" }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.setPermission should support * as origin", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"], + "comment": "Origin (*) is not supported for setPermission in BiDi" + }, { "testIdPattern": "[browsercontext.spec] BrowserContext should fire target events", "platforms": ["darwin", "linux", "win32"], diff --git a/lib/PuppeteerSharp.Tests/BrowserContextTests/BrowserContextSetPermissionTests.cs b/lib/PuppeteerSharp.Tests/BrowserContextTests/BrowserContextSetPermissionTests.cs new file mode 100644 index 000000000..bd33fc908 --- /dev/null +++ b/lib/PuppeteerSharp.Tests/BrowserContextTests/BrowserContextSetPermissionTests.cs @@ -0,0 +1,99 @@ +using System.Threading.Tasks; +using NUnit.Framework; +using PuppeteerSharp.Nunit; + +namespace PuppeteerSharp.Tests.BrowserContextTests +{ + public class BrowserContextSetPermissionTests : PuppeteerPageBaseTest + { + public BrowserContextSetPermissionTests() : base() + { + } + + private Task GetPermissionAsync(IPage page, string name) + => page.EvaluateFunctionAsync( + "name => navigator.permissions.query({ name }).then(result => result.state)", + name); + + [Test, PuppeteerTest("browsercontext.spec", "BrowserContext BrowserContext.setPermission", "should set permission")] + public async Task ShouldSetPermission() + { + await Page.GoToAsync(TestConstants.EmptyPage); + + await Context.SetPermissionAsync(TestConstants.EmptyPage, new PermissionEntry + { + Permission = new PermissionDescriptor { Name = "geolocation" }, + State = PermissionState.Granted, + }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("granted")); + + await Context.SetPermissionAsync(TestConstants.EmptyPage, new PermissionEntry + { + Permission = new PermissionDescriptor { Name = "geolocation" }, + State = PermissionState.Denied, + }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("denied")); + + await Context.SetPermissionAsync(TestConstants.EmptyPage, new PermissionEntry + { + Permission = new PermissionDescriptor { Name = "geolocation" }, + State = PermissionState.Prompt, + }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("prompt")); + } + + [Test, PuppeteerTest("browsercontext.spec", "BrowserContext BrowserContext.setPermission", "should support * as origin")] + public async Task ShouldSupportStarAsOrigin() + { + await Page.GoToAsync(TestConstants.EmptyPage); + + await Context.SetPermissionAsync("*", new PermissionEntry + { + Permission = new PermissionDescriptor { Name = "geolocation" }, + State = PermissionState.Granted, + }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("granted")); + + await Context.SetPermissionAsync("*", new PermissionEntry + { + Permission = new PermissionDescriptor { Name = "geolocation" }, + State = PermissionState.Denied, + }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("denied")); + + await Context.SetPermissionAsync("*", new PermissionEntry + { + Permission = new PermissionDescriptor { Name = "geolocation" }, + State = PermissionState.Prompt, + }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("prompt")); + } + + [Test, PuppeteerTest("browsercontext.spec", "BrowserContext BrowserContext.setPermission", "should support multiple permissions")] + public async Task ShouldSupportMultiplePermissions() + { + await Page.GoToAsync(TestConstants.EmptyPage); + + await Context.SetPermissionAsync( + TestConstants.EmptyPage, + new PermissionEntry { Permission = new PermissionDescriptor { Name = "geolocation" }, State = PermissionState.Granted }, + new PermissionEntry { Permission = new PermissionDescriptor { Name = "midi" }, State = PermissionState.Granted }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("granted")); + Assert.That(await GetPermissionAsync(Page, "midi"), Is.EqualTo("granted")); + + await Context.SetPermissionAsync( + TestConstants.EmptyPage, + new PermissionEntry { Permission = new PermissionDescriptor { Name = "geolocation" }, State = PermissionState.Denied }, + new PermissionEntry { Permission = new PermissionDescriptor { Name = "midi" }, State = PermissionState.Denied }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("denied")); + Assert.That(await GetPermissionAsync(Page, "midi"), Is.EqualTo("denied")); + + await Context.SetPermissionAsync( + TestConstants.EmptyPage, + new PermissionEntry { Permission = new PermissionDescriptor { Name = "geolocation" }, State = PermissionState.Prompt }, + new PermissionEntry { Permission = new PermissionDescriptor { Name = "midi" }, State = PermissionState.Prompt }); + Assert.That(await GetPermissionAsync(Page, "geolocation"), Is.EqualTo("prompt")); + Assert.That(await GetPermissionAsync(Page, "midi"), Is.EqualTo("prompt")); + } + } +} diff --git a/lib/PuppeteerSharp/Bidi/BidiBrowserContext.cs b/lib/PuppeteerSharp/Bidi/BidiBrowserContext.cs index 88ed27211..6533f19d9 100644 --- a/lib/PuppeteerSharp/Bidi/BidiBrowserContext.cs +++ b/lib/PuppeteerSharp/Bidi/BidiBrowserContext.cs @@ -30,7 +30,7 @@ using Microsoft.Extensions.Logging; using PuppeteerSharp.Bidi.Core; using PuppeteerSharp.Helpers; -using WebDriverBiDi.Permissions; +using BidiPermissionState = WebDriverBiDi.Permissions.PermissionState; namespace PuppeteerSharp.Bidi; @@ -66,8 +66,8 @@ public override async Task OverridePermissionsAsync(string origin, IEnumerable + public override async Task SetPermissionAsync(string origin, params PermissionEntry[] permissions) + { + if (origin == "*") + { + throw new PuppeteerException("Origin (*) is not supported by WebDriver BiDi"); + } + + await Task.WhenAll(permissions.Select(entry => + { + if (entry.Permission.AllowWithoutSanitization == true) + { + throw new PuppeteerException("allowWithoutSanitization is not supported by WebDriver BiDi"); + } + + if (entry.Permission.PanTiltZoom == true) + { + throw new PuppeteerException("panTiltZoom is not supported by WebDriver BiDi"); + } + + if (entry.Permission.UserVisibleOnly == true) + { + throw new PuppeteerException("userVisibleOnly is not supported by WebDriver BiDi"); + } + + var state = entry.State switch + { + PermissionState.Granted => BidiPermissionState.Granted, + PermissionState.Denied => BidiPermissionState.Denied, + PermissionState.Prompt => BidiPermissionState.Prompt, + _ => throw new ArgumentOutOfRangeException(nameof(entry), entry.State, "Unknown permission state"), + }; + + return UserContext.SetPermissionsAsync(origin, entry.Permission.Name, state); + })).ConfigureAwait(false); + } + /// public override async Task ClearPermissionOverridesAsync() { @@ -103,7 +140,7 @@ public override async Task ClearPermissionOverridesAsync() foreach (var (origin, permission) in _overrides.ToArray()) { var permissionName = GetPermissionName(permission); - tasks.Add(UserContext.SetPermissionsAsync(origin, permissionName, PermissionState.Prompt) + tasks.Add(UserContext.SetPermissionsAsync(origin, permissionName, BidiPermissionState.Prompt) .ContinueWith( t => { @@ -231,6 +268,7 @@ private static string GetPermissionName(OverridePermission permission) OverridePermission.IdleDetection => "idle-detection", OverridePermission.PersistentStorage => "persistent-storage", OverridePermission.LocalNetworkAccess => "local-network-access", + OverridePermission.LocalFonts => "local-fonts", _ => throw new ArgumentOutOfRangeException(nameof(permission), permission, "Unknown permission"), }; } diff --git a/lib/PuppeteerSharp/Bidi/Core/UserContext.cs b/lib/PuppeteerSharp/Bidi/Core/UserContext.cs index 2c1d7783d..79cc4ce45 100644 --- a/lib/PuppeteerSharp/Bidi/Core/UserContext.cs +++ b/lib/PuppeteerSharp/Bidi/Core/UserContext.cs @@ -28,7 +28,8 @@ using System.Threading.Tasks; using WebDriverBiDi.Browser; using WebDriverBiDi.BrowsingContext; -using WebDriverBiDi.Permissions; +using BidiPermissionState = WebDriverBiDi.Permissions.PermissionState; +using SetPermissionCommandParameters = WebDriverBiDi.Permissions.SetPermissionCommandParameters; namespace PuppeteerSharp.Bidi.Core; @@ -132,7 +133,7 @@ public async Task RemoveAsync() public async Task SetPermissionsAsync( string origin, string permissionName, - PermissionState state) + BidiPermissionState state) { await Session.Driver.Permissions.SetPermissionAsync( new SetPermissionCommandParameters(permissionName, state, origin) diff --git a/lib/PuppeteerSharp/Browser.cs b/lib/PuppeteerSharp/Browser.cs index 466e5c6ed..d0dba677b 100644 --- a/lib/PuppeteerSharp/Browser.cs +++ b/lib/PuppeteerSharp/Browser.cs @@ -174,6 +174,10 @@ public void UnregisterCustomQueryHandler(string name) public void ClearCustomQueryHandlers() => CustomQuerySelectorRegistry.Default.ClearCustomQueryHandlers(); + /// + public Task SetPermissionAsync(string origin, params PermissionEntry[] permissions) + => DefaultContext.SetPermissionAsync(origin, permissions); + /// public Task CreateCDPSessionAsync() => Target.CreateCDPSessionAsync(); diff --git a/lib/PuppeteerSharp/BrowserContext.cs b/lib/PuppeteerSharp/BrowserContext.cs index 77e91058f..fae6555f9 100644 --- a/lib/PuppeteerSharp/BrowserContext.cs +++ b/lib/PuppeteerSharp/BrowserContext.cs @@ -48,6 +48,9 @@ public Task WaitForTargetAsync(Func predicate, WaitForOp /// public abstract Task OverridePermissionsAsync(string origin, IEnumerable permissions); + /// + public abstract Task SetPermissionAsync(string origin, params PermissionEntry[] permissions); + /// public abstract Task ClearPermissionOverridesAsync(); diff --git a/lib/PuppeteerSharp/Cdp/CdpBrowserContext.cs b/lib/PuppeteerSharp/Cdp/CdpBrowserContext.cs index 8e8282ed1..3a579768c 100644 --- a/lib/PuppeteerSharp/Cdp/CdpBrowserContext.cs +++ b/lib/PuppeteerSharp/Cdp/CdpBrowserContext.cs @@ -78,6 +78,30 @@ public override Task OverridePermissionsAsync(string origin, IEnumerable + public override async Task SetPermissionAsync(string origin, params PermissionEntry[] permissions) + { + await Task.WhenAll(permissions.Select(entry => + { + var protocolPermission = new BrowserPermissionDescriptor + { + Name = entry.Permission.Name, + UserVisibleOnly = entry.Permission.UserVisibleOnly, + Sysex = entry.Permission.Sysex, + AllowWithoutSanitization = entry.Permission.AllowWithoutSanitization, + PanTiltZoom = entry.Permission.PanTiltZoom, + }; + + return _connection.SendAsync("Browser.setPermission", new BrowserSetPermissionRequest + { + Origin = origin == "*" ? null : origin, + BrowserContextId = Id, + Permission = protocolPermission, + Setting = entry.State, + }); + })).ConfigureAwait(false); + } + /// public override Task ClearPermissionOverridesAsync() => _connection.SendAsync("Browser.resetPermissions", new BrowserResetPermissionsRequest diff --git a/lib/PuppeteerSharp/Cdp/Messaging/BrowserPermissionDescriptor.cs b/lib/PuppeteerSharp/Cdp/Messaging/BrowserPermissionDescriptor.cs new file mode 100644 index 000000000..322f8c5ad --- /dev/null +++ b/lib/PuppeteerSharp/Cdp/Messaging/BrowserPermissionDescriptor.cs @@ -0,0 +1,15 @@ +namespace PuppeteerSharp.Cdp.Messaging +{ + internal class BrowserPermissionDescriptor + { + public string Name { get; set; } + + public bool? UserVisibleOnly { get; set; } + + public bool? Sysex { get; set; } + + public bool? PanTiltZoom { get; set; } + + public bool? AllowWithoutSanitization { get; set; } + } +} diff --git a/lib/PuppeteerSharp/Cdp/Messaging/BrowserSetPermissionRequest.cs b/lib/PuppeteerSharp/Cdp/Messaging/BrowserSetPermissionRequest.cs new file mode 100644 index 000000000..9fa689255 --- /dev/null +++ b/lib/PuppeteerSharp/Cdp/Messaging/BrowserSetPermissionRequest.cs @@ -0,0 +1,13 @@ +namespace PuppeteerSharp.Cdp.Messaging +{ + internal class BrowserSetPermissionRequest + { + public string Origin { get; set; } + + public string BrowserContextId { get; set; } + + public BrowserPermissionDescriptor Permission { get; set; } + + public PermissionState Setting { get; set; } + } +} diff --git a/lib/PuppeteerSharp/Helpers/Json/SystemTextJsonSerializationContext.cs b/lib/PuppeteerSharp/Helpers/Json/SystemTextJsonSerializationContext.cs index 5ca28094b..7c783056c 100644 --- a/lib/PuppeteerSharp/Helpers/Json/SystemTextJsonSerializationContext.cs +++ b/lib/PuppeteerSharp/Helpers/Json/SystemTextJsonSerializationContext.cs @@ -44,7 +44,9 @@ namespace PuppeteerSharp.Helpers.Json; [JsonSerializable(typeof(BrowserGetWindowBoundsResponse))] [JsonSerializable(typeof(BrowserGetWindowForTargetResponse))] [JsonSerializable(typeof(BrowserGrantPermissionsRequest))] +[JsonSerializable(typeof(BrowserPermissionDescriptor))] [JsonSerializable(typeof(BrowserResetPermissionsRequest))] +[JsonSerializable(typeof(BrowserSetPermissionRequest))] [JsonSerializable(typeof(BrowserSetContentsSizeRequest))] [JsonSerializable(typeof(BrowserSetWindowBoundsRequest))] [JsonSerializable(typeof(BoundingBox[]))] diff --git a/lib/PuppeteerSharp/IBrowser.cs b/lib/PuppeteerSharp/IBrowser.cs index 7392d2ef3..97a24695f 100644 --- a/lib/PuppeteerSharp/IBrowser.cs +++ b/lib/PuppeteerSharp/IBrowser.cs @@ -268,6 +268,14 @@ public interface IBrowser : IDisposable, IAsyncDisposable /// Only supported in headless mode. Fails if the primary screen ID is specified. Task RemoveScreenAsync(string screenId); + /// + /// Sets the permission for a specific origin on the default browser context. + /// + /// The origin to set the permission for, e.g. "https://example.com". Use "*" for all origins (CDP only). + /// The permissions to set. + /// The task. + Task SetPermissionAsync(string origin, params PermissionEntry[] permissions); + /// /// Creates a Chrome Devtools Protocol session attached to the browser. /// diff --git a/lib/PuppeteerSharp/IBrowserContext.cs b/lib/PuppeteerSharp/IBrowserContext.cs index 9eae8cce8..6b7e243ae 100644 --- a/lib/PuppeteerSharp/IBrowserContext.cs +++ b/lib/PuppeteerSharp/IBrowserContext.cs @@ -76,6 +76,14 @@ public interface IBrowserContext : IDisposable, IAsyncDisposable /// Task OverridePermissionsAsync(string origin, IEnumerable permissions); + /// + /// Sets the permission for a specific origin. + /// + /// The origin to set the permission for, e.g. "https://example.com". Use "*" for all origins (CDP only). + /// The permissions to set. + /// The task. + Task SetPermissionAsync(string origin, params PermissionEntry[] permissions); + /// /// An array of all pages inside the browser context. /// diff --git a/lib/PuppeteerSharp/OverridePermission.cs b/lib/PuppeteerSharp/OverridePermission.cs index a166b3f79..01540d016 100644 --- a/lib/PuppeteerSharp/OverridePermission.cs +++ b/lib/PuppeteerSharp/OverridePermission.cs @@ -94,5 +94,11 @@ public enum OverridePermission /// [EnumMember(Value = "local-network-access")] LocalNetworkAccess, + + /// + /// Local Fonts. + /// + [EnumMember(Value = "local-fonts")] + LocalFonts, } } diff --git a/lib/PuppeteerSharp/PermissionDescriptor.cs b/lib/PuppeteerSharp/PermissionDescriptor.cs new file mode 100644 index 000000000..5d9f4a61d --- /dev/null +++ b/lib/PuppeteerSharp/PermissionDescriptor.cs @@ -0,0 +1,33 @@ +namespace PuppeteerSharp +{ + /// + /// Describes a permission to set via . + /// + public class PermissionDescriptor + { + /// + /// Gets or sets the permission name (e.g. "geolocation", "midi", "notifications"). + /// + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether the permission should only apply to user-visible operations. + /// + public bool? UserVisibleOnly { get; set; } + + /// + /// Gets or sets a value indicating whether to enable system exclusive access (MIDI). + /// + public bool? Sysex { get; set; } + + /// + /// Gets or sets a value indicating whether to enable pan-tilt-zoom controls (camera). + /// + public bool? PanTiltZoom { get; set; } + + /// + /// Gets or sets a value indicating whether to allow writing without sanitization (clipboard). + /// + public bool? AllowWithoutSanitization { get; set; } + } +} diff --git a/lib/PuppeteerSharp/PermissionEntry.cs b/lib/PuppeteerSharp/PermissionEntry.cs new file mode 100644 index 000000000..a213436e9 --- /dev/null +++ b/lib/PuppeteerSharp/PermissionEntry.cs @@ -0,0 +1,18 @@ +namespace PuppeteerSharp +{ + /// + /// Represents a permission entry to set via . + /// + public class PermissionEntry + { + /// + /// Gets or sets the permission descriptor. + /// + public PermissionDescriptor Permission { get; set; } + + /// + /// Gets or sets the state to set the permission to. + /// + public PermissionState State { get; set; } + } +} diff --git a/lib/PuppeteerSharp/PermissionState.cs b/lib/PuppeteerSharp/PermissionState.cs new file mode 100644 index 000000000..136e6f122 --- /dev/null +++ b/lib/PuppeteerSharp/PermissionState.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using PuppeteerSharp.Helpers.Json; + +namespace PuppeteerSharp +{ + /// + /// Permission state for . + /// + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum PermissionState + { + /// + /// The permission is granted. + /// + [EnumMember(Value = "granted")] + Granted, + + /// + /// The permission is denied. + /// + [EnumMember(Value = "denied")] + Denied, + + /// + /// The permission is in the prompt state (default). + /// + [EnumMember(Value = "prompt")] + Prompt, + } +}