#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-

"""
Définitions des classes de base du système de gestion des machines
et adhérents du crans

Copyright (C) Frédéric Pauget
Licence : GPLv2
"""

from socket import gethostname
import socket
import smtplib
import re
import os
import random
import string
import time
import datetime
import sys
import pwd
import errno
import ldap
import ldap.modlist
import ldap_passwd
import netaddr
import traceback
import subprocess
import annuaires_pg as annuaires
import config
import config.impression
import config.cotisation as cotisation
import iptools
import ip6tools
import cPickle
import config_mail
from chgpass import change_password

from calendar import monthrange
from affich_tools import coul, prompt, cprint
from email_tools import send_email
from syslog import openlog, closelog, syslog
from unicodedata import normalize
import secrets_new as secrets
import ridtools
from user_tests import isadm
import getpass

cur_user = os.getenv("SUDO_USER") or os.getenv("USER") or os.getenv("LOGNAME") or getpass.getuser()

date_format = '%d/%m/%Y %H:%M'
date_format_new = '%d/%m/%Y %H:%M:%S'
hostname = gethostname().split(".")[0]
smtpserv = "smtp.crans.org"
random.seed() # On initialise le générateur aléatoire


##################################################################################
###  paramètres de connexion à la base LDAP

test_hosts = tuple()

if os.getenv('DBG_LDAP', False):
    # Utilisation de la base de données de test (tests, séminaire...)
    # Il faut au choix :
    #     - faire un import crans_ldap_test
    #  ou - crans_ldap=test /le/script
    host = os.getenv('DBG_LDAP')
    if host == '1':
        if hostname != "vo":
            raise ImportError("La base de test n'est accessible que depuis vo !")
        host = 'localhost'

    uri = ro_uri = 'ldap://%s/' % host
    ldap_auth_dn = 'cn=admin,dc=crans,dc=org'
    ldap_password = '75bdb64f32'

elif cur_user == 'freerad':
    # Freeradius n'a pas accès au secret, donc accès que en local
    uri = ''
    ro_uri = 'ldapi://%2fvar%2frun%2fslapd%2fldapi/'
    ldap_auth_dn = ldap_password = ''

else:
    # pour les autres on utilise ldap.adm.crans.org en rw
    uri = 'ldap://ldap.adm.crans.org/'

    # avec le secret
    try:
        ldap_auth_dn = secrets.get('ldap_auth_dn')
        ldap_password = secrets.get('ldap_password')
    except:
        sys.stdout.write(coul('Warning : impossible de lire le fichier de secret !\n', 'jaune'))
        sys.stdout.write("\n".join(traceback.format_stack()[:-1]) + '\n')
        sys.exit(1)

    # uri pour les instances de CransLdap faisant de la lecture seule
    if os.path.exists('/var/run/ldapi'):
        ro_uri = 'ldapi://%2fvar%2frun%2fldapi/'
    else:
        ro_uri = uri

##################################################################################
### Items de la blackliste
blacklist_items = config.blacklist_items

##################################################################################
### Droits possibles
droits_possibles = [u'Multimachines', u'Cableur', u'Imprimeur', u'Apprenti',
                    u'Webmaster', u'Moderateur', u'Webradio',
                    u'Nounou', u'Tresorier', u'Bureau']

##################################################################################
### Droits critiques, ie que seules les nounous peuvent attribuer
droits_critiques = [u'Nounou', u'Apprenti', u'Webradio']

##################################################################################
### Droits vieux, ie qui permettent d'avoir des droits meme quand on ne
### cotise plus
droits_vieux = [u'Nounou', u'Bureau']

##################################################################################
### Variables internes diverses
#isadm = user_tests.isadm()
#isdeconnecteur = user_tests.isdeconnecteur()
ann_scol = config.ann_scol
#script_utilisateur = user_tests.getuser()
script_utilisateur = cur_user

##################################################################################
### Fonctions utiles
def decode(s):
    """
    Retourne un unicode à partir de s
    s doit être en utf-8
    """

    if type(s) is unicode:
        # Si s est déjà un unicode, on ne décode pas
        return s
    else:
        return s.decode('utf-8', 'ignore') # On ignore les erreurs

def tz(thetz):
    abstz = 100*abs(thetz)
    if thetz == 0:
        return "Z"
    else:
        return "%s%04d" % ("+"*(thetz < 0) + "-"*(thetz > 0), abstz)

def generalizedTimeFormat(stamp):
    """Converts a timestamp (local) in a generalized time format
    for LDAP

    """

    return "%s%s" % (time.strftime("%Y%m%d%H%M%S", time.localtime(stamp)), tz(time.altzone/3600))

def fromGeneralizedTimeFormat(gtf):
    """Converts a GTF stamp to unix timestamp

    """
    return time.mktime(time.strptime(gtf.split("-", 1)[0].split("+", 1)[0].split('Z', 1)[0], "%Y%m%d%H%M%S"))

def strip_accents(a, sois_un_porc_avec_les_espaces = True):
    """ Supression des accents de la chaîne fournie """
    res = normalize('NFKD', decode(a)).encode('ASCII', 'ignore')
    if sois_un_porc_avec_les_espaces:
        return res.replace(' ', '_').replace("'", '')
    else:
        return res

def mailexist(mail):
    """
    Vérifie si une adresse mail existe ou non grace à la commande vrfy
    du serveur mail
    """

    mail = mail.split('@', 1)[0]

    try:
        s = smtplib.SMTP(smtpserv)
        s.putcmd("vrfy", mail)
        r = s.getreply()[0] in [250, 252]
        s.close()
    except:
        raise ValueError(u'Serveur de mail injoignable')

    return r

def preattr(val):
    """
    val est :
        * un entier
        * une chaîne
        * une liste avec un seul entier ou une seule chaîne

    Retourne [ len(str(val).strip), str(val).strip en utf-8 ]
    """

    if isinstance(val, list) and len(val) == 1:
        return preattr(val[0])

    elif isinstance(val, str) or isinstance(val, int):
        val = str(val).strip()
        # On passe tout en utf-8 pour ne pas avoir de problèmes
        # d'accents dans la base
        return [len(val), val]
    elif isinstance(val, unicode):
        val = val.strip()
        return [len(val), val.encode('utf-8')]
    else:
        raise TypeError(type(val))

def is_actif(sanction):
    """
    Retourne True ou False suivant si la sanction fournie (chaîne
    venant de blacklist) est active ou non
    """
    bl = sanction.split('$')
    now = time.time()
    debut = int(bl[0])
    if bl[1] == '-':
        fin = now + 1
    else:
        fin = int(bl[1])
    return debut < now and fin > now

def log(contenu):
    with open('/tmp/crans.log', 'a') as f:
        f.writelines(contenu)
        f.writelines('\n')

def format_mac(mac):
    """
    Formatage des adresses mac
    Transforme une adresse pour obtenir la forme xx:xx:xx:xx:xx:xx
    Le séparateur original peut être :, - ou rien
    Retourne la mac formatée.
    """
    mac = mac.strip()
    if mac == '<automatique>':
        return mac
    l, mac = preattr(mac)
    mac = mac.strip().replace(' ','').replace("-", ":")
    if mac.count(":") == 5:
        # On a une adresse de la forme 0:01:02:18:d1:90
        # On va compléter s'il manque des 0
        mac = ":".join(map(lambda x: x.replace(' ', '0'),
                           map(lambda x: "%02s" % x, mac.split(":"))))
    mac = mac.replace(':', '').lower()
    if len(mac) != 12:
        raise ValueError(u"Longueur de l'adresse mac incorrecte.")
    for c in mac[:]:
        if c not in string.hexdigits:
            raise ValueError(u"Caractère interdit '%s' dans l'adresse mac." % c)
    if mac == '000000000000':
        raise ValueError(u"MAC nulle interdite\nIl doit être possible de modifier l'adresse de la carte.")

    # Formatage
    mac = "%s:%s:%s:%s:%s:%s" % (mac[:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:])

    return mac

def format_gpg_fingerprint(fpr):
    """
    Formatage de fingerpring GPG pour les avoir de la forme
    XXXX XXXX XXXX XXXX XXXX  XXXX XXXX XXXX XXXX XXXX
    """

    # longueur + fpr sans espaces
    l, fpr = preattr(fpr)

    fpr = fpr.replace(':', '').replace('-', '').replace(' ', '')

    if len(fpr) != 40:
        raise ValueError(u"Longueur de la fingerprint GPG incorrecte, merci de respecter le format")

    for c in fpr:
        if c not in string.hexdigits:
            raise ValueError(u"Une fingerpring GPG est exclusivement composée de caractères hexadécimaux, donc il faut virer %s." % c)

    formattedfpr = ""
    for i in range(0,10):
        formattedfpr += fpr[i*4:(i+1)*4]

        if i != 10:
            formattedfpr += " "
        if i == 4:
            formattedfpr += " "

    return formattedfpr

def validate_mail(mail, crans_allowed=False):
    """Valide une adresse e-mail"""
    l, mail = preattr(mail)
    mail = mail.lower()

    #Emplacement de l'@
    a = mail.find('@')
    #Emplacement du . final
    b = mail.rfind('.')

    # Les tests :
        # exactement un @
        # 2 à 4 caractères après le . final
        # @ pas en premier ni juste avant le dernier .
    if mail.count('@') != 1 \
       or not ( l-b >= 2 and l-b <= 6) \
       or a < 1 or b-a < 2:
           raise ValueError(u"Adresse mail incorrecte.")

    # Pas de caractèrs bizarres
    for l in mail[:]:
        if not l in (string.lowercase + string.digits + '+-_.@'):
            raise ValueError(u"Caractère interdit dans l'adresse mail (%s)." % l)

    # Pour les vicieux
    if not crans_allowed and mail.endswith(('@crans.org','@crans.ens-cachan.fr')):
        raise ValueError(u"Adresse mail @crans interdite ici !")

    # On enlève les blacklistes mail_invalide
    return mail

##################################################################################
### Définition des classes

class Service:
    """ Définit un service à redémarrer """
    def __init__(self, nom, args=[], start=[]):
        """
        Nom du service
        Liste des arguments
        Liste d'horaires de démarrages
        """

        self.nom = nom
        self.args = args
        self.start = map(int, start)

    def __unicode__(self):
        starting = self.start
        starting.sort()
        dates = u' et '.join(map(lambda t: t < time.time() and \
                                u"maintenant" or time.strftime(date_format,
                                                              time.localtime(t)),
                                self.start))
        dates = u" à partir d%s %s" % (dates.startswith(u"maintenant") and u"e" or u"u",
                                      dates)
        return (u"%s(%s)%s" % (self.nom,
                              u','.join([i.decode("UTF-8") for i in self.args]),
                              dates)).replace(u" et maintenant", u"")

    def __str__(self):
        return self.__unicode__().encode("utf-8", "ignore")

