Skip to content
Open
1 change: 1 addition & 0 deletions rb/lib/selenium/webdriver/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
require 'selenium/webdriver/common/takes_screenshot'
require 'selenium/webdriver/common/driver'
require 'selenium/webdriver/common/element'
require 'selenium/webdriver/common/web_extension'
require 'selenium/webdriver/common/shadow_root'
require 'selenium/webdriver/common/websocket_connection'
require 'selenium/webdriver/common/child_process'
Expand Down
24 changes: 24 additions & 0 deletions rb/lib/selenium/webdriver/common/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,30 @@ def network
@network ||= WebDriver::Network.new(bridge)
end

#
# Installs a browser extension from an unpacked directory, a packed extension (.xpi/.crx/.zip),
# or base64-encoded bytes; works with remote (Grid) sessions.
#
# @note Chromium requires a BiDi session and installs only unpacked directories
# (SeleniumHQ/selenium#16541); Firefox falls back to the classic endpoint without BiDi.
# @param [String] path directory, packed extension, or base64-encoded bytes
# @return [WebExtension] the installed extension
#

def install_web_extension(...)
bridge.install_web_extension(...)
end

#
# Uninstalls a browser extension installed with {#install_web_extension}.
#
# @param [WebExtension] extension the extension returned by {#install_web_extension}
#

def uninstall_web_extension(extension)
bridge.uninstall_web_extension(extension.id)
end

#-------------------------------- sugar --------------------------------

#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module HasAddons
#

def install_addon(path, temporary = nil)
WebDriver.logger.deprecate('#install_addon', '#install_web_extension', id: :install_addon)
@bridge.install_addon(path, temporary)
end

Expand All @@ -40,6 +41,7 @@ def install_addon(path, temporary = nil)
#

def uninstall_addon(id)
WebDriver.logger.deprecate('#uninstall_addon', '#uninstall_web_extension', id: :uninstall_addon)
@bridge.uninstall_addon(id)
end
end # HasAddons
Expand Down
42 changes: 42 additions & 0 deletions rb/lib/selenium/webdriver/common/web_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

# 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.

module Selenium
module WebDriver
#
# A browser extension installed via Driver#install_web_extension.
# Wraps the identifier the browser assigned; pass it to Driver#uninstall_web_extension.
#
class WebExtension
#
# @return [String] identifier assigned to the extension by the browser
#

attr_reader :id

#
# @api private
#

def initialize(id)
@id = id
end
end # WebExtension
end # WebDriver
end # Selenium
26 changes: 15 additions & 11 deletions rb/lib/selenium/webdriver/common/zipper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,31 @@ def unzip(path)
end

def zip(path)
with_tmp_zip do |zip|
::Find.find(path) do |file|
add_zip_entry zip, file, file.sub("#{path}/", '') unless File.directory?(file)
end
encode_zip(path, path)
end

zip.commit
File.open(zip.name, 'rb') { |io| Base64.strict_encode64 io.read }
end
# Keeps +path+ (a file or directory) as the archive's single top-level entry, unlike #zip
# which flattens a directory's contents; the Grid upload endpoint returns that one entry's path.
def zip_root(path)
encode_zip(path, File.dirname(path))
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
end

def zip_file(path)
# Backwards-compatible name for the file-only behavior #zip_root now subsumes.
alias zip_file zip_root

private

def encode_zip(path, base)
with_tmp_zip do |zip|
add_zip_entry zip, path, File.basename(path)
::Find.find(path) do |file|
add_zip_entry zip, file, file.sub("#{base}/", '') unless File.directory?(file)
end

zip.commit
File.open(zip.name, 'rb') { |io| Base64.strict_encode64 io.read }
end
end

private

def with_tmp_zip(&blk)
# Don't use Tempfile since it lacks rb_file_s_rename permission on Windows.
Dir.mktmpdir do |tmp_dir|
Expand Down
25 changes: 18 additions & 7 deletions rb/lib/selenium/webdriver/firefox/features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,7 @@ def commands(command)
end

def install_addon(path, temporary)
addon = if File.directory?(path)
Zipper.zip(path)
else
File.open(path, 'rb') { |crx_file| Base64.strict_encode64 crx_file.read }
end

payload = {addon: addon}
payload = {addon: encode_extension(path)}
payload[:temporary] = temporary unless temporary.nil?
execute :install_addon, {}, payload
end
Expand All @@ -53,6 +47,23 @@ def uninstall_addon(id)
execute :uninstall_addon, {}, {id: id}
end

def install_web_extension(path, allow_private_browsing: nil, permanent: nil)
unless bidi?
temporary = !permanent unless permanent.nil?
options = {temporary: temporary, allowPrivateBrowsing: allow_private_browsing}.compact
return WebDriver::WebExtension.new(execute(:install_addon, {}, {addon: encode_extension(path), **options}))
end

options = {allow_private_browsing:, permanent:}.compact
result = web_extension.moz.install(extension_data: web_extension_data(path), **options)
WebDriver::WebExtension.new(result.extension)
end

def uninstall_web_extension(extension_id)
bidi? ? web_extension.uninstall(extension: extension_id) : uninstall_addon(extension_id)
nil
end

def full_screenshot
execute :full_page_screenshot
end
Expand Down
23 changes: 23 additions & 0 deletions rb/lib/selenium/webdriver/remote/bidi_bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ def create_session(capabilities)
end
end

def install_web_extension(path)
result = web_extension.install(extension_data: web_extension_data(path))
WebExtension.new(result.extension)
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
end

def uninstall_web_extension(id)
web_extension.uninstall(extension: id)
nil
end

