2013-05-19 17:44:57 +02:00
# -*- 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 . *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
"""
2016-09-21 18:24:26 +02:00
from future import standard_library
standard_library . install_aliases ( )
from builtins import str
from builtins import range
2013-05-19 17:44:57 +02:00
2016-06-01 10:26:12 +03:00
from qgis . PyQt . QtCore import ( pyqtSignal , QObject , QCoreApplication , QFile ,
QDir , QDirIterator , QSettings , QDate , QUrl ,
QFileInfo , QLocale , QByteArray )
2016-04-22 10:38:48 +02:00
from qgis . PyQt . QtXml import QDomDocument
from qgis . PyQt . QtNetwork import QNetworkRequest , QNetworkReply
2013-06-08 21:18:43 +02:00
import sys
import os
2013-06-09 10:56:35 +02:00
import codecs
2016-03-23 11:52:54 +01:00
try :
import configparser
except ImportError :
2016-09-21 18:24:26 +02:00
import configparser as configparser
2016-09-22 14:25:32 +07:00
try :
from importlib import reload
except ImportError :
from imp import reload
2013-06-08 21:18:43 +02:00
import qgis . utils
2016-08-04 09:10:08 +02:00
from qgis . core import Qgis , QgsNetworkAccessManager , QgsAuthManager , QgsWkbTypes
2015-12-03 17:45:41 -07:00
from qgis . gui import QgsMessageBar
2013-07-30 17:31:08 +02:00
from qgis . utils import iface , plugin_paths
2016-03-21 05:04:29 +01:00
from . version_compare import compareVersions , normalizeVersion , isCompatible
2013-05-19 17:44:57 +02:00
2016-03-14 20:26:58 +01:00
2013-05-19 17:44:57 +02:00
"""
Data structure :
2013-06-08 16:00:13 +10:00
mRepositories = dict of dicts : { repoName : { " url " unicode ,
2013-05-19 17:44:57 +02:00
" enabled " bool ,
" valid " bool ,
" Relay " Relay , # Relay object for transmitting signals from QPHttp with adding the repoName information
2013-06-11 21:19:17 +01:00
" Request " QNetworkRequest ,
" xmlData " QNetworkReply ,
2013-05-19 17:44:57 +02:00
" state " int , ( 0 - disabled , 1 - loading , 2 - loaded ok , 3 - error ( to be retried ) , 4 - rejected )
2013-06-08 16:00:13 +10:00
" error " unicode } }
2013-05-19 17:44:57 +02:00
mPlugins = dict of dicts { id : {
2013-06-08 16:00:13 +10:00
" id " unicode # module name
2013-06-25 23:20:24 +02:00
" name " unicode , # human readable plugin name
" description " unicode , # short description of the plugin purpose only
" about " unicode , # longer description: how does it work, where does it install, how to run it?
2013-06-08 16:00:13 +10:00
" category " unicode , # will be removed?
" tags " unicode , # comma separated, spaces allowed
" changelog " unicode , # may be multiline
2013-06-25 23:20:24 +02:00
" author_name " unicode , # author name
" author_email " unicode , # author email
" homepage " unicode , # url to the plugin homepage
2013-06-08 16:00:13 +10:00
" tracker " unicode , # url to a tracker site
2013-06-25 23:20:24 +02:00
" code_repository " unicode , # url to the source code repository
" version_installed " unicode , # installed instance version
" library " unicode , # absolute path to the installed library / Python module
2013-06-08 16:00:13 +10:00
" icon " unicode , # path to the first:(INSTALLED | AVAILABLE) icon
2013-06-25 23:20:24 +02:00
" pythonic " const bool = True # True if Python plugin
2013-05-19 17:44:57 +02:00
" readonly " boolean , # True if core plugin
" installed " boolean , # True if installed
" available " boolean , # True if available in repositories
2013-06-08 16:00:13 +10:00
" status " unicode , # ( not installed | new ) | ( installed | upgradeable | orphan | newer )
" error " unicode , # NULL | broken | incompatible | dependent
2013-06-25 23:20:24 +02:00
" error_details " unicode , # error description
" experimental " boolean , # true if experimental, false if stable
2017-01-16 15:13:30 +01:00
" deprecated " boolean , # true if deprecated, false if actual
2016-06-01 10:26:12 +03:00
" trusted " boolean , # true if trusted, false if not trusted
2013-06-25 23:20:24 +02:00
" version_available " unicode , # available version
" zip_repository " unicode , # the remote repository id
" download_url " unicode , # url for downloading the plugin
" filename " unicode , # the zip file name to be unzipped after downloaded
2017-01-16 15:13:30 +01:00
" downloads " unicode , # number of downloads
2013-06-25 23:20:24 +02:00
" average_vote " unicode , # average vote
" rating_votes " unicode , # number of votes
2013-05-19 17:44:57 +02:00
} }
"""
2013-06-25 23:20:24 +02:00
translatableAttributes = [ " name " , " description " , " about " , " tags " ]
2013-05-19 17:44:57 +02:00
reposGroup = " /Qgis/plugin-repos "
settingsGroup = " /Qgis/plugin-installer "
seenPluginGroup = " /Qgis/plugin-seen "
# Repositories: (name, url, possible depreciated url)
2016-04-04 10:04:42 +02:00
officialRepo = ( QCoreApplication . translate ( " QgsPluginInstaller " , " QGIS Official Plugin Repository " ) , " https://plugins.qgis.org/plugins/plugins.xml " , " https://plugins.qgis.org/plugins " )
2013-05-19 17:44:57 +02:00
depreciatedRepos = [
2015-08-22 14:29:41 +02:00
( " 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 " ) ,
2013-05-19 17:44:57 +02:00
( " Marco Hugentobler ' s Repository " , " http://karlinapp.ethz.ch/python_plugins/python_plugins.xml " ) ,
2015-08-22 14:29:41 +02:00
( " Sourcepole Repository " , " http://build.sourcepole.ch/qgis/plugins.xml " ) ,
( " Volkan Kepoglu ' s Repository " , " http://ggit.metu.edu.tr/~volkan/plugins.xml " )
2013-05-19 17:44:57 +02:00
]
# --- common functions ------------------------------------------------------------------- #
def removeDir ( path ) :
2013-06-08 16:00:13 +10:00
result = " "
2013-05-19 17:44:57 +02:00
if not QFile ( path ) . exists ( ) :
2015-08-22 14:29:41 +02:00
result = QCoreApplication . translate ( " QgsPluginInstaller " , " Nothing to remove! Plugin directory doesn ' t exist: " ) + " \n " + path
2016-03-21 05:04:29 +01:00
elif QFile ( path ) . remove ( ) : # if it is only link, just remove it without resolving.
2015-08-22 14:29:41 +02:00
pass
2013-05-19 17:44:57 +02:00
else :
2015-08-22 14:29:41 +02:00
fltr = QDir . Dirs | QDir . Files | QDir . Hidden
iterator = QDirIterator ( path , fltr , QDirIterator . Subdirectories )
while iterator . hasNext ( ) :
2016-09-22 14:25:32 +07:00
item = iterator . next ( )
2015-08-22 14:29:41 +02:00
if QFile ( item ) . remove ( ) :
pass
fltr = QDir . Dirs | QDir . Hidden
iterator = QDirIterator ( path , fltr , QDirIterator . Subdirectories )
while iterator . hasNext ( ) :
2016-09-22 14:25:32 +07:00
item = iterator . next ( )
2015-08-22 14:29:41 +02:00
if QDir ( ) . rmpath ( item ) :
pass
2013-05-19 17:44:57 +02:00
if QFile ( path ) . exists ( ) :
2015-08-22 14:29:41 +02:00
result = QCoreApplication . translate ( " QgsPluginInstaller " , " Failed to remove the directory: " ) + " \n " + path + " \n " + QCoreApplication . translate ( " QgsPluginInstaller " , " Check permissions or remove it manually " )
2013-05-19 17:44:57 +02:00
# restore plugin directory if removed by QDir().rmpath()
2013-10-10 23:55:28 +02:00
pluginDir = qgis . utils . home_plugin_path
2013-05-19 17:44:57 +02:00
if not QDir ( pluginDir ) . exists ( ) :
2015-08-22 14:29:41 +02:00
QDir ( ) . mkpath ( pluginDir )
2013-05-19 17:44:57 +02:00
return result
# --- /common functions ------------------------------------------------------------------ #
# --- class Relay ----------------------------------------------------------------------- #
class Relay ( QObject ) :
2013-06-08 16:00:13 +10:00
2015-08-22 14:29:41 +02:00
""" Relay object for transmitting signals from QPHttp with adding the repoName information """
# ----------------------------------------- #
2016-09-21 18:24:26 +02:00
anythingChanged = pyqtSignal ( str , int , int )
2015-08-22 14:29:41 +02:00
def __init__ ( self , key ) :
QObject . __init__ ( self )
self . key = key
2013-05-19 17:44:57 +02:00
2015-08-22 14:29:41 +02:00
def stateChanged ( self , state ) :
self . anythingChanged . emit ( self . key , state , 0 )
2013-05-19 17:44:57 +02:00
2015-08-22 14:29:41 +02:00
# ----------------------------------------- #
def dataReadProgress ( self , done , total ) :
state = 4
if total > 0 :
progress = int ( float ( done ) / float ( total ) * 100 )
else :
progress = 0
self . anythingChanged . emit ( self . key , state , progress )
2013-05-19 17:44:57 +02:00
2015-08-22 14:29:41 +02:00
# --- /class Relay ---------------------------------------------------------------------- #
2013-05-19 17:44:57 +02:00
# --- class Repositories ----------------------------------------------------------------- #
class Repositories ( QObject ) :
2015-08-22 14:29:41 +02:00
""" A dict-like class for handling repositories data """
# ----------------------------------------- #
2016-09-21 18:24:26 +02:00
anythingChanged = pyqtSignal ( str , int , int )
repositoryFetched = pyqtSignal ( str )
2015-08-22 14:29:41 +02:00
checkingDone = pyqtSignal ( )
def __init__ ( self ) :
QObject . __init__ ( self )
self . mRepositories = { }
self . httpId = { } # {httpId : repoName}
self . mInspectionFilter = None
# ----------------------------------------- #
def all ( self ) :
""" return dict of all repositories """
return self . mRepositories
# ----------------------------------------- #
def allEnabled ( self ) :
""" return dict of all enabled and valid repositories """
if self . mInspectionFilter :
return { self . mInspectionFilter : self . mRepositories [ self . mInspectionFilter ] }
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 = { }
if self . mInspectionFilter :
# return the inspected repo if unavailable, otherwise empty dict
if self . mRepositories [ self . mInspectionFilter ] [ " state " ] == 3 :
repos [ self . mInspectionFilter ] = self . mRepositories [ self . mInspectionFilter ]
return 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 urlParams ( self ) :
""" return GET parameters to be added to every request """
2016-07-21 22:01:38 +10:00
v = str ( Qgis . QGIS_VERSION_INT )
2016-10-28 16:57:42 +02:00
# TODO: make this proper again after 3.0 release, by uncommenting
# the line below and removing the other return line:
#return "?qgis=%d.%d" % (int(v[0]), int(v[1:3]))
return " ?qgis=3.0 "
2015-08-22 14:29:41 +02:00
# ----------------------------------------- #
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 ( )
try :
# QSettings may contain non-int value...
i = settings . value ( settingsGroup + " /checkOnStartInterval " , 1 , type = int )
except :
# fallback do 1 day by default
i = 1
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 ( )
try :
# QSettings may contain ivalid value...
interval = settings . value ( settingsGroup + " /checkOnStartLastDate " , type = QDate ) . daysTo ( QDate . currentDate ( ) )
except :
interval = 0
if interval > = self . checkingOnStartInterval ( ) :
return True
2013-06-25 23:39:10 +02:00
else :
2015-08-22 14:29:41 +02:00
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 ( ) :
2016-09-21 18:24:26 +02:00
url = settings . value ( key + " /url " , " " , type = str )
2015-08-22 14:29:41 +02:00
if url == officialRepo [ 1 ] :
officialRepoPresent = True
if url == officialRepo [ 2 ] :
2016-03-21 05:04:29 +01:00
settings . setValue ( key + " /url " , officialRepo [ 1 ] ) # correct a depreciated url
2015-08-22 14:29:41 +02:00
officialRepoPresent = True
if not officialRepoPresent :
settings . setValue ( officialRepo [ 0 ] + " /url " , officialRepo [ 1 ] )
for key in settings . childGroups ( ) :
self . mRepositories [ key ] = { }
2016-09-21 18:24:26 +02:00
self . mRepositories [ key ] [ " url " ] = settings . value ( key + " /url " , " " , type = str )
self . mRepositories [ key ] [ " authcfg " ] = settings . value ( key + " /authcfg " , " " , type = str )
2015-08-22 14:29:41 +02:00
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 ] [ " Relay " ] = Relay ( key )
self . mRepositories [ key ] [ " xmlData " ] = None
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 " ] + self . urlParams ( ) )
2016-07-21 22:01:38 +10:00
#v=str(Qgis.QGIS_VERSION_INT)
2015-08-22 14:29:41 +02:00
#url.addQueryItem('qgis', '.'.join([str(int(s)) for s in [v[0], v[1:3]]]) ) # don't include the bugfix version!
self . mRepositories [ key ] [ " QRequest " ] = QNetworkRequest ( url )
2015-12-03 17:45:41 -07:00
authcfg = self . mRepositories [ key ] [ " authcfg " ]
2016-09-21 18:24:26 +02:00
if authcfg and isinstance ( authcfg , str ) :
2015-12-03 17:45:41 -07:00
if not QgsAuthManager . instance ( ) . updateNetworkRequest (
self . mRepositories [ key ] [ " QRequest " ] , authcfg . strip ( ) ) :
msg = QCoreApplication . translate (
" QgsPluginInstaller " ,
" Update of network request with authentication "
" credentials FAILED for configuration ' {0} ' " ) . format ( authcfg )
iface . pluginManagerInterface ( ) . pushMessage ( msg , QgsMessageBar . WARNING )
self . mRepositories [ key ] [ " QRequest " ] = None
return
2015-08-22 14:29:41 +02:00
self . mRepositories [ key ] [ " QRequest " ] . setAttribute ( QNetworkRequest . User , key )
self . mRepositories [ key ] [ " xmlData " ] = QgsNetworkAccessManager . instance ( ) . get ( self . mRepositories [ key ] [ " QRequest " ] )
self . mRepositories [ key ] [ " xmlData " ] . setProperty ( ' reposName ' , key )
self . mRepositories [ key ] [ " xmlData " ] . downloadProgress . connect ( self . mRepositories [ key ] [ " Relay " ] . dataReadProgress )
self . mRepositories [ key ] [ " xmlData " ] . finished . connect ( self . xmlDownloaded )
# ----------------------------------------- #
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 ] [ " state " ] == 1 and self . mRepositories [ key ] [ " xmlData " ] and self . mRepositories [ key ] [ " xmlData " ] . isRunning ( ) :
self . mRepositories [ key ] [ " xmlData " ] . finished . disconnect ( )
self . mRepositories [ key ] [ " xmlData " ] . abort ( )
# ----------------------------------------- #
def xmlDownloaded ( self ) :
""" populate the plugins object with the fetched data """
reply = self . sender ( )
reposName = reply . property ( ' reposName ' )
if reply . error ( ) != QNetworkReply . NoError : # fetching failed
self . mRepositories [ reposName ] [ " state " ] = 3
self . mRepositories [ reposName ] [ " error " ] = reply . errorString ( )
if reply . error ( ) == QNetworkReply . OperationCanceledError :
self . mRepositories [ reposName ] [ " error " ] + = " \n \n " + QCoreApplication . translate ( " QgsPluginInstaller " , " If you haven ' t cancelled the download manually, it was most likely caused by a timeout. In this case consider increasing the connection timeout value in QGIS options window. " )
else :
reposXML = QDomDocument ( )
content = reply . readAll ( )
# Fix lonely ampersands in metadata
2016-03-14 20:26:58 +01:00
a = QByteArray ( )
a . append ( " & " )
b = QByteArray ( )
b . append ( " & " )
content = content . replace ( a , b )
reposXML . setContent ( content )
2015-08-22 14:29:41 +02:00
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
deprecated = False
if pluginNodes . item ( i ) . firstChildElement ( " deprecated " ) . text ( ) . strip ( ) . upper ( ) in [ " TRUE " , " YES " ] :
deprecated = True
2016-06-01 10:26:12 +03:00
trusted = False
if pluginNodes . item ( i ) . firstChildElement ( " trusted " ) . text ( ) . strip ( ) . upper ( ) in [ " TRUE " , " YES " ] :
trusted = True
2015-08-22 14:29:41 +02:00
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 )
if pluginNodes . item ( i ) . toElement ( ) . hasAttribute ( " plugin_id " ) :
plugin_id = pluginNodes . item ( i ) . toElement ( ) . attribute ( " plugin_id " )
else :
plugin_id = None
plugin = {
" id " : name ,
" plugin_id " : plugin_id ,
" name " : pluginNodes . item ( i ) . toElement ( ) . attribute ( " name " ) ,
" version_available " : pluginNodes . item ( i ) . toElement ( ) . attribute ( " version " ) ,
" description " : pluginNodes . item ( i ) . firstChildElement ( " description " ) . text ( ) . strip ( ) ,
" about " : pluginNodes . item ( i ) . firstChildElement ( " about " ) . 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 ,
" deprecated " : deprecated ,
2016-06-01 10:26:12 +03:00
" trusted " : trusted ,
2015-08-22 14:29:41 +02:00
" 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 " ] :
2016-07-21 22:01:38 +10:00
if isCompatible ( Qgis . QGIS_VERSION , qgisMinimumVersion , qgisMaximumVersion ) :
2016-01-30 09:33:24 +11:00
#add the plugin to the cache
2015-08-22 14:29:41 +02:00
plugins . addFromRepository ( plugin )
self . mRepositories [ reposName ] [ " state " ] = 2
else :
# no plugin metadata found
self . mRepositories [ reposName ] [ " state " ] = 3
if reply . attribute ( QNetworkRequest . HttpStatusCodeAttribute ) == 200 :
self . mRepositories [ reposName ] [ " error " ] = QCoreApplication . translate ( " QgsPluginInstaller " , " Server response is 200 OK, but doesn ' t contain plugin metatada. This is most likely caused by a proxy or a wrong repository URL. You can configure proxy settings in QGIS options. " )
else :
self . mRepositories [ reposName ] [ " error " ] = QCoreApplication . translate ( " QgsPluginInstaller " , " Status code: " ) + " %d %s " % (
reply . attribute ( QNetworkRequest . HttpStatusCodeAttribute ) ,
reply . attribute ( QNetworkRequest . HttpReasonPhraseAttribute )
)
self . repositoryFetched . emit ( reposName )
# is the checking done?
if not self . fetchingInProgress ( ) :
self . checkingDone . emit ( )
reply . deleteLater ( )
# ----------------------------------------- #
def inspectionFilter ( self ) :
""" return inspection filter (only one repository to be fetched) """
return self . mInspectionFilter
# ----------------------------------------- #
def setInspectionFilter ( self , key = None ) :
""" temporarily disable all repositories but this for inspection """
self . mInspectionFilter = key
2013-06-09 15:20:16 +02:00
2013-05-19 17:44:57 +02:00
# --- /class Repositories ---------------------------------------------------------------- #
# --- class Plugins ---------------------------------------------------------------------- #
class Plugins ( QObject ) :
2013-06-09 15:20:16 +02:00
2015-08-22 14:29:41 +02:00
""" A dict-like class for handling plugins data """
# ----------------------------------------- #
def __init__ ( self ) :
QObject . __init__ ( self )
2016-03-21 05:04:29 +01:00
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
2015-08-22 14:29:41 +02:00
# ----------------------------------------- #
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 clearRepoCache ( self ) :
""" clears the repo cache before re-fetching repositories """
self . repoCache = { }
# ----------------------------------------- #
def addFromRepository ( self , plugin ) :
""" add given plugin to the repoCache """
repo = plugin [ " zip_repository " ]
2013-06-08 21:18:43 +02:00
try :
2015-08-22 14:29:41 +02:00
self . repoCache [ repo ] + = [ plugin ]
2013-07-15 16:34:07 +02:00
except :
2015-08-22 14:29:41 +02:00
self . repoCache [ repo ] = [ plugin ]
# ----------------------------------------- #
def removeInstalledPlugin ( self , key ) :
""" remove given plugin from the localCache """
if key in self . localCache :
del self . localCache [ key ]
# ----------------------------------------- #
def removeRepository ( self , repo ) :
""" remove whole repository from the repoCache """
if repo in self . repoCache :
del self . repoCache [ repo ]
# ----------------------------------------- #
def getInstalledPlugin ( self , key , path , readOnly , testLoad = True ) :
""" get the metadata of an installed plugin """
def metadataParser ( 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 """
global errorDetails
2016-03-23 11:52:54 +01:00
cp = configparser . ConfigParser ( )
2015-08-22 14:29:41 +02:00
try :
2016-09-23 12:36:05 +07:00
with codecs . open ( metadataFile , " r " , " utf8 " ) as f :
cp . read_file ( f )
2015-08-22 14:29:41 +02:00
return cp . get ( ' general ' , fct )
except Exception as e :
if not errorDetails :
2016-03-21 05:04:29 +01:00
errorDetails = e . args [ 0 ] # set to the first problem
2015-08-22 14:29:41 +02:00
return " "
def pluginMetadata ( fct ) :
""" calls metadataParser for current l10n.
If failed , fallbacks to the standard metadata """
locale = QLocale . system ( ) . name ( )
if locale and fct in translatableAttributes :
value = metadataParser ( " %s [ %s ] " % ( fct , locale ) )
if value :
return value
value = metadataParser ( " %s [ %s ] " % ( fct , locale . split ( " _ " ) [ 0 ] ) )
if value :
return value
return metadataParser ( fct )
if not QDir ( path ) . exists ( ) :
return
2016-03-21 05:04:29 +01:00
global errorDetails # to communicate with the metadataParser fn
2015-08-22 14:29:41 +02:00
plugin = dict ( )
error = " "
errorDetails = " "
version = None
metadataFile = os . path . join ( path , ' metadata.txt ' )
if os . path . exists ( metadataFile ) :
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 ] + " .99 "
#if compatible, add the plugin to the list
2016-07-21 22:01:38 +10:00
if not isCompatible ( Qgis . QGIS_VERSION , qgisMinimumVersion , qgisMaximumVersion ) :
2015-08-22 14:29:41 +02:00
error = " incompatible "
errorDetails = " %s - %s " % ( qgisMinimumVersion , qgisMaximumVersion )
elif testLoad :
# only testLoad if compatible version
try :
pkg = __import__ ( key )
reload ( pkg )
pkg . classFactory ( iface )
except Exception as e :
error = " broken "
2016-09-21 18:24:26 +02:00
errorDetails = str ( e . args [ 0 ] )
2015-08-22 14:29:41 +02:00
except SystemExit as e :
error = " broken "
errorDetails = QCoreApplication . translate ( " QgsPluginInstaller " , " The plugin exited with error status: {0} " ) . format ( e . args [ 0 ] )
except :
error = " broken "
errorDetails = QCoreApplication . translate ( " QgsPluginInstaller " , " Unknown error " )
elif not os . path . exists ( metadataFile ) :
error = " broken "
errorDetails = QCoreApplication . translate ( " QgsPluginInstaller " , " Missing metadata file " )
else :
error = " broken "
e = errorDetails
errorDetails = QCoreApplication . translate ( " QgsPluginInstaller " , u " Error reading metadata " )
if e :
errorDetails + = " : " + e
if not version :
version = " ? "
if error [ : 16 ] == " No module named " :
mona = error . replace ( " No module named " , " " )
if mona != key :
error = " dependent "
errorDetails = mona
icon = pluginMetadata ( " icon " )
if QFileInfo ( icon ) . isRelative ( ) :
icon = path + " / " + icon
plugin = {
" id " : key ,
" plugin_id " : None ,
" name " : pluginMetadata ( " name " ) or key ,
" description " : pluginMetadata ( " description " ) ,
" about " : pluginMetadata ( " about " ) ,
" 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 " ] ,
" deprecated " : pluginMetadata ( " deprecated " ) . strip ( ) . upper ( ) in [ " TRUE " , " YES " ] ,
2016-06-01 10:26:12 +03:00
" trusted " : False ,
2015-08-22 14:29:41 +02:00
" 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 = { }
# reversed list of the plugin paths: first system plugins -> then user plugins -> finally custom path(s)
pluginPaths = list ( plugin_paths )
pluginPaths . reverse ( )
for pluginsPath in pluginPaths :
isTheSystemDir = ( pluginPaths . index ( pluginsPath ) == 0 ) # The curent dir is the system plugins dir
if isTheSystemDir :
# 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 ( ) :
if key not in [ " . " , " .. " ] :
2016-09-22 14:25:32 +07:00
path = QDir . toNativeSeparators ( pluginsPath + " / " + key )
2015-08-22 14:29:41 +02:00
# readOnly = not QFileInfo(pluginsPath).isWritable() # On windows testing the writable status isn't reliable.
readOnly = isTheSystemDir # Assume only the system plugins are not writable.
# only test those not yet loaded. Loaded plugins already proved they're o.k.
2016-02-15 12:51:53 +01:00
# failedToLoad = settings.value("/PythonPlugins/watchDog/" + key) is not None
testLoadThis = testLoad and key not in qgis . utils . plugins
2015-08-22 14:29:41 +02:00
plugin = self . getInstalledPlugin ( key , path = path , readOnly = readOnly , testLoad = testLoadThis )
self . localCache [ key ] = plugin
2016-09-21 18:24:26 +02:00
if key in list ( self . localCache . keys ( ) ) and compareVersions ( self . localCache [ key ] [ " version_installed " ] , plugin [ " version_installed " ] ) == 1 :
2015-08-22 14:29:41 +02:00
# An obsolete plugin in the "user" location is masking a newer one in the "system" location!
self . obsoletePlugins + = [ key ]
except :
# it's not necessary to stop if one of the dirs is inaccessible
pass
if isTheSystemDir :
# remove the temporarily added path
sys . path . remove ( pluginsPath )
# ----------------------------------------- #
def rebuild ( self ) :
""" build or rebuild the mPlugins from the caches """
self . mPlugins = { }
2016-09-21 18:24:26 +02:00
for i in list ( self . localCache . keys ( ) ) :
2015-08-22 14:29:41 +02:00
self . mPlugins [ i ] = self . localCache [ i ] . copy ( )
settings = QSettings ( )
allowExperimental = settings . value ( settingsGroup + " /allowExperimental " , False , type = bool )
allowDeprecated = settings . value ( settingsGroup + " /allowDeprecated " , False , type = bool )
2016-09-21 18:24:26 +02:00
for i in list ( self . repoCache . values ( ) ) :
2015-08-22 14:29:41 +02:00
for j in i :
2016-03-21 05:04:29 +01:00
plugin = j . copy ( ) # do not update repoCache elements!
2015-08-22 14:29:41 +02:00
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 ( allowDeprecated or not plugin [ " deprecated " ] ) \
and not ( key in self . mPlugins 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 key not in self . mPlugins :
self . mPlugins [ key ] = plugin # just add a new plugin
else :
# update local plugin with remote metadata
# description, about, icon: only use remote data if local one not available. Prefer local version because of i18n.
# NOTE: don't prefer local name to not desynchronize names if repository doesn't support i18n.
# Also prefer local icon to avoid downloading.
for attrib in translatableAttributes + [ " icon " ] :
if attrib != " name " :
if not self . mPlugins [ key ] [ attrib ] and plugin [ attrib ] :
self . mPlugins [ key ] [ attrib ] = plugin [ attrib ]
2017-01-16 15:13:30 +01:00
# other remote metadata is preferred:
2015-08-22 14:29:41 +02:00
for attrib in [ " name " , " plugin_id " , " description " , " about " , " category " , " tags " , " changelog " , " author_name " , " author_email " , " homepage " ,
" tracker " , " code_repository " , " experimental " , " deprecated " , " version_available " , " zip_repository " ,
2016-07-25 20:24:12 +03:00
" download_url " , " filename " , " downloads " , " average_vote " , " rating_votes " , " trusted " ] :
2015-08-22 14:29:41 +02:00
if attrib not in translatableAttributes or attrib == " name " : # include name!
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 ( )
2016-09-21 18:24:26 +02:00
seenPlugins = settings . value ( seenPluginGroup , list ( self . mPlugins . keys ( ) ) , type = str )
2015-08-22 14:29:41 +02:00
if len ( seenPlugins ) > 0 :
2016-09-21 18:24:26 +02:00
for i in list ( self . mPlugins . keys ( ) ) :
2015-08-22 14:29:41 +02:00
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 ( )
2016-09-21 18:24:26 +02:00
seenPlugins = settings . value ( seenPluginGroup , list ( self . mPlugins . keys ( ) ) , type = str )
for i in list ( self . mPlugins . keys ( ) ) :
2015-08-22 14:29:41 +02:00
if seenPlugins . count ( i ) == 0 :
seenPlugins + = [ i ]
settings . setValue ( seenPluginGroup , seenPlugins )
# ----------------------------------------- #
def isThereAnythingNew ( self ) :
""" return true if an upgradeable or new plugin detected """
2016-09-21 18:24:26 +02:00
for i in list ( self . mPlugins . values ( ) ) :
2015-08-22 14:29:41 +02:00
if i [ " status " ] in [ " upgradeable " , " new " ] :
return True
return False
2013-05-19 17:44:57 +02:00
# --- /class Plugins --------------------------------------------------------------------- #
# public instances:
repositories = Repositories ( )
plugins = Plugins ( )