class CransLdap:
    """
    Classe de connexion à la base LDAP du crans.
    """
    conn = None
    base_dn = 'ou=data,dc=crans,dc=org'
    base_lock = 'ou=lock,dc=crans,dc=org'
    base_services = 'ou=services,dc=crans,dc=org'

    ### Configuration de la recheche
    # Dictionnaire de transformation des champs
    trans = { 'prénom': 'prenom',
              'chambre': 'chbre',
              'login': 'uid',
              'hostname': 'host',
              'mac': 'macAddress',
              'ip': 'ipHostNumber',
              'telephone': 'tel',
              'position': 'positionBorne' }

    # Les différentes classes LDAP de machines
    ldap_machines_classes = ['machineFixe', 'machineWifi', 'machineCrans', 'borneWifi']

    # Champs de recherche pour la recherche automatique
    auto_search_machines_champs = \
      ['macAddress', 'host', 'ipHostNumber', 'ip6HostNumber', 'hostAlias']

    auto_search_champs = { \
      'adherent': \
        ['nom', 'prenom', 'tel', 'mail', 'chbre', 'mailAlias', 'canonicalAlias', 'mailExt', 'uid'],
      'club': ['nom', 'chbre', 'uid'],
      'machineFixe': auto_search_machines_champs,
      'machineWifi': auto_search_machines_champs,
      'machineCrans': auto_search_machines_champs,
      'borneWifi': auto_search_machines_champs,
      'facture' : [] }

    # Champs de recherche pour la recherche manuelle (en plus de la recherche auto)
    non_auto_search_machines_champs = \
      ['mid', 'historique', 'blacklist', 'info', 'exempt', 'mblacklist',
       'portTCPin', 'portTCPout', 'portUDPin', 'portUDPout', 'dnsIpv6', 'machineAlias',
       'sshFingerprint', 'rid']

    non_auto_search_champs = { \
      'adherent': \
        ['etudes', 'paiement', 'carteEtudiant', 'aid', 'postalAddress',
         'historique', 'blacklist', 'droits', 'uidNumber', 'info',
         'solde', 'controle', 'contourneGreylist', 'rewriteMailHeaders',
         'ablacklist', 'homepageAlias', 'charteMA',
         'adherentPayant', 'gpgFingerprint', 'debutConnexion', 'finConnexion',
         'debutAdhesion', 'finAdhesion'], \
      'club': \
        ['cid', 'responsable', 'paiement', 'historique', 'blacklist',
         'mailAlias', 'info', 'controle', 'ablacklist', 'imprimeurClub'], \
      'machineFixe': non_auto_search_machines_champs,
      'machineCrans': non_auto_search_machines_champs + ['prise'],
      'borneWifi': non_auto_search_machines_champs + \
        ['prise', 'puissance', 'canal', 'hotspot', 'positionBorne', 'nvram'],
      'machineWifi': non_auto_search_machines_champs + ['ipsec'],
      'facture': ['fid', 'debutConnexion', 'finConnexion', 'debutAdhesion', 'finAdhesion']}

    # tous les champs de recherche
    search_champs = {}
    for i in auto_search_champs.keys():
        search_champs[i] = auto_search_champs[i] + non_auto_search_champs[i]
    del i

    # Profondeur des différentes recherches (scope)
    scope = { 'adherent': 1,
              'club': 1,
              'machineFixe': 2,
              'machineWifi': 2,
              'machineCrans': 2,
              'borneWifi': 2,
              'facture': 2}

    def __init__(self, readonly=False):
        self.cur_user = cur_user
        self.connect(readonly)

    def __del__(self):
        # Destruction des locks résiduels
        if hasattr(self, '_locks'):
            for lock in self._locks:
                self.remove_lock(lock)

    def connect(self, readonly=False):
        """ Initialisation la connexion vers le serveur LDAP """
        if readonly:
            self.conn = ldap.initialize(ro_uri)
            return

        self.conn = ldap.initialize(uri)
        nbessais = 0
        while True:
            try:
                self.conn.bind_s(ldap_auth_dn, ldap_password, ldap.AUTH_SIMPLE)
                break
            except ldap.SERVER_DOWN:
                nbessais += 1
                if nbessais > 2:
                    sys.stderr.write("ERREUR : serveur LDAP injoignable\n")
                    sys.exit(1)
                else:
                    time.sleep(0.3)

    def exist(self, arg):
        """
        Vérifie l'existence d'une entrée dans la base et que cette
        entrée n'appartient pas à l'objet en cours.

        La méthode prend en compte les locks.

        arg doit être une expression de recherche ldap

        Si il y a existence, on retourne la liste des dn correspondants
        Sinon retourne une liste vide

        Exemple : exist('chbre=Z345') vérifie si il y a un adhérent en Z345
        """
        r = []

        # Premier test: dans les objets déjà inscrits
        ret = self.conn.search_s(self.base_dn, 2, arg)
        for res in ret:
            # C'est peut être l'objet courant
            try: # Si ce n'est pas une classe fille avec l'attribut dn => erreur
                if res[0] == self.dn:
                    continue
            except:
                pass
            r.append(res[0])

        # Deuxième test : lock ?
        ret = self.conn.search_s(self.base_lock, 1, arg)
        lockid = '%s-%s' % (hostname, os.getpid())
        for res in ret:
            # Lock encore actif ?
            l = res[1]['lockid'][0]
            if l == lockid: continue
            # C'est locké par un autre process que le notre
            # il tourne encore ?
            try:
                if l.split('-')[0] == hostname and os.system('ps %s > /dev/null 2>&1' % l.split('-')[1]):
                    # Il ne tourne plus
                    self.remove_lock(res[0]) # delock
                    continue
            except:
                pass
            r.append(res[0])

        return r

    def lock(self, item, valeur):
        """
        Lock un item avec la valeur valeur, les items possibles
        peuvent être :

            aid $ chbre $ mail $ mailAlias $ canonicalAlias $
            mid $ rid $ macAddress $ host $ hostAlias $ ipHostNumber $
            ip6HostNumber $ gpgFingerprint

        Retourne le dn du lock
        """

        valeur = valeur.encode('utf-8')

        if not valeur:
            # On le lock pas un truc vide
            return True

        lock_dn = '%s=%s,%s' % (item, valeur, self.base_lock)
        lockid = '%s-%s-%s' % (hostname, os.getpid(), time.time())
        modlist = ldap.modlist.addModlist({ 'objectClass': 'lock',
                                            'lockid': lockid,
                                            item: valeur })

        try:
            self.conn.add_s(lock_dn, modlist)
        except ldap.ALREADY_EXISTS:
            # Pas de chance, le lock est déja pris
            try:
                res = self.conn.search_s(lock_dn, 2, 'objectClass=lock')[0]
                l = res[1]['lockid'][0]
            except: l = '%s-1' % hostname
            if l != lockid:
                # C'est locké par un autre process que le notre
                # il tourne encore ?
                if (l.split('-')[0] == hostname and os.system('ps %s > /dev/null 2>&1' % l.split('-')[1] )) or time.time() - 600 >= l.split('-')[2]:
                    # Il ne tourne plus
                    self.remove_lock(res[0]) # delock
                    return self.lock(item, valeur)   # relock
                raise EnvironmentError(u'Objet (%s=%s) locké, patienter.' % (item, valeur), l)
        else:
            if not hasattr(self, '_locks'):
                self._locks = [lock_dn]
            else:
                self._locks.append(lock_dn)

    def remove_lock(self, lockdn):
        """
        Destruction d'un lock
        Destruction de tous les locks si lockdn=*
        """
        # Ne tente de dertuire le lock que si l'on est connecté à la base
        if self.conn:
            # Mettre des verifs ?
            if lockdn != '*':
                self.conn.delete_s(lockdn)
                try:
                    self._locks.remove(lockdn)
                except:
                    # Pas grave si ca foire, le lock n'y est plus
                    pass
            else:
                locks = self.list_locks()
                for l in locks:
                    self.conn.delete_s(l[0])

    def list_locks(self):
        """ Liste les locks """
        return self.conn.search_s(self.base_lock, 1, 'objectClass=lock')

    def services_to_restart(self, new=None, args=[], start=0):
        """
        start indique la date (en secondes depuis epoch) à partir du
        moment où cette action sera effectuée.

        Si new = None retourne la liste des services à redémarrer.

        Si new est fourni, mais ne commence pas par '-', on ajoute
        le service à la liste avec les arguments args (args doit être
        une liste).

        Si new commence par '-', on supprime le service si son start
        est dans le futur.

        Si new commence par '--', on supprime le service sans condition.
        """
        if new: new = preattr(new)[1]

        # Quels services sont déjà à redémarrer ?
        serv = {}       # { service: [ arguments ] }
        serv_dates = {} # { service: [ dates de restart ] }
        services = []
        for s in self.conn.search_s(self.base_services, 1, 'objectClass=service'):
            s = s[1]
            serv[s['cn'][0]] = s.get('args', [])
            serv_dates[s['cn'][0]] = s.get('start', [])
            services.append(Service(s['cn'][0], s.get('args', []), s.get('start', [])))

        # Retourne la liste des services à redémarrer
        if not new: return services

        # Effacement d'un service
        if new[0] == '-':
            if new[1] == '-':
                # Double -- on enlève quelque soit la date
                remove_dn = 'cn=%s,%s' % (new[2:], self.base_services)
            else:
                # On enlève uniquement si la date est passée
                remove_dn = 'cn=%s,%s' % (new[1:], self.base_services)
                if not serv.has_key(new[1:]):
                    # Existe pas => rien à faire
                    return
                keep_date = []
                for date in serv_dates[new[1:]]:
                    if time.time() < int(date):
                        keep_date.append(date)
                if keep_date:
                    mods = [{'start': serv_dates[new[1:]]}, { 'start': keep_date }]
                    self.conn.modify_s(remove_dn, ldap.modlist.modifyModlist(*mods))
                    remove_dn = None

            if remove_dn:
                # Suppression
                try: self.conn.delete_s(remove_dn)
                except: pass
                # Si n'existe pas => Erreur mais le résultat est là.
            return

        serv_dn = 'cn=%s,%s' % (new, self.base_services)

        # Conversion avant stockage dans la base
        if isinstance(args, basestring):
            args = [args]
        args = map(lambda x:preattr(x)[1], args)
        try:
            start = [int(start)]
        except TypeError:
            pass
        start = map(lambda x:preattr(x)[1], start)

        if new in serv.keys():
            modlist = []

            new_args = []
            # Nouveaux arguments ?
            for arg in args:
                if arg not in serv[new]:
                    new_args.append(arg)
            if new_args:
                modlist += ldap.modlist.modifyModlist({'args': serv[new]},
                                                      {'args': serv[new] + new_args})

            new_date = []
            # Nouvelle date ?
            for date in start:
                if date not in serv_dates[new]:
                    new_date.append(date)
            if new_date:
                modlist += ldap.modlist.modifyModlist({'start': serv_dates[new]},
                                                      {'start': serv_dates[new] + new_date})

            if modlist:
                try:
                    self.conn.modify_s(serv_dn, modlist)
                except ldap.TYPE_OR_VALUE_EXISTS:
                    # Pas grave
                    pass
            # else rien à faire
        else:
            # Entrée non présente -> ajout
            modlist = ldap.modlist.addModlist({ 'objectClass': 'service',
                                                'cn':  new,
                                                'args': args,
                                                'start': start })
            try:
                self.conn.add_s(serv_dn, modlist)
            except ldap.ALREADY_EXISTS:
                # Existe déja => rien à faire
                pass


    def make(self, entry, mode=''):
        """
        Crée le bon objet à partir de entry.
        mode a la même signification que dans search.
        """
        # On récupère la bonne classe
        nom_classe = (entry[1].get('objectClass') or ['none'])[0]
        nom_classe = nom_classe[0].upper() + nom_classe[1:]
        try:
            classe = eval(nom_classe)
            # On crée l'objet
            return classe(entry, mode, self.conn)
        except:
            raise ValueError(u"Impossible de créer l'objet %s" % nom_classe)

    def search(self, expression, mode=''):
        """
        Recherche dans la base LDAP, expression est une chaîne :
          * soit une expression : champ1=expr1&champ2=expr2&champ3!=expr3...
          * soit un seul terme, dans ce cas cherche sur les champs de
            auto_search_champs
        Si mode='w', les instances crées seront en mode d'écriture
        """

        if isinstance(expression, str):
            pass
        elif isinstance(expression, unicode):
            expression = expression.encode('utf-8')
        else:
            raise TypeError(u'Chaîne attendue')

        if os.getuid() != 0:
            openlog('CransLdap.search')
            syslog('(%s,%s) %s' % (script_utilisateur, mode, expression))
            closelog()

        if not expression:
            return []

        # On échappe les caractères spéciaux
        expression = expression.replace('\\', '\\\\').replace('(', '\\(').replace(')', '\\)')

        # Il faut un filtre par type d'objet de la base
        filtres = self.auto_search_champs.keys()
        result = {}
        for i in filtres:
            result[i] = []

        # Fonction utile
        def build_filtre(champ, expr, neg=False):
            """
            Retourne une chaine pour recherche dans la base LDAP
            du style (champ=expr) en adaptant les valeurs de expr au champ.
            Si neg = True, retourne le négatif : (!(champ=expr))
            """
            el = ''
            if champ in ['host', 'hostAlias']:
                if expr[-1] == '*':
                    el = '(%s=%s)' % (champ, expr)
                elif '.' not in expr:
                    el = '(%s=%s.*)' % (champ, expr)
                else:
                    el = '(%s=%s*)' % (champ, expr)
            elif champ == 'macAddress':
                # Formatage adresse mac
                try:
                    el = '(macAddress=%s)' % format_mac(expr)
                except:
                    pass
            elif champ == 'paiement' and expr == 'ok':
                # Paiement donnant droit à une connexion maintenant ?
                # (il doit avoir payé pour l'année en cours ou pour
                # l'année précédente si on est en septembre
                #
                # Dans tous les cas, pour un adhérent, le paiement est
                # considéré non ok s'il n'a pas fourni sa carte d'etudiant
                # alors que l'on est desormais en periode de bloquage
                # définifif (cf config.py).
                if config.periode_transitoire:
                    # Pour la période transitoire année précédente ok
                    el = "(|(paiement=%d)(paiement=%d)(finAdhesion>=%s))" % (config.ann_scol, config.ann_scol-1, generalizedTimeFormat(time.time()))
                else:
                    el = "(|(paiement=%s)(finAdhesion>=%s))" % (config.ann_scol, generalizedTimeFormat(time.time()))
                    # Doit-on bloquer en cas de manque de la carte d'etudiant ?
                    # (si période transitoire on ne bloque dans aucun cas)
            elif champ[1:] == 'blacklist':
                el = '(blacklist=%s)' % expr
            else:
                # Cas général
                el = '(%s=%s)' % (champ, expr)
            if neg: el = '(!%s)' % el
            return el

        if '=' in expression:
            #### Recherche avec conditions explicites
            ## Construction des filtres

            # Initialisation
            filtre = {}
            filtre_seul = {}
            # filtre_seul[i] est True si la requête porte sur un champ
            # qui n'est pas dans toutes les machines, mais dans i
            ignore_filtre =[] # liste des filtres à ignorer
            for i in filtres:
                filtre[i] = ''
                filtre_seul[i] = False

            conds = expression.split('&')

            # Test de l'expression de recherche et classement par filtres
            for cond in conds:
                neg = False
                try:
                    champ, expr = cond.strip().split('=')
                    if champ[-1] == '!':
                        # Négation pour ce champ
                        champ = champ[:-1]
                        neg = True
                except:
                    raise ValueError(u'Syntaxe de recherche invalide (%s)' % cond)

                # Transformation de certains champs
                champ = self.trans.get(champ, champ)

                if expr == '': expr = '*'

                ok = False

                # si c'est un champ uniquement adherent (genre droit), on ignore les clubs et vice versa
                if champ in self.search_champs['club'] and champ not in self.search_champs['adherent']:
                    if 'adherent' not in ignore_filtre: ignore_filtre.append('adherent')
                if champ in self.search_champs['adherent'] and champ not in self.search_champs['club']:
                    if 'club' not in ignore_filtre: ignore_filtre.append('club')

                # Construction du filtre
                for i in filtres:
                    if champ in self.search_champs[i]:
                        filtre[i] += build_filtre(champ, expr, neg)
                        ok = True
                        if champ not in self.auto_search_machines_champs \
                          and champ not in self.non_auto_search_machines_champs:
                            filtre_seul[i] = True

                if not ok:
                    raise ValueError(u'Champ de recherche inconnu (%s)' % champ)

            ## Recherche avec chacun des filtres
            r = {} # contiendra les réponses par filtre

            # On efface les bons filtres si la requête porte sur des
            # champs spéciaux
            ok = False
            for i in self.ldap_machines_classes:
                ok = ok or filtre_seul[i]
            if ok:
                # Il y a au moins un champ spécial
                for i in self.ldap_machines_classes:
                    if not filtre_seul[i]:
                        filtre[i] = ''

            for i in filtres:
                if i in ignore_filtre or filtre[i] == '':
                    # Filtre vide ou à ignorer
                    filtre[i] = ''
                    r[i] = None
                else:
                    # Filtre valide
                    filtre[i] = '(&(objectClass=%s)%s)' % (i, filtre[i])
                    r[i] = self.conn.search_s(self.base_dn, self.scope[i], filtre[i])

            ## On a alors une liste de résultats
            ## r = {categorie1: [(result1), (result2), ...], ...}

            # Traitement

            if (r['machineFixe'] != None or r['machineWifi'] != None) \
               and (r['adherent'] != None or r['club'] != None) \
               and len(conds) > 1:

                # On renvoie toutes les machineCrans et borneWifi
                for i in 'machineCrans', 'borneWifi':
                    if r[i] == None:
                        continue
                    for res in r[i]:
                        result[i].append(self.make(res, mode))

                # On croise maintenant les résultats machine et propriétaire

                # Traitement des machines
                mach_adh = [] # liste de dn d'adhérents et de clubs
                for res in r['machineFixe'] + r['machineWifi']:
                    dn = ','.join(res[0].split(',')[-4:])
                    if dn[:3] != 'aid' and dn[:3] != 'cid':
                        continue
                    if dn not in mach_adh:
                        mach_adh.append(dn)

                # Croisement
                bons_dn = [] # liste des dn d'adhérents qui correspondent aux critères
                for i in 'adherent', 'club':
                    if r[i] == None:
                        continue
                    for a in r[i]:
                        if a[0] in mach_adh and not a[0] in bons_dn:
                            bons_dn.append(a[0])
                            result[i].append(self.make(a, mode))

                # Maintenant c'est au tour des bonnes machines
                bons_dn2 = []
                for i in 'machineFixe', 'machineWifi':
                    if r[i] == None:
                        continue
                    for a in r[i]:
                        dn = ','.join(a[0].split(',')[-4:])
                        if dn in bons_dn and not a[0] in bons_dn2:
                            bons_dn2.append(dn)
                            result[i].append(self.make(a, mode))

            else:
                # On retourne tout
                for i in filtres:
                    if r[i] == None:
                        continue
                    for res in r[i]:
                        result[i].append(self.make(res, mode))

        else:
            ### Recherche d'une chaine sur tous les champs
            for i in filtres:
                # Construction du filtre
                filtre = ''
                for champ in self.auto_search_champs[i]:
                    filtre += build_filtre(champ, expression)
                filtre = '(&(|%s)(objectClass=%s))' % (filtre, i)

                # Recherche
                for r in self.conn.search_s(self.base_dn, self.scope[i], filtre):
                    result[i].append(self.make(r, mode))

        # Backward-compatibilité
        result['machine'] = []
        for i in self.ldap_machines_classes:
            result['machine'] += result[i]

        return result

    def getProprio(self, uid, mode=''):
        """
        Renvoie un proprietaire ou un adherent correspondant au
        login/ mot de passe (mot de passe facultatif)
        """
        recherche = self.search(("uid=%s" % uid), mode)
        proprio = None

        if len(recherche['club']) > 0:
            proprio = recherche['club'][0]
        if len(recherche['adherent']) > 0:
            proprio = recherche['adherent'][0]

        return proprio

    __machines = ()
    def all_machines(self, graphic=False):
        """
        Renvoie toutes les machines autorisées.

        Cela affiche des trucs et des bidules si graphic est à True.
        """
        if graphic: from affich_tools import anim, cprint, OK
        if not self.__machines:
            # Récolte des données
            if graphic: cprint(u'Lecture base LDAP', 'gras')
            # Machines de l'assoce
            self.__machines = AssociationCrans(conn = self.conn).machines()
            # Machines des adhérents et clubs de l'année en cours
            #base = self.search('paiement=ok')
            base = self.search('nom=*')
            base = base['adherent'] + base['club']
            base=[a for a in base if a.paiement_ok()]
            if graphic: a = anim('\tTri des machines', len(base))
            for adh in base:
                if graphic: a.cycle()
                # Adhérent ayant payé l'année en cours
                if 'bloq' in adh.blacklist_actif():
                    # Adhérent ignoré
                    continue
                self.__machines += adh.machines()
                if graphic: a.reinit()
            if graphic: cprint(OK)

        return self.__machines

#############################################################################

