Helpviewer/Passwords/PasswordManager.py

changeset 0
de9c2efb9d02
child 6
52e8c820d0dd
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the password manager.
8 """
9
10 import os
11
12 from PyQt4.QtCore import *
13 from PyQt4.QtGui import QMessageBox
14 from PyQt4.QtNetwork import QNetworkRequest
15 from PyQt4.QtWebKit import *
16
17 from Helpviewer.JavaScriptResources import parseForms_js
18
19 from Utilities.AutoSaver import AutoSaver
20 import Utilities
21 import Preferences
22
23 class LoginForm(object):
24 """
25 Class implementing a data structure for login forms.
26 """
27 def __init__(self):
28 """
29 Constructor
30 """
31 self.url = QUrl()
32 self.name = ""
33 self.hasAPassword = False
34 self.elements = [] # list of tuples of element name and value
35 # (QString, QString)
36 self.elementTypes = {} # dict of element name as key and type as value
37
38 def isValid(self):
39 """
40 Public method to test for validity.
41
42 @return flag indicating a valid form (boolean)
43 """
44 return len(self.elements) > 0
45
46 def load(self, data):
47 """
48 Public method to load the form data from a file.
49
50 @param data list of strings to load data from (list of strings)
51 @return flag indicating success (boolean)
52 """
53 self.url = QUrl(data[0])
54 self.name = data[1]
55 self.hasAPassword = data[2] == "True"
56 for element in data[3:]:
57 name, value = element.split(" = ", 1)
58 self.elements.append((name, value))
59
60 def save(self, f):
61 """
62 Public method to save the form data to a file.
63
64 @param f file or file like object open for writing
65 @return flag indicating success (booelan)
66 """
67 f.write("%s\n" % self.url.toString())
68 f.write("%s\n" % self.name)
69 f.write("%s\n" % self.hasAPassword)
70 for element in self.elements:
71 f.write("%s = %s\n" % (element[0], element[1]))
72
73 class PasswordManager(QObject):
74 """
75 Class implementing the password manager.
76
77 @signal changed() emitted to indicate a change
78 """
79 SEPARATOR = "===================="
80 FORMS = "=====FORMS====="
81 NEVER = "=====NEVER====="
82
83 def __init__(self, parent = None):
84 """
85 Constructor
86
87 @param parent reference to the parent object (QObject)
88 """
89 QObject.__init__(self, parent)
90
91 self.__logins = {}
92 self.__loginForms = {}
93 self.__never = []
94 self.__loaded = False
95 self.__saveTimer = AutoSaver(self, self.save)
96
97 self.connect(self, SIGNAL("changed()"), self.__saveTimer.changeOccurred)
98
99 def clear(self):
100 """
101 Public slot to clear the saved passwords.
102 """
103 if not self.__loaded:
104 self.__load()
105
106 self.__logins = {}
107 self.__loginForms = {}
108 self.__never = []
109 self.__saveTimer.changeOccurred()
110 self.__saveTimer.saveIfNeccessary()
111
112 self.emit(SIGNAL("changed()"))
113
114 def getLogin(self, url, realm):
115 """
116 Public method to get the login credentials.
117
118 @param url URL to get the credentials for (QUrl)
119 @param realm realm to get the credentials for (string)
120 @return tuple containing the user name (string) and password (string)
121 """
122 if not self.__loaded:
123 self.__load()
124
125 key = self.__createKey(url, realm)
126 try:
127 return self.__logins[key][0], Utilities.pwDecode(self.__logins[key][1])
128 except KeyError:
129 return "", ""
130
131 def setLogin(self, url, realm, username, password):
132 """
133 Public method to set the login credentials.
134
135 @param url URL to set the credentials for (QUrl)
136 @param realm realm to set the credentials for (string)
137 @param username username for the login (string)
138 @param password password for the login (string)
139 """
140 if not self.__loaded:
141 self.__load()
142
143 key = self.__createKey(url, realm)
144 self.__logins[key] = (username, Utilities.pwEncode(password))
145 self.emit(SIGNAL("changed()"))
146
147 def __createKey(self, url, realm):
148 """
149 Private method to create the key string for the login credentials.
150
151 @param url URL to get the credentials for (QUrl)
152 @param realm realm to get the credentials for (string)
153 @return key string (string)
154 """
155 if realm:
156 key = "%s://%s (%s)" % (url.scheme(), url.authority(), realm)
157 else:
158 key = "%s://%s" % (url.scheme(), url.authority())
159 return key
160
161 def save(self):
162 """
163 Public slot to save the login entries to disk.
164 """
165 if not self.__loaded:
166 return
167
168 loginFile = os.path.join(Utilities.getConfigDir(), "browser", "logins")
169 try:
170 f = open(loginFile, "w")
171 for key, login in self.__logins.items():
172 f.write("%s\n" % key)
173 f.write("%s\n" % login[0])
174 f.write("%s\n" % login[1])
175 f.write("%s\n" % self.SEPARATOR)
176 if self.__loginForms:
177 f.write("%s\n" % self.FORMS)
178 for key, form in self.__loginForms.items():
179 f.write("%s\n" % key)
180 form.save(f)
181 f.write("%s\n" % self.SEPARATOR)
182 if self.__never:
183 f.write("%s\n" % self.NEVER)
184 for key in self.__never:
185 f.write("%s\n") % key
186 f.close()
187 except IOError, err:
188 QMessageBox.critical(None,
189 self.trUtf8("Saving login data"),
190 self.trUtf8("""<p>Login data could not be saved to <b>{0}</b></p>"""
191 """<p>Reason: {1}</p>""").format(loginFile, unicode(err)))
192 return
193
194 def __load(self):
195 """
196 Private method to load the saved login credentials.
197 """
198 loginFile = os.path.join(Utilities.getConfigDir(), "browser", "logins")
199 if os.path.exists(loginFile):
200 try:
201 f = open(loginFile, "r")
202 lines = f.read()
203 f.close()
204 except IOError, err:
205 QMessageBox.critical(None,
206 self.trUtf8("Loading login data"),
207 self.trUtf8("""<p>Login data could not be loaded """
208 """from <b>{0}</b></p>"""
209 """<p>Reason: {1}</p>""")\
210 .format(loginFile, unicode(err)))
211 return
212
213 data = []
214 section = 0 # 0 = login data, 1 = forms data, 2 = never store info
215 for line in lines.splitlines():
216 if line == self.FORMS:
217 section = 1
218 continue
219 elif line == self.NEVER:
220 section = 2
221 continue
222
223 if section == 0:
224 if line != self.SEPARATOR:
225 data.append(line)
226 else:
227 if len(data) != 3:
228 QMessageBox.critical(None,
229 self.trUtf8("Loading login data"),
230 self.trUtf8("""<p>Login data could not be loaded """
231 """from <b>{0}</b></p>"""
232 """<p>Reason: Wrong input format</p>""")\
233 .format(loginFile))
234 return
235 self.__logins[data[0]] = (data[1], data[2])
236 data = []
237
238 elif section == 1:
239 if line != self.SEPARATOR:
240 data.append(line)
241 else:
242 key = data[0]
243 form = LoginForm()
244 form.load(data[1:])
245 self.__loginForms[key] = form
246 data = []
247
248 elif section == 2:
249 self.__never.append(line)
250
251 self.__loaded = True
252
253 def close(self):
254 """
255 Public method to close the open search engines manager.
256 """
257 self.__saveTimer.saveIfNeccessary()
258
259 def removePassword(self, site):
260 """
261 Public method to remove a password entry.
262
263 @param site web site name (string)
264 """
265 if site in self.__logins:
266 del self.__logins[site]
267 if site in self.__loginForms:
268 del self.__loginForms[site]
269 self.emit(SIGNAL("changed()"))
270
271 def allSiteNames(self):
272 """
273 Public method to get a list of all site names.
274
275 @return sorted list of all site names (list of strings)
276 """
277 if not self.__loaded:
278 self.__load()
279
280 return sorted(self.__logins.keys())
281
282 def sitesCount(self):
283 """
284 Public method to get the number of available sites.
285
286 @return number of sites (integer)
287 """
288 if not self.__loaded:
289 self.__load()
290
291 return len(self.__logins)
292
293 def siteInfo(self, site):
294 """
295 Public method to get a reference to the named site.
296
297 @param site web site name (string)
298 @return tuple containing the user name (string) and password (string)
299 """
300 if not self.__loaded:
301 self.__load()
302
303 if site not in self.__logins:
304 return None
305
306 return self.__logins[site][0], Utilities.pwDecode(self.__logins[site][1])
307
308 def post(self, request, data):
309 """
310 Public method to check, if the data to be sent contains login data.
311
312 @param request reference to the network request (QNetworkRequest)
313 @param data data to be sent (QByteArray)
314 """
315 # shall passwords be saved?
316 if not Preferences.getHelp("SavePasswords"):
317 return
318
319 # observe privacy
320 if QWebSettings.globalSettings().testAttribute(
321 QWebSettings.PrivateBrowsingEnabled):
322 return
323
324 if not self.__loaded:
325 self.__load()
326
327 # determine the url
328 refererHeader = request.rawHeader("Referer")
329 if refererHeader.isEmpty():
330 return
331 url = QUrl.fromEncoded(refererHeader)
332 url = self.__stripUrl(url)
333
334 # check that url isn't in __never
335 if url.toString() in self.__never:
336 return
337
338 # check the request type
339 v = request.attribute(QNetworkRequest.User + 101)
340 if not v.isValid():
341 return
342 navType = v.toInt()[0]
343 if navType != QWebPage.NavigationTypeFormSubmitted:
344 return
345
346 # determine the QWebPage
347 v = request.attribute(QNetworkRequest.User + 100)
348 webPage = v.toPyObject()
349 if webPage == NotImplemented or webPage is None:
350 return
351
352 # determine the requests content type
353 contentTypeHeader = request.rawHeader("Content-Type")
354 if contentTypeHeader.isEmpty():
355 return
356 multipart = contentTypeHeader.startsWith("multipart/form-data")
357 if multipart:
358 boundary = contentTypeHeader.split(" ")[1].split("=")[1]
359 else:
360 boundary = None
361
362 # find the matching form on the web page
363 form = self.__findForm(webPage, data, boundary = boundary)
364 if not form.isValid():
365 return
366 form.url = QUrl(url)
367
368 # check, if the form has a password
369 if not form.hasAPassword:
370 return
371
372 # prompt, if the form has never be seen
373 key = self.__createKey(url, "")
374 if key not in self.__loginForms:
375 mb = QMessageBox()
376 mb.setText(self.trUtf8(
377 """<b>Would you like to save this password?</b><br/>"""
378 """To review passwords you have saved and remove them, """
379 """use the password management dialog of the Settings menu."""
380 ))
381 neverButton = mb.addButton(
382 self.trUtf8("Never for this site"), QMessageBox.DestructiveRole)
383 noButton = mb.addButton(self.trUtf8("Not now"), QMessageBox.RejectRole)
384 yesButton = mb.addButton(QMessageBox.Yes)
385 mb.exec_()
386 if mb.clickedButton() == neverButton:
387 self.__never.append(unicode(url.toString()))
388 return
389 elif mb.clickedButton() == noButton:
390 return
391
392 # extract user name and password
393 user = ""
394 password = ""
395 for index in range(len(form.elements)):
396 element = form.elements[index]
397 name = element[0].lower()
398 type_ = form.elementTypes[element[0]]
399 if user == "" and \
400 type_ == "text":
401 user = element[1]
402 elif password == "" and \
403 type_ == "password":
404 password = element[1]
405 form.elements[index] = (element[0], "--PASSWORD--")
406 if user and password:
407 self.__logins[key] = (user, Utilities.pwEncode(password))
408 self.__loginForms[key] = form
409 self.emit(SIGNAL("changed()"))
410
411 def __stripUrl(self, url):
412 """
413 Private method to strip off all unneeded parts of a URL.
414
415 @param url URL to be stripped (QUrl)
416 @return stripped URL (QUrl)
417 """
418 cleanUrl = QUrl(url)
419 cleanUrl.setQueryItems([])
420 cleanUrl.setFragment("")
421 cleanUrl.setUserInfo("")
422 return cleanUrl
423
424 def __findForm(self, webPage, data, boundary = None):
425 """
426 Private method to find the form used for logging in.
427
428 @param webPage reference to the web page (QWebPage)
429 @param data data to be sent (QByteArray)
430 @keyparam boundary boundary string (QByteArray) for multipart encoded data,
431 None for urlencoded data
432 @return parsed form (LoginForm)
433 """
434 form = LoginForm()
435 if boundary is not None:
436 args = self.__extractMultipartQueryItems(data, boundary)
437 else:
438 argsUrl = QUrl.fromEncoded(QByteArray("foo://bar.com/?" + data))
439 encodedArgs = argsUrl.queryItems()
440 args = set()
441 for arg in encodedArgs:
442 key = arg[0]
443 value = arg[1].replace("+", " ")
444 args.add((key, value))
445
446 # extract the forms
447 lst = webPage.mainFrame().evaluateJavaScript(parseForms_js).toList()
448 for formVariant in lst:
449 map = formVariant.toMap()
450 formHasPasswords = False
451 formName = map["name"].toString()
452 formIndex = map["index"].toInt()[0]
453 elements = map["elements"].toList()
454 formElements = set()
455 formElementTypes = {}
456 deadElements = set()
457 for element in elements:
458 elementMap = element.toMap()
459 name = elementMap["name"].toString()
460 value = elementMap["value"].toString()
461 type_ = elementMap["type"].toString()
462 if type_ == "password":
463 formHasPasswords = True
464 t = (name, value)
465 try:
466 if elementMap["autocomplete"].toString() == "off":
467 deadElements.add(t)
468 except KeyError:
469 pass
470 if name:
471 formElements.add(t)
472 formElementTypes[name] = type_
473 if formElements.intersection(args) == args:
474 form.hasAPassword = formHasPasswords
475 if not formName:
476 form.name = formIndex
477 else:
478 form.name = formName
479 args.difference_update(deadElements)
480 for elt in deadElements:
481 if elt[0] in formElementTypes:
482 del formElementTypes[elt[0]]
483 form.elements = list(args)
484 form.elementTypes = formElementTypes
485 break
486
487 return form
488
489 def __extractMultipartQueryItems(self, data, boundary):
490 """
491 Private method to extract the query items for a post operation.
492
493 @param data data to be sent (QByteArray)
494 @param boundary boundary string (QByteArray)
495 @return set of name, value pairs (set of tuple of string, string)
496 """
497 args = set()
498
499 dataStr = unicode(data)
500 boundaryStr = unicode(boundary)
501
502 parts = dataStr.split(boundaryStr + "\r\n")
503 for part in parts:
504 if part.startswith("Content-Disposition"):
505 lines = part.split("\r\n")
506 name = lines[0].split("=")[1][1:-1]
507 value = lines[2]
508 args.add((name, value))
509
510 return args
511
512 def fill(self, page):
513 """
514 Public slot to fill login forms with saved data.
515
516 @param page reference to the web page (QWebPage)
517 """
518 if page is None or page.mainFrame() is None:
519 return
520
521 if not self.__loaded:
522 self.__load()
523
524 url = page.mainFrame().url()
525 url = self.__stripUrl(url)
526 key = self.__createKey(url, "")
527 if key not in self.__loginForms or \
528 key not in self.__logins:
529 return
530
531 form = self.__loginForms[key]
532 if form.url != url:
533 return
534
535 if form.name == "":
536 formName = "0"
537 else:
538 try:
539 formName = "%d" % int(form.name)
540 except ValueError:
541 formName = '"%s"' % form.name
542 for element in form.elements:
543 name = element[0]
544 value = element[1]
545
546 disabled = page.mainFrame().evaluateJavaScript(
547 'document.forms[%s].elements["%s"].disabled' % (formName, name)).toBool()
548 if disabled:
549 continue
550
551 readOnly = page.mainFrame().evaluateJavaScript(
552 'document.forms[%s].elements["%s"].readOnly' % (formName, name)).toBool()
553 if readOnly:
554 continue
555
556 type_ = page.mainFrame().evaluateJavaScript(
557 'document.forms[%s].elements["%s"].type' % (formName, name)).toString()
558 if type_ == "" or \
559 type_ in ["hidden", "reset", "submit"]:
560 continue
561 if type_ == "password":
562 value = Utilities.pwDecode(self.__logins[key][1])
563 setType = type_ == "checkbox" and "checked" or "value"
564 value = value.replace("\\", "\\\\")
565 value = value.replace('"', '\\"')
566 javascript = 'document.forms[%s].elements["%s"].%s="%s";' % \
567 (formName, name, setType, value)
568 page.mainFrame().evaluateJavaScript(javascript)

eric ide

mercurial