Let user accept/decline a plugin license if present

CURA-4222

If a downloaded plugin contains a license file, a dialog which contains
the license information will pop up. The user has to accept the license
before he/she can install the plugin.

If a plugin doesn't have a license file, after it is downloaded, it will
be installed directly just like before.
This commit is contained in:
Lipu Fei 2017-08-30 14:09:49 +02:00
parent ac6f50e686
commit 679c7dcfda
2 changed files with 164 additions and 17 deletions

View file

@ -7,6 +7,7 @@ from UM.Qt.ListModel import ListModel
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Application import Application from UM.Application import Application
from UM.Version import Version from UM.Version import Version
from UM.Message import Message
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
@ -16,6 +17,7 @@ import json
import os import os
import tempfile import tempfile
import platform import platform
import zipfile
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -57,6 +59,30 @@ class PluginBrowser(QObject, Extension):
# same file over and over again, we keep track of the upgraded plugins. # same file over and over again, we keep track of the upgraded plugins.
self._newly_installed_plugin_ids = [] self._newly_installed_plugin_ids = []
# variables for the license agreement dialog
self._license_dialog_plugin_name = ""
self._license_dialog_license_content = ""
self._license_dialog_plugin_file_location = ""
showLicenseDialog = pyqtSignal()
@pyqtSlot(result = str)
def getLicenseDialogPluginName(self):
return self._license_dialog_plugin_name
@pyqtSlot(result = str)
def getLicenseDialogPluginFileLocation(self):
return self._license_dialog_plugin_file_location
@pyqtSlot(result = str)
def getLicenseDialogLicenseContent(self):
return self._license_dialog_license_content
def openLicenseDialog(self, plugin_name, license_content, plugin_file_location):
self._license_dialog_plugin_name = plugin_name
self._license_dialog_license_content = license_content
self._license_dialog_plugin_file_location = plugin_file_location
self.showLicenseDialog.emit()
pluginsMetadataChanged = pyqtSignal() pluginsMetadataChanged = pyqtSignal()
onDownloadProgressChanged = pyqtSignal() onDownloadProgressChanged = pyqtSignal()
@ -71,7 +97,7 @@ class PluginBrowser(QObject, Extension):
self.requestPluginList() self.requestPluginList()
if not self._dialog: if not self._dialog:
self._createDialog() self._dialog = self._createDialog("PluginBrowser.qml")
self._dialog.show() self._dialog.show()
@pyqtSlot() @pyqtSlot()
@ -82,19 +108,20 @@ class PluginBrowser(QObject, Extension):
self._plugin_list_request.setRawHeader(*self._request_header) self._plugin_list_request.setRawHeader(*self._request_header)
self._network_manager.get(self._plugin_list_request) self._network_manager.get(self._plugin_list_request)
def _createDialog(self): def _createDialog(self, qml_name):
Logger.log("d", "PluginBrowser") Logger.log("d", "Creating dialog [%s]", qml_name)
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "PluginBrowser.qml")) path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name))
self._qml_component = QQmlComponent(Application.getInstance()._engine, path) self._qml_component = QQmlComponent(Application.getInstance()._engine, path)
# We need access to engine (although technically we can't) # We need access to engine (although technically we can't)
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._qml_context.setContextProperty("manager", self) self._qml_context.setContextProperty("manager", self)
self._dialog = self._qml_component.create(self._qml_context) dialog = self._qml_component.create(self._qml_context)
if self._dialog is None: if dialog is None:
Logger.log("e", "QQmlComponent status %s", self._qml_component.status()) Logger.log("e", "QQmlComponent status %s", self._qml_component.status())
Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString()) Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString())
return dialog
def setIsDownloading(self, is_downloading): def setIsDownloading(self, is_downloading):
if self._is_downloading != is_downloading: if self._is_downloading != is_downloading:
@ -117,9 +144,53 @@ class PluginBrowser(QObject, Extension):
self._temp_plugin_file.write(self._download_plugin_reply.readAll()) self._temp_plugin_file.write(self._download_plugin_reply.readAll())
self._temp_plugin_file.close() self._temp_plugin_file.close()
# open as read self._checkPluginLicenseOrInstall(location)
if not location.startswith("/"): return
location = "/" + location # Ensure that it starts with a /, as otherwise it doesn't work on windows.
## Checks if the downloaded plugin ZIP file contains a license file or not.
# If it does, it will show a popup dialog displaying the license to the user. The plugin will be installed if the
# user accepts the license.
# If there is no license file, the plugin will be directory installed.
def _checkPluginLicenseOrInstall(self, file_path):
with zipfile.ZipFile(file_path, "r") as zip_ref:
plugin_id = None
for file in zip_ref.infolist():
if file.filename.endswith("/"):
plugin_id = file.filename.strip("/")
break
if plugin_id is None:
msg = i18n_catalog.i18nc("@info:status", "Failed to get plugin ID from <filename>{0}</filename>", file_path)
self._progress_message = Message(msg, lifetime=0, dismissable=False)
return
# find a potential license file
plugin_root_dir = plugin_id + "/"
license_file = None
for f in zip_ref.infolist():
# skip directories (with file_size = 0) and files not in the plugin directory
if f.file_size == 0 or not f.filename.startswith(plugin_root_dir):
continue
file_name = os.path.basename(f.filename).lower()
file_base_name, file_ext = os.path.splitext(file_name)
if file_base_name in ["license", "licence"]:
license_file = f.filename
break
# show a dialog for user to read and accept/decline the license
if license_file is not None:
Logger.log("i", "Found license file for plugin [%s], showing the license dialog to the user", plugin_id)
license_content = zip_ref.read(license_file).decode('utf-8')
self.openLicenseDialog(plugin_id, license_content, file_path)
return
# there is no license file, directly install the plugin
self.installPlugin(file_path)
@pyqtSlot(str)
def installPlugin(self, file_path):
if not file_path.startswith("/"):
location = "/" + file_path # Ensure that it starts with a /, as otherwise it doesn't work on windows.
result = PluginRegistry.getInstance().installPlugin("file://" + location) result = PluginRegistry.getInstance().installPlugin("file://" + location)
self._newly_installed_plugin_ids.append(result["id"]) self._newly_installed_plugin_ids.append(result["id"])
@ -127,8 +198,6 @@ class PluginBrowser(QObject, Extension):
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"]) Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
self._temp_plugin_file.close() # Plugin was installed, delete temp file
@pyqtProperty(int, notify = onDownloadProgressChanged) @pyqtProperty(int, notify = onDownloadProgressChanged)
def downloadProgress(self): def downloadProgress(self):
return self._download_progress return self._download_progress

View file

@ -178,5 +178,83 @@ UM.Dialog
} }
} }
UM.I18nCatalog { id: catalog; name: "cura" } UM.I18nCatalog { id: catalog; name: "cura" }
Connections
{
target: manager
onShowLicenseDialog:
{
licenseDialog.pluginName = manager.getLicenseDialogPluginName();
licenseDialog.licenseContent = manager.getLicenseDialogLicenseContent();
licenseDialog.pluginFileLocation = manager.getLicenseDialogPluginFileLocation();
licenseDialog.show();
}
}
UM.Dialog
{
id: licenseDialog
title: catalog.i18nc("@title:window", "Plugin License Agreement")
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
width: minimumWidth
height: minimumHeight
property var pluginName;
property var licenseContent;
property var pluginFileLocation;
Item
{
anchors.fill: parent
Label
{
id: licenseTitle
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
text: licenseDialog.pluginName + catalog.i18nc("@label", " plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?")
wrapMode: Text.Wrap
}
TextArea
{
id: licenseText
anchors.top: licenseTitle.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: UM.Theme.getSize("default_margin").height
readOnly: true
text: licenseDialog.licenseContent
}
}
rightButtons: [
Button
{
id: acceptButton
anchors.margins: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@action:button", "Accept")
onClicked:
{
licenseDialog.close();
manager.installPlugin(licenseDialog.pluginFileLocation);
}
},
Button
{
id: declineButton
anchors.margins: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@action:button", "Decline")
onClicked:
{
licenseDialog.close();
}
}
]
}
} }
} }