class BaseClasseCrans(CransLdap):
    """ Méthodes de base des classes machines, et BaseProprietaire """

    def proprietaire(self):
        return None

    def carteEtudiant(self, action=None):
        return False

    def adhesion(self, update=False, f=None):
        return 0.0

    def connexion(self, update=False, f=None):
        return 0.0

    def sursis_carte(self):
        return False

    def chbre(self, new=None):
        return "????"

    def __eq__(self, autre):
        """ Test d'égalité de deux instances de club/adhérent/machine,
        retourne True s'il s'agit du même club/adhérent/machine, False sinon """
        return self.__class__ == autre.__class__ and self.id() == autre.id()

    def __enter__(self):
        """Dummy"""
        pass

    def __exit__(self, type, value, traceback):
        """Dummy"""
        pass

    def id(self):
        """ Retourne la valeur de l'attribut caractéristique de la classe (aid,mid,cid)"""
        try:
            s = self.dn.split(',')[0].split('=')
            if s[0] == self.idn:
                return s[1]
        except:
            return ''

    def __str__(self):
        """ Chainde identifiant l'objet de la forme 'uid=1245' """
        return '%s=%s' % (self.idn, self.id())

    def blacklist_actif(self):
        """
        Vérifie si l'instance courante est blacklistée.
        Retourne les sanctions en cours (liste).
        Retourne une liste vide si aucune sanction en cours.
        """
        return self.blacklist_all()[0].keys()

    def blacklist_all(self):
        """
        Vérifie si l'instance courante est blacklistée ou a été
        blacklistée. Retourne les sanctions en cours sous la forme
        d'un couple de deux dictionnaires (l'un pour les sanctions
        actives, l'autre pour les inactive), chacun ayant comme
        clef la sanction et comme valeur une liste de couple de
        dates (en secondes depuis epoch) correspondant aux
        différentes périodes de sanctions.

          ex: {'upload': [(1143336210, 1143509010), ...]}
        """
        bl_liste = self._data.get('blacklist', [])

        actifs = {}
        inactifs = {}

        if isinstance(self, Machine):
            # Il faut aussi regarder la blackliste du propriétaire
            p = self.proprietaire()
            bl_liste += p.blacklist()

        elif isinstance(self, Adherent) and (self.adhesion() > time.time() and self.connexion() > time.time()):
            # blacklistes virtuelle si on est un adhérent pour carte étudiant et chambre invalides
            if self.chbre() == '????':
                actifs['chambre_invalide']=('-','-')

        for sanction in bl_liste:
            champs = sanction.split('$')
            s = champs[2]
            if is_actif(sanction):
                actifs.setdefault(s, []).append((champs[0], champs[1]))
            else:
                inactifs.setdefault(s, []).append((champs[0], champs[1]))
        return (actifs, inactifs)

    def blacklist(self, new=None):
        """
        Blacklistage de la ou de toutes la machines du propriétaire
         * new est une liste de 4 termes :
            [debut_sanction, fin_sanction, sanction, commentaire]
         * début et fin sont le nombre de secondes depuis epoch
         * pour un début ou fin immédiate mettre now
         * pour une fin indéterminée mettre '-'
        Les données sont stockées dans la base sous la forme :
          debut$fin$sanction$commentaire
        Pour modifier une entrée donner un tuple de deux termes :
          (index dans blacklist à modifier, nouvelle liste),
        l'index étant celui dans la liste retournée par blacklist().
        """
        liste = self._data.setdefault('blacklist', [])[:]
        if new == None:
            return map(decode, liste)

        if type(new) == tuple:
            # Modification
            index = new[0]
            new = new[1]
            if new == '':
                liste.pop(index)
                self._set('blacklist', liste)
                return liste
        else:
            index = -1

        if type(new) != list or len(new) != 4:
            raise TypeError

        # Verification que les dates sont OK
        if new[0] == 'now':
            debut = new[0] = int(time.time())
        else:
            try: debut = new[0] = int(new[0])
            except: raise ValueError(u'Date de début blacklist invalide')

        if new[1] == 'now':
            fin = new[1] = int(time.time())
        elif new[1] == '-':
            fin = -1
        else:
            try: fin = new[1] = int(new[1])
            except: raise ValueError(u'Date de fin blacklist invalide')

        if debut == fin:
            raise ValueError(u'Dates de début et de fin identiques')
        elif fin != -1 and debut > fin:
            raise ValueError(u'Date de fin avant date de début')

        # On dépasse la fin de sanction d'1min pour être sûr qu'elle est périmée.
        fin = fin + 60

        new_c = '$'.join(map(str, new))
        new_c = preattr(new_c)[1]

        if index != -1:
            liste[index] = new_c
        else:
            liste.append(new_c)

        if liste != self._data['blacklist']:
            self.modifs.setdefault('blacklist_' + new[2], None)
            if not hasattr(self, "_blacklist_restart"):
                self._blacklist_restart = {}
            restart = self._blacklist_restart.setdefault(new[2], [])
            if debut not in restart:
                restart.append(debut)
            if fin != -1 and fin not in restart:
                restart.append(fin)
            # Si on édite une ancienne blackliste, on met aussi cette bl à jour
            if index != -1:
                old_restart = self._blacklist_restart.setdefault(self._data['blacklist'][index].split('$')[2], [])
                now = int(time.time())
                if now not in old_restart:
                    old_restart.append(now)
            self._data['blacklist'] = liste

        return liste

    def restore(self):
        """ Restore les données à l'état initial (ou pas) """
        self._data = self._init_data.copy()
        self.modifs = {}

    def historique(self):
        """ Retourne l'historique de l'objet """
        return map(decode, self._data.get('historique', []))

    def info(self, new=None):
        """
        Pour ajouter une remarque new doit être la chaîne
        représentant la remarque à ajouter
        Pour modifier new doit être une liste de la forme:
             [ index de la remarque à modifier, nouvelle remarque ]
        l'index est celui obtenu dans la liste retournée par info()
        """
        liste = list(self._data.get('info', []))
        if new == None: return map(decode, liste)

        if type(new) == list:
            # Modif
            index = new[0]
            l, new = preattr(new[1])
            if not new:
                # Supression remarque
                liste.pop(index)
            else:
                # Modif remarque
                liste[index] = new
        elif type(new) == str:
            # Remarque supplémentaire
            l, new = preattr(new)
            if not new:
                # On ajoute pas de remarque vide
                return liste
            # Ajout à la liste
            liste = liste + [new]
        else:
            raise TypeError

        self._set('info', liste)
        return liste

    def _save(self):
        """ Sauvegarde dans la base LDAP """
        if not self.modifs:
            # Rien à faire
            return []

        if not self.dn:
            # Enregistrement à placer en tête de base sauf si
            # facture.
            if isinstance(self, Facture):
                self.dn = self.proprietaire().dn
            else:
                self.dn = self.base_dn

        # Construction de l'historique
        if not self._init_data:
            if self.idn=='fid':
                modif = preattr('création')[1]
            else:
                modif = 'inscription'
        else:
            ### ON NE TOUCHE PAS A SELF.MODIFS, IL EST UTILISÉ PLUS LOIN !!!!!!!
            # Dictionnaire local des modifs
            modif = {}

            # Cas spécial
            if "solde" in self.modifs:
                diff = round(float(self._init_data.get('solde', [0])[0]) - float(self._data.get('solde', [0])[0]), 2)
                if diff > 0:
                    modif['solde'] = "debit %s Euros" % str(diff)
                else:
                    modif['solde'] = "credit %s Euros" % str(-diff)

            # Formate les entrées de l'historique de la forme champ (ancien -> nouveau)
            # On suppose que le champ apparaît forcément dans l'enregistrement
            for champ in ['chbre', 'nom', 'prenom', 'mail', 'tel',
                          'rewriteMailHeaders', 'contourneGreylist',
                          'puissance', 'canal', 'prise', 'responsable',
                          'macAddress', 'ipHostNumber', 'ip6HostNumber',
                          'host', 'positionBorne', 'derniereConnexion',
                          'hotspot', 'dnsIpv6', 'machineAlias', 'finAdhesion',
                          'finConnexion', 'debutConnexion', 'debutAdhesion']:
                if champ in self.modifs:
                    if champ not in self._init_data.keys():
                        valeur_initiale = 'N/A'
                    else:
                        valeur_initiale = self._init_data[champ][0]
                    if not self._data.get(champ, []):
                        valeur_finale = 'N/A'
                    else:
                        valeur_finale = self._data[champ][0]
                    modif[champ] = '%s (%s -> %s)' % (champ,
                                                      valeur_initiale,
                                                      valeur_finale)

            # Formate les entrées de l'historique de la forme champ+diff-diff
            for champ in ['droits', 'controle', 'paiement',
                          'mailAlias', 'hostAlias', 'exempt', 'nvram',
                          'portTCPin', 'portTCPout', 'portUDPin', 'portUDPout',
                          'homepageAlias', 'imprimeurClub', 'gpgFingerprint',
                          'debutConnexion', 'finConnexion', 'debutAdhesion',
                          'finAdhesion']:
                if champ in self.modifs:
                    # Là, on bosse directement sur les listes renvoyées par get
                    ancien = self._init_data.get(champ, [])
                    nouveau = self._data.get(champ, [])

                    # On établit le diff
                    diff = ''.join([ '+%s' % decode(d) for d in nouveau if d not in ancien ])
                    diff += ''.join([ '-%s' % decode(d) for d in ancien if d not in nouveau ])
                    modif[champ] = champ + diff

            for champ in ['sshFingerprint']:
                if champ in self.modifs:
                    modif[champ] = champ

            # On recolle tous les morceaux
            liste_historique = []
            for champ in self.modifs.keys():
                # champs pour lesquels on ne veut pas d'historique
                if champ in ['userPassword']:
                    continue
                ligne = modif.get(champ, champ)
                if self.modifs[champ] != None:
                    ligne += ' [%s]' % self.modifs[champ]
                liste_historique.append(ligne)
            modif = ', '.join(liste_historique)

        timestamp = time.localtime()
        hist = "%s, %s" % ( time.strftime(date_format, timestamp), script_utilisateur )

        if self.modifs.has_key('derniereConnexion'):
            # On nettoie l'historique pour ne garder que la dernière modification
            # (celle en cours)
            self._data['historique'] = [x
                                        for x in self._data['historique']
                                        if 'derniereConnexion' not in x]

        # On loggue
        try:
            fd = file('%s/%s_%s_%s' % ("%s/logs" % config.cimetiere, str(self.__class__).split('.')[-1],
                                       time.strftime('%Y-%m-%d-%H:%M', timestamp), self.nom()), 'wb')
            fd.write("%s\n" % self._data)
            fd.close()
        except:
            pass

        # Suffit-t-il d'ajouter un item au dernier élément de l'historique ?
        try:
            if modif:
                dern = self._data['historique'][-1].split(': ', 2)
                if dern[0] == hist:
                    # Même date et même cableur
                    if modif not in dern[1].split(', '):
                        # Qqch de plus de modifié
                        self._data['historique'][-1] = self._data['historique'][-1] + ', ' +modif
                else:
                    # Nouvelle entrée
                    # NE PAS UTILISER L'OPERATEUR += ICI sinon self._init_data aussi modififié
                    self._data['historique'] = self._data['historique'] + [ preattr("%s : %s" % ( hist, modif ))[1] ]
        except:
            # Nouvelle inscription
            self._data['historique'] = [ "%s : %s" % ( hist, modif ) ]

        if not self._init_data:
            ### Nouvel enregistrement
            # L'enregistrement peut échouer en cas de choix de dn concurrents
            # (il n'y a pas de lock sur les dn)
            for i in range(0, 5):   # 5 tentatives
                # Génération du dn
                res = self.conn.search_s(self.base_dn, ldap.SCOPE_SUBTREE, self.filtre_idn)
                vidn = 1
                vidns = []
                # Liste des dn pris
                for r in res:
                    # r = ( dn, {} )
                    r = r[0].split(',')[0]
                    if r[:4] != '%s=' % self.idn: continue
                    vidns.append(int(r[4:]))
                if self.idn == 'fid' or self.idn == 'aid' or self.idn == 'mid':
                    # Pour une facture on prend un nouveau numéro
                    vidn = max([0] + vidns) + 1
                else:
                    # Sinon on prend le premier libre
                    while vidn in vidns:
                        vidn += 1

                self.dn = '%s=%s,%s' % (self.idn, vidn, self.dn)
                self._data[self.idn] = [ '%d' % vidn ]

                try:
                    # Ecriture
                    modlist = ldap.modlist.addModlist(self._data)
                    self.conn.add_s(self.dn, modlist)
                    break
                except Exception as e:
                    # On logge
                    syslog("ldap_crans: exception: %s" % e)
                    raise
                    if i == 4:
                        raise

        else:
            ### Modification entrée
            if not self._modifiable:
                raise RuntimeError(u'Objet non modifiable : %s' % str(self))
            modlist = ldap.modlist.modifyModlist(self._init_data, self._data)
            try:
                self.conn.modify_s(self.dn, modlist)
            except ldap.TYPE_OR_VALUE_EXISTS, c:
                champ =  c.args[0]['info'].split(':')[0]
                raise RuntimeError('Entree en double dans le champ %r' % champ)

        ### Génération de la liste de services à redémarrer
        # Quasiement tout est traité dans les classes filles.
        if hasattr(self, "_blacklist_restart"):
            for n, t in self._blacklist_restart.items():
                self.services_to_restart("blacklist_%s"%n, [], t)

        # Reinitialisation
        self._init_data = self._data.copy()

    def _delete(self, dn, comment=''):
        """ Sauvegarde puis destruction du dn (et des sous-dn) fourni """
        # Commentaires
        comment = preattr(comment)[1]
        self.modifs.setdefault('destruction (%s)' % comment, None)
        self._save()

        # Sauvegarde
        t = str(self.__class__).split('.')[-1]
        fd = open('%s/%s/%s_%s' % (config.cimetiere, t,
                                   time.strftime('%Y-%m-%d-%H:%M'),
                                   self.nom()), 'wb')
        self.conn = None # Fermeture des connexions à la base sinon cPickle ne marchera pas
        cPickle.dump(self, fd, 2)
        fd.close()
        index = u"%s, %s : %s %s # %s\n" % (time.strftime(date_format),
                                            script_utilisateur, t,
                                            self.Nom(), decode(comment))

        self.connect() # Reconnexion à la base
        # Destruction
        data = self.conn.search_s(dn, 2)

        data.reverse() # Necessaire pour détruire d'abord les sous-dn
        for r in data:
            self.conn.delete_s(r[0])

        try:
            log = open(config.cimetiere + '/index', 'a')
            log.write(index)
            log.close()
        except:
            pass

    def _set(self, champ, val, comment=None):
        """
        Met à jour les données de _data et modifie modifs si besoin.
        Si val=[] ou None, supprime le champ.
        """
        # On convertit éventuellement en utf-8 avant
        # On n'utilise pas preattr pour éviter de tout casser... mais
        # il faudra à l'avenir que _set reçoive toujours en unicode
        # et éviter l'utilisation de preattr autant que possible
        def convert(x):
            if type(x) is unicode: return x.encode('utf-8')
            else: return x
        val = map(convert, val)
        comment = convert(comment)
        # On fait le changement et on note s'il y a vraiment eu une modif
        if self._data.get(champ, []) != val:
            if val:
                self._data[champ] = val
            else:
                self._data.pop(champ)
            if self.modifs.get(champ) == None or comment == None:
                self.modifs[champ] = comment
            else:
                # Ici, self.modifs[champ] et comment devraient être tous deux
                # des chaînes de caractères
                self.modifs[champ] += ', ' + comment

#############################################################################

class BaseProprietaire(BaseClasseCrans):
    """ Méthodes de bases pour les classes Adherent et Club """

    def __init__(self, data=(), mode='', conn=None):
        """
        Si data est fourni initialise l'adhérent avec les valeurs données
        Format de data : tuple comme retourné par une recherche dans la base ldap:
            ( dn, { donnée })
        Si mode='w' : le propriétaire pourra être modifié
        Attention, si mode ='w' mais si l'objet est déja locké il n'y a pas d'erreur
        vérifier l'obtetion du lock grace à la valeur de _modifiable (si =w c'est bon)
        Il est inutile de préciser le mode pour un nouveau proprietaire

        conn est une instance de la classe de connexion à la base LDAP
        """
        self.conn = conn
        if not self.conn:
            self.connect()

        if type(data) != tuple:
            raise TypeError

        self.modifs = {}
        if data:
            self.dn = data[0]
            if mode == 'w':
                try:
