|
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() |