Skip to content

Commit d3d8070

Browse files
authored
[🚀 Feature] [py]: Support FedCM commands for python (#14710)
1 parent dd0b2ba commit d3d8070

File tree

10 files changed

+574
-0
lines changed

10 files changed

+574
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from enum import Enum
19+
from typing import Optional
20+
21+
22+
class LoginState(Enum):
23+
SIGN_IN = "SignIn"
24+
SIGN_UP = "SignUp"
25+
26+
27+
class Account:
28+
"""Represents an account displayed in a FedCM account list.
29+
30+
See: https://w3c-fedid.github.io/FedCM/#dictdef-identityprovideraccount
31+
https://w3c-fedid.github.io/FedCM/#webdriver-accountlist
32+
"""
33+
34+
def __init__(self, account_data):
35+
self._account_data = account_data
36+
37+
@property
38+
def account_id(self) -> Optional[str]:
39+
return self._account_data.get("accountId")
40+
41+
@property
42+
def email(self) -> Optional[str]:
43+
return self._account_data.get("email")
44+
45+
@property
46+
def name(self) -> Optional[str]:
47+
return self._account_data.get("name")
48+
49+
@property
50+
def given_name(self) -> Optional[str]:
51+
return self._account_data.get("givenName")
52+
53+
@property
54+
def picture_url(self) -> Optional[str]:
55+
return self._account_data.get("pictureUrl")
56+
57+
@property
58+
def idp_config_url(self) -> Optional[str]:
59+
return self._account_data.get("idpConfigUrl")
60+
61+
@property
62+
def terms_of_service_url(self) -> Optional[str]:
63+
return self._account_data.get("termsOfServiceUrl")
64+
65+
@property
66+
def privacy_policy_url(self) -> Optional[str]:
67+
return self._account_data.get("privacyPolicyUrl")
68+
69+
@property
70+
def login_state(self) -> Optional[str]:
71+
return self._account_data.get("loginState")
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from typing import List
19+
from typing import Optional
20+
21+
from .account import Account
22+
23+
24+
class Dialog:
25+
"""Represents a FedCM dialog that can be interacted with."""
26+
27+
DIALOG_TYPE_ACCOUNT_LIST = "AccountChooser"
28+
DIALOG_TYPE_AUTO_REAUTH = "AutoReauthn"
29+
30+
def __init__(self, driver) -> None:
31+
self._driver = driver
32+
33+
@property
34+
def type(self) -> Optional[str]:
35+
"""Gets the type of the dialog currently being shown."""
36+
return self._driver.fedcm.dialog_type
37+
38+
@property
39+
def title(self) -> str:
40+
"""Gets the title of the dialog."""
41+
return self._driver.fedcm.title
42+
43+
@property
44+
def subtitle(self) -> Optional[str]:
45+
"""Gets the subtitle of the dialog."""
46+
result = self._driver.fedcm.subtitle
47+
return result.get("subtitle") if result else None
48+
49+
def get_accounts(self) -> List[Account]:
50+
"""Gets the list of accounts shown in the dialog."""
51+
accounts = self._driver.fedcm.account_list
52+
return [Account(account) for account in accounts]
53+
54+
def select_account(self, index: int) -> None:
55+
"""Selects an account from the dialog by index."""
56+
self._driver.fedcm.select_account(index)
57+
58+
def accept(self) -> None:
59+
"""Clicks the continue button in the dialog."""
60+
self._driver.fedcm.accept()
61+
62+
def dismiss(self) -> None:
63+
"""Cancels/dismisses the dialog."""
64+
self._driver.fedcm.dismiss()

‎py/selenium/webdriver/common/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,8 @@ def ignore_local_proxy_environment_variables(self) -> None:
491491

492492
class ArgOptions(BaseOptions):
493493
BINARY_LOCATION_ERROR = "Binary Location Must be a String"
494+
# FedCM capability key
495+
FEDCM_CAPABILITY = "fedcm:accounts"
494496

495497
def __init__(self) -> None:
496498
super().__init__()

‎py/selenium/webdriver/remote/command.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,13 @@ class Command:
122122
GET_DOWNLOADABLE_FILES: str = "getDownloadableFiles"
123123
DOWNLOAD_FILE: str = "downloadFile"
124124
DELETE_DOWNLOADABLE_FILES: str = "deleteDownloadableFiles"
125+
126+
# Federated Credential Management (FedCM)
127+
GET_FEDCM_TITLE: str = "getFedcmTitle"
128+
GET_FEDCM_DIALOG_TYPE: str = "getFedcmDialogType"
129+
GET_FEDCM_ACCOUNT_LIST: str = "getFedcmAccountList"
130+
SELECT_FEDCM_ACCOUNT: str = "selectFedcmAccount"
131+
CLICK_FEDCM_DIALOG_BUTTON: str = "clickFedcmDialogButton"
132+
CANCEL_FEDCM_DIALOG: str = "cancelFedcmDialog"
133+
SET_FEDCM_DELAY: str = "setFedcmDelay"
134+
RESET_FEDCM_COOLDOWN: str = "resetFedcmCooldown"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from typing import List
19+
from typing import Optional
20+
21+
from .command import Command
22+
23+
24+
class FedCM:
25+
def __init__(self, driver) -> None:
26+
self._driver = driver
27+
28+
@property
29+
def title(self) -> str:
30+
"""Gets the title of the dialog."""
31+
return self._driver.execute(Command.GET_FEDCM_TITLE)["value"].get("title")
32+
33+
@property
34+
def subtitle(self) -> Optional[str]:
35+
"""Gets the subtitle of the dialog."""
36+
return self._driver.execute(Command.GET_FEDCM_TITLE)["value"].get("subtitle")
37+
38+
@property
39+
def dialog_type(self) -> str:
40+
"""Gets the type of the dialog currently being shown."""
41+
return self._driver.execute(Command.GET_FEDCM_DIALOG_TYPE).get("value")
42+
43+
@property
44+
def account_list(self) -> List[dict]:
45+
"""Gets the list of accounts shown in the dialog."""
46+
return self._driver.execute(Command.GET_FEDCM_ACCOUNT_LIST).get("value")
47+
48+
def select_account(self, index: int) -> None:
49+
"""Selects an account from the dialog by index."""
50+
self._driver.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index})
51+
52+
def accept(self) -> None:
53+
"""Clicks the continue button in the dialog."""
54+
self._driver.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"})
55+
56+
def dismiss(self) -> None:
57+
"""Cancels/dismisses the FedCM dialog."""
58+
self._driver.execute(Command.CANCEL_FEDCM_DIALOG)
59+
60+
def enable_delay(self) -> None:
61+
"""Re-enables the promise rejection delay for FedCM."""
62+
self._driver.execute(Command.SET_FEDCM_DELAY, {"enabled": True})
63+
64+
def disable_delay(self) -> None:
65+
"""Disables the promise rejection delay for FedCM."""
66+
self._driver.execute(Command.SET_FEDCM_DELAY, {"enabled": False})
67+
68+
def reset_cooldown(self) -> None:
69+
"""Resets the FedCM dialog cooldown, allowing immediate retriggers."""
70+
self._driver.execute(Command.RESET_FEDCM_COOLDOWN)

