diff --git a/permission-element.bs b/permission-element.bs index 6278cf7..f1f6efd 100644 --- a/permission-element.bs +++ b/permission-element.bs @@ -9,7 +9,6 @@ Level: 0 Boilerplate: omit conformance Markup Shorthands: markdown on Editor: Daniel Vogelheim, Google LLC, vogelheim@google.com, https://www.google.com/ -Editor: Andy Paicu, Google LLC Abstract: A `` HTML element to request browser permissions in-page. Suitable styling and UI constraints on this new element ensure that the user @@ -18,8 +17,552 @@ Abstract: A `` HTML element to request browser permissions in-page. The `` element aims to be more accessible and more secure than the current permission flows. + + + # Introduction # {#intro} -# Framework # {#framwork} -# Algorithms # {#algorithms} + +[=User agents=] expose [=powerful features=] to web sites, which are features +that are important to some use cases, but can be easily abused. The arguably +canonical example of such a powerful feature is camera access, which is +essential to many use cases like online meetups, but unsolicited camera +activation would be a major privacy issue. To handle this, user +agents use [=permissions=] to ask the user whether they wish for a particular +access to be allowed or not. + +These permission requests began as a fairly direct passthrough: A site would +ask for some capability and the user agent immediately prompts the user to make +a decision for the request. Meanwhile, spam and abuse have forced user agents +to take a more opinionated approach to protect users' security, privacy, and +attention. The status quo is that users get a multitude of permission requests, +where it's oftentimes unclear to users what the consequences of these requests +might be. + +This spec introduces a new mechanism that requests access to +[=powerful features=] through an in-page element, with built-in protections +against abuse. This wants to tie permission requests to the actual context +in which they will be used, thus reducing "permission spam" and at the same +time providing implementations with a better signal of user intent. + + +# The permission element. # {#the-permission-element} + +
+
[=Categories=]:
+
[=Flow content=].
+
[=Phrasing content=].
+
[=Interactive content=].
+
[=Palpable content=].
+
[=Contexts in which this element can be used=]:
+
Where [=phrasing content=] is expected.
+
[=Content model=]:
+
[=Nothing=].
+
[=Content attributes=]:
+
[=Global attributes=]
+
{{HTMLPermissionElement/type}} — Type of permission this element applies to.
+
{{HTMLPermissionElement/isValid}} — query whether the element can currently be activated.
+
{{HTMLPermissionElement/invalidReason}} — Return a string representation of why the element currently cannot be activated.
+
{{HTMLPermissionElement/ondismiss}} — notifies when the user has dismissed the permission prompt.
+
{{HTMLPermissionElement/onresolve}} — notifies when a permission prompt has been answered by the user (positively or negatively).
+
{{HTMLPermissionElement/onvalidationstatuschange}} — notifies when the validation status changes.
+
[=Accessibility considerations=]:
+
+
[=DOM interface=]:
+
+
+    [Exposed=Window]
+    interface HTMLPermissionElement : HTMLElement {
+      [HTMLConstructor] constructor();
+      [CEReactions, Reflect] attribute DOMString type;
+
+      readonly attribute boolean isValid;
+      readonly attribute PermissionElementBlockerReason invalidReason;
+
+      attribute EventHandler onresolve;
+      attribute EventHandler ondismiss;
+      attribute EventHandler onvalidationstatuschange;
+    };
+   
+
+
+ +ISSUE: Add accessibility considerations. + +ISSUE: Check attribute & event handler & invalid reason names against + current proposal(s). + +The {{HTMLPermissionElement/type}} attribute controls the behavior of the +permission element when it is activated. Is is an [=enumerated attribute=], +whose values are the [=powerful feature/names=] of [=powerful features=]. It +has neither a +[=missing value default=] state nor a [=invalid value default=] state. + +The {{HTMLPermissionElement/isValid}} attribute reflects whether a the +permission element is not currently blocked. + +The {{HTMLPermissionElement/invalidReason}} attribute is an +[=enumerated attribute=] that reflects the internal state of the permission +element. It's value set are {{PermissionElementBlockerReason}} + +The global lang attribute is observed by the +<{permission}> element to select localized text. + +The following are the [=event handlers=] (and their corresponding [=event handler event types=]) that must be supported on <{permission}> elements [=event handler IDL attributes=]: + +
+onresolve: Event
+ondismiss: Event
+onvalidationstatuschange: Event
+
+ +ISSUE: onvalidationstatuschange is probably not a simple Event. + + +## <{permission}> element internal state ## {#permission-element-internal-state} + +The <{permission}> element [=represents=] a user-requestable [=permission=], +which the user can activate to enable (or disable) a particular permission or +set of permissions. It is core to the <{permission}> element that these +requests are triggered by the user, and not by the page's script. To enforce +this, the element checks whether the activation event is {{Event/isTrusted|trusted}}. Additionally it watches a number of conditions, like whether the element is +(partially) occluded, or if it has recently been moved. The element maintains +an internal {{[[BlockerList]]}} to keep track of this. + +The <{permission}> element has the following internal slots: + +* The \[[BlockerList]] is a + list of records, containing a + blocker timestamp and a + blocker reason. The [=blocker + reason=] is a {{PermissionElementBlockerReason}}, but not the empty string. + +* \[[IntersectionObserver]] + is a reference to an {{IntersectionObserver}}. + +* \[[Types]] is null + or an [=ordered set=] of [=powerful features=]. Null represents the + uninitialized state, which allows the value to be modified. The empty + list «[]» is the state in which no permission applies, and which + will no longer allow modification. Note that the + {{HTMLPermissionElement/type}} property reflects this internal state. + +* \[[IntersectionRect]] is a + {{DOMRectReadOnly}} that stores the most recently seen intersection, i.e. + the position of the <{permission}> relative to the [=viewport=]. + +## <{permission}>-supporting state at the [=/navigable=] ## {#permission-element-external-state} + +In order to support the <{permission}> element, the [=/navigable=] maintains +an [=ordered set=] of <{permission}> elements, \[[PermissionElements]]. This [=ordered set=] is used to evaluate the [=blockers=] of type {{PermissionElementBlockerReason/unsuccesful_registration}}. + +## <{permission}> element interesting behaviours ## {#permission-element-very-interesting} + +The <{permission}> element has a few surprising behaviours, to support its +security properties: + +### The {{HTMLPermissionElement/type}} property ### {#permission-element-type-property} + +The permission type cannot be modified. Modifying the permission type at will +may lead to user confusion, and hence we'd like to prevent it. Since, however, +a page may create a <{permission}> element dynamically we still need to offer +an API to modify it. To do do, we distinguish between a freshly initialized and +an empty or invalid (no permission) state, where the former allows setting the +type and the latter does not. + +Example: +```js +// Changing a valid type: +var pepc = document.createElement("permission"); +pepc.type = "camera"; // Okay. +pepc.type; // "camera". +pepc.type = "geolocation"; // Not okay. Would have been okay as initial assignment. +pepc.type; // "camera". Reflects the internal state, which has not changed. + +// Setting an invalid type: +pepc = document.createElement("permission"); +pepc.type = "icecream"; // Ice cream is not a powerful browser feature. Not okay. +pepc.type; // "". Reflects the internal state. +pepc.type = "camera"; // Still Not okay, because type as already been set. + // Would have been okay as initial assignment. +pepc.type; // "". Reflects the internal state, which has not changed. + +``` + +
+The HTMLPermissionElement's {{HTMLPermissionElement/type}} getter steps are: + +1. If {{[[Types]]}} is null: Return `""`. +1. Return a string, containing the concatenation of all [=powerful feature=] + names in {{[[Types]]}}, seperated by " ". + +
+ +
+The HTMLPermissionElement's {{HTMLPermissionElement/type}} setter steps are: + +1. If {{[[Types]]}} is not null: Return. +1. Set {{[[Types]]}} to «[]». +1. Parse the input as a string of [=powerful feature=] names, seperated by whitespace. +1. If any errors occured, return. +1. Check if the set of [=powerful features=] is supported for the {{HTMLPermissionElement}} by the [=user agent=]. If not, return. +1. [=list/Append=] each [=powerful feature=] name to the {{[[Types]]}} [=ordered set=]. + +Note: The supported sets of [=powerful features=] is [=implementation-defined=]. +
+ +### Activation blockers ### {#permission-element-activation-blockers} + +The key goal of the <{permission}> element is to reflect a user's conscious +choice, and we need to make sure the user cannot easily be tricked into +activating it. To do so, the <{permission}> maintains a list of blocker reasons, +which may - permanently or temporarily - prevent the element from being +activated. + +
+enum PermissionElementBlockerReason {
+  "",  // No blocker reason.
+  "type_invalid", "illegal_subframe", "unsuccesful_registration",
+  "recently_attached", "intersection_changed",
+  "intersection_out_of_viewport_or_clipped",
+  "intersection_occluded_or_distorted", "style_invalid"
+};
+
+ +The permission element keeps track of "blockers", reasons why the element (currently) cannot be activated. These blockers come with three lifetimes: Permanent, temporary, and expiring. + +: Permanent blocker +:: Once an element has a permanent blocker, it will be disabled permanently. + There are used for issues that the website owner is expected to fix. + An example is a <{permission}> element inside a <{fencedframe}>. +: Temporary blocker +:: This is a blocker that will only be valid until the blocking condition no + no longer occurs. An example is a <{permission}> element that is not + currently in view. All [=temporary blockers=] turn into + [=expiring blockers=] once the condition no longer applies. +: Expiring blocker +:: This is a blocker that is only valid for a fixed period of time. This is + used to block abuse scenarios like "click jacking". An example is + a <{permission}> element that has recently been moved. + +
+ + + + + + +
Blocker name +Blocker type +Example condition +Order hint +
{{PermissionElementBlockerReason/type_invalid}} +[=permanent blocker|permanent=] +When an unsupported {{HTMLPermissionElement/type|permission type}} has been + set. +1 +
{{PermissionElementBlockerReason/illegal_subframe}} +[=permanent blocker|permanent=] +When the <{permission}> element is used inside a <{fencedframe}>. +2 +
{{PermissionElementBlockerReason/unsuccesful_registration}} +[=temporary blocker|temporary=] +When too many other <{permission}> elements for the same + [=powerful feature=] have been inserted into the same document. +3 +
{{PermissionElementBlockerReason/recently_attached}} +[=expiring blocker|expiring=] +When the <{permission}> element has just been attached to the + DOM. +4 +
{{PermissionElementBlockerReason/intersection_changed}} +[=expiring blocker|expiring=] +When the <{permission}> element is being moved. +6 +
{{PermissionElementBlockerReason/intersection_out_of_viewport_or_clipped}} +[=temporary blocker|temporary=] +When the <{permission}> element is not or not fully in the [=viewport=]. +7 +
{{PermissionElementBlockerReason/intersection_occluded_or_distorted}} +[=temporary blocker|temporary=] +When the <{permission}> element is fully in the [=viewport=], + but still not fully visible (e.g. because it's partly behind other content). +8 +
{{PermissionElementBlockerReason/style_invalid}} +[=temporary blocker|temporary=] + +9 +
+
+ +
+To add a blocker with a +{{PermissionElementBlockerReason}} |reason| and an optional flag |expires|: + +1. [=Assert=]: |reason| is not `""`. + (The empty string in {{PermissionElementBlockerReason}} signals no blocker + is present. Why would you add a non-blocking blockern empty string?) +1. Let |timestamp| be None. +1. If |expires|, then let |timestamp| be [=current high resolution time=] + plus the [=blocker delay=]. +1. [=list/Append=] an entry to the internal {{[[BlockerList]]}} with |reason| + and |timestamp|. + +
+ +
+The blocker delay is 500ms. +
+ +
+To add an expiring blocker with a +{{PermissionElementBlockerReason}} |reason|: + +1. [=Assert=]: |reason| is listed as "expiring" in the [=blocker reason table=]. +1. [=Add a blocker=] with |reason| and true. + +
+ +
+To add a temporary blocker with a +{{PermissionElementBlockerReason}} |reason|: + +1. [=Assert=]: |reason| is listed as "temporary" in the [=blocker reason table=]. +1. [=Add a blocker=] with |reason| and false. + +
+ +
+To add a permanent blocker with a +{{PermissionElementBlockerReason}} |reason|: + +1. [=Assert=]: |reason| is listed as "permanent" in the [=blocker reason table=]. +1. [=Add a blocker=] with |reason| and false. + +
+ +
+To remove blockers with +{{PermissionElementBlockerReason}} |reason| from an |element|: + +1. [=Assert=]: |reason| is listed as "temporary" in the + [=blocker reason table=]. +1. [=list/iterate|For each=] |entry| in |element|'s {{[[BlockerList]]}}: + 1. If |entry|'s reason [=string/is|equals=] |reason|, then [=list/remove=] + |entry| from |element|'s {{[[BlockerList]]}}. +1. [=Add a blocker=] with |reason| and true. + +
+ +
+To determine a {{HTMLPermissionElement}} |element|'s +blocker: + +1. Let |blockers| be the result of [=list/sorting=] |element|'s {{[[BlockerList]]}} + with the [=blocker ordering=] algorithm. +1. If |blockers| is not [=list/empty=] and |blockers|[0] is [=HTMLPermissionElement/blocking=], then return |blockers|[0]. +1. Return nothing. + +
+ +
+To determine blocker ordering for +two blockers |a| and |b|: + +1. Let |really large number| be 99. +1. [=Assert=]: No order hint in the [=blocker reason table=] is equal to or + greater than |really large number|. +1. If |a| is [=HTMLPermissionElement/blocking=], then let |a hint| be the + order hint of |a|'s [=blocker reason|reason=] in the + [=blocker reason table=], otherwise let |a hint| be |really large number|. +1. If |b| is [=HTMLPermissionElement/blocking=], then let |b hint| be the + order hint of |b|'s [=blocker reason|reason=] in the + [=blocker reason table=], otherwise let |b hint| be |really large number|. +1. Return whether |a hint| is less than or equal to |b hint|. + +
+ +
+An {{HTMLPermissionElement}}'s [=blocker=] list's |entry| is +blocking if: + +1. |entry| has no [=blocker timestamp=], +1. or |entry| has a [=blocker timestamp=], and the [=blocker timestamp=] is + greater or equal to the [=current high resolution time=]. + +
+ +NOTE: The spec maintains blockers as a list {{[[BlockerList]]}}, which may + potentially grow indefinitely (since some blocker types simply expire, + but are not removed). + This structure is chosen for the simplicity of explanation, rather than for + efficiency. The details of this blocker structure are not observable except + for a handful of algorithms defined here, which should open plenty of + opportunities for implementations to handle this more efficiently. + +## <{permission}> element algorithms ## {#permission-element-algorithms} + +
+The {{HTMLPermissionElement}} constructor steps are: + +1. Initialize the internal {{[[Types]]}} slot to null. +1. Initialize the internal {{[[BlockerList]]}} to «[]». + +
+ +
+The {{HTMLPermissionElement}} [=insertion steps=] are: + +1. If {{[[Types]]}} is null, set {{[[Types]]}} to «[]». +1. Initialize the internal {{[[BlockerList]]}} to «[]». +1. [=set/Append=] [=this=] to [=node navigable=]'s {{[[PermissionElements]]}}. +1. Initialize the internal {{[[IntersectionRect]]}} with undefined. +1. Initialize the internal {{[[IntersectionObserver]]}} with the result of + constructing a new {{IntersectionObserver}}, with + [=HTMLPermissionElement/IntersectionObserver callback=]. +1. Call {{[[IntersectionObserver]]}}.observe([=this=]). +1. If {{[[Types]]}} [=list/is empty=], then [=add a permanent blocker=] + with reason {{PermissionElementBlockerReason/type_invalid}}. +1. If [=this=] is not [=type permissible=], then [=add a temporary blocker=] + with {{PermissionElementBlockerReason/unsuccesful_registration}}. +1. [=Add an expiring blocker=] with reason + {{PermissionElementBlockerReason/recently_attached}}. +1. If the [=navigable/traversable navigable=] of the [=node navigable=] of + [=this=] + is a [=fenced navigable=], then [=add a permanent blocker=] + with {{PermissionElementBlockerReason/illegal_subframe}}. + +
+ +
+The {{HTMLPermissionElement}} [=removing steps=] are: + +1. [=list/Remove=] [=this=] from [=node navigable=]'s {{[[PermissionElements]]}}. +1. [=Recheck type permissibility=] for [=this=]'s [=node navigable=]. + +
+ +
+HTMLPermissionElement |element|'s isValid getter steps are: + +1. Return whether |element|'s [=HTMLPermissionElement/blocker=] is Nothing. + +
+ +
+HTMLPermissionElement |element|'s invalidReason getter steps are: + +1. If |element|'s [=HTMLPermissionElement/blocker=] is Nothing, return `""`. +1. Otherwise, |element|'s [=HTMLPermissionElement/blocker=]'s reason string. + +
+ +
+A <{permission}> |element|'s [=EventTarget/activation behavior=] given |event| is: + +1. [=Assert=]: |element|'s {{[[Types]]}} is not null. +1. If |element|'s {{[[Types]]}} [=list/is empty=], then return. +1. If |event|.{{Event/isTrusted}} is false, then return. +1. If |element|.{{HTMLPermissionElement/isValid}} is false, then return. +1. [=Request permission to use=] the [=powerful features=] named in |element|'s + {{[[Types]]}}. + +Issue: What about event handlers? +
+ +
+The HTMLPermissionElement's IntersectionObserver callback implements {{IntersectionObserverCallback}} and runs the following steps: + +1. [=Assert=]: The {{IntersectionObserver}}'s {{IntersectionObserver/root}} + is the [=Document=] +1. Let |entries| be the value of the first callback parameter, the + [=/list=] of {{IntersectionObserverEntry|intersection observer entries}}. +1. [=Assert=]: |entries| is not [=list/is empty|empty=]. +1. Let |entry| be |entries|'s last [=list/item=]. +1. If |entry|.{{IntersectionObserverEntry/isVisible}}, then: + 1. [=Remove blockers=] with {{PermissionElementBlockerReason/intersection_occluded_or_distorted}}. + 1. [=Remove blockers=] with {{PermissionElementBlockerReason/intersection_out_of_viewport_or_clipped}}. +1. Otherwise: + 1. If |entry|.{{IntersectionObserverEntry/intersectionRatio}} >= 1, then: + 1. Let |reason| be {{PermissionElementBlockerReason/intersection_occluded_or_distorted}}. + 1. Otherwise: + 1. Let |reason| be {{PermissionElementBlockerReason/intersection_out_of_viewport_or_clipped}}. + 1. [=Add a temporary blocker=] with |reason|. +1. If {{[[IntersectionRect]]}} does not equal + |entry|.{{IntersectionObserverEntry/intersectionRect}} then + [=add an expiring blocker=] with + {{PermissionElementBlockerReason/intersection_changed}}. +1. Set {{[[IntersectionRect]]}} to + |entry|.{{IntersectionObserverEntry/intersectionRect}} + +ISSUE: Do I need to define dictionary equality? +
+ +
+To determine whether an |element| is type permissible: + +1. [=Assert=]: |element|'s [=node navigable=]'s {{[[PermissionElements]]}} + [=set/contains=] |element|. +1. Let |count| be 0. +1. [=list/iterate|For each=] |current| in + |element|'s [=node navigable=]'s {{[[PermissionElements]]}}: + 1. If |current| is |element|, then [=iteration/break=]. + 1. If the [=set/intersection=] of |element|.{{[[Types]]}} with + |current|.{{[[Types]]}} is not [=list/is empty|empty=], + then increment |count| by 1. +1. Return whether |count| is less than 2. + +
+ +
+To recheck type permissibility for a +|document|: + +1. [=list/iterate|For each=] |current| in |document|'s + {{[[PermissionElements]]}}: + 1. If |current| is [=type permissible=], then [=remove blockers=] with + {{PermissionElementBlockerReason/unsuccesful_registration}} from + |current|. + +
+ +# CSS Integration # {#algorithms} # Security & Privacy Considerations # {#secpriv}