# plugs/install.py
#
#

""" signature of downloaded plugins is verified by signature files """

__copyright__ = 'this file is in the public domain'

from gozerbot.generic import geturl2, waitforuser, touch, lockdec, rlog
from gozerbot.commands import cmnds
from gozerbot.examples import examples
from gozerbot.plugins import plugins
from gozerbot.plughelp import plughelp
from gozerbot.aliases import aliasdel
from gozerbot.pdod import Pdod
from gozerbot.dol import Dol
from gozerbot.datadir import datadir
from gozerbot.pgp import pgp, PgpNoPubkeyException, NoGPGException

from gozerbot.fileutils import tarextract
import re, urllib2, urlparse, os, thread, shutil

class Cfg(Pdod):

    """ contains plugin version data """
    def __init__(self):
        Pdod.__init__(self, os.path.join(datadir, 'install'))

    def add(self, plugin, version):
        """ add plugin version """
        self.data[plugin] = version
        self.save()

    def list(self):
        """ list plugin versions """
        return self.data

cfg = Cfg()
installlock = thread.allocate_lock()
locked = lockdec(installlock)

plughelp.add('install', 'install plugin from remote site')

# check if logs dir exists if not create it
if not os.path.isdir('myplugs'):
    os.mkdir('myplugs')
    touch('myplugs/__init__.py') 

installsites = ['http://gozerbot.org/plugs', 'http://tehmaze.com/plugs']

class InstallerException(Exception):
    pass

