eric6/Helpviewer/Passwords/PasswordManager.py

branch
maintenance
changeset 7286
7eb04391adf7
parent 7226
babe80d84a3e
parent 7285
1ff497f33f31
child 7287
1c17f2191bdd
--- a/eric6/Helpviewer/Passwords/PasswordManager.py	Mon Sep 09 18:52:08 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,649 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2009 - 2019 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
-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
-from Globals import qVersionTuple
-
-
-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()
-    
-    SEPARATOR = "===================="
-    FORMS = "=====FORMS====="
-    NEVER = "=====NEVER====="
-    
-    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(), "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.
-        """
-        loginFile = self.getFileName()
-        if not os.path.exists(loginFile):
-            self.__loadNonXml(os.path.splitext(loginFile)[0])
-        else:
-            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 __loadNonXml(self, loginFile):
-        """
-        Private method to load non-XML password files.
-        
-        This method is to convert from the old, non-XML format to the new
-        XML based format.
-        
-        @param loginFile name of the non-XML password file (string)
-        """
-        if os.path.exists(loginFile):
-            try:
-                f = open(loginFile, "r", encoding="utf-8")
-                lines = f.read()
-                f.close()
-            except IOError as err:
-                E5MessageBox.critical(
-                    None,
-                    self.tr("Loading login data"),
-                    self.tr("""<p>Login data could not be loaded """
-                            """from <b>{0}</b></p>"""
-                            """<p>Reason: {1}</p>""")
-                    .format(loginFile, str(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:
-                            E5MessageBox.critical(
-                                None,
-                                self.tr("Loading login data"),
-                                self.tr(
-                                    """<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:
-                        from .LoginForm import LoginForm
-                        key = data[0]
-                        form = LoginForm()
-                        form.url = QUrl(data[1])
-                        form.name = data[2]
-                        form.hasAPassword = data[3] == "True"
-                        for element in data[4:]:
-                            name, value = element.split(" = ", 1)
-                            form.elements.append((name, value))
-                        self.__loginForms[key] = form
-                        data = []
-                
-                elif section == 2:
-                    self.__never.append(line)
-            
-            os.remove(loginFile)
-        
-        self.__loaded = True
-        
-        # this does the conversion
-        self.save()
-    
-    def reload(self):
-        """
-        Public method to reload the login data.
-        """
-        if not self.__loaded:
-            return
-        
-        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)
-    
-    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
-        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)
-        if qVersionTuple() >= (5, 0, 0):
-            cleanUrl.setQuery("")
-        else:
-            cleanUrl.setQueryItems([])
-        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
-    
-    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 qVersionTuple() >= (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 formMap in lst:
-            formHasPasswords = False
-            formName = formMap["name"]
-            formIndex = formMap["index"]
-            if isinstance(formIndex, float) and formIndex.is_integer():
-                formIndex = int(formIndex)
-            elements = formMap["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, oldPwHash = self.__logins[key]
-            newPwHash = Utilities.crypto.pwRecode(oldPwHash, oldPassword,
-                                                  newPassword)
-            self.__logins[key] = (username, newPwHash)
-            count += 1
-        
-        progress.setValue(len(self.__logins))
-        QCoreApplication.processEvents()
-        self.changed.emit()

eric ide

mercurial