fix: harden permission origin, metadata constants, and cookie policy default
- Use live URI from the signal's WebView (inner.GetUri()) instead of the cached wv.URI() in connectPermissionRequestSignal so permission lookups and persistence always use the current page origin - Add exported PermissionMetadataKeyRequestingDomain and PermissionMetadataKeyCurrentDomain constants to entity.PermissionMetadata and replace all raw string literals across the codebase with those constants - Update HandlePermissionRequest / ShowPermissionDialog docs to name both required metadata keys for website_data_access requests - Fix normalizeEngineConfig: unset or invalid cookie_policy now defaults to CookiePolicyNoThirdParty when itp_enabled is false, instead of silently forcing CookiePolicyAlways regardless of the ITP setting
- commit
- a6de72741366d21902dadb618c528bab89a5c054
- author
- bnema · · 2026-03-25 21:14 UTC
- parent
- 2aad925
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +52 -20
M internal/application/port/permission.go +2 -1
| @@ -26,7 +26,8 @@ type PermissionDialogPresenter interface { | |||
| 26 | 26 | // - ctx: context for cancellation and logging | |
| 27 | 27 | // - origin: the website origin requesting permission (e.g., "https://meet.example.com") | |
| 28 | 28 | // - permTypes: the types of permissions being requested | |
| 29 | - | // - metadata: permission-type-specific context (e.g., "requesting_domain" for website_data_access) | |
| 29 | + | // - metadata: permission-type-specific context; for website_data_access both | |
| 30 | + | // entity.PermissionMetadataKeyRequestingDomain and entity.PermissionMetadataKeyCurrentDomain must be set | |
| 30 | 31 | // - callback: invoked when the user makes a decision or dialog is dismissed | |
| 31 | 32 | ShowPermissionDialog( | |
| 32 | 33 | ctx context.Context, | |
| @@ -26,7 +26,8 @@ type PermissionDialogPresenter interface { | |||||
| 26 | // - ctx: context for cancellation and logging | 26 | // - ctx: context for cancellation and logging | ||
| 27 | // - origin: the website origin requesting permission (e.g., "https://meet.example.com") | 27 | // - origin: the website origin requesting permission (e.g., "https://meet.example.com") | ||
| 28 | // - permTypes: the types of permissions being requested | 28 | // - permTypes: the types of permissions being requested | ||
| 29 | - | // - metadata: permission-type-specific context (e.g., "requesting_domain" for website_data_access) | 29 | + | // - metadata: permission-type-specific context; for website_data_access both |
| 30 | + | // entity.PermissionMetadataKeyRequestingDomain and entity.PermissionMetadataKeyCurrentDomain must be set | |||
| 30 | // - callback: invoked when the user makes a decision or dialog is dismissed | 31 | // - callback: invoked when the user makes a decision or dialog is dismissed | ||
| 31 | ShowPermissionDialog( | 32 | ShowPermissionDialog( | ||
| 32 | ctx context.Context, | 33 | ctx context.Context, | ||
M internal/application/port/webview.go +2 -1
| @@ -107,7 +107,8 @@ type WebViewCallbacks struct { | |||
| 107 | 107 | // OnPermissionRequest is called when a site requests permission (mic, camera, screen sharing). | |
| 108 | 108 | // Return true to indicate the request was handled. Call allow()/deny() to respond. | |
| 109 | 109 | // The permission types are passed as strings ("microphone", "camera", "display", etc.). | |
| 110 | - | // The metadata map carries permission-type-specific context (e.g., "requesting_domain" for website_data_access). | |
| 110 | + | // The metadata map carries permission-type-specific context; for website_data_access both | |
| 111 | + | // entity.PermissionMetadataKeyRequestingDomain and entity.PermissionMetadataKeyCurrentDomain must be populated. | |
| 111 | 112 | OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool | |
| 112 | 113 | ||
| 113 | 114 | // OnLinkMiddleClick is called when a link is middle-clicked. | |
| @@ -107,7 +107,8 @@ type WebViewCallbacks struct { | |||||
| 107 | // OnPermissionRequest is called when a site requests permission (mic, camera, screen sharing). | 107 | // OnPermissionRequest is called when a site requests permission (mic, camera, screen sharing). | ||
| 108 | // Return true to indicate the request was handled. Call allow()/deny() to respond. | 108 | // Return true to indicate the request was handled. Call allow()/deny() to respond. | ||
| 109 | // The permission types are passed as strings ("microphone", "camera", "display", etc.). | 109 | // The permission types are passed as strings ("microphone", "camera", "display", etc.). | ||
| 110 | - | // The metadata map carries permission-type-specific context (e.g., "requesting_domain" for website_data_access). | 110 | + | // The metadata map carries permission-type-specific context; for website_data_access both |
| 111 | + | // entity.PermissionMetadataKeyRequestingDomain and entity.PermissionMetadataKeyCurrentDomain must be populated. | |||
| 111 | OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool | 112 | OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool | ||
| 112 | 113 | ||||
| 113 | // OnLinkMiddleClick is called when a link is middle-clicked. | 114 | // OnLinkMiddleClick is called when a link is middle-clicked. | ||
M internal/application/usecase/handle_permission.go +3 -1
| @@ -79,7 +79,9 @@ func (uc *HandlePermissionUseCase) getDialog() port.PermissionDialogPresenter { | |||
| 79 | 79 | // - ctx: context for cancellation and logging | |
| 80 | 80 | // - origin: the website origin (URI) requesting permission | |
| 81 | 81 | // - permTypes: the types of permissions being requested | |
| 82 | - | // - metadata: permission-type-specific context (e.g., "requesting_domain" for website_data_access) | |
| 82 | + | // - metadata: permission-type-specific context; for website_data_access both keys must be present: | |
| 83 | + | // entity.PermissionMetadataKeyRequestingDomain — the origin requesting cross-site data access | |
| 84 | + | // entity.PermissionMetadataKeyCurrentDomain — the domain currently loaded in the WebView | |
| 83 | 85 | // - callback: callbacks to allow or deny the request (must call one) | |
| 84 | 86 | func (uc *HandlePermissionUseCase) HandlePermissionRequest( | |
| 85 | 87 | ctx context.Context, | |
| @@ -79,7 +79,9 @@ func (uc *HandlePermissionUseCase) getDialog() port.PermissionDialogPresenter { | |||||
| 79 | // - ctx: context for cancellation and logging | 79 | // - ctx: context for cancellation and logging | ||
| 80 | // - origin: the website origin (URI) requesting permission | 80 | // - origin: the website origin (URI) requesting permission | ||
| 81 | // - permTypes: the types of permissions being requested | 81 | // - permTypes: the types of permissions being requested | ||
| 82 | - | // - metadata: permission-type-specific context (e.g., "requesting_domain" for website_data_access) | 82 | + | // - metadata: permission-type-specific context; for website_data_access both keys must be present: |
| 83 | + | // entity.PermissionMetadataKeyRequestingDomain — the origin requesting cross-site data access | |||
| 84 | + | // entity.PermissionMetadataKeyCurrentDomain — the domain currently loaded in the WebView | |||
| 83 | // - callback: callbacks to allow or deny the request (must call one) | 85 | // - callback: callbacks to allow or deny the request (must call one) | ||
| 84 | func (uc *HandlePermissionUseCase) HandlePermissionRequest( | 86 | func (uc *HandlePermissionUseCase) HandlePermissionRequest( | ||
| 85 | ctx context.Context, | 87 | ctx context.Context, | ||
M internal/application/usecase/handle_permission_test.go +3 -3
| @@ -552,8 +552,8 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_ShowsDialogAndPersists(t *tes | |||
| 552 | 552 | } | |
| 553 | 553 | ||
| 554 | 554 | meta := entity.PermissionMetadata{ | |
| 555 | - | "requesting_domain": "accounts.google.com", | |
| 556 | - | "current_domain": "shop.example.com", | |
| 555 | + | entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com", | |
| 556 | + | entity.PermissionMetadataKeyCurrentDomain: "shop.example.com", | |
| 557 | 557 | } | |
| 558 | 558 | ||
| 559 | 559 | // Dialog shows and user clicks "Always Allow" | |
| @@ -605,7 +605,7 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_UsesStoredGrant(t *testing.T) | |||
| 605 | 605 | ||
| 606 | 606 | uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{ | |
| 607 | 607 | entity.PermissionTypeWebsiteDataAccess, | |
| 608 | - | }, entity.PermissionMetadata{"requesting_domain": "accounts.google.com", "current_domain": "shop.example.com"}, callback) | |
| 608 | + | }, entity.PermissionMetadata{entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com", entity.PermissionMetadataKeyCurrentDomain: "shop.example.com"}, callback) | |
| 609 | 609 | ||
| 610 | 610 | assert.True(t, allowed, "should use stored granted permission without dialog") | |
| 611 | 611 | dialog.AssertNotCalled(t, "ShowPermissionDialog") | |
| @@ -552,8 +552,8 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_ShowsDialogAndPersists(t *tes | |||||
| 552 | } | 552 | } | ||
| 553 | 553 | ||||
| 554 | meta := entity.PermissionMetadata{ | 554 | meta := entity.PermissionMetadata{ | ||
| 555 | - | "requesting_domain": "accounts.google.com", | 555 | + | entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com", |
| 556 | - | "current_domain": "shop.example.com", | 556 | + | entity.PermissionMetadataKeyCurrentDomain: "shop.example.com", |
| 557 | } | 557 | } | ||
| 558 | 558 | ||||
| 559 | // Dialog shows and user clicks "Always Allow" | 559 | // Dialog shows and user clicks "Always Allow" | ||
| @@ -605,7 +605,7 @@ func TestHandlePermissionUseCase_WebsiteDataAccess_UsesStoredGrant(t *testing.T) | |||||
| 605 | 605 | ||||
| 606 | uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{ | 606 | uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{ | ||
| 607 | entity.PermissionTypeWebsiteDataAccess, | 607 | entity.PermissionTypeWebsiteDataAccess, | ||
| 608 | - | }, entity.PermissionMetadata{"requesting_domain": "accounts.google.com", "current_domain": "shop.example.com"}, callback) | 608 | + | }, entity.PermissionMetadata{entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com", entity.PermissionMetadataKeyCurrentDomain: "shop.example.com"}, callback) |
| 609 | 609 | ||||
| 610 | assert.True(t, allowed, "should use stored granted permission without dialog") | 610 | assert.True(t, allowed, "should use stored granted permission without dialog") | ||
| 611 | dialog.AssertNotCalled(t, "ShowPermissionDialog") | 611 | dialog.AssertNotCalled(t, "ShowPermissionDialog") | ||
M internal/domain/entity/permission.go +14 -1
| @@ -38,8 +38,21 @@ const ( | |||
| 38 | 38 | PermissionTypeWebsiteDataAccess PermissionType = "website_data_access" | |
| 39 | 39 | ) | |
| 40 | 40 | ||
| 41 | + | // Metadata keys for PermissionMetadata. | |
| 42 | + | const ( | |
| 43 | + | // PermissionMetadataKeyRequestingDomain is the origin that is requesting cross-site | |
| 44 | + | // data access (i.e. the third-party domain asking for its cookies/storage). | |
| 45 | + | PermissionMetadataKeyRequestingDomain = "requesting_domain" | |
| 46 | + | ||
| 47 | + | // PermissionMetadataKeyCurrentDomain is the first-party domain currently loaded in | |
| 48 | + | // the WebView — the context in which the cross-site request is happening. | |
| 49 | + | PermissionMetadataKeyCurrentDomain = "current_domain" | |
| 50 | + | ) | |
| 51 | + | ||
| 41 | 52 | // PermissionMetadata carries extra context for specific permission types. | |
| 42 | - | // Keys are permission-type-specific (e.g., "requesting_domain" for website_data_access). | |
| 53 | + | // For website_data_access, callers must populate both keys: | |
| 54 | + | // - PermissionMetadataKeyRequestingDomain: the origin requesting cross-site data access | |
| 55 | + | // - PermissionMetadataKeyCurrentDomain: the domain currently loaded in the WebView | |
| 43 | 56 | type PermissionMetadata map[string]string | |
| 44 | 57 | ||
| 45 | 58 | // PermissionDecision represents the user's decision for a permission. | |
| @@ -38,8 +38,21 @@ const ( | |||||
| 38 | PermissionTypeWebsiteDataAccess PermissionType = "website_data_access" | 38 | PermissionTypeWebsiteDataAccess PermissionType = "website_data_access" | ||
| 39 | ) | 39 | ) | ||
| 40 | 40 | ||||
| 41 | + | // Metadata keys for PermissionMetadata. | |||
| 42 | + | const ( | |||
| 43 | + | // PermissionMetadataKeyRequestingDomain is the origin that is requesting cross-site | |||
| 44 | + | // data access (i.e. the third-party domain asking for its cookies/storage). | |||
| 45 | + | PermissionMetadataKeyRequestingDomain = "requesting_domain" | |||
| 46 | + | ||||
| 47 | + | // PermissionMetadataKeyCurrentDomain is the first-party domain currently loaded in | |||
| 48 | + | // the WebView — the context in which the cross-site request is happening. | |||
| 49 | + | PermissionMetadataKeyCurrentDomain = "current_domain" | |||
| 50 | + | ) | |||
| 51 | + | ||||
| 41 | // PermissionMetadata carries extra context for specific permission types. | 52 | // PermissionMetadata carries extra context for specific permission types. | ||
| 42 | - | // Keys are permission-type-specific (e.g., "requesting_domain" for website_data_access). | 53 | + | // For website_data_access, callers must populate both keys: |
| 54 | + | // - PermissionMetadataKeyRequestingDomain: the origin requesting cross-site data access | |||
| 55 | + | // - PermissionMetadataKeyCurrentDomain: the domain currently loaded in the WebView | |||
| 43 | type PermissionMetadata map[string]string | 56 | type PermissionMetadata map[string]string | ||
| 44 | 57 | ||||
| 45 | // PermissionDecision represents the user's decision for a permission. | 58 | // PermissionDecision represents the user's decision for a permission. | ||
M internal/infrastructure/config/loader.go +14 -3
| @@ -224,16 +224,27 @@ func normalizeEngineConfig(config *Config) { | |||
| 224 | 224 | config.Engine.WebKit.GSKRenderer = GSKRendererAuto | |
| 225 | 225 | } | |
| 226 | 226 | ||
| 227 | - | // Normalize cookie policy (engine) | |
| 227 | + | // Normalize cookie policy (engine). | |
| 228 | + | // When no value (or an invalid value) is set, respect the ITP setting: if ITP is | |
| 229 | + | // disabled the effective default is to block third-party cookies; if ITP is enabled | |
| 230 | + | // all cookies are accepted by default. An explicit "always" value always wins. | |
| 231 | + | itpDefault := func() CookiePolicy { | |
| 232 | + | if config.Engine.WebKit.ITPEnabled { | |
| 233 | + | return CookiePolicyAlways | |
| 234 | + | } | |
| 235 | + | return CookiePolicyNoThirdParty | |
| 236 | + | } | |
| 228 | 237 | switch strings.ToLower(string(config.Engine.CookiePolicy)) { | |
| 229 | - | case "", string(CookiePolicyAlways): | |
| 238 | + | case "": | |
| 239 | + | config.Engine.CookiePolicy = itpDefault() | |
| 240 | + | case string(CookiePolicyAlways): | |
| 230 | 241 | config.Engine.CookiePolicy = CookiePolicyAlways | |
| 231 | 242 | case string(CookiePolicyNoThirdParty): | |
| 232 | 243 | config.Engine.CookiePolicy = CookiePolicyNoThirdParty | |
| 233 | 244 | case string(CookiePolicyNever): | |
| 234 | 245 | config.Engine.CookiePolicy = CookiePolicyNever | |
| 235 | 246 | default: | |
| 236 | - | config.Engine.CookiePolicy = CookiePolicyAlways | |
| 247 | + | config.Engine.CookiePolicy = itpDefault() | |
| 237 | 248 | } | |
| 238 | 249 | ||
| 239 | 250 | // Normalize performance profile (engine) | |
| @@ -224,16 +224,27 @@ func normalizeEngineConfig(config *Config) { | |||||
| 224 | config.Engine.WebKit.GSKRenderer = GSKRendererAuto | 224 | config.Engine.WebKit.GSKRenderer = GSKRendererAuto | ||
| 225 | } | 225 | } | ||
| 226 | 226 | ||||
| 227 | - | // Normalize cookie policy (engine) | 227 | + | // Normalize cookie policy (engine). |
| 228 | + | // When no value (or an invalid value) is set, respect the ITP setting: if ITP is | |||
| 229 | + | // disabled the effective default is to block third-party cookies; if ITP is enabled | |||
| 230 | + | // all cookies are accepted by default. An explicit "always" value always wins. | |||
| 231 | + | itpDefault := func() CookiePolicy { | |||
| 232 | + | if config.Engine.WebKit.ITPEnabled { | |||
| 233 | + | return CookiePolicyAlways | |||
| 234 | + | } | |||
| 235 | + | return CookiePolicyNoThirdParty | |||
| 236 | + | } | |||
| 228 | switch strings.ToLower(string(config.Engine.CookiePolicy)) { | 237 | switch strings.ToLower(string(config.Engine.CookiePolicy)) { | ||
| 229 | - | case "", string(CookiePolicyAlways): | 238 | + | case "": |
| 239 | + | config.Engine.CookiePolicy = itpDefault() | |||
| 240 | + | case string(CookiePolicyAlways): | |||
| 230 | config.Engine.CookiePolicy = CookiePolicyAlways | 241 | config.Engine.CookiePolicy = CookiePolicyAlways | ||
| 231 | case string(CookiePolicyNoThirdParty): | 242 | case string(CookiePolicyNoThirdParty): | ||
| 232 | config.Engine.CookiePolicy = CookiePolicyNoThirdParty | 243 | config.Engine.CookiePolicy = CookiePolicyNoThirdParty | ||
| 233 | case string(CookiePolicyNever): | 244 | case string(CookiePolicyNever): | ||
| 234 | config.Engine.CookiePolicy = CookiePolicyNever | 245 | config.Engine.CookiePolicy = CookiePolicyNever | ||
| 235 | default: | 246 | default: | ||
| 236 | - | config.Engine.CookiePolicy = CookiePolicyAlways | 247 | + | config.Engine.CookiePolicy = itpDefault() |
| 237 | } | 248 | } | ||
| 238 | 249 | ||||
| 239 | // Normalize performance profile (engine) | 250 | // Normalize performance profile (engine) | ||
M internal/infrastructure/webkit/webview.go +10 -6
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "time" | |
| 12 | 12 | ||
| 13 | 13 | "github.com/bnema/dumber/internal/application/port" | |
| 14 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 14 | 15 | urlutil "github.com/bnema/dumber/internal/domain/url" | |
| 15 | 16 | "github.com/bnema/dumber/internal/infrastructure/desktop" | |
| 16 | 17 | "github.com/bnema/dumber/internal/logging" | |
| @@ -162,7 +163,8 @@ type WebView struct { | |||
| 162 | 163 | // PermissionRequest is called when a site requests permission (mic, camera, screen sharing). | |
| 163 | 164 | // Return true to indicate the request was handled. Call allow()/deny() to respond. | |
| 164 | 165 | // The permission types are determined from the request object. | |
| 165 | - | // The metadata map carries permission-type-specific context (e.g., "requesting_domain" for website_data_access). | |
| 166 | + | // The metadata map carries permission-type-specific context; for website_data_access both | |
| 167 | + | // entity.PermissionMetadataKeyRequestingDomain and entity.PermissionMetadataKeyCurrentDomain are populated. | |
| 166 | 168 | OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool | |
| 167 | 169 | ||
| 168 | 170 | logger zerolog.Logger | |
| @@ -979,15 +981,17 @@ func (wv *WebView) connectWebProcessTerminatedSignal() { | |||
| 979 | 981 | // connectPermissionRequestSignal sets up the permission-request signal handler. | |
| 980 | 982 | // This is emitted when a site calls getUserMedia() or getDisplayMedia(). | |
| 981 | 983 | func (wv *WebView) connectPermissionRequestSignal() { | |
| 982 | - | permissionCb := func(_ webkit.WebView, requestPtr uintptr) bool { | |
| 984 | + | permissionCb := func(inner webkit.WebView, requestPtr uintptr) bool { | |
| 983 | 985 | ctx := logging.WithContext(context.Background(), wv.logger) | |
| 984 | 986 | ||
| 985 | 987 | if wv.OnPermissionRequest == nil { | |
| 986 | 988 | return false // Not handled, WebKit will deny by default | |
| 987 | 989 | } | |
| 988 | 990 | ||
| 989 | - | // Extract and normalize origin from current URI | |
| 990 | - | uri := wv.URI() | |
| 991 | + | // Use the live URI from the signal's WebView rather than the cached wv.URI(), | |
| 992 | + | // so permission lookups use the current page origin even if navigation has | |
| 993 | + | // occurred since the last load-changed signal. | |
| 994 | + | uri := inner.GetUri() | |
| 991 | 995 | if uri == "" { | |
| 992 | 996 | wv.logger.Debug().Msg("permission request with empty origin, denying") | |
| 993 | 997 | return false | |
| @@ -1085,8 +1089,8 @@ func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uint | |||
| 1085 | 1089 | Str("requesting_domain", requestingDomain). | |
| 1086 | 1090 | Msg("website data access permission request") | |
| 1087 | 1091 | meta := map[string]string{ | |
| 1088 | - | "requesting_domain": requestingDomain, | |
| 1089 | - | "current_domain": currentDomain, | |
| 1092 | + | entity.PermissionMetadataKeyRequestingDomain: requestingDomain, | |
| 1093 | + | entity.PermissionMetadataKeyCurrentDomain: currentDomain, | |
| 1090 | 1094 | } | |
| 1091 | 1095 | return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), meta | |
| 1092 | 1096 | default: | |
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | "time" | 11 | "time" | ||
| 12 | 12 | ||||
| 13 | "github.com/bnema/dumber/internal/application/port" | 13 | "github.com/bnema/dumber/internal/application/port" | ||
| 14 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 14 | urlutil "github.com/bnema/dumber/internal/domain/url" | 15 | urlutil "github.com/bnema/dumber/internal/domain/url" | ||
| 15 | "github.com/bnema/dumber/internal/infrastructure/desktop" | 16 | "github.com/bnema/dumber/internal/infrastructure/desktop" | ||
| 16 | "github.com/bnema/dumber/internal/logging" | 17 | "github.com/bnema/dumber/internal/logging" | ||
| @@ -162,7 +163,8 @@ type WebView struct { | |||||
| 162 | // PermissionRequest is called when a site requests permission (mic, camera, screen sharing). | 163 | // PermissionRequest is called when a site requests permission (mic, camera, screen sharing). | ||
| 163 | // Return true to indicate the request was handled. Call allow()/deny() to respond. | 164 | // Return true to indicate the request was handled. Call allow()/deny() to respond. | ||
| 164 | // The permission types are determined from the request object. | 165 | // The permission types are determined from the request object. | ||
| 165 | - | // The metadata map carries permission-type-specific context (e.g., "requesting_domain" for website_data_access). | 166 | + | // The metadata map carries permission-type-specific context; for website_data_access both |
| 167 | + | // entity.PermissionMetadataKeyRequestingDomain and entity.PermissionMetadataKeyCurrentDomain are populated. | |||
| 166 | OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool | 168 | OnPermissionRequest func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool | ||
| 167 | 169 | ||||
| 168 | logger zerolog.Logger | 170 | logger zerolog.Logger | ||
| @@ -979,15 +981,17 @@ func (wv *WebView) connectWebProcessTerminatedSignal() { | |||||
| 979 | // connectPermissionRequestSignal sets up the permission-request signal handler. | 981 | // connectPermissionRequestSignal sets up the permission-request signal handler. | ||
| 980 | // This is emitted when a site calls getUserMedia() or getDisplayMedia(). | 982 | // This is emitted when a site calls getUserMedia() or getDisplayMedia(). | ||
| 981 | func (wv *WebView) connectPermissionRequestSignal() { | 983 | func (wv *WebView) connectPermissionRequestSignal() { | ||
| 982 | - | permissionCb := func(_ webkit.WebView, requestPtr uintptr) bool { | 984 | + | permissionCb := func(inner webkit.WebView, requestPtr uintptr) bool { |
| 983 | ctx := logging.WithContext(context.Background(), wv.logger) | 985 | ctx := logging.WithContext(context.Background(), wv.logger) | ||
| 984 | 986 | ||||
| 985 | if wv.OnPermissionRequest == nil { | 987 | if wv.OnPermissionRequest == nil { | ||
| 986 | return false // Not handled, WebKit will deny by default | 988 | return false // Not handled, WebKit will deny by default | ||
| 987 | } | 989 | } | ||
| 988 | 990 | ||||
| 989 | - | // Extract and normalize origin from current URI | 991 | + | // Use the live URI from the signal's WebView rather than the cached wv.URI(), |
| 990 | - | uri := wv.URI() | 992 | + | // so permission lookups use the current page origin even if navigation has |
| 993 | + | // occurred since the last load-changed signal. | |||
| 994 | + | uri := inner.GetUri() | |||
| 991 | if uri == "" { | 995 | if uri == "" { | ||
| 992 | wv.logger.Debug().Msg("permission request with empty origin, denying") | 996 | wv.logger.Debug().Msg("permission request with empty origin, denying") | ||
| 993 | return false | 997 | return false | ||
| @@ -1085,8 +1089,8 @@ func (wv *WebView) determinePermissionTypes(ctx context.Context, requestPtr uint | |||||
| 1085 | Str("requesting_domain", requestingDomain). | 1089 | Str("requesting_domain", requestingDomain). | ||
| 1086 | Msg("website data access permission request") | 1090 | Msg("website data access permission request") | ||
| 1087 | meta := map[string]string{ | 1091 | meta := map[string]string{ | ||
| 1088 | - | "requesting_domain": requestingDomain, | 1092 | + | entity.PermissionMetadataKeyRequestingDomain: requestingDomain, |
| 1089 | - | "current_domain": currentDomain, | 1093 | + | entity.PermissionMetadataKeyCurrentDomain: currentDomain, |
| 1090 | } | 1094 | } | ||
| 1091 | return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), meta | 1095 | return classifyPermissionRequestTypes(ctx, requestKind, false, false, false), meta | ||
| 1092 | default: | 1096 | default: | ||
M internal/ui/dialog/permission.go +2 -2
| @@ -216,8 +216,8 @@ func (d *PermissionDialog) buildBody( | |||
| 216 | 216 | parts = append(parts, "share your screen") | |
| 217 | 217 | } | |
| 218 | 218 | if f.dataAccess { | |
| 219 | - | reqDomain := metadata["requesting_domain"] | |
| 220 | - | curDomain := metadata["current_domain"] | |
| 219 | + | reqDomain := metadata[entity.PermissionMetadataKeyRequestingDomain] | |
| 220 | + | curDomain := metadata[entity.PermissionMetadataKeyCurrentDomain] | |
| 221 | 221 | if reqDomain != "" && curDomain != "" { | |
| 222 | 222 | parts = append(parts, fmt.Sprintf( | |
| 223 | 223 | "allow %s to access its data (including cookies) while you browse %s", | |
| @@ -216,8 +216,8 @@ func (d *PermissionDialog) buildBody( | |||||
| 216 | parts = append(parts, "share your screen") | 216 | parts = append(parts, "share your screen") | ||
| 217 | } | 217 | } | ||
| 218 | if f.dataAccess { | 218 | if f.dataAccess { | ||
| 219 | - | reqDomain := metadata["requesting_domain"] | 219 | + | reqDomain := metadata[entity.PermissionMetadataKeyRequestingDomain] |
| 220 | - | curDomain := metadata["current_domain"] | 220 | + | curDomain := metadata[entity.PermissionMetadataKeyCurrentDomain] |
| 221 | if reqDomain != "" && curDomain != "" { | 221 | if reqDomain != "" && curDomain != "" { | ||
| 222 | parts = append(parts, fmt.Sprintf( | 222 | parts = append(parts, fmt.Sprintf( | ||
| 223 | "allow %s to access its data (including cookies) while you browse %s", | 223 | "allow %s to access its data (including cookies) while you browse %s", | ||
M internal/ui/dialog/permission_test.go +2 -2
| @@ -163,8 +163,8 @@ func TestPermissionDialog_BuildHeadingAndBody_DisplayCombinations(t *testing.T) | |||
| 163 | 163 | // Test website_data_access with populated metadata (Epiphany-style rich message) | |
| 164 | 164 | t.Run("website data access with both domains", func(t *testing.T) { | |
| 165 | 165 | meta := entity.PermissionMetadata{ | |
| 166 | - | "requesting_domain": "accounts.google.com", | |
| 167 | - | "current_domain": "shop.example.com", | |
| 166 | + | entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com", | |
| 167 | + | entity.PermissionMetadataKeyCurrentDomain: "shop.example.com", | |
| 168 | 168 | } | |
| 169 | 169 | body := d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess}, meta) | |
| 170 | 170 | assert.Equal(t, origin+" wants to allow accounts.google.com to access its data (including cookies) while you browse shop.example.com.", body) | |
| @@ -163,8 +163,8 @@ func TestPermissionDialog_BuildHeadingAndBody_DisplayCombinations(t *testing.T) | |||||
| 163 | // Test website_data_access with populated metadata (Epiphany-style rich message) | 163 | // Test website_data_access with populated metadata (Epiphany-style rich message) | ||
| 164 | t.Run("website data access with both domains", func(t *testing.T) { | 164 | t.Run("website data access with both domains", func(t *testing.T) { | ||
| 165 | meta := entity.PermissionMetadata{ | 165 | meta := entity.PermissionMetadata{ | ||
| 166 | - | "requesting_domain": "accounts.google.com", | 166 | + | entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com", |
| 167 | - | "current_domain": "shop.example.com", | 167 | + | entity.PermissionMetadataKeyCurrentDomain: "shop.example.com", |
| 168 | } | 168 | } | ||
| 169 | body := d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess}, meta) | 169 | body := d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess}, meta) | ||
| 170 | assert.Equal(t, origin+" wants to allow accounts.google.com to access its data (including cookies) while you browse shop.example.com.", body) | 170 | assert.Equal(t, origin+" wants to allow accounts.google.com to access its data (including cookies) while you browse shop.example.com.", body) | ||