#                    self.lock(self.idn, self.id())
                    self._modifiable = 'w'
                except EnvironmentError , c:
                    self._modifiable = 0
            else:
                self._modifiable = 0
            # Utile pour construire l'instruction LDAP de modif
            self._init_data = data[1].copy()
            self._data = data[1]
        else:
            # Propriétaire vide
            self.dn = '' # Génération du reste au moment de l'écriture
            self._data = { 'objectClass': [ self.objectClass ] }
            self._init_data = {}
            self._modifiable = 'w'

    def adhesion(self, update=False, f=None):
        """
        Gestion de l'adhésion d'un adhérent

        La durée d'adhésion ne peut être choisie
            * Si update vaut True, on ajoute un an
            * f est une facture passée en référence.

        """
        thetime = time.time()

        # On récupère sur les factures l'ensemble de celles comportant une adhésion.
        adh_factures = self.factures_adh()
        fgtf = lambda fac:fromGeneralizedTimeFormat(fac._data.get('finAdhesion', [config.gtfepoch])[0])
        finAdh = max([0.0] + [fgtf(facture) for facture
                              in adh_factures
                              if facture.controle() != "FALSE"
                                 and facture.recuPaiement() is not None
                             ])

        if update == False:
            return finAdh
        else:
            ftime = datetime.datetime.fromtimestamp(max(finAdh, thetime))
            if not finAdh - thetime < cotisation.delai_readh:
                raise EnvironmentError, u"On ne peut réadhérer que 15 jours avant l'expiration de l'adhésion précédente."

        # Calcul de la nouvelle date de fin d'adhésion.
        # le +86400 est une souplesse pour permettre au câblage de se passer sans warning
        # quand le mec se fait câbler pour un an.
        newFinAdh = time.mktime(ftime.replace(year=ftime.year + cotisation.duree_adh_an).timetuple()) + 86400

        # Si aucune facture n'est passée en référence, on en crée une nouvelle.
        if f is None:
            f = Facture(self)
        if isinstance(self, Adherent):
            f.ajoute(cotisation.dico_adh)
        elif isinstance(self, Club):
            f.ajoute(cotisation.dico_adh_club)
        f._set("finAdhesion", [generalizedTimeFormat(newFinAdh)])
        f._set("debutAdhesion", [generalizedTimeFormat(thetime)])
        self._set("finAdhesion", self._data.get("finAdhesion", []) + [generalizedTimeFormat(newFinAdh)])
        self._set("debutAdhesion", self._data.get("debutAdhesion", []) + [generalizedTimeFormat(thetime)])
        return f

    def update_adhesion(self):
        """Récupère les dates de début et fin d'adhésion dans les factures
        de l'adhérent, et met à jour ses données."""

        debutAdh = []
        finAdh = []
        for facture in self.factures_adh():
            debutAdh.append(facture._data['debutAdhesion'][0])
            finAdh.append(facture._data['finAdhesion'][0])
        self._set('debutAdhesion', debutAdh)
        self._set('finAdhesion', finAdh)

    def droits(self, droits=None, light=False):
        """ Renvoie les droits courants. Non modifiable (sauf si surchargée dans classe enfant)"""
        if droits <> None:
            raise NotImplementedError("Propriété non modifiable")
        return []

    def chsh(self, new=None):
        """ Retourne ou change le shell de l'adhérent """
        if new == None:
            try: return decode(self._data.get('loginShell', [''])[0])
            except: return ''
        else:
            new = preattr(new)[1]
            self._set('loginShell', [new])
            return new

    def email_exterieur(self, new=None):
        """Retourne ou paramètre l'adresse mail extérieure de l'adhérent"""
        if not new:
            return decode(self._data.get('mailExt', [''])[0])

        new = validate_mail(new)

        self._set('mailExt', [new])

        # On enlève les blacklistes mail_invalide
        self.mail_invalide(False)

        return new

    def alias(self, new=None):
        """
        Création ou visualisation des alias mail
        Même sytème d'argument que la méthode info.
        """
        if not self._data.has_key('mailAlias'):
            self._data['mailAlias'] = []
        liste = list(self._data['mailAlias'])
        if new == None:
            return map(decode, liste)

        if type(new) == list:
            # Modif
            index = new[0]
            new = new[1]
            if new == '':
                # Supression alias
                liste.pop(index)
                self._set('mailAlias', liste)
                return liste
        else:
            index = -1

        # Tests
        l, new = preattr(new)
        new = new.lower()
        if l < 2:
            raise ValueError(u"Alias trop court.")
        for c in new[:]:
            if not c in (string.letters + string.digits + '-_.@'):
                raise ValueError(u"Alias : seuls les caractères alphanumériques, le -, le _ et le . sont autorisés." )
        if new[0] not in string.letters:
            raise ValueError(u"Le premier caractère de l'alias doit être alphabétique.")
        if mailexist(new):
            raise ValueError(u"Alias existant ou correspondand à un compte.")

        if new.count('@') >= 2:
            raise ValueError(u"Alias avec plus de un @ ?")

        if '@' not in new:
            new += '@crans.org'
        else:
            new = new.split('@')[0].lower() + '@crans.org'

        if index != -1:
            liste[index] = new
        else:
            liste = liste + [ new ]

        # Lock de mailAlias
        self.lock('mailAlias', new)

        self._set('mailAlias', liste)
        return liste

    def gpgFingerprint(self, new=None):
        """
        Fonction d'ajout d'empreinte GPG
        """

        liste = list(self._data.get('gpgFingerprint', []))

        if new == None:
            return liste

        if not isadm() and isadm(self.compte()):
            raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')


        else:
            if type(new) == list:
                # Modif
                index = new[0]
                new = new[1]
                if new == '':
                    # Supression alias
                    liste.pop(index)
                    self._set('gpgFingerprint', liste)
                    return liste
            else:
                index = -1

        new = format_gpg_fingerprint(new)
        new = new.upper()

        #if gpgexists(new):
        #    raise ValueError(u"Alias existant ou correspondand à un compte.")

        if index != -1:
            liste[index] = new
        else:
            liste = liste + [ new ]

        self.lock('gpgFingerprint', new)
        self._set('gpgFingerprint', liste)
        return liste

    def homepageAlias(self, new=None):
        """
        Creation ou visualisation des alias de page perso.
        Meme systeme d'arguments que pour info.
        """
        liste = self._data.get('homepageAlias', [])[:]
        if new == None: return liste

        def validate(alias):
            alias = alias.lower()
            if not re.match(r"^[a-z0-9](\.?[-a-z0-9]+)*$", alias):
                raise ValueError("Alias incorrect : %s" % alias)
            if "." not in alias:
                alias += ".perso.crans.org"
            if alias.endswith(".crans.org") and not alias.endswith(".perso.crans.org"):
                raise ValueError("Les alias crans doivent se terminer par .perso.crans.org")
            # ...peut-etre faire une verification de domaine ici...
            # On convertit en str (il ne devrait plus y avoir de caractere Unicode)
            return str(alias)

        if type(new) == list:
            # Modif
            index = new[0]
            new = new[1]
            if not new:
                # Supression alias
                liste.pop(index)
            else:
                # Modification alias
                new = validate(new)
                liste[index] = new
        elif type(new) == str:
            # Alias supplémentaire
            if not new:
                # On n'ajoute pas d'alias vide
                return liste
            # Ajout à la liste
            new = validate(new)
            liste.append(new)
        else:
            raise TypeError

        # Lock de l'alias (a faire)
        # self.lock('homepageAlias', new)

        self._set('homepageAlias', liste)
        return liste

    def machines(self):
        """ Retourne les machines (instances) appartenant à la classe """
        # Le champ id n'est pas initialisé lorsque le proprio est en cours
        # de création
        if self.id():
            res = []
            for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, Machine.filtre_idn):
                res.append(self.make(r, self._modifiable))
            return res
        else:
            return []

    def machines_fixes(self):
        """ Retourne les machines fixes appartenant à l'instance """
        if self.id():
            res = []
            for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, 'objectClass=machineFixe'):
                res.append(self.make(r, self._modifiable))
            return res
        else:
            return []

    def machines_wifi(self):
        """ Retourne les machines wifi appartenant à l'instance """
        if self.id():
            res = []
            for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, 'objectClass=machineWifi'):
                res.append(self.make(r, self._modifiable))
            return res
        else:
            return []

    def factures(self, filtre=None):
        """ Retourne les factures (instances) appartenant à la classe """
        # Le champ id n'est pas initialisé lorsque le proprio est en cours
        # de création
        if filtre is None:
            filtre = Facture.filtre_idn
        else:
            filtre = "(&%s%s)" % (filtre, Facture.filtre_idn)
        if self.id():
            res = []
            for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, filtre):
                res.append(self.make(r, self._modifiable))
            return res
        else:
            return []

    def factures_adh(self):
        """ Retourne les factures pour adhésion valides."""
        factures_adh = self.factures("(debutAdhesion=*)")
        return [
            facture
            for facture in factures_adh
            if facture.controle() != "FALSE"
            and facture.recuPaiement() is not None
        ]

    def factures_conn(self):
        """ Retourne les factures pour connexion """
        factures_conn = self.factures("(debutConnexion=*)")
        return [
            facture
            for facture in factures_conn
            if facture.controle() != "FALSE"
            and facture.recuPaiement() is not None
        ]

    def solde(self, operation=None, comment=None):
        """ Retourne ou modifie le solde d'un propriétaire
        operation doit être un nombre positif ou négatif
        (string ou int ou float)
        comment est un commentaire à rajouter dans l'historique
        """
        solde = float(self._data.get('solde', [0])[0])

        if operation == None:
            return solde

        # On effectue une opération
        try:
            new = solde + float(str(operation).replace(',', '.'))
        except ValueError:
            raise ValueError(u"Il faut donner un nombre en argument.")

        # découvert accepté
        if new < config.impression.decouvert:
            raise ValueError(u"Solde minimal atteint, opération non effectuée.")

        self._set('solde', [str(new)], comment)
        return new

    def controle(self, new=None):
        """
        Controle du tresorier
        New est de la forme [+-][pc]
        (p pour le paiement, c pour la carte)
        Retourne une chaine contenant une combinaison de p, c.
        """
        actuel = self._data.get('controle', [''])
        if not actuel:
            actuel = ''
        else:
            actuel = actuel[0]

        if new == None:
            return actuel

        if not re.match(r'^[+-][pck]$', new):
            raise ValueError('modification de controle incorrecte')

        for c in 'pc':
            if new == '+%s' % c and c not in actuel:
                actuel += c
            if new == '-%s' % c:
                actuel = actuel.replace(c, '')

        if actuel == '':
            # La base LDAP n'accepte pas les chaînes vides.
            # On supprime directement le champ controle
            if self._data.has_key('controle'):
                if self._data.pop('controle') != []:
                    # Il y avait vraiment qqch avant
                    if 'controle' not in self.modifs:
                        self.modifs.setdefault('controle', None)
        else:
            self._set('controle', [actuel])

        return actuel

    def contourneGreylist(self, contourneGreylist=None):
        """
        Retourne ou change la greylist pour le compte
         * True  : contourne le GreyListing
         * False : ne contourne pas le greylisting
        Au niveau de la base, on considère la présence ou l'absence
        d'un champ contourneGreylist=OK.
        """

        # Pour postfix il faut retourner :
        # OK : contourne la greyliste
        # cf. man 5 access

        # si l'adhérent n'a pas de compte, on lève une exception
        if not self.compte():
            raise NotImplementedError, u"L'adhérent n'a pas de compte"

        # tente de modifier la valeur
        if contourneGreylist == True:
            self._set('contourneGreylist', ['OK'])
        elif contourneGreylist == False:
            self._set('contourneGreylist', [])
        elif contourneGreylist != None:
            raise ValueError, u"contourneGreylist prend un booléen comme argument"

        # renvoie la valeur trouvée dans la base
        return bool(self._data.get('contourneGreylist', []))

    def verifyPasswd(self, passwd):
        """
        Retourne True si le mot de passe est bon,
        ou False si le mot de passe n'est pas bon.
        """
        if not self.compte():
            raise NotImplementedError, u"L'adhérent n'a pas de compte"
        return ldap_passwd.checkpwd(passwd, self._data['userPassword'][0])

    def changePasswd(self, passwd):
        """
        Modifie le mot de passe de l'adhérent
        """
        if not self.compte():
            raise NotImplementedError, u"L'adhérent n'a pas de compte"
        self._set('userPassword', [ldap_passwd.mkpasswd(passwd)])

    def forward(self, new = None):
        """
        Modifie ou retourne l'adresse de forward de l'adhérent
        NB : il faut avoir un sudo sur /usr/scripts/gestion/mail_config.py
             sinon cette metode ne fonctionnera pas
        """
        if not self.compte():
            raise NotImplementedError, u"L'adhérent n'a pas de compte"
        return config_mail.MailConfig(uid=self._data['uid'][0], forward = new)['forward']

    def spam(self, new = None):
        """
        Modifie ou retourne le traitement des spams de l'adhérent
        new doit être 'accepte', 'marque' ou 'supprime'
        NB : il faut avoir un sudo sur /usr/scripts/gestion/mail_config.py
             sinon cette metode ne fonctionnera pas
        """
        if not self.compte():
            raise NotImplementedError, u"L'adhérent n'a pas de compte"
        return config_mail.MailConfig(uid=self._data['uid'][0], spam = new)['spam']

    def home(self):
        """ Retourne le home de l'adhérent """
        if not self.compte():
            raise NotImplementedError, u"L'adhérent na pas de compte"

        return self._data['homeDirectory'][0]

    def uidNumber(self, new=None):
        """ Retourne ou change l'uidNumber de l'adhérent """
        if not self.compte():
            raise NotImplementedError, u"L'adhérent na pas de compte"
        if new == None:
            return self._data['uidNumber'][0]
        else:
            new = preattr(new)[1]
            self._set('uidNumber', [new])
            return new

    def paiement_ok(self):
        if isinstance(self, Adherent):
            m_paiement = min(self.adhesion(), self.connexion())
        else:
            m_paiement = self.adhesion()
        if (m_paiement > time.time()) or (config.periode_transitoire and config.debut_periode_transitoire <= m_paiement <= config.fin_periode_transitoire):
            return True
        else:
            return False

    def delete(self, comment=''):
        """Destruction du propriétaire"""

        if max(self.connexion(), self.adhesion()) + cotisation.del_post_adh >= time.time():
            raise EnvironmentError("Vous ne pouvez supprimer un adhérent que %s jours après l'expiration de son adhésion et de sa connexion" % (cotisation.del_post_adh_jours,))

        for m in self.machines():
            # Destruction machines
            m.delete(comment)

        for f in self.factures():
            # Destruction factures
            f.delete(comment)

        self._delete(self.dn, comment)

        try:
            if self.compte():
                args = self._data['uid'][0] + ','
                args+= self._data['homeDirectory'][0]
                self.services_to_restart('del_user', [ args ] )
        except:
            # Si ne peux avoir de compte
            pass

    def save(self):
        """
        Enregistre l'adhérent ou le club courant dans la base LDAP
        Ajoute le mail de bienvenue à la liste des services à redémarrer
        Retourne une chaîne indiquant les opération effectuées.
        """
        # Note: un peu trop de fonctions pour un club mais ce n'est pas génant

        ret = ''
        if self._init_data:
            nouveau = 0
        else:
            nouveau = 1

        if 'chbre' in self.modifs and '????' in self._init_data.get("chbre", [''])[:2]:
            self.services_to_restart('bl_chbre_invalide')

        if ('chbre' in self.modifs or 'paiement' in self.modifs) and self._data['chbre'][0] not in ("????", "EXT"):
            bat = self._data['chbre'][0][0]
            ch = self._data['chbre'][0][1:]

        # Enregistrement
        self._save()

        # Message de sortie
        if nouveau:
            ret += coul(u"%s inscrit avec succès." % self.Nom(), 'vert')
            ret += coul('\n%s\n' % ('-'* 78), 'rouge')
            ret += coul(u"Merci d'indiquer son aid (%s) en haut à gauche de la fiche d'adhésion" % self.id(), 'gras')
            ret += coul('\n%s\n' % ('-'* 78), 'rouge')

            if self.idn !='cid':
                # Mail de bienvenue
                self.services_to_restart('mail_bienvenue', [self.mail().encode('utf-8')], start = time.time() + 660)

        else:
            ret += coul(u"Modification %s effectuée avec succès." % self.Nom(), 'vert')

            if any([kw in self.modifs for kw in ['finConnexion', 'finAdhesion']]):
                for m in self.machines():
                    self.services_to_restart('macip', [m.ip()] )
                    #self.services_to_restart('classify', [m.ip()] )
                    self.services_to_restart('dns')
                    self.services_to_restart('dhcp')

            # Vérification si changement de bât, ce qui obligerai un changement d'IP
            if 'adherentPayant' in self.modifs or 'chbre' in self.modifs and self.chbre() != '????':
                # Verif si machines avec bonnes ip
                err = 0
                for m in self.machines():
                    if isinstance(m, MachineWifi):
                        # Machine Wifi
                        continue
                    # Machine fixe
                    ip = m.ip()
                    try:
                        # Tentative de changement d'IP de la machine
                        m.ip(ip)
                    except ValueError:
                        # IP invalide, on la change
                        ret += "\nChangement d'IP machine %s : " % m.nom()
                        try:
                            ret += "%s -> %s" % ( ip, m.ip('<automatique>') )
                            m.save()
                        except Exception, c:
                            ret += coul(u'ERREUR : %s' % c.args[0], 'rouge')
                            err = 1

                if err: ret += '\nEssayer de corriger les erreurs machines en éditant celles-ci.\n'

        # Faut-il créer un compte sur vert ?
        if 'compte' in self.modifs:
            compte = self._data.get('uid', [''])[0]
            if compte:
                ret += u'\nUn compte a été créé :\n    login : %s\n' %  self.compte()
                r = prompt(u"Attribuer tout de suite un mot de passe, (A pour Automatique) ? [o/n/A]", "A")
                if r == 'O' or r == 'o':
                    change_password(login=self.compte())
		if r == 'A' or r == 'a':
		    subprocess.call(['/usr/scripts/cransticket/dump_creds.py','--pass','uid=%s' % self.compte()])
                else:
                    ret += coul(u'    Il faudra penser à attribuer un mot de passe\n', 'jaune')
            # Le deuxième argument est le potentiel chemin de l'ancien compte
            # s'il a jamais existé, ça permet de supprimer le home associé.
            args = "%s,%s" % (compte, self.modifs.get('compte', ''))

            self.services_to_restart('home', [ args ])

        # Modif des droits ?
        if 'droits' in self.modifs:
            self.services_to_restart('droits')
            self.services_to_restart('mail_modif', ['uid=%s' % self._data['uid'][0]])

        # Remise à zero
        self.modifs = {}

        return ret

    def _an(self, champ, action):
        """
        Champ est un champ contenant une liste d'entiers
        Action est un entier représentant une année
                si positif ajoute l'année à la liste
                si négatif le supprime
        """
        if not self._data.has_key(champ):
            trans = []
        else:
            # On va travailler sur une liste d'entiers
            trans = map(int, self._data[champ])

        if action == None:
            return trans

        if type(action) != int: raise TypeError

        touched = False
        if action>0 and action not in trans:
            trans.append(action)
            touched = True
        elif action<0 and -action in trans:
            trans.remove(-action)
            touched = True
        if touched and champ not in self.modifs:
            self.modifs.setdefault(champ, None)

        trans.sort()
        self._data[champ] = map(str, trans)

        return self._data[champ]

#############################################################################

