Wed, 17 Feb 2016 19:49:51 +0100
Continued porting the web browser.
- continued the passwords stuff
--- a/WebBrowser/JavaScript/AutoFillJsObject.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/JavaScript/AutoFillJsObject.py Wed Feb 17 19:49:51 2016 +0100 @@ -46,14 +46,7 @@ @param data data to be submitted @type QByteArray """ - # TODO: AutoFill - pass -##void AutoFillJsObject::formSubmitted(const QString &frameUrl, const QString &username, const QString &password, const QByteArray &data) -##{ -## PageFormData formData; -## formData.username = username; -## formData.password = password; -## formData.postData = data; -## -## mApp->autoFill()->saveForm(m_jsObject->page(), QUrl(frameUrl), formData); -##} + import WebBrowser.WebBrowserWindow + WebBrowser.WebBrowserWindow.WebBrowserWindow.passwordManager()\ + .formSubmitted(urlStr, userName, password, data, + self.__jsObject.page())
--- a/WebBrowser/JavaScript/ExternalJsObject.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/JavaScript/ExternalJsObject.py Wed Feb 17 19:49:51 2016 +0100 @@ -68,6 +68,7 @@ """ return self.__autoFill + # TODO: OpenSearch ##void ExternalJsObject::AddSearchProvider(const QString &engineUrl) ##{ Slot ## mApp->searchEnginesManager()->addEngine(QUrl(engineUrl));
--- a/WebBrowser/Passwords/LoginForm.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/Passwords/LoginForm.py Wed Feb 17 19:49:51 2016 +0100 @@ -9,7 +9,7 @@ from __future__ import unicode_literals -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import QUrl, QByteArray class LoginForm(object): @@ -22,11 +22,7 @@ """ 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 + self.postData = QByteArray() def isValid(self): """ @@ -34,4 +30,5 @@ @return flag indicating a valid form (boolean) """ - return len(self.elements) > 0 + return not self.url.isEmpty() and \ + not self.postData.isEmpty()
--- a/WebBrowser/Passwords/PasswordManager.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/Passwords/PasswordManager.py Wed Feb 17 19:49:51 2016 +0100 @@ -12,11 +12,9 @@ import os from PyQt5.QtCore import pyqtSignal, QObject, QByteArray, QUrl, \ - QCoreApplication, QXmlStreamReader, qVersion + QCoreApplication, QXmlStreamReader from PyQt5.QtWidgets import QApplication -from PyQt5.QtNetwork import QNetworkRequest -##from PyQt5.QtWebKit import QWebSettings -##from PyQt5.QtWebKitWidgets import QWebPage +from PyQt5.QtWebEngineWidgets import QWebEngineScript from E5Gui import E5MessageBox from E5Gui.E5ProgressDialog import E5ProgressDialog @@ -26,6 +24,9 @@ import Utilities.crypto import Preferences +import WebBrowser.WebBrowserWindow +from ..Tools import Scripts + class PasswordManager(QObject): """ @@ -45,6 +46,16 @@ """ super(PasswordManager, self).__init__(parent) + # setup userscript to monitor forms + script = QWebEngineScript() + script.setName("_eric_passwordmonitor") + script.setInjectionPoint(QWebEngineScript.DocumentReady) + script.setWorldId(QWebEngineScript.MainWorld) + script.setRunsOnSubFrames(True) + script.setSourceCode(Scripts.setupFormObserver()) + profile = WebBrowser.WebBrowserWindow.WebBrowserWindow.webProfile() + profile.scripts().insert(script) + self.__logins = {} self.__loginForms = {} self.__never = [] @@ -244,113 +255,69 @@ 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): + def formSubmitted(self, urlStr, userName, password, data, page): """ - Public method to check, if the data to be sent contains login data. + Public method to record login data. - @param request reference to the network request (QNetworkRequest) - @param data data to be sent (QByteArray) + @param urlStr form submission URL + @type str + @param userName name of the user + @type str + @param password user password + @type str + @param data data to be submitted + @type QByteArray + @param page reference to the calling page + @type QWrbEnginePage """ -## # 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() + # shall passwords be saved? + if not Preferences.getUser("SavePasswords"): + return + + if WebBrowser.WebBrowserWindow.WebBrowserWindow.mainWindow()\ + .isPrivate(): + return + + if urlStr in self.__never: + return + + if userName and password: + url = QUrl(urlStr) + url = self.__stripUrl(url) + 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, parent=page.view()) + 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 + + self.__logins[key] = \ + (userName, + Utilities.crypto.pwConvert(password, encode=True)) + from .LoginForm import LoginForm + form = LoginForm() + form.url = url + form.name = userName + form.postData = QByteArray(data) + self.__loginForms[key] = form + self.changed.emit() def __stripUrl(self, url): """ @@ -371,171 +338,31 @@ 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 completePage(self, page): + """ + Public slot to complete login forms with saved data. + + @param page reference to the web page (WebBrowserPage) + """ + if page is None: + return + + if not self.__loaded: + self.__load() + + url = page.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 + + script = Scripts.completeFormData(form.postData) + page.runJavaScript(script) def masterPasswordChanged(self, oldPassword, newPassword): """
--- a/WebBrowser/Passwords/PasswordReader.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/Passwords/PasswordReader.py Wed Feb 17 19:49:51 2016 +0100 @@ -10,7 +10,7 @@ from __future__ import unicode_literals from PyQt5.QtCore import QXmlStreamReader, QIODevice, QFile, \ - QCoreApplication, QUrl + QCoreApplication, QUrl, QByteArray class PasswordReader(QXmlStreamReader): @@ -49,12 +49,12 @@ if self.isStartElement(): version = self.attributes().value("version") if self.name() == "Password" and \ - (not version or version == "1.0"): + (not version or version == "2.0"): self.__readPasswords() else: self.raiseError(QCoreApplication.translate( "PasswordReader", - "The file is not a Passwords version 1.0 file.")) + "The file is not a Passwords version 2.0 file.")) return self.__logins, self.__loginForms, self.__never @@ -112,6 +112,7 @@ if not self.isStartElement() and self.name() != "Forms": return + # TODO: Passwords: adjust reader to new login form while not self.atEnd(): self.readNext() if self.isStartElement(): @@ -122,14 +123,10 @@ 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)) + + elif self.name() == "PostData": + form.postData = QByteArray( + self.readElementText().encode("utf-8")) else: self.__skipUnknownElement()
--- a/WebBrowser/Passwords/PasswordWriter.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/Passwords/PasswordWriter.py Wed Feb 17 19:49:51 2016 +0100 @@ -57,7 +57,7 @@ self.writeStartDocument() self.writeDTD("<!DOCTYPE passwords>") self.writeStartElement("Password") - self.writeAttribute("version", "1.0") + self.writeAttribute("version", "2.0") if logins: self.__writeLogins(logins) @@ -95,15 +95,8 @@ 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.writeTextElement( + "PostData", bytes(form.postData).decode("utf-8")) self.writeEndElement() self.writeEndElement()
--- a/WebBrowser/WebBrowserPage.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/WebBrowserPage.py Wed Feb 17 19:49:51 2016 +0100 @@ -769,7 +769,7 @@ Public method to setup a web channel to our external object. """ oldChannel = self.webChannel() - newChannel = QWebChannel() + newChannel = QWebChannel(self) newChannel.registerObject("eric_object", ExternalJsObject(self)) self.setWebChannel(newChannel)
--- a/WebBrowser/WebBrowserView.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/WebBrowserView.py Wed Feb 17 19:49:51 2016 +0100 @@ -1524,8 +1524,7 @@ self.__mw.historyManager().addHistoryEntry(self) # TODO: AdBlock ## self.__mw.adBlockManager().page().hideBlockedPageEntries(self.page()) - # TODO: Password Manager -## self.__mw.passwordManager().fill(self.page()) + self.__mw.passwordManager().completePage(self.page()) def isLoading(self): """
--- a/WebBrowser/WebBrowserWindow.py Mon Feb 15 20:01:02 2016 +0100 +++ b/WebBrowser/WebBrowserWindow.py Wed Feb 17 19:49:51 2016 +0100 @@ -80,6 +80,7 @@ _fromEric = False UseQtHelp = QTHELP_AVAILABLE + _webProfile = None _networkManager = None ## _cookieJar = None ## _helpEngine = None @@ -135,21 +136,22 @@ if self.__initShortcutsOnly: self.__initActions() else: - if self.isPrivate(): - self.__webProfile = QWebEngineProfile(self) - else: - self.__webProfile = QWebEngineProfile.defaultProfile() - self.__webProfile.downloadRequested.connect( - self.__downloadRequested) - - # Setup QWebChannel user script - script = QWebEngineScript() - script.setName("_eric_webchannel") - script.setInjectionPoint(QWebEngineScript.DocumentCreation) - script.setWorldId(QWebEngineScript.MainWorld) - script.setRunsOnSubFrames(True) - script.setSourceCode(Scripts.setupWebChannel()) - self.__webProfile.scripts().insert(script) + self.webProfile(private) +## if self.isPrivate(): +## self.__webProfile = QWebEngineProfile(self) +## else: +## self.__webProfile = QWebEngineProfile.defaultProfile() +## self.__webProfile.downloadRequested.connect( +## self.__downloadRequested) +## +## # Setup QWebChannel user script +## script = QWebEngineScript() +## script.setName("_eric_webchannel") +## script.setInjectionPoint(QWebEngineScript.DocumentCreation) +## script.setWorldId(QWebEngineScript.MainWorld) +## script.setRunsOnSubFrames(True) +## script.setSourceCode(Scripts.setupWebChannel()) +## self.__webProfile.scripts().insert(script) from .SearchWidget import SearchWidget # TODO: QtHelp @@ -260,6 +262,8 @@ self.__setIconDatabasePath() self.__initWebEngineSettings() + self.passwordManager() + self.__initActions() self.__initMenus() self.__initToolbars() @@ -4029,9 +4033,9 @@ .replace("\n", "") name = "_eric_userstylesheet" - oldScript = self.__webProfile.scripts().findScript(name) + oldScript = self.webProfile().scripts().findScript(name) if not oldScript.isNull(): - self.__webProfile.scripts().remove(oldScript) + self.webProfile().scripts().remove(oldScript) if userStyle: script = QWebEngineScript() @@ -4040,7 +4044,7 @@ script.setWorldId(QWebEngineScript.ApplicationWorld) script.setRunsOnSubFrames(True) script.setSourceCode(Scripts.setStyleSheet(userStyle)) - self.__webProfile.scripts().insert(script) + self.webProfile().scripts().insert(script) ########################################## ## Support for desktop notifications below @@ -4088,9 +4092,10 @@ ## Support for download files below ################################### - def __downloadRequested(self, download): + @classmethod + def downloadRequested(self, download): """ - Private slot to handle a download request. + Class method to handle a download request. @param download reference to the download data @type QWebEngineDownloadItem @@ -4098,3 +4103,36 @@ pass # TODO: DownloadManager ## self.downloadManager().download(download, mainWindow=self) + + ######################################## + ## Support for web engine profiles below + ######################################## + + @classmethod + def webProfile(cls, private=False): + """ + Class method handling the web engine profile. + + @param private flag indicating the privacy mode + @type bool + @return reference to the web profile object + @rtype QWebEngineProfile + """ + if cls._webProfile is None: + if private: + cls._webProfile = QWebEngineProfile() + else: + cls._webProfile = QWebEngineProfile.defaultProfile() + cls._webProfile.downloadRequested.connect( + cls.downloadRequested) + + # Setup QWebChannel user script + script = QWebEngineScript() + script.setName("_eric_webchannel") + script.setInjectionPoint(QWebEngineScript.DocumentCreation) + script.setWorldId(QWebEngineScript.MainWorld) + script.setRunsOnSubFrames(True) + script.setSourceCode(Scripts.setupWebChannel()) + cls._webProfile.scripts().insert(script) + + return cls._webProfile