Skip to content

Commit

Permalink
[🚀 Feature] [py]: Support FedCM commands for python (#14710)
Browse files Browse the repository at this point in the history
  • Loading branch information
navin772 authored Nov 15, 2024
1 parent dd0b2ba commit d3d8070
Show file tree
Hide file tree
Showing 10 changed files with 574 additions and 0 deletions.
71 changes: 71 additions & 0 deletions py/selenium/webdriver/common/fedcm/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from enum import Enum
from typing import Optional


class LoginState(Enum):
SIGN_IN = "SignIn"
SIGN_UP = "SignUp"


class Account:
"""Represents an account displayed in a FedCM account list.
See: https://w3c-fedid.github.io/FedCM/#dictdef-identityprovideraccount
https://w3c-fedid.github.io/FedCM/#webdriver-accountlist
"""

def __init__(self, account_data):
self._account_data = account_data

@property
def account_id(self) -> Optional[str]:
return self._account_data.get("accountId")

@property
def email(self) -> Optional[str]:
return self._account_data.get("email")

@property
def name(self) -> Optional[str]:
return self._account_data.get("name")

@property
def given_name(self) -> Optional[str]:
return self._account_data.get("givenName")

@property
def picture_url(self) -> Optional[str]:
return self._account_data.get("pictureUrl")

@property
def idp_config_url(self) -> Optional[str]:
return self._account_data.get("idpConfigUrl")

@property
def terms_of_service_url(self) -> Optional[str]:
return self._account_data.get("termsOfServiceUrl")

@property
def privacy_policy_url(self) -> Optional[str]:
return self._account_data.get("privacyPolicyUrl")

@property
def login_state(self) -> Optional[str]:
return self._account_data.get("loginState")
64 changes: 64 additions & 0 deletions py/selenium/webdriver/common/fedcm/dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from typing import List
from typing import Optional

from .account import Account


class Dialog:
"""Represents a FedCM dialog that can be interacted with."""

DIALOG_TYPE_ACCOUNT_LIST = "AccountChooser"
DIALOG_TYPE_AUTO_REAUTH = "AutoReauthn"

def __init__(self, driver) -> None:
self._driver = driver

@property
def type(self) -> Optional[str]:
"""Gets the type of the dialog currently being shown."""
return self._driver.fedcm.dialog_type

@property
def title(self) -> str:
"""Gets the title of the dialog."""
return self._driver.fedcm.title

@property
def subtitle(self) -> Optional[str]:
"""Gets the subtitle of the dialog."""
result = self._driver.fedcm.subtitle
return result.get("subtitle") if result else None

def get_accounts(self) -> List[Account]:
"""Gets the list of accounts shown in the dialog."""
accounts = self._driver.fedcm.account_list
return [Account(account) for account in accounts]

def select_account(self, index: int) -> None:
"""Selects an account from the dialog by index."""
self._driver.fedcm.select_account(index)

def accept(self) -> None:
"""Clicks the continue button in the dialog."""
self._driver.fedcm.accept()

def dismiss(self) -> None:
"""Cancels/dismisses the dialog."""
self._driver.fedcm.dismiss()
2 changes: 2 additions & 0 deletions py/selenium/webdriver/common/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,8 @@ def ignore_local_proxy_environment_variables(self) -> None:

class ArgOptions(BaseOptions):
BINARY_LOCATION_ERROR = "Binary Location Must be a String"
# FedCM capability key
FEDCM_CAPABILITY = "fedcm:accounts"

def __init__(self) -> None:
super().__init__()
Expand Down
10 changes: 10 additions & 0 deletions py/selenium/webdriver/remote/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,13 @@ class Command:
GET_DOWNLOADABLE_FILES: str = "getDownloadableFiles"
DOWNLOAD_FILE: str = "downloadFile"
DELETE_DOWNLOADABLE_FILES: str = "deleteDownloadableFiles"

# Federated Credential Management (FedCM)
GET_FEDCM_TITLE: str = "getFedcmTitle"
GET_FEDCM_DIALOG_TYPE: str = "getFedcmDialogType"
GET_FEDCM_ACCOUNT_LIST: str = "getFedcmAccountList"
SELECT_FEDCM_ACCOUNT: str = "selectFedcmAccount"
CLICK_FEDCM_DIALOG_BUTTON: str = "clickFedcmDialogButton"
CANCEL_FEDCM_DIALOG: str = "cancelFedcmDialog"
SET_FEDCM_DELAY: str = "setFedcmDelay"
RESET_FEDCM_COOLDOWN: str = "resetFedcmCooldown"
70 changes: 70 additions & 0 deletions py/selenium/webdriver/remote/fedcm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from typing import List
from typing import Optional

from .command import Command


class FedCM:
def __init__(self, driver) -> None:
self._driver = driver

@property
def title(self) -> str:
"""Gets the title of the dialog."""
return self._driver.execute(Command.GET_FEDCM_TITLE)["value"].get("title")

@property
def subtitle(self) -> Optional[str]:
"""Gets the subtitle of the dialog."""
return self._driver.execute(Command.GET_FEDCM_TITLE)["value"].get("subtitle")

@property
def dialog_type(self) -> str:
"""Gets the type of the dialog currently being shown."""
return self._driver.execute(Command.GET_FEDCM_DIALOG_TYPE).get("value")

@property
def account_list(self) -> List[dict]:
"""Gets the list of accounts shown in the dialog."""
return self._driver.execute(Command.GET_FEDCM_ACCOUNT_LIST).get("value")

def select_account(self, index: int) -> None:
"""Selects an account from the dialog by index."""
self._driver.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index})

