2 # -*- coding: utf-8 -*-
7 Copyright (c) 2013 Frédéric Massart - FMCorz.net
9 This program is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
22 http://github.com/FMCorz/mdk
28 from urllib
import urlencode
, urlretrieve
33 from tempfile
import gettempdir
34 from .config
import Conf
40 class PluginManager(object):
45 'availability': '/availability',
46 'backup': '/backup/util/ui',
51 'calendar': '/calendar',
53 'competency': '/competency',
55 'editor': '/lib/editor',
60 'grading': '/grade/grading',
62 'message': '/message',
66 'plagiarism': '/plagiarism',
67 'portfolio': '/portfolio',
68 'publish': '/course/publish',
69 'question': '/question',
71 'register': '/{admin}/registration',
72 'repository': '/repository',
74 'role': '/{admin}/roles',
78 'webservice': '/webservice'
82 'antivirus': '/lib/antivirus',
83 'availability': '/availability/condition',
84 'qtype': '/question/type',
87 'calendartype': '/calendar/type',
89 'message': '/message/output',
92 'editor': '/lib/editor',
93 'format': '/course/format',
94 'profilefield': '/user/profile/field',
96 'coursereport': '/course/report', # Must be after system reports.
97 'gradeexport': '/grade/export',
98 'gradeimport': '/grade/import',
99 'gradereport': '/grade/report',
100 'gradingform': '/grade/grading/form',
101 'mnetservice': '/mnet/service',
102 'webservice': '/webservice',
103 'repository': '/repository',
104 'portfolio': '/portfolio',
105 'search': '/search/engine',
106 'qbehaviour': '/question/behaviour',
107 'qformat': '/question/format',
108 'plagiarism': '/plagiarism',
109 'tool': '/{admin}/tool',
110 'cachestore': '/cache/stores',
111 'cachelock': '/cache/locks',
116 _supportSubtypes
= ['mod', 'editor', 'local', 'tool']
119 def extract(cls
, f
, plugin
, M
, override
=False):
120 """Extract a plugin zip file to the plugin directory of M"""
122 if type(plugin
) != PluginObject
:
123 raise ValueError('PluginObject expected')
125 if not override
and cls
.hasPlugin(plugin
, M
):
126 raise Exception('Plugin directory already exists')
128 if not cls
.validateZipFile(f
, plugin
.name
):
129 raise Exception('Invalid zip file')
131 zp
= zipfile
.ZipFile(f
)
133 logging
.info('Extracting plugin...')
134 rootDir
= os
.path
.commonprefix(zp
.namelist())
135 extractIn
= cls
.getTypeDirectory(plugin
.t
, M
)
136 zp
.extractall(extractIn
)
137 if plugin
.name
!= rootDir
.rstrip('/'):
138 orig
= os
.path
.join(extractIn
, rootDir
).rstrip('/')
139 dest
= os
.path
.join(extractIn
, plugin
.name
).rstrip('/')
142 for src_dir
, dirs
, files
in os
.walk(orig
):
143 dst_dir
= src_dir
.replace(orig
, dest
)
144 if not os
.path
.exists(dst_dir
):
147 src_file
= os
.path
.join(src_dir
, file_
)
148 dst_file
= os
.path
.join(dst_dir
, file_
)
149 if os
.path
.exists(dst_file
):
151 shutil
.move(src_file
, dst_dir
)
156 raise Exception('Error while extracting the files')
159 def getTypeAndName(cls
, plugin
):
160 """Accepts a full plugin name 'mod_book' and returns the type and plugin name"""
162 if plugin
== 'moodle' or plugin
== 'core' or plugin
== '':
163 return ('core', None)
165 if not '_' in plugin
:
170 (t
, name
) = plugin
.split('_', 1)
177 def getSubsystems(cls
):
178 """Return the list of subsytems and their relative directory"""
179 return cls
._subSystems
182 def getSubsystemDirectory(cls
, subsystem
, M
=None):
183 """Return the subsystem directory, absolute if M is passed"""
184 path
= cls
._subSystems
.get(subsystem
)
186 raise ValueError('Unknown subsystem')
189 path
= path
.replace('{admin}', M
.get('admin', 'admin'))
190 path
= os
.path
.join(M
.get('path'), path
.strip('/'))
195 def getSubsystemOrPluginFromPath(cls
, path
, M
=None):
196 """Get a subsystem from a path. Path should be relative to dirroot or M should be passed.
198 This returns a tuple containing the name of the subsystem or plugin type, and the plugin name
199 if we could resolve one.
203 path
= os
.path
.realpath(os
.path
.abspath(path
))
205 path
= '/' + path
.replace(M
.get('path'), '').strip('/')
206 admindir
= M
.get('admin', 'admin')
207 if path
.startswith('/' + admindir
):
208 path
= re
.sub(r
'^/%s' % admindir
, '/{admin}', path
)
209 subtypes
= cls
.getSubtypes(M
)
210 path
= '/' + path
.lstrip('/')
212 pluginOrSubsystem
= None
217 while head
and head
!= '/' and not pluginOrSubsystem
:
218 # Check plugin types.
219 if not pluginOrSubsystem
:
220 for k
, v
in cls
._pluginTypesPath
.iteritems():
222 pluginOrSubsystem
= k
226 # Check sub plugin types.
227 if not pluginOrSubsystem
:
228 for k
, v
in subtypes
.iteritems():
230 pluginOrSubsystem
= k
235 for k
, v
in cls
._subSystems
.iteritems():
237 pluginOrSubsystem
= k
240 (head
, tail
) = os
.path
.split(candidate
)
243 return (pluginOrSubsystem
, pluginName
)
246 def getSubtypes(cls
, M
):
247 """Get the sub plugins declared in an instance"""
248 regex
= re
.compile(r
'\s*(?P<brackets>[\'"])(.*?)(?P=brackets)\s*=>\s*(?P=brackets)(.*?)(?P=brackets)')
250 for t in cls._supportSubtypes:
251 path = cls.getTypeDirectory(t, M)
252 dirs = os.listdir(path)
254 if not os.path.isdir(os.path.join(path, d)):
256 subpluginsfile = os.path.join(path, d, 'db', 'subplugins.php')
257 if not os.path.isfile(subpluginsfile):
261 f = open(subpluginsfile, 'r')
263 if '$subplugins' in line:
267 search = regex.findall(line)
270 subtypes[match[1]] = '/' + match[2].replace('admin/', '{admin}/').lstrip('/')
272 # Exit when we find a semi-colon.
273 if searchOpen and ';' in line:
279 def getTypeDirectory(cls, t, M=None):
280 """Returns the path to the plugin type directory. If M is passed, the full path is returned."""
281 path = cls._pluginTypesPath.get(t, False)
284 subtypes = cls.getSubtypes(M)
285 path = subtypes.get(t, False)
287 raise ValueError('Unknown plugin or subplugin type')
291 themedir = M.get('themedir', None)
295 path = path.replace('{admin}', M.get('admin', 'admin'))
296 path = os.path.join(M.get('path'), path.strip('/'))
301 def hasPlugin(cls, plugin, M):
302 path = cls.getTypeDirectory(plugin.t, M)
303 target = os.path.join(path, plugin.name)
304 return os.path.isdir(target)
307 def validateZipFile(cls, f, name):
308 zp = zipfile.ZipFile(f, 'r')
310 # Checking that the content is all contained in one single directory
311 rootDir = os.path.commonprefix(zp.namelist())
317 class PluginObject(object):
323 def __init__(self, component):
324 self.component = component
325 (self.t, self.name) = PluginManager.getTypeAndName(component)
328 def getDownloadInfo(self, branch):
329 if not self.dlinfo.get(branch, False):
330 self.dlinfo[branch] = PluginRepository().info(self.component, branch)
331 return self.dlinfo.get(branch, False)
333 def getZip(self, branch, fileCache=None):
334 dlinfo = self.getDownloadInfo(branch)
337 return dlinfo.download(fileCache)
340 class PluginDownloadInfo(dict):
342 def download(self, fileCache=None, cacheDir=C.get('dirs.mdk')):
343 """Download a plugin"""
345 if fileCache == None:
346 fileCache = C.get('plugins.fileCache')
348 dest = os.path.abspath(os.path.expanduser(os.path.join(cacheDir, 'plugins')))
352 if not 'downloadurl' in self.keys():
353 raise ValueError('Expecting the key downloadurl')
354 elif not 'component' in self.keys():
355 raise ValueError('Expecting the key component')
356 elif not 'branch' in self.keys():
357 raise ValueError('Expecting the key branch')
359 dl = self.get('downloadurl')
360 plugin = self.get('component')
361 branch = self.get('branch')
362 target = os.path.join(dest, '%s-%d.zip' % (plugin, branch))
363 md5sum = self.get('downloadmd5')
364 release = self.get('release', 'Unknown')
367 if not os.path.isdir(dest):
368 logging.debug('Creating directory %s' % (dest))
369 tools.mkdir(dest, 0777)
371 if os.path.isfile(target) and (md5sum == None or tools.md5file(target) == md5sum):
372 logging.info('Found cached plugin file: %s' % (os.path.basename(target)))
375 logging.info('Downloading %s (%s)' % (plugin, release))
376 if logging.getLogger().level <= logging.INFO:
377 urlretrieve(dl, target, tools.downloadProcessHook)
378 # Force a new line after the hook display
381 urlretrieve(dl, target)
383 # Highly memory inefficient MD5 check
384 if md5sum and tools.md5file(target) != md5sum:
386 logging.warning('Bad MD5 sum on downloaded file')
392 class PluginRepository(object):
396 host = 'download.moodle.org'
398 localRepository = None
401 def __new__(cls, *args, **kwargs):
402 if not cls._instance:
403 cls._instance = super(PluginRepository, cls).__new__(cls, *args, **kwargs)
404 cls._instance.localRepository = C.get('plugins.localRepository')
405 cls._instance.localRepository = {} if cls._instance.localRepository == None else cls._instance.localRepository
408 def info(self, plugin, branch):
409 """Gets the download information of the plugin, branch is expected to be
410 a whole integer, such as 25 for 2.5, etc...
413 if type(branch) != int:
414 raise ValueError('Branch must be an integer')
416 # Checking local repository
417 lr = self.localRepository.get(plugin, False)
419 info = lr.get(branch, None)
421 versions = [v for v in range(branch, 18, -1)]
423 info = lr.get('>=%d' % v, None)
426 if info and info.get('downloadurl'):
427 logging.info('Found a compatible version for the plugin in local repository')
428 info['component'] = plugin
429 info['branch'] = branch
430 return PluginDownloadInfo(info)
432 # Contacting the remote repository
434 "branch
": round(float(branch) / 10., 1),
438 logging.info('Retrieving information for plugin %s and branch %s' % (data['plugin'], data['branch']))
440 resp = self.request('pluginfo.php', 'GET', data)
441 except PluginRepositoryNotFoundException:
442 logging.info('No result found')
444 except PluginRepositoryException:
445 logging.warning('Error while retrieving information from the plugin database')
448 pluginfo = resp.get('data', {}).get('pluginfo', {})
449 pluginfo['branch'] = branch
451 return PluginDownloadInfo(pluginfo)
453 def request(self, uri, method, data, headers={}):
454 """Sends a request to the server and returns the response status and data"""
456 uri = self.uri + '/' + str(self.apiversion) + '/' + uri.strip('/')
457 method = method.upper()
459 if type(data) == dict:
460 data = urlencode(data)
461 uri += '?%s' % (data)
465 r = httplib.HTTPSConnection(self.host)
467 r = httplib.HTTPConnection(self.host)
468 logging.debug('%s %s%s' % (method, self.host, uri))
469 r.request(method, uri, data, headers)
471 resp = r.getresponse()
472 if resp.status == 404:
473 raise PluginRepositoryNotFoundException()
474 elif resp.status != 200:
475 raise PluginRepositoryException('Error during the request to the plugin database')
480 data = json.loads(data)
482 raise PluginRepositoryException('Could not parse JSON data. Data received:\n%s' % data)
484 return {'status': resp.status, 'data': data}
487 class PluginRepositoryException(Exception):
491 class PluginRepositoryNotFoundException(Exception):