class Installer(object):
    '''
    We're taking care of installing and verifying plugins.
    '''
    
    BACKUP_PATTERN = '%s~'
    SUPPORTED_EXT = ['tar', 'py'] # supported extensions, in this order

    def install(self, plugname, site=''):
        '''
        Install `plugname` from `site`. If no `site` is specified, we scan the list of
        pre-configured `installsites`.
        '''
        errors = [] 
        if plugname.endswith('.py'):
            plugname = plugname[:3]
        plugname = plugname.split(os.sep)[-1]
        if not site:
            for site in installsites:
                try:
                    plugdata, plugsig, plugtype = self.fetchplug(site, plugname)
                    break # no exception? found it!
                except InstallerException, e:
                    errors.append(str(e))
        else:
            plugdata, plugsig, plugtype = self.fetchplug(site, plugname)
        if not plugdata:
            if errors:
                raise InstallerException(', '.join(str(e) for e in errors))
            else:
                raise InstallerException('nothing to do')
        # still there? good
        if self.validate(plugdata, plugsig):
            if plugtype == 'py':
                self.save_py(plugname, plugdata)
            elif plugtype == 'tar':
                self.save_tar(plugname, plugdata)
            if hasattr(plugdata, 'info') and plugdata.info.has_key('last-modified'):
                cfg.add(plugname, ['%s/%s.%s' % (site, plugname, plugtype), plugdata.info['last-modified']])
            return '%s/%s.%s' % (site, plugname, plugtype)
        else:
            raise InstallerException('%s.%s signature validation failed' % (plugname, plugtype))

    def fetchplug(self, site, plugname):
        '''
        Fetch the plugin with `plugname` from `site`, we try any extension available in
        `Installer.SUPPORTED_EXT`. We also fetch the signature file.
        '''
        errors = []
        if not site.startswith('http://'):
            site = 'http://%s' % site
        base = '%s/%s' % (site, plugname)
        for ext in self.SUPPORTED_EXT:
            try:
                plugdata, plugsig = self.fetchplugdata(base, ext)
                return plugdata, plugsig, ext
            except InstallerException, e:
                errors.append(str(e))
        raise InstallerException(errors)

    def fetchplugdata(self, base, ext='py'):
        '''
        Here the actual HTTP request is being made.
        '''
        url = '%s.%s' % (base, ext)
        plug = base.split('/')[-1]
        try:
            plugdata = geturl2(url)
        except urllib2.HTTPError:
            raise InstallerException("no %s" % os.path.basename(url))
        if plugdata:
            try:
                plugsig = geturl2('%s.asc' % url)
            except urllib2.HTTPError:
                raise InstallerException('%s plugin has no signature' % plug)
            else:
                return plugdata, plugsig

    def backup(self, plugname):
        '''
        Backup a plugin with name `plugname` and return if we backed up a file or
        directory, and what its original name was.
        '''
        oldtype = None
        oldname = os.path.join('myplugs', plugname)
        newname = self.BACKUP_PATTERN % oldname
        if os.path.isfile('%s.py' % oldname):
            newname = self.BACKUP_PATTERN % '%s.py' % oldname
            oldname = '%s.py' % oldname
            oldtype = 'file'
            if os.path.isfile(newname):
                os.unlink(newname)
            os.rename(oldname, newname)
        elif os.path.isdir(oldname):
            oldtype = 'dir'
            if os.path.isdir(newname):
                shutil.rmtree(newname)
            os.rename(oldname, newname)
        return oldtype, newname

    def restore(self, plugname, oldtype, oldname):
        '''
        Restore a plugin with `plugname` from `oldname` with type `oldtype`.
        '''
        newname = os.path.join('myplugs', plugname)
        if oldtype == 'dir':
            # remove partial 'new' data
            if os.path.isdir(newname):
                shutils.rmtree(newname)
            os.rename(oldname, newname)
        elif oldtype == 'file':
            newname = '%s.py' % newname
            if os.path.isfile(newname):
                os.unlink(newname)
            os.rename(oldname, newname)

    @locked
    def save_tar(self, plugname, plugdata):
        oldtype, oldname = self.backup(plugname)
        try:
            if not tarextract(plugname, str(plugdata), 'myplugs'): # because it is an istr
                raise InstallerException('%s.tar had no (valid) files to extract' % (plugname))
        except InstallerException:
            raise
        except Exception, e:
            self.restore(plugname, oldtype, oldname)
            raise InstallerException('restored backup, error while extracting %s: %s' % (plugname, str(e))) 

    def save_py(self, plugname, plugdata):
        oldtype, oldname = self.backup(plugname)
        try:
            plugfile = open(os.path.join('myplugs', '%s.py' % plugname), 'w')
            plugfile.write(plugdata)
            plugfile.close()
        except InstallerException:
            raise
        except Exception, e:
            self.restore(plugname, oldtype, oldname)
            raise InstallerException('restored backup, error while writing %s: %s' % (plugfile, str(e))) 

    def validate(self, plugdata, plugsig):
        fingerprint = pgp.verify_signature(plugdata, plugsig)
        if not fingerprint:
            return False
        return True
        
def update():
    """ update remote installed plugins """
    updated = []
    all = cfg.list()
    for plug in all.keys():
        try:
            if plug in plugins.plugdeny.data:
                continue
            data = geturl2(all[plug][0])
            if hasattr(data, 'info') and data.info.has_key('last-modified'):
                if data.info['last-modified'] > all[plug][1]:
                    updated.append(all[plug][0])
        except urllib2.HTTPError:
            pass
        except urllib2.URLError:
            pass
    return updated

