src/eric7/WebBrowser/Passwords/PasswordManager.py

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

eric ide

mercurial