# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """Site ScoDoc pour plusieurs departements: gestion de l'installation et des creation de départements. Chaque departement est géré par un ZScolar sous ZScoDoc. """ import time, string, glob, re, inspect import urllib, urllib2, cgi, xml import datetime try: from cStringIO import StringIO except: from StringIO import StringIO from zipfile import ZipFile import os.path, glob import traceback from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header from email.mime.base import MIMEBase from sco_zope import ( ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit, ClassSecurityInfo, DTMLFile, Globals, ) # try: import Products.ZPsycopgDA.DA as ZopeDA except: import ZPsycopgDA.DA as ZopeDA # interp.py import sco_utils from sco_utils import ( SCO_DEFAULT_SQL_USERS_CNX, SCO_ENCODING, CUSTOM_HTML_HEADER_CNX, SCO_USERS_LIST, SCO_WEBSITE, SCO_EXC_MAIL, SCO_DEV_MAIL, scodoc_html2txt, VERSION, SCODOC_CFG_DIR, ) from sco_permissions import ScoView, ScoSuperAdmin from sco_exceptions import AccessDenied from notes_log import log import sco_find_etud from ZScoUsers import pwdFascistCheck class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit): "ZScoDoc object" meta_type = "ZScoDoc" security = ClassSecurityInfo() file_path = Globals.package_home(globals()) # This is the list of the methods associated to 'tabs' in the ZMI # Be aware that The first in the list is the one shown by default, so if # the 'View' tab is the first, you will never see your tabs by cliquing # on the object. manage_options = ( ({"label": "Contents", "action": "manage_main"},) + PropertyManager.manage_options # add the 'Properties' tab + ({"label": "View", "action": "index_html"},) + Item.manage_options # add the 'Undo' & 'Owner' tab + RoleManager.manage_options # add the 'Security' tab ) def __init__(self, id, title): "Initialise a new instance of ZScoDoc" self.id = id self.title = title self.manage_addProperty("admin_password_initialized", "0", "string") security.declareProtected(ScoView, "ScoDocURL") def ScoDocURL(self): "base URL for this instance (top level for ScoDoc site)" return self.absolute_url() def _check_admin_perm(self, REQUEST): """Check if user has permission to add/delete departements""" authuser = REQUEST.AUTHENTICATED_USER if authuser.has_role("manager") or authuser.has_permission(ScoSuperAdmin, self): return "" else: return """

Vous n'avez pas le droit d'accéder à cette page

