eric6/WebBrowser/Passwords/PasswordManager.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2019 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
16 from PyQt5.QtWidgets import QApplication
17 from PyQt5.QtWebEngineWidgets import QWebEngineScript
18
19 from E5Gui import E5MessageBox
20 from E5Gui.E5ProgressDialog import E5ProgressDialog
21
22 from Utilities.AutoSaver import AutoSaver
23 import Utilities
24 import Utilities.crypto
25 import Preferences
26
27 import WebBrowser.WebBrowserWindow
28 from ..Tools import Scripts
29 from ..WebBrowserPage import WebBrowserPage
30
31
32 class PasswordManager(QObject):
33 """
34 Class implementing the password manager.
35
36 @signal changed() emitted to indicate a change
37 @signal passwordsSaved() emitted after the passwords were saved
38 """
39 changed = pyqtSignal()
40 passwordsSaved = pyqtSignal()
41
42 def __init__(self, parent=None):
43 """
44 Constructor
45
46 @param parent reference to the parent object (QObject)
47 """
48 super(PasswordManager, self).__init__(parent)
49
50 # setup userscript to monitor forms
51 script = QWebEngineScript()
52 script.setName("_eric_passwordmonitor")
53 script.setInjectionPoint(QWebEngineScript.DocumentReady)
54 script.setWorldId(WebBrowserPage.SafeJsWorld)
55 script.setRunsOnSubFrames(True)
56 script.setSourceCode(Scripts.setupFormObserver())
57 profile = WebBrowser.WebBrowserWindow.WebBrowserWindow.webProfile()
58 profile.scripts().insert(script)
59
60 self.__logins = {}
61 self.__loginForms = {}
62 self.__never = []
63 self.__loaded = False
64 self.__saveTimer = AutoSaver(self, self.save)
65
66 self.changed.connect(self.__saveTimer.changeOccurred)
67
68 def clear(self):
69 """
70 Public slot to clear the saved passwords.
71 """
72 if not self.__loaded:
73 self.__load()
74
75 self.__logins = {}
76 self.__loginForms = {}
77 self.__never = []
78 self.__saveTimer.changeOccurred()
79 self.__saveTimer.saveIfNeccessary()
80
81 self.changed.emit()
82
83 def getLogin(self, url, realm):
84 """
85 Public method to get the login credentials.
86
87 @param url URL to get the credentials for (QUrl)
88 @param realm realm to get the credentials for (string)
89 @return tuple containing the user name (string) and password (string)
90 """
91 if not self.__loaded:
92 self.__load()
93
94 key = self.__createKey(url, realm)
95 try:
96 return self.__logins[key][0], Utilities.crypto.pwConvert(
97 self.__logins[key][1], encode=False)
98 except KeyError:
99 return "", ""
100
101 def setLogin(self, url, realm, username, password):
102 """
103 Public method to set the login credentials.
104
105 @param url URL to set the credentials for (QUrl)
106 @param realm realm to set the credentials for (string)
107 @param username username for the login (string)
108 @param password password for the login (string)
109 """
110 if not self.__loaded:
111 self.__load()
112
113 key = self.__createKey(url, realm)
114 self.__logins[key] = (
115 username,
116 Utilities.crypto.pwConvert(password, encode=True)
117 )
118 self.changed.emit()
119
120 def __createKey(self, url, realm):
121 """
122 Private method to create the key string for the login credentials.
123
124 @param url URL to get the credentials for (QUrl)
125 @param realm realm to get the credentials for (string)
126 @return key string (string)
127 """
128 authority = url.authority()
129 if authority.startswith("@"):
130 authority = authority[1:]
131 if realm:
132 key = "{0}://{1} ({2})".format(
133 url.scheme(), authority, realm)
134 else:
135 key = "{0}://{1}".format(url.scheme(), authority)
136 return key
137
138 def getFileName(self):
139 """
140 Public method to get the file name of the passwords file.
141
142 @return name of the passwords file (string)
143 """
144 return os.path.join(Utilities.getConfigDir(),
145 "web_browser", "logins.xml")
146
147 def save(self):
148 """
149 Public slot to save the login entries to disk.
150 """
151 if not self.__loaded:
152 return
153
154 from WebBrowser.WebBrowserWindow import WebBrowserWindow
155 if not WebBrowserWindow.isPrivate():
156 from .PasswordWriter import PasswordWriter
157 loginFile = self.getFileName()
158 writer = PasswordWriter()
159 if not writer.write(
160 loginFile, self.__logins, self.__loginForms, self.__never):
161 E5MessageBox.critical(
162 None,
163 self.tr("Saving login data"),
164 self.tr(
165 """<p>Login data could not be saved to"""
166 """ <b>{0}</b></p>"""
167 ).format(loginFile))
168 else:
169 self.passwordsSaved.emit()
170
171 def __load(self):
172 """
173 Private method to load the saved login credentials.
174 """
175 if self.__loaded:
176 return
177
178 loginFile = self.getFileName()
179 if os.path.exists(loginFile):
180 from .PasswordReader import PasswordReader
181 reader = PasswordReader()
182 self.__logins, self.__loginForms, self.__never = \
183 reader.read(loginFile)
184 if reader.error() != QXmlStreamReader.NoError:
185 E5MessageBox.warning(
186 None,
187 self.tr("Loading login data"),
188 self.tr("""Error when loading login data on"""
189 """ line {0}, column {1}:\n{2}""")
190 .format(reader.lineNumber(),
191 reader.columnNumber(),
192 reader.errorString()))
193
194 self.__loaded = True
195
196 def reload(self):
197 """
198 Public method to reload the login data.
199 """
200 if not self.__loaded:
201 return
202
203 self.__loaded = False
204 self.__load()
205
206 def close(self):
207 """
208 Public method to close the passwords manager.
209 """
210 self.__saveTimer.saveIfNeccessary()
211
212 def removePassword(self, site):
213 """
214 Public method to remove a password entry.
215
216 @param site web site name (string)
217 """
218 if site in self.__logins:
219 del self.__logins[site]
220 if site in self.__loginForms:
221 del self.__loginForms[site]
222 self.changed.emit()
223
224 def allSiteNames(self):
225 """
226 Public method to get a list of all site names.
227
228 @return sorted list of all site names (list of strings)
229 """
230 if not self.__loaded:
231 self.__load()
232
233 return sorted(self.__logins.keys())
234
235 def sitesCount(self):
236 """
237 Public method to get the number of available sites.
238
239 @return number of sites (integer)
240 """
241 if not self.__loaded:
242 self.__load()
243
244 return len(self.__logins)
245
246 def siteInfo(self, site):
247 """
248 Public method to get a reference to the named site.
249
250 @param site web site name (string)
251 @return tuple containing the user name (string) and password (string)
252 """
253 if not self.__loaded:
254 self.__load()
255
256 if site not in self.__logins:
257 return None
258
259 return self.__logins[site][0], Utilities.crypto.pwConvert(
260 self.__logins[site][1], encode=False)
261
262 def formSubmitted(self, urlStr, userName, password, data, page):
263 """
264 Public method to record login data.
265
266 @param urlStr form submission URL
267 @type str
268 @param userName name of the user
269 @type str
270 @param password user password
271 @type str
272 @param data data to be submitted
273 @type QByteArray
274 @param page reference to the calling page
275 @type QWebEnginePage
276 """
277 # shall passwords be saved?
278 if not Preferences.getUser("SavePasswords"):
279 return
280
281 if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate():
282 return
283
284 if not self.__loaded:
285 self.__load()
286
287 if urlStr in self.__never:
288 return
289
290 if userName and password:
291 url = QUrl(urlStr)
292 url = self.__stripUrl(url)
293 key = self.__createKey(url, "")
294 if key not in self.__loginForms:
295 mb = E5MessageBox.E5MessageBox(
296 E5MessageBox.Question,
297 self.tr("Save password"),
298 self.tr(
299 """<b>Would you like to save this password?</b><br/>"""
300 """To review passwords you have saved and remove"""
301 """ them, use the password management dialog of the"""
302 """ Settings menu."""),
303 modal=True, parent=page.view())
304 neverButton = mb.addButton(
305 self.tr("Never for this site"),
306 E5MessageBox.DestructiveRole)
307 noButton = mb.addButton(
308 self.tr("Not now"), E5MessageBox.RejectRole)
309 mb.addButton(E5MessageBox.Yes)
310 mb.exec_()
311 if mb.clickedButton() == neverButton:
312 self.__never.append(url.toString())
313 return
314 elif mb.clickedButton() == noButton:
315 return
316
317 self.__logins[key] = \
318 (userName,
319 Utilities.crypto.pwConvert(password, encode=True))
320 from .LoginForm import LoginForm
321 form = LoginForm()
322 form.url = url
323 form.name = userName
324 form.postData = Utilities.crypto.pwConvert(
325 bytes(data).decode("utf-8"), encode=True)
326 self.__loginForms[key] = form
327 self.changed.emit()
328
329 def __stripUrl(self, url):
330 """
331 Private method to strip off all unneeded parts of a URL.
332
333 @param url URL to be stripped (QUrl)
334 @return stripped URL (QUrl)
335 """
336 cleanUrl = QUrl(url)
337 cleanUrl.setQuery("")
338 cleanUrl.setUserInfo("")
339
340 authority = cleanUrl.authority()
341 if authority.startswith("@"):
342 authority = authority[1:]
343 cleanUrl = QUrl("{0}://{1}{2}".format(
344 cleanUrl.scheme(), authority, cleanUrl.path()))
345 cleanUrl.setFragment("")
346 return cleanUrl
347
348 def completePage(self, page):
349 """
350 Public slot to complete login forms with saved data.
351
352 @param page reference to the web page (WebBrowserPage)
353 """
354 if page is None:
355 return
356
357 if not self.__loaded:
358 self.__load()
359
360 url = page.url()
361 url = self.__stripUrl(url)
362 key = self.__createKey(url, "")
363 if key not in self.__loginForms or \
364 key not in self.__logins:
365 return
366
367 form = self.__loginForms[key]
368 if form.url != url:
369 return
370
371 postData = QByteArray(Utilities.crypto.pwConvert(
372 form.postData, encode=False).encode("utf-8"))
373 script = Scripts.completeFormData(postData)
374 page.runJavaScript(script, WebBrowserPage.SafeJsWorld)
375
376 def masterPasswordChanged(self, oldPassword, newPassword):
377 """
378 Public slot to handle the change of the master password.
379
380 @param oldPassword current master password (string)
381 @param newPassword new master password (string)
382 """
383 if not self.__loaded:
384 self.__load()
385
386 progress = E5ProgressDialog(
387 self.tr("Re-encoding saved passwords..."),
388 None, 0, len(self.__logins) + len(self.__loginForms),
389 self.tr("%v/%m Passwords"),
390 QApplication.activeModalWidget())
391 progress.setMinimumDuration(0)
392 progress.setWindowTitle(self.tr("Passwords"))
393 count = 0
394
395 # step 1: do the logins
396 for key in self.__logins:
397 progress.setValue(count)
398 QCoreApplication.processEvents()
399 username, pwHash = self.__logins[key]
400 pwHash = Utilities.crypto.pwRecode(
401 pwHash, oldPassword, newPassword)
402 self.__logins[key] = (username, pwHash)
403 count += 1
404
405 # step 2: do the login forms
406 for key in self.__loginForms:
407 progress.setValue(count)
408 QCoreApplication.processEvents()
409 postData = self.__loginForms[key].postData
410 postData = Utilities.crypto.pwRecode(
411 postData, oldPassword, newPassword)
412 self.__loginForms[key].postData = postData
413 count += 1
414
415 progress.setValue(len(self.__logins) + len(self.__loginForms))
416 QCoreApplication.processEvents()
417 self.changed.emit()

eric ide

mercurial