WebBrowser/Passwords/PasswordManager.py

branch
QtWebEngine
changeset 4743
f9e2e536d130
parent 4631
5c1a96925da4
child 4744
ad3f6c1caf8d
diff -r f9d1090f6ab9 -r f9e2e536d130 WebBrowser/Passwords/PasswordManager.py
--- /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()

eric ide

mercurial