nutools/lib/ulib/support/cgiparams.py

370 lines
17 KiB
Python
Raw Normal View History

2021-02-24 10:55:56 +04:00
#!/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)