Mon, 15 Feb 2016 20:01:02 +0100
Continued porting the web browser.
- started adding the passwords stuff
--- a/WebBrowser/Network/NetworkManager.py Sun Feb 14 19:34:05 2016 +0100 +++ b/WebBrowser/Network/NetworkManager.py Mon Feb 15 20:01:02 2016 +0100 @@ -9,10 +9,15 @@ from __future__ import unicode_literals -from PyQt5.QtNetwork import QNetworkAccessManager +from PyQt5.QtWidgets import QDialog +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkProxy from E5Gui import E5MessageBox +from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired + +import Preferences + class NetworkManager(QNetworkAccessManager): """ @@ -29,10 +34,11 @@ self.__ignoredSslErrors = {} # dictionary of temporarily ignore SSL errors - # TODO: Proxy Authentication -## self.proxyAuthenticationRequired.connect(proxyAuthenticationRequired) - # TODO: Authentication -## self.authenticationRequired.connect(self.authenticationRequired) + self.proxyAuthenticationRequired.connect( + lambda proxy, auth: self.proxyAuthentication( + proxy.hostName(), auth)) + self.authenticationRequired.connect( + lambda reply, auth: self.authentication(reply.url(), auth)) def certificateError(self, error, view): """ @@ -68,3 +74,62 @@ return True return False + + def authentication(self, url, auth): + """ + Public slot to handle an authentication request. + + @param url URL requesting authentication (QUrl) + @param auth reference to the authenticator object (QAuthenticator) + """ + urlRoot = "{0}://{1}"\ + .format(url.scheme(), url.authority()) + realm = auth.realm() + if not realm and 'realm' in auth.options(): + realm = auth.option("realm") + if realm: + info = self.tr("<b>Enter username and password for '{0}', " + "realm '{1}'</b>").format(urlRoot, realm) + else: + info = self.tr("<b>Enter username and password for '{0}'</b>")\ + .format(urlRoot) + + from UI.AuthenticationDialog import AuthenticationDialog + # TODO: Password Manager +## import WebBrowser.WebBrowserWindow + + dlg = AuthenticationDialog(info, auth.user(), + Preferences.getUser("SavePasswords"), + Preferences.getUser("SavePasswords")) + # TODO: Password Manager +## if Preferences.getUser("SavePasswords"): +## username, password = \ +## WebBrowser.WebBrowserWindow.WebBrowserWindow.passwordManager()\ +## .getLogin(url, realm) +## if username: +## dlg.setData(username, password) + if dlg.exec_() == QDialog.Accepted: + username, password = dlg.getData() + auth.setUser(username) + auth.setPassword(password) + # TODO: Password Manager +## if Preferences.getUser("SavePasswords"): +## WebBrowser.WebBrowserWindow.WebBrowserWindow.passwordManager()\ +## .setLogin(url, realm, username, password) + + def proxyAuthentication(self, hostname, auth): + """ + Public slot to handle a proxy authentication request. + + @param hostname name of the proxy host + @type str + @param auth reference to the authenticator object + @type QAuthenticator + """ + proxy = QNetworkProxy.applicationProxy() + if proxy.user() and proxy.password(): + auth.setUser(proxy.user()) + auth.setPassword(proxy.password()) + return + + proxyAuthenticationRequired(proxy, auth)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/LoginForm.py Mon Feb 15 20:01:02 2016 +0100 @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a data structure for login forms. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QUrl + + +class LoginForm(object): + """ + Class implementing a data structure for login forms. + """ + def __init__(self): + """ + Constructor + """ + self.url = QUrl() + self.name = "" + self.hasAPassword = False + self.elements = [] + # list of tuples of element name and value (string, string) + self.elementTypes = {} + # dict of element name as key and type as value + + def isValid(self): + """ + Public method to test for validity. + + @return flag indicating a valid form (boolean) + """ + return len(self.elements) > 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordManager.py Mon Feb 15 20:01:02 2016 +0100 @@ -0,0 +1,568 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the password manager. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QObject, QByteArray, QUrl, \ + QCoreApplication, QXmlStreamReader, qVersion +from PyQt5.QtWidgets import QApplication +from PyQt5.QtNetwork import QNetworkRequest +##from PyQt5.QtWebKit import QWebSettings +##from PyQt5.QtWebKitWidgets import QWebPage + +from E5Gui import E5MessageBox +from E5Gui.E5ProgressDialog import E5ProgressDialog + +from Utilities.AutoSaver import AutoSaver +import Utilities +import Utilities.crypto +import Preferences + + +class PasswordManager(QObject): + """ + Class implementing the password manager. + + @signal changed() emitted to indicate a change + @signal passwordsSaved() emitted after the passwords were saved + """ + changed = pyqtSignal() + passwordsSaved = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(PasswordManager, self).__init__(parent) + + self.__logins = {} + self.__loginForms = {} + self.__never = [] + self.__loaded = False + self.__saveTimer = AutoSaver(self, self.save) + + self.changed.connect(self.__saveTimer.changeOccurred) + + def clear(self): + """ + Public slot to clear the saved passwords. + """ + if not self.__loaded: + self.__load() + + self.__logins = {} + self.__loginForms = {} + self.__never = [] + self.__saveTimer.changeOccurred() + self.__saveTimer.saveIfNeccessary() + + self.changed.emit() + + def getLogin(self, url, realm): + """ + Public method to get the login credentials. + + @param url URL to get the credentials for (QUrl) + @param realm realm to get the credentials for (string) + @return tuple containing the user name (string) and password (string) + """ + if not self.__loaded: + self.__load() + + key = self.__createKey(url, realm) + try: + return self.__logins[key][0], Utilities.crypto.pwConvert( + self.__logins[key][1], encode=False) + except KeyError: + return "", "" + + def setLogin(self, url, realm, username, password): + """ + Public method to set the login credentials. + + @param url URL to set the credentials for (QUrl) + @param realm realm to set the credentials for (string) + @param username username for the login (string) + @param password password for the login (string) + """ + if not self.__loaded: + self.__load() + + key = self.__createKey(url, realm) + self.__logins[key] = ( + username, + Utilities.crypto.pwConvert(password, encode=True) + ) + self.changed.emit() + + def __createKey(self, url, realm): + """ + Private method to create the key string for the login credentials. + + @param url URL to get the credentials for (QUrl) + @param realm realm to get the credentials for (string) + @return key string (string) + """ + authority = url.authority() + if authority.startswith("@"): + authority = authority[1:] + if realm: + key = "{0}://{1} ({2})".format( + url.scheme(), authority, realm) + else: + key = "{0}://{1}".format(url.scheme(), authority) + return key + + def getFileName(self): + """ + Public method to get the file name of the passwords file. + + @return name of the passwords file (string) + """ + return os.path.join(Utilities.getConfigDir(), + "web_browser", "logins.xml") + + def save(self): + """ + Public slot to save the login entries to disk. + """ + if not self.__loaded: + return + + from .PasswordWriter import PasswordWriter + loginFile = self.getFileName() + writer = PasswordWriter() + if not writer.write( + loginFile, self.__logins, self.__loginForms, self.__never): + E5MessageBox.critical( + None, + self.tr("Saving login data"), + self.tr( + """<p>Login data could not be saved to <b>{0}</b></p>""" + ).format(loginFile)) + else: + self.passwordsSaved.emit() + + def __load(self): + """ + Private method to load the saved login credentials. + """ + if self.__loaded: + return + + loginFile = self.getFileName() + if os.path.exists(loginFile): + from .PasswordReader import PasswordReader + reader = PasswordReader() + self.__logins, self.__loginForms, self.__never = \ + reader.read(loginFile) + if reader.error() != QXmlStreamReader.NoError: + E5MessageBox.warning( + None, + self.tr("Loading login data"), + self.tr("""Error when loading login data on""" + """ line {0}, column {1}:\n{2}""") + .format(reader.lineNumber(), + reader.columnNumber(), + reader.errorString())) + + self.__loaded = True + + def reload(self): + """ + Public method to reload the login data. + """ + if not self.__loaded: + return + + self.__loaded = False + self.__load() + + def close(self): + """ + Public method to close the passwords manager. + """ + self.__saveTimer.saveIfNeccessary() + + def removePassword(self, site): + """ + Public method to remove a password entry. + + @param site web site name (string) + """ + if site in self.__logins: + del self.__logins[site] + if site in self.__loginForms: + del self.__loginForms[site] + self.changed.emit() + + def allSiteNames(self): + """ + Public method to get a list of all site names. + + @return sorted list of all site names (list of strings) + """ + if not self.__loaded: + self.__load() + + return sorted(self.__logins.keys()) + + def sitesCount(self): + """ + Public method to get the number of available sites. + + @return number of sites (integer) + """ + if not self.__loaded: + self.__load() + + return len(self.__logins) + + def siteInfo(self, site): + """ + Public method to get a reference to the named site. + + @param site web site name (string) + @return tuple containing the user name (string) and password (string) + """ + if not self.__loaded: + self.__load() + + if site not in self.__logins: + return None + + return self.__logins[site][0], Utilities.crypto.pwConvert( + self.__logins[site][1], encode=False) + + # TODO: Password Manager: processing of form data + def post(self, request, data): + """ + Public method to check, if the data to be sent contains login data. + + @param request reference to the network request (QNetworkRequest) + @param data data to be sent (QByteArray) + """ +## # shall passwords be saved? +## if not Preferences.getUser("SavePasswords"): +## return +## +## # observe privacy +## # TODO: Privacy, i.e. isPrivate() +#### if QWebSettings.globalSettings().testAttribute( +#### QWebSettings.PrivateBrowsingEnabled): +#### return +## +## if not self.__loaded: +## self.__load() +## +## # determine the url +## refererHeader = request.rawHeader(b"Referer") +## if refererHeader.isEmpty(): +## return +## url = QUrl.fromEncoded(refererHeader) +## url = self.__stripUrl(url) +## +## # check that url isn't in __never +## if url.toString() in self.__never: +## return +## +## # check the request type +## navType = request.attribute(QNetworkRequest.User + 101) +## if navType is None: +## return +## if navType != QWebPage.NavigationTypeFormSubmitted: +## return +## +## # determine the QWebPage +## webPage = request.attribute(QNetworkRequest.User + 100) +## if webPage is None: +## return +## +## # determine the requests content type +## contentTypeHeader = request.rawHeader(b"Content-Type") +## if contentTypeHeader.isEmpty(): +## return +## multipart = contentTypeHeader.startsWith(b"multipart/form-data") +## if multipart: +## boundary = contentTypeHeader.split(" ")[1].split("=")[1] +## else: +## boundary = None +## +## # find the matching form on the web page +## form = self.__findForm(webPage, data, boundary=boundary) +## if not form.isValid(): +## return +## form.url = QUrl(url) +## +## # check, if the form has a password +## if not form.hasAPassword: +## return +## +## # prompt, if the form has never be seen +## key = self.__createKey(url, "") +## if key not in self.__loginForms: +## mb = E5MessageBox.E5MessageBox( +## E5MessageBox.Question, +## self.tr("Save password"), +## self.tr( +## """<b>Would you like to save this password?</b><br/>""" +## """To review passwords you have saved and remove them, """ +## """use the password management dialog of the Settings""" +## """ menu."""), +## modal=True) +## neverButton = mb.addButton( +## self.tr("Never for this site"), +## E5MessageBox.DestructiveRole) +## noButton = mb.addButton( +## self.tr("Not now"), E5MessageBox.RejectRole) +## mb.addButton(E5MessageBox.Yes) +## mb.exec_() +## if mb.clickedButton() == neverButton: +## self.__never.append(url.toString()) +## return +## elif mb.clickedButton() == noButton: +## return +## +## # extract user name and password +## user = "" +## password = "" +## for index in range(len(form.elements)): +## element = form.elements[index] +## type_ = form.elementTypes[element[0]] +## if user == "" and \ +## type_ == "text": +## user = element[1] +## elif password == "" and \ +## type_ == "password": +## password = element[1] +## form.elements[index] = (element[0], "--PASSWORD--") +## if user and password: +## self.__logins[key] = \ +## (user, Utilities.crypto.pwConvert(password, encode=True)) +## self.__loginForms[key] = form +## self.changed.emit() + + def __stripUrl(self, url): + """ + Private method to strip off all unneeded parts of a URL. + + @param url URL to be stripped (QUrl) + @return stripped URL (QUrl) + """ + cleanUrl = QUrl(url) + cleanUrl.setQuery("") + cleanUrl.setUserInfo("") + + authority = cleanUrl.authority() + if authority.startswith("@"): + authority = authority[1:] + cleanUrl = QUrl("{0}://{1}{2}".format( + cleanUrl.scheme(), authority, cleanUrl.path())) + cleanUrl.setFragment("") + return cleanUrl + + # TODO: Password Manager: processing of form data +## def __findForm(self, webPage, data, boundary=None): +## """ +## Private method to find the form used for logging in. +## +## @param webPage reference to the web page (QWebPage) +## @param data data to be sent (QByteArray) +## @keyparam boundary boundary string (QByteArray) for multipart +## encoded data, None for urlencoded data +## @return parsed form (LoginForm) +## """ +## from .LoginForm import LoginForm +## form = LoginForm() +## if boundary is not None: +## args = self.__extractMultipartQueryItems(data, boundary) +## else: +## if qVersion() >= "5.0.0": +## from PyQt5.QtCore import QUrlQuery +## argsUrl = QUrl.fromEncoded( +## QByteArray(b"foo://bar.com/?" + QUrl.fromPercentEncoding( +## data.replace(b"+", b"%20")).encode("utf-8"))) +## encodedArgs = QUrlQuery(argsUrl).queryItems() +## else: +## argsUrl = QUrl.fromEncoded( +## QByteArray(b"foo://bar.com/?" + data.replace(b"+", b"%20")) +## ) +## encodedArgs = argsUrl.queryItems() +## args = set() +## for arg in encodedArgs: +## key = arg[0] +## value = arg[1] +## args.add((key, value)) +## +## # extract the forms +## from Helpviewer.JavaScriptResources import parseForms_js +## lst = webPage.mainFrame().evaluateJavaScript(parseForms_js) +## for map in lst: +## formHasPasswords = False +## formName = map["name"] +## formIndex = map["index"] +## if isinstance(formIndex, float) and formIndex.is_integer(): +## formIndex = int(formIndex) +## elements = map["elements"] +## formElements = set() +## formElementTypes = {} +## deadElements = set() +## for elementMap in elements: +## try: +## name = elementMap["name"] +## value = elementMap["value"] +## type_ = elementMap["type"] +## except KeyError: +## continue +## if type_ == "password": +## formHasPasswords = True +## t = (name, value) +## try: +## if elementMap["autocomplete"] == "off": +## deadElements.add(t) +## except KeyError: +## pass +## if name: +## formElements.add(t) +## formElementTypes[name] = type_ +## if formElements.intersection(args) == args: +## form.hasAPassword = formHasPasswords +## if not formName: +## form.name = formIndex +## else: +## form.name = formName +## args.difference_update(deadElements) +## for elt in deadElements: +## if elt[0] in formElementTypes: +## del formElementTypes[elt[0]] +## form.elements = list(args) +## form.elementTypes = formElementTypes +## break +## +## return form +## +## def __extractMultipartQueryItems(self, data, boundary): +## """ +## Private method to extract the query items for a post operation. +## +## @param data data to be sent (QByteArray) +## @param boundary boundary string (QByteArray) +## @return set of name, value pairs (set of tuple of string, string) +## """ +## args = set() +## +## dataStr = bytes(data).decode() +## boundaryStr = bytes(boundary).decode() +## +## parts = dataStr.split(boundaryStr + "\r\n") +## for part in parts: +## if part.startswith("Content-Disposition"): +## lines = part.split("\r\n") +## name = lines[0].split("=")[1][1:-1] +## value = lines[2] +## args.add((name, value)) +## +## return args +## +## def fill(self, page): +## """ +## Public slot to fill login forms with saved data. +## +## @param page reference to the web page (QWebPage) +## """ +## if page is None or page.mainFrame() is None: +## return +## +## if not self.__loaded: +## self.__load() +## +## url = page.mainFrame().url() +## url = self.__stripUrl(url) +## key = self.__createKey(url, "") +## if key not in self.__loginForms or \ +## key not in self.__logins: +## return +## +## form = self.__loginForms[key] +## if form.url != url: +## return +## +## if form.name == "": +## formName = "0" +## else: +## try: +## formName = "{0:d}".format(int(form.name)) +## except ValueError: +## formName = '"{0}"'.format(form.name) +## for element in form.elements: +## name = element[0] +## value = element[1] +## +## disabled = page.mainFrame().evaluateJavaScript( +## 'document.forms[{0}].elements["{1}"].disabled'.format( +## formName, name)) +## if disabled: +## continue +## +## readOnly = page.mainFrame().evaluateJavaScript( +## 'document.forms[{0}].elements["{1}"].readOnly'.format( +## formName, name)) +## if readOnly: +## continue +## +## type_ = page.mainFrame().evaluateJavaScript( +## 'document.forms[{0}].elements["{1}"].type'.format( +## formName, name)) +## if type_ == "" or \ +## type_ in ["hidden", "reset", "submit"]: +## continue +## if type_ == "password": +## value = Utilities.crypto.pwConvert( +## self.__logins[key][1], encode=False) +## setType = type_ == "checkbox" and "checked" or "value" +## value = value.replace("\\", "\\\\") +## value = value.replace('"', '\\"') +## javascript = \ +## 'document.forms[{0}].elements["{1}"].{2}="{3}";'.format( +## formName, name, setType, value) +## page.mainFrame().evaluateJavaScript(javascript) + + def masterPasswordChanged(self, oldPassword, newPassword): + """ + Public slot to handle the change of the master password. + + @param oldPassword current master password (string) + @param newPassword new master password (string) + """ + if not self.__loaded: + self.__load() + + progress = E5ProgressDialog( + self.tr("Re-encoding saved passwords..."), + None, 0, len(self.__logins), self.tr("%v/%m Passwords"), + QApplication.activeModalWidget()) + progress.setMinimumDuration(0) + progress.setWindowTitle(self.tr("Passwords")) + count = 0 + + for key in self.__logins: + progress.setValue(count) + QCoreApplication.processEvents() + username, hash = self.__logins[key] + hash = Utilities.crypto.pwRecode(hash, oldPassword, newPassword) + self.__logins[key] = (username, hash) + count += 1 + + progress.setValue(len(self.__logins)) + QCoreApplication.processEvents() + self.changed.emit()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordModel.py Mon Feb 15 20:01:02 2016 +0100 @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a model for password management. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QModelIndex, QAbstractTableModel + + +class PasswordModel(QAbstractTableModel): + """ + Class implementing a model for password management. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the password manager (PasswordManager) + @param parent reference to the parent object (QObject) + """ + super(PasswordModel, self).__init__(parent) + + self.__manager = manager + manager.changed.connect(self.__passwordsChanged) + + self.__headers = [ + self.tr("Website"), + self.tr("Username"), + self.tr("Password") + ] + + self.__showPasswords = False + + def setShowPasswords(self, on): + """ + Public methods to show passwords. + + @param on flag indicating if passwords shall be shown (boolean) + """ + self.__showPasswords = on + self.beginResetModel() + self.endResetModel() + + def showPasswords(self): + """ + Public method to indicate, if passwords shall be shown. + + @return flag indicating if passwords shall be shown (boolean) + """ + return self.__showPasswords + + def __passwordsChanged(self): + """ + Private slot handling a change of the registered passwords. + """ + self.beginResetModel() + self.endResetModel() + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row start row (integer) + @param count number of rows to remove (integer) + @param parent parent index (QModelIndex) + @return flag indicating success (boolean) + """ + if parent.isValid(): + return False + + if count <= 0: + return False + + lastRow = row + count - 1 + + self.beginRemoveRows(parent, row, lastRow) + + siteList = self.__manager.allSiteNames() + for index in range(row, lastRow + 1): + self.__manager.removePassword(siteList[index]) + + # removeEngine emits changed() + #self.endRemoveRows() + + return True + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of rows of the model. + + @param parent parent index (QModelIndex) + @return number of rows (integer) + """ + if parent.isValid(): + return 0 + else: + return self.__manager.sitesCount() + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns of the model. + + @param parent parent index (QModelIndex) + @return number of columns (integer) + """ + if self.__showPasswords: + return 3 + else: + return 2 + + def data(self, index, role): + """ + Public method to get data from the model. + + @param index index to get data for (QModelIndex) + @param role role of the data to retrieve (integer) + @return requested data + """ + if index.row() >= self.__manager.sitesCount() or index.row() < 0: + return None + + site = self.__manager.allSiteNames()[index.row()] + siteInfo = self.__manager.siteInfo(site) + + if siteInfo is None: + return None + + if role == Qt.DisplayRole: + if index.column() == 0: + return site + elif index.column() in [1, 2]: + return siteInfo[index.column() - 1] + + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + pass + + return None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordReader.py Mon Feb 15 20:01:02 2016 +0100 @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to read login data files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamReader, QIODevice, QFile, \ + QCoreApplication, QUrl + + +class PasswordReader(QXmlStreamReader): + """ + Class implementing a reader object for login data files. + """ + def __init__(self): + """ + Constructor + """ + super(PasswordReader, self).__init__() + + def read(self, fileNameOrDevice): + """ + Public method to read a login data file. + + @param fileNameOrDevice name of the file to read (string) + or reference to the device to read (QIODevice) + @return tuple containing the logins, forms and never URLs + """ + self.__logins = {} + self.__loginForms = {} + self.__never = [] + + if isinstance(fileNameOrDevice, QIODevice): + self.setDevice(fileNameOrDevice) + else: + f = QFile(fileNameOrDevice) + if not f.exists(): + return self.__logins, self.__loginForms, self.__never + f.open(QFile.ReadOnly) + self.setDevice(f) + + while not self.atEnd(): + self.readNext() + if self.isStartElement(): + version = self.attributes().value("version") + if self.name() == "Password" and \ + (not version or version == "1.0"): + self.__readPasswords() + else: + self.raiseError(QCoreApplication.translate( + "PasswordReader", + "The file is not a Passwords version 1.0 file.")) + + return self.__logins, self.__loginForms, self.__never + + def __readPasswords(self): + """ + Private method to read and parse the login data file. + """ + if not self.isStartElement() and self.name() != "Password": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + if self.name() == "Logins": + self.__readLogins() + elif self.name() == "Forms": + self.__readForms() + elif self.name() == "Nevers": + self.__readNevers() + else: + self.__skipUnknownElement() + + def __readLogins(self): + """ + Private method to read the login information. + """ + if not self.isStartElement() and self.name() != "Logins": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + if self.name() == "Login": + continue + else: + break + + if self.isStartElement(): + if self.name() == "Login": + attributes = self.attributes() + key = attributes.value("key") + user = attributes.value("user") + password = attributes.value("password") + self.__logins[key] = (user, password) + else: + self.__skipUnknownElement() + + def __readForms(self): + """ + Private method to read the forms information. + """ + if not self.isStartElement() and self.name() != "Forms": + return + + while not self.atEnd(): + self.readNext() + if self.isStartElement(): + if self.name() == "Form": + from .LoginForm import LoginForm + attributes = self.attributes() + key = attributes.value("key") + form = LoginForm() + form.url = QUrl(attributes.value("url")) + form.name = attributes.value("name") + form.hasAPassword = attributes.value("password") == "yes" + elif self.name() == "Elements": + continue + elif self.name() == "Element": + attributes = self.attributes() + name = attributes.value("name") + value = attributes.value("value") + form.elements.append((name, value)) + else: + self.__skipUnknownElement() + + if self.isEndElement(): + if self.name() == "Form": + self.__loginForms[key] = form + continue + elif self.name() in ["Elements", "Element"]: + continue + else: + break + + def __readNevers(self): + """ + Private method to read the never URLs. + """ + if not self.isStartElement() and self.name() != "Nevers": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + if self.name() == "Never": + continue + else: + break + + if self.isStartElement(): + if self.name() == "Never": + self.__never.append(self.attributes().value("url")) + else: + self.__skipUnknownElement() + + def __skipUnknownElement(self): + """ + Private method to skip over all unknown elements. + """ + if not self.isStartElement(): + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + self.__skipUnknownElement()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordWriter.py Mon Feb 15 20:01:02 2016 +0100 @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to write login data files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamWriter, QIODevice, QFile + + +class PasswordWriter(QXmlStreamWriter): + """ + Class implementing a writer object to generate login data files. + """ + def __init__(self): + """ + Constructor + """ + super(PasswordWriter, self).__init__() + + self.setAutoFormatting(True) + + def write(self, fileNameOrDevice, logins, forms, nevers): + """ + Public method to write an login data file. + + @param fileNameOrDevice name of the file to write (string) + or device to write to (QIODevice) + @param logins dictionary with login data (user name, password) + @param forms list of forms data (list of LoginForm) + @param nevers list of URLs to never store data for (list of strings) + @return flag indicating success (boolean) + """ + if isinstance(fileNameOrDevice, QIODevice): + f = fileNameOrDevice + else: + f = QFile(fileNameOrDevice) + if not f.open(QFile.WriteOnly): + return False + + self.setDevice(f) + return self.__write(logins, forms, nevers) + + def __write(self, logins, forms, nevers): + """ + Private method to write an login data file. + + @param logins dictionary with login data (user name, password) + @param forms list of forms data (list of LoginForm) + @param nevers list of URLs to never store data for (list of strings) + @return flag indicating success (boolean) + """ + self.writeStartDocument() + self.writeDTD("<!DOCTYPE passwords>") + self.writeStartElement("Password") + self.writeAttribute("version", "1.0") + + if logins: + self.__writeLogins(logins) + if forms: + self.__writeForms(forms) + if nevers: + self.__writeNevers(nevers) + + self.writeEndDocument() + return True + + def __writeLogins(self, logins): + """ + Private method to write the login data. + + @param logins dictionary with login data (user name, password) + """ + self.writeStartElement("Logins") + for key, login in logins.items(): + self.writeEmptyElement("Login") + self.writeAttribute("key", key) + self.writeAttribute("user", login[0]) + self.writeAttribute("password", login[1]) + self.writeEndElement() + + def __writeForms(self, forms): + """ + Private method to write forms data. + + @param forms list of forms data (list of LoginForm) + """ + self.writeStartElement("Forms") + for key, form in forms.items(): + self.writeStartElement("Form") + self.writeAttribute("key", key) + self.writeAttribute("url", form.url.toString()) + self.writeAttribute("name", str(form.name)) + self.writeAttribute( + "password", "yes" if form.hasAPassword else "no") + if form.elements: + self.writeStartElement("Elements") + for element in form.elements: + self.writeEmptyElement("Element") + self.writeAttribute("name", element[0]) + self.writeAttribute("value", element[1]) + self.writeEndElement() + self.writeEndElement() + self.writeEndElement() + + def __writeNevers(self, nevers): + """ + Private method to write the URLs never to store login data for. + + @param nevers list of URLs to never store data for (list of strings) + """ + self.writeStartElement("Nevers") + for never in nevers: + self.writeEmptyElement("Never") + self.writeAttribute("url", never) + self.writeEndElement()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordsDialog.py Mon Feb 15 20:01:02 2016 +0100 @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show all saved logins. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QSortFilterProxyModel +from PyQt5.QtGui import QFont, QFontMetrics +from PyQt5.QtWidgets import QDialog + +from E5Gui import E5MessageBox + +from .Ui_PasswordsDialog import Ui_PasswordsDialog + + +class PasswordsDialog(QDialog, Ui_PasswordsDialog): + """ + Class implementing a dialog to show all saved logins. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(PasswordsDialog, self).__init__(parent) + self.setupUi(self) + + self.__showPasswordsText = self.tr("Show Passwords") + self.__hidePasswordsText = self.tr("Hide Passwords") + self.passwordsButton.setText(self.__showPasswordsText) + + self.removeButton.clicked.connect( + self.passwordsTable.removeSelected) + self.removeAllButton.clicked.connect(self.passwordsTable.removeAll) + + import Helpviewer.HelpWindow + from .PasswordModel import PasswordModel + + self.passwordsTable.verticalHeader().hide() + self.__passwordModel = PasswordModel( + Helpviewer.HelpWindow.HelpWindow.passwordManager(), self) + self.__proxyModel = QSortFilterProxyModel(self) + self.__proxyModel.setSourceModel(self.__passwordModel) + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + self.passwordsTable.setModel(self.__proxyModel) + + fm = QFontMetrics(QFont()) + height = fm.height() + fm.height() // 3 + self.passwordsTable.verticalHeader().setDefaultSectionSize(height) + self.passwordsTable.verticalHeader().setMinimumSectionSize(-1) + + self.__calculateHeaderSizes() + + def __calculateHeaderSizes(self): + """ + Private method to calculate the section sizes of the horizontal header. + """ + fm = QFontMetrics(QFont()) + for section in range(self.__passwordModel.columnCount()): + header = self.passwordsTable.horizontalHeader()\ + .sectionSizeHint(section) + if section == 0: + header = fm.width("averagebiglongsitename") + elif section == 1: + header = fm.width("averagelongusername") + elif section == 2: + header = fm.width("averagelongpassword") + buffer = fm.width("mm") + header += buffer + self.passwordsTable.horizontalHeader()\ + .resizeSection(section, header) + self.passwordsTable.horizontalHeader().setStretchLastSection(True) + + @pyqtSlot() + def on_passwordsButton_clicked(self): + """ + Private slot to switch the password display mode. + """ + if self.__passwordModel.showPasswords(): + self.__passwordModel.setShowPasswords(False) + self.passwordsButton.setText(self.__showPasswordsText) + else: + res = E5MessageBox.yesNo( + self, + self.tr("Saved Passwords"), + self.tr("""Do you really want to show passwords?""")) + if res: + self.__passwordModel.setShowPasswords(True) + self.passwordsButton.setText(self.__hidePasswordsText) + self.__calculateHeaderSizes()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordsDialog.ui Mon Feb 15 20:01:02 2016 +0100 @@ -0,0 +1,202 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PasswordsDialog</class> + <widget class="QDialog" name="PasswordsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>350</height> + </rect> + </property> + <property name="windowTitle"> + <string>Saved Passwords</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Enter search term</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="E5TableView" name="passwordsTable"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>208</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="passwordsButton"> + <property name="toolTip"> + <string>Press to toggle the display of passwords</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5TableView</class> + <extends>QTableView</extends> + <header>E5Gui/E5TableView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>passwordsTable</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>passwordsButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>PasswordsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>237</x> + <y>340</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>PasswordsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>325</x> + <y>340</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/__init__.py Mon Feb 15 20:01:02 2016 +0100 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the password management interface. +"""
--- a/WebBrowser/Tools/Scripts.py Sun Feb 14 19:34:05 2016 +0100 +++ b/WebBrowser/Tools/Scripts.py Mon Feb 15 20:01:02 2016 +0100 @@ -243,3 +243,112 @@ values += valueSource.format(name, value) return source.format(url.toString(), values) + + +def setupFormObserver(): + """ + Function generating a script to monitor a web form for user entries. + + @return script to monitor a web page + @rtype str + """ + source = """ + (function() { + function findUsername(inputs) { + for (var i = 0; i < inputs.length; ++i) + if (inputs[i].type == 'text' && inputs[i].value.length && + inputs[i].name.indexOf('user') != -1) + return inputs[i].value; + for (var i = 0; i < inputs.length; ++i) + if (inputs[i].type == 'text' && inputs[i].value.length && + inputs[i].name.indexOf('name') != -1) + return inputs[i].value; + for (var i = 0; i < inputs.length; ++i) + if (inputs[i].type == 'text' && inputs[i].value.length) + return inputs[i].value; + for (var i = 0; i < inputs.length; ++i) + if (inputs[i].type == 'email' && inputs[i].value.length) + return inputs[i].value; + return ''; + } + + function registerForm(form) { + form.addEventListener('submit', function() { + var form = this; + var data = ''; + var password = ''; + var inputs = form.getElementsByTagName('input'); + for (var i = 0; i < inputs.length; ++i) { + var input = inputs[i]; + var type = input.type.toLowerCase(); + if (type != 'text' && type != 'password' && + type != 'email') + continue; + if (!password && type == 'password') + password = input.value; + data += encodeURIComponent(input.name); + data += '='; + data += encodeURIComponent(input.value); + data += '&'; + } + if (!password) + return; + data = data.substring(0, data.length - 1); + var url = window.location.href; + var username = findUsername(inputs); + external.autoFill.formSubmitted(url, username, password, + data); + }, true); + } + + for (var i = 0; i < document.forms.length; ++i) + registerForm(document.forms[i]); + + var observer = new MutationObserver(function(mutations) { + for (var i = 0; i < mutations.length; ++i) + for (var j = 0; j < mutations[i].addedNodes.length; ++j) + if (mutations[i].addedNodes[j].tagName == 'form') + registerForm(mutations[i].addedNodes[j]); + }); + observer.observe(document.documentElement, { childList: true }); + + })()""" + return source + + +def completeFormData(data): + """ + Function generating a script to fill in form data. + + @param data data to be filled into the form + @type QByteArray + @return script to fill a form + @rtype str + """ + source = """ + (function() {{ + var data = '{0}'.split('&'); + var inputs = document.getElementsByTagName('input'); + + for (var i = 0; i < data.length; ++i) {{ + var pair = data[i].split('='); + if (pair.length != 2) + continue; + var key = decodeURIComponent(pair[0]); + var val = decodeURIComponent(pair[1]); + for (var j = 0; j < inputs.length; ++j) {{ + var input = inputs[j]; + var type = input.type.toLowerCase(); + if (type != 'text' && type != 'password' && + type != 'email') + continue; + if (input.name == key) + input.value = val; + }} + }} + + }})()""" + + data = bytes(data).decode("utf-8") + data = data.replace("'", "\\'") + return source.format(data)
--- a/WebBrowser/WebBrowserPage.py Sun Feb 14 19:34:05 2016 +0100 +++ b/WebBrowser/WebBrowserPage.py Mon Feb 15 20:01:02 2016 +0100 @@ -201,6 +201,10 @@ ## self.__restoreFrameStateRequested) self.featurePermissionRequested.connect( self.__featurePermissionRequested) + + self.authenticationRequired.connect( + WebBrowser.WebBrowserWindow.WebBrowserWindow.networkManager() + .authentication) def acceptNavigationRequest(self, url, type_, isMainFrame): """
--- a/WebBrowser/WebBrowserWindow.py Sun Feb 14 19:34:05 2016 +0100 +++ b/WebBrowser/WebBrowserWindow.py Mon Feb 15 20:01:02 2016 +0100 @@ -85,7 +85,7 @@ ## _helpEngine = None _bookmarksManager = None _historyManager = None -## _passwordManager = None + _passwordManager = None ## _adblockManager = None ## _downloadManager = None ## _feedsManager = None @@ -1541,22 +1541,21 @@ self.__showEnginesConfigurationDialog) self.__actions.append(self.searchEnginesAct) - # TODO: Passwords -## self.passwordsAct = E5Action( -## self.tr('Manage Saved Passwords'), -## UI.PixmapCache.getIcon("passwords.png"), -## self.tr('Manage Saved Passwords...'), -## 0, 0, -## self, 'webbrowser_manage_passwords') -## self.passwordsAct.setStatusTip(self.tr( -## 'Manage the saved passwords')) -## self.passwordsAct.setWhatsThis(self.tr( -## """<b>Manage Saved Passwords...</b>""" -## """<p>Opens a dialog to manage the saved passwords.</p>""" -## )) -## if not self.__initShortcutsOnly: -## self.passwordsAct.triggered.connect(self.__showPasswordsDialog) -## self.__actions.append(self.passwordsAct) + self.passwordsAct = E5Action( + self.tr('Manage Saved Passwords'), + UI.PixmapCache.getIcon("passwords.png"), + self.tr('Manage Saved Passwords...'), + 0, 0, + self, 'webbrowser_manage_passwords') + self.passwordsAct.setStatusTip(self.tr( + 'Manage the saved passwords')) + self.passwordsAct.setWhatsThis(self.tr( + """<b>Manage Saved Passwords...</b>""" + """<p>Opens a dialog to manage the saved passwords.</p>""" + )) + if not self.__initShortcutsOnly: + self.passwordsAct.triggered.connect(self.__showPasswordsDialog) + self.__actions.append(self.passwordsAct) # TODO: AdBlock ## self.adblockAct = E5Action( @@ -1862,8 +1861,8 @@ menu.addAction(self.editMessageFilterAct) menu.addSeparator() menu.addAction(self.searchEnginesAct) -## menu.addSeparator() -## menu.addAction(self.passwordsAct) + menu.addSeparator() + menu.addAction(self.passwordsAct) ## if SSL_AVAILABLE: ## menu.addAction(self.certificatesAct) ## menu.addSeparator() @@ -2481,8 +2480,8 @@ ## self.historyManager().close() ## -## self.passwordManager().close() -## + self.passwordManager().close() + ## self.adBlockManager().close() ## ## self.userAgentsManager().close() @@ -2821,13 +2820,12 @@ @param oldPassword current master password (string) @param newPassword new master password (string) """ - # TODO: PasswordManager -## from Preferences.ConfigurationDialog import ConfigurationDialog -## self.passwordManager().masterPasswordChanged(oldPassword, newPassword) -## if self.__fromEric and isinstance(self.sender(), ConfigurationDialog): -## # we were called from our local configuration dialog -## Preferences.convertPasswords(oldPassword, newPassword) -## Utilities.crypto.changeRememberedMaster(newPassword) + from Preferences.ConfigurationDialog import ConfigurationDialog + self.passwordManager().masterPasswordChanged(oldPassword, newPassword) + if self.__fromEric and isinstance(self.sender(), ConfigurationDialog): + # we were called from our local configuration dialog + Preferences.convertPasswords(oldPassword, newPassword) + Utilities.crypto.changeRememberedMaster(newPassword) ## def __showAcceptedLanguages(self): ## """ @@ -3239,7 +3237,7 @@ item = backItems[index] act = QAction(self) act.setData(-1 * (index + 1)) - icon = HelpWindow.icon(item.url()) + icon = WebBrowserWindow.icon(item.url()) act.setIcon(icon) act.setText(item.title()) self.backMenu.addAction(act) @@ -3256,7 +3254,7 @@ item = forwardItems[index] act = QAction(self) act.setData(index + 1) - icon = HelpWindow.icon(item.url()) + icon = WebBrowserWindow.icon(item.url()) act.setIcon(icon) act.setText(item.title()) self.forwardMenu.addAction(act) @@ -3311,9 +3309,8 @@ # TODO: Cookies ## if cookies: ## self.cookieJar().clear() - # TODO: Passwords -## if passwords: -## self.passwordManager().clear() + if passwords: + self.passwordManager().clear() # TODO: Web Databases ## if databases: ## if hasattr(QWebDatabase, "removeAllDatabases"): @@ -3357,15 +3354,15 @@ """ return self.searchEnginesAct -## def __showPasswordsDialog(self): -## """ -## Private slot to show the passwords management dialog. -## """ -## from .Passwords.PasswordsDialog import PasswordsDialog -## -## dlg = PasswordsDialog(self) -## dlg.exec_() -## + def __showPasswordsDialog(self): + """ + Private slot to show the passwords management dialog. + """ + from .Passwords.PasswordsDialog import PasswordsDialog + + dlg = PasswordsDialog(self) + dlg.exec_() + ## def __showCertificatesDialog(self): ## """ ## Private slot to show the certificates management dialog. @@ -3512,19 +3509,19 @@ return cls._historyManager -## @classmethod -## def passwordManager(cls): -## """ -## Class method to get a reference to the password manager. -## -## @return reference to the password manager (PasswordManager) -## """ -## if cls._passwordManager is None: -## from .Passwords.PasswordManager import PasswordManager -## cls._passwordManager = PasswordManager() -## -## return cls._passwordManager -## + @classmethod + def passwordManager(cls): + """ + Class method to get a reference to the password manager. + + @return reference to the password manager (PasswordManager) + """ + if cls._passwordManager is None: + from .Passwords.PasswordManager import PasswordManager + cls._passwordManager = PasswordManager() + + return cls._passwordManager + ## @classmethod ## def adBlockManager(cls): ## """ @@ -3551,7 +3548,7 @@ ## """ ## Class method to get a reference to the download manager. ## -## @return reference to the password manager (DownloadManager) +## @return reference to the download manager (DownloadManager) ## """ ## if cls._downloadManager is None: ## from .Download.DownloadManager import DownloadManager
--- a/eric6.e4p Sun Feb 14 19:34:05 2016 +0100 +++ b/eric6.e4p Mon Feb 15 20:01:02 2016 +0100 @@ -1319,6 +1319,13 @@ <Source>WebBrowser/OpenSearch/OpenSearchReader.py</Source> <Source>WebBrowser/OpenSearch/OpenSearchWriter.py</Source> <Source>WebBrowser/OpenSearch/__init__.py</Source> + <Source>WebBrowser/Passwords/LoginForm.py</Source> + <Source>WebBrowser/Passwords/PasswordManager.py</Source> + <Source>WebBrowser/Passwords/PasswordModel.py</Source> + <Source>WebBrowser/Passwords/PasswordReader.py</Source> + <Source>WebBrowser/Passwords/PasswordWriter.py</Source> + <Source>WebBrowser/Passwords/PasswordsDialog.py</Source> + <Source>WebBrowser/Passwords/__init__.py</Source> <Source>WebBrowser/SearchWidget.py</Source> <Source>WebBrowser/Tools/Scripts.py</Source> <Source>WebBrowser/Tools/WebBrowserTools.py</Source> @@ -1750,6 +1757,7 @@ <Form>WebBrowser/History/HistoryDialog.ui</Form> <Form>WebBrowser/OpenSearch/OpenSearchDialog.ui</Form> <Form>WebBrowser/OpenSearch/OpenSearchEditDialog.ui</Form> + <Form>WebBrowser/Passwords/PasswordsDialog.ui</Form> <Form>WebBrowser/SearchWidget.ui</Form> <Form>WebBrowser/UrlBar/BookmarkActionSelectionDialog.ui</Form> <Form>WebBrowser/UrlBar/BookmarkInfoDialog.ui</Form>