1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing the password manager. 8""" 9 10import os 11 12from PyQt5.QtCore import ( 13 pyqtSignal, QObject, QByteArray, QUrl, QCoreApplication, QXmlStreamReader 14) 15from PyQt5.QtWidgets import QApplication 16from PyQt5.QtWebEngineWidgets import QWebEngineScript 17 18from E5Gui import E5MessageBox 19from E5Gui.E5ProgressDialog import E5ProgressDialog 20 21from Utilities.AutoSaver import AutoSaver 22import Utilities 23import Utilities.crypto 24import Preferences 25 26import WebBrowser.WebBrowserWindow 27from ..Tools import Scripts 28from ..WebBrowserPage import WebBrowserPage 29 30 31class 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 423