WebBrowser/Passwords/PasswordManager.py

branch
QtWebEngine
changeset 4743
f9e2e536d130
parent 4631
5c1a96925da4
child 4744
ad3f6c1caf8d
equal deleted inserted replaced
4742:f9d1090f6ab9 4743:f9e2e536d130
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the password manager.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13
14 from PyQt5.QtCore import pyqtSignal, QObject, QByteArray, QUrl, \
15 QCoreApplication, QXmlStreamReader, qVersion
16 from PyQt5.QtWidgets import QApplication
17 from PyQt5.QtNetwork import QNetworkRequest
18 ##from PyQt5.QtWebKit import QWebSettings
19 ##from PyQt5.QtWebKitWidgets import QWebPage
20
21 from E5Gui import E5MessageBox
22 from E5Gui.E5ProgressDialog import E5ProgressDialog
23
24 from Utilities.AutoSaver import AutoSaver
25 import Utilities
26 import Utilities.crypto
27 import Preferences
28
29
30 class PasswordManager(QObject):
31 """
32 Class implementing the password manager.
33
34 @signal changed() emitted to indicate a change
35 @signal passwordsSaved() emitted after the passwords were saved
36 """
37 changed = pyqtSignal()
38 passwordsSaved = pyqtSignal()
39
40 def __init__(self, parent=None):
41 """
42 Constructor
43
44 @param parent reference to the parent object (QObject)
45 """
46 super(PasswordManager, self).__init__(parent)
47
48 self.__logins = {}
49 self.__loginForms = {}
50 self.__never = []
51 self.__loaded = False
52 self.__saveTimer = AutoSaver(self, self.save)
53
54 self.changed.connect(self.__saveTimer.changeOccurred)
55
56 def clear(self):
57 """
58 Public slot to clear the saved passwords.
59 """
60 if not self.__loaded:
61 self.__load()
62
63 self.__logins = {}
64 self.__loginForms = {}
65 self.__never = []
66 self.__saveTimer.changeOccurred()
67 self.__saveTimer.saveIfNeccessary()
68
69 self.changed.emit()
70
71 def getLogin(self, url, realm):
72 """
73 Public method to get the login credentials.
74
75 @param url URL to get the credentials for (QUrl)
76 @param realm realm to get the credentials for (string)
77 @return tuple containing the user name (string) and password (string)
78 """
79 if not self.__loaded:
80 self.__load()
81
82 key = self.__createKey(url, realm)
83 try:
84 return self.__logins[key][0], Utilities.crypto.pwConvert(
85 self.__logins[key][1], encode=False)
86 except KeyError:
87 return "", ""
88
89 def setLogin(self, url, realm, username, password):
90 """
91 Public method to set the login credentials.
92
93 @param url URL to set the credentials for (QUrl)
94 @param realm realm to set the credentials for (string)
95 @param username username for the login (string)
96 @param password password for the login (string)
97 """
98 if not self.__loaded:
99 self.__load()
100
101 key = self.__createKey(url, realm)
102 self.__logins[key] = (
103 username,
104 Utilities.crypto.pwConvert(password, encode=True)
105 )
106 self.changed.emit()
107
108 def __createKey(self, url, realm):
109 """
110 Private method to create the key string for the login credentials.
111
112 @param url URL to get the credentials for (QUrl)
113 @param realm realm to get the credentials for (string)
114 @return key string (string)
115 """
116 authority = url.authority()
117 if authority.startswith("@"):
118 authority = authority[1:]
119 if realm:
120 key = "{0}://{1} ({2})".format(
121 url.scheme(), authority, realm)
122 else:
123 key = "{0}://{1}".format(url.scheme(), authority)
124 return key
125
126 def getFileName(self):
127 """
128 Public method to get the file name of the passwords file.
129
130 @return name of the passwords file (string)
131 """
132 return os.path.join(Utilities.getConfigDir(),
133 "web_browser", "logins.xml")
134
135 def save(self):
136 """
137 Public slot to save the login entries to disk.
138 """
139 if not self.__loaded:
140 return
141
142 from .PasswordWriter import PasswordWriter
143 loginFile = self.getFileName()
144 writer = PasswordWriter()
145 if not writer.write(
146 loginFile, self.__logins, self.__loginForms, self.__never):
147 E5MessageBox.critical(
148 None,
149 self.tr("Saving login data"),
150 self.tr(
151 """<p>Login data could not be saved to <b>{0}</b></p>"""
152 ).format(loginFile))
153 else:
154 self.passwordsSaved.emit()
155
156 def __load(self):
157 """
158 Private method to load the saved login credentials.
159 """
160 if self.__loaded:
161 return
162
163 loginFile = self.getFileName()
164 if os.path.exists(loginFile):
165 from .PasswordReader import PasswordReader
166 reader = PasswordReader()
167 self.__logins, self.__loginForms, self.__never = \
168 reader.read(loginFile)
169 if reader.error() != QXmlStreamReader.NoError:
170 E5MessageBox.warning(
171 None,
172 self.tr("Loading login data"),
173 self.tr("""Error when loading login data on"""
174 """ line {0}, column {1}:\n{2}""")
175 .format(reader.lineNumber(),
176 reader.columnNumber(),
177 reader.errorString()))
178
179 self.__loaded = True
180
181 def reload(self):
182 """
183 Public method to reload the login data.
184 """
185 if not self.__loaded:
186 return
187
188 self.__loaded = False
189 self.__load()
190
191 def close(self):
192 """
193 Public method to close the passwords manager.
194 """
195 self.__saveTimer.saveIfNeccessary()
196
197 def removePassword(self, site):
198 """
199 Public method to remove a password entry.
200
201 @param site web site name (string)
202 """
203 if site in self.__logins:
204 del self.__logins[site]
205 if site in self.__loginForms:
206 del self.__loginForms[site]
207 self.changed.emit()
208
209 def allSiteNames(self):
210 """
211 Public method to get a list of all site names.
212
213 @return sorted list of all site names (list of strings)
214 """
215 if not self.__loaded:
216 self.__load()
217
218 return sorted(self.__logins.keys())
219
220 def sitesCount(self):
221 """
222 Public method to get the number of available sites.
223
224 @return number of sites (integer)
225 """
226 if not self.__loaded:
227 self.__load()
228
229 return len(self.__logins)
230
231 def siteInfo(self, site):
232 """
233 Public method to get a reference to the named site.
234
235 @param site web site name (string)
236 @return tuple containing the user name (string) and password (string)
237 """
238 if not self.__loaded:
239 self.__load()
240
241 if site not in self.__logins:
242 return None
243
244 return self.__logins[site][0], Utilities.crypto.pwConvert(
245 self.__logins[site][1], encode=False)
246
247 # TODO: Password Manager: processing of form data
248 def post(self, request, data):
249 """
250 Public method to check, if the data to be sent contains login data.
251
252 @param request reference to the network request (QNetworkRequest)
253 @param data data to be sent (QByteArray)
254 """
255 ## # shall passwords be saved?
256 ## if not Preferences.getUser("SavePasswords"):
257 ## return
258 ##
259 ## # observe privacy
260 ## # TODO: Privacy, i.e. isPrivate()
261 #### if QWebSettings.globalSettings().testAttribute(
262 #### QWebSettings.PrivateBrowsingEnabled):
263 #### return
264 ##
265 ## if not self.__loaded:
266 ## self.__load()
267 ##
268 ## # determine the url
269 ## refererHeader = request.rawHeader(b"Referer")
270 ## if refererHeader.isEmpty():
271 ## return
272 ## url = QUrl.fromEncoded(refererHeader)
273 ## url = self.__stripUrl(url)
274 ##
275 ## # check that url isn't in __never
276 ## if url.toString() in self.__never:
277 ## return
278 ##
279 ## # check the request type
280 ## navType = request.attribute(QNetworkRequest.User + 101)
281 ## if navType is None:
282 ## return
283 ## if navType != QWebPage.NavigationTypeFormSubmitted:
284 ## return
285 ##
286 ## # determine the QWebPage
287 ## webPage = request.attribute(QNetworkRequest.User + 100)
288 ## if webPage is None:
289 ## return
290 ##
291 ## # determine the requests content type
292 ## contentTypeHeader = request.rawHeader(b"Content-Type")
293 ## if contentTypeHeader.isEmpty():
294 ## return
295 ## multipart = contentTypeHeader.startsWith(b"multipart/form-data")
296 ## if multipart:
297 ## boundary = contentTypeHeader.split(" ")[1].split("=")[1]
298 ## else:
299 ## boundary = None
300 ##
301 ## # find the matching form on the web page
302 ## form = self.__findForm(webPage, data, boundary=boundary)
303 ## if not form.isValid():
304 ## return
305 ## form.url = QUrl(url)
306 ##
307 ## # check, if the form has a password
308 ## if not form.hasAPassword:
309 ## return
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
355 def __stripUrl(self, url):
356 """
357 Private method to strip off all unneeded parts of a URL.
358
359 @param url URL to be stripped (QUrl)
360 @return stripped URL (QUrl)
361 """
362 cleanUrl = QUrl(url)
363 cleanUrl.setQuery("")
364 cleanUrl.setUserInfo("")
365
366 authority = cleanUrl.authority()
367 if authority.startswith("@"):
368 authority = authority[1:]
369 cleanUrl = QUrl("{0}://{1}{2}".format(
370 cleanUrl.scheme(), authority, cleanUrl.path()))
371 cleanUrl.setFragment("")
372 return cleanUrl
373
374 # TODO: Password Manager: processing of form data
375 ## def __findForm(self, webPage, data, boundary=None):
376 ## """
377 ## Private method to find the form used for logging in.
378 ##
379 ## @param webPage reference to the web page (QWebPage)
380 ## @param data data to be sent (QByteArray)
381 ## @keyparam boundary boundary string (QByteArray) for multipart
382 ## encoded data, None for urlencoded data
383 ## @return parsed form (LoginForm)
384 ## """
385 ## from .LoginForm import LoginForm
386 ## form = LoginForm()
387 ## if boundary is not None:
388 ## args = self.__extractMultipartQueryItems(data, boundary)
389 ## else:
390 ## if qVersion() >= "5.0.0":
391 ## from PyQt5.QtCore import QUrlQuery
392 ## argsUrl = QUrl.fromEncoded(
393 ## QByteArray(b"foo://bar.com/?" + QUrl.fromPercentEncoding(
394 ## data.replace(b"+", b"%20")).encode("utf-8")))
395 ## encodedArgs = QUrlQuery(argsUrl).queryItems()
396 ## else:
397 ## argsUrl = QUrl.fromEncoded(
398 ## QByteArray(b"foo://bar.com/?" + data.replace(b"+", b"%20"))
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
540 def masterPasswordChanged(self, oldPassword, newPassword):
541 """
542 Public slot to handle the change of the master password.
543
544 @param oldPassword current master password (string)
545 @param newPassword new master password (string)
546 """
547 if not self.__loaded:
548 self.__load()
549
550 progress = E5ProgressDialog(
551 self.tr("Re-encoding saved passwords..."),
552 None, 0, len(self.__logins), self.tr("%v/%m Passwords"),
553 QApplication.activeModalWidget())
554 progress.setMinimumDuration(0)
555 progress.setWindowTitle(self.tr("Passwords"))
556 count = 0
557
558 for key in self.__logins:
559 progress.setValue(count)
560 QCoreApplication.processEvents()
561 username, hash = self.__logins[key]
562 hash = Utilities.crypto.pwRecode(hash, oldPassword, newPassword)
563 self.__logins[key] = (username, hash)
564 count += 1
565
566 progress.setValue(len(self.__logins))
567 QCoreApplication.processEvents()
568 self.changed.emit()

eric ide

mercurial