diff --git a/youtube_dl/selenium/__init__.py b/youtube_dl/selenium/__init__.py new file mode 100644 index 000000000..89a4a530d --- /dev/null +++ b/youtube_dl/selenium/__init__.py @@ -0,0 +1,19 @@ +# 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. + + +__version__ = "3.11.0" diff --git a/youtube_dl/selenium/common/__init__.py b/youtube_dl/selenium/common/__init__.py new file mode 100644 index 000000000..ea71319a6 --- /dev/null +++ b/youtube_dl/selenium/common/__init__.py @@ -0,0 +1,18 @@ +# 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 . import exceptions # noqa diff --git a/youtube_dl/selenium/common/exceptions.py b/youtube_dl/selenium/common/exceptions.py new file mode 100644 index 000000000..df61eca00 --- /dev/null +++ b/youtube_dl/selenium/common/exceptions.py @@ -0,0 +1,327 @@ +# 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. + +""" +Exceptions that may happen in all the webdriver code. +""" + + +class WebDriverException(Exception): + """ + Base webdriver exception. + """ + + def __init__(self, msg=None, screen=None, stacktrace=None): + self.msg = msg + self.screen = screen + self.stacktrace = stacktrace + + def __str__(self): + exception_msg = "Message: %s\n" % self.msg + if self.screen is not None: + exception_msg += "Screenshot: available via screen\n" + if self.stacktrace is not None: + stacktrace = "\n".join(self.stacktrace) + exception_msg += "Stacktrace:\n%s" % stacktrace + return exception_msg + + +class ErrorInResponseException(WebDriverException): + """ + Thrown when an error has occurred on the server side. + + This may happen when communicating with the firefox extension + or the remote driver server. + """ + def __init__(self, response, msg): + WebDriverException.__init__(self, msg) + self.response = response + + +class InvalidSwitchToTargetException(WebDriverException): + """ + Thrown when frame or window target to be switched doesn't exist. + """ + pass + + +class NoSuchFrameException(InvalidSwitchToTargetException): + """ + Thrown when frame target to be switched doesn't exist. + """ + pass + + +class NoSuchWindowException(InvalidSwitchToTargetException): + """ + Thrown when window target to be switched doesn't exist. + + To find the current set of active window handles, you can get a list + of the active window handles in the following way:: + + print driver.window_handles + + """ + pass + + +class NoSuchElementException(WebDriverException): + """ + Thrown when element could not be found. + + If you encounter this exception, you may want to check the following: + * Check your selector used in your find_by... + * Element may not yet be on the screen at the time of the find operation, + (webpage is still loading) see selenium.webdriver.support.wait.WebDriverWait() + for how to write a wait wrapper to wait for an element to appear. + """ + pass + + +class NoSuchAttributeException(WebDriverException): + """ + Thrown when the attribute of element could not be found. + + You may want to check if the attribute exists in the particular browser you are + testing against. Some browsers may have different property names for the same + property. (IE8's .innerText vs. Firefox .textContent) + """ + pass + + +class StaleElementReferenceException(WebDriverException): + """ + Thrown when a reference to an element is now "stale". + + Stale means the element no longer appears on the DOM of the page. + + + Possible causes of StaleElementReferenceException include, but not limited to: + * You are no longer on the same page, or the page may have refreshed since the element + was located. + * The element may have been removed and re-added to the screen, since it was located. + Such as an element being relocated. + This can happen typically with a javascript framework when values are updated and the + node is rebuilt. + * Element may have been inside an iframe or another context which was refreshed. + """ + pass + + +class InvalidElementStateException(WebDriverException): + """ + Thrown when a command could not be completed because the element is in an invalid state. + + This can be caused by attempting to clear an element that isn't both editable and resettable. + """ + pass + + +class UnexpectedAlertPresentException(WebDriverException): + """ + Thrown when an unexpected alert is appeared. + + Usually raised when when an expected modal is blocking webdriver form executing any + more commands. + """ + def __init__(self, msg=None, screen=None, stacktrace=None, alert_text=None): + super(UnexpectedAlertPresentException, self).__init__(msg, screen, stacktrace) + self.alert_text = alert_text + + def __str__(self): + return "Alert Text: %s\n%s" % (self.alert_text, super(UnexpectedAlertPresentException, self).__str__()) + + +class NoAlertPresentException(WebDriverException): + """ + Thrown when switching to no presented alert. + + This can be caused by calling an operation on the Alert() class when an alert is + not yet on the screen. + """ + pass + + +class ElementNotVisibleException(InvalidElementStateException): + """ + Thrown when an element is present on the DOM, but + it is not visible, and so is not able to be interacted with. + + Most commonly encountered when trying to click or read text + of an element that is hidden from view. + """ + pass + + +class ElementNotInteractableException(InvalidElementStateException): + """ + Thrown when an element is present in the DOM but interactions + with that element will hit another element do to paint order + """ + pass + + +class ElementNotSelectableException(InvalidElementStateException): + """ + Thrown when trying to select an unselectable element. + + For example, selecting a 'script' element. + """ + pass + + +class InvalidCookieDomainException(WebDriverException): + """ + Thrown when attempting to add a cookie under a different domain + than the current URL. + """ + pass + + +class UnableToSetCookieException(WebDriverException): + """ + Thrown when a driver fails to set a cookie. + """ + pass + + +class RemoteDriverServerException(WebDriverException): + """ + """ + pass + + +class TimeoutException(WebDriverException): + """ + Thrown when a command does not complete in enough time. + """ + pass + + +class MoveTargetOutOfBoundsException(WebDriverException): + """ + Thrown when the target provided to the `ActionsChains` move() + method is invalid, i.e. out of document. + """ + pass + + +class UnexpectedTagNameException(WebDriverException): + """ + Thrown when a support class did not get an expected web element. + """ + pass + + +class InvalidSelectorException(NoSuchElementException): + """ + Thrown when the selector which is used to find an element does not return + a WebElement. Currently this only happens when the selector is an xpath + expression and it is either syntactically invalid (i.e. it is not a + xpath expression) or the expression does not select WebElements + (e.g. "count(//input)"). + """ + pass + + +class ImeNotAvailableException(WebDriverException): + """ + Thrown when IME support is not available. This exception is thrown for every IME-related + method call if IME support is not available on the machine. + """ + pass + + +class ImeActivationFailedException(WebDriverException): + """ + Thrown when activating an IME engine has failed. + """ + pass + + +class InvalidArgumentException(WebDriverException): + """ + The arguments passed to a command are either invalid or malformed. + """ + pass + + +class JavascriptException(WebDriverException): + """ + An error occurred while executing JavaScript supplied by the user. + """ + pass + + +class NoSuchCookieException(WebDriverException): + """ + No cookie matching the given path name was found amongst the associated cookies of the + current browsing context's active document. + """ + pass + + +class ScreenshotException(WebDriverException): + """ + A screen capture was made impossible. + """ + pass + + +class ElementClickInterceptedException(WebDriverException): + """ + The Element Click command could not be completed because the element receiving the events + is obscuring the element that was requested clicked. + """ + pass + + +class InsecureCertificateException(WebDriverException): + """ + Navigation caused the user agent to hit a certificate warning, which is usually the result + of an expired or invalid TLS certificate. + """ + pass + + +class InvalidCoordinatesException(WebDriverException): + """ + The coordinates provided to an interactions operation are invalid. + """ + pass + + +class InvalidSessionIdException(WebDriverException): + """ + Occurs if the given session id is not in the list of active sessions, meaning the session + either does not exist or that it's not active. + """ + pass + + +class SessionNotCreatedException(WebDriverException): + """ + A new session could not be created. + """ + pass + + +class UnknownMethodException(WebDriverException): + """ + The requested command matched a known URL but did not match an method for that URL. + """ + pass diff --git a/youtube_dl/selenium/webdriver/__init__.py b/youtube_dl/selenium/webdriver/__init__.py new file mode 100644 index 000000000..1bb8f2c8c --- /dev/null +++ b/youtube_dl/selenium/webdriver/__init__.py @@ -0,0 +1,38 @@ +# 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 .firefox.webdriver import WebDriver as Firefox # noqa +from .firefox.firefox_profile import FirefoxProfile # noqa +from .firefox.options import Options as FirefoxOptions # noqa +from .chrome.webdriver import WebDriver as Chrome # noqa +from .chrome.options import Options as ChromeOptions # noqa +from .ie.webdriver import WebDriver as Ie # noqa +from .edge.webdriver import WebDriver as Edge # noqa +from .opera.webdriver import WebDriver as Opera # noqa +from .safari.webdriver import WebDriver as Safari # noqa +from .blackberry.webdriver import WebDriver as BlackBerry # noqa +from .phantomjs.webdriver import WebDriver as PhantomJS # noqa +from .android.webdriver import WebDriver as Android # noqa +from .webkitgtk.webdriver import WebDriver as WebKitGTK # noqa +from .webkitgtk.options import Options as WebKitGTKOptions # noqa +from .remote.webdriver import WebDriver as Remote # noqa +from .common.desired_capabilities import DesiredCapabilities # noqa +from .common.action_chains import ActionChains # noqa +from .common.touch_actions import TouchActions # noqa +from .common.proxy import Proxy # noqa + +__version__ = '3.9.0' diff --git a/youtube_dl/selenium/webdriver/android/__init__.py b/youtube_dl/selenium/webdriver/android/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/android/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/android/webdriver.py b/youtube_dl/selenium/webdriver/android/webdriver.py new file mode 100644 index 000000000..68a55c27e --- /dev/null +++ b/youtube_dl/selenium/webdriver/android/webdriver.py @@ -0,0 +1,42 @@ +# 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 selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +class WebDriver(RemoteWebDriver): + """ + Simple RemoteWebDriver wrapper to start connect to Selendroid's WebView app + + For more info on getting started with Selendroid + http://selendroid.io/mobileWeb.html + """ + + def __init__(self, host="localhost", port=4444, desired_capabilities=DesiredCapabilities.ANDROID): + """ + Creates a new instance of Selendroid using the WebView app + + :Args: + - host - location of where selendroid is running + - port - port that selendroid is running on + - desired_capabilities: Dictionary object with capabilities + """ + RemoteWebDriver.__init__( + self, + command_executor="http://%s:%d/wd/hub" % (host, port), + desired_capabilities=desired_capabilities) diff --git a/youtube_dl/selenium/webdriver/blackberry/__init__.py b/youtube_dl/selenium/webdriver/blackberry/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/blackberry/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/blackberry/webdriver.py b/youtube_dl/selenium/webdriver/blackberry/webdriver.py new file mode 100644 index 000000000..bd0d6f817 --- /dev/null +++ b/youtube_dl/selenium/webdriver/blackberry/webdriver.py @@ -0,0 +1,116 @@ +# 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. + +import os +import platform +import subprocess + +try: + import http.client as http_client +except ImportError: + import httplib as http_client + +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support.ui import WebDriverWait + +LOAD_TIMEOUT = 5 + + +class WebDriver(RemoteWebDriver): + """ + Controls the BlackBerry Browser and allows you to drive it. + + :Args: + - device_password - password for the BlackBerry device or emulator you are + trying to drive + - bb_tools_dir path to the blackberry-deploy executable. If the default + is used it assumes it is in the $PATH + - hostip - the ip for the device you are trying to drive. Falls back to + 169.254.0.1 which is the default ip used + - port - the port being used for WebDriver on device. defaults to 1338 + - desired_capabilities: Dictionary object with non-browser specific + capabilities only, such as "proxy" or "loggingPref". + + Note: To get blackberry-deploy you will need to install the BlackBerry + WebWorks SDK - the default install will put it in the $PATH for you. + Download at https://developer.blackberry.com/html5/downloads/ + """ + def __init__(self, device_password, bb_tools_dir=None, + hostip='169.254.0.1', port=1338, desired_capabilities={}): + remote_addr = 'http://{}:{}'.format(hostip, port) + + filename = 'blackberry-deploy' + if platform.system() == "Windows": + filename += '.bat' + + if bb_tools_dir is not None: + if os.path.isdir(bb_tools_dir): + bb_deploy_location = os.path.join(bb_tools_dir, filename) + if not os.path.isfile(bb_deploy_location): + raise WebDriverException('Invalid blackberry-deploy location: {}'.format(bb_deploy_location)) + else: + raise WebDriverException('Invalid blackberry tools location, must be a directory: {}'.format(bb_tools_dir)) + else: + bb_deploy_location = filename + + """ + Now launch the BlackBerry browser before allowing anything else to run. + """ + try: + launch_args = [bb_deploy_location, + '-launchApp', + str(hostip), + '-package-name', 'sys.browser', + '-package-id', 'gYABgJYFHAzbeFMPCCpYWBtHAm0', + '-password', str(device_password)] + + with open(os.devnull, 'w') as fp: + p = subprocess.Popen(launch_args, stdout=fp) + + returncode = p.wait() + + if returncode == 0: + # wait for the BlackBerry10 browser to load. + is_running_args = [bb_deploy_location, + '-isAppRunning', + str(hostip), + '-package-name', 'sys.browser', + '-package-id', 'gYABgJYFHAzbeFMPCCpYWBtHAm0', + '-password', str(device_password)] + + WebDriverWait(None, LOAD_TIMEOUT)\ + .until(lambda x: subprocess.check_output(is_running_args) + .find('result::true'), + message='waiting for BlackBerry10 browser to load') + + RemoteWebDriver.__init__(self, + command_executor=remote_addr, + desired_capabilities=desired_capabilities) + else: + raise WebDriverException('blackberry-deploy failed to launch browser') + except Exception as e: + raise WebDriverException('Something went wrong launching blackberry-deploy', stacktrace=getattr(e, 'stacktrace', None)) + + def quit(self): + """ + Closes the browser and shuts down the + """ + try: + RemoteWebDriver.quit(self) + except http_client.BadStatusLine: + pass diff --git a/youtube_dl/selenium/webdriver/chrome/__init__.py b/youtube_dl/selenium/webdriver/chrome/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/chrome/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/chrome/options.py b/youtube_dl/selenium/webdriver/chrome/options.py new file mode 100644 index 000000000..33bd38864 --- /dev/null +++ b/youtube_dl/selenium/webdriver/chrome/options.py @@ -0,0 +1,192 @@ +# 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. + +import base64 +import os + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +class Options(object): + KEY = "goog:chromeOptions" + + def __init__(self): + self._binary_location = '' + self._arguments = [] + self._extension_files = [] + self._extensions = [] + self._experimental_options = {} + self._debugger_address = None + + @property + def binary_location(self): + """ + Returns the location of the binary otherwise an empty string + """ + return self._binary_location + + @binary_location.setter + def binary_location(self, value): + """ + Allows you to set where the chromium binary lives + + :Args: + - value: path to the Chromium binary + """ + self._binary_location = value + + @property + def debugger_address(self): + """ + Returns the address of the remote devtools instance + """ + return self._debugger_address + + @debugger_address.setter + def debugger_address(self, value): + """ + Allows you to set the address of the remote devtools instance + that the ChromeDriver instance will try to connect to during an + active wait. + + :Args: + - value: address of remote devtools instance if any (hostname[:port]) + """ + self._debugger_address = value + + @property + def arguments(self): + """ + Returns a list of arguments needed for the browser + """ + return self._arguments + + def add_argument(self, argument): + """ + Adds an argument to the list + + :Args: + - Sets the arguments + """ + if argument: + self._arguments.append(argument) + else: + raise ValueError("argument can not be null") + + @property + def extensions(self): + """ + Returns a list of encoded extensions that will be loaded into chrome + + """ + encoded_extensions = [] + for ext in self._extension_files: + file_ = open(ext, 'rb') + # Should not use base64.encodestring() which inserts newlines every + # 76 characters (per RFC 1521). Chromedriver has to remove those + # unnecessary newlines before decoding, causing performance hit. + encoded_extensions.append(base64.b64encode(file_.read()).decode('UTF-8')) + + file_.close() + return encoded_extensions + self._extensions + + def add_extension(self, extension): + """ + Adds the path to the extension to a list that will be used to extract it + to the ChromeDriver + + :Args: + - extension: path to the \*.crx file + """ + if extension: + extension_to_add = os.path.abspath(os.path.expanduser(extension)) + if os.path.exists(extension_to_add): + self._extension_files.append(extension_to_add) + else: + raise IOError("Path to the extension doesn't exist") + else: + raise ValueError("argument can not be null") + + def add_encoded_extension(self, extension): + """ + Adds Base64 encoded string with extension data to a list that will be used to extract it + to the ChromeDriver + + :Args: + - extension: Base64 encoded string with extension data + """ + if extension: + self._extensions.append(extension) + else: + raise ValueError("argument can not be null") + + @property + def experimental_options(self): + """ + Returns a dictionary of experimental options for chrome. + """ + return self._experimental_options + + def add_experimental_option(self, name, value): + """ + Adds an experimental option which is passed to chrome. + + Args: + name: The experimental option name. + value: The option value. + """ + self._experimental_options[name] = value + + @property + def headless(self): + """ + Returns whether or not the headless argument is set + """ + return '--headless' in self._arguments + + def set_headless(self, headless=True): + """ + Sets the headless argument + + Args: + headless: boolean value indicating to set the headless option + """ + args = {'--headless', '--disable-gpu'} + if headless: + self._arguments.extend(args) + else: + self._arguments = list(set(self._arguments) - args) + + def to_capabilities(self): + """ + Creates a capabilities with all the options that have been set and + + returns a dictionary with everything + """ + chrome = DesiredCapabilities.CHROME.copy() + + chrome_options = self.experimental_options.copy() + chrome_options["extensions"] = self.extensions + if self.binary_location: + chrome_options["binary"] = self.binary_location + chrome_options["args"] = self.arguments + if self.debugger_address: + chrome_options["debuggerAddress"] = self.debugger_address + + chrome[self.KEY] = chrome_options + + return chrome diff --git a/youtube_dl/selenium/webdriver/chrome/remote_connection.py b/youtube_dl/selenium/webdriver/chrome/remote_connection.py new file mode 100644 index 000000000..42adac3cb --- /dev/null +++ b/youtube_dl/selenium/webdriver/chrome/remote_connection.py @@ -0,0 +1,27 @@ +# 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 selenium.webdriver.remote.remote_connection import RemoteConnection + + +class ChromeRemoteConnection(RemoteConnection): + + def __init__(self, remote_server_addr, keep_alive=True): + RemoteConnection.__init__(self, remote_server_addr, keep_alive) + self._commands["launchApp"] = ('POST', '/session/$sessionId/chromium/launch_app') + self._commands["setNetworkConditions"] = ('POST', '/session/$sessionId/chromium/network_conditions') + self._commands["getNetworkConditions"] = ('GET', '/session/$sessionId/chromium/network_conditions') diff --git a/youtube_dl/selenium/webdriver/chrome/service.py b/youtube_dl/selenium/webdriver/chrome/service.py new file mode 100644 index 000000000..5a67b2cb5 --- /dev/null +++ b/youtube_dl/selenium/webdriver/chrome/service.py @@ -0,0 +1,45 @@ +# 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 selenium.webdriver.common import service + + +class Service(service.Service): + """ + Object that manages the starting and stopping of the ChromeDriver + """ + + def __init__(self, executable_path, port=0, service_args=None, + log_path=None, env=None): + """ + Creates a new instance of the Service + + :Args: + - executable_path : Path to the ChromeDriver + - port : Port the service is running on + - service_args : List of args to pass to the chromedriver service + - log_path : Path for the chromedriver service to log to""" + + self.service_args = service_args or [] + if log_path: + self.service_args.append('--log-path=%s' % log_path) + + service.Service.__init__(self, executable_path, port=port, env=env, + start_error_message="Please see https://sites.google.com/a/chromium.org/chromedriver/home") + + def command_line_args(self): + return ["--port=%d" % self.port] + self.service_args diff --git a/youtube_dl/selenium/webdriver/chrome/webdriver.py b/youtube_dl/selenium/webdriver/chrome/webdriver.py new file mode 100644 index 000000000..02533400b --- /dev/null +++ b/youtube_dl/selenium/webdriver/chrome/webdriver.py @@ -0,0 +1,132 @@ +# 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. +import warnings + +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from .remote_connection import ChromeRemoteConnection +from .service import Service +from .options import Options + + +class WebDriver(RemoteWebDriver): + """ + Controls the ChromeDriver and allows you to drive the browser. + + You will need to download the ChromeDriver executable from + http://chromedriver.storage.googleapis.com/index.html + """ + + def __init__(self, executable_path="chromedriver", port=0, + options=None, service_args=None, + desired_capabilities=None, service_log_path=None, + chrome_options=None): + """ + Creates a new instance of the chrome driver. + + Starts the service and then creates new instance of chrome driver. + + :Args: + - executable_path - path to the executable. If the default is used it assumes the executable is in the $PATH + - port - port you would like the service to run, if left as 0, a free port will be found. + - desired_capabilities: Dictionary object with non-browser specific + capabilities only, such as "proxy" or "loggingPref". + - options: this takes an instance of ChromeOptions + """ + if chrome_options: + warnings.warn('use options instead of chrome_options', DeprecationWarning) + options = chrome_options + + if options is None: + # desired_capabilities stays as passed in + if desired_capabilities is None: + desired_capabilities = self.create_options().to_capabilities() + else: + if desired_capabilities is None: + desired_capabilities = options.to_capabilities() + else: + desired_capabilities.update(options.to_capabilities()) + + self.service = Service( + executable_path, + port=port, + service_args=service_args, + log_path=service_log_path) + self.service.start() + + try: + RemoteWebDriver.__init__( + self, + command_executor=ChromeRemoteConnection( + remote_server_addr=self.service.service_url), + desired_capabilities=desired_capabilities) + except Exception: + self.quit() + raise + self._is_remote = False + + def launch_app(self, id): + """Launches Chrome app specified by id.""" + return self.execute("launchApp", {'id': id}) + + def get_network_conditions(self): + """ + Gets Chrome network emulation settings. + + :Returns: + A dict. For example: + + {'latency': 4, 'download_throughput': 2, 'upload_throughput': 2, + 'offline': False} + + """ + return self.execute("getNetworkConditions")['value'] + + def set_network_conditions(self, **network_conditions): + """ + Sets Chrome network emulation settings. + + :Args: + - network_conditions: A dict with conditions specification. + + :Usage: + driver.set_network_conditions( + offline=False, + latency=5, # additional latency (ms) + download_throughput=500 * 1024, # maximal throughput + upload_throughput=500 * 1024) # maximal throughput + + Note: 'throughput' can be used to set both (for download and upload). + """ + self.execute("setNetworkConditions", { + 'network_conditions': network_conditions + }) + + def quit(self): + """ + Closes the browser and shuts down the ChromeDriver executable + that is started when starting the ChromeDriver + """ + try: + RemoteWebDriver.quit(self) + except Exception: + # We don't care about the message because something probably has gone wrong + pass + finally: + self.service.stop() + + def create_options(self): + return Options() diff --git a/youtube_dl/selenium/webdriver/common/__init__.py b/youtube_dl/selenium/webdriver/common/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/common/action_chains.py b/youtube_dl/selenium/webdriver/common/action_chains.py new file mode 100644 index 000000000..4ed297e0f --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/action_chains.py @@ -0,0 +1,378 @@ +# 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. + +""" +The ActionChains implementation, +""" + +import time + +from selenium.webdriver.remote.command import Command + +from .utils import keys_to_typing +from .actions.action_builder import ActionBuilder + + +class ActionChains(object): + """ + ActionChains are a way to automate low level interactions such as + mouse movements, mouse button actions, key press, and context menu interactions. + This is useful for doing more complex actions like hover over and drag and drop. + + Generate user actions. + When you call methods for actions on the ActionChains object, + the actions are stored in a queue in the ActionChains object. + When you call perform(), the events are fired in the order they + are queued up. + + ActionChains can be used in a chain pattern:: + + menu = driver.find_element_by_css_selector(".nav") + hidden_submenu = driver.find_element_by_css_selector(".nav #submenu1") + + ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform() + + Or actions can be queued up one by one, then performed.:: + + menu = driver.find_element_by_css_selector(".nav") + hidden_submenu = driver.find_element_by_css_selector(".nav #submenu1") + + actions = ActionChains(driver) + actions.move_to_element(menu) + actions.click(hidden_submenu) + actions.perform() + + Either way, the actions are performed in the order they are called, one after + another. + """ + + def __init__(self, driver): + """ + Creates a new ActionChains. + + :Args: + - driver: The WebDriver instance which performs user actions. + """ + self._driver = driver + self._actions = [] + if self._driver.w3c: + self.w3c_actions = ActionBuilder(driver) + + def perform(self): + """ + Performs all stored actions. + """ + if self._driver.w3c: + self.w3c_actions.perform() + else: + for action in self._actions: + action() + + def reset_actions(self): + """ + Clears actions that are already stored on the remote end. + """ + if self._driver.w3c: + self._driver.execute(Command.W3C_CLEAR_ACTIONS) + else: + self._actions = [] + + def click(self, on_element=None): + """ + Clicks an element. + + :Args: + - on_element: The element to click. + If None, clicks on current mouse position. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.click(on_element) + self.w3c_actions.key_action.pause() + self.w3c_actions.key_action.pause() + else: + if on_element: + self.move_to_element(on_element) + self._actions.append(lambda: self._driver.execute( + Command.CLICK, {'button': 0})) + return self + + def click_and_hold(self, on_element=None): + """ + Holds down the left mouse button on an element. + + :Args: + - on_element: The element to mouse down. + If None, clicks on current mouse position. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.click_and_hold(on_element) + self.w3c_actions.key_action.pause() + if on_element: + self.w3c_actions.key_action.pause() + else: + if on_element: + self.move_to_element(on_element) + self._actions.append(lambda: self._driver.execute( + Command.MOUSE_DOWN, {})) + return self + + def context_click(self, on_element=None): + """ + Performs a context-click (right click) on an element. + + :Args: + - on_element: The element to context-click. + If None, clicks on current mouse position. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.context_click(on_element) + self.w3c_actions.key_action.pause() + else: + if on_element: + self.move_to_element(on_element) + self._actions.append(lambda: self._driver.execute( + Command.CLICK, {'button': 2})) + return self + + def double_click(self, on_element=None): + """ + Double-clicks an element. + + :Args: + - on_element: The element to double-click. + If None, clicks on current mouse position. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.double_click(on_element) + for _ in range(4): + self.w3c_actions.key_action.pause() + else: + if on_element: + self.move_to_element(on_element) + self._actions.append(lambda: self._driver.execute( + Command.DOUBLE_CLICK, {})) + return self + + def drag_and_drop(self, source, target): + """ + Holds down the left mouse button on the source element, + then moves to the target element and releases the mouse button. + + :Args: + - source: The element to mouse down. + - target: The element to mouse up. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.click_and_hold(source) \ + .move_to(target) \ + .release() + for _ in range(3): + self.w3c_actions.key_action.pause() + else: + self.click_and_hold(source) + self.release(target) + return self + + def drag_and_drop_by_offset(self, source, xoffset, yoffset): + """ + Holds down the left mouse button on the source element, + then moves to the target offset and releases the mouse button. + + :Args: + - source: The element to mouse down. + - xoffset: X offset to move to. + - yoffset: Y offset to move to. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.click_and_hold(source) \ + .move_to_location(xoffset, yoffset) \ + .release() + for _ in range(3): + self.w3c_actions.key_action.pause() + else: + self.click_and_hold(source) + self.move_by_offset(xoffset, yoffset) + self.release() + return self + + def key_down(self, value, element=None): + """ + Sends a key press only, without releasing it. + Should only be used with modifier keys (Control, Alt and Shift). + + :Args: + - value: The modifier key to send. Values are defined in `Keys` class. + - element: The element to send keys. + If None, sends a key to current focused element. + + Example, pressing ctrl+c:: + + ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform() + + """ + if element: + self.click(element) + if self._driver.w3c: + self.w3c_actions.key_action.key_down(value) + self.w3c_actions.pointer_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.SEND_KEYS_TO_ACTIVE_ELEMENT, + {"value": keys_to_typing(value)})) + return self + + def key_up(self, value, element=None): + """ + Releases a modifier key. + + :Args: + - value: The modifier key to send. Values are defined in Keys class. + - element: The element to send keys. + If None, sends a key to current focused element. + + Example, pressing ctrl+c:: + + ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform() + + """ + if element: + self.click(element) + if self._driver.w3c: + self.w3c_actions.key_action.key_up(value) + self.w3c_actions.pointer_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.SEND_KEYS_TO_ACTIVE_ELEMENT, + {"value": keys_to_typing(value)})) + return self + + def move_by_offset(self, xoffset, yoffset): + """ + Moving the mouse to an offset from current mouse position. + + :Args: + - xoffset: X offset to move to, as a positive or negative integer. + - yoffset: Y offset to move to, as a positive or negative integer. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.move_by(xoffset, yoffset) + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.MOVE_TO, { + 'xoffset': int(xoffset), + 'yoffset': int(yoffset)})) + return self + + def move_to_element(self, to_element): + """ + Moving the mouse to the middle of an element. + + :Args: + - to_element: The WebElement to move to. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.move_to(to_element) + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.MOVE_TO, {'element': to_element.id})) + return self + + def move_to_element_with_offset(self, to_element, xoffset, yoffset): + """ + Move the mouse by an offset of the specified element. + Offsets are relative to the top-left corner of the element. + + :Args: + - to_element: The WebElement to move to. + - xoffset: X offset to move to. + - yoffset: Y offset to move to. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.move_to(to_element, xoffset, yoffset) + self.w3c_actions.key_action.pause() + else: + self._actions.append( + lambda: self._driver.execute(Command.MOVE_TO, { + 'element': to_element.id, + 'xoffset': int(xoffset), + 'yoffset': int(yoffset)})) + return self + + def pause(self, seconds): + """ Pause all inputs for the specified duration in seconds """ + if self._driver.w3c: + self.w3c_actions.pointer_action.pause(seconds) + self.w3c_actions.key_action.pause(seconds) + else: + self._actions.append(lambda: time.sleep(seconds)) + return self + + def release(self, on_element=None): + """ + Releasing a held mouse button on an element. + + :Args: + - on_element: The element to mouse up. + If None, releases on current mouse position. + """ + if on_element: + self.move_to_element(on_element) + if self._driver.w3c: + self.w3c_actions.pointer_action.release() + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute(Command.MOUSE_UP, {})) + return self + + def send_keys(self, *keys_to_send): + """ + Sends keys to current focused element. + + :Args: + - keys_to_send: The keys to send. Modifier keys constants can be found in the + 'Keys' class. + """ + if self._driver.w3c: + self.w3c_actions.key_action.send_keys(keys_to_send) + else: + self._actions.append(lambda: self._driver.execute( + Command.SEND_KEYS_TO_ACTIVE_ELEMENT, {'value': keys_to_typing(keys_to_send)})) + return self + + def send_keys_to_element(self, element, *keys_to_send): + """ + Sends keys to an element. + + :Args: + - element: The element to send keys. + - keys_to_send: The keys to send. Modifier keys constants can be found in the + 'Keys' class. + """ + if self._driver.w3c: + self.w3c_actions.key_action.send_keys(keys_to_send, element=element) + else: + self._actions.append(lambda: element.send_keys(*keys_to_send)) + return self + + # Context manager so ActionChains can be used in a 'with .. as' statements. + def __enter__(self): + return self # Return created instance of self. + + def __exit__(self, _type, _value, _traceback): + pass # Do nothing, does not require additional cleanup. diff --git a/youtube_dl/selenium/webdriver/common/actions/__init__.py b/youtube_dl/selenium/webdriver/common/actions/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/common/actions/action_builder.py b/youtube_dl/selenium/webdriver/common/actions/action_builder.py new file mode 100644 index 000000000..18b2dee4c --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/action_builder.py @@ -0,0 +1,82 @@ +# 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 selenium.webdriver.remote.command import Command +from . import interaction +from .key_actions import KeyActions +from .key_input import KeyInput +from .pointer_actions import PointerActions +from .pointer_input import PointerInput + + +class ActionBuilder(object): + def __init__(self, driver, mouse=None, keyboard=None): + if mouse is None: + mouse = PointerInput(interaction.POINTER, "mouse") + if keyboard is None: + keyboard = KeyInput(interaction.KEY) + self.devices = [mouse, keyboard] + self._key_action = KeyActions(keyboard) + self._pointer_action = PointerActions(mouse) + self.driver = driver + + def get_device_with(self, name): + try: + idx = self.devices.index(name) + return self.devices[idx] + except: + pass + + @property + def pointer_inputs(self): + return [device for device in self.devices if device.type == interaction.POINTER] + + @property + def key_inputs(self): + return [device for device in self.devices if device.type == interaction.KEY] + + @property + def key_action(self): + return self._key_action + + @property + def pointer_action(self): + return self._pointer_action + + def add_key_input(self, name): + new_input = KeyInput(name) + self._add_input(new_input) + return new_input + + def add_pointer_input(self, type_, name): + new_input = PointerInput(type_, name) + self._add_input(new_input) + return new_input + + def perform(self): + enc = {"actions": []} + for device in self.devices: + encoded = device.encode() + if encoded['actions']: + enc["actions"].append(encoded) + self.driver.execute(Command.W3C_ACTIONS, enc) + + def clear_actions(self): + self.driver.execute(Command.W3C_CLEAR_ACTIONS) + + def _add_input(self, input): + self.devices.append(input) diff --git a/youtube_dl/selenium/webdriver/common/actions/input_device.py b/youtube_dl/selenium/webdriver/common/actions/input_device.py new file mode 100644 index 000000000..984ef3100 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/input_device.py @@ -0,0 +1,43 @@ +# 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. + +import uuid + + +class InputDevice(object): + """ + Describes the input device being used for the action. + """ + def __init__(self, name=None): + if name is None: + self.name = uuid.uuid4() + else: + self.name = name + + self.actions = [] + + def add_action(self, action): + """ + + """ + self.actions.append(action) + + def clear_actions(self): + self.actions = [] + + def create_pause(self, duraton=0): + pass diff --git a/youtube_dl/selenium/webdriver/common/actions/interaction.py b/youtube_dl/selenium/webdriver/common/actions/interaction.py new file mode 100644 index 000000000..3c808941c --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/interaction.py @@ -0,0 +1,43 @@ +# 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. + + +KEY = "key" +POINTER = "pointer" +NONE = "none" +SOURCE_TYPES = set([KEY, POINTER, NONE]) + + +class Interaction(object): + + PAUSE = "pause" + + def __init__(self, source): + self.source = source + + +class Pause(Interaction): + + def __init__(self, source, duration=0): + super(Interaction, self).__init__() + self.source = source + self.duration = duration + + def encode(self): + output = {"type": self.PAUSE} + output["duration"] = self.duration * 1000 + return output diff --git a/youtube_dl/selenium/webdriver/common/actions/key_actions.py b/youtube_dl/selenium/webdriver/common/actions/key_actions.py new file mode 100644 index 000000000..f0c749283 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/key_actions.py @@ -0,0 +1,52 @@ +# 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 .interaction import Interaction +from .key_input import KeyInput +from ..utils import keys_to_typing + + +class KeyActions(Interaction): + + def __init__(self, source=None): + if source is None: + source = KeyInput() + self.source = source + super(KeyActions, self).__init__(source) + + def key_down(self, letter, element=None): + return self._key_action("create_key_down", + letter, element) + + def key_up(self, letter, element=None): + return self._key_action("create_key_up", + letter, element) + + def pause(self, duration=0): + return self._key_action("create_pause", duration) + + def send_keys(self, text, element=None): + if not isinstance(text, list): + text = keys_to_typing(text) + for letter in text: + self.key_down(letter, element) + self.key_up(letter, element) + return self + + def _key_action(self, action, letter, element=None): + meth = getattr(self.source, action) + meth(letter) + return self diff --git a/youtube_dl/selenium/webdriver/common/actions/key_input.py b/youtube_dl/selenium/webdriver/common/actions/key_input.py new file mode 100644 index 000000000..f392c6bc6 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/key_input.py @@ -0,0 +1,51 @@ +# 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 . import interaction + +from .input_device import InputDevice +from .interaction import (Interaction, + Pause) + + +class KeyInput(InputDevice): + def __init__(self, name): + super(KeyInput, self).__init__() + self.name = name + self.type = interaction.KEY + + def encode(self): + return {"type": self.type, "id": self.name, "actions": [acts.encode() for acts in self.actions]} + + def create_key_down(self, key): + self.add_action(TypingInteraction(self, "keyDown", key)) + + def create_key_up(self, key): + self.add_action(TypingInteraction(self, "keyUp", key)) + + def create_pause(self, pause_duration=0): + self.add_action(Pause(self, pause_duration)) + + +class TypingInteraction(Interaction): + + def __init__(self, source, type_, key): + super(TypingInteraction, self).__init__(source) + self.type = type_ + self.key = key + + def encode(self): + return {"type": self.type, "value": self.key} diff --git a/youtube_dl/selenium/webdriver/common/actions/mouse_button.py b/youtube_dl/selenium/webdriver/common/actions/mouse_button.py new file mode 100644 index 000000000..7261fd821 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/mouse_button.py @@ -0,0 +1,5 @@ +class MouseButton(object): + + LEFT = 0 + MIDDLE = 1 + RIGHT = 2 diff --git a/youtube_dl/selenium/webdriver/common/actions/pointer_actions.py b/youtube_dl/selenium/webdriver/common/actions/pointer_actions.py new file mode 100644 index 000000000..d3ca19be6 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/pointer_actions.py @@ -0,0 +1,100 @@ +# 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 . import interaction + +from .interaction import Interaction +from .mouse_button import MouseButton +from .pointer_input import PointerInput + +from selenium.webdriver.remote.webelement import WebElement + + +class PointerActions(Interaction): + + def __init__(self, source=None): + if source is None: + source = PointerInput(interaction.POINTER, "mouse") + self.source = source + super(PointerActions, self).__init__(source) + + def pointer_down(self, button=MouseButton.LEFT): + self._button_action("create_pointer_down", button=button) + + def pointer_up(self, button=MouseButton.LEFT): + self._button_action("create_pointer_up", button=button) + + def move_to(self, element, x=None, y=None): + if not isinstance(element, WebElement): + raise AttributeError("move_to requires a WebElement") + if x is not None or y is not None: + el_rect = element.rect + left_offset = el_rect['width'] / 2 + top_offset = el_rect['height'] / 2 + left = -left_offset + (x or 0) + top = -top_offset + (y or 0) + else: + left = 0 + top = 0 + self.source.create_pointer_move(origin=element, x=int(left), y=int(top)) + return self + + def move_by(self, x, y): + self.source.create_pointer_move(origin=interaction.POINTER, x=int(x), y=int(y)) + return self + + def move_to_location(self, x, y): + self.source.create_pointer_move(origin='viewport', x=int(x), y=int(y)) + return self + + def click(self, element=None): + if element: + self.move_to(element) + self.pointer_down(MouseButton.LEFT) + self.pointer_up(MouseButton.LEFT) + return self + + def context_click(self, element=None): + if element: + self.move_to(element) + self.pointer_down(MouseButton.RIGHT) + self.pointer_up(MouseButton.RIGHT) + return self + + def click_and_hold(self, element=None): + if element: + self.move_to(element) + self.pointer_down() + return self + + def release(self): + self.pointer_up() + return self + + def double_click(self, element=None): + if element: + self.move_to(element) + self.click() + self.click() + + def pause(self, duration=0): + self.source.create_pause(duration) + return self + + def _button_action(self, action, button=MouseButton.LEFT): + meth = getattr(self.source, action) + meth(button) + return self diff --git a/youtube_dl/selenium/webdriver/common/actions/pointer_input.py b/youtube_dl/selenium/webdriver/common/actions/pointer_input.py new file mode 100644 index 000000000..724ee5a4e --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/actions/pointer_input.py @@ -0,0 +1,58 @@ +# 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 .input_device import InputDevice + +from selenium.webdriver.remote.webelement import WebElement + + +class PointerInput(InputDevice): + + DEFAULT_MOVE_DURATION = 250 + + def __init__(self, type_, name): + super(PointerInput, self).__init__() + self.type = type_ + self.name = name + + def create_pointer_move(self, duration=DEFAULT_MOVE_DURATION, x=None, y=None, origin=None): + action = dict(type="pointerMove", duration=duration) + action["x"] = x + action["y"] = y + if isinstance(origin, WebElement): + action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id} + elif origin is not None: + action["origin"] = origin + + self.add_action(action) + + def create_pointer_down(self, button): + self.add_action({"type": "pointerDown", "duration": 0, "button": button}) + + def create_pointer_up(self, button): + self.add_action({"type": "pointerUp", "duration": 0, "button": button}) + + def create_pointer_cancel(self): + self.add_action({"type": "pointerCancel"}) + + def create_pause(self, pause_duration): + self.add_action({"type": "pause", "duration": pause_duration * 1000}) + + def encode(self): + return {"type": self.type, + "parameters": {"pointerType": self.name}, + "id": self.name, + "actions": [acts for acts in self.actions]} diff --git a/youtube_dl/selenium/webdriver/common/alert.py b/youtube_dl/selenium/webdriver/common/alert.py new file mode 100644 index 000000000..c37fdee7d --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/alert.py @@ -0,0 +1,122 @@ +# 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. + +""" +The Alert implementation. +""" + +from selenium.webdriver.common.utils import keys_to_typing +from selenium.webdriver.remote.command import Command + + +class Alert(object): + """ + Allows to work with alerts. + + Use this class to interact with alert prompts. It contains methods for dismissing, + accepting, inputting, and getting text from alert prompts. + + Accepting / Dismissing alert prompts:: + + Alert(driver).accept() + Alert(driver).dismiss() + + Inputting a value into an alert prompt: + + name_prompt = Alert(driver) + name_prompt.send_keys("Willian Shakesphere") + name_prompt.accept() + + + Reading a the text of a prompt for verification: + + alert_text = Alert(driver).text + self.assertEqual("Do you wish to quit?", alert_text) + + """ + + def __init__(self, driver): + """ + Creates a new Alert. + + :Args: + - driver: The WebDriver instance which performs user actions. + """ + self.driver = driver + + @property + def text(self): + """ + Gets the text of the Alert. + """ + if self.driver.w3c: + return self.driver.execute(Command.W3C_GET_ALERT_TEXT)["value"] + else: + return self.driver.execute(Command.GET_ALERT_TEXT)["value"] + + def dismiss(self): + """ + Dismisses the alert available. + """ + if self.driver.w3c: + self.driver.execute(Command.W3C_DISMISS_ALERT) + else: + self.driver.execute(Command.DISMISS_ALERT) + + def accept(self): + """ + Accepts the alert available. + + Usage:: + Alert(driver).accept() # Confirm a alert dialog. + """ + if self.driver.w3c: + self.driver.execute(Command.W3C_ACCEPT_ALERT) + else: + self.driver.execute(Command.ACCEPT_ALERT) + + def send_keys(self, keysToSend): + """ + Send Keys to the Alert. + + :Args: + - keysToSend: The text to be sent to Alert. + + + """ + if self.driver.w3c: + self.driver.execute(Command.W3C_SET_ALERT_VALUE, {'value': keys_to_typing(keysToSend), + 'text': keysToSend}) + else: + self.driver.execute(Command.SET_ALERT_VALUE, {'text': keysToSend}) + + def authenticate(self, username, password): + """ + Send the username / password to an Authenticated dialog (like with Basic HTTP Auth). + Implicitly 'clicks ok' + + Usage:: + driver.switch_to.alert.authenticate('cheese', 'secretGouda') + + :Args: + -username: string to be set in the username section of the dialog + -password: string to be set in the password section of the dialog + """ + self.driver.execute( + Command.SET_ALERT_CREDENTIALS, + {'username': username, 'password': password}) + self.accept() diff --git a/youtube_dl/selenium/webdriver/common/by.py b/youtube_dl/selenium/webdriver/common/by.py new file mode 100644 index 000000000..7b1228d2a --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/by.py @@ -0,0 +1,35 @@ +# 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. + +""" +The By implementation. +""" + + +class By(object): + """ + Set of supported locator strategies. + """ + + ID = "id" + XPATH = "xpath" + LINK_TEXT = "link text" + PARTIAL_LINK_TEXT = "partial link text" + NAME = "name" + TAG_NAME = "tag name" + CLASS_NAME = "class name" + CSS_SELECTOR = "css selector" diff --git a/youtube_dl/selenium/webdriver/common/desired_capabilities.py b/youtube_dl/selenium/webdriver/common/desired_capabilities.py new file mode 100644 index 000000000..a1d09a557 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/desired_capabilities.py @@ -0,0 +1,128 @@ +# 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. + +""" +The Desired Capabilities implementation. +""" + + +class DesiredCapabilities(object): + """ + Set of default supported desired capabilities. + + Use this as a starting point for creating a desired capabilities object for + requesting remote webdrivers for connecting to selenium server or selenium grid. + + Usage Example:: + + from selenium import webdriver + + selenium_grid_url = "http://198.0.0.1:4444/wd/hub" + + # Create a desired capabilities object as a starting point. + capabilities = DesiredCapabilities.FIREFOX.copy() + capabilities['platform'] = "WINDOWS" + capabilities['version'] = "10" + + # Instantiate an instance of Remote WebDriver with the desired capabilities. + driver = webdriver.Remote(desired_capabilities=capabilities, + command_executor=selenium_grid_url) + + Note: Always use '.copy()' on the DesiredCapabilities object to avoid the side + effects of altering the Global class instance. + + """ + + FIREFOX = { + "browserName": "firefox", + "marionette": True, + "acceptInsecureCerts": True, + } + + INTERNETEXPLORER = { + "browserName": "internet explorer", + "version": "", + "platform": "WINDOWS", + } + + EDGE = { + "browserName": "MicrosoftEdge", + "version": "", + "platform": "WINDOWS" + } + + CHROME = { + "browserName": "chrome", + "version": "", + "platform": "ANY", + } + + OPERA = { + "browserName": "opera", + "version": "", + "platform": "ANY", + } + + SAFARI = { + "browserName": "safari", + "version": "", + "platform": "MAC", + } + + HTMLUNIT = { + "browserName": "htmlunit", + "version": "", + "platform": "ANY", + } + + HTMLUNITWITHJS = { + "browserName": "htmlunit", + "version": "firefox", + "platform": "ANY", + "javascriptEnabled": True, + } + + IPHONE = { + "browserName": "iPhone", + "version": "", + "platform": "MAC", + } + + IPAD = { + "browserName": "iPad", + "version": "", + "platform": "MAC", + } + + ANDROID = { + "browserName": "android", + "version": "", + "platform": "ANDROID", + } + + PHANTOMJS = { + "browserName": "phantomjs", + "version": "", + "platform": "ANY", + "javascriptEnabled": True, + } + + WEBKITGTK = { + "browserName": "MiniBrowser", + "version": "", + "platform": "ANY", + } diff --git a/youtube_dl/selenium/webdriver/common/html5/__init__.py b/youtube_dl/selenium/webdriver/common/html5/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/html5/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/common/html5/application_cache.py b/youtube_dl/selenium/webdriver/common/html5/application_cache.py new file mode 100644 index 000000000..ecfe5e10d --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/html5/application_cache.py @@ -0,0 +1,48 @@ +# 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. + +""" +The ApplicationCache implementaion. +""" + +from selenium.webdriver.remote.command import Command + + +class ApplicationCache(object): + + UNCACHED = 0 + IDLE = 1 + CHECKING = 2 + DOWNLOADING = 3 + UPDATE_READY = 4 + OBSOLETE = 5 + + def __init__(self, driver): + """ + Creates a new Aplication Cache. + + :Args: + - driver: The WebDriver instance which performs user actions. + """ + self.driver = driver + + @property + def status(self): + """ + Returns a current status of application cache. + """ + return self.driver.execute(Command.GET_APP_CACHE_STATUS)['value'] diff --git a/youtube_dl/selenium/webdriver/common/keys.py b/youtube_dl/selenium/webdriver/common/keys.py new file mode 100644 index 000000000..cd3bb76c7 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/keys.py @@ -0,0 +1,96 @@ +# 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. + +""" +The Keys implementation. +""" + +from __future__ import unicode_literals + + +class Keys(object): + """ + Set of special keys codes. + """ + + NULL = '\ue000' + CANCEL = '\ue001' # ^break + HELP = '\ue002' + BACKSPACE = '\ue003' + BACK_SPACE = BACKSPACE + TAB = '\ue004' + CLEAR = '\ue005' + RETURN = '\ue006' + ENTER = '\ue007' + SHIFT = '\ue008' + LEFT_SHIFT = SHIFT + CONTROL = '\ue009' + LEFT_CONTROL = CONTROL + ALT = '\ue00a' + LEFT_ALT = ALT + PAUSE = '\ue00b' + ESCAPE = '\ue00c' + SPACE = '\ue00d' + PAGE_UP = '\ue00e' + PAGE_DOWN = '\ue00f' + END = '\ue010' + HOME = '\ue011' + LEFT = '\ue012' + ARROW_LEFT = LEFT + UP = '\ue013' + ARROW_UP = UP + RIGHT = '\ue014' + ARROW_RIGHT = RIGHT + DOWN = '\ue015' + ARROW_DOWN = DOWN + INSERT = '\ue016' + DELETE = '\ue017' + SEMICOLON = '\ue018' + EQUALS = '\ue019' + + NUMPAD0 = '\ue01a' # number pad keys + NUMPAD1 = '\ue01b' + NUMPAD2 = '\ue01c' + NUMPAD3 = '\ue01d' + NUMPAD4 = '\ue01e' + NUMPAD5 = '\ue01f' + NUMPAD6 = '\ue020' + NUMPAD7 = '\ue021' + NUMPAD8 = '\ue022' + NUMPAD9 = '\ue023' + MULTIPLY = '\ue024' + ADD = '\ue025' + SEPARATOR = '\ue026' + SUBTRACT = '\ue027' + DECIMAL = '\ue028' + DIVIDE = '\ue029' + + F1 = '\ue031' # function keys + F2 = '\ue032' + F3 = '\ue033' + F4 = '\ue034' + F5 = '\ue035' + F6 = '\ue036' + F7 = '\ue037' + F8 = '\ue038' + F9 = '\ue039' + F10 = '\ue03a' + F11 = '\ue03b' + F12 = '\ue03c' + + META = '\ue03d' + COMMAND = '\ue03d' diff --git a/youtube_dl/selenium/webdriver/common/proxy.py b/youtube_dl/selenium/webdriver/common/proxy.py new file mode 100644 index 000000000..ccd362af6 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/proxy.py @@ -0,0 +1,334 @@ +# 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. + +""" +The Proxy implementation. +""" + + +class ProxyTypeFactory: + """ + Factory for proxy types. + """ + + @staticmethod + def make(ff_value, string): + return {'ff_value': ff_value, 'string': string} + + +class ProxyType: + """ + Set of possible types of proxy. + + Each proxy type has 2 properties: + 'ff_value' is value of Firefox profile preference, + 'string' is id of proxy type. + """ + + DIRECT = ProxyTypeFactory.make(0, 'DIRECT') # Direct connection, no proxy (default on Windows). + MANUAL = ProxyTypeFactory.make(1, 'MANUAL') # Manual proxy settings (e.g., for httpProxy). + PAC = ProxyTypeFactory.make(2, 'PAC') # Proxy autoconfiguration from URL. + RESERVED_1 = ProxyTypeFactory.make(3, 'RESERVED1') # Never used. + AUTODETECT = ProxyTypeFactory.make(4, 'AUTODETECT') # Proxy autodetection (presumably with WPAD). + SYSTEM = ProxyTypeFactory.make(5, 'SYSTEM') # Use system settings (default on Linux). + UNSPECIFIED = ProxyTypeFactory.make(6, 'UNSPECIFIED') # Not initialized (for internal use). + + @classmethod + def load(cls, value): + if isinstance(value, dict) and 'string' in value: + value = value['string'] + value = str(value).upper() + for attr in dir(cls): + attr_value = getattr(cls, attr) + if isinstance(attr_value, dict) and \ + 'string' in attr_value and \ + attr_value['string'] is not None and \ + attr_value['string'] == value: + return attr_value + raise Exception("No proxy type is found for %s" % (value)) + + +class Proxy(object): + """ + Proxy contains information about proxy type and necessary proxy settings. + """ + + proxyType = ProxyType.UNSPECIFIED + autodetect = False + ftpProxy = '' + httpProxy = '' + noProxy = '' + proxyAutoconfigUrl = '' + sslProxy = '' + socksProxy = '' + socksUsername = '' + socksPassword = '' + + def __init__(self, raw=None): + """ + Creates a new Proxy. + + :Args: + - raw: raw proxy data. If None, default class values are used. + """ + if raw is not None: + if 'proxyType' in raw and raw['proxyType'] is not None: + self.proxy_type = ProxyType.load(raw['proxyType']) + if 'ftpProxy' in raw and raw['ftpProxy'] is not None: + self.ftp_proxy = raw['ftpProxy'] + if 'httpProxy' in raw and raw['httpProxy'] is not None: + self.http_proxy = raw['httpProxy'] + if 'noProxy' in raw and raw['noProxy'] is not None: + self.no_proxy = raw['noProxy'] + if 'proxyAutoconfigUrl' in raw and raw['proxyAutoconfigUrl'] is not None: + self.proxy_autoconfig_url = raw['proxyAutoconfigUrl'] + if 'sslProxy' in raw and raw['sslProxy'] is not None: + self.sslProxy = raw['sslProxy'] + if 'autodetect' in raw and raw['autodetect'] is not None: + self.auto_detect = raw['autodetect'] + if 'socksProxy' in raw and raw['socksProxy'] is not None: + self.socks_proxy = raw['socksProxy'] + if 'socksUsername' in raw and raw['socksUsername'] is not None: + self.socks_username = raw['socksUsername'] + if 'socksPassword' in raw and raw['socksPassword'] is not None: + self.socks_password = raw['socksPassword'] + + @property + def proxy_type(self): + """ + Returns proxy type as `ProxyType`. + """ + return self.proxyType + + @proxy_type.setter + def proxy_type(self, value): + """ + Sets proxy type. + + :Args: + - value: The proxy type. + """ + self._verify_proxy_type_compatibility(value) + self.proxyType = value + + @property + def auto_detect(self): + """ + Returns autodetect setting. + """ + return self.autodetect + + @auto_detect.setter + def auto_detect(self, value): + """ + Sets autodetect setting. + + :Args: + - value: The autodetect value. + """ + if isinstance(value, bool): + if self.autodetect is not value: + self._verify_proxy_type_compatibility(ProxyType.AUTODETECT) + self.proxyType = ProxyType.AUTODETECT + self.autodetect = value + else: + raise ValueError("Autodetect proxy value needs to be a boolean") + + @property + def ftp_proxy(self): + """ + Returns ftp proxy setting. + """ + return self.ftpProxy + + @ftp_proxy.setter + def ftp_proxy(self, value): + """ + Sets ftp proxy setting. + + :Args: + - value: The ftp proxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.ftpProxy = value + + @property + def http_proxy(self): + """ + Returns http proxy setting. + """ + return self.httpProxy + + @http_proxy.setter + def http_proxy(self, value): + """ + Sets http proxy setting. + + :Args: + - value: The http proxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.httpProxy = value + + @property + def no_proxy(self): + """ + Returns noproxy setting. + """ + return self.noProxy + + @no_proxy.setter + def no_proxy(self, value): + """ + Sets noproxy setting. + + :Args: + - value: The noproxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.noProxy = value + + @property + def proxy_autoconfig_url(self): + """ + Returns proxy autoconfig url setting. + """ + return self.proxyAutoconfigUrl + + @proxy_autoconfig_url.setter + def proxy_autoconfig_url(self, value): + """ + Sets proxy autoconfig url setting. + + :Args: + - value: The proxy autoconfig url value. + """ + self._verify_proxy_type_compatibility(ProxyType.PAC) + self.proxyType = ProxyType.PAC + self.proxyAutoconfigUrl = value + + @property + def ssl_proxy(self): + """ + Returns https proxy setting. + """ + return self.sslProxy + + @ssl_proxy.setter + def ssl_proxy(self, value): + """ + Sets https proxy setting. + + :Args: + - value: The https proxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.sslProxy = value + + @property + def socks_proxy(self): + """ + Returns socks proxy setting. + """ + return self.socksProxy + + @socks_proxy.setter + def socks_proxy(self, value): + """ + Sets socks proxy setting. + + :Args: + - value: The socks proxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.socksProxy = value + + @property + def socks_username(self): + """ + Returns socks proxy username setting. + """ + return self.socksUsername + + @socks_username.setter + def socks_username(self, value): + """ + Sets socks proxy username setting. + + :Args: + - value: The socks proxy username value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.socksUsername = value + + @property + def socks_password(self): + """ + Returns socks proxy password setting. + """ + return self.socksPassword + + @socks_password.setter + def socks_password(self, value): + """ + Sets socks proxy password setting. + + :Args: + - value: The socks proxy password value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.socksPassword = value + + def _verify_proxy_type_compatibility(self, compatibleProxy): + if self.proxyType != ProxyType.UNSPECIFIED and self.proxyType != compatibleProxy: + raise Exception(" Specified proxy type (%s) not compatible with current setting (%s)" % (compatibleProxy, self.proxyType)) + + def add_to_capabilities(self, capabilities): + """ + Adds proxy information as capability in specified capabilities. + + :Args: + - capabilities: The capabilities to which proxy will be added. + """ + proxy_caps = {} + proxy_caps['proxyType'] = self.proxyType['string'] + if self.autodetect: + proxy_caps['autodetect'] = self.autodetect + if self.ftpProxy: + proxy_caps['ftpProxy'] = self.ftpProxy + if self.httpProxy: + proxy_caps['httpProxy'] = self.httpProxy + if self.proxyAutoconfigUrl: + proxy_caps['proxyAutoconfigUrl'] = self.proxyAutoconfigUrl + if self.sslProxy: + proxy_caps['sslProxy'] = self.sslProxy + if self.noProxy: + proxy_caps['noProxy'] = self.noProxy + if self.socksProxy: + proxy_caps['socksProxy'] = self.socksProxy + if self.socksUsername: + proxy_caps['socksUsername'] = self.socksUsername + if self.socksPassword: + proxy_caps['socksPassword'] = self.socksPassword + capabilities['proxy'] = proxy_caps diff --git a/youtube_dl/selenium/webdriver/common/service.py b/youtube_dl/selenium/webdriver/common/service.py new file mode 100644 index 000000000..af04a9f28 --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/service.py @@ -0,0 +1,178 @@ +# 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. + +import errno +import os +import platform +import subprocess +from subprocess import PIPE +import time +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common import utils + +try: + from subprocess import DEVNULL + _HAS_NATIVE_DEVNULL = True +except ImportError: + DEVNULL = -3 + _HAS_NATIVE_DEVNULL = False + + +class Service(object): + + def __init__(self, executable, port=0, log_file=DEVNULL, env=None, start_error_message=""): + self.path = executable + + self.port = port + if self.port == 0: + self.port = utils.free_port() + + if not _HAS_NATIVE_DEVNULL and log_file == DEVNULL: + log_file = open(os.devnull, 'wb') + + self.start_error_message = start_error_message + self.log_file = log_file + self.env = env or os.environ + + @property + def service_url(self): + """ + Gets the url of the Service + """ + return "http://%s" % utils.join_host_port('localhost', self.port) + + def command_line_args(self): + raise NotImplemented("This method needs to be implemented in a sub class") + + def start(self): + """ + Starts the Service. + + :Exceptions: + - WebDriverException : Raised either when it can't start the service + or when it can't connect to the service + """ + try: + cmd = [self.path] + cmd.extend(self.command_line_args()) + self.process = subprocess.Popen(cmd, env=self.env, + close_fds=platform.system() != 'Windows', + stdout=self.log_file, + stderr=self.log_file, + stdin=PIPE) + except TypeError: + raise + except OSError as err: + if err.errno == errno.ENOENT: + raise WebDriverException( + "'%s' executable needs to be in PATH. %s" % ( + os.path.basename(self.path), self.start_error_message) + ) + elif err.errno == errno.EACCES: + raise WebDriverException( + "'%s' executable may have wrong permissions. %s" % ( + os.path.basename(self.path), self.start_error_message) + ) + else: + raise + except Exception as e: + raise WebDriverException( + "The executable %s needs to be available in the path. %s\n%s" % + (os.path.basename(self.path), self.start_error_message, str(e))) + count = 0 + while True: + self.assert_process_still_running() + if self.is_connectable(): + break + count += 1 + time.sleep(1) + if count == 30: + raise WebDriverException("Can not connect to the Service %s" % self.path) + + def assert_process_still_running(self): + return_code = self.process.poll() + if return_code is not None: + raise WebDriverException( + 'Service %s unexpectedly exited. Status code was: %s' + % (self.path, return_code) + ) + + def is_connectable(self): + return utils.is_connectable(self.port) + + def send_remote_shutdown_command(self): + try: + from urllib import request as url_request + URLError = url_request.URLError + except ImportError: + import urllib2 as url_request + import urllib2 + URLError = urllib2.URLError + + try: + url_request.urlopen("%s/shutdown" % self.service_url) + except URLError: + return + + for x in range(30): + if not self.is_connectable(): + break + else: + time.sleep(1) + + def stop(self): + """ + Stops the service. + """ + if self.log_file != PIPE and not (self.log_file == DEVNULL and _HAS_NATIVE_DEVNULL): + try: + self.log_file.close() + except Exception: + pass + + if self.process is None: + return + + try: + self.send_remote_shutdown_command() + except TypeError: + pass + + try: + if self.process: + for stream in [self.process.stdin, + self.process.stdout, + self.process.stderr]: + try: + stream.close() + except AttributeError: + pass + self.process.terminate() + self.process.wait() + self.process.kill() + self.process = None + except OSError: + pass + + def __del__(self): + # `subprocess.Popen` doesn't send signal on `__del__`; + # so we attempt to close the launched process when `__del__` + # is triggered. + try: + self.stop() + except Exception: + pass diff --git a/youtube_dl/selenium/webdriver/common/touch_actions.py b/youtube_dl/selenium/webdriver/common/touch_actions.py new file mode 100644 index 000000000..89b25290e --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/touch_actions.py @@ -0,0 +1,192 @@ +# 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. + +""" +The Touch Actions implementation +""" + +from selenium.webdriver.remote.command import Command + + +class TouchActions(object): + """ + Generate touch actions. Works like ActionChains; actions are stored in the + TouchActions object and are fired with perform(). + """ + + def __init__(self, driver): + """ + Creates a new TouchActions object. + + :Args: + - driver: The WebDriver instance which performs user actions. + It should be with touchscreen enabled. + """ + self._driver = driver + self._actions = [] + + def perform(self): + """ + Performs all stored actions. + """ + for action in self._actions: + action() + + def tap(self, on_element): + """ + Taps on a given element. + + :Args: + - on_element: The element to tap. + """ + self._actions.append(lambda: self._driver.execute( + Command.SINGLE_TAP, {'element': on_element.id})) + return self + + def double_tap(self, on_element): + """ + Double taps on a given element. + + :Args: + - on_element: The element to tap. + """ + self._actions.append(lambda: self._driver.execute( + Command.DOUBLE_TAP, {'element': on_element.id})) + return self + + def tap_and_hold(self, xcoord, ycoord): + """ + Touch down at given coordinates. + + :Args: + - xcoord: X Coordinate to touch down. + - ycoord: Y Coordinate to touch down. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_DOWN, { + 'x': int(xcoord), + 'y': int(ycoord)})) + return self + + def move(self, xcoord, ycoord): + """ + Move held tap to specified location. + + :Args: + - xcoord: X Coordinate to move. + - ycoord: Y Coordinate to move. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_MOVE, { + 'x': int(xcoord), + 'y': int(ycoord)})) + return self + + def release(self, xcoord, ycoord): + """ + Release previously issued tap 'and hold' command at specified location. + + :Args: + - xcoord: X Coordinate to release. + - ycoord: Y Coordinate to release. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_UP, { + 'x': int(xcoord), + 'y': int(ycoord)})) + return self + + def scroll(self, xoffset, yoffset): + """ + Touch and scroll, moving by xoffset and yoffset. + + :Args: + - xoffset: X offset to scroll to. + - yoffset: Y offset to scroll to. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_SCROLL, { + 'xoffset': int(xoffset), + 'yoffset': int(yoffset)})) + return self + + def scroll_from_element(self, on_element, xoffset, yoffset): + """ + Touch and scroll starting at on_element, moving by xoffset and yoffset. + + :Args: + - on_element: The element where scroll starts. + - xoffset: X offset to scroll to. + - yoffset: Y offset to scroll to. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_SCROLL, { + 'element': on_element.id, + 'xoffset': int(xoffset), + 'yoffset': int(yoffset)})) + return self + + def long_press(self, on_element): + """ + Long press on an element. + + :Args: + - on_element: The element to long press. + """ + self._actions.append(lambda: self._driver.execute( + Command.LONG_PRESS, {'element': on_element.id})) + return self + + def flick(self, xspeed, yspeed): + """ + Flicks, starting anywhere on the screen. + + :Args: + - xspeed: The X speed in pixels per second. + - yspeed: The Y speed in pixels per second. + """ + self._actions.append(lambda: self._driver.execute( + Command.FLICK, { + 'xspeed': int(xspeed), + 'yspeed': int(yspeed)})) + return self + + def flick_element(self, on_element, xoffset, yoffset, speed): + """ + Flick starting at on_element, and moving by the xoffset and yoffset + with specified speed. + + :Args: + - on_element: Flick will start at center of element. + - xoffset: X offset to flick to. + - yoffset: Y offset to flick to. + - speed: Pixels per second to flick. + """ + self._actions.append(lambda: self._driver.execute( + Command.FLICK, { + 'element': on_element.id, + 'xoffset': int(xoffset), + 'yoffset': int(yoffset), + 'speed': int(speed)})) + return self + + # Context manager so TouchActions can be used in a 'with .. as' statements. + def __enter__(self): + return self # Return created instance of self. + + def __exit__(self, _type, _value, _traceback): + pass # Do nothing, does not require additional cleanup. diff --git a/youtube_dl/selenium/webdriver/common/utils.py b/youtube_dl/selenium/webdriver/common/utils.py new file mode 100644 index 000000000..3b443da2a --- /dev/null +++ b/youtube_dl/selenium/webdriver/common/utils.py @@ -0,0 +1,152 @@ +# 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. + +""" +The Utils methods. +""" +import socket +from selenium.webdriver.common.keys import Keys + +try: + basestring +except NameError: + # Python 3 + basestring = str + + +def free_port(): + """ + Determines a free port using sockets. + """ + free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + free_socket.bind(('0.0.0.0', 0)) + free_socket.listen(5) + port = free_socket.getsockname()[1] + free_socket.close() + return port + + +def find_connectable_ip(host, port=None): + """Resolve a hostname to an IP, preferring IPv4 addresses. + + We prefer IPv4 so that we don't change behavior from previous IPv4-only + implementations, and because some drivers (e.g., FirefoxDriver) do not + support IPv6 connections. + + If the optional port number is provided, only IPs that listen on the given + port are considered. + + :Args: + - host - A hostname. + - port - Optional port number. + + :Returns: + A single IP address, as a string. If any IPv4 address is found, one is + returned. Otherwise, if any IPv6 address is found, one is returned. If + neither, then None is returned. + + """ + try: + addrinfos = socket.getaddrinfo(host, None) + except socket.gaierror: + return None + + ip = None + for family, _, _, _, sockaddr in addrinfos: + connectable = True + if port: + connectable = is_connectable(port, sockaddr[0]) + + if connectable and family == socket.AF_INET: + return sockaddr[0] + if connectable and not ip and family == socket.AF_INET6: + ip = sockaddr[0] + return ip + + +def join_host_port(host, port): + """Joins a hostname and port together. + + This is a minimal implementation intended to cope with IPv6 literals. For + example, _join_host_port('::1', 80) == '[::1]:80'. + + :Args: + - host - A hostname. + - port - An integer port. + + """ + if ':' in host and not host.startswith('['): + return '[%s]:%d' % (host, port) + return '%s:%d' % (host, port) + + +def is_connectable(port, host="localhost"): + """ + Tries to connect to the server at port to see if it is running. + + :Args: + - port - The port to connect. + """ + socket_ = None + try: + socket_ = socket.create_connection((host, port), 1) + result = True + except socket.error: + result = False + finally: + if socket_: + socket_.close() + return result + + +def is_url_connectable(port): + """ + Tries to connect to the HTTP server at /status path + and specified port to see if it responds successfully. + + :Args: + - port - The port to connect. + """ + try: + from urllib import request as url_request + except ImportError: + import urllib2 as url_request + + try: + res = url_request.urlopen("http://127.0.0.1:%s/status" % port) + if res.getcode() == 200: + return True + else: + return False + except Exception: + return False + + +def keys_to_typing(value): + """Processes the values that will be typed in the element.""" + typing = [] + for val in value: + if isinstance(val, Keys): + typing.append(val) + elif isinstance(val, int): + val = str(val) + for i in range(len(val)): + typing.append(val[i]) + else: + for i in range(len(val)): + typing.append(val[i]) + return typing diff --git a/youtube_dl/selenium/webdriver/edge/__init__.py b/youtube_dl/selenium/webdriver/edge/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/edge/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/edge/options.py b/youtube_dl/selenium/webdriver/edge/options.py new file mode 100644 index 000000000..24052a8c5 --- /dev/null +++ b/youtube_dl/selenium/webdriver/edge/options.py @@ -0,0 +1,45 @@ +# 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 selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +class Options(object): + + def __init__(self): + self._page_load_strategy = "normal" + + @property + def page_load_strategy(self): + return self._page_load_strategy + + @page_load_strategy.setter + def page_load_strategy(self, value): + if value not in ['normal', 'eager', 'none']: + raise ValueError("Page Load Strategy should be 'normal', 'eager' or 'none'.") + self._page_load_strategy = value + + def to_capabilities(self): + """ + Creates a capabilities with all the options that have been set and + + returns a dictionary with everything + """ + edge = DesiredCapabilities.EDGE.copy() + edge['pageLoadStrategy'] = self._page_load_strategy + + return edge diff --git a/youtube_dl/selenium/webdriver/edge/service.py b/youtube_dl/selenium/webdriver/edge/service.py new file mode 100644 index 000000000..9eac51171 --- /dev/null +++ b/youtube_dl/selenium/webdriver/edge/service.py @@ -0,0 +1,57 @@ +# 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 selenium.webdriver.common import service + + +class Service(service.Service): + + def __init__(self, executable_path, port=0, verbose=False, log_path=None): + """ + Creates a new instance of the EdgeDriver service. + + EdgeDriver provides an interface for Microsoft WebDriver to use + with Microsoft Edge. + + :param executable_path: Path to the Microsoft WebDriver binary. + :param port: Run the remote service on a specified port. + Defaults to 0, which binds to a random open port of the + system's choosing. + :verbose: Whether to make the webdriver more verbose (passes the + --verbose option to the binary). Defaults to False. + :param log_path: Optional path for the webdriver binary to log to. + Defaults to None which disables logging. + + """ + + self.service_args = [] + if verbose: + self.service_args.append("--verbose") + + params = { + "executable": executable_path, + "port": port, + "start_error_message": "Please download from http://go.microsoft.com/fwlink/?LinkId=619687" + } + + if log_path: + params["log_file"] = open(log_path, "a+") + + service.Service.__init__(self, **params) + + def command_line_args(self): + return ["--port=%d" % self.port] + self.service_args diff --git a/youtube_dl/selenium/webdriver/edge/webdriver.py b/youtube_dl/selenium/webdriver/edge/webdriver.py new file mode 100644 index 000000000..e78b14e02 --- /dev/null +++ b/youtube_dl/selenium/webdriver/edge/webdriver.py @@ -0,0 +1,48 @@ +# 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 selenium.webdriver.common import utils +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from selenium.webdriver.remote.remote_connection import RemoteConnection +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from .service import Service + + +class WebDriver(RemoteWebDriver): + + def __init__(self, executable_path='MicrosoftWebDriver.exe', + capabilities=None, port=0, verbose=False, log_path=None): + self.port = port + if self.port == 0: + self.port = utils.free_port() + + self.edge_service = Service(executable_path, port=self.port, verbose=verbose, log_path=log_path) + self.edge_service.start() + + if capabilities is None: + capabilities = DesiredCapabilities.EDGE + + RemoteWebDriver.__init__( + self, + command_executor=RemoteConnection('http://localhost:%d' % self.port, + resolve_ip=False), + desired_capabilities=capabilities) + self._is_remote = False + + def quit(self): + RemoteWebDriver.quit(self) + self.edge_service.stop() diff --git a/youtube_dl/selenium/webdriver/firefox/__init__.py b/youtube_dl/selenium/webdriver/firefox/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/firefox/amd64/x_ignore_nofocus.so b/youtube_dl/selenium/webdriver/firefox/amd64/x_ignore_nofocus.so new file mode 100755 index 000000000..916e530f3 Binary files /dev/null and b/youtube_dl/selenium/webdriver/firefox/amd64/x_ignore_nofocus.so differ diff --git a/youtube_dl/selenium/webdriver/firefox/extension_connection.py b/youtube_dl/selenium/webdriver/firefox/extension_connection.py new file mode 100644 index 000000000..ca715108d --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/extension_connection.py @@ -0,0 +1,84 @@ +# 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. + +import logging +import time + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common import utils +from selenium.webdriver.remote.command import Command +from selenium.webdriver.remote.remote_connection import RemoteConnection +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary + +LOGGER = logging.getLogger(__name__) +PORT = 0 +HOST = None +_URL = "" + + +class ExtensionConnection(RemoteConnection): + def __init__(self, host, firefox_profile, firefox_binary=None, timeout=30): + self.profile = firefox_profile + self.binary = firefox_binary + HOST = host + timeout = int(timeout) + + if self.binary is None: + self.binary = FirefoxBinary() + + if HOST is None: + HOST = "127.0.0.1" + + PORT = utils.free_port() + self.profile.port = PORT + self.profile.update_preferences() + + self.profile.add_extension() + + self.binary.launch_browser(self.profile, timeout=timeout) + _URL = "http://%s:%d/hub" % (HOST, PORT) + RemoteConnection.__init__( + self, _URL, keep_alive=True) + + def quit(self, sessionId=None): + self.execute(Command.QUIT, {'sessionId': sessionId}) + while self.is_connectable(): + LOGGER.info("waiting to quit") + time.sleep(1) + + def connect(self): + """Connects to the extension and retrieves the session id.""" + return self.execute(Command.NEW_SESSION, + {'desiredCapabilities': DesiredCapabilities.FIREFOX}) + + @classmethod + def connect_and_quit(self): + """Connects to an running browser and quit immediately.""" + self._request('%s/extensions/firefox/quit' % _URL) + + @classmethod + def is_connectable(self): + """Trys to connect to the extension but do not retrieve context.""" + utils.is_connectable(self.profile.port) + + +class ExtensionConnectionError(Exception): + """An internal error occurred int the extension. + + Might be caused by bad input or bugs in webdriver + """ + pass diff --git a/youtube_dl/selenium/webdriver/firefox/firefox_binary.py b/youtube_dl/selenium/webdriver/firefox/firefox_binary.py new file mode 100644 index 000000000..f619f1e1a --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/firefox_binary.py @@ -0,0 +1,217 @@ +# 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. + + +import os +import platform +from subprocess import Popen, STDOUT +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common import utils +import time + + +class FirefoxBinary(object): + + NO_FOCUS_LIBRARY_NAME = "x_ignore_nofocus.so" + + def __init__(self, firefox_path=None, log_file=None): + """ + Creates a new instance of Firefox binary. + + :Args: + - firefox_path - Path to the Firefox executable. By default, it will be detected from the standard locations. + - log_file - A file object to redirect the firefox process output to. It can be sys.stdout. + Please note that with parallel run the output won't be synchronous. + By default, it will be redirected to /dev/null. + """ + self._start_cmd = firefox_path + # We used to default to subprocess.PIPE instead of /dev/null, but after + # a while the pipe would fill up and Firefox would freeze. + self._log_file = log_file or open(os.devnull, "wb") + self.command_line = None + if self._start_cmd is None: + self._start_cmd = self._get_firefox_start_cmd() + if not self._start_cmd.strip(): + raise WebDriverException( + "Failed to find firefox binary. You can set it by specifying " + "the path to 'firefox_binary':\n\nfrom " + "selenium.webdriver.firefox.firefox_binary import " + "FirefoxBinary\n\nbinary = " + "FirefoxBinary('/path/to/binary')\ndriver = " + "webdriver.Firefox(firefox_binary=binary)") + # Rather than modifying the environment of the calling Python process + # copy it and modify as needed. + self._firefox_env = os.environ.copy() + self._firefox_env["MOZ_CRASHREPORTER_DISABLE"] = "1" + self._firefox_env["MOZ_NO_REMOTE"] = "1" + self._firefox_env["NO_EM_RESTART"] = "1" + + def add_command_line_options(self, *args): + self.command_line = args + + def launch_browser(self, profile, timeout=30): + """Launches the browser for the given profile name. + It is assumed the profile already exists. + """ + self.profile = profile + + self._start_from_profile_path(self.profile.path) + self._wait_until_connectable(timeout=timeout) + + def kill(self): + """Kill the browser. + + This is useful when the browser is stuck. + """ + if self.process: + self.process.kill() + self.process.wait() + + def _start_from_profile_path(self, path): + self._firefox_env["XRE_PROFILE_PATH"] = path + + if platform.system().lower() == 'linux': + self._modify_link_library_path() + command = [self._start_cmd, "-foreground"] + if self.command_line is not None: + for cli in self.command_line: + command.append(cli) + self.process = Popen( + command, stdout=self._log_file, stderr=STDOUT, + env=self._firefox_env) + + def _wait_until_connectable(self, timeout=30): + """Blocks until the extension is connectable in the firefox.""" + count = 0 + while not utils.is_connectable(self.profile.port): + if self.process.poll() is not None: + # Browser has exited + raise WebDriverException( + "The browser appears to have exited " + "before we could connect. If you specified a log_file in " + "the FirefoxBinary constructor, check it for details.") + if count >= timeout: + self.kill() + raise WebDriverException( + "Can't load the profile. Possible firefox version mismatch. " + "You must use GeckoDriver instead for Firefox 48+. Profile " + "Dir: %s If you specified a log_file in the " + "FirefoxBinary constructor, check it for details." + % (self.profile.path)) + count += 1 + time.sleep(1) + return True + + def _find_exe_in_registry(self): + try: + from _winreg import OpenKey, QueryValue, HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER + except ImportError: + from winreg import OpenKey, QueryValue, HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER + import shlex + keys = (r"SOFTWARE\Classes\FirefoxHTML\shell\open\command", + r"SOFTWARE\Classes\Applications\firefox.exe\shell\open\command") + command = "" + for path in keys: + try: + key = OpenKey(HKEY_LOCAL_MACHINE, path) + command = QueryValue(key, "") + break + except OSError: + try: + key = OpenKey(HKEY_CURRENT_USER, path) + command = QueryValue(key, "") + break + except OSError: + pass + else: + return "" + + if not command: + return "" + + return shlex.split(command)[0] + + def _get_firefox_start_cmd(self): + """Return the command to start firefox.""" + start_cmd = "" + if platform.system() == "Darwin": + start_cmd = "/Applications/Firefox.app/Contents/MacOS/firefox-bin" + # fallback to homebrew installation for mac users + if not os.path.exists(start_cmd): + start_cmd = os.path.expanduser("~") + start_cmd + elif platform.system() == "Windows": + start_cmd = (self._find_exe_in_registry() or self._default_windows_location()) + elif platform.system() == 'Java' and os._name == 'nt': + start_cmd = self._default_windows_location() + else: + for ffname in ["firefox", "iceweasel"]: + start_cmd = self.which(ffname) + if start_cmd is not None: + break + else: + # couldn't find firefox on the system path + raise RuntimeError( + "Could not find firefox in your system PATH." + + " Please specify the firefox binary location or install firefox") + return start_cmd + + def _default_windows_location(self): + program_files = [os.getenv("PROGRAMFILES", r"C:\Program Files"), + os.getenv("PROGRAMFILES(X86)", r"C:\Program Files (x86)")] + for path in program_files: + binary_path = os.path.join(path, r"Mozilla Firefox\firefox.exe") + if os.access(binary_path, os.X_OK): + return binary_path + return "" + + def _modify_link_library_path(self): + existing_ld_lib_path = os.environ.get('LD_LIBRARY_PATH', '') + + new_ld_lib_path = self._extract_and_check( + self.profile, self.NO_FOCUS_LIBRARY_NAME, "x86", "amd64") + + new_ld_lib_path += existing_ld_lib_path + + self._firefox_env["LD_LIBRARY_PATH"] = new_ld_lib_path + self._firefox_env['LD_PRELOAD'] = self.NO_FOCUS_LIBRARY_NAME + + def _extract_and_check(self, profile, no_focus_so_name, x86, amd64): + + paths = [x86, amd64] + built_path = "" + for path in paths: + library_path = os.path.join(profile.path, path) + if not os.path.exists(library_path): + os.makedirs(library_path) + import shutil + shutil.copy(os.path.join( + os.path.dirname(__file__), + path, + self.NO_FOCUS_LIBRARY_NAME), + library_path) + built_path += library_path + ":" + + return built_path + + def which(self, fname): + """Returns the fully qualified path by searching Path of the given + name""" + for pe in os.environ['PATH'].split(os.pathsep): + checkname = os.path.join(pe, fname) + if os.access(checkname, os.X_OK) and not os.path.isdir(checkname): + return checkname + return None diff --git a/youtube_dl/selenium/webdriver/firefox/firefox_profile.py b/youtube_dl/selenium/webdriver/firefox/firefox_profile.py new file mode 100644 index 000000000..849291783 --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/firefox_profile.py @@ -0,0 +1,384 @@ +# 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 __future__ import with_statement + +import base64 +import copy +import json +import os +import re +import shutil +import sys +import tempfile +import zipfile + +try: + from cStringIO import StringIO as BytesIO +except ImportError: + from io import BytesIO + +from xml.dom import minidom +from selenium.webdriver.common.proxy import ProxyType +from selenium.common.exceptions import WebDriverException + + +WEBDRIVER_EXT = "webdriver.xpi" +WEBDRIVER_PREFERENCES = "webdriver_prefs.json" +EXTENSION_NAME = "fxdriver@googlecode.com" + + +class AddonFormatError(Exception): + """Exception for not well-formed add-on manifest files""" + + +class FirefoxProfile(object): + ANONYMOUS_PROFILE_NAME = "WEBDRIVER_ANONYMOUS_PROFILE" + DEFAULT_PREFERENCES = None + + def __init__(self, profile_directory=None): + """ + Initialises a new instance of a Firefox Profile + + :args: + - profile_directory: Directory of profile that you want to use. + This defaults to None and will create a new + directory when object is created. + """ + if not FirefoxProfile.DEFAULT_PREFERENCES: + with open(os.path.join(os.path.dirname(__file__), + WEBDRIVER_PREFERENCES)) as default_prefs: + FirefoxProfile.DEFAULT_PREFERENCES = json.load(default_prefs) + + self.default_preferences = copy.deepcopy( + FirefoxProfile.DEFAULT_PREFERENCES['mutable']) + self.native_events_enabled = True + self.profile_dir = profile_directory + self.tempfolder = None + if self.profile_dir is None: + self.profile_dir = self._create_tempfolder() + else: + self.tempfolder = tempfile.mkdtemp() + newprof = os.path.join(self.tempfolder, "webdriver-py-profilecopy") + shutil.copytree(self.profile_dir, newprof, + ignore=shutil.ignore_patterns("parent.lock", "lock", ".parentlock")) + self.profile_dir = newprof + os.chmod(self.profile_dir, 0o755) + self._read_existing_userjs(os.path.join(self.profile_dir, "user.js")) + self.extensionsDir = os.path.join(self.profile_dir, "extensions") + self.userPrefs = os.path.join(self.profile_dir, "user.js") + if os.path.isfile(self.userPrefs): + os.chmod(self.userPrefs, 0o644) + + # Public Methods + def set_preference(self, key, value): + """ + sets the preference that we want in the profile. + """ + self.default_preferences[key] = value + + def add_extension(self, extension=WEBDRIVER_EXT): + self._install_extension(extension) + + def update_preferences(self): + for key, value in FirefoxProfile.DEFAULT_PREFERENCES['frozen'].items(): + self.default_preferences[key] = value + self._write_user_prefs(self.default_preferences) + + # Properties + + @property + def path(self): + """ + Gets the profile directory that is currently being used + """ + return self.profile_dir + + @property + def port(self): + """ + Gets the port that WebDriver is working on + """ + return self._port + + @port.setter + def port(self, port): + """ + Sets the port that WebDriver will be running on + """ + if not isinstance(port, int): + raise WebDriverException("Port needs to be an integer") + try: + port = int(port) + if port < 1 or port > 65535: + raise WebDriverException("Port number must be in the range 1..65535") + except (ValueError, TypeError): + raise WebDriverException("Port needs to be an integer") + self._port = port + self.set_preference("webdriver_firefox_port", self._port) + + @property + def accept_untrusted_certs(self): + return self.default_preferences["webdriver_accept_untrusted_certs"] + + @accept_untrusted_certs.setter + def accept_untrusted_certs(self, value): + if value not in [True, False]: + raise WebDriverException("Please pass in a Boolean to this call") + self.set_preference("webdriver_accept_untrusted_certs", value) + + @property + def assume_untrusted_cert_issuer(self): + return self.default_preferences["webdriver_assume_untrusted_issuer"] + + @assume_untrusted_cert_issuer.setter + def assume_untrusted_cert_issuer(self, value): + if value not in [True, False]: + raise WebDriverException("Please pass in a Boolean to this call") + + self.set_preference("webdriver_assume_untrusted_issuer", value) + + @property + def native_events_enabled(self): + return self.default_preferences['webdriver_enable_native_events'] + + @native_events_enabled.setter + def native_events_enabled(self, value): + if value not in [True, False]: + raise WebDriverException("Please pass in a Boolean to this call") + self.set_preference("webdriver_enable_native_events", value) + + @property + def encoded(self): + """ + A zipped, base64 encoded string of profile directory + for use with remote WebDriver JSON wire protocol + """ + self.update_preferences() + fp = BytesIO() + zipped = zipfile.ZipFile(fp, 'w', zipfile.ZIP_DEFLATED) + path_root = len(self.path) + 1 # account for trailing slash + for base, dirs, files in os.walk(self.path): + for fyle in files: + filename = os.path.join(base, fyle) + zipped.write(filename, filename[path_root:]) + zipped.close() + return base64.b64encode(fp.getvalue()).decode('UTF-8') + + def set_proxy(self, proxy): + import warnings + + warnings.warn( + "This method has been deprecated. Please pass in the proxy object to the Driver Object", + DeprecationWarning) + if proxy is None: + raise ValueError("proxy can not be None") + + if proxy.proxy_type is ProxyType.UNSPECIFIED: + return + + self.set_preference("network.proxy.type", proxy.proxy_type['ff_value']) + + if proxy.proxy_type is ProxyType.MANUAL: + self.set_preference("network.proxy.no_proxies_on", proxy.no_proxy) + self._set_manual_proxy_preference("ftp", proxy.ftp_proxy) + self._set_manual_proxy_preference("http", proxy.http_proxy) + self._set_manual_proxy_preference("ssl", proxy.ssl_proxy) + self._set_manual_proxy_preference("socks", proxy.socks_proxy) + elif proxy.proxy_type is ProxyType.PAC: + self.set_preference("network.proxy.autoconfig_url", proxy.proxy_autoconfig_url) + + def _set_manual_proxy_preference(self, key, setting): + if setting is None or setting is '': + return + + host_details = setting.split(":") + self.set_preference("network.proxy.%s" % key, host_details[0]) + if len(host_details) > 1: + self.set_preference("network.proxy.%s_port" % key, int(host_details[1])) + + def _create_tempfolder(self): + """ + Creates a temp folder to store User.js and the extension + """ + return tempfile.mkdtemp() + + def _write_user_prefs(self, user_prefs): + """ + writes the current user prefs dictionary to disk + """ + with open(self.userPrefs, "w") as f: + for key, value in user_prefs.items(): + f.write('user_pref("%s", %s);\n' % (key, json.dumps(value))) + + def _read_existing_userjs(self, userjs): + import warnings + + PREF_RE = re.compile(r'user_pref\("(.*)",\s(.*)\)') + try: + with open(userjs) as f: + for usr in f: + matches = re.search(PREF_RE, usr) + try: + self.default_preferences[matches.group(1)] = json.loads(matches.group(2)) + except Exception: + warnings.warn("(skipping) failed to json.loads existing preference: " + + matches.group(1) + matches.group(2)) + except Exception: + # The profile given hasn't had any changes made, i.e no users.js + pass + + def _install_extension(self, addon, unpack=True): + """ + Installs addon from a filepath, url + or directory of addons in the profile. + - path: url, absolute path to .xpi, or directory of addons + - unpack: whether to unpack unless specified otherwise in the install.rdf + """ + if addon == WEBDRIVER_EXT: + addon = os.path.join(os.path.dirname(__file__), WEBDRIVER_EXT) + + tmpdir = None + xpifile = None + if addon.endswith('.xpi'): + tmpdir = tempfile.mkdtemp(suffix='.' + os.path.split(addon)[-1]) + compressed_file = zipfile.ZipFile(addon, 'r') + for name in compressed_file.namelist(): + if name.endswith('/'): + if not os.path.isdir(os.path.join(tmpdir, name)): + os.makedirs(os.path.join(tmpdir, name)) + else: + if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))): + os.makedirs(os.path.dirname(os.path.join(tmpdir, name))) + data = compressed_file.read(name) + with open(os.path.join(tmpdir, name), 'wb') as f: + f.write(data) + xpifile = addon + addon = tmpdir + + # determine the addon id + addon_details = self._addon_details(addon) + addon_id = addon_details.get('id') + assert addon_id, 'The addon id could not be found: %s' % addon + + # copy the addon to the profile + addon_path = os.path.join(self.extensionsDir, addon_id) + if not unpack and not addon_details['unpack'] and xpifile: + if not os.path.exists(self.extensionsDir): + os.makedirs(self.extensionsDir) + os.chmod(self.extensionsDir, 0o755) + shutil.copy(xpifile, addon_path + '.xpi') + else: + if not os.path.exists(addon_path): + shutil.copytree(addon, addon_path, symlinks=True) + + # remove the temporary directory, if any + if tmpdir: + shutil.rmtree(tmpdir) + + def _addon_details(self, addon_path): + """ + Returns a dictionary of details about the addon. + + :param addon_path: path to the add-on directory or XPI + + Returns:: + + {'id': u'rainbow@colors.org', # id of the addon + 'version': u'1.4', # version of the addon + 'name': u'Rainbow', # name of the addon + 'unpack': False } # whether to unpack the addon + """ + + details = { + 'id': None, + 'unpack': False, + 'name': None, + 'version': None + } + + def get_namespace_id(doc, url): + attributes = doc.documentElement.attributes + namespace = "" + for i in range(attributes.length): + if attributes.item(i).value == url: + if ":" in attributes.item(i).name: + # If the namespace is not the default one remove 'xlmns:' + namespace = attributes.item(i).name.split(':')[1] + ":" + break + return namespace + + def get_text(element): + """Retrieve the text value of a given node""" + rc = [] + for node in element.childNodes: + if node.nodeType == node.TEXT_NODE: + rc.append(node.data) + return ''.join(rc).strip() + + if not os.path.exists(addon_path): + raise IOError('Add-on path does not exist: %s' % addon_path) + + try: + if zipfile.is_zipfile(addon_path): + # Bug 944361 - We cannot use 'with' together with zipFile because + # it will cause an exception thrown in Python 2.6. + try: + compressed_file = zipfile.ZipFile(addon_path, 'r') + manifest = compressed_file.read('install.rdf') + finally: + compressed_file.close() + elif os.path.isdir(addon_path): + with open(os.path.join(addon_path, 'install.rdf'), 'r') as f: + manifest = f.read() + else: + raise IOError('Add-on path is neither an XPI nor a directory: %s' % addon_path) + except (IOError, KeyError) as e: + raise AddonFormatError(str(e), sys.exc_info()[2]) + + try: + doc = minidom.parseString(manifest) + + # Get the namespaces abbreviations + em = get_namespace_id(doc, 'http://www.mozilla.org/2004/em-rdf#') + rdf = get_namespace_id(doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#') + + description = doc.getElementsByTagName(rdf + 'Description').item(0) + if description is None: + description = doc.getElementsByTagName('Description').item(0) + for node in description.childNodes: + # Remove the namespace prefix from the tag for comparison + entry = node.nodeName.replace(em, "") + if entry in details.keys(): + details.update({entry: get_text(node)}) + if details.get('id') is None: + for i in range(description.attributes.length): + attribute = description.attributes.item(i) + if attribute.name == em + 'id': + details.update({'id': attribute.value}) + except Exception as e: + raise AddonFormatError(str(e), sys.exc_info()[2]) + + # turn unpack into a true/false value + if isinstance(details['unpack'], str): + details['unpack'] = details['unpack'].lower() == 'true' + + # If no ID is set, the add-on is invalid + if details.get('id') is None: + raise AddonFormatError('Add-on id could not be found.') + + return details diff --git a/youtube_dl/selenium/webdriver/firefox/options.py b/youtube_dl/selenium/webdriver/firefox/options.py new file mode 100644 index 000000000..e68d027c3 --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/options.py @@ -0,0 +1,160 @@ +# 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 selenium.common.exceptions import InvalidArgumentException +from selenium.webdriver.common.proxy import Proxy +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary +from selenium.webdriver.firefox.firefox_profile import FirefoxProfile + + +class Log(object): + def __init__(self): + self.level = None + + def to_capabilities(self): + if self.level is not None: + return {"log": {"level": self.level}} + return {} + + +class Options(object): + KEY = "moz:firefoxOptions" + + def __init__(self): + self._binary = None + self._preferences = {} + self._profile = None + self._proxy = None + self._arguments = [] + self.log = Log() + + @property + def binary(self): + """Returns the FirefoxBinary instance""" + return self._binary + + @binary.setter + def binary(self, new_binary): + """Sets location of the browser binary, either by string or + ``FirefoxBinary`` instance. + + """ + if not isinstance(new_binary, FirefoxBinary): + new_binary = FirefoxBinary(new_binary) + self._binary = new_binary + + @property + def binary_location(self): + """Returns the location of the binary.""" + return self.binary._start_cmd + + @binary_location.setter # noqa + def binary_location(self, value): + """ Sets the location of the browser binary by string """ + self.binary = value + + @property + def preferences(self): + """Returns a dict of preferences.""" + return self._preferences + + def set_preference(self, name, value): + """Sets a preference.""" + self._preferences[name] = value + + @property + def proxy(self): + """ returns Proxy if set otherwise None.""" + return self._proxy + + @proxy.setter + def proxy(self, value): + if not isinstance(value, Proxy): + raise InvalidArgumentException("Only Proxy objects can be passed in.") + self._proxy = value + + @property + def profile(self): + """Returns the Firefox profile to use.""" + return self._profile + + @profile.setter + def profile(self, new_profile): + """Sets location of the browser profile to use, either by string + or ``FirefoxProfile``. + + """ + if not isinstance(new_profile, FirefoxProfile): + new_profile = FirefoxProfile(new_profile) + self._profile = new_profile + + @property + def arguments(self): + """Returns a list of browser process arguments.""" + return self._arguments + + def add_argument(self, argument): + """Add argument to be used for the browser process.""" + if argument is None: + raise ValueError() + self._arguments.append(argument) + + @property + def headless(self): + """ + Returns whether or not the headless argument is set + """ + return '-headless' in self._arguments + + def set_headless(self, headless=True): + """ + Sets the headless argument + + Args: + headless: boolean value indicating to set the headless option + """ + if headless: + self._arguments.append('-headless') + elif '-headless' in self._arguments: + self._arguments.remove('-headless') + + def to_capabilities(self): + """Marshals the Firefox options to a `moz:firefoxOptions` + object. + + """ + # This intentionally looks at the internal properties + # so if a binary or profile has _not_ been set, + # it will defer to geckodriver to find the system Firefox + # and generate a fresh profile. + opts = {} + + if self._binary is not None: + opts["binary"] = self._binary._start_cmd + if len(self._preferences) > 0: + opts["prefs"] = self._preferences + if self._proxy is not None: + self._proxy.add_to_capabilities(opts) + if self._profile is not None: + opts["profile"] = self._profile.encoded + if len(self._arguments) > 0: + opts["args"] = self._arguments + + opts.update(self.log.to_capabilities()) + + if len(opts) > 0: + return {Options.KEY: opts} + return {} diff --git a/youtube_dl/selenium/webdriver/firefox/remote_connection.py b/youtube_dl/selenium/webdriver/firefox/remote_connection.py new file mode 100644 index 000000000..4e894f158 --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/remote_connection.py @@ -0,0 +1,34 @@ +# 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 selenium.webdriver.remote.remote_connection import RemoteConnection + + +class FirefoxRemoteConnection(RemoteConnection): + def __init__(self, remote_server_addr, keep_alive=True): + RemoteConnection.__init__(self, remote_server_addr, keep_alive) + + self._commands["GET_CONTEXT"] = ('GET', '/session/$sessionId/moz/context') + self._commands["SET_CONTEXT"] = ("POST", "/session/$sessionId/moz/context") + self._commands["ELEMENT_GET_ANONYMOUS_CHILDREN"] = \ + ("POST", "/session/$sessionId/moz/xbl/$id/anonymous_children") + self._commands["ELEMENT_FIND_ANONYMOUS_ELEMENTS_BY_ATTRIBUTE"] = \ + ("POST", "/session/$sessionId/moz/xbl/$id/anonymous_by_attribute") + self._commands["INSTALL_ADDON"] = \ + ("POST", "/session/$sessionId/moz/addon/install") + self._commands["UNINSTALL_ADDON"] = \ + ("POST", "/session/$sessionId/moz/addon/uninstall") diff --git a/youtube_dl/selenium/webdriver/firefox/service.py b/youtube_dl/selenium/webdriver/firefox/service.py new file mode 100644 index 000000000..f762eb7cb --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/service.py @@ -0,0 +1,54 @@ +# 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 selenium.webdriver.common import service + + +class Service(service.Service): + """Object that manages the starting and stopping of the + GeckoDriver.""" + + def __init__(self, executable_path, port=0, service_args=None, + log_path="geckodriver.log", env=None): + """Creates a new instance of the GeckoDriver remote service proxy. + + GeckoDriver provides a HTTP interface speaking the W3C WebDriver + protocol to Marionette. + + :param executable_path: Path to the GeckoDriver binary. + :param port: Run the remote service on a specified port. + Defaults to 0, which binds to a random open port of the + system's choosing. + :param service_args: Optional list of arguments to pass to the + GeckoDriver binary. + :param log_path: Optional path for the GeckoDriver to log to. + Defaults to _geckodriver.log_ in the current working directory. + :param env: Optional dictionary of output variables to expose + in the services' environment. + + """ + log_file = open(log_path, "a+") if log_path is not None and log_path != "" else None + + service.Service.__init__( + self, executable_path, port=port, log_file=log_file, env=env) + self.service_args = service_args or [] + + def command_line_args(self): + return ["--port", "%d" % self.port] + self.service_args + + def send_remote_shutdown_command(self): + pass diff --git a/youtube_dl/selenium/webdriver/firefox/webdriver.py b/youtube_dl/selenium/webdriver/firefox/webdriver.py new file mode 100644 index 000000000..cdb36fc3a --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/webdriver.py @@ -0,0 +1,265 @@ +# 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. +import warnings + +try: + import http.client as http_client +except ImportError: + import httplib as http_client + +try: + basestring +except NameError: # Python 3.x + basestring = str + +import shutil +import socket +import sys +from contextlib import contextmanager + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver + +from .extension_connection import ExtensionConnection +from .firefox_binary import FirefoxBinary +from .firefox_profile import FirefoxProfile +from .options import Options +from .remote_connection import FirefoxRemoteConnection +from .service import Service +from .webelement import FirefoxWebElement + + +class WebDriver(RemoteWebDriver): + + # There is no native event support on Mac + NATIVE_EVENTS_ALLOWED = sys.platform != "darwin" + + CONTEXT_CHROME = "chrome" + CONTEXT_CONTENT = "content" + + _web_element_cls = FirefoxWebElement + + def __init__(self, firefox_profile=None, firefox_binary=None, + timeout=30, capabilities=None, proxy=None, + executable_path="geckodriver", options=None, + log_path="geckodriver.log", firefox_options=None, + service_args=None): + """Starts a new local session of Firefox. + + Based on the combination and specificity of the various keyword + arguments, a capabilities dictionary will be constructed that + is passed to the remote end. + + The keyword arguments given to this constructor are helpers to + more easily allow Firefox WebDriver sessions to be customised + with different options. They are mapped on to a capabilities + dictionary that is passed on to the remote end. + + As some of the options, such as `firefox_profile` and + `options.profile` are mutually exclusive, precedence is + given from how specific the setting is. `capabilities` is the + least specific keyword argument, followed by `options`, + followed by `firefox_binary` and `firefox_profile`. + + In practice this means that if `firefox_profile` and + `options.profile` are both set, the selected profile + instance will always come from the most specific variable. + In this case that would be `firefox_profile`. This will result in + `options.profile` to be ignored because it is considered + a less specific setting than the top-level `firefox_profile` + keyword argument. Similarily, if you had specified a + `capabilities["moz:firefoxOptions"]["profile"]` Base64 string, + this would rank below `options.profile`. + + :param firefox_profile: Instance of ``FirefoxProfile`` object + or a string. If undefined, a fresh profile will be created + in a temporary location on the system. + :param firefox_binary: Instance of ``FirefoxBinary`` or full + path to the Firefox binary. If undefined, the system default + Firefox installation will be used. + :param timeout: Time to wait for Firefox to launch when using + the extension connection. + :param capabilities: Dictionary of desired capabilities. + :param proxy: The proxy settings to us when communicating with + Firefox via the extension connection. + :param executable_path: Full path to override which geckodriver + binary to use for Firefox 47.0.1 and greater, which + defaults to picking up the binary from the system path. + :param options: Instance of ``options.Options``. + :param log_path: Where to log information from the driver. + + """ + if firefox_options: + warnings.warn('use options instead of firefox_options', DeprecationWarning) + options = firefox_options + self.binary = None + self.profile = None + self.service = None + + if capabilities is None: + capabilities = DesiredCapabilities.FIREFOX.copy() + if options is None: + options = Options() + + capabilities = dict(capabilities) + + if capabilities.get("binary"): + self.binary = capabilities["binary"] + + # options overrides capabilities + if options is not None: + if options.binary is not None: + self.binary = options.binary + if options.profile is not None: + self.profile = options.profile + + # firefox_binary and firefox_profile + # override options + if firefox_binary is not None: + if isinstance(firefox_binary, basestring): + firefox_binary = FirefoxBinary(firefox_binary) + self.binary = firefox_binary + options.binary = firefox_binary + if firefox_profile is not None: + if isinstance(firefox_profile, basestring): + firefox_profile = FirefoxProfile(firefox_profile) + self.profile = firefox_profile + options.profile = firefox_profile + + # W3C remote + # TODO(ato): Perform conformance negotiation + + if capabilities.get("marionette"): + capabilities.pop("marionette") + self.service = Service( + executable_path, + service_args=service_args, + log_path=log_path) + self.service.start() + + capabilities.update(options.to_capabilities()) + + executor = FirefoxRemoteConnection( + remote_server_addr=self.service.service_url) + RemoteWebDriver.__init__( + self, + command_executor=executor, + desired_capabilities=capabilities, + keep_alive=True) + + # Selenium remote + else: + if self.binary is None: + self.binary = FirefoxBinary() + if self.profile is None: + self.profile = FirefoxProfile() + + # disable native events if globally disabled + self.profile.native_events_enabled = ( + self.NATIVE_EVENTS_ALLOWED and self.profile.native_events_enabled) + + if proxy is not None: + proxy.add_to_capabilities(capabilities) + + executor = ExtensionConnection("127.0.0.1", self.profile, + self.binary, timeout) + RemoteWebDriver.__init__( + self, + command_executor=executor, + desired_capabilities=capabilities, + keep_alive=True) + + self._is_remote = False + + def quit(self): + """Quits the driver and close every associated window.""" + try: + RemoteWebDriver.quit(self) + except (http_client.BadStatusLine, socket.error): + # Happens if Firefox shutsdown before we've read the response from + # the socket. + pass + + if self.w3c: + self.service.stop() + else: + self.binary.kill() + + if self.profile is not None: + try: + shutil.rmtree(self.profile.path) + if self.profile.tempfolder is not None: + shutil.rmtree(self.profile.tempfolder) + except Exception as e: + print(str(e)) + + @property + def firefox_profile(self): + return self.profile + + # Extension commands: + + def set_context(self, context): + self.execute("SET_CONTEXT", {"context": context}) + + @contextmanager + def context(self, context): + """Sets the context that Selenium commands are running in using + a `with` statement. The state of the context on the server is + saved before entering the block, and restored upon exiting it. + + :param context: Context, may be one of the class properties + `CONTEXT_CHROME` or `CONTEXT_CONTENT`. + + Usage example:: + + with selenium.context(selenium.CONTEXT_CHROME): + # chrome scope + ... do stuff ... + """ + initial_context = self.execute('GET_CONTEXT').pop('value') + self.set_context(context) + try: + yield + finally: + self.set_context(initial_context) + + def install_addon(self, path, temporary=None): + """ + Installs Firefox addon. + + Returns identifier of installed addon. This identifier can later + be used to uninstall addon. + + :param path: Absolute path to the addon that will be installed. + + :Usage: + driver.install_addon('/path/to/firebug.xpi') + """ + payload = {"path": path} + if temporary is not None: + payload["temporary"] = temporary + return self.execute("INSTALL_ADDON", payload)["value"] + + def uninstall_addon(self, identifier): + """ + Uninstalls Firefox addon using its identifier. + + :Usage: + driver.uninstall_addon('addon@foo.com') + """ + self.execute("UNINSTALL_ADDON", {"id": identifier}) diff --git a/youtube_dl/selenium/webdriver/firefox/webdriver.xpi b/youtube_dl/selenium/webdriver/firefox/webdriver.xpi new file mode 100644 index 000000000..3b59c7fbf Binary files /dev/null and b/youtube_dl/selenium/webdriver/firefox/webdriver.xpi differ diff --git a/youtube_dl/selenium/webdriver/firefox/webdriver_prefs.json b/youtube_dl/selenium/webdriver/firefox/webdriver_prefs.json new file mode 100644 index 000000000..0dbe56bbd --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/webdriver_prefs.json @@ -0,0 +1,70 @@ +{ + "frozen": { + "app.update.auto": false, + "app.update.enabled": false, + "browser.displayedE10SNotice": 4, + "browser.download.manager.showWhenStarting": false, + "browser.EULA.override": true, + "browser.EULA.3.accepted": true, + "browser.link.open_external": 2, + "browser.link.open_newwindow": 2, + "browser.offline": false, + "browser.reader.detectedFirstArticle": true, + "browser.safebrowsing.enabled": false, + "browser.safebrowsing.malware.enabled": false, + "browser.search.update": false, + "browser.selfsupport.url" : "", + "browser.sessionstore.resume_from_crash": false, + "browser.shell.checkDefaultBrowser": false, + "browser.tabs.warnOnClose": false, + "browser.tabs.warnOnOpen": false, + "datareporting.healthreport.service.enabled": false, + "datareporting.healthreport.uploadEnabled": false, + "datareporting.healthreport.service.firstRun": false, + "datareporting.healthreport.logging.consoleEnabled": false, + "datareporting.policy.dataSubmissionEnabled": false, + "datareporting.policy.dataSubmissionPolicyAccepted": false, + "devtools.errorconsole.enabled": true, + "dom.disable_open_during_load": false, + "extensions.autoDisableScopes": 10, + "extensions.blocklist.enabled": false, + "extensions.checkCompatibility.nightly": false, + "extensions.logging.enabled": true, + "extensions.update.enabled": false, + "extensions.update.notifyUser": false, + "javascript.enabled": true, + "network.manage-offline-status": false, + "network.http.phishy-userpass-length": 255, + "offline-apps.allow_by_default": true, + "prompts.tab_modal.enabled": false, + "security.fileuri.origin_policy": 3, + "security.fileuri.strict_origin_policy": false, + "signon.rememberSignons": false, + "toolkit.networkmanager.disable": true, + "toolkit.telemetry.prompted": 2, + "toolkit.telemetry.enabled": false, + "toolkit.telemetry.rejected": true, + "xpinstall.signatures.required": false, + "xpinstall.whitelist.required": false + }, + "mutable": { + "browser.dom.window.dump.enabled": true, + "browser.laterrun.enabled": false, + "browser.newtab.url": "about:blank", + "browser.newtabpage.enabled": false, + "browser.startup.page": 0, + "browser.startup.homepage": "about:blank", + "browser.startup.homepage_override.mstone": "ignore", + "browser.usedOnWindows10.introURL": "about:blank", + "dom.max_chrome_script_run_time": 30, + "dom.max_script_run_time": 30, + "dom.report_all_js_exceptions": true, + "javascript.options.showInConsole": true, + "network.captive-portal-service.enabled": false, + "security.csp.enable": false, + "startup.homepage_welcome_url": "about:blank", + "startup.homepage_welcome_url.additional": "about:blank", + "webdriver_accept_untrusted_certs": true, + "webdriver_assume_untrusted_issuer": true + } +} diff --git a/youtube_dl/selenium/webdriver/firefox/webelement.py b/youtube_dl/selenium/webdriver/firefox/webelement.py new file mode 100644 index 000000000..4117ec067 --- /dev/null +++ b/youtube_dl/selenium/webdriver/firefox/webelement.py @@ -0,0 +1,49 @@ +# 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 selenium.webdriver.remote.webelement import WebElement as RemoteWebElement + + +class FirefoxWebElement(RemoteWebElement): + + @property + def anonymous_children(self): + """Retrieve the anonymous children of this element in an XBL + context. This is only available in chrome context. + + See the `anonymous content documentation + `_ + on MDN for more information. + + """ + return self._execute( + "ELEMENT_GET_ANONYMOUS_CHILDREN", + {"value": None}) + + def find_anonymous_element_by_attribute(self, name, value): + """Retrieve an anonymous descendant with a specified attribute + value. Typically used with an (arbitrary) anonid attribute to + retrieve a specific anonymous child in an XBL binding. + + See the `anonymous content documentation + `_ + on MDN for more information. + + """ + return self._execute( + "ELEMENT_FIND_ANONYMOUS_ELEMENTS_BY_ATTRIBUTE", + {"name": name, "value": value})["value"] diff --git a/youtube_dl/selenium/webdriver/firefox/x86/x_ignore_nofocus.so b/youtube_dl/selenium/webdriver/firefox/x86/x_ignore_nofocus.so new file mode 100755 index 000000000..8e7db8de3 Binary files /dev/null and b/youtube_dl/selenium/webdriver/firefox/x86/x_ignore_nofocus.so differ diff --git a/youtube_dl/selenium/webdriver/ie/__init__.py b/youtube_dl/selenium/webdriver/ie/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/ie/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/ie/options.py b/youtube_dl/selenium/webdriver/ie/options.py new file mode 100644 index 000000000..e126b11ce --- /dev/null +++ b/youtube_dl/selenium/webdriver/ie/options.py @@ -0,0 +1,339 @@ +# 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. + + +class ElementScrollBehavior(object): + TOP = 0 + BOTTOM = 1 + + +class Options(object): + + KEY = 'se:ieOptions' + SWITCHES = 'ie.browserCommandLineSwitches' + + BROWSER_ATTACH_TIMEOUT = 'browserAttachTimeout' + ELEMENT_SCROLL_BEHAVIOR = 'elementScrollBehavior' + ENSURE_CLEAN_SESSION = 'ie.ensureCleanSession' + FILE_UPLOAD_DIALOG_TIMEOUT = 'ie.fileUploadDialogTimeout' + FORCE_CREATE_PROCESS_API = 'ie.forceCreateProcessApi' + FORCE_SHELL_WINDOWS_API = 'ie.forceShellWindowsApi' + FULL_PAGE_SCREENSHOT = 'ie.enableFullPageScreenshot' + IGNORE_PROTECTED_MODE_SETTINGS = 'ignoreProtectedModeSettings' + IGNORE_ZOOM_LEVEL = 'ignoreZoomSetting' + INITIAL_BROWSER_URL = 'initialBrowserUrl' + NATIVE_EVENTS = 'nativeEvents' + PERSISTENT_HOVER = 'enablePersistentHover' + REQUIRE_WINDOW_FOCUS = 'requireWindowFocus' + USE_PER_PROCESS_PROXY = 'ie.usePerProcessProxy' + VALIDATE_COOKIE_DOCUMENT_TYPE = 'ie.validateCookieDocumentType' + + def __init__(self): + self._arguments = [] + self._options = {} + self._additional = {} + + @property + def arguments(self): + """ Returns a list of browser process arguments """ + return self._arguments + + def add_argument(self, argument): + """ Add argument to be used for the browser process """ + if argument is None: + raise ValueError() + self._arguments.append(argument) + + @property + def options(self): + """ Returns a dictionary of browser options """ + return self._options + + @property + def browser_attach_timeout(self): + """ Returns the options Browser Attach Timeout in milliseconds """ + return self._options.get(self.BROWSER_ATTACH_TIMEOUT) + + @browser_attach_timeout.setter + def browser_attach_timeout(self, value): + """ + Sets the options Browser Attach Timeout + + :Args: + - value: Timeout in milliseconds + + """ + if not isinstance(value, int): + raise ValueError('Browser Attach Timeout must be an integer.') + self._options[self.BROWSER_ATTACH_TIMEOUT] = value + + @property + def element_scroll_behavior(self): + """ Returns the options Element Scroll Behavior in milliseconds """ + return self._options.get(self.ELEMENT_SCROLL_BEHAVIOR) + + @element_scroll_behavior.setter + def element_scroll_behavior(self, value): + """ + Sets the options Element Scroll Behavior + + :Args: + - value: 0 - Top, 1 - Bottom + + """ + if value not in [ElementScrollBehavior.TOP, ElementScrollBehavior.BOTTOM]: + raise ValueError('Element Scroll Behavior out of range.') + self._options[self.ELEMENT_SCROLL_BEHAVIOR] = value + + @property + def ensure_clean_session(self): + """ Returns the options Ensure Clean Session value """ + return self._options.get(self.ENSURE_CLEAN_SESSION) + + @ensure_clean_session.setter + def ensure_clean_session(self, value): + """ + Sets the options Ensure Clean Session value + + :Args: + - value: boolean value + + """ + self._options[self.ENSURE_CLEAN_SESSION] = value + + @property + def file_upload_dialog_timeout(self): + """ Returns the options File Upload Dialog Timeout in milliseconds """ + return self._options.get(self.FILE_UPLOAD_DIALOG_TIMEOUT) + + @file_upload_dialog_timeout.setter + def file_upload_dialog_timeout(self, value): + """ + Sets the options File Upload Dialog Timeout value + + :Args: + - value: Timeout in milliseconds + + """ + if not isinstance(value, int): + raise ValueError('File Upload Dialog Timeout must be an integer.') + self._options[self.FILE_UPLOAD_DIALOG_TIMEOUT] = value + + @property + def force_create_process_api(self): + """ Returns the options Force Create Process Api value """ + return self._options.get(self.FORCE_CREATE_PROCESS_API) + + @force_create_process_api.setter + def force_create_process_api(self, value): + """ + Sets the options Force Create Process Api value + + :Args: + - value: boolean value + + """ + self._options[self.FORCE_CREATE_PROCESS_API] = value + + @property + def force_shell_windows_api(self): + """ Returns the options Force Shell Windows Api value """ + return self._options.get(self.FORCE_SHELL_WINDOWS_API) + + @force_shell_windows_api.setter + def force_shell_windows_api(self, value): + """ + Sets the options Force Shell Windows Api value + + :Args: + - value: boolean value + + """ + self._options[self.FORCE_SHELL_WINDOWS_API] = value + + @property + def full_page_screenshot(self): + """ Returns the options Full Page Screenshot value """ + return self._options.get(self.FULL_PAGE_SCREENSHOT) + + @full_page_screenshot.setter + def full_page_screenshot(self, value): + """ + Sets the options Full Page Screenshot value + + :Args: + - value: boolean value + + """ + self._options[self.FULL_PAGE_SCREENSHOT] = value + + @property + def ignore_protected_mode_settings(self): + """ Returns the options Ignore Protected Mode Settings value """ + return self._options.get(self.IGNORE_PROTECTED_MODE_SETTINGS) + + @ignore_protected_mode_settings.setter + def ignore_protected_mode_settings(self, value): + """ + Sets the options Ignore Protected Mode Settings value + + :Args: + - value: boolean value + + """ + self._options[self.IGNORE_PROTECTED_MODE_SETTINGS] = value + + @property + def ignore_zoom_level(self): + """ Returns the options Ignore Zoom Level value """ + return self._options.get(self.IGNORE_ZOOM_LEVEL) + + @ignore_zoom_level.setter + def ignore_zoom_level(self, value): + """ + Sets the options Ignore Zoom Level value + + :Args: + - value: boolean value + + """ + self._options[self.IGNORE_ZOOM_LEVEL] = value + + @property + def initial_browser_url(self): + """ Returns the options Initial Browser Url value """ + return self._options.get(self.INITIAL_BROWSER_URL) + + @initial_browser_url.setter + def initial_browser_url(self, value): + """ + Sets the options Initial Browser Url value + + :Args: + - value: URL string + + """ + self._options[self.INITIAL_BROWSER_URL] = value + + @property + def native_events(self): + """ Returns the options Native Events value """ + return self._options.get(self.NATIVE_EVENTS) + + @native_events.setter + def native_events(self, value): + """ + Sets the options Native Events value + + :Args: + - value: boolean value + + """ + self._options[self.NATIVE_EVENTS] = value + + @property + def persistent_hover(self): + """ Returns the options Persistent Hover value """ + return self._options.get(self.PERSISTENT_HOVER) + + @persistent_hover.setter + def persistent_hover(self, value): + """ + Sets the options Persistent Hover value + + :Args: + - value: boolean value + + """ + self._options[self.PERSISTENT_HOVER] = value + + @property + def require_window_focus(self): + """ Returns the options Require Window Focus value """ + return self._options.get(self.REQUIRE_WINDOW_FOCUS) + + @require_window_focus.setter + def require_window_focus(self, value): + """ + Sets the options Require Window Focus value + + :Args: + - value: boolean value + + """ + self._options[self.REQUIRE_WINDOW_FOCUS] = value + + @property + def use_per_process_proxy(self): + """ Returns the options User Per Process Proxy value """ + return self._options.get(self.USE_PER_PROCESS_PROXY) + + @use_per_process_proxy.setter + def use_per_process_proxy(self, value): + """ + Sets the options User Per Process Proxy value + + :Args: + - value: boolean value + + """ + self._options[self.USE_PER_PROCESS_PROXY] = value + + @property + def validate_cookie_document_type(self): + """ Returns the options Validate Cookie Document Type value """ + return self._options.get(self.VALIDATE_COOKIE_DOCUMENT_TYPE) + + @validate_cookie_document_type.setter + def validate_cookie_document_type(self, value): + """ + Sets the options Validate Cookie Document Type value + + :Args: + - value: boolean value + + """ + self._options[self.VALIDATE_COOKIE_DOCUMENT_TYPE] = value + + @property + def additional_options(self): + """ Returns the additional options """ + return self._additional + + def add_additional_option(self, name, value): + """ + Adds an additional option not yet added as a safe option for IE + + :Args: + - name: name of the option to add + - value: value of the option to add + + """ + self._additional[name] = value + + def to_capabilities(self): + """ Marshals the IE options to a the correct object """ + opts = self._options.copy() + if len(self._arguments) > 0: + opts[self.SWITCHES] = ' '.join(self._arguments) + + if len(self._additional) > 0: + opts.update(self._additional) + + if len(opts) > 0: + return {self.KEY: opts} + return {} diff --git a/youtube_dl/selenium/webdriver/ie/service.py b/youtube_dl/selenium/webdriver/ie/service.py new file mode 100644 index 000000000..c9083e169 --- /dev/null +++ b/youtube_dl/selenium/webdriver/ie/service.py @@ -0,0 +1,50 @@ +# 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 selenium.webdriver.common import service + + +class Service(service.Service): + """ + Object that manages the starting and stopping of the IEDriver + """ + + def __init__(self, executable_path, port=0, host=None, log_level=None, log_file=None): + """ + Creates a new instance of the Service + + :Args: + - executable_path : Path to the IEDriver + - port : Port the service is running on + - host : IP address the service port is bound + - log_level : Level of logging of service, may be "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE". + Default is "FATAL". + - log_file : Target of logging of service, may be "stdout", "stderr" or file path. + Default is "stdout".""" + self.service_args = [] + if host is not None: + self.service_args.append("--host=%s" % host) + if log_level is not None: + self.service_args.append("--log-level=%s" % log_level) + if log_file is not None: + self.service_args.append("--log-file=%s" % log_file) + + service.Service.__init__(self, executable_path, port=port, + start_error_message="Please download from http://selenium-release.storage.googleapis.com/index.html and read up at https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver") + + def command_line_args(self): + return ["--port=%d" % self.port] + self.service_args diff --git a/youtube_dl/selenium/webdriver/ie/webdriver.py b/youtube_dl/selenium/webdriver/ie/webdriver.py new file mode 100644 index 000000000..1b9f3915b --- /dev/null +++ b/youtube_dl/selenium/webdriver/ie/webdriver.py @@ -0,0 +1,95 @@ +# 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. +import warnings + +from selenium.webdriver.common import utils +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from .service import Service +from .options import Options + +DEFAULT_TIMEOUT = 30 +DEFAULT_PORT = 0 +DEFAULT_HOST = None +DEFAULT_LOG_LEVEL = None +DEFAULT_LOG_FILE = None + + +class WebDriver(RemoteWebDriver): + """ Controls the IEServerDriver and allows you to drive Internet Explorer """ + + def __init__(self, executable_path='IEDriverServer.exe', capabilities=None, + port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT, host=DEFAULT_HOST, + log_level=DEFAULT_LOG_LEVEL, log_file=DEFAULT_LOG_FILE, options=None, + ie_options=None): + """ + Creates a new instance of the chrome driver. + + Starts the service and then creates new instance of chrome driver. + + :Args: + - executable_path - path to the executable. If the default is used it assumes the executable is in the $PATH + - capabilities: capabilities Dictionary object + - port - port you would like the service to run, if left as 0, a free port will be found. + - log_level - log level you would like the service to run. + - log_file - log file you would like the service to log to. + - options: IE Options instance, providing additional IE options + """ + if ie_options: + warnings.warn('use options instead of ie_options', DeprecationWarning) + options = ie_options + self.port = port + if self.port == 0: + self.port = utils.free_port() + self.host = host + self.log_level = log_level + self.log_file = log_file + + if options is None: + # desired_capabilities stays as passed in + if capabilities is None: + capabilities = self.create_options().to_capabilities() + else: + if capabilities is None: + capabilities = options.to_capabilities() + else: + capabilities.update(options.to_capabilities()) + + self.iedriver = Service( + executable_path, + port=self.port, + host=self.host, + log_level=self.log_level, + log_file=self.log_file) + + self.iedriver.start() + + if capabilities is None: + capabilities = DesiredCapabilities.INTERNETEXPLORER + + RemoteWebDriver.__init__( + self, + command_executor='http://localhost:%d' % self.port, + desired_capabilities=capabilities) + self._is_remote = False + + def quit(self): + RemoteWebDriver.quit(self) + self.iedriver.stop() + + def create_options(self): + return Options() diff --git a/youtube_dl/selenium/webdriver/opera/__init__.py b/youtube_dl/selenium/webdriver/opera/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/opera/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/opera/options.py b/youtube_dl/selenium/webdriver/opera/options.py new file mode 100644 index 000000000..a27fd223b --- /dev/null +++ b/youtube_dl/selenium/webdriver/opera/options.py @@ -0,0 +1,106 @@ +# 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 selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +class Options(ChromeOptions): + KEY = "operaOptions" + + def __init__(self): + ChromeOptions.__init__(self) + self._android_package_name = '' + self._android_device_socket = '' + self._android_command_line_file = '' + + @property + def android_package_name(self): + """ + Returns the name of the Opera package + """ + return self._android_package_name + + @android_package_name.setter + def android_package_name(self, value): + """ + Allows you to set the package name + + :Args: + - value: devtools socket name + """ + self._android_package_name = value + + @property + def android_device_socket(self): + """ + Returns the name of the devtools socket + """ + return self._android_device_socket + + @android_device_socket.setter + def android_device_socket(self, value): + """ + Allows you to set the devtools socket name + + :Args: + - value: devtools socket name + """ + self._android_device_socket = value + + @property + def android_command_line_file(self): + """ + Returns the path of the command line file + """ + return self._android_command_line_file + + @android_command_line_file.setter + def android_command_line_file(self, value): + """ + Allows you to set where the command line file lives + + :Args: + - value: command line file path + """ + self._android_command_line_file = value + + def to_capabilities(self): + """ + Creates a capabilities with all the options that have been set and + + returns a dictionary with everything + """ + capabilities = ChromeOptions.to_capabilities(self) + capabilities.update(DesiredCapabilities.OPERA) + opera_options = capabilities[self.KEY] + + if self.android_package_name: + opera_options["androidPackage"] = self.android_package_name + if self.android_device_socket: + opera_options["androidDeviceSocket"] = self.android_device_socket + if self.android_command_line_file: + opera_options["androidCommandLineFile"] = \ + self.android_command_line_file + return capabilities + + +class AndroidOptions(Options): + + def __init__(self): + Options.__init__(self) + self.android_package_name = 'com.opera.browser' diff --git a/youtube_dl/selenium/webdriver/opera/webdriver.py b/youtube_dl/selenium/webdriver/opera/webdriver.py new file mode 100644 index 000000000..ec2a7dd87 --- /dev/null +++ b/youtube_dl/selenium/webdriver/opera/webdriver.py @@ -0,0 +1,78 @@ +# 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. +import warnings + +from selenium.webdriver.chrome.webdriver import WebDriver as ChromiumDriver +from .options import Options + + +class OperaDriver(ChromiumDriver): + """Controls the new OperaDriver and allows you + to drive the Opera browser based on Chromium.""" + + def __init__(self, executable_path=None, port=0, + options=None, service_args=None, + desired_capabilities=None, service_log_path=None, + opera_options=None): + """ + Creates a new instance of the operadriver. + + Starts the service and then creates new instance of operadriver. + + :Args: + - executable_path - path to the executable. If the default is used + it assumes the executable is in the $PATH + - port - port you would like the service to run, if left as 0, + a free port will be found. + - desired_capabilities: Dictionary object with non-browser specific + capabilities only, such as "proxy" or "loggingPref". + - options: this takes an instance of ChromeOptions + """ + if opera_options: + warnings.warn('use options instead of opera_options', DeprecationWarning) + options = opera_options + + executable_path = (executable_path if executable_path is not None + else "operadriver") + ChromiumDriver.__init__(self, + executable_path=executable_path, + port=port, + options=options, + service_args=service_args, + desired_capabilities=desired_capabilities, + service_log_path=service_log_path) + + def create_options(self): + return Options() + + +class WebDriver(OperaDriver): + class ServiceType: + CHROMIUM = 2 + + def __init__(self, + desired_capabilities=None, + executable_path=None, + port=0, + service_log_path=None, + service_args=None, + options=None): + OperaDriver.__init__(self, executable_path=executable_path, + port=port, options=options, + service_args=service_args, + desired_capabilities=desired_capabilities, + service_log_path=service_log_path) diff --git a/youtube_dl/selenium/webdriver/phantomjs/__init__.py b/youtube_dl/selenium/webdriver/phantomjs/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/phantomjs/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/phantomjs/service.py b/youtube_dl/selenium/webdriver/phantomjs/service.py new file mode 100644 index 000000000..37b4e9f8c --- /dev/null +++ b/youtube_dl/selenium/webdriver/phantomjs/service.py @@ -0,0 +1,68 @@ +# 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. +import os +import tempfile +from selenium.webdriver.common import service + + +class Service(service.Service): + """ + Object that manages the starting and stopping of PhantomJS / Ghostdriver + """ + + def __init__(self, executable_path, port=0, service_args=None, log_path=None): + """ + Creates a new instance of the Service + + :Args: + - executable_path : Path to PhantomJS binary + - port : Port the service is running on + - service_args : A List of other command line options to pass to PhantomJS + - log_path: Path for PhantomJS service to log to + """ + self.service_args = service_args + if self.service_args is None: + self.service_args = [] + else: + self.service_args = service_args[:] + if not log_path: + log_path = "ghostdriver.log" + if not self._args_contain("--cookies-file="): + self._cookie_temp_file_handle, self._cookie_temp_file = tempfile.mkstemp() + self.service_args.append("--cookies-file=" + self._cookie_temp_file) + else: + self._cookie_temp_file = None + + service.Service.__init__(self, executable_path, port=port, log_file=open(log_path, 'w')) + + def _args_contain(self, arg): + return len(list(filter(lambda x: x.startswith(arg), self.service_args))) > 0 + + def command_line_args(self): + return self.service_args + ["--webdriver=%d" % self.port] + + @property + def service_url(self): + """ + Gets the url of the GhostDriver Service + """ + return "http://localhost:%d/wd/hub" % self.port + + def send_remote_shutdown_command(self): + if self._cookie_temp_file: + os.close(self._cookie_temp_file_handle) + os.remove(self._cookie_temp_file) diff --git a/youtube_dl/selenium/webdriver/phantomjs/webdriver.py b/youtube_dl/selenium/webdriver/phantomjs/webdriver.py new file mode 100644 index 000000000..e07024f79 --- /dev/null +++ b/youtube_dl/selenium/webdriver/phantomjs/webdriver.py @@ -0,0 +1,80 @@ +# 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. + +import warnings + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from .service import Service + + +class WebDriver(RemoteWebDriver): + """ + Wrapper to communicate with PhantomJS through Ghostdriver. + + You will need to follow all the directions here: + https://github.com/detro/ghostdriver + """ + + def __init__(self, executable_path="phantomjs", + port=0, desired_capabilities=DesiredCapabilities.PHANTOMJS, + service_args=None, service_log_path=None): + """ + Creates a new instance of the PhantomJS / Ghostdriver. + + Starts the service and then creates new instance of the driver. + + :Args: + - executable_path - path to the executable. If the default is used it assumes the executable is in the $PATH + - port - port you would like the service to run, if left as 0, a free port will be found. + - desired_capabilities: Dictionary object with non-browser specific + capabilities only, such as "proxy" or "loggingPref". + - service_args : A List of command line arguments to pass to PhantomJS + - service_log_path: Path for phantomjs service to log to. + """ + warnings.warn('Selenium support for PhantomJS has been deprecated, please use headless ' + 'versions of Chrome or Firefox instead') + self.service = Service( + executable_path, + port=port, + service_args=service_args, + log_path=service_log_path) + self.service.start() + + try: + RemoteWebDriver.__init__( + self, + command_executor=self.service.service_url, + desired_capabilities=desired_capabilities) + except Exception: + self.quit() + raise + + self._is_remote = False + + def quit(self): + """ + Closes the browser and shuts down the PhantomJS executable + that is started when starting the PhantomJS + """ + try: + RemoteWebDriver.quit(self) + except Exception: + # We don't care about the message because something probably has gone wrong + pass + finally: + self.service.stop() diff --git a/youtube_dl/selenium/webdriver/remote/__init__.py b/youtube_dl/selenium/webdriver/remote/__init__.py new file mode 100644 index 000000000..a5b1e6f85 --- /dev/null +++ b/youtube_dl/selenium/webdriver/remote/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/youtube_dl/selenium/webdriver/remote/command.py b/youtube_dl/selenium/webdriver/remote/command.py new file mode 100644 index 000000000..e2f7edbdb --- /dev/null +++ b/youtube_dl/selenium/webdriver/remote/command.py @@ -0,0 +1,174 @@ +# 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. + + +class Command(object): + """ + Defines constants for the standard WebDriver commands. + + While these constants have no meaning in and of themselves, they are + used to marshal commands through a service that implements WebDriver's + remote wire protocol: + + https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol + + """ + + # Keep in sync with org.openqa.selenium.remote.DriverCommand + + STATUS = "status" + NEW_SESSION = "newSession" + GET_ALL_SESSIONS = "getAllSessions" + DELETE_SESSION = "deleteSession" + CLOSE = "close" + QUIT = "quit" + GET = "get" + GO_BACK = "goBack" + GO_FORWARD = "goForward" + REFRESH = "refresh" + ADD_COOKIE = "addCookie" + GET_COOKIE = "getCookie" + GET_ALL_COOKIES = "getCookies" + DELETE_COOKIE = "deleteCookie" + DELETE_ALL_COOKIES = "deleteAllCookies" + FIND_ELEMENT = "findElement" + FIND_ELEMENTS = "findElements" + FIND_CHILD_ELEMENT = "findChildElement" + FIND_CHILD_ELEMENTS = "findChildElements" + CLEAR_ELEMENT = "clearElement" + CLICK_ELEMENT = "clickElement" + SEND_KEYS_TO_ELEMENT = "sendKeysToElement" + SEND_KEYS_TO_ACTIVE_ELEMENT = "sendKeysToActiveElement" + SUBMIT_ELEMENT = "submitElement" + UPLOAD_FILE = "uploadFile" + GET_CURRENT_WINDOW_HANDLE = "getCurrentWindowHandle" + W3C_GET_CURRENT_WINDOW_HANDLE = "w3cGetCurrentWindowHandle" + GET_WINDOW_HANDLES = "getWindowHandles" + W3C_GET_WINDOW_HANDLES = "w3cGetWindowHandles" + GET_WINDOW_SIZE = "getWindowSize" + W3C_GET_WINDOW_SIZE = "w3cGetWindowSize" + W3C_GET_WINDOW_POSITION = "w3cGetWindowPosition" + GET_WINDOW_POSITION = "getWindowPosition" + SET_WINDOW_SIZE = "setWindowSize" + W3C_SET_WINDOW_SIZE = "w3cSetWindowSize" + SET_WINDOW_RECT = "setWindowRect" + GET_WINDOW_RECT = "getWindowRect" + SET_WINDOW_POSITION = "setWindowPosition" + W3C_SET_WINDOW_POSITION = "w3cSetWindowPosition" + SWITCH_TO_WINDOW = "switchToWindow" + SWITCH_TO_FRAME = "switchToFrame" + SWITCH_TO_PARENT_FRAME = "switchToParentFrame" + GET_ACTIVE_ELEMENT = "getActiveElement" + W3C_GET_ACTIVE_ELEMENT = "w3cGetActiveElement" + GET_CURRENT_URL = "getCurrentUrl" + GET_PAGE_SOURCE = "getPageSource" + GET_TITLE = "getTitle" + EXECUTE_SCRIPT = "executeScript" + W3C_EXECUTE_SCRIPT = "w3cExecuteScript" + W3C_EXECUTE_SCRIPT_ASYNC = "w3cExecuteScriptAsync" + GET_ELEMENT_TEXT = "getElementText" + GET_ELEMENT_VALUE = "getElementValue" + GET_ELEMENT_TAG_NAME = "getElementTagName" + SET_ELEMENT_SELECTED = "setElementSelected" + IS_ELEMENT_SELECTED = "isElementSelected" + IS_ELEMENT_ENABLED = "isElementEnabled" + IS_ELEMENT_DISPLAYED = "isElementDisplayed" + GET_ELEMENT_LOCATION = "getElementLocation" + GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = "getElementLocationOnceScrolledIntoView" + GET_ELEMENT_SIZE = "getElementSize" + GET_ELEMENT_RECT = "getElementRect" + GET_ELEMENT_ATTRIBUTE = "getElementAttribute" + GET_ELEMENT_PROPERTY = "getElementProperty" + GET_ELEMENT_VALUE_OF_CSS_PROPERTY = "getElementValueOfCssProperty" + ELEMENT_EQUALS = "elementEquals" + SCREENSHOT = "screenshot" + ELEMENT_SCREENSHOT = "elementScreenshot" + IMPLICIT_WAIT = "implicitlyWait" + EXECUTE_ASYNC_SCRIPT = "executeAsyncScript" + SET_SCRIPT_TIMEOUT = "setScriptTimeout" + SET_TIMEOUTS = "setTimeouts" + MAXIMIZE_WINDOW = "windowMaximize" + W3C_MAXIMIZE_WINDOW = "w3cMaximizeWindow" + GET_LOG = "getLog" + GET_AVAILABLE_LOG_TYPES = "getAvailableLogTypes" + FULLSCREEN_WINDOW = "fullscreenWindow" + MINIMIZE_WINDOW = "minimizeWindow" + + # Alerts + DISMISS_ALERT = "dismissAlert" + W3C_DISMISS_ALERT = "w3cDismissAlert" + ACCEPT_ALERT = "acceptAlert" + W3C_ACCEPT_ALERT = "w3cAcceptAlert" + SET_ALERT_VALUE = "setAlertValue" + W3C_SET_ALERT_VALUE = "w3cSetAlertValue" + GET_ALERT_TEXT = "getAlertText" + W3C_GET_ALERT_TEXT = "w3cGetAlertText" + SET_ALERT_CREDENTIALS = "setAlertCredentials" + + # Advanced user interactions + W3C_ACTIONS = "actions" + W3C_CLEAR_ACTIONS = "clearActionState" + CLICK = "mouseClick" + DOUBLE_CLICK = "mouseDoubleClick" + MOUSE_DOWN = "mouseButtonDown" + MOUSE_UP = "mouseButtonUp" + MOVE_TO = "mouseMoveTo" + + # Screen Orientation + SET_SCREEN_ORIENTATION = "setScreenOrientation" + GET_SCREEN_ORIENTATION = "getScreenOrientation" + + # Touch Actions + SINGLE_TAP = "touchSingleTap" + TOUCH_DOWN = "touchDown" + TOUCH_UP = "touchUp" + TOUCH_MOVE = "touchMove" + TOUCH_SCROLL = "touchScroll" + DOUBLE_TAP = "touchDoubleTap" + LONG_PRESS = "touchLongPress" + FLICK = "touchFlick" + + # HTML 5 + EXECUTE_SQL = "executeSql" + + GET_LOCATION = "getLocation" + SET_LOCATION = "setLocation" + + GET_APP_CACHE = "getAppCache" + GET_APP_CACHE_STATUS = "getAppCacheStatus" + CLEAR_APP_CACHE = "clearAppCache" + + GET_LOCAL_STORAGE_ITEM = "getLocalStorageItem" + REMOVE_LOCAL_STORAGE_ITEM = "removeLocalStorageItem" + GET_LOCAL_STORAGE_KEYS = "getLocalStorageKeys" + SET_LOCAL_STORAGE_ITEM = "setLocalStorageItem" + CLEAR_LOCAL_STORAGE = "clearLocalStorage" + GET_LOCAL_STORAGE_SIZE = "getLocalStorageSize" + + GET_SESSION_STORAGE_ITEM = "getSessionStorageItem" + REMOVE_SESSION_STORAGE_ITEM = "removeSessionStorageItem" + GET_SESSION_STORAGE_KEYS = "getSessionStorageKeys" + SET_SESSION_STORAGE_ITEM = "setSessionStorageItem" + CLEAR_SESSION_STORAGE = "clearSessionStorage" + GET_SESSION_STORAGE_SIZE = "getSessionStorageSize" + + # Mobile + GET_NETWORK_CONNECTION = "getNetworkConnection" + SET_NETWORK_CONNECTION = "setNetworkConnection" + CURRENT_CONTEXT_HANDLE = "getCurrentContextHandle" + CONTEXT_HANDLES = "getContextHandles" + SWITCH_TO_CONTEXT = "switchToContext" diff --git a/youtube_dl/selenium/webdriver/remote/errorhandler.py b/youtube_dl/selenium/webdriver/remote/errorhandler.py new file mode 100644 index 000000000..e184c94cf --- /dev/null +++ b/youtube_dl/selenium/webdriver/remote/errorhandler.py @@ -0,0 +1,245 @@ +# 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 selenium.common.exceptions import (ElementClickInterceptedException, + ElementNotInteractableException, + ElementNotSelectableException, + ElementNotVisibleException, + ErrorInResponseException, + InsecureCertificateException, + InvalidCoordinatesException, + InvalidElementStateException, + InvalidSessionIdException, + InvalidSelectorException, + ImeNotAvailableException, + ImeActivationFailedException, + InvalidArgumentException, + InvalidCookieDomainException, + JavascriptException, + MoveTargetOutOfBoundsException, + NoSuchCookieException, + NoSuchElementException, + NoSuchFrameException, + NoSuchWindowException, + NoAlertPresentException, + ScreenshotException, + SessionNotCreatedException, + StaleElementReferenceException, + TimeoutException, + UnableToSetCookieException, + UnexpectedAlertPresentException, + UnknownMethodException, + WebDriverException) + +try: + basestring +except NameError: # Python 3.x + basestring = str + + +class ErrorCode(object): + """ + Error codes defined in the WebDriver wire protocol. + """ + # Keep in sync with org.openqa.selenium.remote.ErrorCodes and errorcodes.h + SUCCESS = 0 + NO_SUCH_ELEMENT = [7, 'no such element'] + NO_SUCH_FRAME = [8, 'no such frame'] + UNKNOWN_COMMAND = [9, 'unknown command'] + STALE_ELEMENT_REFERENCE = [10, 'stale element reference'] + ELEMENT_NOT_VISIBLE = [11, 'element not visible'] + INVALID_ELEMENT_STATE = [12, 'invalid element state'] + UNKNOWN_ERROR = [13, 'unknown error'] + ELEMENT_IS_NOT_SELECTABLE = [15, 'element not selectable'] + JAVASCRIPT_ERROR = [17, 'javascript error'] + XPATH_LOOKUP_ERROR = [19, 'invalid selector'] + TIMEOUT = [21, 'timeout'] + NO_SUCH_WINDOW = [23, 'no such window'] + INVALID_COOKIE_DOMAIN = [24, 'invalid cookie domain'] + UNABLE_TO_SET_COOKIE = [25, 'unable to set cookie'] + UNEXPECTED_ALERT_OPEN = [26, 'unexpected alert open'] + NO_ALERT_OPEN = [27, 'no such alert'] + SCRIPT_TIMEOUT = [28, 'script timeout'] + INVALID_ELEMENT_COORDINATES = [29, 'invalid element coordinates'] + IME_NOT_AVAILABLE = [30, 'ime not available'] + IME_ENGINE_ACTIVATION_FAILED = [31, 'ime engine activation failed'] + INVALID_SELECTOR = [32, 'invalid selector'] + SESSION_NOT_CREATED = [33, 'session not created'] + MOVE_TARGET_OUT_OF_BOUNDS = [34, 'move target out of bounds'] + INVALID_XPATH_SELECTOR = [51, 'invalid selector'] + INVALID_XPATH_SELECTOR_RETURN_TYPER = [52, 'invalid selector'] + + ELEMENT_NOT_INTERACTABLE = [60, 'element not interactable'] + INSECURE_CERTIFICATE = ['insecure certificate'] + INVALID_ARGUMENT = [61, 'invalid argument'] + INVALID_COORDINATES = ['invalid coordinates'] + INVALID_SESSION_ID = ['invalid session id'] + NO_SUCH_COOKIE = [62, 'no such cookie'] + UNABLE_TO_CAPTURE_SCREEN = [63, 'unable to capture screen'] + ELEMENT_CLICK_INTERCEPTED = [64, 'element click intercepted'] + UNKNOWN_METHOD = ['unknown method exception'] + + METHOD_NOT_ALLOWED = [405, 'unsupported operation'] + + +class ErrorHandler(object): + """ + Handles errors returned by the WebDriver server. + """ + def check_response(self, response): + """ + Checks that a JSON response from the WebDriver does not have an error. + + :Args: + - response - The JSON response from the WebDriver server as a dictionary + object. + + :Raises: If the response contains an error message. + """ + status = response.get('status', None) + if status is None or status == ErrorCode.SUCCESS: + return + value = None + message = response.get("message", "") + screen = response.get("screen", "") + stacktrace = None + if isinstance(status, int): + value_json = response.get('value', None) + if value_json and isinstance(value_json, basestring): + import json + try: + value = json.loads(value_json) + if len(value.keys()) == 1: + value = value['value'] + status = value.get('error', None) + if status is None: + status = value["status"] + message = value["value"] + if not isinstance(message, basestring): + value = message + message = message.get('message') + else: + message = value.get('message', None) + except ValueError: + pass + + exception_class = ErrorInResponseException + if status in ErrorCode.NO_SUCH_ELEMENT: + exception_class = NoSuchElementException + elif status in ErrorCode.NO_SUCH_FRAME: + exception_class = NoSuchFrameException + elif status in ErrorCode.NO_SUCH_WINDOW: + exception_class = NoSuchWindowException + elif status in ErrorCode.STALE_ELEMENT_REFERENCE: + exception_class = StaleElementReferenceException + elif status in ErrorCode.ELEMENT_NOT_VISIBLE: + exception_class = ElementNotVisibleException + elif status in ErrorCode.INVALID_ELEMENT_STATE: + exception_class = InvalidElementStateException + elif status in ErrorCode.INVALID_SELECTOR \ + or status in ErrorCode.INVALID_XPATH_SELECTOR \ + or status in ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER: + exception_class = InvalidSelectorException + elif status in ErrorCode.ELEMENT_IS_NOT_SELECTABLE: + exception_class = ElementNotSelectableException + elif status in ErrorCode.ELEMENT_NOT_INTERACTABLE: + exception_class = ElementNotInteractableException + elif status in ErrorCode.INVALID_COOKIE_DOMAIN: + exception_class = InvalidCookieDomainException + elif status in ErrorCode.UNABLE_TO_SET_COOKIE: + exception_class = UnableToSetCookieException + elif status in ErrorCode.TIMEOUT: + exception_class = TimeoutException + elif status in ErrorCode.SCRIPT_TIMEOUT: + exception_class = TimeoutException + elif status in ErrorCode.UNKNOWN_ERROR: + exception_class = WebDriverException + elif status in ErrorCode.UNEXPECTED_ALERT_OPEN: + exception_class = UnexpectedAlertPresentException + elif status in ErrorCode.NO_ALERT_OPEN: + exception_class = NoAlertPresentException + elif status in ErrorCode.IME_NOT_AVAILABLE: + exception_class = ImeNotAvailableException + elif status in ErrorCode.IME_ENGINE_ACTIVATION_FAILED: + exception_class = ImeActivationFailedException + elif status in ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS: + exception_class = MoveTargetOutOfBoundsException + elif status in ErrorCode.JAVASCRIPT_ERROR: + exception_class = JavascriptException + elif status in ErrorCode.SESSION_NOT_CREATED: + exception_class = SessionNotCreatedException + elif status in ErrorCode.INVALID_ARGUMENT: + exception_class = InvalidArgumentException + elif status in ErrorCode.NO_SUCH_COOKIE: + exception_class = NoSuchCookieException + elif status in ErrorCode.UNABLE_TO_CAPTURE_SCREEN: + exception_class = ScreenshotException + elif status in ErrorCode.ELEMENT_CLICK_INTERCEPTED: + exception_class = ElementClickInterceptedException + elif status in ErrorCode.INSECURE_CERTIFICATE: + exception_class = InsecureCertificateException + elif status in ErrorCode.INVALID_COORDINATES: + exception_class = InvalidCoordinatesException + elif status in ErrorCode.INVALID_SESSION_ID: + exception_class = InvalidSessionIdException + elif status in ErrorCode.UNKNOWN_METHOD: + exception_class = UnknownMethodException + else: + exception_class = WebDriverException + if value == '' or value is None: + value = response['value'] + if isinstance(value, basestring): + if exception_class == ErrorInResponseException: + raise exception_class(response, value) + raise exception_class(value) + if message == "" and 'message' in value: + message = value['message'] + + screen = None + if 'screen' in value: + screen = value['screen'] + + stacktrace = None + if 'stackTrace' in value and value['stackTrace']: + stacktrace = [] + try: + for frame in value['stackTrace']: + line = self._value_or_default(frame, 'lineNumber', '') + file = self._value_or_default(frame, 'fileName', '') + if line: + file = "%s:%s" % (file, line) + meth = self._value_or_default(frame, 'methodName', '') + if 'className' in frame: + meth = "%s.%s" % (frame['className'], meth) + msg = " at %s (%s)" + msg = msg % (meth, file) + stacktrace.append(msg) + except TypeError: + pass + if exception_class == ErrorInResponseException: + raise exception_class(response, message) + elif exception_class == UnexpectedAlertPresentException: + alert_text = None + if 'data' in value: + alert_text = value['data'].get('text') + elif 'alert' in value: + alert_text = value['alert'].get('text') + raise exception_class(message, screen, stacktrace, alert_text) + raise exception_class(message, screen, stacktrace) + + def _value_or_default(self, obj, key, default): + return obj[key] if key in obj else default diff --git a/youtube_dl/selenium/webdriver/remote/file_detector.py b/youtube_dl/selenium/webdriver/remote/file_detector.py new file mode 100644 index 000000000..caec9c30f --- /dev/null +++ b/youtube_dl/selenium/webdriver/remote/file_detector.py @@ -0,0 +1,58 @@ +# 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. + +import abc +import os +from selenium.webdriver.common.utils import keys_to_typing + + +class FileDetector(object): + """ + Used for identifying whether a sequence of chars represents the path to a + file. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def is_local_file(self, *keys): + return + + +class UselessFileDetector(FileDetector): + """ + A file detector that never finds anything. + """ + def is_local_file(self, *keys): + return None + + +class LocalFileDetector(FileDetector): + """ + Detects files on the local disk. + """ + def is_local_file(self, *keys): + file_path = ''.join(keys_to_typing(keys)) + + if not file_path: + return None + + try: + if os.path.isfile(file_path): + return file_path + except Exception: + pass + return None diff --git a/youtube_dl/selenium/webdriver/remote/getAttribute.js b/youtube_dl/selenium/webdriver/remote/getAttribute.js new file mode 100644 index 000000000..2a361b181 --- /dev/null +++ b/youtube_dl/selenium/webdriver/remote/getAttribute.js @@ -0,0 +1,8 @@ +function(){return function(){var d=this;function f(a){return"string"==typeof a};function h(a,b){this.code=a;this.a=l[a]||m;this.message=b||"";a=this.a.replace(/((?:^|\s+)[a-z])/g,function(a){return a.toUpperCase().replace(/^[\s\xa0]+/g,"")});b=a.length-5;if(0>b||a.indexOf("Error",b)!=b)a+="Error";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||""} +(function(){var a=Error;function b(){}b.prototype=a.prototype;h.b=a.prototype;h.prototype=new b;h.prototype.constructor=h;h.a=function(b,c,g){for(var e=Array(arguments.length-2),k=2;kparseFloat(D)){C=String(F);break a}}C=D}var G;var H=d.document;G=H&&y?B()||("CSS1Compat"==H.compatMode?parseInt(C,10):5):void 0;var ba=r("Firefox"),ca=v()||r("iPod"),da=r("iPad"),I=r("Android")&&!(w()||r("Firefox")||r("Opera")||r("Silk")),ea=w(),J=r("Safari")&&!(w()||r("Coast")||r("Opera")||r("Edge")||r("Silk")||r("Android"))&&!(v()||r("iPad")||r("iPod"));function K(a){return(a=a.exec(n))?a[1]:""}(function(){if(ba)return K(/Firefox\/([0-9.]+)/);if(y||z||x)return C;if(ea)return v()||r("iPad")||r("iPod")?K(/CriOS\/([0-9.]+)/):K(/Chrome\/([0-9.]+)/);if(J&&!(v()||r("iPad")||r("iPod")))return K(/Version\/([0-9.]+)/);if(ca||da){var a=/Version\/(\S+).*Mobile\/(\S+)/.exec(n);if(a)return a[1]+"."+a[2]}else if(I)return(a=K(/Android\s+([0-9.]+)/))?a:K(/Version\/([0-9.]+)/);return""})();var L,M=function(){if(!A)return!1;var a=d.Components;if(!a)return!1;try{if(!a.classes)return!1}catch(g){return!1}var b=a.classes,a=a.interfaces,e=b["@mozilla.org/xpcom/version-comparator;1"].getService(a.nsIVersionComparator),c=b["@mozilla.org/xre/app-info;1"].getService(a.nsIXULAppInfo).version;L=function(a){e.compare(c,""+a)};return!0}(),N=y&&!(8<=Number(G)),fa=y&&!(9<=Number(G));I&&M&&L(2.3);I&&M&&L(4);J&&M&&L(6);var ga={SCRIPT:1,STYLE:1,HEAD:1,IFRAME:1,OBJECT:1},O={IMG:" ",BR:"\n"};function P(a,b,e){if(!(a.nodeName in ga))if(3==a.nodeType)e?b.push(String(a.nodeValue).replace(/(\r\n|\r|\n)/g,"")):b.push(a.nodeValue);else if(a.nodeName in O)b.push(O[a.nodeName]);else for(a=a.firstChild;a;)P(a,b,e),a=a.nextSibling};function Q(a,b){b=b.toLowerCase();return"style"==b?ha(a.style.cssText):N&&"value"==b&&R(a,"INPUT")?a.value:fa&&!0===a[b]?String(a.getAttribute(b)):(a=a.getAttributeNode(b))&&a.specified?a.value:null}var ia=/[;]+(?=(?:(?:[^"]*"){2})*[^"]*$)(?=(?:(?:[^']*'){2})*[^']*$)(?=(?:[^()]*\([^()]*\))*[^()]*$)/; +function ha(a){var b=[];t(a.split(ia),function(a){var c=a.indexOf(":");0