Hide navigation sidebar
Hide table of contents sidebar
Back to top
Toggle table of contents sidebar

# Source code for nodriver.core.config

    # Copyright 2024 by UltrafunkAmsterdam (https://github.com/UltrafunkAmsterdam)
    # All rights reserved.
    # This file is part of the nodriver package.
    # and is released under the "GNU AFFERO GENERAL PUBLIC LICENSE".
    # Please see the LICENSE.txt file that should have been included as part of this package.

    import logging
    import os
    import pathlib
    import secrets
    import sys
    import tempfile
    import zipfile
    from typing import List, Optional, TypeVar

    __all__ = [
        "Config",
        "find_chrome_executable",
        "temp_profile_dir",
        "is_root",
        "is_posix",
        "PathLike",
    ]

    logger = logging.getLogger(__name__)
    is_posix = sys.platform.startswith(("darwin", "cygwin", "linux", "linux2"))

    PathLike = TypeVar("PathLike", bound=str | pathlib.Path)
    AUTO = None

    class Config:
        """
        Config object
        """

        def __init__(
            self,
            user_data_dir: Optional[PathLike] = AUTO,
            headless: Optional[bool] = False,
            browser_executable_path: Optional[PathLike] = AUTO,
            browser_args: Optional[List[str]] = AUTO,
            sandbox: Optional[bool] = True,
            lang: Optional[str] = "en-US",
            host: str = AUTO,
            port: int = AUTO,
            expert: bool = AUTO,
            **kwargs: dict,
        ):
            """
            creates a config object.
            Can be called without any arguments to generate a best-practice config, which is recommended.

            calling the object, eg :  myconfig() , will return the list of arguments which
            are provided to the browser.

            additional arguments can be added using the :py:obj:`~add_argument method`

            Instances of this class are usually not instantiated by end users.

            :param user_data_dir: the data directory to use
            :param headless: set to True for headless mode
            :param browser_executable_path: specify browser executable, instead of using autodetect
            :param browser_args: forwarded to browser executable. eg : ["--some-chromeparam=somevalue", "some-other-param=someval"]
            :param sandbox: disables sandbox
            :param autodiscover_targets: use autodiscovery of targets
            :param lang: language string to use other than the default "en-US,en;q=0.9"
            :param expert: when set to True, enabled "expert" mode.
                   This conveys, the inclusion of parameters:  ----disable-site-isolation-trials,
                   as well as some scripts and patching useful for debugging (for example, ensuring shadow-root is always in "open" mode)

            :param kwargs:

            :type user_data_dir: PathLike
            :type headless: bool
            :type browser_executable_path: PathLike
            :type browser_args: list[str]
            :type sandbox: bool
            :type lang: str
            :type kwargs: dict
            """

            if not browser_args:
                browser_args = []

            if not user_data_dir:
                self._user_data_dir = temp_profile_dir()
                self._custom_data_dir = False
            else:
                self.user_data_dir = user_data_dir

            if not browser_executable_path:
                browser_executable_path = find_chrome_executable()

            self._browser_args = browser_args

            self.browser_executable_path = browser_executable_path
            self.headless = headless
            self.sandbox = sandbox
            self.host = host
            self.port = port
            self.expert = expert
            self._extensions = []
            # when using posix-ish operating system and running as root
            # you must use no_sandbox = True, which in case is corrected here
            if is_posix and is_root() and sandbox:
                logger.info("detected root usage, auto disabling sandbox mode")
                self.sandbox = False

            self.autodiscover_targets = True
            self.lang = lang

            # other keyword args will be accessible by attribute
            self.__dict__.update(kwargs)
            super().__init__()
            self._default_browser_args = [
                "--remote-allow-origins=*",
                "--no-first-run",
                "--no-service-autorun",
                "--no-default-browser-check",
                "--homepage=about:blank",
                "--no-pings",
                "--password-store=basic",
                "--disable-infobars",
                "--disable-breakpad",
                "--disable-dev-shm-usage",
                "--disable-session-crashed-bubble",
                "--disable-search-engine-choice-screen",
            ]

        @property
        def browser_args(self):
            return sorted(self._default_browser_args + self._browser_args)

        @property
        def user_data_dir(self):
            return self._user_data_dir

        @user_data_dir.setter
        def user_data_dir(self, path: PathLike):
            self._user_data_dir = str(path)
            self._custom_data_dir = True

        @property
        def uses_custom_data_dir(self) -> bool:
            return self._custom_data_dir

        def add_extension(self, extension_path: PathLike):
            """
            adds an extension to load, you could point extension_path
            to a folder (containing the manifest), or extension file (crx)

            :param extension_path:
            :type extension_path:
            :return:
            :rtype:
            """
            path = pathlib.Path(extension_path)

            if not path.exists():
                raise FileNotFoundError("could not find anything here: %s" % str(path))

            if path.is_file():
                tf = tempfile.mkdtemp(prefix=f"extension_", suffix=secrets.token_hex(4))
                with zipfile.ZipFile(path, "r") as z:
                    z.extractall(tf)
                    self._extensions.append(tf)

            elif path.is_dir():
                for item in path.rglob("manifest.*"):
                    path = item.parent
                self._extensions.append(path)

        def __call__(self):
            # the host and port will be added when starting
            # the browser, as by the time it starts, the port
            # is probably already taken
            args = self._default_browser_args.copy()
            args += ["--user-data-dir=%s" % self.user_data_dir]
            args += ["--disable-session-crashed-bubble"]
            args += ["--disable-features=IsolateOrigins,site-per-process"]
            if self.expert:
                args += ["--disable-site-isolation-trials"]
            if self._browser_args:
                args.extend([arg for arg in self._browser_args if arg not in args])
            if self.headless:
                args.append("--headless=new")
            if not self.sandbox:
                args.append("--no-sandbox")
            if self.host:
                args.append("--remote-debugging-host=%s" % self.host)
            if self.port:
                args.append("--remote-debugging-port=%s" % self.port)
            return args

        def add_argument(self, arg: str):
            if any(
                x in arg.lower()
                for x in [
                    "headless",
                    "data-dir",
                    "data_dir",
                    "no-sandbox",
                    "no_sandbox",
                    "lang",
                ]
            ):
                raise ValueError(
                    '"%s" not allowed. please use one of the attributes of the Config object to set it'
                    % arg
                )
            self._browser_args.append(arg)

        def __repr__(self):
            s = f"{self.__class__.__name__}"
            for k, v in ({**self.__dict__, **self.__class__.__dict__}).items():
                if k[0] == "_":
                    continue
                if not v:
                    continue
                if isinstance(v, property):
                    v = getattr(self, k)
                if callable(v):
                    continue
                s += f"\n\t{k} = {v}"
            return s

    def is_root():
        """
        helper function to determine if user trying to launch chrome
        under linux as root, which needs some alternative handling
        :return:
        :rtype:
        """
        import ctypes
        import os

        try:
            return os.getuid() == 0
        except AttributeError:
            return ctypes.windll.shell32.IsUserAnAdmin() != 0

    def temp_profile_dir():
        """generate a temp dir (path)"""
        path = os.path.normpath(tempfile.mkdtemp(prefix="uc_"))
        return path

    def find_chrome_executable(return_all=False):
        """
        Finds the chrome, beta, canary, chromium executable
        and returns the disk path
        """
        candidates = []
        if is_posix:
            for item in os.environ.get("PATH").split(os.pathsep):
                for subitem in (
                    "google-chrome",
                    "chromium",
                    "chromium-browser",
                    "chrome",
                    "google-chrome-stable",
                ):
                    candidates.append(os.sep.join((item, subitem)))
            if "darwin" in sys.platform:
                candidates += [
                    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
                    "/Applications/Chromium.app/Contents/MacOS/Chromium",
                ]

        else:
            for item in map(
                os.environ.get,
                ("PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA", "PROGRAMW6432"),
            ):
                if item is not None:
                    for subitem in (
                        "Google/Chrome/Application",
                        "Google/Chrome Beta/Application",
                        "Google/Chrome Canary/Application",
                    ):
                        candidates.append(os.sep.join((item, subitem, "chrome.exe")))
        rv = []
        for candidate in candidates:
            if os.path.exists(candidate) and os.access(candidate, os.X_OK):
                logger.debug("%s is a valid candidate... " % candidate)
                rv.append(candidate)
            else:
                logger.debug(
                    "%s is not a valid candidate because don't exist or not executable "
                    % candidate
                )

        winner = None

        if return_all and rv:
            return rv

        if rv and len(rv) > 1:
            # assuming the shortest path wins
            winner = min(rv, key=lambda x: len(x))

        elif len(rv) == 1:
            winner = rv[0]

        if winner:
            return os.path.normpath(winner)

        raise FileNotFoundError(
            "could not find a valid chrome browser binary. please make sure chrome is installed."
            "or use the keyword argument 'browser_executable_path=/path/to/your/browser' "
        )