|
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())) |