def handle_install(bot, ievent):
    """ install <server> <dir> <plugin> .. install plugin from server """
    try:
        (server, dirr, plug) = ievent.args
    except ValueError:
        if 'install-plug' in ievent.origtxt:
            ievent.missing("<server> <dir> <plug>")
        else:
            ievent.missing("<server> <dir> <plug> (maybe try install-plug ?)")
        return
    if plug.endswith('.py'):
        plug = plug[:-3]
    plug = plug.split(os.sep)[-1]
    url = 'http://%s/%s/%s' % (server, dirr, plug)
    try:
        readme = geturl2(url + '.README')
        if readme:
            readme = readme.replace('\n', ' .. ')
            readme = re.sub('\s+', ' ', readme)
            readme += ' (yes/no)'
            ievent.reply(readme)
            response =  waitforuser(bot, ievent.userhost)
            if not response or response.txt != 'yes':
                ievent.reply('not installing %s' % plug)
                return
    except:
        pass
    installer = Installer()
    try:
        installer.install(plug, 'http://%s/%s' % (server, dirr))
    except InstallerException, e:
        ievent.reply('error installing %s: ' % plug, result=[str(x) for x \
in list(e)], dot=True)
        return
    except Exception, ex:
        ievent.reply(str(ex))
        return
    if plugins.reload('myplugs', plug):
        ievent.reply("%s reloaded" % plug)
    else:
        ievent.reply('reload of %s failed' % plug)

cmnds.add('install', handle_install, 'OPER')
examples.add('install', 'install <server> <dir> <plug>: install \
http://<plugserver>/<dir>/<plugin>.py from server (maybe try install-plug \
?)', 'install gozerbot.org plugs autovoice')

def handle_installplug(bot, ievent):
    """ remotely install a plugin """
    if not ievent.args:
        ievent.missing('<plugname>')
        return
    notinstalled = []
    installed = []
    for plug in ievent.args:
        errors = {}
        ok = False
        installer = Installer()
        url = ''
        plug = plug.split(os.sep)[-1]
        for site in installsites:
            try:
                readme = geturl2('%s/%s.README' % (site, plug))
                if readme:
                    readme = readme.replace('\n', ' .. ')
                    readme = re.sub('\s+', ' ', readme)
                    readme += ' (yes/no)'
                    ievent.reply(readme)
                    response =  waitforuser(bot, ievent.userhost)
                    if not response or response.txt != 'yes':
                        ievent.reply('not installing %s' % plug)
                        notinstalled.append(plug)
                        break
            except:
                pass
            installer = Installer()
            try:
                url = installer.install(plug, site)
                if url:
                    ievent.reply('%s installed' % url) 
                    installed.append(plug)
                    break # stop iterating sites
            except NoGPGException, ex:
                ievent.reply("couldn't run gpg .. please install gnupg if you \
want to install remote plugins")
                return
            except Exception, ex:
                errors[site] = str(ex)
        if plug in installed and plugins.reload('myplugs', plug):
            ievent.reply("%s reloaded" % plug)
            return
        elif plug in installed:
            ievent.reply('reload of %s failed' % plug)
            return
        if plug in notinstalled:
            return
        errordict = Dol()
        errorlist = []
        for i, j in errors.iteritems():
            errordict.add(j, i)
        for error, sites in errordict.iteritems():
            errorlist.append("%s => %s" % (' , '.join(sites), error)) 
        ievent.reply("couldn't install %s .. reasons: " % plug, errorlist, \
dot=True)
    
cmnds.add('install-plug', handle_installplug, 'OPER')
examples.add('install-plug', 'install-plug <list of plugins> .. try to \
install plugins checking all known sites', '1) install-plug 8b 2) \
install-plug 8b koffie')
aliasdel('install-plug')

def handle_installkey(bot, ievent):
    """ install a remote gpg key into the keyring """
    if not bot.ownercheck(ievent):
        return
    if len(ievent.args) != 1:
        return ievent.missing('<key id> or <key file url>')
    url = ievent.args[0]
    if url.startswith('http://'):
        try:
            pubkey = geturl2(url)
        except urllib2.HTTPError:
            return ievent.reply('failed to fetch key from %s' % ievent.args[0])
        except urllib2.URLError, ex:
            ievent.reply("there was a problem fetching %s .. %s" % (url, \
str(ex)))
            return
        fingerprint = pgp.imports(pubkey)
        if fingerprint:
            return ievent.reply('installed key with fingerprint %s' % \
fingerprint)
        else:
            return ievent.reply('no valid pgp public key found')
    if not re.compile('^[0-9a-f]*', re.I).search(ievent.args[0]):
        return ievent.reply('invalid key id') 
    if pgp.imports_keyserver(ievent.args[0]):
        return ievent.reply('imported key %s from %s' % \
(ievent.args[0].upper(), pgp.keyserver))
    ievent.reply('failed to import key %s from %s' % (ievent.args[0].upper(), \
pgp.keyserver))

