#!/usr/bin/env python
# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

u"""Ce script est prévu pour être utilisé dans un script CGI.

Il permet de lister les paramètres du formulaire et d'y accéder depuis un script
bash. Le tableau QVARS est initialisé avec la liste des variables correspondant
aux paramètres pour lesquels une valeur est définie.

Les arguments de ce script doivent être de la forme NAME[=DEFAULT]. Si le
paramètres était fourni dans la requête, il est affiché, sous forme de scalaire
ou de tableau. S'il n'était pas fourni, la valeur par défaut est affichée.
"""

import sys, os, re, cgi, urllib, types, tempfile, csv
from os import path

cgitb = None # marqueur pour savoir si le module cgitb a été importé
#import cgitb; cgitb.enable()

def quote(value, q=False):
    value = str(value)
    if value or q:
        value = "'%s'" % value.replace("'", "'\\''")
        if value != "''": value = re.sub(r"(?<!\\)''", "", value)
    return value

def print_scalar(name, value):
    print "%s=%s" % (name, quote(value))

def print_array(name, values):
    parts = ["%s=(" % name]
    first = True
    for value in values:
        if first: first = False
        else: parts.append(" ")
        parts.append(quote(value, True))
    parts.append(")")
    print "".join(parts)

class File(object):
    # classe wrapper permettant de savoir si nous avons déjà traité un paramètre
    # de type file
    _f = None
    def __init__(self, f): self.__dict__['_f'] = f
    def __getattr__(self, name): return getattr(self._f, name)
    def __setattr__(self, name, value): setattr(self._f, name, value)

class FileInfo(object):
    FIELDS = ('name', 'value', 'file', 'type', 'invalid')

    _param = None
    _items = None

    def __init__(self, param, *items):
        nitems = len(items)
        nfields = len(self.FIELDS)
        if nitems < nfields: items = items + ('',) * (nfields - nitems)
        items = items[:nfields]
        self._param = param
        self._items = list(items)

    def __len__(self): return len(self.FIELDS)
    def __getitem__(self, key): return self._items[key]

    param = property(lambda self: self._param)
    name = property(lambda self: self._items[0])
    value = property(lambda self: self._items[1])
    file = property(lambda self: self._items[2])
    type = property(lambda self: self._items[3])
    def __set_invalid(self, value): self._items[4] = value
    invalid = property(lambda self: self._items[4], __set_invalid)

class FieldStorage(cgi.FieldStorage):
    fm = None

    def make_file(self, binary=None):
        outf = self.fm._new_file(self)
        if outf is not None: return outf
        return cgi.FieldStorage.make_file(self, binary)