class Adherent(BaseProprietaire):
    """ Classe de définition d'un adhérent """
    objectClass = 'adherent'
    idn = 'aid'
    filtre_idn = '(objectClass=adherent)'

    ### Méthodes Nom utilisée lors de l'affichage des propriétés
    ### (commune avec les classes AssociationCrans et Club)
    def Nom(self):
        """ Retourne prenom nom """
        return "%s %s" % ( self.prenom() , self.nom() )

    def nom(self, new=None):
        return self.__nom_prenom('nom', new)

    def prenom(self, new=None):
        return self.__nom_prenom('prenom', new)

    def __nom_prenom(self, champ, new):
        if new == None:
            return decode(self._data.get(champ, [''])[0])

        l, new = preattr(new)
        new = new.capitalize()
        for c in strip_accents(new[:], False):
            if c not in (string.letters + '- '):
                raise ValueError(u"Seuls les caractères alphabétiques, l'espace et le - sont permis dans %s." % champ)
        if l<2:
            raise ValueError(u"%s trop court." % champ)
        if new[0] not in string.letters:
            raise ValueError(u"Le premier caractère du %s doit être une lettre" % champ)

        self._set(champ, [new])
        if self._data.has_key('gecos'):
            gecos = '%s %s' % tuple(map(lambda x: strip_accents(x.capitalize(), False), (self.prenom(), self.nom())))
            self._data['gecos'] = [ preattr(gecos)[1] + ',,,' ]
        return new

    def tel(self, new=None):
        if new == None:
            return self._data.get('tel', [''])[0]

        if new != 'inconnu':
            l, new = preattr(new)
            if not new.isdigit() or l < 6 or l > 15:
                raise ValueError(u"Numéro de téléphone incorrect (il doit comporter uniquement des chiffres).")

        self._set('tel', [new])
        return new

    def chbre(self, new=None):
        """
        Définit la chambre d'un adhérent, EXT pour personne extérieure au campus
        """
        if new == None:
            return decode(self._data.get('chbre', [''])[0])

        l, new = preattr(new)
        if l == 0:
            raise ValueError(u"Chambre incorrecte.")

        if new.upper() == 'EXT':
            # N'est pas ou plus sur le campus
            # Machine fixe ?
            #       for m in self.machines():
            #           if not isinstance(m, MachineWifi):
            #               raise ValueError(u'Un adhérent en dehors du campus ne doit pas avoir de machine fixe.')

            self._set('chbre', ['EXT'])
            return 'EXT'
        elif new.upper() == '????':
            # On ne sait pas ou est l'adhérent
            self._set('chbre', ['????'])
            return '????'

        new = new.capitalize()
        bat = new[0].lower()

        if bat in annuaires.bat_switchs:
            # On a la liste des chambres
            chbres = annuaires.chbre_prises(bat).keys()
            if new[1:] not in chbres or len(new)<4 or not new[1:4].isdigit():
                chbres.sort()
                aide = u"Chambre inconnue dans le batiment, les chambres valides sont :"
                a = 0
                for c in chbres:
                    if len(c)>=3 and not c[:3].isdigit():
                        # C'est un local club
                        continue
                    if int(a/14)>int((a-1)/14): aide += '\n  '
                    if c[0] != 'X':
                        aide += c.ljust(5)
                        a = a+1
                aide += u'\n'
                raise ValueError(aide)

        else:
            raise ValueError(u'Bâtiment inconnu.')

        # La chambre est valide, est-elle déja occupée ?
        test = self.exist('chbre=%s' % new)
        if test:
            search = test[0].split(',')[0]
            if search.split('=')[0] != 'aid':
                raise ValueError(u'Chambre déjà occupée.')
            adh = self.search(search, self._modifiable)['adherent']
            if len(adh) != 1:
                raise ValueError(u'Chambre déjà occupée.')
            else:
                raise ValueError(u'Chambre déjà occupée.', adh[0])

        # Lock de la chambre
        self.lock('chbre', new)

        self._set('chbre', [new])
        self._set('postalAddress', [])
        return new

    def adresse(self, new=None):
        """ Défini l'adresse pour les personnes extérieures (dont la chambre = EXT)
        L'adresse est une liste de 4 éléments : numero, rue, code postal, ville
        """
        if new == None:
            if self.chbre() != 'EXT':
                # Personne sur le campus
                return [u'', u'', u'', u'']
            else:
                addr = self._data.get('postalAddress', ['','', '', ''])[:4]
                if len(addr) < 4:
                    addr = addr + ['']*(4-len(addr))
                return map(decode, addr)

        if type(new) != list and len(new) != 4:
            raise TypeError

        l_min = [ 2, 0, 5, 2 ]
        for i in range(0, 4):
            l, new[i] = preattr(new[i])
            if l < l_min[i]: raise ValueError(u"Adresse incorrecte.")

        # Correction si necessaire
        if not new[1]:
            new[1] = ' '

        self._set('postalAddress', new)
        return new

    def mail(self, new=None):
        if new == None:
            email = self._data.get('mail', [''])[0]
            if not '@' in email:
                return decode(email) + '@crans.org'
            else:
                return decode(email)

        new = validate_mail(new)

        # Il ne doit pas y avoir de compte
        self.supprimer_compte()
        self._set('mail', [new])

        # On enlève les blacklistes mail_invalide
        self.mail_invalide(False)

        # on renvoie le mail de bienvenue
        self.services_to_restart('mail_bienvenue', [new.encode('utf-8')])

        return new

    def email(self, new=None):
        """ Retourne l'adresse mail, ajoute le @crans.org si nécessaire """
        # pour la compatibilité entre les fonctions
        if new:
            self.mail(new)

        # ajout du @crans.org si nécessaire
        mail = self.mail()
        if not '@' in mail:
            mail += '@crans.org'
        return mail

    def mail_invalide(self, valeur=None):
        """
        L'adresse est invalide.
        Au niveau de la base, on considère l'absence ou la présence d'une
        blackliste mail_invalide.
        """
        if valeur == False:
            # On enlève les blacklistes
            bl_mail_list = [x for x in self.blacklist() if 'mail_invalide' in x]
            now = int(time.time())
            for bl_mail in bl_mail_list:
                bl_data = bl_mail.split('$')
                if int(bl_data[0]) <= now and bl_data[1] == '-':
                    self.blacklist((self.blacklist().index(bl_mail),
                            [bl_data[0], 'now', 'mail_invalide', bl_data[3]]))
                elif int(bl_data[0]) >= now:
                    self.blacklist((self.blacklist().index(bl_mail),
                            [str(now-2), str(now-1), 'mail_invalide', bl_data[3]]))
                    # Sale, mais évite d'avoir plusieurs blacklistes sur les mêmes dates
                    now -= 2

        elif valeur == None:
            # On retourne l'existence d'une blackliste active ou future
            bl_mail_list = [x for x in self.blacklist() if 'mail_invalide' in x]
            now = time.time()
            for bl_mail in bl_mail_list:
                bl_data = bl_mail.split('$')
                if (int(bl_data[0]) <= now and bl_data[1] == '-') or int(bl_data[0]) >= now:
                    return True
            return False

        else:
            raise ValueError, u'mail_invalide ne peut prendre que None ou False en argument'

    def charteMA(self, valeur=None):
        """
        La charte des membres actifs est signee. False par defaut.
        """

        # tente de modifier la valeur
        if valeur == True:
            self._set('charteMA', ['TRUE'])
        elif valeur == False:
            self._set('charteMA', [])
        elif valeur != None:
            raise ValueError, u"charteMA prend un booléen comme argument"

        # renvoie la valeur trouvée dans la base
        return bool(self._data.get('charteMA', []))

    def connexion(self, mois=None, f=None):
        """
        Gestion de la connexion d'un adhérent

            * valeur est un entier définissant un nombre de mois
            * f est une facture
        """
        thetime = time.time()

        # On récupère sur les factures l'ensemble de celles comportant une connexion.
        conn_factures = self.factures_conn()
        fgtf = lambda fac:fromGeneralizedTimeFormat(fac._data.get('finConnexion', [config.gtfepoch])[0])
        finConn = max([0.0] + [fgtf(facture) for facture
                               in conn_factures
                               if facture.controle() != "FALSE"
                                   and facture.recuPaiement() is not None
                              ])

        if mois is None:
            return finConn
        elif not isinstance(mois, int):
            raise ValueError, u"Le nombre de mois doit être un entier"
        else:
            ftime = max(finConn, thetime)

        if mois >= cotisation.duree_conn_plafond:
            mois = 12

        # Calcul de la nouvelle date de fin d'adhésion.
        curyear = datetime.datetime.now().year
        curmonth = datetime.datetime.now().month
        nbJours = 0
        for i in xrange(1, mois+1):
            nbJours += monthrange((curmonth + i - 1)/12 + curyear, (curmonth + i - 1)%12 + 12 * ((curmonth + i - 1) % 12 == 0))[1]

        # On ajoute 3600 secondes sur suggestion de Raphaël Bonaque (<bonaque@crans.org>), pour tenir compte des malheureux qui
        # pourraient subir le changement d'heure.
        newFinConn = ftime + 86400 * nbJours + 3600

        # Si aucune facture n'est passée en référence, on en crée une nouvelle.
        if f is None:
            f = Facture(self)
        f.ajoute(cotisation.dico_cotis(mois))
        f._set("finConnexion", [generalizedTimeFormat(newFinConn)])
        f._set("debutConnexion", [generalizedTimeFormat(thetime)])
        self._set("finConnexion", self._data.get("finConnexion", []) + [generalizedTimeFormat(newFinConn)])
        self._set("debutConnexion", self._data.get("debutConnexion", []) + [generalizedTimeFormat(thetime)])
        return f

    def update_connexion(self):
        """Récupère les dates de début et fin de connexion dans les factures
        de l'adhérent, et met à jour ses données."""

        debutConn = []
        finConn = []
        for facture in self.factures_conn():
            debutConn.append(facture._data['debutConnexion'][0])
            finConn.append(facture._data['finConnexion'][0])
        self._set('debutConnexion', debutConn)
        self._set('finConnexion', finConn)

    def adherentPayant(self, valeur = None):
        """
        L'adhérent paie sa cotisation (a droit au WiFi, à un compte Crans, ... True par défaut
        """

        if isinstance(valeur, bool):
            if valeur:
                set_to = []
            else:
                set_to = ['FALSE']
            self._set('adherentPayant', set_to)
        elif valeur is not None:
            raise ValueError, u"adherentPayant prend un booléen comme argument"

        # bool d'une liste non vide est true, on inverse donc (la base ne peut contenir que FALSE)
        return not bool(self._data.get('adherentPayant', []))

    def supprimer_compte(self):
        """
        Supprime le compte sur zamok. Penser à définir l'adresse mail après.
        """
        if not self.compte():
            return
        self._set('mail', [''])
        if abs(self.solde()) >= 0.01:
            raise ValueError(u"Le solde d'un adhérent doit être nul pour supprimer son compte crans.")
        self._data['objectClass'] = ['adherent']

        if not 'compte' in self.modifs:
            self.modifs['compte'] = "%s,%s" % (self.compte(), self.home())
        for c in [ 'uid', 'cn', 'shadowLastChange', 'shadowMax',
                   'shadowWarning', 'loginShell', 'userPassword',
                   'uidNumber', 'gidNumber', 'homeDirectory', 'gecos',
                   'droits', 'mailAlias', 'canonicalAlias',
                   'rewriteMailHeaders', 'contourneGreylist',
                   'homepageAlias', 'derniereConnexion', 'solde',
                   'gpgFingerprint']:
            try: self._data.pop(c)
            except: pass

    def etudes(self, index_or_new):
        """
        Retourne l'un des 3 champs études (selon index_or_new si entier)
        """
        if type(index_or_new) == int:
            if self._data.has_key('etudes'):
                return decode(self._data['etudes'][index_or_new])
            else:
                return ''

        if type(index_or_new) != list:
            raise TypeError

        if not self._data.has_key('etudes'):
            self._data['etudes'] = ['', '', '']

        # Pas grand chose à faire à part vérifier que ce sont bien des chaines
        if len(index_or_new) != 3:
            raise ValueError(u"Format études non valides.")

        new = ['', '', '']
        for i in range(0, 3):
            n = preattr(index_or_new[i])[1]
            if n in new or n == '':
                raise ValueError(u"Etudes non valides.")
            new[i] = n

        self._set('etudes', new)

        return new

    def carteEtudiant(self, action=None):
        """
        Action est un entier représentant une année
                si positif ajoute l'année à la liste
                si négatif le supprime
        """
        if action == True:
            self._set('carteEtudiant', ['TRUE'])
        elif action == False:
            self._set('carteEtudiant', [])
        return bool(self._data.get('carteEtudiant', []))

    def checkPassword(self, password):
        """Vérifie le mot de passe de l'adhérent"""
        anon = ldap.initialize(uri)
        try:
            r = anon.simple_bind(self.dn, password)
            anon.result(r)
        except ldap.INVALID_CREDENTIALS:
            # A priori, le mot de passe est pas bon, il se peut aussi
            # que l'utilisateur n'existe pas
            return False
        return True


    def compte(self, login=None, uidNumber=0, hash_pass='', shell=config.login_shell):
        """
        Création d'un compte à un adhérent
        (la création se fait après l'écriture dans la base par la méthode save)
        Si login = None, retourne le compte de l'adhérent
        """
        if not login:
            return self._data.get('uid', [''])[0]

        # Supression des accents et espaces
        login = strip_accents(login)

        l, login = preattr(login)
        login = login.lower()

        if login and not self.adherentPayant():
            raise ValueError(u"L'adhérent ne paie pas de cotisation, il n'a pas droit à un compte.")

        if 'posixAccount' in self._data['objectClass']:
            if login != self._data['uid']:
                # A déja un compte
                raise ValueError(u"L'adhérent à déjà un compte. Login : %s" % self._data['uid'][0])
            else:
                return login

        for c in login[:]:
            if not c in (string.letters + '-'):
                raise ValueError(u"Seuls les caractères alphabétiques non accentués et le - sont autorisés dans le login.")
        if l < 2:
            raise ValueError(u"Login trop court.")
        if l > config.maxlen_login:
            raise ValueError(u"Login trop long.")
        if login[0] == '-':
            raise ValueError(u"- interdit en première position.")

        if mailexist(login):
            raise ValueError(u"Login existant ou correspondant à un alias mail.", 1)

        home = '/home/%s/%s' % (login[0], login)
        if os.path.exists(home):
            raise ValueError(u'Création du compte impossible : home existant', 1)

        if os.path.exists("/var/mail/" + login):
            raise ValueError(u'Création du compte impossible : /var/mail/%s existant' % login, 1)

        # Lock du mail
        self.lock('mail', login)

        self._data['mail'] = ["%s@crans.org" % (login)]
        if not 'compte' in self.modifs:
            self.modifs.setdefault('compte', ",")

        # Création de l'alias canonique
        if self.nom() and self.prenom():
            a = '%s.%s' % (self.prenom().capitalize(), self.nom().capitalize())
            if self.canonical_alias(a) == None:
                self.canonical_alias(login)

        self._data['objectClass'] = ['adherent', 'cransAccount', 'posixAccount', 'shadowAccount']
        self._data['uid'] = [login]
        self._data['cn'] = [preattr(self.Nom())[1]]
        #self._data['shadowLastChange'] = [ '12632' ]
        #self._data['shadowMax'] = [ '99999']
        #self._data['shadowWarning'] = [ '7' ]
        self._data['loginShell'] = [shell]
        if hash_pass:
            self._data['userPassword'] = [hash_pass]

        if uidNumber:
            if self.exist('(uidNumber=%s)' % uidNumber):
                raise ValueError(u'uidNumber pris')
        else:
            pool_uid = range(1001, 11000)
            random.shuffle(pool_uid)
            while len(pool_uid) > 0:
                uidNumber = pool_uid.pop()  # On choisit une IP
                if not self.exist('(uidNumber=%s)' % uidNumber):
                    # On a trouvé un uid libre
                    pool_uid.append(uidNumber)
                    break
            if not len(pool_uid):
                raise ValueError(u"Plus d'uid disponibles !")

        try:
            self.lock('uidNumber', str(uidNumber))
        except:
            # Quelqu'un nous a piqué l'uid que l'on venait de choisir !
            return self.compte(login, uidNumber, hash_pass, shell)

        self._data['uidNumber'] = [str(uidNumber)]
        self._data['gidNumber'] = [str(config.gid)]

        self._data['homeDirectory'] = [ preattr(home)[1] ]
        gecos = '%s %s' % tuple(map(lambda x: strip_accents(x.capitalize()), (self.prenom(), self.nom())))
        self._data['gecos'] = [ preattr(gecos)[1] + ',,,' ]

        # On enleve les blacklistes mail_invalide
        self.mail_invalide(False)

        return decode(login)

    def active(self, status=None):
        """Permet d'activer ou désactiver un compte"""
        if self.compte() == '':
            raise EnvironmentError("Can't deactivate non-existing account")
        if status == None:
            return not (self._data.get("shadowExpire", [""])[0] == "0")
        elif status == False:
            self._set('shadowExpire', ["0"])
        else:
            self._set('shadowExpire', [])

        return not (self._data.get("shadowExpire", [""])[0] == "0")

    def canonical_alias(self, new=None):
        """ Retourne ou défini l'alias canonique"""
        if new == None:
            try: return decode(self._data['canonicalAlias'][0])
            except: return ''
        else:
            a = strip_accents(new)
            a = preattr(a)[1]

            if not mailexist(a):
                # Attribution de l'alias, sinon on passe

                # Lock de canonicalAlias
                self.lock('canonicalAlias', a)

                # Attribution
                self._set('canonicalAlias', ["%s@crans.org" % (a)])
                return a

    def droits(self, droits=None, light=False):
        """Modifie les droits d'un compte.
        droits est la liste des droits à donner à l'utilisateur,
        light permet de modifier les droits non critiques sans être Nounou."""

        if droits != None and 'cransAccount' not in self._data.get('objectClass', []):
            raise EnvironmentError(u'Il faut avoir un compte pour avoir des droits.')

        if droits == None:
            return map(decode, self._data.get('droits', []))

        # Si pas light, il faut être Nounou
        if not light:
            if not isadm():
                raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')

        # On vérifie la liste donnée
        if type(droits) != list:
            raise TypeError(u'Une liste est attendue')

        new = []
        for droit in droits:
            droit = droit.strip()
            if droit == '': continue
            if droit not in droits_possibles:
                raise ValueError(u'Droit %s incorrect' % droit)
            new.append(droit)

        ancien = self._data.get('droits', [])
        # On envoie les mails de "bienvenue" pour chaque nouveau droit
        for droit in new:
            if droit not in ancien:
                db.services_to_restart("mail_ajout_droits", self.compte().encode('latin-1') + ":" + droit)

        # Si light, alors on n'a pas le droit de modifier les droits critiques
        if light:
            diff = [droit for droit in (ancien + new)
                    if (droit not in ancien or droit not in new) and (droit in droits_critiques)]
            if len(diff) > 0:
                raise ValueError("Droits critiques modifies (?) :: %s" % ','.join(diff))

        # Sauvegarde
        if new != self._data.get('droits', []):
            self._set('droits', new)

        return new

    def droitsGeles(self):
        reponse = False
        if config.bl_vieux_cableurs:
            l = self.droits()
            if l != []:
                if not self.paiement_ok():
                    reponse = True
                    for d in droits_vieux:
                        if d in l:
                            reponse = False
        return reponse

    def rewriteMailHeaders(self, rewrite=None):
        """
        Réécriture des entêtes mail avec l'alias canonique
         * True  : réécrit les en-têtes
         * False : ne réécrit pas les en-têtes
        Au niveau de la base, on considère l'absence ou la présence
        de rewriteMailHeaders=TRUE.
        """

        # si l'adhérent n'a pas de compte, on lève une exception
        if not self.compte():
            raise NotImplementedError, u"L'adhérent n'a pas de compte"

        # tente de modifier la valeur
        if rewrite == True:
            self._set('rewriteMailHeaders', ['TRUE'])
        elif rewrite == False:
            self._set('rewriteMailHeaders', [])
        elif rewrite != None:
            raise ValueError, u"rewriteMailHeaders prend un booléen comme argument"

        # renvoie la valeur trouvée dans la base
        return bool(self._data.get('rewriteMailHeaders', []))

    def derniereConnexion(self, new=False):
        """
        Date de dernière connexion. Le format est le nombre de secondes
        écoulées depuis Epoch. Si new est donné, met à jour la valeur si
        elle est plus récente.
        """

        # si l'adhérent n'a pas de compte, on lève une exception
        if not self.compte():
            raise NotImplementedError, u"L'adhérent n'a pas de compte"

        current = int(self._data.get('derniereConnexion', ['0'])[0])

        if new:
            # lève une exception si l'argument n'est pas valide
            new = int(new)
            if new > current:
                self._set('derniereConnexion', [str(new)])
                current = new

        return current

    def dateInscription(self):
        """Renvoie la date d'inscription."""
        # En théorie, c'est la date de la première entrée dans l'historique
        if self.historique():
            h = self.historique()[0]
            h = h[:h.find(",")]
            try:
                dateInsc = time.mktime(time.strptime(h, date_format_new))
            except:
                dateInsc = time.mktime(time.strptime(h, date_format))
            return dateInsc
        else:
            # Lors de l'inscription d'un nouvel adhérent, celui-ci n'a pas
            # encore d'historique. On retourne alors la date en cours.
            return time.time()

    def is_nounou( self ):
        return u"Nounou" in self.droits()


