WebBrowser/Passwords/PasswordManager.py

branch
QtWebEngine
changeset 4744
ad3f6c1caf8d
parent 4743
f9e2e536d130
child 4749
750577d35452
equal deleted inserted replaced
4743:f9e2e536d130 4744:ad3f6c1caf8d
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.
42 Constructor 43 Constructor
43 44
44 @param parent reference to the parent object (QObject) 45 @param parent reference to the parent object (QObject)
45 """ 46 """
46 super(PasswordManager, self).__init__(parent) 47 super(PasswordManager, self).__init__(parent)
48
49 # setup userscript to monitor forms
50 script = QWebEngineScript()
51 script.setName("_eric_passwordmonitor")
52 script.setInjectionPoint(QWebEngineScript.DocumentReady)
53 script.setWorldId(QWebEngineScript.MainWorld)
54 script.setRunsOnSubFrames(True)
55 script.setSourceCode(Scripts.setupFormObserver())
56 profile = WebBrowser.WebBrowserWindow.WebBrowserWindow.webProfile()
57 profile.scripts().insert(script)
47 58
48 self.__logins = {} 59 self.__logins = {}
49 self.__loginForms = {} 60 self.__loginForms = {}
50 self.__never = [] 61 self.__never = []
51 self.__loaded = False 62 self.__loaded = False
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

eric ide

mercurial