# -*- coding: utf-8 -*-

##########################################################################
#
#                       Gestion de la securite
#
##########################################################################
#    WxGeometrie
#    Dynamic geometry, graph plotter, and more for french mathematic teachers.
#    Copyright (C) 2005-2013  Nicolas Pourcelot
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

import keyword, re, types
import math # pour avoir droit aux fonctions mathematiques dans eval_restricted par defaut.
import random as module_random# idem

from ..pylib import advanced_split
from .. import param


def _avertissement(*arg, **kw):
    print("Instruction interdite en mode securisé.")

#fonctions_interdites = ["eval", "compile", "execfile", "file", "open", "write", "getattr", "setattr"]

liste_blanche = set(('False', 'None', 'True', 'abs', 'all', 'any', 'bool',  'callable', 'chr', 'close', \
            'coerce', 'complex', 'dict', 'divmod', 'enumerate', 'filter', 'float', 'frozenset', 'globals', 'hash', 'hex', \
            'id', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'list', 'locals', 'long', 'map', 'max', 'min', 'object', 'oct', \
            'ord', 'pow', 'range', 'repr', 'reversed', 'round', 'set', 'slice', 'sorted', 'str', 'sum', \
            'tuple', 'type', 'zip'\
            'IndexError', 'SyntaxError', 'NameError', 'StandardError', 'UnicodeDecodeError', 'RuntimeWarning', \
            'Warning', 'FloatingPointError', 'FutureWarning', 'ImportWarning', 'TypeError', 'KeyboardInterrupt', \
            'UserWarning', 'SystemError', 'BaseException', 'RuntimeError', 'GeneratorExit', 'StopIteration', \
            'LookupError', 'UnicodeError', 'IndentationError', 'Exception', 'UnicodeTranslateError', 'UnicodeEncodeError', \
            'PendingDeprecationWarning', 'ArithmeticError', 'MemoryError', 'ImportError', 'KeyError', 'SyntaxWarning', \
            'EnvironmentError', 'OSError', 'DeprecationWarning', 'UnicodeWarning', 'ValueError', 'NotImplemented', \
            'TabError', 'ZeroDivisionError', 'ReferenceError', 'AssertionError', 'UnboundLocalError', 'NotImplementedError', \
            'AttributeError', 'OverflowError', 'WindowsError'))

liste_noire = set(__builtins__.keys())
liste_noire.difference_update(liste_blanche)

dictionnaire_builtins = {}.fromkeys(list(liste_noire), _avertissement)
dictionnaire_modules = {}.fromkeys([key for key, objet in globals().items() if type(objet) == types.ModuleType])

ajouts_math = {"pi": math.pi, "e": math.e, "i": 1j}
for key, obj in math.__dict__.items():
    if type(obj) == types.BuiltinFunctionType:
        ajouts_math[key] = obj

for key, obj in module_random.__dict__.items():
    if type(obj) == types.BuiltinFunctionType:
        ajouts_math[key] = obj

##keywords = ('and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'with', 'yield')

### mots clés qui peuvent être utilisés
##keywords_autorises = set('and', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'if', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'with', 'yield')

keywords = set(keyword.kwlist)

# mots clés dont il faut s'assurer qu'ils ne puissent PAS être utilisés
keywords_interdits = set(('exec', 'global', 'import')) # attention à global !

# mots clés qui peuvent être utilisés
keywords_autorises = keywords.difference(keywords_interdits)

### ici, il s'agit de mots clés qui doivent rester en début de ligne
### (plus précisément, il est impossible de rajouter devant une affectation, style 'variable = ').
##keywords_debut_ligne = ('break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'pass', 'for', 'if', 'print', 'raise', 'return', 'try', 'while', 'with', 'yield')

