mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-23 00:02:38 -05:00
781 lines
34 KiB
Python
781 lines
34 KiB
Python
# -*- coding:utf-8 -*-
|
|
"""
|
|
/***************************************************************************
|
|
Plugin Installer module
|
|
-------------------
|
|
Date : May 2013
|
|
Copyright : (C) 2013 by Borys Jurgiel
|
|
Email : info at borysjurgiel dot pl
|
|
|
|
This module is based on former plugin_installer plugin:
|
|
Copyright (C) 2007-2008 Matthew Perry
|
|
Copyright (C) 2008-2013 Borys Jurgiel
|
|
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
"""
|
|
|
|
from PyQt4.QtCore import *
|
|
from PyQt4.QtXml import QDomDocument
|
|
from PyQt4.QtNetwork import *
|
|
import sys
|
|
import os
|
|
import ConfigParser
|
|
import qgis.utils
|
|
from qgis.core import *
|
|
from qgis.utils import iface
|
|
from version_compare import compareVersions, normalizeVersion
|
|
|
|
"""
|
|
Data structure:
|
|
mRepositories = dict of dicts: {repoName : {"url" unicode,
|
|
"enabled" bool,
|
|
"valid" bool,
|
|
"QPHttp" QPHttp,
|
|
"Relay" Relay, # Relay object for transmitting signals from QPHttp with adding the repoName information
|
|
"xmlData" QBuffer,
|
|
"state" int, (0 - disabled, 1-loading, 2-loaded ok, 3-error (to be retried), 4-rejected)
|
|
"error" unicode}}
|
|
|
|
|
|
mPlugins = dict of dicts {id : {
|
|
"id" unicode # module name
|
|
"name" unicode, #
|
|
"description" unicode, #
|
|
"category" unicode, # will be removed?
|
|
"tags" unicode, # comma separated, spaces allowed
|
|
"changelog" unicode, # may be multiline
|
|
"author_name" unicode, #
|
|
"author_email" unicode, #
|
|
"homepage" unicode, # url to a tracker site
|
|
"tracker" unicode, # url to a tracker site
|
|
"code_repository" unicode, # url to a repository with code
|
|
"version_installed" unicode, #
|
|
"library" unicode, # full path to the installed library/Python module
|
|
"icon" unicode, # path to the first:(INSTALLED | AVAILABLE) icon
|
|
"pythonic" const bool=True
|
|
"readonly" boolean, # True if core plugin
|
|
"installed" boolean, # True if installed
|
|
"available" boolean, # True if available in repositories
|
|
"status" unicode, # ( not installed | new ) | ( installed | upgradeable | orphan | newer )
|
|
"error" unicode, # NULL | broken | incompatible | dependent
|
|
"error_details" unicode, # more details
|
|
"experimental" boolean, # choosen version: experimental or stable?
|
|
"version_available" unicode, # choosen version: version
|
|
"zip_repository" unicode, # choosen version: the remote repository id
|
|
"download_url" unicode, # choosen version: url for downloading
|
|
"filename" unicode, # choosen version: the zip file to be downloaded
|
|
"downloads" unicode, # choosen version: number of dowloads
|
|
"average_vote" unicode, # choosen version: average vote
|
|
"rating_votes" unicode, # choosen version: number of votes
|
|
"stable:version_available" unicode, # stable version found in repositories
|
|
"stable:download_source" unicode,
|
|
"stable:download_url" unicode,
|
|
"stable:filename" unicode,
|
|
"stable:downloads" unicode,
|
|
"stable:average_vote" unicode,
|
|
"stable:rating_votes" unicode,
|
|
"experimental:version_available" unicode, # experimental version found in repositories
|
|
"experimental:download_source" unicode,
|
|
"experimental:download_url" unicode,
|
|
"experimental:filename" unicode,
|
|
"experimental:downloads" unicode,
|
|
"experimental:average_vote" unicode,
|
|
"experimental:rating_votes" unicode
|
|
}}
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
reposGroup = "/Qgis/plugin-repos"
|
|
settingsGroup = "/Qgis/plugin-installer"
|
|
seenPluginGroup = "/Qgis/plugin-seen"
|
|
|
|
|
|
# Repositories: (name, url, possible depreciated url)
|
|
officialRepo = ("QGIS Official Repository", "http://plugins.qgis.org/plugins/plugins.xml","http://plugins.qgis.org/plugins")
|
|
depreciatedRepos = [
|
|
("Old QGIS Official Repository", "http://pyqgis.org/repo/official"),
|
|
("Old QGIS Contributed Repository","http://pyqgis.org/repo/contributed"),
|
|
("Aaron Racicot's Repository", "http://qgisplugins.z-pulley.com"),
|
|
("Barry Rowlingson's Repository", "http://www.maths.lancs.ac.uk/~rowlings/Qgis/Plugins/plugins.xml"),
|
|
("Bob Bruce's Repository", "http://www.mappinggeek.ca/QGISPythonPlugins/Bobs-QGIS-plugins.xml"),
|
|
("Borys Jurgiel's Repository", "http://bwj.aster.net.pl/qgis/plugins.xml"),
|
|
("Carson Farmer's Repository", "http://www.ftools.ca/cfarmerQgisRepo.xml"),
|
|
("CatAIS Repository", "http://www.catais.org/qgis/plugins.xml"),
|
|
("Faunalia Repository", "http://www.faunalia.it/qgis/plugins.xml"),
|
|
("GIS-Lab Repository", "http://gis-lab.info/programs/qgis/qgis-repo.xml"),
|
|
("Kappasys Repository", "http://www.kappasys.org/qgis/plugins.xml"),
|
|
("Martin Dobias' Sandbox", "http://mapserver.sk/~wonder/qgis/plugins-sandbox.xml"),
|
|
("Marco Hugentobler's Repository", "http://karlinapp.ethz.ch/python_plugins/python_plugins.xml"),
|
|
("Sourcepole Repository", "http://build.sourcepole.ch/qgis/plugins.xml"),
|
|
("Volkan Kepoglu's Repository", "http://ggit.metu.edu.tr/~volkan/plugins.xml")
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# --- common functions ------------------------------------------------------------------- #
|
|
def removeDir(path):
|
|
result = ""
|
|
if not QFile(path).exists():
|
|
result = QCoreApplication.translate("QgsPluginInstaller","Nothing to remove! Plugin directory doesn't exist:")+"\n"+path
|
|
elif QFile(path).remove(): # if it is only link, just remove it without resolving.
|
|
pass
|
|
else:
|
|
fltr = QDir.Dirs | QDir.Files | QDir.Hidden
|
|
iterator = QDirIterator(path, fltr, QDirIterator.Subdirectories)
|
|
while iterator.hasNext():
|
|
item = iterator.next()
|
|
if QFile(item).remove():
|
|
pass
|
|
fltr = QDir.Dirs | QDir.Hidden
|
|
iterator = QDirIterator(path, fltr, QDirIterator.Subdirectories)
|
|
while iterator.hasNext():
|
|
item = iterator.next()
|
|
if QDir().rmpath(item):
|
|
pass
|
|
if QFile(path).exists():
|
|
result = QCoreApplication.translate("QgsPluginInstaller","Failed to remove the directory:")+"\n"+path+"\n"+QCoreApplication.translate("QgsPluginInstaller","Check permissions or remove it manually")
|
|
# restore plugin directory if removed by QDir().rmpath()
|
|
pluginDir = QFileInfo(QgsApplication.qgisUserDbFilePath()).path() + "/python/plugins"
|
|
if not QDir(pluginDir).exists():
|
|
QDir().mkpath(pluginDir)
|
|
return result
|
|
# --- /common functions ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
# --- class QPHttp ----------------------------------------------------------------------- #
|
|
# --- It's a temporary workaround for broken proxy handling in Qt ------------------------- #
|
|
class QPHttp(QHttp):
|
|
def __init__(self,*args):
|
|
QHttp.__init__(self,*args)
|
|
settings = QSettings()
|
|
settings.beginGroup("proxy")
|
|
if settings.value("/proxyEnabled", False, type=bool):
|
|
self.proxy=QNetworkProxy()
|
|
proxyType = settings.value( "/proxyType", "0", type=unicode)
|
|
if len(args)>0 and settings.value("/proxyExcludedUrls","", type=unicode).contains(args[0]):
|
|
proxyType = "NoProxy"
|
|
if proxyType in ["1","Socks5Proxy"]: self.proxy.setType(QNetworkProxy.Socks5Proxy)
|
|
elif proxyType in ["2","NoProxy"]: self.proxy.setType(QNetworkProxy.NoProxy)
|
|
elif proxyType in ["3","HttpProxy"]: self.proxy.setType(QNetworkProxy.HttpProxy)
|
|
elif proxyType in ["4","HttpCachingProxy"] and QT_VERSION >= 0X040400: self.proxy.setType(QNetworkProxy.HttpCachingProxy)
|
|
elif proxyType in ["5","FtpCachingProxy"] and QT_VERSION >= 0X040400: self.proxy.setType(QNetworkProxy.FtpCachingProxy)
|
|
else: self.proxy.setType(QNetworkProxy.DefaultProxy)
|
|
self.proxy.setHostName(settings.value("/proxyHost","", type=unicode))
|
|
self.proxy.setPort(settings.value("/proxyPort", 0, type=int))
|
|
self.proxy.setUser(settings.value("/proxyUser", "", type=unicode))
|
|
self.proxy.setPassword(settings.value("/proxyPassword", "", type=unicode))
|
|
self.setProxy(self.proxy)
|
|
settings.endGroup()
|
|
return None
|
|
# --- /class QPHttp ---------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
# --- class Relay ----------------------------------------------------------------------- #
|
|
class Relay(QObject):
|
|
""" Relay object for transmitting signals from QPHttp with adding the repoName information """
|
|
# ----------------------------------------- #
|
|
anythingChanged = pyqtSignal( unicode, int, int )
|
|
|
|
def __init__(self, key):
|
|
QObject.__init__(self)
|
|
self.key = key
|
|
|
|
def stateChanged(self, state):
|
|
self.anythingChanged.emit( self.key, state, 0 )
|
|
|
|
# ----------------------------------------- #
|
|
def dataReadProgress(self, done, total):
|
|
state = 4
|
|
if total:
|
|
progress = int(float(done)/float(total)*100)
|
|
else:
|
|
progress = 0
|
|
self.anythingChanged.emit( self.key, state, progress )
|
|
|
|
# --- /class Relay ---------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
# --- class Repositories ----------------------------------------------------------------- #
|
|
class Repositories(QObject):
|
|
""" A dict-like class for handling repositories data """
|
|
# ----------------------------------------- #
|
|
|
|
anythingChanged = pyqtSignal( unicode, int, int )
|
|
repositoryFetched = pyqtSignal( unicode )
|
|
checkingDone = pyqtSignal()
|
|
|
|
def __init__(self):
|
|
QObject.__init__(self)
|
|
self.mRepositories = {}
|
|
self.httpId = {} # {httpId : repoName}
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def all(self):
|
|
""" return dict of all repositories """
|
|
return self.mRepositories
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def allEnabled(self):
|
|
""" return dict of all enabled and valid repositories """
|
|
repos = {}
|
|
for i in self.mRepositories:
|
|
if self.mRepositories[i]["enabled"] and self.mRepositories[i]["valid"]:
|
|
repos[i] = self.mRepositories[i]
|
|
return repos
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def allUnavailable(self):
|
|
""" return dict of all unavailable repositories """
|
|
repos = {}
|
|
for i in self.mRepositories:
|
|
if self.mRepositories[i]["enabled"] and self.mRepositories[i]["valid"] and self.mRepositories[i]["state"] == 3:
|
|
repos[i] = self.mRepositories[i]
|
|
return repos
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def setRepositoryData(self, reposName, key, value):
|
|
""" write data to the mRepositories dict """
|
|
self.mRepositories[reposName][key] = value
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def remove(self, reposName):
|
|
""" remove given item from the mRepositories dict """
|
|
del self.mRepositories[reposName]
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def rename(self, oldName, newName):
|
|
""" rename repository key """
|
|
if oldName == newName:
|
|
return
|
|
self.mRepositories[newName] = self.mRepositories[oldName]
|
|
del self.mRepositories[oldName]
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def checkingOnStart(self):
|
|
""" return true if checking for news and updates is enabled """
|
|
settings = QSettings()
|
|
return settings.value(settingsGroup+"/checkOnStart", False, type=bool)
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def setCheckingOnStart(self, state):
|
|
""" set state of checking for news and updates """
|
|
settings = QSettings()
|
|
settings.setValue(settingsGroup+"/checkOnStart", state)
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def checkingOnStartInterval(self):
|
|
""" return checking for news and updates interval """
|
|
settings = QSettings()
|
|
i = settings.value(settingsGroup+"/checkOnStartInterval", 1, type=int)
|
|
if i < 0: i = 1
|
|
# allowed values: 0,1,3,7,14,30 days
|
|
interval = 0
|
|
for j in [1,3,7,14,30]:
|
|
if i >= j:
|
|
interval = j
|
|
return interval
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def setCheckingOnStartInterval(self, interval):
|
|
""" set checking for news and updates interval """
|
|
settings = QSettings()
|
|
settings.setValue(settingsGroup+"/checkOnStartInterval", interval)
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def saveCheckingOnStartLastDate(self):
|
|
""" set today's date as the day of last checking """
|
|
settings = QSettings()
|
|
settings.setValue(settingsGroup+"/checkOnStartLastDate", QDate.currentDate())
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def timeForChecking(self):
|
|
""" determine whether it's the time for checking for news and updates now """
|
|
if self.checkingOnStartInterval() == 0:
|
|
return True
|
|
settings = QSettings()
|
|
interval = settings.value(settingsGroup+"/checkOnStartLastDate",type=QDate).daysTo(QDate.currentDate())
|
|
if interval >= self.checkingOnStartInterval():
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def load(self):
|
|
""" populate the mRepositories dict"""
|
|
self.mRepositories = {}
|
|
settings = QSettings()
|
|
settings.beginGroup(reposGroup)
|
|
# first, update repositories in QSettings if needed
|
|
officialRepoPresent = False
|
|
for key in settings.childGroups():
|
|
url = settings.value(key+"/url", "", type=unicode)
|
|
if url == officialRepo[1]:
|
|
officialRepoPresent = True
|
|
if url == officialRepo[2]:
|
|
settings.setValue(key+"/url", officialRepo[1]) # correct a depreciated url
|
|
officialRepoPresent = True
|
|
if not officialRepoPresent:
|
|
settings.setValue(officialRepo[0]+"/url", officialRepo[1])
|
|
|
|
for key in settings.childGroups():
|
|
self.mRepositories[key] = {}
|
|
self.mRepositories[key]["url"] = settings.value(key+"/url", "", type=unicode)
|
|
self.mRepositories[key]["enabled"] = settings.value(key+"/enabled", True, type=bool)
|
|
self.mRepositories[key]["valid"] = settings.value(key+"/valid", True, type=bool)
|
|
self.mRepositories[key]["QPHttp"] = QPHttp()
|
|
self.mRepositories[key]["Relay"] = Relay(key)
|
|
self.mRepositories[key]["xmlData"] = QBuffer()
|
|
self.mRepositories[key]["state"] = 0
|
|
self.mRepositories[key]["error"] = ""
|
|
settings.endGroup()
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def requestFetching(self,key):
|
|
""" start fetching the repository given by key """
|
|
self.mRepositories[key]["state"] = 1
|
|
url = QUrl(self.mRepositories[key]["url"])
|
|
path = url.toPercentEncoding(url.path(), "!$&'()*+,;=:@/")
|
|
path = unicode(path)
|
|
v=str(QGis.QGIS_VERSION_INT)
|
|
path += "?qgis=%d.%d" % ( int(v[0]), int(v[1:3]) )
|
|
port = url.port()
|
|
if port < 0:
|
|
port = 80
|
|
self.mRepositories[key]["QPHttp"] = QPHttp(url.host(), port)
|
|
self.mRepositories[key]["QPHttp"].requestFinished.connect(self.xmlDownloaded)
|
|
self.mRepositories[key]["QPHttp"].stateChanged.connect(self.mRepositories[key]["Relay"].stateChanged)
|
|
self.mRepositories[key]["QPHttp"].dataReadProgress.connect(self.mRepositories[key]["Relay"].dataReadProgress)
|
|
self.connect(self.mRepositories[key]["Relay"], SIGNAL("anythingChanged(unicode, int, int)"), self, SIGNAL("anythingChanged (unicode, int, int)"))
|
|
i = self.mRepositories[key]["QPHttp"].get(path, self.mRepositories[key]["xmlData"])
|
|
self.httpId[i] = key
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def fetchingInProgress(self):
|
|
""" return true if fetching repositories is still in progress """
|
|
for key in self.mRepositories:
|
|
if self.mRepositories[key]["state"] == 1:
|
|
return True
|
|
return False
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def killConnection(self, key):
|
|
""" kill the fetching on demand """
|
|
if self.mRepositories[key]["QPHttp"].state():
|
|
self.mRepositories[key]["QPHttp"].abort()
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def xmlDownloaded(self,nr,state):
|
|
""" populate the plugins object with the fetched data """
|
|
if not self.httpId.has_key(nr):
|
|
return
|
|
reposName = self.httpId[nr]
|
|
if state: # fetching failed
|
|
self.mRepositories[reposName]["state"] = 3
|
|
self.mRepositories[reposName]["error"] = self.mRepositories[reposName]["QPHttp"].errorString()
|
|
else:
|
|
repoData = self.mRepositories[reposName]["xmlData"]
|
|
reposXML = QDomDocument()
|
|
reposXML.setContent(repoData.data())
|
|
pluginNodes = reposXML.elementsByTagName("pyqgis_plugin")
|
|
if pluginNodes.size():
|
|
for i in range(pluginNodes.size()):
|
|
fileName = pluginNodes.item(i).firstChildElement("file_name").text().strip()
|
|
if not fileName:
|
|
fileName = QFileInfo(pluginNodes.item(i).firstChildElement("download_url").text().strip().split("?")[0]).fileName()
|
|
name = fileName.partition(".")[0]
|
|
experimental = False
|
|
if pluginNodes.item(i).firstChildElement("experimental").text().strip().upper() in ["TRUE","YES"]:
|
|
experimental = True
|
|
icon = pluginNodes.item(i).firstChildElement("icon").text().strip()
|
|
if icon and not icon.startswith("http"):
|
|
icon = "http://%s/%s" % ( QUrl(self.mRepositories[reposName]["url"]).host() , icon )
|
|
|
|
plugin = {
|
|
"id" : name,
|
|
"name" : pluginNodes.item(i).toElement().attribute("name"),
|
|
"version_available" : pluginNodes.item(i).toElement().attribute("version"),
|
|
"description" : pluginNodes.item(i).firstChildElement("description").text().strip(),
|
|
"author_name" : pluginNodes.item(i).firstChildElement("author_name").text().strip(),
|
|
"homepage" : pluginNodes.item(i).firstChildElement("homepage").text().strip(),
|
|
"download_url" : pluginNodes.item(i).firstChildElement("download_url").text().strip(),
|
|
"category" : pluginNodes.item(i).firstChildElement("category").text().strip(),
|
|
"tags" : pluginNodes.item(i).firstChildElement("tags").text().strip(),
|
|
"changelog" : pluginNodes.item(i).firstChildElement("changelog").text().strip(),
|
|
"author_email" : pluginNodes.item(i).firstChildElement("author_email").text().strip(),
|
|
"tracker" : pluginNodes.item(i).firstChildElement("tracker").text().strip(),
|
|
"code_repository" : pluginNodes.item(i).firstChildElement("repository").text().strip(),
|
|
"downloads" : pluginNodes.item(i).firstChildElement("downloads").text().strip(),
|
|
"average_vote" : pluginNodes.item(i).firstChildElement("average_vote").text().strip(),
|
|
"rating_votes" : pluginNodes.item(i).firstChildElement("rating_votes").text().strip(),
|
|
"icon" : icon,
|
|
"experimental" : experimental,
|
|
"filename" : fileName,
|
|
"installed" : False,
|
|
"available" : True,
|
|
"status" : "not installed",
|
|
"error" : "",
|
|
"error_details" : "",
|
|
"version_installed" : "",
|
|
"zip_repository" : reposName,
|
|
"library" : "",
|
|
"readonly" : False
|
|
}
|
|
qgisMinimumVersion = pluginNodes.item(i).firstChildElement("qgis_minimum_version").text().strip()
|
|
if not qgisMinimumVersion: qgisMinimumVersion = "2"
|
|
qgisMaximumVersion = pluginNodes.item(i).firstChildElement("qgis_maximum_version").text().strip()
|
|
if not qgisMaximumVersion: qgisMaximumVersion = qgisMinimumVersion[0] + ".99"
|
|
#if compatible, add the plugin to the list
|
|
if not pluginNodes.item(i).firstChildElement("disabled").text().strip().upper() in ["TRUE","YES"]:
|
|
if compareVersions(QGis.QGIS_VERSION, qgisMinimumVersion) < 2 and compareVersions(qgisMaximumVersion, QGis.QGIS_VERSION) < 2:
|
|
#add the plugin to the cache
|
|
plugins.addFromRepository(plugin)
|
|
# set state=2, even if the repo is empty
|
|
self.mRepositories[reposName]["state"] = 2
|
|
|
|
self.repositoryFetched.emit( reposName )
|
|
|
|
# is the checking done?
|
|
if not self.fetchingInProgress():
|
|
self.checkingDone.emit()
|
|
|
|
# --- /class Repositories ---------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
# --- class Plugins ---------------------------------------------------------------------- #
|
|
class Plugins(QObject):
|
|
""" A dict-like class for handling plugins data """
|
|
# ----------------------------------------- #
|
|
def __init__(self):
|
|
QObject.__init__(self)
|
|
self.mPlugins = {} # the dict of plugins (dicts)
|
|
self.repoCache = {} # the dict of lists of plugins (dicts)
|
|
self.localCache = {} # the dict of plugins (dicts)
|
|
self.obsoletePlugins = [] # the list of outdated 'user' plugins masking newer 'system' ones
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def all(self):
|
|
""" return all plugins """
|
|
return self.mPlugins
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def allUpgradeable(self):
|
|
""" return all upgradeable plugins """
|
|
result = {}
|
|
for i in self.mPlugins:
|
|
if self.mPlugins[i]["status"] == "upgradeable":
|
|
result[i] = self.mPlugins[i]
|
|
return result
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def keyByUrl(self, name):
|
|
""" return plugin key by given url """
|
|
plugins = [i for i in self.mPlugins if self.mPlugins[i]["download_url"] == name]
|
|
if plugins:
|
|
return plugins[0]
|
|
return None
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def addFromRepository(self, plugin):
|
|
""" add given plugin to the repoCache """
|
|
repo = plugin["zip_repository"]
|
|
try:
|
|
self.repoCache[repo] += [plugin]
|
|
except:
|
|
self.repoCache[repo] = [plugin]
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def removeInstalledPlugin(self, key):
|
|
""" remove given plugin from the localCache """
|
|
if self.localCache.has_key(key):
|
|
del self.localCache[key]
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def removeRepository(self, repo):
|
|
""" remove whole repository from the repoCache """
|
|
if self.repoCache.has_key(repo):
|
|
del self.repoCache[repo]
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def getInstalledPlugin(self, key, readOnly, testLoad=True):
|
|
""" get the metadata of an installed plugin """
|
|
def pluginMetadata(fct):
|
|
""" plugin metadata parser reimplemented from qgis.utils
|
|
for better control on wchich module is examined
|
|
in case there is an installed plugin masking a core one """
|
|
metadataFile = os.path.join(path, 'metadata.txt')
|
|
if not os.path.exists(metadataFile):
|
|
return "" # plugin has no metadata.txt file
|
|
cp = ConfigParser.ConfigParser()
|
|
res = cp.read(metadataFile)
|
|
if not len(res):
|
|
return "" # failed reading metadata.txt file
|
|
try:
|
|
return cp.get('general', fct)
|
|
except Exception:
|
|
return ""
|
|
|
|
if readOnly:
|
|
path = QDir.cleanPath( QgsApplication.pkgDataPath() ) + "/python/plugins/" + key
|
|
else:
|
|
path = QDir.cleanPath( QgsApplication.qgisSettingsDirPath() ) + "/python/plugins/" + key
|
|
|
|
if not QDir(path).exists():
|
|
return
|
|
|
|
plugin = dict()
|
|
error = ""
|
|
errorDetails = ""
|
|
|
|
version = normalizeVersion( pluginMetadata("version") )
|
|
if version:
|
|
qgisMinimumVersion = pluginMetadata("qgisMinimumVersion").strip()
|
|
if not qgisMinimumVersion: qgisMinimumVersion = "0"
|
|
qgisMaximumVersion = pluginMetadata("qgisMaximumVersion").strip()
|
|
if not qgisMaximumVersion: qgisMaximumVersion = qgisMinimumVersion[0] + ".999"
|
|
#if compatible, add the plugin to the list
|
|
if compareVersions(QGis.QGIS_VERSION, qgisMinimumVersion) == 2 or compareVersions(qgisMaximumVersion, QGis.QGIS_VERSION) == 2:
|
|
error = "incompatible"
|
|
errorDetails = "%s - %s" % (qgisMinimumVersion, qgisMaximumVersion)
|
|
|
|
if testLoad:
|
|
try:
|
|
exec "import %s" % key in globals(), locals()
|
|
exec "reload (%s)" % key in globals(), locals()
|
|
exec "%s.classFactory(iface)" % key in globals(), locals()
|
|
except Exception, error:
|
|
error = unicode(error.args[0])
|
|
else:
|
|
# seems there is no metadata.txt file. Maybe it's an old plugin for QGIS 1.x.
|
|
version = "-1"
|
|
error = "incompatible"
|
|
errorDetails = "1.x"
|
|
|
|
if error[:16] == "No module named ":
|
|
mona = error.replace("No module named ","")
|
|
if mona != key:
|
|
error = "dependent"
|
|
errorDetails = mona
|
|
if not error in ["", "dependent", "incompatible"]:
|
|
errorDetails = error
|
|
error = "broken"
|
|
|
|
icon = pluginMetadata("icon")
|
|
if QFileInfo( icon ).isRelative():
|
|
icon = path + "/" + icon;
|
|
|
|
plugin = {
|
|
"id" : key,
|
|
"name" : pluginMetadata("name") or key,
|
|
"description" : pluginMetadata("description"),
|
|
"icon" : icon,
|
|
"category" : pluginMetadata("category"),
|
|
"tags" : pluginMetadata("tags"),
|
|
"changelog" : pluginMetadata("changelog"),
|
|
"author_name" : pluginMetadata("author_name") or pluginMetadata("author"),
|
|
"author_email" : pluginMetadata("email"),
|
|
"homepage" : pluginMetadata("homepage"),
|
|
"tracker" : pluginMetadata("tracker"),
|
|
"code_repository" : pluginMetadata("repository"),
|
|
"version_installed" : version,
|
|
"library" : path,
|
|
"pythonic" : True,
|
|
"experimental" : pluginMetadata("experimental").strip().upper() in ["TRUE","YES"],
|
|
"version_available" : "",
|
|
"zip_repository" : "",
|
|
"download_url" : path, # warning: local path as url!
|
|
"filename" : "",
|
|
"downloads" : "",
|
|
"average_vote" : "",
|
|
"rating_votes" : "",
|
|
"available" : False, # Will be overwritten, if any available version found.
|
|
"installed" : True,
|
|
"status" : "orphan", # Will be overwritten, if any available version found.
|
|
"error" : error,
|
|
"error_details" : errorDetails,
|
|
"readonly" : readOnly }
|
|
return plugin
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def getAllInstalled(self, testLoad=True):
|
|
""" Build the localCache """
|
|
self.localCache = {}
|
|
# first, try to add the readonly plugins...
|
|
pluginsPath = unicode(QDir.convertSeparators(QDir.cleanPath(QgsApplication.pkgDataPath() + "/python/plugins")))
|
|
# temporarily add the system path as the first element to force loading the readonly plugins, even if masked by user ones.
|
|
sys.path = [pluginsPath] + sys.path
|
|
try:
|
|
pluginDir = QDir(pluginsPath)
|
|
pluginDir.setFilter(QDir.AllDirs)
|
|
for key in pluginDir.entryList():
|
|
key = unicode(key)
|
|
if not key in [".",".."]:
|
|
self.localCache[key] = self.getInstalledPlugin(key, readOnly=True, testLoad=False)
|
|
except:
|
|
# return QCoreApplication.translate("QgsPluginInstaller","Couldn't open the system plugin directory")
|
|
pass # it's not necessary to stop due to this error
|
|
# remove the temporarily added path
|
|
sys.path.remove(pluginsPath)
|
|
# ...then try to add locally installed ones
|
|
try:
|
|
pluginDir = QDir.convertSeparators(QDir.cleanPath(QgsApplication.qgisSettingsDirPath() + "/python/plugins"))
|
|
pluginDir = QDir(pluginDir)
|
|
pluginDir.setFilter(QDir.AllDirs)
|
|
except:
|
|
return QCoreApplication.translate("QgsPluginInstaller","Couldn't open the local plugin directory")
|
|
for key in pluginDir.entryList():
|
|
key = unicode(key)
|
|
if not key in [".",".."]:
|
|
plugin = self.getInstalledPlugin(key, readOnly=False, testLoad=testLoad)
|
|
if key in self.localCache.keys() and compareVersions(self.localCache[key]["version_installed"],plugin["version_installed"]) == 1:
|
|
# An obsolete plugin in the "user" location is masking a newer one in the "system" location!
|
|
self.obsoletePlugins += [key]
|
|
self.localCache[key] = plugin
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def rebuild(self):
|
|
""" build or rebuild the mPlugins from the caches """
|
|
self.mPlugins = {}
|
|
for i in self.localCache.keys():
|
|
self.mPlugins[i] = self.localCache[i].copy()
|
|
settings = QSettings()
|
|
allowExperimental = settings.value(settingsGroup+"/allowExperimental", False, type=bool)
|
|
for i in self.repoCache.values():
|
|
for plugin in i:
|
|
key = plugin["id"]
|
|
# check if the plugin is allowed and if there isn't any better one added already.
|
|
if (allowExperimental or not plugin["experimental"]) \
|
|
and not (self.mPlugins.has_key(key) and self.mPlugins[key]["version_available"] and compareVersions(self.mPlugins[key]["version_available"], plugin["version_available"]) < 2):
|
|
# The mPlugins dict contains now locally installed plugins.
|
|
# Now, add the available one if not present yet or update it if present already.
|
|
if not self.mPlugins.has_key(key):
|
|
self.mPlugins[key] = plugin # just add a new plugin
|
|
else:
|
|
# update local plugin with remote metadata
|
|
# only use remote icon if local one is not available
|
|
if self.mPlugins[key]["icon"] == key and plugin["icon"]:
|
|
self.mPlugins[key]["icon"] = plugin["icon"]
|
|
# other remote metadata is preffered:
|
|
for attrib in ["name", "description", "category", "tags", "changelog", "author_name", "author_email", "homepage",
|
|
"tracker", "code_repository", "experimental", "version_available", "zip_repository",
|
|
"download_url", "filename", "downloads", "average_vote", "rating_votes"]:
|
|
if plugin[attrib]:
|
|
self.mPlugins[key][attrib] = plugin[attrib]
|
|
# set status
|
|
#
|
|
# installed available status
|
|
# ---------------------------------------
|
|
# none any "not installed" (will be later checked if is "new")
|
|
# any none "orphan"
|
|
# same same "installed"
|
|
# less greater "upgradeable"
|
|
# greater less "newer"
|
|
if not self.mPlugins[key]["version_available"]:
|
|
self.mPlugins[key]["status"] = "orphan"
|
|
elif not self.mPlugins[key]["version_installed"]:
|
|
self.mPlugins[key]["status"] = "not installed"
|
|
elif self.mPlugins[key]["version_installed"] in ["?", "-1"]:
|
|
self.mPlugins[key]["status"] = "installed"
|
|
elif compareVersions(self.mPlugins[key]["version_available"],self.mPlugins[key]["version_installed"]) == 0:
|
|
self.mPlugins[key]["status"] = "installed"
|
|
elif compareVersions(self.mPlugins[key]["version_available"],self.mPlugins[key]["version_installed"]) == 1:
|
|
self.mPlugins[key]["status"] = "upgradeable"
|
|
else:
|
|
self.mPlugins[key]["status"] = "newer"
|
|
# debug: test if the status match the "installed" tag:
|
|
if self.mPlugins[key]["status"] in ["not installed"] and self.mPlugins[key]["installed"]:
|
|
raise Exception("Error: plugin status is ambiguous (1)")
|
|
if self.mPlugins[key]["status"] in ["installed","orphan","upgradeable","newer"] and not self.mPlugins[key]["installed"]:
|
|
raise Exception("Error: plugin status is ambiguous (2)")
|
|
self.markNews()
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def markNews(self):
|
|
""" mark all new plugins as new """
|
|
settings = QSettings()
|
|
seenPlugins = settings.value(seenPluginGroup, self.mPlugins.keys(), type=unicode)
|
|
if len(seenPlugins) > 0:
|
|
for i in self.mPlugins.keys():
|
|
if seenPlugins.count(i) == 0 and self.mPlugins[i]["status"] == "not installed":
|
|
self.mPlugins[i]["status"] = "new"
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def updateSeenPluginsList(self):
|
|
""" update the list of all seen plugins """
|
|
settings = QSettings()
|
|
seenPlugins = settings.value(seenPluginGroup, self.mPlugins.keys(), type=unicode)
|
|
for i in self.mPlugins.keys():
|
|
if seenPlugins.count(i) == 0:
|
|
seenPlugins += [i]
|
|
settings.setValue(seenPluginGroup, seenPlugins)
|
|
|
|
|
|
# ----------------------------------------- #
|
|
def isThereAnythingNew(self):
|
|
""" return true if an upgradeable or new plugin detected """
|
|
for i in self.mPlugins.values():
|
|
if i["status"] in ["upgradeable","new"]:
|
|
return True
|
|
return False
|
|
|
|
|
|
# --- /class Plugins --------------------------------------------------------------------- #
|
|
|
|
|
|
# public instances:
|
|
repositories = Repositories()
|
|
plugins = Plugins()
|