class Club(BaseProprietaire):
    """ Classe de définition d'un club """
    idn = 'cid'
    filtre_idn = '(objectClass=club)'
    objectClass = 'club'

    def Nom(self, new=None):
        """ Définit ou retourne le nom du club """
        if new == None:
            return decode(self._data.get('nom', [''])[0])

        l, new = preattr(new)
        new = new.capitalize()
        if l<2:
            raise ValueError(u"Nom trop court.")

        self._set('nom', [new])
        return new

    def etudes(*args):
        """ Etudes bidon pour club """
        return u'N/A'

    def nom(self):
        """ Retourne le nom du club, utilisé lors de la destruction """
        return strip_accents(self.Nom())

    def carteEtudiant(self, pd=None):
        return True

    def responsable(self, adher=None):
        """ Responsable du club, adher doit être une instance de la classe adhérent """
        if adher == None:
            aid = decode(self._data.get('responsable', [''])[0])
            if aid:
                l = self.search('aid=%s' % aid)['adherent']
                if l:
                    return l[0]
                else:
                    raise ValueError("Responsable Invalide (%s)" % aid)
            else: raise ValueError("Pas de responsable enregistré")

        if adher.__class__ != Adherent:
            raise ValueError

        if not adher.id():
            raise AttributeError(u'Adhérent invalide')

        self._set('responsable', [adher.id()])
        return adher

    def imprimeurs(self, ajouter=None, retirer=None):
        """Retourne la liste des aid des personnes autorisees a imprimer avec
        le compte *@club-*, ou ajoute/retire un aid a cette liste."""
        if self._data.has_key('imprimeurClub'):
            liste = list(self._data['imprimeurClub'])
        else:
            liste = []
        if ajouter != None:
            if len(db.search('aid=%s' % ajouter)['adherent']) > 0:
                if ajouter not in liste:
                    liste.append(ajouter)
                    self._set('imprimeurClub', liste)
                    self.modifs['imprimeurClub'] = None
                    return True
                return False
            return False
        elif retirer != None:
            if retirer in liste:
                liste.remove(retirer)
                self._set('imprimeurClub', liste)
                return True
            return False
        return liste

    def chbre(self, new=None):
        """ Définit le local du club
        new doit être une des clefs de l'annuaire locaux_clubs"""
        if new == None:
            return decode(self._data.get('chbre', [''])[0])

        annu = annuaires.locaux_clubs()
        if new not in annu.keys():
            raise ValueError(u'Local invalide', annu)

        self._set('chbre', [new])
        return new

    def local(self):
        """ Retourne le local à partir de la chambre enregistrée et
        de la conversion avec l'annuaire locaux_clubs """
        annu = annuaires.locaux_clubs()
        return decode(annu.get(self.chbre(), ''))

    def compte(self, login=None):
        """ Créé un compte au club sur vert"""
        if login == None:
            return self._data.get('uid', [''])[0]

        # Génération du login : club-<login fourni>
        login = login.lower()
        if not login.startswith('club-'):
            login = 'club-' + login
        if not re.match('^[a-z0-9]*[a-z]+[a-z0-9-]*$', login):
            raise ValueError(u'Login incorrect')
        login = preattr(login)[1]

        if 'posixAccount' in self._data['objectClass']:
            if login != self._data['uid']:
                # A déja un compte
                raise ValueError(u"Le club à déjà un compte. Login : %s" % self._data['uid'][0])
            else:
                return login

        if mailexist(login) and not os.system('/usr/sbin/list_lists | grep -qi %s' % login):
            # la 2ème vérif est pour vérifier que ce n'est pas la ML du club
            raise ValueError(u"Login existant ou correspondant à un alias mail.", 1)

        home = '/home/%s/%s' % (login[0], login.replace('-', '/', 1))
        if os.path.exists(home):
            raise ValueError(u'Création du compte impossible : home existant', 1)

        if os.path.exists("/var/mail/"+login):
            raise ValueError(u'Création du compte impossible : /var/mail/%s existant'%login, 1)

        # Lock du mail
        self.lock('mail', login)

        if not 'compte' in self.modifs:
            self.modifs.setdefault('compte', ',')

        self._data['objectClass'] = ['club', 'cransAccount', 'posixAccount', 'shadowAccount']
        self._data['uid'] = [ login ]
        self._data['cn'] = [ preattr(self.Nom())[1] ]
        self._data['loginShell' ] = [ config.club_login_shell ]

        # Détermination de l'uid
        uidNumber = 1000
        while self.exist('(uidNumber=%s)' % uidNumber):
            uidNumber += 1
        try:
            self.lock('uidNumber', str(uidNumber))
        except:
            # Quelqu'un nous a piqué l'uid que l'on venait de choisir !
            return self.compte(login)

        self._data['uidNumber'] = [ str(uidNumber) ]
        self._data['gidNumber'] = [ str(config.club_gid) ]
        self._data['homeDirectory'] = [ preattr(home)[1] ]

        return decode(login)

    def email(self):
        """ Retourne l'adresse mail du responsable """
        return self.responsable().email()

    def update_connexion(self):
        """Dummy"""
        pass