def get(url)
browsing_context.navigate(context: window_handle, url: url, wait: readiness_state)
nil
Expand Down Expand Up @@ -91,6 +101,19 @@ def browsing_context
@browsing_context ||= BiDi::Protocol::BrowsingContext.new(connection)
end

def web_extension
@web_extension ||= BiDi::Protocol::WebExtension.new(connection)
end

# A directory only resolves on the machine running the browser, so upload it to the remote
# end and reference the returned path; archives and base64 bytes travel inline.
def web_extension_data(path)
return web_extension.extension_base64_encoded(value: encode_extension(path)) unless File.directory?(path)

path = upload(path) if respond_to?(:upload)
web_extension.extension_path(path: path)
end

def readiness_state
READINESS_STATE.fetch(capabilities[:page_load_strategy] || 'normal')
end
Expand Down
26 changes: 20 additions & 6 deletions rb/lib/selenium/webdriver/remote/bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -593,14 +593,18 @@ def click_fedcm_dialog_button
execute :click_fedcm_dialog_button, {}, {dialogButton: 'ConfirmIdpLoginContinue'}
end

def bidi
msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
raise(WebDriver::Error::WebDriverError, msg)
def bidi(*)
raise WebDriver::Error::WebDriverError,
'BiDi must be enabled by setting #web_socket_url to true in options class'
end
alias connection bidi
alias web_extension bidi
alias install_web_extension bidi
alias uninstall_web_extension bidi
private :web_extension

def connection
msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
raise(WebDriver::Error::WebDriverError, msg)
def bidi?
!@bidi.nil?
end

def command_list
Expand All @@ -609,6 +613,16 @@ def command_list

private

def encode_extension(path)
if File.directory?(path)
Zipper.zip(path)
elsif File.file?(path)
File.open(path, 'rb') { |file| Base64.strict_encode64(file.read) }
else
path # already base64-encoded bytes
end
end

#
# executes a command on the remote server.
#
Expand Down
17 changes: 9 additions & 8 deletions rb/lib/selenium/webdriver/remote/features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,21 @@ def commands(command)
end

def upload(local_file)
unless File.file?(local_file)
WebDriver.logger.error("File detector only works with files. #{local_file.inspect} isn`t a file!",
id: :file_detector)
raise Error::WebDriverError, "You are trying to upload something that isn't a file."
end

execute :upload_file, {}, {file: Zipper.zip_file(local_file)}
execute :upload_file, {}, {file: Zipper.zip_root(local_file)}
end

def upload_if_necessary(keys)
local_files = keys.first&.split("\n")&.filter_map { |key| @file_detector.call(Array(key)) }
return keys unless local_files&.any?

keys = local_files.map { |local_file| upload(local_file) }
keys = local_files.map do |local_file|
unless File.file?(local_file)
WebDriver.logger.error("File detector only works with files. #{local_file.inspect} isn`t a file!",
id: :file_detector)
raise Error::WebDriverError, "You are trying to upload something that isn't a file."
end
upload(local_file)
end
Array(keys.join("\n"))
end

Expand Down
8 changes: 8 additions & 0 deletions rb/sig/interfaces/bridge.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@

interface _Bridge
def execute: (untyped command, ?Hash[untyped, untyped] opts, ?untyped? command_hash) -> untyped

def bidi?: () -> bool

def web_extension: () -> Selenium::WebDriver::BiDi::Protocol::WebExtension

def web_extension_data: (String path) -> untyped

def encode_extension: (String path) -> String
end
4 changes: 4 additions & 0 deletions rb/sig/lib/selenium/webdriver/common/driver.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ module Selenium

def add_virtual_authenticator: (untyped options) -> VirtualAuthenticator

def install_web_extension: (String path, **untyped options) -> WebExtension

def uninstall_web_extension: (WebExtension extension) -> void

alias first find_element

alias all find_elements
Expand Down
29 changes: 29 additions & 0 deletions rb/sig/lib/selenium/webdriver/common/web_extension.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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.


module Selenium
module WebDriver
class WebExtension
@id: String

attr_reader id: String

def initialize: (String id) -> void
end
end
end
6 changes: 5 additions & 1 deletion rb/sig/lib/selenium/webdriver/common/zipper.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ module Selenium

def self.zip: (untyped path) -> untyped

def self.zip_file: (untyped path) -> untyped
def self.zip_root: (untyped path) -> untyped

alias self.zip_file self.zip_root

private

def self.encode_zip: (untyped path, untyped base) -> untyped

def self.with_tmp_zip: () { () -> untyped } -> untyped

def self.add_zip_entry: (untyped zip, untyped file, untyped entry_name) -> untyped
Expand Down
4 changes: 4 additions & 0 deletions rb/sig/lib/selenium/webdriver/firefox/features.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ module Selenium

def uninstall_addon: (untyped id) -> untyped

def install_web_extension: (String path, ?allow_private_browsing: bool?, ?permanent: bool?) -> Selenium::WebDriver::WebExtension

def uninstall_web_extension: (String extension_id) -> void

def full_screenshot: () -> untyped

def context=: (untyped context) -> untyped
Expand Down
10 changes: 10 additions & 0 deletions rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ module Selenium

@connection: untyped

@web_extension: BiDi::Protocol::WebExtension

attr_reader bidi: BiDi

attr_reader connection: untyped

def create_session: (untyped capabilities) -> void

def install_web_extension: (String path) -> WebExtension

def uninstall_web_extension: (String id) -> void

def get: (String url) -> void

def go_back: () -> void
Expand All @@ -50,6 +56,10 @@ module Selenium

def browsing_context: () -> BiDi::Protocol::BrowsingContext

def web_extension: () -> BiDi::Protocol::WebExtension

def web_extension_data: (String path) -> untyped

def readiness_state: () -> Symbol
end
end
Expand Down
Loading