class FileManager(object):
    fimap = None
    filist = None
    tries = None
    count = None
    destdir = None
    spec = None
    type = None
    exitcode = None
    form = None

    def __init__(self, destdir=None, spec=None, type=None, parse_env=True):
        FieldStorage.fm = self
        if destdir is None: destdir = '/tmp'
        self.fimap = {}
        self.filist = []
        self.tries = 0
        self.count = 0
        self.destdir = destdir
        if spec is not None: spec = re.compile(spec)
        self.spec = spec
        self.type = type
        if parse_env: self.parse_env()

    def _new_file(self, param):
        fname = param.name
        if self.spec is not None and self.spec.match(param.filename) is None:
            invalid = 'name'
        elif self.type is not None and self.type != param.type:
            invalid = 'type'
        else:
            invalid = ''
            self.count += 1
        self.tries += 1
        fp = path.join(self.destdir, 'upload%i' % self.tries)
        outf = File(open(fp, 'w+b'))
        fileinfo = FileInfo(param, fname, param.filename, fp, param.type, invalid)
        if not self.fimap.has_key(fname):
            self.fimap[fname] = []
        self.fimap[fname].append(fileinfo)
        self.filist.append(fileinfo)
        return outf

    def parse_env(self):
        self.form = FieldStorage(keep_blank_values=True)
        for param in self.form.list:
            if param.filename and not isinstance(param.file, File):
                outf = self._new_file(param)
                outf.write(param.value)
                outf.close()
        for fi in self.filist:
            param = fi.param
            if param is None: continue
            if param.done == -1:
                fi.invalid = 'incomplete'

    def get_params(self, fname):
        return [param for param in self.form.list if param.name == fname]

    def print_param(self, fname, vname=None, defvalue=None, skip_invalids=None):
        if vname is None: vname = fname.replace('-', '_')
        if self.fimap.has_key(fname):
            filenames = ['%s_filename' % vname]
            files = ['%s_file' % vname]
            types = ['%s_type' % vname]
            invalids = ['%s_invalid' % vname]
            count = 0
            for fi in self.fimap[fname]:
                if fi.invalid and skip_invalids: continue
                filenames.append(fi.value)
                files.append(fi.file)
                types.append(fi.type)
                invalids.append(fi.invalid)
                count += 1
            if count == 0: return False
            print_scalar(vname, "FILE-UPLOAD")
            print_scalar('%s_count' % vname, count)
            for values in filenames, files, types, invalids:
                if len(values) == 2: print_scalar(values[0], values[1])
                else: print_array(values[0], values[1:])
            return True

        values = self.form.getlist(fname)
        if defvalue is not None:
            if len(values) == 0: print_scalar(vname, defvalue)
            elif len(values) == 1: print_scalar(vname, values[0])
            else: print_array(vname, values)
        else:
            if len(values) == 1: print_scalar(vname, values[0])
            else: print_array(vname, values)
        return True

    def write_csv(self, outf, nvs=None, skip_invalids=None):
        close = False
        if type(outf) is types.StringType:
            outf = open(outf, 'w+b')
            close = True
        w = csv.writer(outf)
        try:
            w.writerow(FileInfo.FIELDS)
            if nvs is None:
                nvs = self.form.keys()
                # s'assurer que les noms sont dans l'ordre de filist
                for fi in self.filist:
                    fname = fi.name
                    if fname in nvs:
                        nvs.remove(fname)
                        nvs.append(fname)
            for nv in nvs:
                mo = RE_NAME_VALUE.match(nv)
                if mo is None: continue
                fname, defvalue = mo.group(1), mo.group(2)
                if self.fimap.has_key(fname):
                    for fi in self.fimap[fname]:
                        if not fi.invalid or not skip_invalids:
                            w.writerow(fi)
                else:
                    if defvalue is None: defvalue = ''
                    params = self.get_params(fname)
                    if params:
                        param = params[0]
                        values = [param.value for param in params]
                    else:
                        param = None
                        values = [defvalue]
                    w.writerow(FileInfo(param, fname, ";".join(values)))
        finally:
            if close: outf.close()

    def exitcode(self):
        if self.count == 0: return 2
        elif self.count != self.tries: return 1
        else: return 0

RE_COMMA = re.compile(',')
def lfix(values):
    if values is None: return None
    fvalues = []
    for parts in values:
        parts = RE_COMMA.split(parts)
        fvalues.extend(parts)
    return fvalues

def build_query_string(form, includes=None, excludes=None, prefix=None):
    includes = lfix(includes)
    if includes is None: names = form.keys()
    else: names = includes[:]
    excludes = lfix(excludes)
    if excludes is not None:
        for name in excludes:
            if name in names: names.remove(name)
    params = []
    for name in names:
        values = form.getlist(name)
        for value in values:
            params.append((name, value))
    query_string = urllib.urlencode(params)
    if prefix:
        if query_string != "": query_string = "%s&%s" % (prefix, query_string)
        elif prefix != "": query_string = prefix
    if query_string:
        query_string = "?%s" % query_string
    return query_string

RE_NAME_VALUE = re.compile(r'([a-zA-Z0-9_-]+)(?:=(.*))?$')
SHELL_ACTION = 'shell'
PRINTCSV_ACTION = 'printcsv'
CMD_ACTION = 'cmd'
INFOS_ARGTYPE = 'infos'
FILES_ARGTYPE = 'files'
CSV_ARGTYPE = 'csv'