""" def _check_users_folder(self, REQUEST=None): """Vérifie UserFolder et le crée s'il le faut""" try: self.UsersDB return "" except: e = self._check_admin_perm(REQUEST) if not e: # admin permissions: self.create_users_cnx(REQUEST) self.create_users_folder(REQUEST) return '
Création du connecteur utilisateurs réussie
' else: return """
Installation non terminée: connectez vous avec les droits d'administrateur
""" security.declareProtected("View", "create_users_folder") def create_users_folder(self, REQUEST=None): """Create Zope user folder""" e = self._check_admin_perm(REQUEST) if e: return e if REQUEST is None: REQUEST = {} REQUEST.form["pgauth_connection"] = "UsersDB" REQUEST.form["pgauth_table"] = "sco_users" REQUEST.form["pgauth_usernameColumn"] = "user_name" REQUEST.form["pgauth_passwordColumn"] = "passwd" REQUEST.form["pgauth_rolesColumn"] = "roles" add_method = self.manage_addProduct["OFSP"].manage_addexUserFolder log("create_users_folder: in %s" % self.id) return add_method( authId="pgAuthSource", propId="nullPropSource", memberId="nullMemberSource", groupId="nullGroupSource", cryptoId="MD51", # doAuth='1', doProp='1', doMember='1', doGroup='1', allDone='1', cookie_mode=2, session_length=500, not_session_length=0, REQUEST=REQUEST, ) def _fix_users_folder(self): """removes docLogin and docLogout dtml methods from exUserFolder, so that we use ours. (called each time be index_html, to fix old ScoDoc installations.) """ try: self.acl_users.manage_delObjects(ids=["docLogin", "docLogout"]) except: pass # add missing getAuthFailedMessage (bug in exUserFolder ?) try: self.getAuthFailedMessage except: log("adding getAuthFailedMessage to Zope install") parent = self.aq_parent from OFS.DTMLMethod import addDTMLMethod # pylint: disable=import-error addDTMLMethod(parent, "getAuthFailedMessage", file="Identification") security.declareProtected("View", "create_users_cnx") def create_users_cnx(self, REQUEST=None): """Create Zope connector to UsersDB Note: la connexion est fixée (SCOUSERS) (base crée par l'installeur) ! Les utilisateurs avancés pourront la changer ensuite. """ # ce connecteur zope - db est encore pour l'instant utilisé par exUserFolder.pgAuthSource # (en lecture seule en principe) oid = "UsersDB" log("create_users_cnx: in %s" % self.id) da = ZopeDA.Connection( oid, "Cnx bd utilisateurs", SCO_DEFAULT_SQL_USERS_CNX, False, check=1, tilevel=2, encoding="LATIN1", ) self._setObject(oid, da) security.declareProtected("View", "change_admin_user") def change_admin_user(self, password, REQUEST=None): """Change password of admin user""" # note: controle sur le role et non pas sur une permission # (non definies au top level) if not REQUEST.AUTHENTICATED_USER.has_role("Manager"): log("user %s is not Manager" % REQUEST.AUTHENTICATED_USER) log("roles=%s" % REQUEST.AUTHENTICATED_USER.getRolesInContext(self)) raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") log("trying to change admin password") # 1-- check strong password if pwdFascistCheck(password) != None: log("refusing weak password") return REQUEST.RESPONSE.redirect( "change_admin_user_form?message=Mot%20de%20passe%20trop%20simple,%20recommencez" ) # 2-- change password for admin user username = "admin" acl_users = self.aq_parent.acl_users user = acl_users.getUser(username) r = acl_users._changeUser( username, password, password, user.roles, user.domains ) if not r: # OK, set property to indicate we changed the password log("admin password changed successfully") self.manage_changeProperties(admin_password_initialized="1") return r or REQUEST.RESPONSE.redirect("index_html") security.declareProtected("View", "change_admin_user_form") def change_admin_user_form(self, message="", REQUEST=None): """Form allowing to change the ScoDoc admin password""" # note: controle sur le role et non pas sur une permission # (non definies au top level) if not REQUEST.AUTHENTICATED_USER.has_role("Manager"): raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") H = [ self.scodoc_top_html_header( REQUEST, page_title="ScoDoc: changement mot de passe" ) ] if message: H.append('
%s
' % message) H.append( """

Changement du mot de passe administrateur (utilisateur admin)

Nouveau mot de passe:
Confirmation:
""" ) H.append("""""") return "\n".join(H) security.declareProtected("View", "list_depts") def list_depts(self, REQUEST=None): """List departments folders (returns a list of Zope folders containing a ZScolar instance) """ folders = self.objectValues("Folder") # select folders with Scolarite object: r = [] for folder in folders: try: _ = folder.Scolarite r.append(folder) except: pass return r security.declareProtected("View", "create_dept") def create_dept(self, REQUEST=None, DeptId="", pass2=False): """Creation (ajout) d'un site departement (instance ZScolar + dossier la contenant) """ e = self._check_admin_perm(REQUEST) if e: return e if not DeptId: raise ValueError("nom de departement invalide") if not pass2: # 1- Creation de repertoire Dept log("creating Zope folder " + DeptId) add_method = self.manage_addProduct["OFSP"].manage_addFolder add_method(DeptId, title="Site dept. " + DeptId) DeptFolder = self[DeptId] if not pass2: # 2- Creation du repertoire Fotos log("creating Zope folder %s/Fotos" % DeptId) add_method = DeptFolder.manage_addProduct["OFSP"].manage_addFolder add_method("Fotos", title="Photos identites " + DeptId) # 3- Creation instance ScoDoc log("creating Zope ZScolar instance") add_method = DeptFolder.manage_addProduct["ScoDoc"].manage_addZScolarForm return add_method(DeptId, REQUEST=REQUEST) security.declareProtected("View", "delete_dept") def delete_dept(self, REQUEST=None, DeptId="", force=False): """Supprime un departement (de Zope seulement, ne touche pas la BD)""" e = self._check_admin_perm(REQUEST) if e: return e if not force and DeptId not in [x.id for x in self.list_depts()]: raise ValueError("nom de departement invalide") self.manage_delObjects(ids=[DeptId]) return ( "

Département " + DeptId + """ supprimé du serveur web (la base de données n'est pas affectée)!

Continuer

""" % REQUEST.URL1 ) _top_level_css = """ """ _html_begin = """ %(page_title)s """ def scodoc_top_html_header(self, REQUEST, page_title="ScoDoc"): H = [ self._html_begin % {"page_title": "ScoDoc: bienvenue", "encoding": SCO_ENCODING}, self._top_level_css, """""", CUSTOM_HTML_HEADER_CNX, ] return "\n".join(H) security.declareProtected("View", "index_html") def index_html(self, REQUEST=None, message=None): """Top level page for ScoDoc""" authuser = REQUEST.AUTHENTICATED_USER deptList = self.list_depts() self._fix_users_folder() # fix our exUserFolder isAdmin = not self._check_admin_perm(REQUEST) try: admin_password_initialized = self.admin_password_initialized except: admin_password_initialized = "0" if isAdmin and admin_password_initialized != "1": REQUEST.RESPONSE.redirect( "ScoDoc/change_admin_user_form?message=Le%20mot%20de%20passe%20administrateur%20doit%20etre%20change%20!" ) # Si l'URL indique que l'on est dans un folder, affiche page login du departement try: deptfoldername = REQUEST.URL0.split("ScoDoc")[1].split("/")[1] if deptfoldername in [x.id for x in self.list_depts()]: return self.index_dept(deptfoldername=deptfoldername, REQUEST=REQUEST) except: pass H = [ self.scodoc_top_html_header(REQUEST, page_title="ScoDoc: bienvenue"), self._check_users_folder(REQUEST=REQUEST), # ensure setup is done ] if message: H.append('
%s
' % message) if isAdmin and not message: H.append('
Attention: connecté comme administrateur
') H.append( """

ScoDoc: gestion scolarité

""" ) if authuser.has_role("Authenticated"): H.append( """

Bonjour %s.

""" % str(authuser) ) H.append( """

N'oubliez pas de vous déconnecter après usage.

""" ) else: H.append( """

Ce site est réservé au personnel autorisé

""" ) H.append(self.authentication_form(destination=".")) if not deptList: H.append("aucun département existant !") # si pas de dept et pas admin, propose lien pour loger admin if not isAdmin: H.append( """

Identifiez vous comme administrateur (au début: nom 'admin', mot de passe 'scodoc')

""" ) else: H.append('") # Recherche etudiant H.append(sco_find_etud.form_search_etud_in_accessible_depts(self, REQUEST)) if isAdmin: H.append('

Administration de ScoDoc

') else: H.append( '

Se connecter comme administrateur

' % REQUEST.BASE0 ) H.append( """

ScoDoc est un logiciel libre de suivi de la scolarité des étudiants conçu par E. Viennet (Université Paris 13).

""" % (SCO_WEBSITE,) ) H.append("""""") return "\n".join(H) def authentication_form(self, destination=""): """html snippet for authentication""" return ( """

Nom:
Mot de passe:

""" % destination ) security.declareProtected("View", "index_dept") def index_dept(self, deptfoldername="", REQUEST=None): """Page d'accueil departement""" authuser = REQUEST.AUTHENTICATED_USER try: dept = getattr(self, deptfoldername) if authuser.has_permission(ScoView, dept): return REQUEST.RESPONSE.redirect("ScoDoc/%s/Scolarite" % deptfoldername) except: log( "*** problem in index_dept (%s) user=%s" % (deptfoldername, str(authuser)) ) H = [ self.standard_html_header(REQUEST), """

Scolarité du département %s

""" % deptfoldername, """

Ce site est réservé au personnel du département.

""", self.authentication_form(destination="Scolarite"), """

Pour quitter, logout

Retour à l'accueil

""" % self.ScoDocURL(), self.standard_html_footer(REQUEST), ] return "\n".join(H) security.declareProtected("View", "doLogin") def doLogin(self, REQUEST=None, destination=None): "redirect to destination after login" if destination: return REQUEST.RESPONSE.redirect(destination) security.declareProtected("View", "docLogin") docLogin = DTMLFile("dtml/docLogin", globals()) security.declareProtected("View", "docLogout") docLogout = DTMLFile("dtml/docLogout", globals()) security.declareProtected("View", "query_string_to_form_inputs") def query_string_to_form_inputs(self, query_string=""): """Return html snippet representing the query string as POST form hidden inputs. This is useful in conjonction with exUserfolder to correctly redirect the response after authentication. """ H = [] for a in query_string.split("&"): if a: nv = a.split("=") if len(nv) == 2: name, value = nv H.append( '' ) return "\n" + "\n".join(H) security.declareProtected("View", "standard_html_header") def standard_html_header(self, REQUEST=None): """Standard HTML header for pages outside depts""" # not used in ZScolar, see sco_header return """ ScoDoc: accueil %s""" % ( SCO_ENCODING, CUSTOM_HTML_HEADER_CNX, ) security.declareProtected("View", "standard_html_footer") def standard_html_footer(self, REQUEST=None): return """

Problèmes et suggestions sur le logiciel: %s

ScoDoc est un logiciel libre développé par Emmanuel Viennet.

""" % ( SCO_USERS_LIST, SCO_USERS_LIST, ) # sendEmail is not used through the web def sendEmail(self, msg): # sends an email to the address using the mailhost, if there is one try: mail_host = self.MailHost except: log("warning: sendEmail: no MailHost found !") return # a failed notification shouldn't cause a Zope error on a site. try: mail_host.send(msg.as_string()) log("sendEmail: ok") except Exception as e: log("sendEmail: exception while sending message") log(e) pass def sendEmailFromException(self, msg): # Send email by hand, as it seems to be not possible to use Zope Mail Host # from an exception handler (see https://bugs.launchpad.net/zope2/+bug/246748) log("sendEmailFromException") try: p = os.popen("sendmail -t", "w") # old brute force method p.write(msg.as_string()) exitcode = p.close() if exitcode: log("sendmail exit code: %s" % exitcode) except: log("an exception occurred sending mail") security.declareProtected("View", "standard_error_message") def standard_error_message( self, error_value=None, error_message=None, error_type=None, error_traceback=None, error_tb=None, **kv ): "Recuperation des exceptions Zope" sco_exc_mail = SCO_EXC_MAIL # pylint: disable=unused-variable sco_dev_mail = SCO_DEV_MAIL # pylint: disable=unused-variable # neat (or should I say dirty ?) hack to get REQUEST # in fact, our caller (probably SimpleItem.py) has the REQUEST variable # that we'd like to use for our logs, but does not pass it as an argument. try: frame = inspect.currentframe() REQUEST = frame.f_back.f_locals["REQUEST"] except: REQUEST = {} # Authentication uses exceptions, pass them up HTTP_X_FORWARDED_FOR = REQUEST.get("HTTP_X_FORWARDED_FOR", "") if error_type == "LoginRequired": # raise 'LoginRequired', '' # copied from exuserFolder (beurk, old style exception...) # if REQUEST: # REQUEST.response.setStatus( 401, "Unauthorized") # ?????? log("LoginRequired from %s" % HTTP_X_FORWARDED_FOR) self.login_page = error_value return error_value elif error_type == "Unauthorized": log("Unauthorized from %s" % HTTP_X_FORWARDED_FOR) return self.acl_users.docLogin(self, REQUEST=REQUEST) log("exception caught: %s" % error_type) log(traceback.format_exc()) if error_type == "ScoGenError": return "

" + str(error_value) + "

" elif error_type in ("ScoValueError", "FormatError"): # Not a bug, presents a gentle message to the user: H = [ self.standard_html_header(REQUEST), """

Erreur !

%s

""" % error_value, ] if error_value.dest_url: H.append('

Continuer

' % error_value.dest_url) H.append(self.standard_html_footer(REQUEST)) return "\n".join(H) else: # Other exceptions, try carefully to build an error page... # log('exc A') H = [] try: H.append(self.standard_html_header(REQUEST)) except: pass H.append( """

Erreur !

Une erreur est survenue

Error Type: %(error_type)s
Error Value: %(error_value)s


L'URL est peut-etre incorrecte ?

Si l'erreur persiste, contactez Emmanuel Viennet: %(sco_dev_mail)s en copiant ce message d'erreur et le contenu du cadre bleu ci-dessous si possible.

""" % vars() ) # display error traceback (? may open a security risk via xss attack ?) # log('exc B') # pylint: disable=unused-variable txt_html = self._report_request(REQUEST, fmt="html") H.append( """

Zope Traceback (à envoyer par mail à %(sco_dev_mail)s)

%(error_tb)s

Informations:
%(txt_html)s

Merci de votre patience !

""" % vars() ) try: H.append(self.standard_html_footer(REQUEST)) except: log("no footer found for error page") pass # --- Mail: # pylint: disable=unused-variable error_traceback_txt = scodoc_html2txt(error_tb) txt = ( """ ErrorType: %(error_type)s %(error_traceback_txt)s """ % vars() ) self.send_debug_alert(txt, REQUEST=REQUEST) # --- log("done processing exception") # log( '\n page=\n' + '\n'.join(H) ) return "\n".join(H) def _report_request(self, REQUEST, fmt="txt"): """string describing current request for bug reports""" # pylint: disable=unused-variable AUTHENTICATED_USER = REQUEST.get("AUTHENTICATED_USER", "") dt = time.asctime() URL = REQUEST.get("URL", "") QUERY_STRING = REQUEST.get("QUERY_STRING", "") if QUERY_STRING: QUERY_STRING = "?" + QUERY_STRING METHOD = REQUEST.get("REQUEST_METHOD", "") if fmt == "txt": REFERER = REQUEST.get("HTTP_REFERER", "") HTTP_USER_AGENT = REQUEST.get("HTTP_USER_AGENT", "") else: REFERER = "na" HTTP_USER_AGENT = "na" form = REQUEST.get("form", "") HTTP_X_FORWARDED_FOR = REQUEST.get("HTTP_X_FORWARDED_FOR", "") SCOVERSION = sco_utils.get_scodoc_version() or VERSION.SCOVERSION txt = ( """ Version: %(SCOVERSION)s User: %(AUTHENTICATED_USER)s Date: %(dt)s URL: %(URL)s%(QUERY_STRING)s Method: %(METHOD)s REFERER: %(REFERER)s Form: %(form)s Origin: %(HTTP_X_FORWARDED_FOR)s Agent: %(HTTP_USER_AGENT)s """ % vars() ) if fmt == "html": txt = txt.replace("\n", "
") return txt security.declareProtected( ScoSuperAdmin, "send_debug_alert" ) # not called through the web def send_debug_alert(self, txt, REQUEST=None): """Send an alert email (bug report) to ScoDoc developpers""" if not SCO_EXC_MAIL: log("send_debug_alert: email disabled") return if REQUEST: txt = self._report_request(REQUEST) + txt URL = REQUEST.get("URL", "") else: URL = "send_debug_alert" msg = MIMEMultipart() subj = Header("[scodoc] exc %s" % URL, SCO_ENCODING) msg["Subject"] = subj recipients = [SCO_EXC_MAIL] msg["To"] = " ,".join(recipients) msg["From"] = "scodoc-alert" msg.epilogue = "" msg.attach(MIMEText(txt, "plain", SCO_ENCODING)) self.sendEmailFromException(msg) log("Sent mail alert:\n" + txt) security.declareProtected("View", "scodoc_admin") def scodoc_admin(self, REQUEST=None): """Page Operations d'administration""" e = self._check_admin_perm(REQUEST) if e: return e H = [ self.scodoc_top_html_header(REQUEST, page_title="ScoDoc: bienvenue"), """

Administration ScoDoc

changer le mot de passe super-administrateur

retour à la page d'accueil

Création d'un département

Le département doit avoir été créé au préalable sur le serveur en utilisant le script create_dept.sh (à lancer comme root dans le répertoire config de ScoDoc).

""" % self.absolute_url(), ] deptList = [x.id for x in self.list_depts()] # definis dans Zope deptIds = set(self._list_depts_ids()) # definis sur le filesystem existingDepts = set(deptList) addableDepts = deptIds - existingDepts if not addableDepts: # aucun departement defini: aide utilisateur H.append("

Aucun département à ajouter !

") else: H.append("""
""" ) if deptList: H.append( """

Suppression d'un département

Ceci permet de supprimer le site web associé à un département, mais n'affecte pas la base de données (le site peut donc être recréé sans perte de données).

""" ) H.append("""""") return "\n".join(H) def _list_depts_ids(self): """Liste de id de departements definis par create_dept.sh (fichiers depts/*.cfg) """ filenames = glob.glob(SCODOC_CFG_DIR + "/config/depts/*.cfg") ids = [os.path.split(os.path.splitext(f)[0])[1] for f in filenames] return ids security.declareProtected("View", "http_expiration_date") def http_expiration_date(self): "http expiration date for cachable elements (css, ...)" d = datetime.timedelta(minutes=10) return (datetime.datetime.utcnow() + d).strftime("%a, %d %b %Y %H:%M:%S GMT") security.declareProtected("View", "get_etud_dept") def get_etud_dept(self, REQUEST=None): """Returns the dept id (eg "GEII") of an etud (identified by etudid, INE or NIP in REQUEST). Warning: This function is inefficient and its result should be cached. """ depts = self.list_depts() depts_etud = [] # liste des depts où l'etud est defini for dept in depts: etuds = dept.Scolarite.getEtudInfo(REQUEST=REQUEST) if etuds: depts_etud.append((dept, etuds)) if not depts_etud: return "" # not found elif len(depts_etud) == 1: return depts_etud[0][0].id # inscriptions dans plusieurs departements: cherche la plus recente last_dept = None last_date = None for (dept, etuds) in depts_etud: dept.Scolarite.fillEtudsInfo(etuds) etud = etuds[0] if etud["sems"]: if (not last_date) or (etud["sems"][0]["date_fin_iso"] > last_date): last_date = etud["sems"][0]["date_fin_iso"] last_dept = dept if not last_dept: # est present dans plusieurs semestres mais inscrit dans aucun return depts_etud[0][0] return last_dept.id security.declareProtected("View", "table_etud_in_accessible_depts") table_etud_in_accessible_depts = sco_find_etud.table_etud_in_accessible_depts security.declareProtected("View", "search_inscr_etud_by_nip") search_inscr_etud_by_nip = sco_find_etud.search_inscr_etud_by_nip def manage_addZScoDoc(self, id="ScoDoc", title="Site ScoDoc", REQUEST=None): "Add a ZScoDoc instance to a folder." log("============== creating a new ScoDoc instance =============") zscodoc = ZScoDoc( id, title ) # ne cree (presque rien), tout se passe lors du 1er accès self._setObject(id, zscodoc) if REQUEST is not None: REQUEST.RESPONSE.redirect("%s/manage_workspace" % REQUEST.URL1) return id