eric7/WebBrowser/Passwords/PasswordManager.py

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

eric ide

mercurial