eric7/WebBrowser/GreaseMonkey/GreaseMonkeyScript.py

branch
eric7
changeset 8312
800c432b34c8
parent 8235
78e6d29eb773
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the GreaseMonkey script.
8 """
9
10 import re
11
12 from PyQt5.QtCore import (
13 pyqtSignal, pyqtSlot, QObject, QUrl, QByteArray, QCryptographicHash
14 )
15 from PyQt5.QtGui import QIcon, QPixmap, QImage
16 from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
17 from PyQt5.QtWebEngineWidgets import QWebEngineScript
18
19 from .GreaseMonkeyJavaScript import bootstrap_js, values_js
20 from .GreaseMonkeyDownloader import GreaseMonkeyDownloader
21
22 from ..Tools.DelayedFileWatcher import DelayedFileWatcher
23 from ..WebBrowserPage import WebBrowserPage
24 from ..WebBrowserWindow import WebBrowserWindow
25
26
27 class GreaseMonkeyScript(QObject):
28 """
29 Class implementing the GreaseMonkey script.
30
31 @signal scriptChanged() emitted to indicate a script change
32 @signal updatingChanged(bool) emitted to indicate a change of the
33 updating state
34 """
35 DocumentStart = 0
36 DocumentEnd = 1
37 DocumentIdle = 2
38
39 scriptChanged = pyqtSignal()
40 updatingChanged = pyqtSignal(bool)
41
42 def __init__(self, manager, path):
43 """
44 Constructor
45
46 @param manager reference to the manager object (GreaseMonkeyManager)
47 @param path path of the Javascript file (string)
48 """
49 super().__init__(manager)
50
51 self.__manager = manager
52 self.__fileWatcher = DelayedFileWatcher(parent=None)
53
54 self.__name = ""
55 self.__namespace = "GreaseMonkeyNS"
56 self.__description = ""
57 self.__version = ""
58
59 self.__include = []
60 self.__exclude = []
61 self.__require = []
62
63 self.__icon = QIcon()
64 self.__iconUrl = QUrl()
65 self.__downloadUrl = QUrl()
66 self.__updateUrl = QUrl()
67 self.__startAt = GreaseMonkeyScript.DocumentEnd
68
69 self.__script = ""
70 self.__fileName = path
71 self.__enabled = True
72 self.__valid = False
73 self.__noFrames = False
74
75 self.__updating = False
76
77 self.__downloaders = []
78 self.__iconReplies = []
79
80 self.__parseScript()
81
82 self.__fileWatcher.delayedFileChanged.connect(
83 self.__watchedFileChanged)
84
85 def isValid(self):
86 """
87 Public method to check the validity of the script.
88
89 @return flag indicating a valid script (boolean)
90 """
91 return self.__valid
92
93 def name(self):
94 """
95 Public method to get the name of the script.
96
97 @return name of the script (string)
98 """
99 return self.__name
100
101 def nameSpace(self):
102 """
103 Public method to get the name space of the script.
104
105 @return name space of the script (string)
106 """
107 return self.__namespace
108
109 def fullName(self):
110 """
111 Public method to get the full name of the script.
112
113 @return full name of the script (string)
114 """
115 return "{0}/{1}".format(self.__namespace, self.__name)
116
117 def description(self):
118 """
119 Public method to get the description of the script.
120
121 @return description of the script (string)
122 """
123 return self.__description
124
125 def version(self):
126 """
127 Public method to get the version of the script.
128
129 @return version of the script (string)
130 """
131 return self.__version
132
133 def icon(self):
134 """
135 Public method to get the icon of the script.
136
137 @return script icon
138 @rtype QIcon
139 """
140 return self.__icon
141
142 def iconUrl(self):
143 """
144 Public method to get the icon URL of the script.
145
146 @return icon URL of the script (QUrl)
147 """
148 return QUrl(self.__iconUrl)
149
150 def downloadUrl(self):
151 """
152 Public method to get the download URL of the script.
153
154 @return download URL of the script (QUrl)
155 """
156 return QUrl(self.__downloadUrl)
157
158 def updateUrl(self):
159 """
160 Public method to get the update URL of the script.
161
162 @return update URL of the script (QUrl)
163 """
164 return QUrl(self.__updateUrl)
165
166 def startAt(self):
167 """
168 Public method to get the start point of the script.
169
170 @return start point of the script (DocumentStart or DocumentEnd)
171 """
172 return self.__startAt
173
174 def noFrames(self):
175 """
176 Public method to get the noFrames flag.
177
178 @return flag indicating to not run on sub frames
179 @rtype bool
180 """
181 return self.__noFrames
182
183 def isEnabled(self):
184 """
185 Public method to check, if the script is enabled.
186
187 @return flag indicating an enabled state (boolean)
188 """
189 return self.__enabled and self.__valid
190
191 def setEnabled(self, enable):
192 """
193 Public method to enable a script.
194
195 @param enable flag indicating the new enabled state (boolean)
196 """
197 self.__enabled = enable
198
199 def include(self):
200 """
201 Public method to get the list of included URLs.
202
203 @return list of included URLs (list of strings)
204 """
205 return self.__include[:]
206
207 def exclude(self):
208 """
209 Public method to get the list of excluded URLs.
210
211 @return list of excluded URLs (list of strings)
212 """
213 return self.__exclude[:]
214
215 def require(self):
216 """
217 Public method to get the list of required scripts.
218
219 @return list of required scripts (list of strings)
220 """
221 return self.__require[:]
222
223 def fileName(self):
224 """
225 Public method to get the path of the Javascript file.
226
227 @return path of the Javascript file (string)
228 """
229 return self.__fileName
230
231 def isUpdating(self):
232 """
233 Public method to get the updating flag.
234
235 @return updating flag
236 @rtype bool
237 """
238 return self.__updating
239
240 @pyqtSlot(str)
241 def __watchedFileChanged(self, fileName):
242 """
243 Private slot handling changes of the script file.
244
245 @param fileName path of the script file
246 @type str
247 """
248 if self.__fileName == fileName:
249 self.__reloadScript()
250
251 def __parseScript(self):
252 """
253 Private method to parse the given script and populate the data
254 structure.
255 """
256 self.__name = ""
257 self.__namespace = "GreaseMonkeyNS"
258 self.__description = ""
259 self.__version = ""
260
261 self.__include = []
262 self.__exclude = []
263 self.__require = []
264
265 self.__icon = QIcon()
266 self.__iconUrl = QUrl()
267 self.__downloadUrl = QUrl()
268 self.__updateUrl = QUrl()
269 self.__startAt = GreaseMonkeyScript.DocumentEnd
270
271 self.__script = ""
272 self.__enabled = True
273 self.__valid = False
274 self.__noFrames = False
275
276 try:
277 with open(self.__fileName, "r", encoding="utf-8") as f:
278 fileData = f.read()
279 except OSError:
280 # silently ignore because it shouldn't happen
281 return
282
283 if self.__fileName not in self.__fileWatcher.files():
284 self.__fileWatcher.addPath(self.__fileName)
285
286 rx = re.compile("// ==UserScript==(.*)// ==/UserScript==")
287 match = rx.search(fileData)
288 if match is None:
289 # invalid script file
290 return
291
292 metaDataBlock = match.group(1).strip()
293 if metaDataBlock == "":
294 # invalid script file
295 return
296
297 for line in metaDataBlock.splitlines():
298 if not line.strip():
299 continue
300
301 if not line.startswith("// @"):
302 continue
303
304 line = line[3:].replace("\t", " ")
305 index = line.find(" ")
306
307 key = line[:index].strip()
308 value = line[index + 1:].strip() if index > 0 else ""
309
310 if not key:
311 continue
312
313 if key == "@name":
314 self.__name = value
315
316 elif key == "@namespace":
317 self.__namespace = value
318
319 elif key == "@description":
320 self.__description = value
321
322 elif key == "@version":
323 self.__version = value
324
325 elif key in ["@include", "@match"]:
326 self.__include.append(value)
327
328 elif key in ["@exclude", "@exclude_match"]:
329 self.__exclude.append(value)
330
331 elif key == "@require":
332 self.__require.append(value)
333
334 elif key == "@run-at":
335 if value == "document-end":
336 self.__startAt = GreaseMonkeyScript.DocumentEnd
337 elif value == "document-start":
338 self.__startAt = GreaseMonkeyScript.DocumentStart
339 elif value == "document-idle":
340 self.__startAt = GreaseMonkeyScript.DocumentIdle
341
342 elif key == "@downloadURL" and self.__downloadUrl.isEmpty():
343 self.__downloadUrl = QUrl(value)
344
345 elif key == "@updateURL" and self.__updateUrl.isEmpty():
346 self.__updateUrl = QUrl(value)
347
348 elif key == "@icon":
349 self.__iconUrl = QUrl(value)
350
351 elif key == "@noframes":
352 self.__noFrames = True
353
354 self.__iconUrl = self.__downloadUrl.resolved(self.__iconUrl)
355
356 if not self.__include:
357 self.__include.append("*")
358
359 nspace = bytes(QCryptographicHash.hash(
360 QByteArray(self.fullName().encode("utf-8")),
361 QCryptographicHash.Algorithm.Md4).toHex()).decode("ascii")
362 valuesScript = values_js.format(nspace)
363 self.__script = "(function(){{{0}\n{1}\n{2}\n}})();".format(
364 valuesScript, self.__manager.requireScripts(self.__require),
365 fileData
366 )
367 self.__valid = True
368
369 self.__downloadIcon()
370 self.__downloadRequires()
371
372 def webScript(self):
373 """
374 Public method to create a script object.
375
376 @return prepared script object
377 @rtype QWebEngineScript
378 """
379 script = QWebEngineScript()
380 script.setSourceCode("{0}\n{1}".format(
381 bootstrap_js, self.__script
382 ))
383 script.setName(self.fullName())
384 script.setWorldId(WebBrowserPage.SafeJsWorld)
385 script.setRunsOnSubFrames(not self.__noFrames)
386 return script
387
388 def updateScript(self):
389 """
390 Public method to updated the script.
391 """
392 if not self.__downloadUrl.isValid() or self.__updating:
393 return
394
395 self.__updating = True
396 self.updatingChanged.emit(self.__updating)
397
398 downloader = GreaseMonkeyDownloader(
399 self.__downloadUrl,
400 self.__manager,
401 GreaseMonkeyDownloader.DownloadMainScript)
402 downloader.updateScript(self.__fileName)
403 downloader.finished.connect(
404 lambda: self.__downloaderFinished(downloader))
405 downloader.error.connect(
406 lambda: self.__downloaderError(downloader))
407 self.__downloaders.append(downloader)
408
409 self.__downloadRequires()
410
411 def __downloaderFinished(self, downloader):
412 """
413 Private slot to handle a finished download.
414
415 @param downloader reference to the downloader object
416 @type GreaseMonkeyDownloader
417 """
418 if downloader in self.__downloaders:
419 self.__downloaders.remove(downloader)
420 self.__updating = False
421 self.updatingChanged.emit(self.__updating)
422
423 def __downloaderError(self, downloader):
424 """
425 Private slot to handle a downloader error.
426
427 @param downloader reference to the downloader object
428 @type GreaseMonkeyDownloader
429 """
430 if downloader in self.__downloaders:
431 self.__downloaders.remove(downloader)
432 self.__updating = False
433 self.updatingChanged.emit(self.__updating)
434
435 def __reloadScript(self):
436 """
437 Private method to reload the script.
438 """
439 self.__parseScript()
440
441 self.__manager.removeScript(self, False)
442 self.__manager.addScript(self)
443
444 self.scriptChanged.emit()
445
446 def __downloadRequires(self):
447 """
448 Private method to download the required scripts.
449 """
450 for urlStr in self.__require:
451 if not self.__manager.requireScripts([urlStr]):
452 downloader = GreaseMonkeyDownloader(
453 QUrl(urlStr),
454 self.__manager,
455 GreaseMonkeyDownloader.DownloadRequireScript)
456 downloader.finished.connect(
457 lambda: self.__requireDownloaded(downloader))
458 downloader.error.connect(
459 lambda: self.__requireDownloadError(downloader))
460 self.__downloaders.append(downloader)
461
462 def __requireDownloaded(self, downloader):
463 """
464 Private slot to handle a finished download of a required script.
465
466 @param downloader reference to the downloader object
467 @type GreaseMonkeyDownloader
468 """
469 if downloader in self.__downloaders:
470 self.__downloaders.remove(downloader)
471
472 self.__reloadScript()
473
474 def __requireDownloadError(self, downloader):
475 """
476 Private slot to handle a downloader error.
477
478 @param downloader reference to the downloader object
479 @type GreaseMonkeyDownloader
480 """
481 if downloader in self.__downloaders:
482 self.__downloaders.remove(downloader)
483
484 def __downloadIcon(self):
485 """
486 Private slot to download the script icon.
487 """
488 if self.__iconUrl.isValid():
489 request = QNetworkRequest(self.__iconUrl)
490 reply = WebBrowserWindow.networkManager().get(request)
491 reply.finished.connect(lambda: self.__iconDownloaded(reply))
492 self.__iconReplies.append(reply)
493
494 def __iconDownloaded(self, reply):
495 """
496 Private slot to handle a finished download of a script icon.
497
498 @param reply reference to the network reply
499 @type QNetworkReply
500 """
501 if reply in self.__iconReplies:
502 self.__iconReplies.remove(reply)
503
504 reply.deleteLater()
505 if reply.error() == QNetworkReply.NetworkError.NoError:
506 self.__icon = QPixmap.fromImage(QImage.fromData(reply.readAll()))

eric ide

mercurial