10 from __future__ import unicode_literals |
10 from __future__ import unicode_literals |
11 |
11 |
12 import os |
12 import os |
13 |
13 |
14 from PyQt5.QtCore import pyqtSignal, QObject, QByteArray, QUrl, \ |
14 from PyQt5.QtCore import pyqtSignal, QObject, QByteArray, QUrl, \ |
15 QCoreApplication, QXmlStreamReader, qVersion |
15 QCoreApplication, QXmlStreamReader |
16 from PyQt5.QtWidgets import QApplication |
16 from PyQt5.QtWidgets import QApplication |
17 from PyQt5.QtNetwork import QNetworkRequest |
17 from PyQt5.QtWebEngineWidgets import QWebEngineScript |
18 ##from PyQt5.QtWebKit import QWebSettings |
|
19 ##from PyQt5.QtWebKitWidgets import QWebPage |
|
20 |
18 |
21 from E5Gui import E5MessageBox |
19 from E5Gui import E5MessageBox |
22 from E5Gui.E5ProgressDialog import E5ProgressDialog |
20 from E5Gui.E5ProgressDialog import E5ProgressDialog |
23 |
21 |
24 from Utilities.AutoSaver import AutoSaver |
22 from Utilities.AutoSaver import AutoSaver |
25 import Utilities |
23 import Utilities |
26 import Utilities.crypto |
24 import Utilities.crypto |
27 import Preferences |
25 import Preferences |
|
26 |
|
27 import WebBrowser.WebBrowserWindow |
|
28 from ..Tools import Scripts |
28 |
29 |
29 |
30 |
30 class PasswordManager(QObject): |
31 class PasswordManager(QObject): |
31 """ |
32 """ |
32 Class implementing the password manager. |
33 Class implementing the password manager. |
242 return None |
253 return None |
243 |
254 |
244 return self.__logins[site][0], Utilities.crypto.pwConvert( |
255 return self.__logins[site][0], Utilities.crypto.pwConvert( |
245 self.__logins[site][1], encode=False) |
256 self.__logins[site][1], encode=False) |
246 |
257 |
247 # TODO: Password Manager: processing of form data |
258 def formSubmitted(self, urlStr, userName, password, data, page): |
248 def post(self, request, data): |
259 """ |
249 """ |
260 Public method to record login data. |
250 Public method to check, if the data to be sent contains login data. |
261 |
251 |
262 @param urlStr form submission URL |
252 @param request reference to the network request (QNetworkRequest) |
263 @type str |
253 @param data data to be sent (QByteArray) |
264 @param userName name of the user |
254 """ |
265 @type str |
255 ## # shall passwords be saved? |
266 @param password user password |
256 ## if not Preferences.getUser("SavePasswords"): |
267 @type str |
257 ## return |
268 @param data data to be submitted |
258 ## |
269 @type QByteArray |
259 ## # observe privacy |
270 @param page reference to the calling page |
260 ## # TODO: Privacy, i.e. isPrivate() |
271 @type QWrbEnginePage |
261 #### if QWebSettings.globalSettings().testAttribute( |
272 """ |
262 #### QWebSettings.PrivateBrowsingEnabled): |
273 # shall passwords be saved? |
263 #### return |
274 if not Preferences.getUser("SavePasswords"): |
264 ## |
275 return |
265 ## if not self.__loaded: |
276 |
266 ## self.__load() |
277 if WebBrowser.WebBrowserWindow.WebBrowserWindow.mainWindow()\ |
267 ## |
278 .isPrivate(): |
268 ## # determine the url |
279 return |
269 ## refererHeader = request.rawHeader(b"Referer") |
280 |
270 ## if refererHeader.isEmpty(): |
281 if urlStr in self.__never: |
271 ## return |
282 return |
272 ## url = QUrl.fromEncoded(refererHeader) |
283 |
273 ## url = self.__stripUrl(url) |
284 if userName and password: |
274 ## |
285 url = QUrl(urlStr) |
275 ## # check that url isn't in __never |
286 url = self.__stripUrl(url) |
276 ## if url.toString() in self.__never: |
287 key = self.__createKey(url, "") |
277 ## return |
288 if key not in self.__loginForms: |
278 ## |
289 mb = E5MessageBox.E5MessageBox( |
279 ## # check the request type |
290 E5MessageBox.Question, |
280 ## navType = request.attribute(QNetworkRequest.User + 101) |
291 self.tr("Save password"), |
281 ## if navType is None: |
292 self.tr( |
282 ## return |
293 """<b>Would you like to save this password?</b><br/>""" |
283 ## if navType != QWebPage.NavigationTypeFormSubmitted: |
294 """To review passwords you have saved and remove""" |
284 ## return |
295 """ them, use the password management dialog of the""" |
285 ## |
296 """ Settings menu."""), |
286 ## # determine the QWebPage |
297 modal=True, parent=page.view()) |
287 ## webPage = request.attribute(QNetworkRequest.User + 100) |
298 neverButton = mb.addButton( |
288 ## if webPage is None: |
299 self.tr("Never for this site"), |
289 ## return |
300 E5MessageBox.DestructiveRole) |
290 ## |
301 noButton = mb.addButton( |
291 ## # determine the requests content type |
302 self.tr("Not now"), E5MessageBox.RejectRole) |
292 ## contentTypeHeader = request.rawHeader(b"Content-Type") |
303 mb.addButton(E5MessageBox.Yes) |
293 ## if contentTypeHeader.isEmpty(): |
304 mb.exec_() |
294 ## return |
305 if mb.clickedButton() == neverButton: |
295 ## multipart = contentTypeHeader.startsWith(b"multipart/form-data") |
306 self.__never.append(url.toString()) |
296 ## if multipart: |
307 return |
297 ## boundary = contentTypeHeader.split(" ")[1].split("=")[1] |
308 elif mb.clickedButton() == noButton: |
298 ## else: |
309 return |
299 ## boundary = None |
310 |
300 ## |
311 self.__logins[key] = \ |
301 ## # find the matching form on the web page |
312 (userName, |
302 ## form = self.__findForm(webPage, data, boundary=boundary) |
313 Utilities.crypto.pwConvert(password, encode=True)) |
303 ## if not form.isValid(): |
314 from .LoginForm import LoginForm |
304 ## return |
315 form = LoginForm() |
305 ## form.url = QUrl(url) |
316 form.url = url |
306 ## |
317 form.name = userName |
307 ## # check, if the form has a password |
318 form.postData = QByteArray(data) |
308 ## if not form.hasAPassword: |
319 self.__loginForms[key] = form |
309 ## return |
320 self.changed.emit() |
310 ## |
|
311 ## # prompt, if the form has never be seen |
|
312 ## key = self.__createKey(url, "") |
|
313 ## if key not in self.__loginForms: |
|
314 ## mb = E5MessageBox.E5MessageBox( |
|
315 ## E5MessageBox.Question, |
|
316 ## self.tr("Save password"), |
|
317 ## self.tr( |
|
318 ## """<b>Would you like to save this password?</b><br/>""" |
|
319 ## """To review passwords you have saved and remove them, """ |
|
320 ## """use the password management dialog of the Settings""" |
|
321 ## """ menu."""), |
|
322 ## modal=True) |
|
323 ## neverButton = mb.addButton( |
|
324 ## self.tr("Never for this site"), |
|
325 ## E5MessageBox.DestructiveRole) |
|
326 ## noButton = mb.addButton( |
|
327 ## self.tr("Not now"), E5MessageBox.RejectRole) |
|
328 ## mb.addButton(E5MessageBox.Yes) |
|
329 ## mb.exec_() |
|
330 ## if mb.clickedButton() == neverButton: |
|
331 ## self.__never.append(url.toString()) |
|
332 ## return |
|
333 ## elif mb.clickedButton() == noButton: |
|
334 ## return |
|
335 ## |
|
336 ## # extract user name and password |
|
337 ## user = "" |
|
338 ## password = "" |
|
339 ## for index in range(len(form.elements)): |
|
340 ## element = form.elements[index] |
|
341 ## type_ = form.elementTypes[element[0]] |
|
342 ## if user == "" and \ |
|
343 ## type_ == "text": |
|
344 ## user = element[1] |
|
345 ## elif password == "" and \ |
|
346 ## type_ == "password": |
|
347 ## password = element[1] |
|
348 ## form.elements[index] = (element[0], "--PASSWORD--") |
|
349 ## if user and password: |
|
350 ## self.__logins[key] = \ |
|
351 ## (user, Utilities.crypto.pwConvert(password, encode=True)) |
|
352 ## self.__loginForms[key] = form |
|
353 ## self.changed.emit() |
|
354 |
321 |
355 def __stripUrl(self, url): |
322 def __stripUrl(self, url): |
356 """ |
323 """ |
357 Private method to strip off all unneeded parts of a URL. |
324 Private method to strip off all unneeded parts of a URL. |
358 |
325 |
369 cleanUrl = QUrl("{0}://{1}{2}".format( |
336 cleanUrl = QUrl("{0}://{1}{2}".format( |
370 cleanUrl.scheme(), authority, cleanUrl.path())) |
337 cleanUrl.scheme(), authority, cleanUrl.path())) |
371 cleanUrl.setFragment("") |
338 cleanUrl.setFragment("") |
372 return cleanUrl |
339 return cleanUrl |
373 |
340 |
374 # TODO: Password Manager: processing of form data |
341 def completePage(self, page): |
375 ## def __findForm(self, webPage, data, boundary=None): |
342 """ |
376 ## """ |
343 Public slot to complete login forms with saved data. |
377 ## Private method to find the form used for logging in. |
344 |
378 ## |
345 @param page reference to the web page (WebBrowserPage) |
379 ## @param webPage reference to the web page (QWebPage) |
346 """ |
380 ## @param data data to be sent (QByteArray) |
347 if page is None: |
381 ## @keyparam boundary boundary string (QByteArray) for multipart |
348 return |
382 ## encoded data, None for urlencoded data |
349 |
383 ## @return parsed form (LoginForm) |
350 if not self.__loaded: |
384 ## """ |
351 self.__load() |
385 ## from .LoginForm import LoginForm |
352 |
386 ## form = LoginForm() |
353 url = page.url() |
387 ## if boundary is not None: |
354 url = self.__stripUrl(url) |
388 ## args = self.__extractMultipartQueryItems(data, boundary) |
355 key = self.__createKey(url, "") |
389 ## else: |
356 if key not in self.__loginForms or \ |
390 ## if qVersion() >= "5.0.0": |
357 key not in self.__logins: |
391 ## from PyQt5.QtCore import QUrlQuery |
358 return |
392 ## argsUrl = QUrl.fromEncoded( |
359 |
393 ## QByteArray(b"foo://bar.com/?" + QUrl.fromPercentEncoding( |
360 form = self.__loginForms[key] |
394 ## data.replace(b"+", b"%20")).encode("utf-8"))) |
361 if form.url != url: |
395 ## encodedArgs = QUrlQuery(argsUrl).queryItems() |
362 return |
396 ## else: |
363 |
397 ## argsUrl = QUrl.fromEncoded( |
364 script = Scripts.completeFormData(form.postData) |
398 ## QByteArray(b"foo://bar.com/?" + data.replace(b"+", b"%20")) |
365 page.runJavaScript(script) |
399 ## ) |
|
400 ## encodedArgs = argsUrl.queryItems() |
|
401 ## args = set() |
|
402 ## for arg in encodedArgs: |
|
403 ## key = arg[0] |
|
404 ## value = arg[1] |
|
405 ## args.add((key, value)) |
|
406 ## |
|
407 ## # extract the forms |
|
408 ## from Helpviewer.JavaScriptResources import parseForms_js |
|
409 ## lst = webPage.mainFrame().evaluateJavaScript(parseForms_js) |
|
410 ## for map in lst: |
|
411 ## formHasPasswords = False |
|
412 ## formName = map["name"] |
|
413 ## formIndex = map["index"] |
|
414 ## if isinstance(formIndex, float) and formIndex.is_integer(): |
|
415 ## formIndex = int(formIndex) |
|
416 ## elements = map["elements"] |
|
417 ## formElements = set() |
|
418 ## formElementTypes = {} |
|
419 ## deadElements = set() |
|
420 ## for elementMap in elements: |
|
421 ## try: |
|
422 ## name = elementMap["name"] |
|
423 ## value = elementMap["value"] |
|
424 ## type_ = elementMap["type"] |
|
425 ## except KeyError: |
|
426 ## continue |
|
427 ## if type_ == "password": |
|
428 ## formHasPasswords = True |
|
429 ## t = (name, value) |
|
430 ## try: |
|
431 ## if elementMap["autocomplete"] == "off": |
|
432 ## deadElements.add(t) |
|
433 ## except KeyError: |
|
434 ## pass |
|
435 ## if name: |
|
436 ## formElements.add(t) |
|
437 ## formElementTypes[name] = type_ |
|
438 ## if formElements.intersection(args) == args: |
|
439 ## form.hasAPassword = formHasPasswords |
|
440 ## if not formName: |
|
441 ## form.name = formIndex |
|
442 ## else: |
|
443 ## form.name = formName |
|
444 ## args.difference_update(deadElements) |
|
445 ## for elt in deadElements: |
|
446 ## if elt[0] in formElementTypes: |
|
447 ## del formElementTypes[elt[0]] |
|
448 ## form.elements = list(args) |
|
449 ## form.elementTypes = formElementTypes |
|
450 ## break |
|
451 ## |
|
452 ## return form |
|
453 ## |
|
454 ## def __extractMultipartQueryItems(self, data, boundary): |
|
455 ## """ |
|
456 ## Private method to extract the query items for a post operation. |
|
457 ## |
|
458 ## @param data data to be sent (QByteArray) |
|
459 ## @param boundary boundary string (QByteArray) |
|
460 ## @return set of name, value pairs (set of tuple of string, string) |
|
461 ## """ |
|
462 ## args = set() |
|
463 ## |
|
464 ## dataStr = bytes(data).decode() |
|
465 ## boundaryStr = bytes(boundary).decode() |
|
466 ## |
|
467 ## parts = dataStr.split(boundaryStr + "\r\n") |
|
468 ## for part in parts: |
|
469 ## if part.startswith("Content-Disposition"): |
|
470 ## lines = part.split("\r\n") |
|
471 ## name = lines[0].split("=")[1][1:-1] |
|
472 ## value = lines[2] |
|
473 ## args.add((name, value)) |
|
474 ## |
|
475 ## return args |
|
476 ## |
|
477 ## def fill(self, page): |
|
478 ## """ |
|
479 ## Public slot to fill login forms with saved data. |
|
480 ## |
|
481 ## @param page reference to the web page (QWebPage) |
|
482 ## """ |
|
483 ## if page is None or page.mainFrame() is None: |
|
484 ## return |
|
485 ## |
|
486 ## if not self.__loaded: |
|
487 ## self.__load() |
|
488 ## |
|
489 ## url = page.mainFrame().url() |
|
490 ## url = self.__stripUrl(url) |
|
491 ## key = self.__createKey(url, "") |
|
492 ## if key not in self.__loginForms or \ |
|
493 ## key not in self.__logins: |
|
494 ## return |
|
495 ## |
|
496 ## form = self.__loginForms[key] |
|
497 ## if form.url != url: |
|
498 ## return |
|
499 ## |
|
500 ## if form.name == "": |
|
501 ## formName = "0" |
|
502 ## else: |
|
503 ## try: |
|
504 ## formName = "{0:d}".format(int(form.name)) |
|
505 ## except ValueError: |
|
506 ## formName = '"{0}"'.format(form.name) |
|
507 ## for element in form.elements: |
|
508 ## name = element[0] |
|
509 ## value = element[1] |
|
510 ## |
|
511 ## disabled = page.mainFrame().evaluateJavaScript( |
|
512 ## 'document.forms[{0}].elements["{1}"].disabled'.format( |
|
513 ## formName, name)) |
|
514 ## if disabled: |
|
515 ## continue |
|
516 ## |
|
517 ## readOnly = page.mainFrame().evaluateJavaScript( |
|
518 ## 'document.forms[{0}].elements["{1}"].readOnly'.format( |
|
519 ## formName, name)) |
|
520 ## if readOnly: |
|
521 ## continue |
|
522 ## |
|
523 ## type_ = page.mainFrame().evaluateJavaScript( |
|
524 ## 'document.forms[{0}].elements["{1}"].type'.format( |
|
525 ## formName, name)) |
|
526 ## if type_ == "" or \ |
|
527 ## type_ in ["hidden", "reset", "submit"]: |
|
528 ## continue |
|
529 ## if type_ == "password": |
|
530 ## value = Utilities.crypto.pwConvert( |
|
531 ## self.__logins[key][1], encode=False) |
|
532 ## setType = type_ == "checkbox" and "checked" or "value" |
|
533 ## value = value.replace("\\", "\\\\") |
|
534 ## value = value.replace('"', '\\"') |
|
535 ## javascript = \ |
|
536 ## 'document.forms[{0}].elements["{1}"].{2}="{3}";'.format( |
|
537 ## formName, name, setType, value) |
|
538 ## page.mainFrame().evaluateJavaScript(javascript) |
|
539 |
366 |
540 def masterPasswordChanged(self, oldPassword, newPassword): |
367 def masterPasswordChanged(self, oldPassword, newPassword): |
541 """ |
368 """ |
542 Public slot to handle the change of the master password. |
369 Public slot to handle the change of the master password. |
543 |
370 |