def accept(self) -> None:
"""Clicks the continue button in the dialog."""
self._driver.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"})

def dismiss(self) -> None:
"""Cancels/dismisses the FedCM dialog."""
self._driver.execute(Command.CANCEL_FEDCM_DIALOG)

def enable_delay(self) -> None:
"""Re-enables the promise rejection delay for FedCM."""
self._driver.execute(Command.SET_FEDCM_DELAY, {"enabled": True})

def disable_delay(self) -> None:
"""Disables the promise rejection delay for FedCM."""
self._driver.execute(Command.SET_FEDCM_DELAY, {"enabled": False})

def reset_cooldown(self) -> None:
"""Resets the FedCM dialog cooldown, allowing immediate retriggers."""
self._driver.execute(Command.RESET_FEDCM_COOLDOWN)
9 changes: 9 additions & 0 deletions py/selenium/webdriver/remote/remote_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@
Command.GET_DOWNLOADABLE_FILES: ("GET", "/session/$sessionId/se/files"),
Command.DOWNLOAD_FILE: ("POST", "/session/$sessionId/se/files"),
Command.DELETE_DOWNLOADABLE_FILES: ("DELETE", "/session/$sessionId/se/files"),
# Federated Credential Management (FedCM)
Command.GET_FEDCM_TITLE: ("GET", "/session/$sessionId/fedcm/gettitle"),
Command.GET_FEDCM_DIALOG_TYPE: ("GET", "/session/$sessionId/fedcm/getdialogtype"),
Command.GET_FEDCM_ACCOUNT_LIST: ("GET", "/session/$sessionId/fedcm/accountlist"),
Command.CLICK_FEDCM_DIALOG_BUTTON: ("POST", "/session/$sessionId/fedcm/clickdialogbutton"),
Command.CANCEL_FEDCM_DIALOG: ("POST", "/session/$sessionId/fedcm/canceldialog"),
Command.SELECT_FEDCM_ACCOUNT: ("POST", "/session/$sessionId/fedcm/selectaccount"),
Command.SET_FEDCM_DELAY: ("POST", "/session/$sessionId/fedcm/setdelayenabled"),
Command.RESET_FEDCM_COOLDOWN: ("POST", "/session/$sessionId/fedcm/resetcooldown"),
}


Expand Down
78 changes: 78 additions & 0 deletions py/selenium/webdriver/remote/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.bidi.script import Script
from selenium.webdriver.common.by import By
from selenium.webdriver.common.options import ArgOptions
from selenium.webdriver.common.options import BaseOptions
from selenium.webdriver.common.print_page_options import PrintOptions
from selenium.webdriver.common.timeouts import Timeouts
Expand All @@ -53,10 +54,12 @@
)
from selenium.webdriver.support.relative_locator import RelativeBy

from ..common.fedcm.dialog import Dialog
from .bidi_connection import BidiConnection
from .client_config import ClientConfig
from .command import Command
from .errorhandler import ErrorHandler
from .fedcm import FedCM
from .file_detector import FileDetector
from .file_detector import LocalFileDetector
from .locator_converter import LocatorConverter
Expand Down Expand Up @@ -236,6 +239,7 @@ def __init__(
self._authenticator_id = None
self.start_client()
self.start_session(capabilities)
self._fedcm = FedCM(self)

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

self.execute(Command.DELETE_DOWNLOADABLE_FILES)

@property
def fedcm(self) -> FedCM:
"""
:Returns:
- FedCM: an object providing access to all Federated Credential Management (FedCM) dialog commands.
:Usage:
::
title = driver.fedcm.title
subtitle = driver.fedcm.subtitle
dialog_type = driver.fedcm.dialog_type
accounts = driver.fedcm.account_list
driver.fedcm.select_account(0)
driver.fedcm.accept()
driver.fedcm.dismiss()
driver.fedcm.enable_delay()
driver.fedcm.disable_delay()
driver.fedcm.reset_cooldown()
"""
return self._fedcm

@property
def supports_fedcm(self) -> bool:
"""Returns whether the browser supports FedCM capabilities."""
return self.capabilities.get(ArgOptions.FEDCM_CAPABILITY, False)

def _require_fedcm_support(self):
"""Raises an exception if FedCM is not supported."""
if not self.supports_fedcm:
raise WebDriverException(
"This browser does not support Federated Credential Management. "
"Please ensure you're using a supported browser."
)

@property
def dialog(self):
"""Returns the FedCM dialog object for interaction."""
self._require_fedcm_support()
return Dialog(self)

def fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None):
"""Waits for and returns the FedCM dialog.
Args:
timeout: How long to wait for the dialog
poll_frequency: How frequently to poll
ignored_exceptions: Exceptions to ignore while waiting
Returns:
The FedCM dialog object if found
Raises:
TimeoutException if dialog doesn't appear
WebDriverException if FedCM not supported
"""
from selenium.common.exceptions import NoAlertPresentException
from selenium.webdriver.support.wait import WebDriverWait

self._require_fedcm_support()

if ignored_exceptions is None:
ignored_exceptions = (NoAlertPresentException,)

def _check_fedcm():
try:
dialog = Dialog(self)
return dialog if dialog.type else None
except NoAlertPresentException:
return None

wait = WebDriverWait(self, timeout, poll_frequency=poll_frequency, ignored_exceptions=ignored_exceptions)
return wait.until(lambda _: _check_fedcm())
Loading

0 comments on commit d3d8070

Please sign in to comment.