Sat, 23 Dec 2023 15:48:12 +0100
Updated copyright for 2024.
# -*- coding: utf-8 -*- # Copyright (c) 2007 - 2024 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a dialog showing the available plugins. """ import enum import glob import os import re import zipfile from collections import ChainMap, defaultdict from PyQt6.QtCore import ( QCoreApplication, QFile, QIODevice, QPoint, QProcess, Qt, QUrl, pyqtSignal, pyqtSlot, ) from PyQt6.QtNetwork import ( QNetworkAccessManager, QNetworkInformation, QNetworkReply, QNetworkRequest, ) from PyQt6.QtWidgets import ( QAbstractButton, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QMenu, QToolButton, QTreeWidgetItem, QVBoxLayout, QWidget, ) from eric7 import Globals, Preferences from eric7.EricGui import EricPixmapCache from eric7.EricNetwork.EricNetworkProxyFactory import proxyAuthenticationRequired from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp from eric7.EricWidgets.EricMainWindow import EricMainWindow from eric7.EricXML.PluginRepositoryReader import PluginRepositoryReader from eric7.Globals import getConfig from eric7.SystemUtilities import PythonUtilities try: from eric7.EricNetwork.EricSslErrorHandler import ( EricSslErrorHandler, EricSslErrorState, ) SSL_AVAILABLE = True except ImportError: SSL_AVAILABLE = False from .PluginManager import PluginManager from .Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog class PluginStatus(enum.Enum): """ Class defining the various plugin status. """ UpToDate = 0 New = 1 LocalUpdate = 2 RemoteUpdate = 3 Error = 4 class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog): """ Class implementing a dialog showing the available plugins. @signal closeAndInstall() emitted when the Close & Install button is pressed """ closeAndInstall = pyqtSignal() DescrRole = Qt.ItemDataRole.UserRole UrlRole = Qt.ItemDataRole.UserRole + 1 FilenameRole = Qt.ItemDataRole.UserRole + 2 AuthorRole = Qt.ItemDataRole.UserRole + 3 def __init__(self, pluginManager, integrated=False, parent=None): """ Constructor @param pluginManager reference to the plugin manager object @type PluginManager @param integrated flag indicating the integration into the sidebar @type bool @param parent parent of this dialog @type QWidget """ super().__init__(parent) self.setupUi(self) if pluginManager is None: # started as external plug-in repository dialog self.__pluginManager = PluginManager() self.__external = True else: self.__pluginManager = pluginManager self.__external = False self.__integratedWidget = integrated self.__statusTranslations = { "stable": self.tr("Stable"), "unstable": self.tr("Unstable"), "obsolete": self.tr("Obsolete"), "unknown": self.tr("Unknown"), } self.__initHeaderItemsCache() if self.__integratedWidget: self.layout().setContentsMargins(0, 3, 0, 0) self.__actionButtonsLayout = QHBoxLayout() self.__actionButtonsLayout.addStretch() self.__updateButton = QToolButton(self) self.__updateButton.setIcon(EricPixmapCache.getIcon("reload")) self.__updateButton.setToolTip(self.tr("Update")) self.__updateButton.clicked.connect(self.__updateList) self.__actionButtonsLayout.addWidget(self.__updateButton) self.__downloadButton = QToolButton(self) self.__downloadButton.setIcon(EricPixmapCache.getIcon("download")) self.__downloadButton.setToolTip(self.tr("Download")) self.__downloadButton.clicked.connect(self.__downloadButtonClicked) self.__actionButtonsLayout.addWidget(self.__downloadButton) self.__downloadInstallButton = QToolButton(self) self.__downloadInstallButton.setIcon( EricPixmapCache.getIcon("downloadPlus") ) self.__downloadInstallButton.setToolTip(self.tr("Download & Install")) self.__downloadInstallButton.clicked.connect( self.__downloadInstallButtonClicked ) self.__actionButtonsLayout.addWidget(self.__downloadInstallButton) self.__downloadCancelButton = QToolButton(self) self.__downloadCancelButton.setIcon(EricPixmapCache.getIcon("cancel")) self.__downloadCancelButton.setToolTip(self.tr("Cancel")) self.__downloadCancelButton.clicked.connect(self.__downloadCancel) self.__actionButtonsLayout.addWidget(self.__downloadCancelButton) self.__installButton = QToolButton(self) self.__installButton.setIcon(EricPixmapCache.getIcon("plus")) self.__installButton.setToolTip(self.tr("Install")) self.__installButton.clicked.connect(self.__closeAndInstall) self.__actionButtonsLayout.addWidget(self.__installButton) self.__actionButtonsLayout.addStretch() self.layout().insertLayout(0, self.__actionButtonsLayout) self.buttonBox.hide() self.repositoryList.setHeaderHidden(True) else: self.__updateButton = self.buttonBox.addButton( self.tr("Update"), QDialogButtonBox.ButtonRole.ActionRole ) self.__downloadButton = self.buttonBox.addButton( self.tr("Download"), QDialogButtonBox.ButtonRole.ActionRole ) self.__downloadInstallButton = self.buttonBox.addButton( self.tr("Download && Install"), QDialogButtonBox.ButtonRole.ActionRole ) self.__downloadCancelButton = self.buttonBox.addButton( self.tr("Cancel"), QDialogButtonBox.ButtonRole.ActionRole ) self.__installButton = self.buttonBox.addButton( self.tr("Close && Install"), QDialogButtonBox.ButtonRole.ActionRole ) self.__closeButton = self.buttonBox.addButton( self.tr("Close"), QDialogButtonBox.ButtonRole.RejectRole ) self.__closeButton.setEnabled(True) self.repositoryList.headerItem().setText( self.repositoryList.columnCount(), "" ) self.repositoryList.header().setSortIndicator( 0, Qt.SortOrder.AscendingOrder ) self.__downloadButton.setEnabled(False) self.__downloadInstallButton.setEnabled(False) self.__downloadCancelButton.setEnabled(False) self.__installButton.setEnabled(False) self.repositoryUrlEdit.setText(Preferences.getUI("PluginRepositoryUrl7")) self.__pluginContextMenu = QMenu(self) self.__hideAct = self.__pluginContextMenu.addAction( self.tr("Hide"), self.__hidePlugin ) self.__hideSelectedAct = self.__pluginContextMenu.addAction( self.tr("Hide Selected"), self.__hideSelectedPlugins ) self.__pluginContextMenu.addSeparator() self.__showAllAct = self.__pluginContextMenu.addAction( self.tr("Show All"), self.__showAllPlugins ) self.__pluginContextMenu.addSeparator() self.__pluginContextMenu.addAction( self.tr("Cleanup Downloads"), self.__cleanupDownloads ) self.pluginRepositoryFile = os.path.join( Globals.getConfigDir(), "PluginRepository" ) self.__pluginManager.pluginRepositoryFileDownloaded.connect(self.__populateList) # attributes for the network objects self.__networkManager = QNetworkAccessManager(self) self.__networkManager.proxyAuthenticationRequired.connect( proxyAuthenticationRequired ) if SSL_AVAILABLE: self.__sslErrorHandler = EricSslErrorHandler(self) self.__networkManager.sslErrors.connect(self.__sslErrors) self.__replies = [] if Preferences.getUI("DynamicOnlineCheck") and QNetworkInformation.load( QNetworkInformation.Feature.Reachability ): self.__reachabilityChanged(QNetworkInformation.instance().reachability()) QNetworkInformation.instance().reachabilityChanged.connect( self.__reachabilityChanged ) else: # assume to be 'always online' if no backend could be loaded or # dynamic online check is switched of self.__reachabilityChanged(QNetworkInformation.Reachability.Online) self.__pluginsToDownload = [] self.__pluginsDownloaded = [] self.__isDownloadInstall = False self.__allDownloadedOk = False self.__hiddenPlugins = Preferences.getPluginManager("HiddenPlugins") self.__populateList() def __reachabilityChanged(self, reachability): """ Private slot handling reachability state changes. @param reachability new reachability state @type QNetworkInformation.Reachability """ online = reachability == QNetworkInformation.Reachability.Online self.__online = online self.__updateButton.setEnabled(online) self.on_repositoryList_itemSelectionChanged() if not self.__integratedWidget: msg = ( self.tr("Internet Reachability Status: Reachable") if online else self.tr("Internet Reachability Status: Not Reachable") ) self.statusLabel.setText(msg) @pyqtSlot(QAbstractButton) def on_buttonBox_clicked(self, button): """ Private slot to handle the click of a button of the button box. @param button reference to the button pressed @type QAbstractButton """ if button == self.__updateButton: self.__updateList() elif button == self.__downloadButton: self.__downloadButtonClicked() elif button == self.__downloadInstallButton: self.__downloadInstallButtonClicked() elif button == self.__downloadCancelButton: self.__downloadCancel() elif button == self.__installButton: self.__closeAndInstall() @pyqtSlot() def __downloadButtonClicked(self): """ Private slot to handle a click of the Download button. """ self.__isDownloadInstall = False self.__downloadPlugins() @pyqtSlot() def __downloadInstallButtonClicked(self): """ Private slot to handle a click of the Download & Install button. """ self.__isDownloadInstall = True self.__allDownloadedOk = True self.__downloadPlugins() def __formatDescription(self, lines): """ Private method to format the description. @param lines lines of the description @type list of str @return formatted description @rtype str """ # remove empty line at start and end newlines = lines[:] if len(newlines) and newlines[0] == "": del newlines[0] if len(newlines) and newlines[-1] == "": del newlines[-1] # replace empty lines by newline character index = 0 while index < len(newlines): if newlines[index] == "": newlines[index] = "\n" index += 1 # join lines by a blank return " ".join(newlines) def __changeScheme(self, url, newScheme=""): """ Private method to change the scheme of the given URL. @param url URL to be modified @type str @param newScheme scheme to be set for the given URL @type str @return modified URL @rtype str """ if not newScheme: newScheme = ( "http:" if Preferences.getPluginManager("ForceHttpPluginDownload") else self.repositoryUrlEdit.text().split("//", 1)[0] ) return newScheme + "//" + url.split("//", 1)[1] @pyqtSlot(QPoint) def on_repositoryList_customContextMenuRequested(self, pos): """ Private slot to show the context menu. @param pos position to show the menu @type QPoint """ self.__hideAct.setEnabled( self.repositoryList.currentItem() is not None and len(self.__selectedItems()) == 1 ) self.__hideSelectedAct.setEnabled(len(self.__selectedItems()) > 1) self.__showAllAct.setEnabled(bool(self.__hasHiddenPlugins())) self.__pluginContextMenu.popup(self.repositoryList.mapToGlobal(pos)) @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) def on_repositoryList_currentItemChanged(self, current, previous): """ Private slot to handle the change of the current item. @param current reference to the new current item @type QTreeWidgetItem @param previous reference to the old current item @type QTreeWidgetItem """ if self.__repositoryMissing or current is None: self.descriptionEdit.clear() self.authorEdit.clear() return url = current.data(0, PluginRepositoryWidget.UrlRole) url = "" if url is None else self.__changeScheme(url) self.urlEdit.setText(url) self.descriptionEdit.setPlainText( current.data(0, PluginRepositoryWidget.DescrRole) and self.__formatDescription( current.data(0, PluginRepositoryWidget.DescrRole) ) or "" ) self.authorEdit.setText( current.data(0, PluginRepositoryWidget.AuthorRole) or "" ) def __selectedItems(self): """ Private method to get all selected items without the status and category items. @return list of selected items without header items @rtype list of QTreeWidgetItem """ selectedItems = [] allCategoryItems = ChainMap(*self.__categoryItems.values()) for itm in self.repositoryList.selectedItems(): if ( itm not in self.__statusItems.values() and itm not in allCategoryItems.values() ): selectedItems.append(itm) return selectedItems @pyqtSlot() def on_repositoryList_itemSelectionChanged(self): """ Private slot to handle a change of the selection. """ enable = bool(self.__selectedItems()) self.__downloadButton.setEnabled(enable and self.__online) self.__downloadInstallButton.setEnabled(enable and self.__online) self.__installButton.setEnabled(enable) def reloadList(self): """ Public method to reload the list of plugins. """ self.__populateList() @pyqtSlot() def __updateList(self): """ Private slot to download a new list and display the contents. """ url = self.repositoryUrlEdit.text() if Preferences.getPluginManager("ForceHttpPluginDownload"): url = url.replace("https://", "http://") self.__pluginManager.downLoadRepositoryFile(url=url) def __downloadPluginDone(self, status, filename): """ Private method called, when the download of a plugin is finished. @param status flag indicating a successful download @type bool @param filename full path of the downloaded file @type str """ if status: self.__pluginsDownloaded.append(filename) if self.__isDownloadInstall: self.__allDownloadedOk &= status if len(self.__pluginsToDownload): self.__pluginsToDownload.pop(0) if len(self.__pluginsToDownload): self.__downloadPlugin() else: self.__downloadPluginsDone() def __downloadPlugin(self): """ Private method to download the next plugin. """ self.__downloadFile( self.__pluginsToDownload[0][0], self.__pluginsToDownload[0][1], self.__downloadPluginDone, ) def __downloadPlugins(self): """ Private slot to download the selected plugins. """ self.__pluginsDownloaded = [] self.__pluginsToDownload = [] self.__downloadButton.setEnabled(False) self.__downloadInstallButton.setEnabled(False) self.__installButton.setEnabled(False) newScheme = ( "http:" if Preferences.getPluginManager("ForceHttpPluginDownload") else self.repositoryUrlEdit.text().split("//", 1)[0] ) for itm in self.__selectedItems(): url = self.__changeScheme( itm.data(0, PluginRepositoryWidget.UrlRole), newScheme ) filename = os.path.join( Preferences.getPluginManager("DownloadPath"), itm.data(0, PluginRepositoryWidget.FilenameRole), ) self.__pluginsToDownload.append((url, filename)) if self.__pluginsToDownload: self.__downloadPlugin() def __downloadPluginsDone(self): """ Private method called, when the download of the plugins is finished. """ self.__downloadButton.setEnabled(len(self.__selectedItems())) self.__downloadInstallButton.setEnabled(len(self.__selectedItems())) self.__installButton.setEnabled(len(self.__selectedItems())) ui = ericApp().getObject("UserInterface") if not self.__external else None if ui is not None: ui.showNotification( EricPixmapCache.getPixmap("plugin48"), self.tr("Download Plugin Files"), self.tr("""The requested plugins were downloaded."""), ) if self.__isDownloadInstall: self.closeAndInstall.emit() else: if ui is None: EricMessageBox.information( self, self.tr("Download Plugin Files"), self.tr("""The requested plugins were downloaded."""), ) self.downloadProgress.setValue(0) # repopulate the list to update the refresh icons self.__populateList() def __resortRepositoryList(self): """ Private method to resort the tree. """ if self.__integratedWidget: self.repositoryList.sortItems( self.repositoryList.sortColumn(), Qt.SortOrder.AscendingOrder, ) else: self.repositoryList.sortItems( self.repositoryList.sortColumn(), self.repositoryList.header().sortIndicatorOrder(), ) def __initHeaderItemsCache(self): """ Private method to initialize the cache variables for the header items. """ self.__statusItems = defaultdict(lambda: None) self.__categoryItems = defaultdict(dict) def __populateList(self): """ Private method to populate the list of available plugins. """ self.__initHeaderItemsCache() self.repositoryList.clear() self.__newItems = 0 self.__updateLocalItems = 0 self.__updateRemoteItems = 0 self.downloadProgress.setValue(0) if os.path.exists(self.pluginRepositoryFile): self.__repositoryMissing = False f = QFile(self.pluginRepositoryFile) if f.open(QIODevice.OpenModeFlag.ReadOnly): reader = PluginRepositoryReader(f, self.addEntry) reader.readXML() self.repositoryList.resizeColumnToContents(0) self.repositoryList.resizeColumnToContents(1) self.repositoryList.resizeColumnToContents(2) self.__resortRepositoryList() url = Preferences.getUI("PluginRepositoryUrl7") if url != self.repositoryUrlEdit.text(): self.repositoryUrlEdit.setText(url) EricMessageBox.warning( self, self.tr("Plugins Repository URL Changed"), self.tr( """The URL of the Plugins Repository has""" """ changed. Select the "Update" button to get""" """ the new repository file.""" ), ) else: EricMessageBox.critical( self, self.tr("Read plugins repository file"), self.tr( "<p>The plugins repository file <b>{0}</b> " "could not be read. Select Update</p>" ).format(self.pluginRepositoryFile), ) else: self.__repositoryMissing = True QTreeWidgetItem( self.repositoryList, ["", self.tr("No plugin repository file available.\nSelect Update.")], ) self.repositoryList.resizeColumnToContents(1) self.newLabel.setText(self.tr("New: <b>{0}</b>").format(self.__newItems)) self.updateLocalLabel.setText( self.tr("Local Updates: <b>{0}</b>").format(self.__updateLocalItems) ) self.updateRemoteLabel.setText( self.tr("Remote Updates: <b>{0}</b>").format(self.__updateRemoteItems) ) def __downloadFile(self, url, filename, doneMethod=None): """ Private slot to download the given file. @param url URL for the download @type str @param filename local name of the file @type str @param doneMethod method to be called when done @type function """ if self.__online: self.__updateButton.setEnabled(False) self.__downloadButton.setEnabled(False) self.__downloadInstallButton.setEnabled(False) if not self.__integratedWidget: self.__closeButton.setEnabled(False) self.__downloadCancelButton.setEnabled(True) if Preferences.getPluginManager("ForceHttpPluginDownload"): url = url.replace("https://", "http://") self.statusLabel.setText(url) request = QNetworkRequest(QUrl(url)) request.setAttribute( QNetworkRequest.Attribute.CacheLoadControlAttribute, QNetworkRequest.CacheLoadControl.AlwaysNetwork, ) reply = self.__networkManager.get(request) reply.finished.connect( lambda: self.__downloadFileDone(reply, filename, doneMethod) ) reply.downloadProgress.connect(self.__downloadProgress) self.__replies.append(reply) else: EricMessageBox.warning( self, self.tr("Error downloading file"), self.tr( """<p>Could not download the requested file""" """ from {0}.</p><p>Error: {1}</p>""" ).format(url, self.tr("No connection to Internet.")), ) def __downloadFileDone(self, reply, fileName, doneMethod): """ Private method called, after the file has been downloaded from the Internet. @param reply reference to the reply object of the download @type QNetworkReply @param fileName local name of the file @type str @param doneMethod method to be called when done @type function """ self.__updateButton.setEnabled(True) if not self.__integratedWidget: self.__closeButton.setEnabled(True) self.__downloadCancelButton.setEnabled(False) ok = True if reply in self.__replies: self.__replies.remove(reply) if reply.error() != QNetworkReply.NetworkError.NoError: ok = False if reply.error() != QNetworkReply.NetworkError.OperationCanceledError: EricMessageBox.warning( self, self.tr("Error downloading file"), self.tr( """<p>Could not download the requested file""" """ from {0}.</p><p>Error: {1}</p>""" ).format(reply.url().toString(), reply.errorString()), ) self.downloadProgress.setValue(0) if self.repositoryList.topLevelItemCount(): if self.repositoryList.currentItem() is None: self.repositoryList.setCurrentItem( self.repositoryList.topLevelItem(0) ) else: self.__downloadButton.setEnabled(len(self.__selectedItems())) self.__downloadInstallButton.setEnabled(len(self.__selectedItems())) reply.deleteLater() return downloadIODevice = QFile(fileName + ".tmp") downloadIODevice.open(QIODevice.OpenModeFlag.WriteOnly) # read data in chunks chunkSize = 64 * 1024 * 1024 while True: data = reply.read(chunkSize) if data is None or len(data) == 0: break downloadIODevice.write(data) downloadIODevice.close() if QFile.exists(fileName): QFile.remove(fileName) downloadIODevice.rename(fileName) reply.deleteLater() if doneMethod is not None: doneMethod(ok, fileName) def __downloadCancel(self, reply=None): """ Private slot to cancel the current download. @param reply reference to the network reply @type QNetworkReply """ if reply is None and bool(self.__replies): reply = self.__replies[0] self.__pluginsToDownload = [] if reply is not None: reply.abort() def __downloadProgress(self, done, total): """ Private slot to show the download progress. @param done number of bytes downloaded so far @type int @param total total bytes to be downloaded @type int """ if total: self.downloadProgress.setMaximum(total) self.downloadProgress.setValue(done) def addEntry( self, name, short, description, url, author, version, filename, status, category, ): """ Public method to add an entry to the list. @param name data for the name field @type str @param short data for the short field @type str @param description data for the description field @type list of str @param url data for the url field @type str @param author data for the author field @type str @param version data for the version field @type str @param filename data for the filename field @type str @param status status of the plugin (one of stable, unstable, unknown) @type str @param category category designation of the plugin @type str """ pluginName = filename.rsplit("-", 1)[0] if pluginName in self.__hiddenPlugins: return # 1. determine and create the status item statusItem = self.__statusItems[status] if statusItem is None: statusItem = QTreeWidgetItem( self.repositoryList, [ self.__statusTranslations.get( status, self.__statusTranslations["unknown"] ) ], ) statusItem.setExpanded(True) statusItem.setFirstColumnSpanned(True) self.__statusItems[status] = statusItem # 2. determine and create the category item try: categoryItem = self.__categoryItems[status][category] except KeyError: # create the category item categoryItem = QTreeWidgetItem(statusItem, [category]) categoryItem.setExpanded(True) categoryItem.setFirstColumnSpanned(True) self.__categoryItems[status][category] = categoryItem # 3. create the plugin item if self.__integratedWidget: entryFormat = "<b>{0}</b> - Version: <i>{1}</i><br/>{2}" itm = QTreeWidgetItem(categoryItem) itm.setFirstColumnSpanned(True) label = QLabel(entryFormat.format(name, version, short)) self.repositoryList.setItemWidget(itm, 0, label) else: itm = QTreeWidgetItem(categoryItem, [name, version, short]) itm.setData(0, PluginRepositoryWidget.UrlRole, url) itm.setData(0, PluginRepositoryWidget.FilenameRole, filename) itm.setData(0, PluginRepositoryWidget.AuthorRole, author) itm.setData(0, PluginRepositoryWidget.DescrRole, description) iconColumn = 0 if self.__integratedWidget else 1 updateStatus = self.__updateStatus(filename, version) if updateStatus == PluginStatus.UpToDate: itm.setIcon(iconColumn, EricPixmapCache.getIcon("empty")) itm.setToolTip(iconColumn, self.tr("up-to-date")) elif updateStatus == PluginStatus.New: itm.setIcon(iconColumn, EricPixmapCache.getIcon("download")) itm.setToolTip(iconColumn, self.tr("new download available")) self.__newItems += 1 elif updateStatus == PluginStatus.LocalUpdate: itm.setIcon(iconColumn, EricPixmapCache.getIcon("updateLocal")) itm.setToolTip(iconColumn, self.tr("update installable")) self.__updateLocalItems += 1 elif updateStatus == PluginStatus.RemoteUpdate: itm.setIcon(iconColumn, EricPixmapCache.getIcon("updateRemote")) itm.setToolTip(iconColumn, self.tr("updated download available")) self.__updateRemoteItems += 1 elif updateStatus == PluginStatus.Error: itm.setIcon(iconColumn, EricPixmapCache.getIcon("warning")) itm.setToolTip(iconColumn, self.tr("error determining status")) def __updateStatus(self, filename, version): """ Private method to check the given archive update status. @param filename data for the filename field @type str @param version data for the version field @type str @return plug-in update status @rtype int (one of PluginStatusNew, PluginStatusUpToDate, PluginStatusLocalUpdate, PluginStatusRemoteUpdate) """ archive = os.path.join(Preferences.getPluginManager("DownloadPath"), filename) # check, if it is an update (i.e. we already have archives # with the same pattern) archivesPattern = archive.rsplit("-", 1)[0] + "-*.zip" if len(glob.glob(archivesPattern)) == 0: # Check against installed/loaded plug-ins pluginName = filename.rsplit("-", 1)[0] pluginDetails = self.__pluginManager.getPluginDetails(pluginName) if pluginDetails is None or pluginDetails["moduleName"] != pluginName: return PluginStatus.New if pluginDetails["error"]: return PluginStatus.Error pluginVersionTuple = Globals.versionToTuple(pluginDetails["version"])[:3] versionTuple = Globals.versionToTuple(version)[:3] if pluginVersionTuple < versionTuple: return PluginStatus.RemoteUpdate else: return PluginStatus.UpToDate # check, if the archive exists if not os.path.exists(archive): return PluginStatus.RemoteUpdate # check, if the archive is a valid zip file if not zipfile.is_zipfile(archive): return PluginStatus.RemoteUpdate zipFile = zipfile.ZipFile(archive, "r") try: aversion = zipFile.read("VERSION").decode("utf-8") except KeyError: aversion = "" zipFile.close() if aversion == version: # Check against installed/loaded plug-ins pluginName = filename.rsplit("-", 1)[0] pluginDetails = self.__pluginManager.getPluginDetails(pluginName) if pluginDetails is None: return PluginStatus.LocalUpdate if ( Globals.versionToTuple(pluginDetails["version"])[:3] < Globals.versionToTuple(version)[:3] ): return PluginStatus.LocalUpdate else: return PluginStatus.UpToDate else: return PluginStatus.RemoteUpdate def __sslErrors(self, reply, errors): """ Private slot to handle SSL errors. @param reply reference to the reply object @type QNetworkReply @param errors list of SSL errors @type list of QSslError """ ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0] if ignored == EricSslErrorState.NOT_IGNORED: self.__downloadCancel(reply) def getDownloadedPlugins(self): """ Public method to get the list of recently downloaded plugin files. @return list of plugin filenames @rtype list of str """ return self.__pluginsDownloaded @pyqtSlot(bool) def on_repositoryUrlEditButton_toggled(self, checked): """ Private slot to set the read only status of the repository URL line edit. @param checked state of the push button @type bool """ self.repositoryUrlEdit.setReadOnly(not checked) def __closeAndInstall(self): """ Private method to close the dialog and invoke the install dialog. """ if not self.__pluginsDownloaded and self.__selectedItems(): for itm in self.__selectedItems(): filename = os.path.join( Preferences.getPluginManager("DownloadPath"), itm.data(0, PluginRepositoryWidget.FilenameRole), ) self.__pluginsDownloaded.append(filename) self.closeAndInstall.emit() def __hidePlugin(self): """ Private slot to hide the current plug-in. """ itm = self.__selectedItems()[0] pluginName = itm.data(0, PluginRepositoryWidget.FilenameRole).rsplit("-", 1)[0] self.__updateHiddenPluginsList([pluginName]) def __hideSelectedPlugins(self): """ Private slot to hide all selected plug-ins. """ hideList = [] for itm in self.__selectedItems(): pluginName = itm.data(0, PluginRepositoryWidget.FilenameRole).rsplit( "-", 1 )[0] hideList.append(pluginName) self.__updateHiddenPluginsList(hideList) def __showAllPlugins(self): """ Private slot to show all plug-ins. """ self.__hiddenPlugins = [] self.__updateHiddenPluginsList([]) def __hasHiddenPlugins(self): """ Private method to check, if there are any hidden plug-ins. @return flag indicating the presence of hidden plug-ins @rtype bool """ return bool(self.__hiddenPlugins) def __updateHiddenPluginsList(self, hideList): """ Private method to store the list of hidden plug-ins to the settings. @param hideList list of plug-ins to add to the list of hidden ones @type list of str """ if hideList: self.__hiddenPlugins.extend( [p for p in hideList if p not in self.__hiddenPlugins] ) Preferences.setPluginManager("HiddenPlugins", self.__hiddenPlugins) self.__populateList() def __cleanupDownloads(self): """ Private slot to cleanup the plug-in downloads area. """ PluginRepositoryDownloadCleanup() class PluginRepositoryDialog(QDialog): """ Class for the dialog variant. """ def __init__(self, pluginManager, parent=None): """ Constructor @param pluginManager reference to the plugin manager object @type PluginManager @param parent reference to the parent widget @type QWidget """ super().__init__(parent) self.setSizeGripEnabled(True) self.__layout = QVBoxLayout(self) self.__layout.setContentsMargins(0, 0, 0, 0) self.setLayout(self.__layout) self.cw = PluginRepositoryWidget(pluginManager, parent=self) size = self.cw.size() self.__layout.addWidget(self.cw) self.resize(size) self.setWindowTitle(self.cw.windowTitle()) self.cw.buttonBox.accepted.connect(self.accept) self.cw.buttonBox.rejected.connect(self.reject) self.cw.closeAndInstall.connect(self.__closeAndInstall) def __closeAndInstall(self): """ Private slot to handle the closeAndInstall signal. """ self.done(QDialog.DialogCode.Accepted + 1) def getDownloadedPlugins(self): """ Public method to get the list of recently downloaded plugin files. @return list of plugin filenames @rtype list of str """ return self.cw.getDownloadedPlugins() class PluginRepositoryWindow(EricMainWindow): """ Main window class for the standalone dialog. """ def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget @type QWidget """ super().__init__(parent) self.cw = PluginRepositoryWidget(None, parent=self) size = self.cw.size() self.setCentralWidget(self.cw) self.resize(size) self.setWindowTitle(self.cw.windowTitle()) self.setStyle(Preferences.getUI("Style"), Preferences.getUI("StyleSheet")) self.cw.buttonBox.accepted.connect(self.close) self.cw.buttonBox.rejected.connect(self.close) self.cw.closeAndInstall.connect(self.__startPluginInstall) def __startPluginInstall(self): """ Private slot to start the eric plugin installation dialog. """ proc = QProcess() applPath = os.path.join(getConfig("ericDir"), "eric7_plugininstall.py") args = [] args.append(applPath) args += self.cw.getDownloadedPlugins() if not os.path.isfile(applPath) or not proc.startDetached( PythonUtilities.getPythonExecutable(), args ): EricMessageBox.critical( self, self.tr("Process Generation Error"), self.tr( "<p>Could not start the process.<br>" "Ensure that it is available as <b>{0}</b>.</p>" ).format(applPath), self.tr("OK"), ) self.close() def PluginRepositoryDownloadCleanup(quiet=False): """ Module function to clean up the plug-in downloads area. @param quiet flag indicating quiet operations @type bool """ pluginsRegister = [] # list of plug-ins contained in the repository def registerPlugin( name, # noqa: U100 short, # noqa: U100 description, # noqa: U100 url, author, # noqa: U100 version, # noqa: U100 filename, # noqa: U100 status, # noqa: U100 category, # noqa: U100 ): """ Method to register a plug-in's data. @param name data for the name field @type str @param short data for the short field @type str @param description data for the description field @type list of str @param url data for the url field @type str @param author data for the author field @type str @param version data for the version field @type str @param filename data for the filename field @type str @param status status of the plugin (one of stable, unstable, unknown) @type str @param category category designation of the plugin @type str """ pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0] if pluginName not in pluginsRegister: pluginsRegister.append(pluginName) downloadPath = Preferences.getPluginManager("DownloadPath") downloads = {} # plug-in name as key, file name and version as value # step 1: extract plug-ins and downloaded files with os.scandir(downloadPath) as dirEntriesIterator: for pluginFile in dirEntriesIterator: if not pluginFile.is_file(): continue try: pluginName, pluginVersion = pluginFile.name.replace(".zip", "").rsplit( "-", 1 ) pluginVersionList = re.split("[._-]", pluginVersion) for index in range(len(pluginVersionList)): try: pluginVersionList[index] = int(pluginVersionList[index]) except ValueError: # use default of 0 pluginVersionList[index] = 0 except ValueError: # rsplit() returned just one entry, i.e. file name doesn't contain # version info separated by '-' # => assume version 0.0.0 pluginName = pluginFile.replace(".zip", "") pluginVersionList = [0, 0, 0] if pluginName not in downloads: downloads[pluginName] = [] downloads[pluginName].append((pluginFile, tuple(pluginVersionList))) # step 2: delete old entries hiddenPlugins = Preferences.getPluginManager("HiddenPlugins") for pluginName in downloads: downloads[pluginName].sort(key=lambda x: x[1]) removeFiles = ( [f[0] for f in downloads[pluginName]] if ( pluginName in hiddenPlugins and not Preferences.getPluginManager("KeepHidden") ) else [ f[0] for f in downloads[pluginName][ : -Preferences.getPluginManager("KeepGenerations") ] ] ) for removeFile in removeFiles: try: os.remove(os.path.join(downloadPath, removeFile)) except OSError as err: if not quiet: EricMessageBox.critical( None, QCoreApplication.translate( "PluginRepositoryWidget", "Cleanup of Plugin Downloads" ), QCoreApplication.translate( "PluginRepositoryWidget", """<p>The plugin download <b>{0}</b> could""" """ not be deleted.</p><p>Reason: {1}</p>""", ).format(removeFile, str(err)), ) # step 3: delete entries of obsolete plug-ins pluginRepositoryFile = os.path.join(Globals.getConfigDir(), "PluginRepository") if os.path.exists(pluginRepositoryFile): f = QFile(pluginRepositoryFile) if f.open(QIODevice.OpenModeFlag.ReadOnly): reader = PluginRepositoryReader(f, registerPlugin) reader.readXML() for pluginName in downloads: if pluginName not in pluginsRegister: removeFiles = [f[0] for f in downloads[pluginName]] for removeFile in removeFiles: try: os.remove(os.path.join(downloadPath, removeFile)) except OSError as err: if not quiet: EricMessageBox.critical( None, QCoreApplication.translate( "PluginRepositoryWidget", "Cleanup of Plugin Downloads", ), QCoreApplication.translate( "PluginRepositoryWidget", "<p>The plugin download <b>{0}</b>" " could not be deleted.</p>" "<p>Reason: {1}</p>" "", ).format(removeFile, str(err)), )