PluginBrowser.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # PluginBrowser is released under the terms of the LGPLv3 or higher.
  3. from UM.Extension import Extension
  4. from UM.i18n import i18nCatalog
  5. from UM.Logger import Logger
  6. from UM.Qt.ListModel import ListModel
  7. from UM.PluginRegistry import PluginRegistry
  8. from UM.Application import Application
  9. from UM.Version import Version
  10. from UM.Message import Message
  11. from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
  12. from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
  13. import json
  14. import os
  15. import tempfile
  16. import platform
  17. import zipfile
  18. i18n_catalog = i18nCatalog("cura")
  19. class PluginBrowser(QObject, Extension):
  20. def __init__(self, parent=None):
  21. super().__init__(parent)
  22. self._api_version = 2
  23. self._api_url = "http://software.ultimaker.com/cura/v%s/" % self._api_version
  24. self._plugin_list_request = None
  25. self._download_plugin_request = None
  26. self._download_plugin_reply = None
  27. self._network_manager = None
  28. self._plugins_metadata = []
  29. self._plugins_model = None
  30. self._dialog = None
  31. self._download_progress = 0
  32. self._is_downloading = False
  33. self._request_header = [b"User-Agent",
  34. str.encode("%s/%s (%s %s)" % (Application.getInstance().getApplicationName(),
  35. Application.getInstance().getVersion(),
  36. platform.system(),
  37. platform.machine(),
  38. )
  39. )
  40. ]
  41. # Installed plugins are really installed after reboot. In order to prevent the user from downloading the
  42. # same file over and over again, we keep track of the upgraded plugins.
  43. self._newly_installed_plugin_ids = []
  44. # variables for the license agreement dialog
  45. self._license_dialog_plugin_name = ""
  46. self._license_dialog_license_content = ""
  47. self._license_dialog_plugin_file_location = ""
  48. showLicenseDialog = pyqtSignal()
  49. @pyqtSlot(result = str)
  50. def getLicenseDialogPluginName(self):
  51. return self._license_dialog_plugin_name
  52. @pyqtSlot(result = str)
  53. def getLicenseDialogPluginFileLocation(self):
  54. return self._license_dialog_plugin_file_location
  55. @pyqtSlot(result = str)
  56. def getLicenseDialogLicenseContent(self):
  57. return self._license_dialog_license_content
  58. def openLicenseDialog(self, plugin_name, license_content, plugin_file_location):
  59. self._license_dialog_plugin_name = plugin_name
  60. self._license_dialog_license_content = license_content
  61. self._license_dialog_plugin_file_location = plugin_file_location
  62. self.showLicenseDialog.emit()
  63. pluginsMetadataChanged = pyqtSignal()
  64. onDownloadProgressChanged = pyqtSignal()
  65. onIsDownloadingChanged = pyqtSignal()
  66. @pyqtProperty(bool, notify = onIsDownloadingChanged)
  67. def isDownloading(self):
  68. return self._is_downloading
  69. @pyqtSlot()
  70. def browsePlugins(self):
  71. self._createNetworkManager()
  72. self.requestPluginList()
  73. if not self._dialog:
  74. self._dialog = self._createDialog("PluginBrowser.qml")
  75. self._dialog.show()
  76. @pyqtSlot()
  77. def requestPluginList(self):
  78. Logger.log("i", "Requesting plugin list")
  79. url = QUrl(self._api_url + "plugins")
  80. self._plugin_list_request = QNetworkRequest(url)
  81. self._plugin_list_request.setRawHeader(*self._request_header)
  82. self._network_manager.get(self._plugin_list_request)
  83. def _createDialog(self, qml_name):
  84. Logger.log("d", "Creating dialog [%s]", qml_name)
  85. path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
  86. dialog = Application.getInstance().createQmlComponent(path, {"manager": self})
  87. return dialog
  88. def setIsDownloading(self, is_downloading):
  89. if self._is_downloading != is_downloading:
  90. self._is_downloading = is_downloading
  91. self.onIsDownloadingChanged.emit()
  92. def _onDownloadPluginProgress(self, bytes_sent, bytes_total):
  93. if bytes_total > 0:
  94. new_progress = bytes_sent / bytes_total * 100
  95. self.setDownloadProgress(new_progress)
  96. if new_progress == 100.0:
  97. self.setIsDownloading(False)
  98. self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
  99. # must not delete the temporary file on Windows
  100. self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curaplugin", delete = False)
  101. location = self._temp_plugin_file.name
  102. # write first and close, otherwise on Windows, it cannot read the file
  103. self._temp_plugin_file.write(self._download_plugin_reply.readAll())
  104. self._temp_plugin_file.close()
  105. self._checkPluginLicenseOrInstall(location)
  106. return
  107. ## Checks if the downloaded plugin ZIP file contains a license file or not.
  108. # If it does, it will show a popup dialog displaying the license to the user. The plugin will be installed if the
  109. # user accepts the license.
  110. # If there is no license file, the plugin will be directory installed.
  111. def _checkPluginLicenseOrInstall(self, file_path):
  112. with zipfile.ZipFile(file_path, "r") as zip_ref:
  113. plugin_id = None
  114. for file in zip_ref.infolist():
  115. if file.filename.endswith("/"):
  116. plugin_id = file.filename.strip("/")
  117. break
  118. if plugin_id is None:
  119. msg = i18n_catalog.i18nc("@info:status", "Failed to get plugin ID from <filename>{0}</filename>", file_path)
  120. msg_title = i18n_catalog.i18nc("@info:tile", "Warning")
  121. self._progress_message = Message(msg, lifetime=0, dismissable=False, title = msg_title)
  122. return
  123. # find a potential license file
  124. plugin_root_dir = plugin_id + "/"
  125. license_file = None
  126. for f in zip_ref.infolist():
  127. # skip directories (with file_size = 0) and files not in the plugin directory
  128. if f.file_size == 0 or not f.filename.startswith(plugin_root_dir):
  129. continue
  130. file_name = os.path.basename(f.filename).lower()
  131. file_base_name, file_ext = os.path.splitext(file_name)
  132. if file_base_name in ["license", "licence"]:
  133. license_file = f.filename
  134. break
  135. # show a dialog for user to read and accept/decline the license
  136. if license_file is not None:
  137. Logger.log("i", "Found license file for plugin [%s], showing the license dialog to the user", plugin_id)
  138. license_content = zip_ref.read(license_file).decode('utf-8')
  139. self.openLicenseDialog(plugin_id, license_content, file_path)
  140. return
  141. # there is no license file, directly install the plugin
  142. self.installPlugin(file_path)
  143. @pyqtSlot(str)
  144. def installPlugin(self, file_path):
  145. if not file_path.startswith("/"):
  146. location = "/" + file_path # Ensure that it starts with a /, as otherwise it doesn't work on windows.
  147. else:
  148. location = file_path
  149. result = PluginRegistry.getInstance().installPlugin("file://" + location)
  150. self._newly_installed_plugin_ids.append(result["id"])
  151. self.pluginsMetadataChanged.emit()
  152. Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
  153. @pyqtProperty(int, notify = onDownloadProgressChanged)
  154. def downloadProgress(self):
  155. return self._download_progress
  156. def setDownloadProgress(self, progress):
  157. if progress != self._download_progress:
  158. self._download_progress = progress
  159. self.onDownloadProgressChanged.emit()
  160. @pyqtSlot(str)
  161. def downloadAndInstallPlugin(self, url):
  162. Logger.log("i", "Attempting to download & install plugin from %s", url)
  163. url = QUrl(url)
  164. self._download_plugin_request = QNetworkRequest(url)
  165. self._download_plugin_request.setRawHeader(*self._request_header)
  166. self._download_plugin_reply = self._network_manager.get(self._download_plugin_request)
  167. self.setDownloadProgress(0)
  168. self.setIsDownloading(True)
  169. self._download_plugin_reply.downloadProgress.connect(self._onDownloadPluginProgress)
  170. @pyqtSlot()
  171. def cancelDownload(self):
  172. Logger.log("i", "user cancelled the download of a plugin")
  173. self._download_plugin_reply.abort()
  174. self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
  175. self._download_plugin_reply = None
  176. self._download_plugin_request = None
  177. self.setDownloadProgress(0)
  178. self.setIsDownloading(False)
  179. @pyqtProperty(QObject, notify=pluginsMetadataChanged)
  180. def pluginsModel(self):
  181. if self._plugins_model is None:
  182. self._plugins_model = ListModel()
  183. self._plugins_model.addRoleName(Qt.UserRole + 1, "name")
  184. self._plugins_model.addRoleName(Qt.UserRole + 2, "version")
  185. self._plugins_model.addRoleName(Qt.UserRole + 3, "short_description")
  186. self._plugins_model.addRoleName(Qt.UserRole + 4, "author")
  187. self._plugins_model.addRoleName(Qt.UserRole + 5, "already_installed")
  188. self._plugins_model.addRoleName(Qt.UserRole + 6, "file_location")
  189. self._plugins_model.addRoleName(Qt.UserRole + 7, "can_upgrade")
  190. else:
  191. self._plugins_model.clear()
  192. items = []
  193. for metadata in self._plugins_metadata:
  194. items.append({
  195. "name": metadata["label"],
  196. "version": metadata["version"],
  197. "short_description": metadata["short_description"],
  198. "author": metadata["author"],
  199. "already_installed": self._checkAlreadyInstalled(metadata["id"]),
  200. "file_location": metadata["file_location"],
  201. "can_upgrade": self._checkCanUpgrade(metadata["id"], metadata["version"])
  202. })
  203. self._plugins_model.setItems(items)
  204. return self._plugins_model
  205. def _checkCanUpgrade(self, id, version):
  206. plugin_registry = PluginRegistry.getInstance()
  207. metadata = plugin_registry.getMetaData(id)
  208. if metadata != {}:
  209. if id in self._newly_installed_plugin_ids:
  210. return False # We already updated this plugin.
  211. current_version = Version(metadata["plugin"]["version"])
  212. new_version = Version(version)
  213. if new_version > current_version:
  214. return True
  215. return False
  216. def _checkAlreadyInstalled(self, id):
  217. plugin_registry = PluginRegistry.getInstance()
  218. metadata = plugin_registry.getMetaData(id)
  219. if metadata != {}:
  220. return True
  221. else:
  222. if id in self._newly_installed_plugin_ids:
  223. return True # We already installed this plugin, but the registry just doesn't know it yet.
  224. return False
  225. def _onRequestFinished(self, reply):
  226. reply_url = reply.url().toString()
  227. if reply.error() == QNetworkReply.TimeoutError:
  228. Logger.log("w", "Got a timeout.")
  229. # Reset everything.
  230. self.setDownloadProgress(0)
  231. self.setIsDownloading(False)
  232. if self._download_plugin_reply:
  233. self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
  234. self._download_plugin_reply.abort()
  235. self._download_plugin_reply = None
  236. return
  237. elif reply.error() == QNetworkReply.HostNotFoundError:
  238. Logger.log("w", "Unable to reach server.")
  239. return
  240. if reply.operation() == QNetworkAccessManager.GetOperation:
  241. if reply_url == self._api_url + "plugins":
  242. try:
  243. json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
  244. self._plugins_metadata = json_data
  245. self.pluginsMetadataChanged.emit()
  246. except json.decoder.JSONDecodeError:
  247. Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
  248. return
  249. else:
  250. # Ignore any operation that is not a get operation
  251. pass
  252. def _onNetworkAccesibleChanged(self, accessible):
  253. if accessible == 0:
  254. self.setDownloadProgress(0)
  255. self.setIsDownloading(False)
  256. if self._download_plugin_reply:
  257. self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
  258. self._download_plugin_reply.abort()
  259. self._download_plugin_reply = None
  260. def _createNetworkManager(self):
  261. if self._network_manager:
  262. self._network_manager.finished.disconnect(self._onRequestFinished)
  263. self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged)
  264. self._network_manager = QNetworkAccessManager()
  265. self._network_manager.finished.connect(self._onRequestFinished)
  266. self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged)