--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/WebBrowser/GreaseMonkey/GreaseMonkeyScript.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,506 @@ +# -*- 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().__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() + 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()))