eric6/WebBrowser/GreaseMonkey/GreaseMonkeyScript.py

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

eric ide

mercurial