cmnds.add('install-key', handle_installkey, 'OPER')
examples.add('install-key', 'install a pgp key', \
'1) install-key 2A22EC17F9EBC3D8 2) install-key \
http://pgp.mit.edu:11371/pks/lookup?op=get&search=0xF9EBC3D8')

def handle_installlist(bot, ievent):
    """ install-list .. list the available remote installable plugs """
    errors = []
    result = []
    for i in installsites:
        try:
            pluglist = geturl2(i)
        except Exception, ex:
            errors.append(i)
            continue
        result += re.findall('<a href="(.*?)\.py">', pluglist)
        try:
            result.remove('__init__')
        except ValueError:
            pass
    if result:
        result.sort()
        ievent.reply('available plugins: ', result, dot=True)
    if errors:
        ievent.reply("couldn't extract plugin list from: ", errors, dot=True)

cmnds.add('install-list', handle_installlist, 'OPER')
examples.add('install-list', 'list plugins that can be installed', \
'install-list')

def handle_installsites(bot, ievent):
    """ show from which sites one can install plugins """
    ievent.reply("install sites: ", installsites, dot=True)

cmnds.add('install-sites', handle_installsites, 'USER')
examples.add('install-sites', 'show verified sites', 'install-sites')

def handle_installclean(bot, ievent):
    import glob
    removed = []
    for item in glob.glob(Installer.BACKUP_PATTERN % os.path.join('myplugs', '*')):
        if os.path.isfile(item):
            os.unlink(item)
            removed.append(os.path.basename(item))
        elif os.path.isdir(item):
            shutil.rmtree(item)
            removed.append(os.path.basename(item))
    if removed:
        ievent.reply('removed: ', result=removed, dot=True)
    else:
        ievent.reply('nothing to do')

#cmnds.add('install-clean', handle_installclean, 'OPER')

def handle_installupdate(bot, ievent):
    """ update command for remote installed plugins """ 
    updating = update()
    update_pass = []
    update_fail = []
    plugs = []
    installer = Installer()
    if updating:
        updating.sort()
        ievent.reply('updating: %s' % ', '.join(updating))
        for release in updating:
            parts = urlparse.urlparse(release)
            what = parts[2].split('/')[-1].replace('.py', '')
            try:
                Installer.install(release)
                update_pass.append(what)
            except Exception, ex:
                update_fail.append(release)
        update_pass.sort()
        update_fail.sort()
        if update_fail:
            ievent.reply("failed updating %s" % " .. ".join(update_fail))
        if update_pass:
            plugs = ['myplugs/%s.py' % plug for plug in update_pass]
            ievent.reply("reloading %s" %  " .. ".join(plugs))
            failed = plugins.listreload(plugs)
            if failed:
                ievent.reply("failed to reload %s" % ' .. '.join(failed))
                return
            else:
                ievent.reply('done')
    else:
        ievent.reply('no updates found') 

cmnds.add('install-update', handle_installupdate, 'OPER')
examples.add('install-update', 'update remote installed plugins', \
'install-update')

def handle_installversion(bot, ievent):
    """ show versions of installed plugins """
    all = cfg.list()
    plugs = all.keys()
    if plugs:
        plugs.sort()
        ievent.reply(', '.join(['%s: %s' % (all[plug][0], all[plug][1]) \
for plug in plugs]))
    else:
        ievent.reply('no versions tracked')

cmnds.add('install-version', handle_installversion, 'OPER')
examples.add('install-version', 'show version of remote installed plugins', \
'install-version')
