src/eric7/WebBrowser/GreaseMonkey/GreaseMonkeyScript.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9162
8b75b1668583
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the GreaseMonkey script.
8 """
9
10 import re
11
12 from PyQt6.QtCore import (
13 pyqtSignal, pyqtSlot, QObject, QUrl, QByteArray, QCryptographicHash
14 )
15 from PyQt6.QtGui import QIcon, QPixmap, QImage
16 from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
17 from PyQt6.QtWebEngineCore 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(
287 r"""// ==UserScript==(.*)// ==/UserScript==""",
288 re.DOTALL
289 )
290 match = rx.search(fileData)
291 if match is None:
292 # invalid script file
293 return
294
295 metaDataBlock = match.group(1).strip()
296 if metaDataBlock == "":
297 # invalid script file
298 return
299
300 for line in metaDataBlock.splitlines():
301 if not line.strip():
302 continue
303
304 if not line.startswith("// @"):
305 continue
306
307 line = line[3:].replace("\t", " ")
308 index = line.find(" ")
309
310 key = line[:index].strip()
311 value = line[index + 1:].strip() if index > 0 else ""
312
313 if not key:
314 continue
315
316 if key == "@name":
317 self.__name = value
318
319 elif key == "@namespace":
320 self.__namespace = value
321
322 elif key == "@description":
323 self.__description = value
324
325 elif key == "@version":
326 self.__version = value
327
328 elif key in ["@include", "@match"]:
329 self.__include.append(value)
330
331 elif key in ["@exclude", "@exclude_match"]:
332 self.__exclude.append(value)
333
334 elif key == "@require":
335 self.__require.append(value)
336
337 elif key == "@run-at":
338 if value == "document-end":
339 self.__startAt = GreaseMonkeyScript.DocumentEnd
340 elif value == "document-start":
341 self.__startAt = GreaseMonkeyScript.DocumentStart
342 elif value == "document-idle":
343 self.__startAt = GreaseMonkeyScript.DocumentIdle
344
345 elif key == "@downloadURL" and self.__downloadUrl.isEmpty():
346 self.__downloadUrl = QUrl(value)
347
348 elif key == "@updateURL" and self.__updateUrl.isEmpty():
349 self.__updateUrl = QUrl(value)
350
351 elif key == "@icon":
352 self.__iconUrl = QUrl(value)
353
354 elif key == "@noframes":
355 self.__noFrames = True
356
357 self.__iconUrl = self.__downloadUrl.resolved(self.__iconUrl)
358
359 if not self.__include:
360 self.__include.append("*")
361
362 nspace = bytes(QCryptographicHash.hash(
363 QByteArray(self.fullName().encode("utf-8")),
364 QCryptographicHash.Algorithm.Md4).toHex()).decode("ascii")
365 valuesScript = values_js.format(nspace)
366 self.__script = "(function(){{{0}\n{1}\n{2}\n}})();".format(
367 valuesScript, self.__manager.requireScripts(self.__require),
368 fileData
369 )
370 self.__valid = True
371
372 self.__downloadIcon()
373 self.__downloadRequires()
374
375 def webScript(self):
376 """
377 Public method to create a script object.
378
379 @return prepared script object
380 @rtype QWebEngineScript
381 """
382 script = QWebEngineScript()
383 script.setSourceCode("{0}\n{1}".format(
384 bootstrap_js, self.__script
385 ))
386 script.setName(self.fullName())
387 script.setWorldId(WebBrowserPage.SafeJsWorld)
388 script.setRunsOnSubFrames(not self.__noFrames)
389 return script
390
391 def updateScript(self):
392 """
393 Public method to updated the script.
394 """
395 if not self.__downloadUrl.isValid() or self.__updating:
396 return
397
398 self.__updating = True
399 self.updatingChanged.emit(self.__updating)
400
401 downloader = GreaseMonkeyDownloader(
402 self.__downloadUrl,
403 self.__manager,
404 GreaseMonkeyDownloader.DownloadMainScript)
405 downloader.updateScript(self.__fileName)
406 downloader.finished.connect(
407 lambda: self.__downloaderFinished(downloader))
408 downloader.error.connect(
409 lambda: self.__downloaderError(downloader))
410 self.__downloaders.append(downloader)
411
412 self.__downloadRequires()
413
414 def __downloaderFinished(self, downloader):
415 """
416 Private slot to handle a finished download.
417
418 @param downloader reference to the downloader object
419 @type GreaseMonkeyDownloader
420 """
421 if downloader in self.__downloaders:
422 self.__downloaders.remove(downloader)
423 self.__updating = False
424 self.updatingChanged.emit(self.__updating)
425
426 def __downloaderError(self, downloader):
427 """
428 Private slot to handle a downloader error.
429
430 @param downloader reference to the downloader object
431 @type GreaseMonkeyDownloader
432 """
433 if downloader in self.__downloaders:
434 self.__downloaders.remove(downloader)
435 self.__updating = False
436 self.updatingChanged.emit(self.__updating)
437
438 def __reloadScript(self):
439 """
440 Private method to reload the script.
441 """
442 self.__parseScript()
443
444 self.__manager.removeScript(self, False)
445 self.__manager.addScript(self)
446
447 self.scriptChanged.emit()
448
449 def __downloadRequires(self):
450 """
451 Private method to download the required scripts.
452 """
453 for urlStr in self.__require:
454 if not self.__manager.requireScripts([urlStr]):
455 downloader = GreaseMonkeyDownloader(
456 QUrl(urlStr),
457 self.__manager,
458 GreaseMonkeyDownloader.DownloadRequireScript)
459 downloader.finished.connect(
460 lambda: self.__requireDownloaded(downloader))
461 downloader.error.connect(
462 lambda: self.__requireDownloadError(downloader))
463 self.__downloaders.append(downloader)
464
465 def __requireDownloaded(self, downloader):
466 """
467 Private slot to handle a finished download of a required script.
468
469 @param downloader reference to the downloader object
470 @type GreaseMonkeyDownloader
471 """
472 if downloader in self.__downloaders:
473 self.__downloaders.remove(downloader)
474
475 self.__reloadScript()
476
477 def __requireDownloadError(self, downloader):
478 """
479 Private slot to handle a downloader error.
480
481 @param downloader reference to the downloader object
482 @type GreaseMonkeyDownloader
483 """
484 if downloader in self.__downloaders:
485 self.__downloaders.remove(downloader)
486
487 def __downloadIcon(self):
488 """
489 Private slot to download the script icon.
490 """
491 if self.__iconUrl.isValid():
492 request = QNetworkRequest(self.__iconUrl)
493 reply = WebBrowserWindow.networkManager().get(request)
494 reply.finished.connect(lambda: self.__iconDownloaded(reply))
495 self.__iconReplies.append(reply)
496
497 def __iconDownloaded(self, reply):
498 """
499 Private slot to handle a finished download of a script icon.
500
501 @param reply reference to the network reply
502 @type QNetworkReply
503 """
504 if reply in self.__iconReplies:
505 self.__iconReplies.remove(reply)
506
507 reply.deleteLater()
508 if reply.error() == QNetworkReply.NetworkError.NoError:
509 self.__icon = QPixmap.fromImage(QImage.fromData(reply.readAll()))

eric ide

mercurial