‎py/selenium/webdriver/remote/remote_connection.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@
126126
Command.GET_DOWNLOADABLE_FILES: ("GET", "/session/$sessionId/se/files"),
127127
Command.DOWNLOAD_FILE: ("POST", "/session/$sessionId/se/files"),
128128
Command.DELETE_DOWNLOADABLE_FILES: ("DELETE", "/session/$sessionId/se/files"),
129+
# Federated Credential Management (FedCM)
130+
Command.GET_FEDCM_TITLE: ("GET", "/session/$sessionId/fedcm/gettitle"),
131+
Command.GET_FEDCM_DIALOG_TYPE: ("GET", "/session/$sessionId/fedcm/getdialogtype"),
132+
Command.GET_FEDCM_ACCOUNT_LIST: ("GET", "/session/$sessionId/fedcm/accountlist"),
133+
Command.CLICK_FEDCM_DIALOG_BUTTON: ("POST", "/session/$sessionId/fedcm/clickdialogbutton"),
134+
Command.CANCEL_FEDCM_DIALOG: ("POST", "/session/$sessionId/fedcm/canceldialog"),
135+
Command.SELECT_FEDCM_ACCOUNT: ("POST", "/session/$sessionId/fedcm/selectaccount"),
136+
Command.SET_FEDCM_DELAY: ("POST", "/session/$sessionId/fedcm/setdelayenabled"),
137+
Command.RESET_FEDCM_COOLDOWN: ("POST", "/session/$sessionId/fedcm/resetcooldown"),
129138
}
130139