def run_cgiparams():
    from optparse import OptionParser, OptionGroup
    OP = OptionParser(usage=u"\n\t%prog [options] params...", description=__doc__)
    OP.set_defaults(action=SHELL_ACTION)
    OP.set_defaults(argtype=INFOS_ARGTYPE)
    OP.set_defaults(clear=True)
    OP.add_option('--devel', action='store_true', dest='devel',
                  help=u"Activer le mode développement: le module cgitb est chargé et activé.")
    OG = OptionGroup(OP, "ACTIONS", u"Ces options permettent de choisir le type d'action effectuée")
    OG.add_option('-a', '--shell', action='store_const', const=SHELL_ACTION, dest='action',
                  help=u"Afficher les valeurs pour traitement par le shell. C'est la valeur par défaut.")
    OG.add_option('-b', '--printcsv', action='store_const', const=PRINTCSV_ACTION, dest='action',
                  help=u"Afficher la liste des paramètres et des fichiers reçus au format CSV. Chaque ligne est de la forme 'name,value,file,type,invalid'. S'il s'agit d'un paramètre normal, name est le nom du paramètre, value la liste des valeurs du paramètres séparées par ';', file, type et invalid sont vides. S'il s'agit d'un fichier reçu, name est le nom du paramètre, value le nom du fichier tel qu'il a été fourni par le client, file le chemin complet vers le fichier reçu, type le type mime du contenu, et invalid a une valeur non vide si le fichier ne correspond pas à certains critères. En ce qui concernent le paramètre invalid, il vaut 'name' si le fichier reçu ne correspond pas au paramètre --filespec, 'type' si le fichier ne correspond pas au paramètre --type, ou 'incomplete' si le fichier n'a pas été transféré complètement.")
    OG.add_option('-c', '--cmd', action='store_const', const=CMD_ACTION, dest='action',
                  help=u"Lancer une commande en lui fournissant les informations nécessaires. La commande ne reçoit QUE les informations concernant les fichiers reçus.")
    OP.add_option_group(OG)
    OG = OptionGroup(OP, "OPTIONS COMMUNES", u"Les options suivantes sont communes à toutes les actions")
    OG.add_option('-f', '--filespec', dest='filespec',
                  help=u"N'accepter que des fichiers dont le nom correspond à l'expression régulière spécifiée. Les autres fichiers sont ignorés, comme s'ils n'avaient même pas été spécifiés.")
    OG.add_option('-t', '--type', dest='filetype',
                  help=u"N'accepter que des fichiers dont le type mime correspond à la valeur spécifiée. Les autres fichiers sont ignorés, comme s'ils n'avaient même pas été spécifiés.")
    OG.add_option('-d', '--destdir', dest='destdir',
                  help=u"Spécifier le répertoire dans lequel enregistrer les fichiers reçus. Ce répertoire est créé si nécessaire.")
    OG.add_option('-R', '--remove-files', action='store_true', dest='clear',
                  help=u"Supprimer les fichiers reçus après avoir lancé la commande. C'est l'option par défaut. Pour information, même si ça n'a à priori aucun intérêt, cette option est honorée avec les actions --shell et --printcsv")
    OG.add_option('-K', '--keep-files', action='store_false', dest='clear',
                  help=u"Ne pas supprimer les fichiers reçus après avoir lancé la commande. Avec les actions --shell et --printcsv, si on prévoit de recevoir des fichiers, cette option devrait être spécifiée.")
    OG.add_option('-V', '--skip-invalids', action='store_true', dest='skip_invalids',
                  help=u"Ne pas mentionner les fichiers reçus invalides")
    OP.add_option_group(OG)
    OG = OptionGroup(OP, "PARAMETRES", u"Les options suivantes concernent le traitement et l'affichage des paramètres normaux (action --shell)")
    OG.add_option('--qvars', dest="qvars", default="QVARS",
                  help=u"Spécifier le nom du tableau qui contiendra la liste des paramètres pour lesquels une valeur est définie. Par défaut, utiliser QVARS")
    OG.add_option('-q', '--query-string', action='store_true', dest='print_qs',
                  help=u"Reconstruire et afficher la valeur de QUERY_STRING en fonction des paramètres fournis dans la requête. Les options -i et -x permettent de sélectionner les paramètres qui y sont inclus.")
    OG.add_option('-p', '--prefix', dest="prefix",
                  help=u"Avec l'option --query-string, ajouter les paramètres supplémentaires spécifiés à QUERY_STRING.")
    OG.add_option('-i', '--include', dest='includes', action='append',
                  help=u"Spécifier les paramètres utilisés pour construire QUERY_STRING. Spécifier plusieurs paramètres en les séparant par des virgules.")
    OG.add_option('-x', '--exclude', dest='excludes', action='append',
                  help=u"Spécifier un paramètre à exclure pour construire QUERY_STRING. Il est possible de spécifier plusieurs paramètres en les séparant par des virgules.")
    OP.add_option_group(OG)
    OG = OptionGroup(OP, "FICHIERS", u"Les options suivantes concernent l'accès aux fichiers reçus (action --cmd)")
    OG.add_option('-G', '--infos', action='store_const', const=INFOS_ARGTYPE, dest='argtype',
                  help=u"La commande est lancée avec N*4 arguments, N étant le nombre de fichiers reçus. Pour chaque fichier, les arguments sont fournis dans l'ordre donné par l'option --printcsv. C'est l'option par défaut.")
    OG.add_option('-F', '--files', action='store_const', const=FILES_ARGTYPE, dest='argtype',
                  help=u"La commande est lancée avec N arguments, N étant le nombre de fichiers reçus. Chaque argument est le chemin vers le fichier reçu, correspondant à la colonne file de l'option --printcsv")
    OG.add_option('-C', '--csv', action='store_const', const=CSV_ARGTYPE, dest='argtype',
                  help=u"La commande est lancée avec en argument un fichier temporaire au format CSV qui liste les paramètres ET les fichiers reçus, contrairement à -G et -F. Cf l'option --printcsv pour le format de ce fichier.")
    OP.add_option_group(OG)
    o, args = OP.parse_args()

    if o.devel and cgitb is None:
        import cgitb; cgitb.enable()

    fm = FileManager(o.destdir, o.filespec, o.filetype)
    if o.action == SHELL_ACTION:
        qvars = []
        if args:
            for nv in args:
                mo = RE_NAME_VALUE.match(nv)
                if mo is None: continue
                fname, defvalue = mo.group(1), mo.group(2)
                if defvalue is None: defvalue = ''
                vname = fname.replace('-', '_')
                if fm.print_param(fname, vname, defvalue, skip_invalids=o.skip_invalids):
                    if vname not in qvars: qvars.append(vname)
        else:
            for fname in fm.form.keys():
                vname = fname.replace('-', '_')
                if fm.print_param(fname, vname, skip_invalids=o.skip_invalids):
                    qvars.append(vname)
        print_array(o.qvars, qvars)
        if o.print_qs:
            qs = build_query_string(fm.form, o.includes, o.excludes, o.prefix)
            print_scalar('QUERY_STRING', qs)

    elif o.action == PRINTCSV_ACTION:
        fm.write_csv(sys.stdout, args or None, skip_invalids=o.skip_invalids)

    elif o.action == CMD_ACTION:
        tmpfile = None
        if not args: args = ['echo']

        if o.argtype == INFOS_ARGTYPE:
            for fi in fm.filist:
                if fi.invalid and o.skip_invalids: continue
                args.extend(fi)
        elif o.argtype == FILES_ARGTYPE:
            for fi in fm.filist:
                if fi.invalid and o.skip_invalids: continue
                args.extend(fi.file)
        elif o.argtype == CSV_ARGTYPE:
            fd, tmpfile = tempfile.mkstemp()
            os.close(fd)
            fm.write_csv(tmpfile, skip_invalids=o.skip_invalids)
            args.append(tmpfile)
        else:
            raise AssertionError("Unknown argtype")

        os.spawnvp(os.P_WAIT, args[0], args)

        if tmpfile is not None:
            try: os.remove(tmpfile)
            except: pass

    if o.clear:
        for fi in fm.filist:
            try: os.remove(fi.file)
            except: pass

    return fm.exitcode()

if __name__ == '__main__':
    ec = run_cgiparams()
    sys.exit(ec)
