src/eric7/WebBrowser/GreaseMonkey/GreaseMonkeyScript.py

Wed, 13 Jul 2022 14:55:47 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 13 Jul 2022 14:55:47 +0200
branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9473
3f23dbf37dbe
permissions
-rw-r--r--

Reformatted the source code using the 'Black' utility.

# -*- coding: utf-8 -*-

# Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the GreaseMonkey script.
"""

import re

from PyQt6.QtCore import (
    pyqtSignal,
    pyqtSlot,
    QObject,
    QUrl,
    QByteArray,
    QCryptographicHash,
)
from PyQt6.QtGui import QIcon, QPixmap, QImage
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt6.QtWebEngineCore import QWebEngineScript

from .GreaseMonkeyJavaScript import bootstrap_js, values_js
from .GreaseMonkeyDownloader import GreaseMonkeyDownloader

from ..Tools.DelayedFileWatcher import DelayedFileWatcher
from ..WebBrowserPage import WebBrowserPage
from ..WebBrowserWindow import WebBrowserWindow


class GreaseMonkeyScript(QObject):
    """
    Class implementing the GreaseMonkey script.

    @signal scriptChanged() emitted to indicate a script change
    @signal updatingChanged(bool) emitted to indicate a change of the
        updating state
    """

    DocumentStart = 0
    DocumentEnd = 1
    DocumentIdle = 2

    scriptChanged = pyqtSignal()
    updatingChanged = pyqtSignal(bool)

    def __init__(self, manager, path):
        """
        Constructor

        @param manager reference to the manager object (GreaseMonkeyManager)
        @param path path of the Javascript file (string)
        """
        super().__init__(manager)

        self.__manager = manager
        self.__fileWatcher = DelayedFileWatcher(parent=None)

        self.__name = ""
        self.__namespace = "GreaseMonkeyNS"
        self.__description = ""
        self.__version = ""

        self.__include = []
        self.__exclude = []
        self.__require = []

        self.__icon = QIcon()
        self.__iconUrl = QUrl()
        self.__downloadUrl = QUrl()
        self.__updateUrl = QUrl()
        self.__startAt = GreaseMonkeyScript.DocumentEnd

        self.__script = ""
        self.__fileName = path
        self.__enabled = True
        self.__valid = False
        self.__noFrames = False

        self.__updating = False

        self.__downloaders = []
        self.__iconReplies = []

        self.__parseScript()

        self.__fileWatcher.delayedFileChanged.connect(self.__watchedFileChanged)

    def isValid(self):
        """
        Public method to check the validity of the script.

        @return flag indicating a valid script (boolean)
        """
        return self.__valid

    def name(self):
        """
        Public method to get the name of the script.

        @return name of the script (string)
        """
        return self.__name

    def nameSpace(self):
        """
        Public method to get the name space of the script.

        @return name space of the script (string)
        """
        return self.__namespace

    def fullName(self):
        """
        Public method to get the full name of the script.

        @return full name of the script (string)
        """
        return "{0}/{1}".format(self.__namespace, self.__name)

    def description(self):
        """
        Public method to get the description of the script.

        @return description of the script (string)
        """
        return self.__description

    def version(self):
        """
        Public method to get the version of the script.

        @return version of the script (string)
        """
        return self.__version

    def icon(self):
        """
        Public method to get the icon of the script.

        @return script icon
        @rtype QIcon
        """
        return self.__icon

    def iconUrl(self):
        """
        Public method to get the icon URL of the script.

        @return icon URL of the script (QUrl)
        """
        return QUrl(self.__iconUrl)

    def downloadUrl(self):
        """
        Public method to get the download URL of the script.

        @return download URL of the script (QUrl)
        """
        return QUrl(self.__downloadUrl)

    def updateUrl(self):
        """
        Public method to get the update URL of the script.

        @return update URL of the script (QUrl)
        """
        return QUrl(self.__updateUrl)

    def startAt(self):
        """
        Public method to get the start point of the script.

        @return start point of the script (DocumentStart or DocumentEnd)
        """
        return self.__startAt

    def noFrames(self):
        """
        Public method to get the noFrames flag.

        @return flag indicating to not run on sub frames
        @rtype bool
        """
        return self.__noFrames

    def isEnabled(self):
        """
        Public method to check, if the script is enabled.

        @return flag indicating an enabled state (boolean)
        """
        return self.__enabled and self.__valid

    def setEnabled(self, enable):
        """
        Public method to enable a script.

        @param enable flag indicating the new enabled state (boolean)
        """
        self.__enabled = enable

    def include(self):
        """
        Public method to get the list of included URLs.

        @return list of included URLs (list of strings)
        """
        return self.__include[:]

    def exclude(self):
        """
        Public method to get the list of excluded URLs.

        @return list of excluded URLs (list of strings)
        """
        return self.__exclude[:]

    def require(self):
        """
        Public method to get the list of required scripts.

        @return list of required scripts (list of strings)
        """
        return self.__require[:]

    def fileName(self):
        """
        Public method to get the path of the Javascript file.

        @return path of the Javascript file (string)
        """
        return self.__fileName

    def isUpdating(self):
        """
        Public method to get the updating flag.

        @return updating flag
        @rtype bool
        """
        return self.__updating

    @pyqtSlot(str)
    def __watchedFileChanged(self, fileName):
        """
        Private slot handling changes of the script file.

        @param fileName path of the script file
        @type str
        """
        if self.__fileName == fileName:
            self.__reloadScript()

    def __parseScript(self):
        """
        Private method to parse the given script and populate the data
        structure.
        """
        self.__name = ""
        self.__namespace = "GreaseMonkeyNS"
        self.__description = ""
        self.__version = ""

        self.__include = []
        self.__exclude = []
        self.__require = []

        self.__icon = QIcon()
        self.__iconUrl = QUrl()
        self.__downloadUrl = QUrl()
        self.__updateUrl = QUrl()
        self.__startAt = GreaseMonkeyScript.DocumentEnd

        self.__script = ""
        self.__enabled = True
        self.__valid = False
        self.__noFrames = False

        try:
            with open(self.__fileName, "r", encoding="utf-8") as f:
                fileData = f.read()
        except OSError:
            # silently ignore because it shouldn't happen
            return

        if self.__fileName not in self.__fileWatcher.files():
            self.__fileWatcher.addPath(self.__fileName)

        rx = re.compile(r"""// ==UserScript==(.*)// ==/UserScript==""", re.DOTALL)
        match = rx.search(fileData)
        if match is None:
            # invalid script file
            return

        metaDataBlock = match.group(1).strip()
        if metaDataBlock == "":
            # invalid script file
            return

        for line in metaDataBlock.splitlines():
            if not line.strip():
                continue

            if not line.startswith("// @"):
                continue

            line = line[3:].replace("\t", " ")
            index = line.find(" ")

            key = line[:index].strip()
            value = line[index + 1 :].strip() if index > 0 else ""

            if not key:
                continue

            if key == "@name":
                self.__name = value

            elif key == "@namespace":
                self.__namespace = value

            elif key == "@description":
                self.__description = value

            elif key == "@version":
                self.__version = value

            elif key in ["@include", "@match"]:
                self.__include.append(value)

            elif key in ["@exclude", "@exclude_match"]:
                self.__exclude.append(value)

            elif key == "@require":
                self.__require.append(value)

            elif key == "@run-at":
                if value == "document-end":
                    self.__startAt = GreaseMonkeyScript.DocumentEnd
                elif value == "document-start":
                    self.__startAt = GreaseMonkeyScript.DocumentStart
                elif value == "document-idle":
                    self.__startAt = GreaseMonkeyScript.DocumentIdle

            elif key == "@downloadURL" and self.__downloadUrl.isEmpty():
                self.__downloadUrl = QUrl(value)

            elif key == "@updateURL" and self.__updateUrl.isEmpty():
                self.__updateUrl = QUrl(value)

            elif key == "@icon":
                self.__iconUrl = QUrl(value)

            elif key == "@noframes":
                self.__noFrames = True

        self.__iconUrl = self.__downloadUrl.resolved(self.__iconUrl)

        if not self.__include:
            self.__include.append("*")

        nspace = bytes(
            QCryptographicHash.hash(
                QByteArray(self.fullName().encode("utf-8")),
                QCryptographicHash.Algorithm.Md4,
            ).toHex()
        ).decode("ascii")
        valuesScript = values_js.format(nspace)
        self.__script = "(function(){{{0}\n{1}\n{2}\n}})();".format(
            valuesScript, self.__manager.requireScripts(self.__require), fileData
        )
        self.__valid = True

        self.__downloadIcon()
        self.__downloadRequires()

    def webScript(self):
        """
        Public method to create a script object.

        @return prepared script object
        @rtype QWebEngineScript
        """
        script = QWebEngineScript()
        script.setSourceCode("{0}\n{1}".format(bootstrap_js, self.__script))
        script.setName(self.fullName())
        script.setWorldId(WebBrowserPage.SafeJsWorld)
        script.setRunsOnSubFrames(not self.__noFrames)
        return script

    def updateScript(self):
        """
        Public method to updated the script.
        """
        if not self.__downloadUrl.isValid() or self.__updating:
            return

        self.__updating = True
        self.updatingChanged.emit(self.__updating)

        downloader = GreaseMonkeyDownloader(
            self.__downloadUrl,
            self.__manager,
            GreaseMonkeyDownloader.DownloadMainScript,
        )
        downloader.updateScript(self.__fileName)
        downloader.finished.connect(lambda: self.__downloaderFinished(downloader))
        downloader.error.connect(lambda: self.__downloaderError(downloader))
        self.__downloaders.append(downloader)

        self.__downloadRequires()

    def __downloaderFinished(self, downloader):
        """
        Private slot to handle a finished download.

        @param downloader reference to the downloader object
        @type GreaseMonkeyDownloader
        """
        if downloader in self.__downloaders:
            self.__downloaders.remove(downloader)
        self.__updating = False
        self.updatingChanged.emit(self.__updating)

    def __downloaderError(self, downloader):
        """
        Private slot to handle a downloader error.

        @param downloader reference to the downloader object
        @type GreaseMonkeyDownloader
        """
        if downloader in self.__downloaders:
            self.__downloaders.remove(downloader)
        self.__updating = False
        self.updatingChanged.emit(self.__updating)

    def __reloadScript(self):
        """
        Private method to reload the script.
        """
        self.__parseScript()

        self.__manager.removeScript(self, False)
        self.__manager.addScript(self)

        self.scriptChanged.emit()

    def __downloadRequires(self):
        """
        Private method to download the required scripts.
        """
        for urlStr in self.__require:
            if not self.__manager.requireScripts([urlStr]):
                downloader = GreaseMonkeyDownloader(
                    QUrl(urlStr),
                    self.__manager,
                    GreaseMonkeyDownloader.DownloadRequireScript,
                )
                downloader.finished.connect(
                    lambda: self.__requireDownloaded(downloader)
                )
                downloader.error.connect(
                    lambda: self.__requireDownloadError(downloader)
                )
                self.__downloaders.append(downloader)

    def __requireDownloaded(self, downloader):
        """
        Private slot to handle a finished download of a required script.

        @param downloader reference to the downloader object
        @type GreaseMonkeyDownloader
        """
        if downloader in self.__downloaders:
            self.__downloaders.remove(downloader)

        self.__reloadScript()

    def __requireDownloadError(self, downloader):
        """
        Private slot to handle a downloader error.

        @param downloader reference to the downloader object
        @type GreaseMonkeyDownloader
        """
        if downloader in self.__downloaders:
            self.__downloaders.remove(downloader)

    def __downloadIcon(self):
        """
        Private slot to download the script icon.
        """
        if self.__iconUrl.isValid():
            request = QNetworkRequest(self.__iconUrl)
            reply = WebBrowserWindow.networkManager().get(request)
            reply.finished.connect(lambda: self.__iconDownloaded(reply))
            self.__iconReplies.append(reply)

    def __iconDownloaded(self, reply):
        """
        Private slot to handle a finished download of a script icon.

        @param reply reference to the network reply
        @type QNetworkReply
        """
        if reply in self.__iconReplies:
            self.__iconReplies.remove(reply)

        reply.deleteLater()
        if reply.error() == QNetworkReply.NetworkError.NoError:
            self.__icon = QPixmap.fromImage(QImage.fromData(reply.readAll()))

eric ide

mercurial