131140

‎py/selenium/webdriver/remote/webdriver.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from selenium.common.exceptions import WebDriverException
4444
from selenium.webdriver.common.bidi.script import Script
4545
from selenium.webdriver.common.by import By
46+
from selenium.webdriver.common.options import ArgOptions
4647
from selenium.webdriver.common.options import BaseOptions
4748
from selenium.webdriver.common.print_page_options import PrintOptions
4849
from selenium.webdriver.common.timeouts import Timeouts
@@ -53,10 +54,12 @@
5354
)
5455
from selenium.webdriver.support.relative_locator import RelativeBy
5556

57+
from ..common.fedcm.dialog import Dialog
5658
from .bidi_connection import BidiConnection
5759
from .client_config import ClientConfig
5860
from .command import Command
5961
from .errorhandler import ErrorHandler
62+
from .fedcm import FedCM
6063
from .file_detector import FileDetector
6164
from .file_detector import LocalFileDetector
6265
from .locator_converter import LocatorConverter
@@ -236,6 +239,7 @@ def __init__(
236239
self._authenticator_id = None
237240
self.start_client()
238241
self.start_session(capabilities)
242+
self._fedcm = FedCM(self)
239243

240244
self._websocket_connection = None
241245
self._script = None
@@ -1222,3 +1226,77 @@ def delete_downloadable_files(self) -> None:
12221226
raise WebDriverException("You must enable downloads in order to work with downloadable files.")
12231227

12241228
self.execute(Command.DELETE_DOWNLOADABLE_FILES)
1229+
1230+
@property
1231+
def fedcm(self) -> FedCM:
1232+
"""
1233+
:Returns:
1234+
- FedCM: an object providing access to all Federated Credential Management (FedCM) dialog commands.
1235+
1236+
:Usage:
1237+
::
1238+
1239+
title = driver.fedcm.title
1240+
subtitle = driver.fedcm.subtitle
1241+
dialog_type = driver.fedcm.dialog_type
1242+
accounts = driver.fedcm.account_list
1243+
driver.fedcm.select_account(0)
1244+
driver.fedcm.accept()
1245+
driver.fedcm.dismiss()
1246+
driver.fedcm.enable_delay()
1247+
driver.fedcm.disable_delay()
1248+
driver.fedcm.reset_cooldown()
1249+
"""
1250+
return self._fedcm
1251+
1252+
@property
1253+
def supports_fedcm(self) -> bool:
1254+
"""Returns whether the browser supports FedCM capabilities."""
1255+
return self.capabilities.get(ArgOptions.FEDCM_CAPABILITY, False)
1256+
1257+
def _require_fedcm_support(self):
1258+
"""Raises an exception if FedCM is not supported."""
1259+
if not self.supports_fedcm:
1260+
raise WebDriverException(
1261+
"This browser does not support Federated Credential Management. "
1262+
"Please ensure you're using a supported browser."
1263+
)
1264+
1265+
@property
1266+
def dialog(self):
1267+
"""Returns the FedCM dialog object for interaction."""
1268+
self._require_fedcm_support()
1269+
return Dialog(self)
1270+
1271+
def fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None):
1272+
"""Waits for and returns the FedCM dialog.
1273+
1274+
Args:
1275+
timeout: How long to wait for the dialog
1276+
poll_frequency: How frequently to poll
1277+
ignored_exceptions: Exceptions to ignore while waiting
1278+
1279+
Returns:
1280+
The FedCM dialog object if found
1281+
1282+
Raises:
1283+
TimeoutException if dialog doesn't appear
1284+
WebDriverException if FedCM not supported
1285+
"""
1286+
from selenium.common.exceptions import NoAlertPresentException
1287+
from selenium.webdriver.support.wait import WebDriverWait
1288+
1289+
self._require_fedcm_support()
1290+
1291+
if ignored_exceptions is None:
1292+
ignored_exceptions = (NoAlertPresentException,)
1293+
1294+
def _check_fedcm():
1295+
try:
1296+
dialog = Dialog(self)
1297+
return dialog if dialog.type else None
1298+
except NoAlertPresentException:
1299+
return None
1300+
1301+
wait = WebDriverWait(self, timeout, poll_frequency=poll_frequency, ignored_exceptions=ignored_exceptions)
1302+
return wait.until(lambda _: _check_fedcm())

0 commit comments

Comments
 (0)
close