class Machine(BaseClasseCrans):
    """ Classe de définition d'une machine """
    idn = 'mid'
    filtre_idn = '(|(objectClass=machineFixe)(objectClass=machineWifi)'
    filtre_idn += '(objectClass=machineCrans)(objectClass=borneWifi))'

    def __init__(self, parent_or_tuple, mode='', conn=None):
        """
        parent_or_tuple est :
         * soit une instance d'une classe pouvant posséder une machine
           (Adherent, Club ou AssociationCrans), la nouvelle machine lui
           sera alors associée.
         * soit directement le tuple définissant une machine (tel que
           retourné par les fonctions de recherche ldap)

        Pour l'édition d'une machine, mode devra être égal à 'w'
        Attention, si mode='w' mais si l'objet est déja locké il n'y a
        pas d'erreur, vérifier l'obtention du lock grâce à la valeur de
        _modifiable (si ='w' c'est bon)

        conn est une instance de la classe de connexion à la base LDAP
        """
        # Définition de l'objectClass LDAP à partir du nom de la classe Python
        self.objectClass = str(self.__class__).split('.')[-1]
        self.objectClass = self.objectClass[0].lower() + self.objectClass[1:]

        # Initialisation de la connexion
        self.conn = conn
        if not self.conn:
            self.connect()

        self.modifs = {}
        t = parent_or_tuple.__class__
        if t == tuple:
            # Initialisation avec données fournies
            self.dn = parent_or_tuple[0]
            if mode == 'w':
                try:
                    self.lock(self.idn, self.id())
                    self._modifiable = 'w'
                except EnvironmentError , c:
                    self._modifiable = 0
            else:
                self._modifiable = 0
            # Utile pour construire l'instruction LDAP de modif
            self._init_data = parent_or_tuple[1].copy()
            self._data = parent_or_tuple[1]

            # Propriéraire inconnu mais ce n'est pas grave
            self.__proprietaire = None

        elif t in [Adherent, Club, AssociationCrans] and mode != 'w':
            # Machine vide
            self.__proprietaire = parent_or_tuple
            self.dn = parent_or_tuple.dn
            self._data = {'objectClass': [self.objectClass]}
            self._init_data = {}
            self._modifiable = 'w'

            chbre = self.__proprietaire.chbre()
            #       if chbre == 'EXT' and mode == 'fixe':
            #           raise ValueError(u'Il faut une chambre pour pouvoir posséder une machine fixe')

            if chbre == '????':
                raise ValueError(u'ERREUR: la chambre du propriétaire est inconnue')

        else:
            raise TypeError(u'Arguments invalides')

    def Nom(self):
        """ Retourne le nom de la machine """
        return self.nom()

    def mac(self, mac=None, multi_ok=0, lock=True):
        """
        Défini ou retourne l'adresse mac de la machine
        Adresse valide si:
            12 caractères hexa avec - ou : comme séparateurs
            non nulle
        Stoque l'adresse sous la forme xx:xx:xx:xx:xx:xx
        Si multi_ok = 1 permet d'avoir plusieur fois la même mac sur le réseau
        """

        if mac == None:
            return decode(self._data.get('macAddress', [''])[0])

        mac = format_mac(mac)

        if mac == '<automatique>':
            self._set('macAddress', [mac])
            self.ipv6('')
            return mac

        # La MAC serait-elle une MAC à la con ?
        if mac == "00:04:4b:80:80:03":
            raise ValueError(u"Il s'agit de l'unique adresse MAC achetée par nVidia pour ses cartes réseau. Il faut changer cette adresse.", 2)
        elif mac[0:11] == "44:45:53:54":
            raise ValueError(u"Il s'agit de l'adresse MAC d'une interface PPP.", 2)

        # Le test final : vendeur connu
        prefix = mac[:8].upper() + ' '
        vendor = ''
        try:
            for line in open('/usr/scripts/gestion/ethercodes.dat').readlines():
                if line.startswith(prefix):
                    vendor = line.replace(prefix, '').replace('( )', '').strip()
                    break
        except IOError:
            # Le fichier existe pas, on sort
            raise RuntimeError(u"Fichier de fabriquants de MAC non trouvé !")

        if not multi_ok and not vendor:
            raise ValueError(
                """Le constructeur correspondant à cette adresse MAC ne peut être trouvé.
L'adresse MAC correspond peut-être à un pont réseau, désactivez ce pont réseau.
Contactez nounou si la MAC est bien celle d'une carte.""", 3)

        # La mac serait-elle déjà connue ?
        if not multi_ok and self.exist('macAddress=%s' % mac):
            raise ValueError(u"Mac déjà utilisée sur le réseau.", 1)

        # Lock de la mac
        if lock:
            self.lock('macAddress', mac)
        net = self.netv6()
        try:
            self._set('macAddress', [mac])
            if net.size > 1:
                self.ipv6(ip6tools.mac_to_ipv6(net, mac), lock)
            else:
                self.ipv6(config.ipv6_machines_speciales[int(self.rid())], lock)
        except Exception as e:
            raise ValueError('La correspondance MAC <-> EUID64 a plante. Tapez PEB et son entourage. (net : %s, mac : %s, exception levee : %s)' % (net, mac, e))
        return mac

    def __host_alias(self, champ, new):
        """ Vérification de la validité d'un nom de machine """
        # Supression des accents
        new = strip_accents(unicode(new, 'utf-8'))

        l, new = preattr(new)
        new = new.lower()
        l = len(new.split('.')[0])

        if l<2:
            raise ValueError(u"%s trop court." % champ.capitalize())

        if self.proprietaire().__class__ != AssociationCrans:
            new = new.split('.', 1)[0]
            for c in new[:]:
                if not c in (string.letters + string.digits + '-'):
                    raise ValueError(u"Seuls les caractères alphabétiques minuscules et les - sont autorisés pour %s" % champ)

            if l > 17:
                raise ValueError(u"%s trop long." % champ.capitalize())

        if new.endswith('-'):
            raise ValueError(u"Le dernier caractère du champ %s ne peut être un tiret" % champ)

        # Ajout du domaine si necessaire
        if new.find('.') == -1:
            try:
                new += '.' + config.domains[self.objectClass]
            except:
                raise RuntimeError(u"%s : domaine non trouvé pour %s" % (champ.capitalize(), self.__class__))

        # Pas déja pris ?
        if self.exist('(|(host=%s)(hostAlias=%s))' % (new, new)):
            raise ValueError(u"%s : nom déjà pris" % champ.capitalize())

        # Lock host
        self.lock('host', new)

        return new

    def nom(self, new=None):
        """
        Défini ou retourne le nom de machine.
        Un nom de machine valide ne comporte que des caractères
        alphabétiques minuscules et le -
        """
        if new == None:
            return decode(self._data.get('host', [''])[0])

        new = self.__host_alias('nom de machine', new)
        self._set('host', [new])
        return new.split('.')[0]

    def prise(self, new=None):
        """ Retourne ou défini la prise associée à la machine
        La définition n'est possible que si la  machine est
        une machine de l'assoce.
        Si la prise est inconne retourne N/A
        """
        if new == None:
            if self.proprietaire().__class__ == AssociationCrans:
                return decode(self._data.get('prise', ['N/A'])[0])
            else:
                chbre = self.proprietaire().chbre()
                if chbre and chbre[0].lower() in annuaires.bat_switchs:
                    try:
                        return annuaires.chbre_prises(chbre[0], chbre[1:])
                    except:
                        return 'N/A'

        # Attribution de la prise
        new = preattr(new)[1]
        if new == 'N/A':
            if self._data.has_key('prise'):
                self._set('prise', [])
            return

        if not re.match('^[a-cg-jkmopv][0-7][0-5][0-9]$', new.lower()):
            raise ValueError('Prise incorrecte')

        self._set('prise', [new.upper()])
        return new

    def alias(self, new=None):
        """
        Création ou visualisation des alias d'une machine.
        Même sytème d'argument que la méthode info.
        """
        if not self._data.has_key('hostAlias'):
            self._data['hostAlias'] = []
        liste = list(self._data['hostAlias'])
        if new == None:
            return map(decode, liste)

        if type(new) == list:
            # Modif
            res = self.conn.search_s(self.dn, 1, "xid=*")
            if res:
                raise EnvironmentError("La machine possède des certificats, utilisez lc_ldap ou l'intranet2")
            index = new[0]
            new = new[1]
            if new == '':
                liste.pop(index)
                self._set('hostAlias', liste)
                return liste
        else:
            index = -1

        # Test si valide
        new = self.__host_alias('alias', new)

        if index != -1:
            liste[index] = new
        else:
            liste = liste + [new]

        self._set('hostAlias', liste)
        return liste

    def sshFingerprint(self, new=None):
        """
        Fonction d'ajout d'empreinte SSH
        """

        liste = list(self._data.get('sshFingerprint', []))

        if new == None:
            return liste

        else:
            if type(new) == list:
                # Modif
                index = new[0]
                new = new[1]
                if new == '':
                    # Supression alias
                    liste.pop(index)
                    self._set('sshFingerprint', liste)
                    return liste
            else:
                index = -1

        #if sshexists(new):
        #    raise ValueError(u"FPR existant ou correspondand à un compte.")

        if index != -1:
            liste[index] = new
        else:
            liste = liste + [ new ]

        self._set('sshFingerprint', liste)
        return liste

    def ip(self, ip=None, lock=True, force=False):
        """
        Défini ou retourne l'IP de la machine.
        Les IP sont stoquées sous forme xxx.xxx.xxx.xxx et doivent être fournies ainsi.
        Si l'IP n'est pas définie retourne <automatique>.
        Si ip=<automatique> attribue la permière IP libre du sous-réseau.
        """
        if ip == None:
            if self._data.has_key('ipHostNumber'):
                return decode(self._data['ipHostNumber'][0])
            elif isinstance(self.proprietaire(), AssociationCrans) and not isinstance(self, BorneWifi):
                return ''
            else:
                return '<automatique>'

        l, ip = preattr(ip)

        # Dans quel sous réseau se trouve le rid?
        if isinstance(self, MachineWifi):
            mach_type = u'wifi-adh'
            plage_rid = config.rid_primaires['wifi-adh']
        # Si on ajoute une bornv6, on prend dans la plage v6 only
        elif isinstance(self, BorneWifi):
            mach_type = u'bornes'
            try:
                if self.bornev6:
                    plage_rid = config.rid_primaires['bornes-v6']
            except AttributeError:
                plage_rid = config.rid_primaires['bornes']
        elif isinstance(self.proprietaire(), AssociationCrans):
            mach_type = u'crans'
            plage_rid = config.rid_primaires['serveurs']
        else:
            proprio = self.proprietaire()
            if proprio.etudes(0) == 'Personnel ENS':
                mach_type = u'personnel-ens'
                plage_rid = config.rid_primaires['personnel-ens']
            elif not isinstance(proprio, Adherent) or proprio.adherentPayant():
                proprio_subnet = 'adherents'
                mach_type = 'adherents'
                try:
                    plage_rid = config.rid_primaires[proprio_subnet]
                except:
                    raise RuntimeError(u'Impossible de trouver le réseau où placer la machine.')

        # Le switch-bloc suivant modifie ip pour le faire correspondre à une
        # vraie ip et le rid correspondant. Si ip='', cela signifie que l'on ne
        # veut plus d'ipv4 (cela arrive si on a un rid ipv6 only par ex)

        # Si <automatique> et self.rid(), recalcule l'ip à partir du rid
        if ip == '<automatique>' and self.rid():
            rid = int(self.rid())
            rid_obj = ridtools.Rid(rid)
            if rid_obj.ipv4_dispo:
                ip = unicode(rid_obj.ipv4())
            else:
                ip = ''
        #On essaye d'attribuer un rid si ip auto et pas encore de rid
        elif ip == '<automatique>':
            for plage in plage_rid:
                rid_pris = [
                    int(elem[1]['rid'][0])
                    for elem in self.conn.search_ext_s(self.base_dn, ldap.SCOPE_SUBTREE, "(&(rid>=%s)(rid<=%s))" % (plage[0], plage[1]))
                ]
                # Par defaut la plage est pas v6 (cf bornes/bornes v6)
                for rid in xrange(plage[0], plage[1] + 1):
                    if rid not in rid_pris:
                        # On verifie que l'ip se termine pas par 0 ou 255 et que il y a une ipv4 dispo (on sort si on est dans une plage v6)
                        if not ridtools.Rid(rid=rid).ipv4_dispo:
                            if not force:
                                raise ValueError(u"La machine sera v6 only", 1)
                            ip = ''
                            break
                        # On vire les ip qui se terminent par 0 ou 255 (toutes, thanks windows)
                        if rid % 256 != 0 and rid % 256 != 255:
                            ip = unicode(ridtools.Rid(rid=rid).ipv4())
                            break
                # Si on a une ip, on s'en va
                if ip != '<automatique>':
                    break

            # Si après tout ca, on a encore auto, c'est qu'il n'y a plus d'ip
            if ip == '<automatique>':
                raise RuntimeError(u"Plus d'IP (rid) libres dans %s." % ' et '.join(net))

        # Sinon, ip fournie, contrôle qu'elle est compatible avec le type machine
        # et calcule le rid qui va bien avec
        elif ip != '':
            # L'ip est elle dans le bon sous-réseau ?
            # (accessoirement teste si l'IP est valide et ne correspond pas
            # à l'adresse de broadcast ou de réseau)
            rid = ridtools.Rid(ipv4=ip).rid
            if not mach_type==u'crans' and not unicode(ridtools.find_rid_plage(rid)[0])==mach_type :
                raise ValueError(u'IP invalide ou en dehors du sous-réseau alloué.', 1)
            # Reformatage
            ip = iptools.DecToQuad(iptools.QuadToDec(ip))
            # L'ip est-elle déja allouée ?
            if self.exist('ipHostNumber=%s' % ip):
                raise ValueError(u'IP déjà prise.')
        else: # cas où ip == ''
            if not self.rid():
                raise ValueError(u'Vous devez indiquer une ip ou <automatique>')
            rid = int(self.rid())

        # Après ce bloc, rid et ip ont été fixés
        # on calcule maintenant les modifs à lancer
        old_ip = (self._data.get('ipHostNumber', None) or [''])[0]

        maj_ip = ip != old_ip
        maj_rid = self.rid() != str(rid)

        # Et on fait les modifs, ip d'abord, car self.rid() pourrait
        # faire des tests supplémentaires
        if maj_ip:
            # Lock ip
            if lock and ip:
                self.lock('ipHostNumber', ip)
            self._set('ipHostNumber', [ip] if ip else [])

        if maj_rid:
            self.rid(str(rid), lock=lock)

        return ip

    def rid(self, rid=None, lock=True):
        """
        Retourne le rid si rid == None, l'affecte sinon.
        """
        if rid == None:
            return self._data.get('rid', [''])[0]

        if self.ip() not in ['<automatique>', '']:
            rid_t = ridtools.Rid(ipv4=self.ip())
            if int(rid_t) != int(rid):
                raise ValueError('L\'ip et le rid doivent correspondre. rid : %s, ip : %s, rid_t : %s' % (int(rid), self.ip(), rid_t))

        if lock:
            self.lock('rid', str(rid))
        self._set('rid', [str(rid)])

    def exempt(self, new=None):
        """
        Liste des réseaux vers lesquels on ne compte pas l'upload
        Cette liste est transférée dans la base postgres du serveur de comptage
        (currently odlyd)
        Pour ajouter un réseau new doit être la chaîne
        représentant le réseau à ajouter
        Pour modifier new doit être une liste de la forme :
             [ index du nouveau réseau , nouveau réseau ]
        l'index est celui obtenu dans la liste retournée par exempt()
        """
        if not self._data.has_key('exempt'):
            self._data['exempt'] = []
        liste = list(self._data['exempt'])
        if new == None: return map(decode, liste)

        if type(new) == list:
            # Modif
            index = new[0]
            l, new = preattr(new[1])
            if not new:
                # Supression réseau
                liste.pop(index)
            else:
                # Modif remarque
                liste[index] = new
        elif type(new) == str:
            # Réseau supplémentaire
            l, new = preattr(new)
            if not new:
                # On ajoute pas de réseau vide
                return liste
            # Ajout à la liste
            liste = liste + [new]
        else:
            raise TypeError

        self._set('exempt', liste)
        return liste

    def proprietaire(self):
        """
        retroune le propriétaire de la machine (classe Adherent, Club ou AssociationCrans)
        """
        if not self.__proprietaire:
            res = self.conn.search_s(','.join(self.dn.split(',')[1:]), 0)
            if 'adherent' in res[0][1]['objectClass']:
                self.__proprietaire = Adherent(res[0], self._modifiable, self.conn)
            elif 'club' in res[0][1]['objectClass']:
                self.__proprietaire = Club(res[0], self._modifiable, self.conn)
            else:
                self.__proprietaire = AssociationCrans(conn = self.conn)

        return self.__proprietaire

    def save(self):
        """
        Enregistre la machine courante dans la base LDAP
        Retourne une chaîne indiquant les opération effectuées.
        """
        if self.proprietaire().__class__ == AssociationCrans and not (isadm() or user_tests.getuser() == 'www-data'):
            raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')

        ret = ''

        # test si la machine a des certificats
        if 'host' in self.modifs:
            res = self.conn.search_s(self.dn, 1, "xid=*")
            if res:
                raise EnvironmentError("La machine possède des certificats, utilisez lc_ldap ou l'intranet2")

        # Besoin de redémarrer les firewalls ?
        if 'ipHostNumber' in self.modifs or 'macAddress' in self.modifs:
            reconf_ip = self._init_data.get('ipHostNumber', [])
            reconf_ip += self._data.get('ipHostNumber', [])
        else:
            reconf_ip = []

        # On vire les doublons dans reconf_ip
        reconf_ip = list(dict(zip(reconf_ip, [None]*len(reconf_ip))))

        # Si la configuration ip à changer on met dynamiquement à jour le dhcp
        if reconf_ip:
            from gen_confs.dhcpd_new import dydhcp
            for server in config.dhcp_servers:
                try:
                    dhcp=dydhcp(server)
                    dhcp.del_host(self._init_data.get('ipHostNumber',[self.ip()])[0],self._init_data.get('macAddress',[self.mac()])[0])
                    dhcp.add_host(self._data.get('ipHostNumber',[self.ip()])[0],self._data.get('macAddress',[self.mac()])[0],self.nom())
                except socket.error:
                    sys.stderr.write((u'Dhcp generate failed for %s\n' % server).encode('utf-8'))
        # Enregistrement
        self._save()

        # Si le proprio ne paie pas de cotisation, il n'y a pas de services à
        # redémarrer (config automatique pour le vlan inscriptions)
        if not isinstance(self.proprietaire(), Adherent) or self.proprietaire().adherentPayant():

            # Clef IPsec
            if 'ipsec' in self.modifs:
                ret += coul(u'Clef WiFi de la machine : %s\n' % self.ipsec(), 'cyan')

            # Reconfiguration firewalls et dhcps
            if reconf_ip:
                self.services_to_restart('macip', reconf_ip)
                #self.services_to_restart('classify', reconf_ip)
                self.services_to_restart('dhcp')
            if 'portTCPin' in self.modifs or 'portTCPout' in self.modifs or \
               'portUDPin' in self.modifs or 'portUDPout' in self.modifs:
                self.services_to_restart('odlyd-ports', [self.ip()])
                self.services_to_restart('mail_modif', ['ip=%s' % self.ip()])

            # Reconfiguration DNS ?
            if 'host' in self.modifs or 'ipHostNumber' in self.modifs or 'hostAlias' in self.modifs or 'dnsIpv6' in self.modifs or 'sshFingerprint' in self.modifs:
                self.services_to_restart('dns')

            # Regénération blackliste nécessaire ?
            bl = self.blacklist_actif()
            if bl and ('ipHostNumber' in self.modifs or 'host' in self.modifs):
                for s in bl:
                    self.services_to_restart(s, [self.ip()])

            # Regénération de l'autostatus et mail de changmement ?
            if self.proprietaire().__class__ == AssociationCrans:
                self.services_to_restart('autostatus')
                self.services_to_restart('mail_modif', ['ip=%s' % self.ip()])

            # Synchronisation avec la base pgsql pour les exemptions
            if 'exempt' in self.modifs or ('ipHostNumber' in self.modifs and self.exempt()):
                self.services_to_restart('filtrage_exemptions')

            if 'exempt' in self.modifs:
                self.services_to_restart('mail_modif', ['ip=%s' % self.ip()])

            # Synchronisation avec la base pgsql pour la liste des machines
            if 'ipHostNumber' in self.modifs:
                self.services_to_restart('filtrage_machines')

        # Remise à zéro
        self.modifs = {}

        # Message de sortie
        ret += coul(u"Machine %s enregistrée avec succès." % self._data['host'][0], 'vert')

        return ret

    def delete(self, comment=''):
        """ Destruction de la machine"""
        if self.proprietaire().__class__ == AssociationCrans and not isadm():
            raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')

        res = self.conn.search_s(self.dn, 1, "xid=*")
        if res:
            raise EnvironmentError("La machine possède des certificats, utilisez lc_ldap ou l'intranet2")

        self.proprio = self.__proprietaire.Nom() # On met dans un coin le nom du proprio
        self.__proprietaire = None               # On oublie le propriétaire
        self._delete(self.dn, comment)

        # On supprime la machine du dhcp
        from gen_confs.dhcpd_new import dydhcp
        for server in config.dhcp_servers:
            try:
                dhcp=dydhcp(server)
                dhcp.del_host(self.ip(),self.mac())
            except socket.error:
                sys.stderr.write((u'Dhcp generate failed for %s\n' % server).encode('utf-8'))

        # Services à redémarrer
        self.services_to_restart('dhcp')

        if self.exempt():
            self.services_to_restart('filtrage_exemptions')
        self.services_to_restart('filtrage_machines')

        self.services_to_restart('dns')
        self.services_to_restart('macip', [self.ip()])
        #self.services_to_restart('classify', [self.ip()])

    def portTCPin(self, ports=None):
        """ Ports TCP ouverts depuis l'extérieur pour la machine """
        return self.__port(ports, 'portTCPin')

    def portTCPout(self, ports=None):
        """ Ports TCP ouverts vers l'extérieur pour la machine """
        return  self.__port(ports, 'portTCPout')

    def portUDPin(self, ports=None):
        """ Ports UDP ouverts vers l'extérieur pour la machine """
        return self.__port(ports, 'portUDPin')

    def portUDPout(self, ports=None):
        """ Ports UDP ouverts vers l'extérieur pour la machine """
        return self.__port(ports, 'portUDPout')

    def __port(self, ports, champ):
        if ports == None:
            return self._data.get(champ, [])

        # Les ports doivent être de la forme [port][:[port]]. On met
        # la spécification sous une forme qui donne un tri intéressant
        def parse(x):
            try:
                liste = x.split(':')
                assert(len(liste) == 1 or len(liste) == 2)
                return map(lambda x: x and int(x) or '', liste)
            except:
                raise ValueError(u'Spécification de ports incorrecte : %s' % x)

        ports = map(parse, ports)
        ports.sort()
        self._set(champ, map(lambda x: ':'.join(map(str, x)), ports))

    def dnsIpv6(self, dnsIpv6 = None):
        """Accès au champ DNS IPv6"""
        if dnsIpv6 == True:
            self._set('dnsIpv6', ['TRUE'])
        elif dnsIpv6 == False:
            self._set('dnsIpv6', [])
        elif dnsIpv6 != None:
            raise ValueError, u"dnsIpv6 prend un booléen comme argument"

        # renvoie la valeur trouvée dans la base
        return bool(self._data.get('dnsIpv6', []))

    def machineAlias(self, machineAlias = None):
        """Accès au champ DNS IPv6"""
        if machineAlias == True:
            self._set('machineAlias', ['TRUE'])
        elif machineAlias == False:
            self._set('machineAlias', [])
        elif machineAlias != None:
            raise ValueError, u"machineAlias prend un booléen comme argument"

        # renvoie la valeur trouvée dans la base
        return bool(self._data.get('machineAlias', []))

    def netv6(self):
        """Retourne le réseau IPv6 Cr@ns associé à la machine"""
        nettype, _ = ridtools.find_rid_plage(int(self.rid()))

        if nettype != 'special':
            return netaddr.IPNetwork(config.prefix[nettype][0])
        else:
            return netaddr.IPNetwork(config.ipv6_machines_speciales[int(self.rid())])

    def ipv6(self, ipv6=None, lock=True):
        """Retourne l'adresse IPv6 correspondant à la machine"""

        if ipv6 == None:
            if self._data.get('ip6HostNumber', []) == []:
                return None
            else:
                return netaddr.IPAddress(self._data.get('ip6HostNumber')[0])

        if ipv6 == '':
            self._set('ip6HostNumber', [])
            return

        ipv6 = str(ipv6)
        net = self.netv6()

        if net.size == 1:
            ipv6_t = config.ipv6_machines_speciales[int(self.rid())]
        else:
            ipv6_t = str(ip6tools.mac_to_ipv6(net, netaddr.EUI(self.mac())))
        if ipv6 != ipv6_t:
            raise ValueError('L\'ipv6 n\'est pas celle EUID64 attendue. %s, %s' % (ipv6, ipv6_t))

        if lock:
            self.lock('ip6HostNumber', ipv6)
        self._set('ip6HostNumber', [ipv6])

    def nom6(self):
        """Retourne le nom "ipv6" de la machine"""
        if self.dnsIpv6():
            return self.nom()
        else:
            s = self.nom().split('.')
            s.insert(1, 'v6')
            return '.'.join(s)

    def __hash__(self):
        """Retourne un hash de l'objet Machine"""
        return hash(self.nom())

class MachineFixe(Machine):
    """ Classe de définition d'une machine fixe """

    def __init__(self, parent_or_tuple, typ='fixe', conn=None):
        Machine.__init__(self, parent_or_tuple, typ, conn)


class MachineWifi(Machine):
    """ Classe de définition d'une machine wifi """

    def __init__(self, parent_or_tuple, typ='wifi', conn=None):
        Machine.__init__(self, parent_or_tuple, typ, conn)
        if not isinstance(parent_or_tuple, tuple):
            # Initialisaton d'une nouvelle machine wifi
            self.ipsec(True)

    def ipsec(self, clef=None):
        """
        Affichage (clef=None), génération (clef=True) ou définition de la clef WiFi (anciennement IPsec)
        de la machine. Si clef différent de True et None: prend la clef fournie.
        """
        if clef == None:
            return decode(self._data.get('ipsec', [''])[0])

        if clef == True:
            # Génération
            clef = ''
            for i in range(10):
                clef += random.choice(filter(lambda x: x != 'l' and x != 'o', string.lowercase) +
                                      filter(lambda x: x != '1' and x != '0', string.digits))

        self._set('ipsec', [clef])

        return clef


class MachineCrans(Machine):
    """ Classe de définition d'une machine du Crans """

    def __init__(self, parent_or_tuple, typ='fixe', conn=None):
        Machine.__init__(self, parent_or_tuple, typ, conn)

    def nombrePrises(self, new=None):
        """ Nombre de prises, pour les switchs """
        if not new:
            return int(self._data.get('nombrePrises', [-1])[0])

        try:
            new = int(new)
        except:
            raise ValueError(u'Le nombre de prises doit être un entier')

        vals = [24, 26, 28, 50, 52]
        if new not in vals:
            raise ValueError('Le nombre de prises doit etre %s ou %d' % (
                    ', '.join(str(val) for val in vals[:-1]), vals[-1]))

        self._set('nombrePrises', [str(new)])
        return new

class BorneWifi(Machine):
    """Classe de définition d'une borne wifi"""

    def __init__(self, parent_or_tuple, typ='borne', conn=None):
        Machine.__init__(self, parent_or_tuple, typ, conn)
        if not isinstance(parent_or_tuple, tuple):
            # Initialisaton d'une nouvelle borne wifi
            self._data['canal'] = ['2047']
            self._data['puissance'] = ['60']
            self._data['hotspot'] = ['FALSE']

    def mac2(self):
        """Retourne l'adresse MAC + 2"""
        mac = self.mac().split(":")
        mac[-1] = "%0.2x" % (int(mac[-1], 16) + 2)
        return ":".join(mac)

    def hotspot(self, new=None):
        """Cette borne est-elle partagée avec l'ENS ?"""
        if new == None:
            # Le schéma nous assure que ce champ existe toujours
            return self._data['hotspot'][0] == "TRUE"
        else:
            if new:
                self._set('hotspot', ['TRUE'])
            else:
                self._set('hotspot', ['FALSE'])

    def canal(self, new=None, raw=False):
        """Attribution ou visualisation du canal d'une borne wifi"""
        if not new:
            canaux = self._data.get('canal', [''])[0]
            if raw:
                return canaux
            else:
                canaux = int(canaux)
                if canaux < 14:
                    # Compatibilité ascendante
                    return str(canaux)
                lcanal1 = []
                for i in range(1, 14):
                    found = False
                    if canaux & (1 << (i - 1)):
                        lcanal2 = []
                        for c in lcanal1:
                            if c[1] == i - 1:
                                lcanal2.append((c[0], i))
                                found = True
                            else:
                                lcanal2.append(c)
                        if not found:
                            lcanal2.append((i, i))
                        lcanal1 = lcanal2
                lcanal3 = []
                for c in lcanal1:
                    if c[0] == c[1]:
                        lcanal3.append("%d" % c[0])
                    else:
                        lcanal3.append("%d-%d" % (c[0], c[1]))
                return ",".join(lcanal3)


        try:
            new = int(new)
            if new < 0 or new > 13: raise
        except:
            # Nouveau système, il doit s'agir d'une liste de canaux
            try:
                lcanal3 = str(new).split(",")
                lcanal = []
                for c in lcanal3:
                    c2 = c.split("-")
                    if len(c2) == 1:
                        lcanal.append(int(c2[0]))
                    else:
                        for i in range(int(c2[0]), int(c2[1]) + 1):
                            lcanal.append(i)
                new = 0
                for c in lcanal:
                    if c not in range(0, 14):
                        raise
                    new = new + (1 << (c - 1))
            except:
                raise ValueError(u'Canal invalide : doit être entre 0 et 13 ou une liste de canaux')

        self._set('canal', [str(new)])
        return new

    def puissance(self, new=None):
        """Attribution ou visualisation de la puissance d'une borne wifi"""
        if not new:
            return self._data.get('puissance', [''])[0]

        try:
            new = int(new)
            if new < -99 or new > 99: raise
        except:
            raise ValueError(u'Puissance invalide : doit être entre -99 et 99')

        self._set('puissance', [str(new)])
        return new

    def position(self, new=False):
        """
        Attribution ou visualisation de la position d'une borne wifi.
        Renvoie un couple de coordonnées (en strings) si elles existent,
        None sinon. new doit être un couple de coordonnées (en strings),
        None (pour enlever les coordonnées) ou False (retourne les valeurs
        actuelles).
        On utilise des strings plutôt que des floats à cause de la précision.
        """
        if new == False:
            valeur = self._data.get('positionBorne', [''])[0]
            if valeur:
                return tuple(valeur.split(' '))
            else:
                return None

        elif new == None:
            self._set('positionBorne', [])
            return None

        else:
            self._set('positionBorne', [' '.join(new)])
            return new

    def nvram(self, champ=None, new=False):
        """
        Définit et renvoie un champ nvram d'une borne wifi.
        Si champ=None, renvoie sous forme de liste key=value tous
        les champs. On peut aussi définir tous les champs en
        passant dans new la liste.
        Sinon, définit et renvoie la valeur du champ indiqué.
        Le champ est juste retourné, supprimé ou modifié selon
        que new vaut False, None ou autre chose.
        """
        current = self._data.get('nvram', [])[:]

        if champ == None:
            if type(new) is list:
                self._set('nvram', new)
                return new
            else:
                return current

        # Recherche l'index du champ
        index = None
        for i in range(len(current)):
            current_champ = current[i]
            if current_champ.startswith(champ):
                sep_index = current_champ.find('=') + 1
                if sep_index > 0:
                    current_champ = current_champ[sep_index:]
                index = i
                break

        if new == False:
            return index != None and current_champ or None

        elif new == None:
            if index != None:
                del current[index]
                self._set('nvram', current)
            return None

        else:
            if index == None:
                current.append("%s=%s" % (champ, new))
            else:
                current[index] = "%s=%s" % (champ, new)
            self._set('nvram', current)
            return new