# Mots clés devant lesquels il est possible de rajouter une affectation, style 'variable = ...'.
keywords_affectables = set(('lambda', 'not'))
# Mots clés qui ne sont jamais en début de ligne.
keywords_milieu = set(('or', 'and', 'as', 'in'))
# Les autres, qui doivent rester en tête de ligne.
keywords_non_affectables = keywords.difference(keywords_affectables).difference(keywords_milieu)

def keywords_interdits_presents(chaine):
    return bool(re.search(r'(?<!\w)(' + '|'.join(keywords_interdits) + ')(?!\w)', chaine))

def expression_affectable(chaine):
    return not bool(re.match('[ ]*(' + '|'.join(keywords_non_affectables) + ')(?!\w)', chaine))


# Exemple d'usage :
# dico = {"pi":3.1415926535897931, "e":2.7182818284590451}    # dictionnaire local
# dico.update(securite.dictionnaire_builtins)
# exec(string, dico)

def eval_restricted(s, dico_perso = None):
    """eval_restricted(s) évalue s dans un contexte vierge et légèrement sécurisé.

    Toutes les fonctions disponibles par défaut sont filtrées.

    Note: eval_restricted est le plus securisée possible, mais contrairement
    à eval_safe, il utilise la fonction eval ; il existe donc forcément des
    failles de sécurité.
    """
    dico = {"rand": module_random.random}
    dico.update(dictionnaire_builtins) # supprime certaines fonctions par defaut (en les redefinissant)
    dico.update(ajouts_math) # ajoute les fonctions mathematiques usuelles
    if dico_perso is not None:
        dico.update(dico_perso)
    for module in ("os", "sys", "securite"):
        dico.pop(module, None)
    return eval(s, dico)


def eval_safe(s):
    """eval_safe(repr(x)) retourne x pour les types les plus usuels
    (int, str, float, bool, None, list, tuple, dict.)
    Mais aucune évaluation n'est faite, ce qui évite d'éxécuter un code dangereux.
    Le type de s est detecté, et la transformation appropriée appliquée.

    NB1: eval_safe n'est pas destiné à remplacer eval :
    - il est relativement lent,
    - le nombre de types supportés est volontairement réduit,
    - il n'est pas souple (eval_safe("2-3") renvoie une erreur).
    La fonction eval_safe est orientée uniquement securité.

    NB2: eval_safe est récursif (il peut lire des listes de tuples de ...).

    NB3: eval_safe est parfaitement securisé, car il ne fait (presque) jamais appel à une instruction eval.
    Contrairement à la fonction eval_restricted, qui est 'légèrement' securisée."""

    s = s.strip()

    def filtre(chaine, caracteres):
        return re.match("^[%s]*$" %caracteres, chaine) is not None



    if filtre(s, "-0-9") or s[-1] == "L":   # entier
        return int(s)

    if filtre(s, "-+.e0-9"):    # flottant
        return float(s)

    if len(s) > 1 and s[0] == s[-1] and s[0] in ("'", '"'):    # chaine
        # s est évalué dans un contexte parfaitement vierge.
        return eval(s, {"__builtins__": {}}, {"__builtins__": {}})

    if len(s) > 2 and s[0] == "u" and s[1] == s[-1] and s[1] in ("'", '"'): #unicode
        return eval(s, {"__builtins__": {}}, {"__builtins__": {}}) # idem

    if s == "None":
        return None

    if s == "True":
        # Eviter return eval(s), car True peut etre redefini. Exemple : True = False (!)
        return True

    if s == "False":
        return False

    if s[0] in ("(", "["):  # tuple ou liste
        liste = [eval_safe(t) for t in advanced_split(s[1:-1], ",") if t]
        if s[0] == "(":
            liste = tuple(liste)
        return liste

    if s[0] == "{":     # dictionnaire
        dict = {}
        liste = [advanced_split(t, ":") for t in advanced_split(s[1:-1], ",") if t]
        for key, val in liste:
            dict[eval_safe(key)] = eval_safe(val)
        return dict


    raise TypeError("%s: types int, str, float, bool, ou None requis (ou liste ou tuple de ces types)" % repr(s))
