Wed, 30 Dec 2020 11:00:05 +0100
Updated copyright for 2021.
# -*- coding: utf-8 -*- # Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the GreaseMonkey script. """ import re from PyQt5.QtCore import ( pyqtSignal, pyqtSlot, QObject, QUrl, QByteArray, QCryptographicHash ) from PyQt5.QtGui import QIcon, QPixmap, QImage from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtWebEngineWidgets 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(GreaseMonkeyScript, self).__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("// ==UserScript==(.*)// ==/UserScript==") 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() if index > 0: value = line[index + 1:].strip() else: value = "" 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.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.NoError: self.__icon = QPixmap.fromImage(QImage.fromData(reply.readAll()))