370 lines
17 KiB
Python
Executable File
370 lines
17 KiB
Python
Executable File
#!/usr/bin/env python2
|
|
# -*- 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)
|