--- a/PluginManager/PluginManager.py Fri Dec 13 22:45:47 2013 +0100 +++ b/PluginManager/PluginManager.py Fri Dec 13 23:39:14 2013 +0100 @@ -12,12 +12,23 @@ import os import sys import imp +import zipfile -from PyQt4.QtCore import pyqtSignal, QObject +from PyQt4.QtCore import pyqtSignal, QObject, QDate, QFile, QFileInfo, QUrl, \ + QIODevice from PyQt4.QtGui import QPixmap +from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, \ + QNetworkReply from E5Gui import E5MessageBox +from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired +try: + from E5Network.E5SslErrorHandler import E5SslErrorHandler + SSL_AVAILABLE = True +except ImportError: + SSL_AVAILABLE = False + from .PluginExceptions import PluginPathError, PluginModulesError, \ PluginLoadError, PluginActivationError, PluginModuleFormatError, \ PluginClassFormatError @@ -119,6 +130,18 @@ self.__loadPlugins() self.__checkPluginsDownloadDirectory() + + self.pluginRepositoryFile = \ + os.path.join(Utilities.getConfigDir(), "PluginRepository") + + # attributes for the network objects + self.__networkManager = QNetworkAccessManager(self) + self.__networkManager.proxyAuthenticationRequired.connect( + proxyAuthenticationRequired) + if SSL_AVAILABLE: + self.__sslErrorHandler = E5SslErrorHandler(self) + self.__networkManager.sslErrors.connect(self.__sslErrors) + self.__replies = [] def finalizeSetup(self): """ @@ -170,7 +193,7 @@ fname = os.path.join(self.pluginDirs["user"], "__init__.py") if not os.path.exists(fname): if not os.path.exists(self.pluginDirs["user"]): - os.mkdir(self.pluginDirs["user"], 0o755) + os.mkdir(self.pluginDirs["user"], 0o755) try: f = open(fname, "w") f.close() @@ -343,7 +366,7 @@ module.error = self.trUtf8( "Module failed to load. Error: {0}").format(str(err)) self.__failedModules[name] = module - print("Error loading plugin module:", name) + print("Error loading plugin module:", name) print(str(err)) def unloadPlugin(self, name): @@ -662,29 +685,29 @@ infos = [] for name in list(self.__activeModules.keys()): - pname, shortDesc, error, version = \ + pname, shortDesc, error, version = \ self.__getShortInfo(self.__activeModules[name]) - infos.append((name, pname, version, True, True, shortDesc, error)) + infos.append((name, pname, version, True, True, shortDesc, error)) for name in list(self.__inactiveModules.keys()): - pname, shortDesc, error, version = \ + pname, shortDesc, error, version = \ self.__getShortInfo(self.__inactiveModules[name]) infos.append( - (name, pname, version, True, False, shortDesc, error)) + (name, pname, version, True, False, shortDesc, error)) for name in list(self.__onDemandActiveModules.keys()): - pname, shortDesc, error, version = \ + pname, shortDesc, error, version = \ self.__getShortInfo(self.__onDemandActiveModules[name]) infos.append( - (name, pname, version, False, True, shortDesc, error)) + (name, pname, version, False, True, shortDesc, error)) for name in list(self.__onDemandInactiveModules.keys()): - pname, shortDesc, error, version = \ + pname, shortDesc, error, version = \ self.__getShortInfo(self.__onDemandInactiveModules[name]) infos.append( - (name, pname, version, False, False, shortDesc, error)) + (name, pname, version, False, False, shortDesc, error)) for name in list(self.__failedModules.keys()): - pname, shortDesc, error, version = \ + pname, shortDesc, error, version = \ self.__getShortInfo(self.__failedModules[name]) infos.append( - (name, pname, version, False, False, shortDesc, error)) + (name, pname, version, False, False, shortDesc, error)) return infos def __getShortInfo(self, module): @@ -1017,3 +1040,158 @@ Public slot to react to changes in configuration. """ self.__checkPluginsDownloadDirectory() + + ######################################################################## + ## Methods for automatic plug-in update check below + ######################################################################## + + def checkPluginUpdatesAvailable(self): + """ + Public method to check the availability of updates of plug-ins. + """ + period = Preferences.getPluginManager("UpdatesCheckInterval") + if period == 0: + return + elif period in [2, 3, 4]: + lastModified = QFileInfo(self.pluginRepositoryFile).lastModified() + if lastModified.isValid() and lastModified.date().isValid(): + lastModifiedDate = lastModified.date() + now = QDate.currentDate() + if period == 2 and lastModifiedDate.day() == now.day(): + # daily + return + elif period == 3 and lastModifiedDate.daysTo(now) < 7: + # weekly + return + elif period == 4 and lastModifiedDate.month() == now.month(): + # monthly + return + + self.__updateAvailable = False + + request = QNetworkRequest( + QUrl(Preferences.getUI("PluginRepositoryUrl5"))) + request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, + QNetworkRequest.AlwaysNetwork) + reply = self.__networkManager.get(request) + reply.finished[()].connect(self.__downloadRepositoryFileDone) + self.__replies.append(reply) + + def __downloadRepositoryFileDone(self): + """ + Private method called after the repository file was downloaded. + """ + reply = self.sender() + if reply in self.__replies: + self.__replies.remove(reply) + if reply.error() != QNetworkReply.NoError: + E5MessageBox.warning( + None, + self.trUtf8("Error downloading file"), + self.trUtf8( + """<p>Could not download the requested file""" + """ from {0}.</p><p>Error: {1}</p>""" + ).format(Preferences.getUI("PluginRepositoryUrl5"), + reply.errorString()) + ) + return + + ioDevice = QFile(self.pluginRepositoryFile + ".tmp") + ioDevice.open(QIODevice.WriteOnly) + ioDevice.write(reply.readAll()) + ioDevice.close() + if QFile.exists(self.pluginRepositoryFile): + QFile.remove(self.pluginRepositoryFile) + ioDevice.rename(self.pluginRepositoryFile) + + if os.path.exists(self.pluginRepositoryFile): + f = QFile(self.pluginRepositoryFile) + if f.open(QIODevice.ReadOnly): + # save current URL + url = Preferences.getUI("PluginRepositoryUrl5") + + # read the repository file + from E5XML.PluginRepositoryReader import PluginRepositoryReader + reader = PluginRepositoryReader(f, self.checkPluginEntry) + reader.readXML() + if url != Preferences.getUI("PluginRepositoryUrl5"): + # redo if it is a redirect + self.checkPluginUpdatesAvailable() + return + + if self.__updateAvailable: + res = E5MessageBox.information( + None, + self.trUtf8("New plugin versions available"), + self.trUtf8("<p>There are new plug-ins or plug-in" + " updates available. Use the plug-in" + " repository dialog to get them.</p>"), + E5MessageBox.StandardButtons( + E5MessageBox.Ignore | \ + E5MessageBox.Open), + E5MessageBox.Open) + if res == E5MessageBox.Open: + self.__ui.showPluginsAvailable() + + def checkPluginEntry(self, name, short, description, url, author, version, + filename, status): + """ + Public method to check a plug-in's data for an update. + + @param name data for the name field (string) + @param short data for the short field (string) + @param description data for the description field (list of strings) + @param url data for the url field (string) + @param author data for the author field (string) + @param version data for the version field (string) + @param filename data for the filename field (string) + @param status status of the plugin (string [stable, unstable, unknown]) + """ + archive = os.path.join(Preferences.getPluginManager("DownloadPath"), + filename) + + # Check against installed/loaded plug-ins + pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0] + pluginDetails = self.getPluginDetails(pluginName) + if pluginDetails is None: + if not Preferences.getPluginManager("CheckInstalledOnly"): + self.__updateAvailable = True + return + + if pluginDetails["version"] < version: + self.__updateAvailable = True + return + + if not Preferences.getPluginManager("CheckInstalledOnly"): + # Check against downloaded plugin archives + # 1. Check, if the archive file exists + if not os.path.exists(archive): + self.__updateAvailable = True + return + + # 2. Check, if the archive is a valid zip file + if not zipfile.is_zipfile(archive): + self.__updateAvailable = True + return + + # 3. Check the version of the archive file + zip = zipfile.ZipFile(archive, "r") + try: + aversion = zip.read("VERSION").decode("utf-8") + except KeyError: + aversion = "" + zip.close() + + if aversion != version: + self.__updateAvailable = True + + def __sslErrors(self, reply, errors): + """ + Private slot to handle SSL errors. + + @param reply reference to the reply object (QNetworkReply) + @param errors list of SSL errors (list of QSslError) + """ + ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0] + if ignored == E5SslErrorHandler.NotIgnored: + self.__downloadCancelled = True