class Facture(BaseClasseCrans):
    """ Classe de définition d'une facture """
    objectClass = 'facture'
    idn = 'fid'
    filtre_idn = '(objectClass=facture)'

    def __init__(self, parent_or_tuple, mode='', conn=None):
        """
        parent_or_tuple est :
         * soit une instance d'une classe pouvant posséder une facture
           (Adherent, Club), la nouvelle facture lui sera alors associée.
         * soit directement le tuple définissant une facture (tel que
           retourné par les fonctions de recherche ldap)

        Pour l'édition d'une facture, mode devra être égal à 'w'
        Attention, si mode='w' mais si l'objet est déja locké il n'y a
        pas d'erreur, vérifier l'obtention du lock grâce à la valeur de
        _modifiable (si ='w' c'est bon)

        conn est une instance de la classe de connexion à la base LDAP
        """

        # Initialisation de la connexion
        self.conn = conn
        if not self.conn:
            self.connect()

        self.modifs = {}
        t = parent_or_tuple.__class__
        if t == tuple:
            # Initialisation avec données fournies
            self.dn = parent_or_tuple[0]
            if mode == 'w':
                try:
                    self.lock(self.idn, self.id())
                    self._modifiable = 'w'
                except EnvironmentError , c:
                    self._modifiable = 0
            else:
                self._modifiable = 0
            # Utile pour construire l'instruction LDAP de modif
            self._init_data = parent_or_tuple[1].copy()
            self._data = parent_or_tuple[1]

            # Propriéraire inconnu mais ce n'est pas grave
            self.__proprietaire = None

        elif t in [Adherent, Club] and mode != 'w':
            # Facture vide
            self.__proprietaire = parent_or_tuple
            self.dn = parent_or_tuple.dn
            self._data = {'objectClass': [self.objectClass], 'modePaiement':['']}
            self._init_data = {}
            self._modifiable = 'w'

        else:
            raise TypeError(u'Arguments invalides')

    def numero(self):
        """ Retourne le numéro de facture """
        fid = self._data.get('fid', [None])[0]
        if fid == None:
            raise NotImplementedError, u'Il faut enregistrer une facture pour connaitre son numero'
        return fid

    def nom(self):
        """ Utilisé pour la fonction delete() """
        return "Facture%s" % self.numero()

    Nom = nom

    def proprietaire(self):
        """
        retroune le propriétaire de la facture (classe Adherent ou Club)
        """
        # si la facture est en mode w mais pas le proprio, on tente de prendre
        # le proprio en w
        if self.__proprietaire and self.__proprietaire._modifiable != self._modifiable:
            self.__proprietaire = None

        # récupère le proprio si ce n'est pas encore fait
        if not self.__proprietaire:
            res = self.conn.search_s(','.join(self.dn.split(',')[1:]), 0)
            if 'adherent' in res[0][1]['objectClass']:
                self.__proprietaire = Adherent(res[0], self._modifiable, self.conn)
            elif 'club' in res[0][1]['objectClass']:
                self.__proprietaire = Club(res[0], self._modifiable, self.conn)
            else:
                raise ValueError, u'Propriétaire inconnu'

        return self.__proprietaire

    def modePaiement(self, new=None):
        """
        Définit ou retourne le mode de paiement.
        Le mode de paiement doit être une chaine de caractère
        """

        # modification du mode de paiement
        if new != None:
            if self.recuPaiement():
                raise ValueError, u'Facture déja payée'

            if not self._modifiable:
                raise NotImplementedError, "La facture n'est pas modifiable"

            if new not in config.modePaiement:
                raise ValueError, u'Mode de paiement non accepté'

            self._set('modePaiement', [new])

        return decode(self._data.get('modePaiement', [''])[0])

    def recuPaiement(self, new=None):
        """
        Définit ou retourne qui a recu le paiement
        """

        # on vérifie que la facture n'est pas déja payéee
        if new and self._data.get('recuPaiement', []):
            raise ValueError, u'Facture déja payée'

        # modification de la valeur
        if new is not None and new != False:
            # on vérifie que la facture est modifiable
            if not self._modifiable and new:
                raise NotImplementedError, "La facture n'est pas modifiable"

            # on crédite les articles, si c'est pas possible, la metode
            # levera une exeption
            self._crediter()

            # modifie la base ldap
            self._set("recuPaiement", [new])
        elif new == False:
            # on vérifie que la facture est modifiable
            if not self._modifiable and new:
                raise NotImplementedError, "La facture n'est pas modifiable"

            # on crédite les articles, si c'est pas possible, la metode
            # levera une exeption
            self._vider()

            # modifie la base ldap
            self._set("recuPaiement", [])

        # renvoie la valeur trouvée dans la base
        return self._data.get("recuPaiement", [None])[0]

    def controle(self, action=None):
        if action is None:
            return self._data.get("controle", [''])[0]
        else:
            if action == True:
                self._set("controle", ["TRUE"])
            elif action == False:
                self._set("controle", ["FALSE"])
            elif action == "":
                self._set("controle", [])
            else:
                raise ValueError("Mauvaise valeur pour l'attribut controle : %r" % (repr(action),))

    def _del_recu_paiement(self):
        """ Pour test """
        self._set("recuPaiement", [])

    def _crediter(self):
        """
        Crédite les articles à son propriétaire
        """

        # si la facture n'existe pas encore, on la sauve pour générer un numéro
        if not self._data.has_key('fid'):
            self.save()

        # on vérifie que le propriétaire est modifiable
        if not self.proprietaire()._modifiable:
            raise SystemError, u"Impossible de créditer les articles, le proprietaire n'est pas modifiable"

        # on crédite les articles
        for art in self._articles():
            # solde impression (on débite d'abord si jamais quelqu'un s'amuse à recharger son solde avec son solde)
            if art["code"] == "SOLDE":
                proprio = self.proprietaire()
                proprio.solde(operation=art['nombre']*art["pu"],
                    comment="Facture n°%s : %s" %
                        (self.numero(),
                         art['designation'].encode(config.ldap_encoding, errors='ignore')))
                proprio.save()
        if self.modePaiement() == 'solde':
            proprio = self.proprietaire()
            proprio.solde(operation=0.0 - self.total(), comment="Facture n°%s" % self.numero())
            proprio.save()

    def _vider(self):
        """
        Retire les articles à son propriétaire
        """

        # on vérifie que le propriétaire est modifiable
        if not self.proprietaire()._modifiable:
            raise SystemError, u"Impossible de créditer les articles, le proprietaire n'est pas modifiable"

        # on crédite les articles
        for art in self._articles():
            # solde impression (on débite d'abord si jamais quelqu'un s'amuse à recharger son solde avec son solde)
            if art["code"] == "SOLDE":
                proprio = self.proprietaire()
                proprio.solde(operation=-art['nombre']*art["pu"], comment="Revert facture n°%s : %s" % (self.numero(), art['designation']))
                proprio.save()
            if art["code"] == "FRAIS":
                self.supprime(art)
        if self.modePaiement() == 'solde':
            proprio = self.proprietaire()
            proprio.solde(operation=0.0 + self.total(), comment="Revert facture n°%s" % self.numero())
            proprio.save()

    def _frais(self):
        """
        Retourne une liste d'articles correspondants aux divers frais
        Facturer les frais étant illicite (article L112-12 du code monétaire et financier),
        on droppe cette fonctionnalité. La fonction ne reste que pour des raisons de rétro-compatibilité.
        """

        return []

    def _articles(self, arts = None):
        """Retourne ou modifie la liste des articles de la base"""

        # modifie la liste des articles
        if arts != None:
            self._set('article',
                      ['%s~~%s~~%s~~%s' % (
                        art['code'],
                        art['designation'].decode(config.in_encoding, errors='replace'),
                        str(art['nombre']),
                        str(art['pu']))
                        for art in arts])

        # charge la liste des articles
        arts = []
        for art in self._data.get("article", []):
            art = art.split('~~')
            art = { 'code'        : art[0],
                    'designation' : art[1].decode(config.ldap_encoding, errors='replace'),
                    'nombre'      : int(art[2]),
                    'pu'          : float(art[3]) }
            arts.append(art)

        return arts

    def remise(self, amount, reason):
        """Effectue une remise du montant annoncé. Cela crée un article
        explicite annonçant qu'il y a une remise. Une remise ne peut
        jamais excéder le montant total actuel de la facture."""

        amount = min(amount, self.total())
        self.ajoute({
            'nombre': 1,
            'code': 'REMISE',
            'designation': u'Remise : %s' % (reason,),
            'pu': -amount,
        })

    def ajoute(self, ajoute):
        """Ajoute un/des article(s) à la facture
        ajoute est un article ou une liste d'articles
        """
        # on ne eut pas modifier une facture payée
        if self.recuPaiement():
            raise ValueError, u'On ne peut pas modifier une facture payée'

        # charge la liste des articles
        arts = self._articles()

        # ajoute les articles
        if type(ajoute)==dict:
            ajoute = [ajoute]

        if type(ajoute)==list:
            for art in ajoute:
                if int(art['nombre']) != float(art['nombre']):
                    raise ValueError, u'nombre doit être un entier'
                if round(art['pu'], 2) != art['pu']:
                    raise ValueError, u'pu ne doit pas avoir plus de 2 chiffres apres la virgule'
                art['nombre'] = int(art['nombre'])
                if '~~' in art['designation'].decode(config.in_encoding):
                    raise ValueError, u'Ne pas mettre de ~~ dans les champs'
                arts.append(art)

        # enregistre la nouvelle liste
        self._articles(arts)

    def supprime(self, supprime=None, pop=None):
        """Supprime un/des article(s) à la facture
        arts est un article ou une liste d'articles
        """
        # on ne eut pas modifier une facture payée
        if self.recuPaiement():
            raise ValueError, u'On ne peut pas modifier une facture payée'

        # charge la liste des articles
        arts = self._articles()

        if pop is not None:
            _ = arts.pop()

        # on supprime les anciens articles
        if type(supprime) == dict:
            supprime = [supprime]
        if type(supprime) == list:
            for art in supprime:
                arts.remove(art)

        # enregistre la nouvelle liste
        self._articles(arts)

    def articles(self):
        """
        Retourne la liste des articles.
        Un article est un dictionnaire de la forme :
            { 'code'        : string,
              'designation' : unicode,
              'nombre'      : int,
              'pu'          : int/float }
        """
        return self._articles()

    def total(self):
        """
        Calcule le total de la facture.
        """
        s = 0
        for art in self.articles():
            s += art['nombre'] * art['pu']
        return s

    def urlPaypal(self, useSandbox = False, businessMail = "paypal@crans.org",
                  return_page=None, cancel_return_page=None):
        """
        Retourne l'url paypal pour le paiement de cette facture
        """
        if useSandbox:
            url  = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_cart"
        else:
            url  = "https://www.paypal.com/cgi-bin/webscr?cmd=_cart"
        url += "&upload=1"
        url += "&business=%s" % businessMail
        url += "&currency_code=EUR"
        url += "&no_shipping=1"
        url += "&no_note=1"
        if return_page != None:
            url += "&return=%s" % return_page
        if cancel_return_page != None:
            url += "&cancel_return=%s" % cancel_return_page
        url += "&invoice="+self.numero() # num de facture

        item_id = 0
        for item in self.articles():
            item_id += 1
            url += "&item_name_%d=%s" % (item_id, item['designation'].encode('ascii', errors='replace'))
            url += "&amount_%d=%s" % (item_id, item['pu'])
            url += "&quantity_%d=%s" % (item_id, int(item['nombre']))

        return url

    def save(self):
        """
        Enregistre la facture dans la base LDAP
        Retourne une chaîne indiquant les opération effectuées.
        """
        # Enregistrement
        self._save()
        # Remise à zéro
        self.modifs = {}
        # Message de sortie
        return coul(u"Facture n°%s enregistrée avec succès." % self.numero(), 'vert')

    def delete(self, comment=''):
        """Suppression de la facture"""

        if self.controle():
            if max(self.proprietaire().connexion(), self.proprietaire().adhesion()) + cotisation.del_post_adh >= time.time():
                raise EnvironmentError(u"La facture a déjà été controlée, contacter trésorerie")

        self.__proprietaire = None
        self._delete(self.dn, comment)

class _FakeProprio(CransLdap):
    """Définitions de base d'un propriétaire virtuel"""
    idn = ''
    def __init__(self, conn=None):
        self.conn = conn
        if not self.conn:
            self.connect()
        self.dn = self.base_dn

    def id(self):
        return ''

    def blacklist(self, new=None):
        if new is not None:
            print >>sys.stderr, "Tentative d'ajout de blacklist à un propriétaire virtuel :"
            try:
                print >>sys.stderr, "Nom : %s" % self.Nom()
            except AttributeError:
                print >>sys.stderr, "Impossible de récupérer le nom..."
            print >>sys.stderr, "Blacklist :"
            print >>sys.stderr, new
            raise ValueError("Impossible de blacklister %r" % str(self))
        return []

    def carteEtudiant(self):
        return True

    def blacklist_actif(self):
        return []

    def mail(self, new=None):
        return 'roots@crans.org'

    def machines(self):
        res = self.conn.search_s(self.dn, 1, Machine.filtre_idn)
        m = []
        for r in res:
            m.append(self.make(r))
        return m

    def adhesion(self):
        return time.time() + 86400

    def connexion(self):
        return time.time() + 86400

class AssociationCrans(_FakeProprio):
    """ Classe définissant l'assoce (pour affichage de ses machines) """
    def __init__(self, conn=None):
        _FakeProprio.__init__(self, conn)
    def nom(self):
        return "Crans"
    def Nom(self):
        return "Crans"
    def chbre(self):
        return "CRA"
    def email(self):
        return "roots@crans.org"

    def paiement_ok(self):
        """b2moo: why not. Des fos gest_crans plante dessus"""
        return True


db = None
def crans_ldap(readonly=False):
    """ Renvoie un objet CransLdap """
    global db
    if readonly:
        return CransLdap(readonly)
    if db == None:
        db = CransLdap()
    return db

def pid_exists(pid):
    """Check whether pid exists in the current process table.
    Credits:
    http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
    """
    if pid < 0:
        return False
    try:
        os.kill(pid, 0)
    except OSError, e:
        return e.errno == errno.EPERM
    else:
        return True

if __name__ == '__main__':
    import sys

    usage = u"""Usage %s [ACTION]
    --lock       : donne la liste des locks actifs
    --purgelock  : supprime tous les locks de la base LDAP
    --zombielock : supprime les locks de processus plantés sur ce serveur""" % sys.argv[0]

    if len(sys.argv) != 2:
        cprint(usage)
        sys.exit(1)

    elif '--lock' in sys.argv:
        cprint(u"Liste des locks")
        for lock in crans_ldap().list_locks():
            print "%s\t %s" % (lock[1]["lockid"][0], lock[0].split(',')[0])

    elif '--purgelock' in sys.argv:
        cprint(u"Suppression de tous les locks")
        crans_ldap().remove_lock('*')

    elif '--zombielock' in sys.argv:
        cprint(u"Suppression des locks zombies")
        for (dn, attrs) in crans_ldap().list_locks():
            [lockhost, pid] = attrs['lockid'][0].split('-')[0:2]
            if hostname == lockhost and not pid_exists(int(pid)):
                cprint(u"Suppresion de %s" % repr(attrs))
                crans_ldap().remove_lock(dn)

## CASSÉ SUPPRIME LES MACHINES CRANS !!!!!!!!!!
#    elif '--menage' in sys.argv:
#        cprint(u"Ménage des machines des adhérents partis...")
#        machines = crans_ldap().search('paiement!=%s&host=*.crans.org' % ann_scol , 'w')['machine']
#        cprint(u"Destruction de %i machines" % len(machines))
#        for m in machines:
#            cprint(u'Destruction de %s' % m.nom())
#            m.delete('Ménage')

    else:
        cprint(usage)
        sys.exit(1)
