Helpviewer/Passwords/PasswordManager.py

changeset 0
de9c2efb9d02
child 6
52e8c820d0dd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Helpviewer/Passwords/PasswordManager.py	Mon Dec 28 16:03:33 2009 +0000
@@ -0,0 +1,568 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the password manager.
+"""
+
+import os
+
+from PyQt4.QtCore import *
+from PyQt4.QtGui import QMessageBox
+from PyQt4.QtNetwork import QNetworkRequest
+from PyQt4.QtWebKit import *
+
+from Helpviewer.JavaScriptResources import parseForms_js
+
+from Utilities.AutoSaver import AutoSaver
+import Utilities
+import Preferences
+
+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 
+                                # (QString, QString)
+        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
+    
+    def load(self, data):
+        """
+        Public method to load the form data from a file.
+        
+        @param data list of strings to load data from (list of strings)
+        @return flag indicating success (boolean)
+        """
+        self.url = QUrl(data[0])
+        self.name = data[1]
+        self.hasAPassword = data[2] == "True"
+        for element in data[3:]:
+            name, value = element.split(" = ", 1)
+            self.elements.append((name, value))
+    
+    def save(self, f):
+        """
+        Public method to save the form data to a file.
+        
+        @param f file or file like object open for writing
+        @return flag indicating success (booelan)
+        """
+        f.write("%s\n" % self.url.toString())
+        f.write("%s\n" % self.name)
+        f.write("%s\n" % self.hasAPassword)
+        for element in self.elements:
+            f.write("%s = %s\n" % (element[0], element[1]))
+
+class PasswordManager(QObject):
+    """
+    Class implementing the password manager.
+    
+    @signal changed() emitted to indicate a change
+    """
+    SEPARATOR = "===================="
+    FORMS = "=====FORMS====="
+    NEVER = "=====NEVER====="
+    
+    def __init__(self, parent = None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object (QObject)
+        """
+        QObject.__init__(self, parent)
+        
+        self.__logins = {}
+        self.__loginForms = {}
+        self.__never = []
+        self.__loaded = False
+        self.__saveTimer = AutoSaver(self, self.save)
+        
+        self.connect(self, SIGNAL("changed()"), 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.emit(SIGNAL("changed()"))
+    
+    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.pwDecode(self.__logins[key][1])
+        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.pwEncode(password))
+        self.emit(SIGNAL("changed()"))
+    
+    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)
+        """
+        if realm:
+            key = "%s://%s (%s)" % (url.scheme(), url.authority(), realm)
+        else:
+            key = "%s://%s" % (url.scheme(), url.authority())
+        return key
+    
+    def save(self):
+        """
+        Public slot to save the login entries to disk.
+        """
+        if not self.__loaded:
+            return
+        
+        loginFile = os.path.join(Utilities.getConfigDir(), "browser", "logins")
+        try:
+            f = open(loginFile, "w")
+            for key, login in self.__logins.items():
+                f.write("%s\n" % key)
+                f.write("%s\n" % login[0])
+                f.write("%s\n" % login[1])
+                f.write("%s\n" % self.SEPARATOR)
+            if self.__loginForms:
+                f.write("%s\n" % self.FORMS)
+                for key, form in self.__loginForms.items():
+                    f.write("%s\n" % key)
+                    form.save(f)
+                    f.write("%s\n" % self.SEPARATOR)
+            if self.__never:
+                f.write("%s\n" % self.NEVER)
+                for key in self.__never:
+                    f.write("%s\n") % key
+            f.close()
+        except IOError, err:
+            QMessageBox.critical(None,
+                self.trUtf8("Saving login data"),
+                self.trUtf8("""<p>Login data could not be saved to <b>{0}</b></p>"""
+                            """<p>Reason: {1}</p>""").format(loginFile, unicode(err)))
+            return
+    
+    def __load(self):
+        """
+        Private method to load the saved login credentials.
+        """
+        loginFile = os.path.join(Utilities.getConfigDir(), "browser", "logins")
+        if os.path.exists(loginFile):
+            try:
+                f = open(loginFile, "r")
+                lines = f.read()
+                f.close()
+            except IOError, err:
+                QMessageBox.critical(None,
+                    self.trUtf8("Loading login data"),
+                    self.trUtf8("""<p>Login data could not be loaded """
+                                """from <b>{0}</b></p>"""
+                                """<p>Reason: {1}</p>""")\
+                        .format(loginFile, unicode(err)))
+                return
+            
+            data = []
+            section = 0     # 0 = login data, 1 = forms data, 2 = never store info
+            for line in lines.splitlines():
+                if line == self.FORMS:
+                    section = 1
+                    continue
+                elif line == self.NEVER:
+                    section = 2
+                    continue
+                
+                if section == 0:
+                    if line != self.SEPARATOR:
+                        data.append(line)
+                    else:
+                        if len(data) != 3:
+                            QMessageBox.critical(None,
+                                self.trUtf8("Loading login data"),
+                                self.trUtf8("""<p>Login data could not be loaded """
+                                            """from <b>{0}</b></p>"""
+                                            """<p>Reason: Wrong input format</p>""")\
+                                    .format(loginFile))
+                            return
+                        self.__logins[data[0]] = (data[1], data[2])
+                        data = []
+                
+                elif section == 1:
+                    if line != self.SEPARATOR:
+                        data.append(line)
+                    else:
+                        key = data[0]
+                        form = LoginForm()
+                        form.load(data[1:])
+                        self.__loginForms[key] = form
+                        data = []
+                
+                elif section == 2:
+                    self.__never.append(line)
+        
+        self.__loaded = True
+    
+    def close(self):
+        """
+        Public method to close the open search engines 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.emit(SIGNAL("changed()"))
+    
+    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.pwDecode(self.__logins[site][1])
+    
+    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.getHelp("SavePasswords"):
+            return
+        
+        # observe privacy
+        if QWebSettings.globalSettings().testAttribute(
+                QWebSettings.PrivateBrowsingEnabled):
+            return
+        
+        if not self.__loaded:
+            self.__load()
+        
+        # determine the url
+        refererHeader = request.rawHeader("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
+        v = request.attribute(QNetworkRequest.User + 101)
+        if not v.isValid():
+            return
+        navType = v.toInt()[0]
+        if navType != QWebPage.NavigationTypeFormSubmitted:
+            return
+        
+        # determine the QWebPage
+        v = request.attribute(QNetworkRequest.User + 100)
+        webPage = v.toPyObject()
+        if webPage == NotImplemented or webPage is None:
+            return
+        
+        # determine the requests content type
+        contentTypeHeader = request.rawHeader("Content-Type")
+        if contentTypeHeader.isEmpty():
+            return
+        multipart = contentTypeHeader.startsWith("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 = QMessageBox()
+            mb.setText(self.trUtf8(
+                """<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."""
+            ))
+            neverButton = mb.addButton(
+                self.trUtf8("Never for this site"), QMessageBox.DestructiveRole)
+            noButton = mb.addButton(self.trUtf8("Not now"), QMessageBox.RejectRole)
+            yesButton = mb.addButton(QMessageBox.Yes)
+            mb.exec_()
+            if mb.clickedButton() == neverButton:
+                self.__never.append(unicode(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]
+            name = element[0].lower()
+            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.pwEncode(password))
+            self.__loginForms[key] = form
+            self.emit(SIGNAL("changed()"))
+    
+    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.setQueryItems([])
+        cleanUrl.setFragment("")
+        cleanUrl.setUserInfo("")
+        return cleanUrl
+    
+    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)
+        """
+        form = LoginForm()
+        if boundary is not None:
+            args = self.__extractMultipartQueryItems(data, boundary)
+        else:
+            argsUrl = QUrl.fromEncoded(QByteArray("foo://bar.com/?" + data))
+            encodedArgs = argsUrl.queryItems()
+            args = set()
+            for arg in encodedArgs:
+                key = arg[0]
+                value = arg[1].replace("+", " ")
+                args.add((key, value))
+        
+        # extract the forms
+        lst = webPage.mainFrame().evaluateJavaScript(parseForms_js).toList()
+        for formVariant in lst:
+            map = formVariant.toMap()
+            formHasPasswords = False
+            formName = map["name"].toString()
+            formIndex = map["index"].toInt()[0]
+            elements = map["elements"].toList()
+            formElements = set()
+            formElementTypes = {}
+            deadElements = set()
+            for element in elements:
+                elementMap = element.toMap()
+                name = elementMap["name"].toString()
+                value = elementMap["value"].toString()
+                type_ = elementMap["type"].toString()
+                if type_ == "password":
+                    formHasPasswords = True
+                t = (name, value)
+                try:
+                    if elementMap["autocomplete"].toString() == "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 = unicode(data)
+        boundaryStr = unicode(boundary)
+        
+        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 = "%d" % int(form.name)
+            except ValueError:
+                formName = '"%s"' % form.name
+        for element in form.elements:
+            name = element[0]
+            value = element[1]
+            
+            disabled = page.mainFrame().evaluateJavaScript(
+                'document.forms[%s].elements["%s"].disabled' % (formName, name)).toBool()
+            if disabled:
+                continue
+            
+            readOnly = page.mainFrame().evaluateJavaScript(
+                'document.forms[%s].elements["%s"].readOnly' % (formName, name)).toBool()
+            if readOnly:
+                continue
+            
+            type_ = page.mainFrame().evaluateJavaScript(
+                'document.forms[%s].elements["%s"].type' % (formName, name)).toString()
+            if type_ == "" or \
+               type_ in ["hidden", "reset", "submit"]:
+                continue
+            if type_ == "password":
+                value = Utilities.pwDecode(self.__logins[key][1])
+            setType = type_ == "checkbox" and "checked" or "value"
+            value = value.replace("\\", "\\\\")
+            value = value.replace('"', '\\"')
+            javascript = 'document.forms[%s].elements["%s"].%s="%s";' % \
+                         (formName, name, setType, value)
+            page.mainFrame().evaluateJavaScript(javascript)

eric ide

mercurial