diff options
34 files changed, 5488 insertions, 0 deletions
diff --git a/htdocs/images/carbon.png b/htdocs/images/carbon.png Binary files differnew file mode 100644 index 0000000..a72416c --- /dev/null +++ b/htdocs/images/carbon.png diff --git a/htdocs/images/jabber_logo.png b/htdocs/images/jabber_logo.png Binary files differnew file mode 100644 index 0000000..f1b2d77 --- /dev/null +++ b/htdocs/images/jabber_logo.png diff --git a/htdocs/images/left-sm.png b/htdocs/images/left-sm.png Binary files differnew file mode 100644 index 0000000..5754a6d --- /dev/null +++ b/htdocs/images/left-sm.png diff --git a/htdocs/images/right-sm.png b/htdocs/images/right-sm.png Binary files differnew file mode 100644 index 0000000..fda4da2 --- /dev/null +++ b/htdocs/images/right-sm.png diff --git a/htdocs/main.py b/htdocs/main.py new file mode 100644 index 0000000..5921752 --- /dev/null +++ b/htdocs/main.py @@ -0,0 +1,34 @@ +#main url mapper + +import sys +sys.path.append("../py-bin") + +from utils import BasicHandler, process_request, set_logging_defaults +from jabberman import JabberManager +from login import LoginMixIn +from mail_auth import MailAuthMixIn +from setup import SetupMixIn + +set_logging_defaults() + +class MainHandler(BasicHandler, MailAuthMixIn, LoginMixIn, SetupMixIn): + def do_process(self, req): + command = req.params.get("cmd", "") + + if command == "": + self.login_form(req) + else: + if hasattr(self, command): + method = getattr(self, command) + if hasattr(method, 'web_callable') and method.web_callable: + self.jman = JabberManager(self.session) + method(req) + else: + self.invalid_page(req) + else: + self.invalid_page(req) + + def invalid_page(self, req): + self.error_page(req, "Ungueltiger Request.") + +process_request(MainHandler) diff --git a/py-bin/config.py b/py-bin/config.py new file mode 100644 index 0000000..2dd0fe8 --- /dev/null +++ b/py-bin/config.py @@ -0,0 +1,44 @@ +#webreg global config file + +# debug mode: for testing only!!! +# never ever activate on production server! +debugmode = True #x + +# mail configuration: +mail_from_addr = "jabber@immerda.ch" +# to use sendmail for delivery, use this: +use_sendmail = True +# to use python smtplib, configure these smtp options +#smtp_server = "sicher.immerda.ch" +# if you comment out user/pass, no smtp auth will be done +#smtp_user = "" +#smtp_pass = "" + +# domain name policy +mail_domains = ["immerda.ch", "cronopios.org", "einfachsicher.ch"] +extra_domains = ["imsg.ch", "unerkenntli.ch", "auchno.ch"] #x adapt to existing ones! +# username and password policy +user_re ='^[a-zA-Z0-9_\-.]+$' +password_re = '^[a-z0-9_\-.]+$' +min_password_length = debugmode and 2 or 8 + +# path configuration +# keep templates and all code except main.py outside doc-root! +template_dir = "/e/e1/var/www/jabber/py-bin/templates" +# keep the following stuff on an encrypted fs! (especially sessions) +session_dir = "/e/e1/tmp" +jabberdb_path = "/e/e1/var/db/jabberman/jabber_db" +logfile_path = "/e/e1/var/log/lighttpd/webreg.log" + +# self reference, needed for http redirects +#script_url = "https://jabber.immerda.ch:60843/main.py" +script_url = "https://127.0.0.1:8000/main.py" + +# secret needed for session ids and registration tokens +the_secret = "w1bXM13wHSI5BWrtMs97Cwxf7qWjL1xu" + +#experimental stuff... +#x check again +ejabberdctl_path = "/e/e1/opt/ejabberd/bin/ejabberdctl" +ejabberdctl_environ = {"PATH":"/bin:/usr/bin", "HOME":"/e/e1/home/lighttpd"} + diff --git a/py-bin/ejabberdctl.py b/py-bin/ejabberdctl.py new file mode 100644 index 0000000..a1ef5ff --- /dev/null +++ b/py-bin/ejabberdctl.py @@ -0,0 +1,47 @@ +#ejabberdctl + +import subprocess, logging +import config + +#x remove +#def __run(path, params, env): +# params = [path] + params +# try: +# result = os.spawnve(os.P_WAIT, path, params, env) +# return (result == 0) +# except Exception: +# return False + +class EJabberdCtl: + def create_account(self, user, server, password): + if self.__ejabberdctl(["register", user, server, password]): + logging.info("Created account %s@%s." % (user, server)) + return True + return False + + def remove_account(self, user, server): + if self.__ejabberdctl(["unregister", user, server]): + logging.info("Removed account %s@%s." % (user, server)) + return True + return False + + def change_password(self, user, server, password): + if self.__ejabberdctl(["set-password", user, server, password]): + logging.info("Changed Password for %s@%s." % (user, server)) + return True + return False + + def __ejabberdctl(self, params): + return self.__run([config.ejabberdctl_path] + params, config.ejabberdctl_environ) + + def __run(self, path_and_params, environ={}): + try: + result = subprocess.call(path_and_params, env=environ) + if result != 0: + logging.error("Error invoking '%s': Result = %s." % + (str(path_and_params), str(result))) + return (result == 0) + except Exception, e: + logging.error("Error invoking '%s': %s." % (str(path_and_params), str(e))) + return False + diff --git a/py-bin/jabberman.py b/py-bin/jabberman.py new file mode 100644 index 0000000..11e3bdf --- /dev/null +++ b/py-bin/jabberman.py @@ -0,0 +1,392 @@ +#jabber manager + +import shelve, atexit, sha, hmac, random, os, time, re +import config +from ejabberdctl import EJabberdCtl + +class JabberUser: + def __init__(self, user_id): + self.user, self.domain = user_id.split("@") + self.accounts = [] + + def get_user_id(self): + return self.user + "@" + self.domain + + def get_default_jabber_id(self): + return self.user + "@jabber." + self.domain + + def is_active(self): + return hasattr(self, "password_hash") + + def check_password(self, password): + if not self.is_active(): + return False + return self.password_hash == self.__hash_password(password) + + def set_password(self, password): + self.password_hash = self.__hash_password(password) + + def __hash_password(self, password): + return sha.new(password).hexdigest() + + @staticmethod + def generate_token(): + data = str(random.getrandbits(256)) + str(time.time()*1000) + str(os.getpid()) + return "+" + hmac.new(config.the_secret, data, sha).hexdigest() + + def set_token(self, token): + self.token = token + + def validate_token(self, token): + if token[1:] == self.token[1:]: + if self.__is_token_expired(): + return (False, "Benutzerkonto bereits aktiviert.") + return (True, self) + else: + return (False, "Zugriff verweigert.") + + def __is_token_expired(self): + return self.token[0] != "+" + + def expire_token(self): + self.token = "-" + self.token[1:] + + def add_account(self, jabber_id): + self.accounts.append(jabber_id) + + def has_account(self, jabber_id): + return jabber_id in self.accounts + + def get_account_list(self): + return list(self.accounts) + + def get_extra_account_list(self): + default_acc = self.get_default_jabber_id() + return filter(lambda acc: acc != default_acc, self.accounts) + + def remove_account(self, jabber_id): + self.accounts.remove(jabber_id) + +class JabberAccount: + def __init__(self, jabber_id): + self.user, self.server = jabber_id.split("@") + + def get_jabber_id(self): + return self.user + "@" + self.server + +class JabberDB: + def __init__(self): + self.db = shelve.open(config.jabberdb_path, 'c') + atexit.register(self.db.close) + + def login_user(self, user_id, password): + user = self.__load_user(user_id) + if user and user.check_password(password): + return user + return None + + def generate_token(self, user_id): + if self.__load_user(user_id): + return (False, "Benutzer existiert bereits!") + + return (True, JabberUser.generate_token()) + + def prepare_user(self, user_id, token): + if self.__load_user(user_id): + return (False, "Benutzer existiert bereits!") + + user = JabberUser(user_id) + user.set_token(token) + self.__store_user(user) + + return (True, "Benutzer registriert, Aktivierung noch ausstehend.") + + def validate_token(self, user_id, token): + user = self.__load_user(user_id) + if not user: + return (False, "Zugriff verweigert.") + + return user.validate_token(token) + + def activate_user(self, user_id, password, token): + user = self.__load_user(user_id) + if not user: + return (False, "Zugriff verweigert.") + + ok, status = user.validate_token(token) + if not ok: + return (False, status) + + user.expire_token() + user.set_password(password) + self.__store_user(user) + + return (True, user) + + def add_account(self, user_id, jabber_id, check_only = False): + user = self.__load_user(user_id) + if not user: + return (False, "Zugriff verweigert.") + + account = self.__load_account(jabber_id) + if account: + return (False, "Sorry, Jabber Benutzerkonto %s bereits vergeben." % jabber_id) + + if check_only: + return (True, "Jabber kann hinzugefuegt werden.") + + account = JabberAccount(jabber_id) + self.__store_account(account) + user.add_account(jabber_id) + self.__store_user(user) + + return (True, "Jabber Konto hinzugefuegt.") + + def remove_account(self, user_id, jabber_id, check_only = False): + user = self.__load_user(user_id) + if (not user) or (not user.has_account(jabber_id)): + return (False, "Zugriff verweigert.") + + if check_only: + return (True, "Jabber darf geloescht werden.") + + self.__delete_account(jabber_id) + user.remove_account(jabber_id) + self.__store_user(user) + + return (True, "Jabber Konto geloescht.") + + def change_password(self, user_id, password): + user = self.__load_user(user_id) + if not user: + return (False, "Zugriff verweigert.") + + user.set_password(password) + self.__store_user(user) + return (True, "Passwort geaendert.") + + def __load_user(self, user_id): + return self.db.get("#usr#" + user_id) + + def __store_user(self, user): + self.db["#usr#" + user.get_user_id()] = user + + def __load_account(self, jabber_id): + return self.db.get("#acc#" + jabber_id) + + def __store_account(self, account): + self.db["#acc#" + account.get_jabber_id()] = account + + def __delete_account(self, jabber_id): + del(self.db["#acc#" + jabber_id]) + + +class JabberManager: + def __init__(self, session): + self.jadb = JabberDB() + self.session = session + self.current_user, self.authenticated = None, False + self.ejctl = EJabberdCtl() + + def get_user(self): + return self.current_user + + def authenticate(self): + if self.authenticated == True: + return True + if (not "uid" in self.session) or (not "pass" in self.session): + return (False, "Nicht angemeldet.") + ok, status_or_user = self.login( + self.session["uid"], self.session["pass"]) + return (ok, status_or_user) + + def login(self, user_id, password): + ok, status = self.check_user_id(user_id) + if not ok: + return (False, status) + + self.current_user = self.jadb.login_user(user_id, password) + if self.current_user: + self.__set_session(user_id, password = password) + else: + self.__clear_session() + return (False, "Benutzername oder Passwort falsch.") + + self.authenticated = True + return (True, self.current_user) + + def logout(self): + self.current_user, self.authenticated = None, False + self.__clear_session() + + def generate_token(self, user_id): + ok, status = self.check_user_id(user_id) + if not ok: + return (False, status) + + return self.jadb.generate_token(user_id) + + def prepare_user(self, user_id, token): + ok, status = self.check_user_id(user_id) + if not ok: + return (False, status) + + return self.jadb.prepare_user(user_id, token) + + def validate_token(self, user_id, token): + if user_id == "": + try: + user_id, token = self.session["uid"], self.session["tok"] + except Exception: + return (False, "Zugriff verweigert.") + + ok, status = self.check_user_id(user_id) + if not ok: + return (False, "Zugriff verweigert.") + + ok, status_or_user = self.jadb.validate_token(user_id, token) + if ok: + self.current_user = status_or_user + self.__set_session(user_id, token = token) + return (ok, status_or_user) + + def activate_user(self, password): + try: + user_id, token = self.session["uid"], self.session["tok"] + except Exception: + return (False, "Zugriff verweigert.") + ok, status = self.check_user_id(user_id) + if not ok: + return (False, "Zugriff verweigert.") + + ok, status_or_user = self.jadb.activate_user(user_id, password, token) + if ok: + self.current_user, self.authenticated = status_or_user, True + self.__set_session(user_id, password = password) + else: + self.__clear_session() + return (False, status_or_user) + + ok, status = self.add_account(self.current_user.get_default_jabber_id()) + if not ok: + #todo: handle this smarter somehow + return (False, status) + + return (True, status) + + def change_password(self, password): + if not self.authenticated: + return (False, "Zugriff verweigert.") + + user_id = self.current_user.get_user_id() + ok, status = self.jadb.change_password(user_id, password) + if ok: + self.__set_session(user_id, password = password) + else: + self.__clear_session() + return (False, status) + + for jabber_id in self.current_user.get_account_list(): + acc = JabberAccount(jabber_id) + if not self.ejctl.change_password(acc.user, acc.server, password): + msg = "Konnte Jaber Passwort fuer %s nicht setzen." % acc.get_jabber_id() + return (False, msg) + + return (True, "Passwort erfolgreich geaendert.") + + def is_acceptable_password(self, password, password2): + if password != password2: + return (False, "Passwoerter nicht identisch.") + if len(password) < config.min_password_length: + return (False, "Passwort ist zu kurz.") + if not re.match(config.password_re, password): + return (False, "Passwort enthaelt unerlaubte Zeichen.") + return (True, "Passwort OK.") + + def add_account(self, jabber_id): + if not self.authenticated: + return (False, "Zugriff verweigert.") + + ok, status = JabberManager.check_jabber_id(jabber_id) + if not ok: + return (False, status) + + acc = JabberAccount(jabber_id) + try: + password = self.session["pass"] + except Exception: + return (False, "Zugriff verweigert.") + + user_id = self.current_user.get_user_id() + ok, status = self.jadb.add_account(user_id, jabber_id, check_only = True) + if not ok: + return (False, status) + + if not self.ejctl.create_account(acc.user, acc.server, password): + return (False, "Konnte Konto %s nicht erstellen." % acc.get_jabber_id()) + + user_id = self.current_user.get_user_id() + return self.jadb.add_account(user_id, jabber_id) + + def remove_account(self, jabber_id): + if not self.authenticated: + return (False, "Zugriff verweigert.") + + ok, status = JabberManager.check_jabber_id(jabber_id) + if not ok: + return (False, "Zugriff verweigert.") + + user_id = self.current_user.get_user_id() + if jabber_id == self.current_user.get_default_jabber_id(): + return (False, "Hauptkonto darf nicht geloescht werden!") + + ok, status = self.jadb.remove_account(user_id, jabber_id, check_only = True) + if not ok: + return (False, status) + + acc = JabberAccount(jabber_id) + if not self.ejctl.remove_account(acc.user, acc.server): + return (False, "Konnte Konto %s nicht loeschen." % acc.get_jabber_id()) + + return self.jadb.remove_account(user_id, jabber_id) + + def __set_session(self, user_id, password = None, token = None): + self.__clear_session() + self.session["uid"] = user_id + if password: + self.session["pass"] = password + if token: + self.session["tok"] = token + + def __clear_session(self): + if self.session.get("uid"): + del(self.session["uid"]) + if self.session.get("pass"): + del(self.session["pass"]) + if self.session.get("token"): + del(self.session["tok"]) + + @staticmethod + def __check_account_form(acc_type, domains, account): + try: + user, domain = account.split("@") + except ValueError: + status = "Ungueltige %s Adresse. Erwartete Form: user@domain." % acc_type + return (False, status) + + if not re.match(config.user_re, user): + return (False, "Benutzername %s nicht erlaubt." % user) + if domain not in domains: + return (False, "Domain %s nicht erlaubt." % domain) + return (True, "%s Adresse akzeptiert." % acc_type) + + @staticmethod + def check_user_id(user_id): + return JabberManager.__check_account_form("E-Mail", config.mail_domains, user_id) + + @staticmethod + def check_jabber_id(jabber_id): + domains = map(lambda d: "jabber." + d, config.mail_domains) + config.extra_domains + return JabberManager.__check_account_form("Jabber", domains, jabber_id) + + diff --git a/py-bin/lib/__init__.py b/py-bin/lib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/py-bin/lib/__init__.py diff --git a/py-bin/lib/em.py b/py-bin/lib/em.py new file mode 100755 index 0000000..6079316 --- /dev/null +++ b/py-bin/lib/em.py @@ -0,0 +1,3288 @@ +#!/usr/local/bin/python +# +# $Id: //projects/empy/em.py#146 $ $Date: 2003/10/27 $ + +""" +A system for processing Python as markup embedded in text. +""" + + +__program__ = 'empy' +__version__ = '3.3' +__url__ = 'http://www.alcyone.com/software/empy/' +__author__ = 'Erik Max Francis <max@alcyone.com>' +__copyright__ = 'Copyright (C) 2002-2003 Erik Max Francis' +__license__ = 'LGPL' + + +import copy +import getopt +import os +import re +import string +import sys +import types + +try: + # The equivalent of import cStringIO as StringIO. + import cStringIO + StringIO = cStringIO + del cStringIO +except ImportError: + import StringIO + +# For backward compatibility, we can't assume these are defined. +False, True = 0, 1 + +# Some basic defaults. +FAILURE_CODE = 1 +DEFAULT_PREFIX = '@' +DEFAULT_PSEUDOMODULE_NAME = 'empy' +DEFAULT_SCRIPT_NAME = '?' +SIGNIFICATOR_RE_SUFFIX = r"%(\S+)\s*(.*)\s*$" +SIGNIFICATOR_RE_STRING = DEFAULT_PREFIX + SIGNIFICATOR_RE_SUFFIX +BANGPATH = '#!' +DEFAULT_CHUNK_SIZE = 8192 +DEFAULT_ERRORS = 'strict' + +# Character information. +IDENTIFIER_FIRST_CHARS = '_abcdefghijklmnopqrstuvwxyz' \ + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +IDENTIFIER_CHARS = IDENTIFIER_FIRST_CHARS + '0123456789.' +ENDING_CHARS = {'(': ')', '[': ']', '{': '}'} + +# Environment variable names. +OPTIONS_ENV = 'EMPY_OPTIONS' +PREFIX_ENV = 'EMPY_PREFIX' +PSEUDO_ENV = 'EMPY_PSEUDO' +FLATTEN_ENV = 'EMPY_FLATTEN' +RAW_ENV = 'EMPY_RAW_ERRORS' +INTERACTIVE_ENV = 'EMPY_INTERACTIVE' +BUFFERED_ENV = 'EMPY_BUFFERED_OUTPUT' +NO_OVERRIDE_ENV = 'EMPY_NO_OVERRIDE' +UNICODE_ENV = 'EMPY_UNICODE' +INPUT_ENCODING_ENV = 'EMPY_UNICODE_INPUT_ENCODING' +OUTPUT_ENCODING_ENV = 'EMPY_UNICODE_OUTPUT_ENCODING' +INPUT_ERRORS_ENV = 'EMPY_UNICODE_INPUT_ERRORS' +OUTPUT_ERRORS_ENV = 'EMPY_UNICODE_OUTPUT_ERRORS' + +# Interpreter options. +BANGPATH_OPT = 'processBangpaths' # process bangpaths as comments? +BUFFERED_OPT = 'bufferedOutput' # fully buffered output? +RAW_OPT = 'rawErrors' # raw errors? +EXIT_OPT = 'exitOnError' # exit on error? +FLATTEN_OPT = 'flatten' # flatten pseudomodule namespace? +OVERRIDE_OPT = 'override' # override sys.stdout with proxy? +CALLBACK_OPT = 'noCallbackError' # is no custom callback an error? + +# Usage info. +OPTION_INFO = [ +("-V --version", "Print version and exit"), +("-h --help", "Print usage and exit"), +("-H --extended-help", "Print extended usage and exit"), +("-k --suppress-errors", "Do not exit on errors; go interactive"), +("-p --prefix=<char>", "Change prefix to something other than @"), +(" --no-prefix", "Do not do any markup processing at all"), +("-m --module=<name>", "Change the internal pseudomodule name"), +("-f --flatten", "Flatten the members of pseudmodule to start"), +("-r --raw-errors", "Show raw Python errors"), +("-i --interactive", "Go into interactive mode after processing"), +("-n --no-override-stdout", "Do not override sys.stdout with proxy"), +("-o --output=<filename>", "Specify file for output as write"), +("-a --append=<filename>", "Specify file for output as append"), +("-b --buffered-output", "Fully buffer output including open"), +(" --binary", "Treat the file as a binary"), +(" --chunk-size=<chunk>", "Use this chunk size for reading binaries"), +("-P --preprocess=<filename>", "Interpret EmPy file before main processing"), +("-I --import=<modules>", "Import Python modules before processing"), +("-D --define=<definition>", "Execute Python assignment statement"), +("-E --execute=<statement>", "Execute Python statement before processing"), +("-F --execute-file=<filename>", "Execute Python file before processing"), +(" --pause-at-end", "Prompt at the ending of processing"), +(" --relative-path", "Add path of EmPy script to sys.path"), +(" --no-callback-error", "Custom markup without callback is error"), +(" --no-bangpath-processing", "Suppress bangpaths as comments"), +("-u --unicode", "Enable Unicode subsystem (Python 2+ only)"), +(" --unicode-encoding=<e>", "Set both input and output encodings"), +(" --unicode-input-encoding=<e>", "Set input encoding"), +(" --unicode-output-encoding=<e>", "Set output encoding"), +(" --unicode-errors=<E>", "Set both input and output error handler"), +(" --unicode-input-errors=<E>", "Set input error handler"), +(" --unicode-output-errors=<E>", "Set output error handler"), +] + +USAGE_NOTES = """\ +Notes: Whitespace immediately inside parentheses of @(...) are +ignored. Whitespace immediately inside braces of @{...} are ignored, +unless ... spans multiple lines. Use @{ ... }@ to suppress newline +following expansion. Simple expressions ignore trailing dots; `@x.' +means `@(x).'. A #! at the start of a file is treated as a @# +comment.""" + +MARKUP_INFO = [ +("@# ... NL", "Comment; remove everything up to newline"), +("@? NAME NL", "Set the current context name"), +("@! INTEGER NL", "Set the current context line number"), +("@ WHITESPACE", "Remove following whitespace; line continuation"), +("@\\ ESCAPE_CODE", "A C-style escape sequence"), +("@@", "Literal @; @ is escaped (duplicated prefix)"), +("@), @], @}", "Literal close parenthesis, bracket, brace"), +("@ STRING_LITERAL", "Replace with string literal contents"), +("@( EXPRESSION )", "Evaluate expression and substitute with str"), +("@( TEST [? THEN [! ELSE]] )", "If test is true, evaluate then, otherwise else"), +("@( TRY $ CATCH )", "Expand try expression, or catch if it raises"), +("@ SIMPLE_EXPRESSION", "Evaluate simple expression and substitute;\n" + "e.g., @x, @x.y, @f(a, b), @l[i], etc."), +("@` EXPRESSION `", "Evaluate expression and substitute with repr"), +("@: EXPRESSION : [DUMMY] :", "Evaluates to @:...:expansion:"), +("@{ STATEMENTS }", "Statements are executed for side effects"), +("@[ CONTROL ]", "Control markups: if E; elif E; for N in E;\n" + "while E; try; except E, N; finally; continue;\n" + "break; end X"), +("@%% KEY WHITESPACE VALUE NL", "Significator form of __KEY__ = VALUE"), +("@< CONTENTS >", "Custom markup; meaning provided by user"), +] + +ESCAPE_INFO = [ +("@\\0", "NUL, null"), +("@\\a", "BEL, bell"), +("@\\b", "BS, backspace"), +("@\\dDDD", "three-digit decimal code DDD"), +("@\\e", "ESC, escape"), +("@\\f", "FF, form feed"), +("@\\h", "DEL, delete"), +("@\\n", "LF, linefeed, newline"), +("@\\N{NAME}", "Unicode character named NAME"), +("@\\oOOO", "three-digit octal code OOO"), +("@\\qQQQQ", "four-digit quaternary code QQQQ"), +("@\\r", "CR, carriage return"), +("@\\s", "SP, space"), +("@\\t", "HT, horizontal tab"), +("@\\uHHHH", "16-bit hexadecimal Unicode HHHH"), +("@\\UHHHHHHHH", "32-bit hexadecimal Unicode HHHHHHHH"), +("@\\v", "VT, vertical tab"), +("@\\xHH", "two-digit hexadecimal code HH"), +("@\\z", "EOT, end of transmission"), +] + +PSEUDOMODULE_INFO = [ +("VERSION", "String representing EmPy version"), +("SIGNIFICATOR_RE_STRING", "Regular expression matching significators"), +("SIGNIFICATOR_RE_SUFFIX", "The above stub, lacking the prefix"), +("interpreter", "Currently-executing interpreter instance"), +("argv", "The EmPy script name and command line arguments"), +("args", "The command line arguments only"), +("identify()", "Identify top context as name, line"), +("setContextName(name)", "Set the name of the current context"), +("setContextLine(line)", "Set the line number of the current context"), +("atExit(callable)", "Invoke no-argument function at shutdown"), +("getGlobals()", "Retrieve this interpreter's globals"), +("setGlobals(dict)", "Set this interpreter's globals"), +("updateGlobals(dict)", "Merge dictionary into interpreter's globals"), +("clearGlobals()", "Start globals over anew"), +("saveGlobals([deep])", "Save a copy of the globals"), +("restoreGlobals([pop])", "Restore the most recently saved globals"), +("defined(name, [loc])", "Find if the name is defined"), +("evaluate(expression, [loc])", "Evaluate the expression"), +("serialize(expression, [loc])", "Evaluate and serialize the expression"), +("execute(statements, [loc])", "Execute the statements"), +("single(source, [loc])", "Execute the 'single' object"), +("atomic(name, value, [loc])", "Perform an atomic assignment"), +("assign(name, value, [loc])", "Perform an arbitrary assignment"), +("significate(key, [value])", "Significate the given key, value pair"), +("include(file, [loc])", "Include filename or file-like object"), +("expand(string, [loc])", "Explicitly expand string and return"), +("string(data, [name], [loc])", "Process string-like object"), +("quote(string)", "Quote prefixes in provided string and return"), +("flatten([keys])", "Flatten module contents into globals namespace"), +("getPrefix()", "Get current prefix"), +("setPrefix(char)", "Set new prefix"), +("stopDiverting()", "Stop diverting; data sent directly to output"), +("createDiversion(name)", "Create a diversion but do not divert to it"), +("retrieveDiversion(name)", "Retrieve the actual named diversion object"), +("startDiversion(name)", "Start diverting to given diversion"), +("playDiversion(name)", "Recall diversion and then eliminate it"), +("replayDiversion(name)", "Recall diversion but retain it"), +("purgeDiversion(name)", "Erase diversion"), +("playAllDiversions()", "Stop diverting and play all diversions in order"), +("replayAllDiversions()", "Stop diverting and replay all diversions"), +("purgeAllDiversions()", "Stop diverting and purge all diversions"), +("getFilter()", "Get current filter"), +("resetFilter()", "Reset filter; no filtering"), +("nullFilter()", "Install null filter"), +("setFilter(shortcut)", "Install new filter or filter chain"), +("attachFilter(shortcut)", "Attach single filter to end of current chain"), +("areHooksEnabled()", "Return whether or not hooks are enabled"), +("enableHooks()", "Enable hooks (default)"), +("disableHooks()", "Disable hook invocation"), +("getHooks()", "Get all the hooks"), +("clearHooks()", "Clear all hooks"), +("addHook(hook, [i])", "Register the hook (optionally insert)"), +("removeHook(hook)", "Remove an already-registered hook from name"), +("invokeHook(name_, ...)", "Manually invoke hook"), +("getCallback()", "Get interpreter callback"), +("registerCallback(callback)", "Register callback with interpreter"), +("deregisterCallback()", "Deregister callback from interpreter"), +("invokeCallback(contents)", "Invoke the callback directly"), +("Interpreter", "The interpreter class"), +] + +ENVIRONMENT_INFO = [ +(OPTIONS_ENV, "Specified options will be included"), +(PREFIX_ENV, "Specify the default prefix: -p <value>"), +(PSEUDO_ENV, "Specify name of pseudomodule: -m <value>"), +(FLATTEN_ENV, "Flatten empy pseudomodule if defined: -f"), +(RAW_ENV, "Show raw errors if defined: -r"), +(INTERACTIVE_ENV, "Enter interactive mode if defined: -i"), +(BUFFERED_ENV, "Fully buffered output if defined: -b"), +(NO_OVERRIDE_ENV, "Do not override sys.stdout if defined: -n"), +(UNICODE_ENV, "Enable Unicode subsystem: -n"), +(INPUT_ENCODING_ENV, "Unicode input encoding"), +(OUTPUT_ENCODING_ENV, "Unicode output encoding"), +(INPUT_ERRORS_ENV, "Unicode input error handler"), +(OUTPUT_ERRORS_ENV, "Unicode output error handler"), +] + +class Error(Exception): + """The base class for all EmPy errors.""" + pass + +EmpyError = EmPyError = Error # DEPRECATED + +class DiversionError(Error): + """An error related to diversions.""" + pass + +class FilterError(Error): + """An error related to filters.""" + pass + +class StackUnderflowError(Error): + """A stack underflow.""" + pass + +class SubsystemError(Error): + """An error associated with the Unicode subsystem.""" + pass + +class FlowError(Error): + """An exception related to control flow.""" + pass + +class ContinueFlow(FlowError): + """A continue control flow.""" + pass + +class BreakFlow(FlowError): + """A break control flow.""" + pass + +class ParseError(Error): + """A parse error occurred.""" + pass + +class TransientParseError(ParseError): + """A parse error occurred which may be resolved by feeding more data. + Such an error reaching the toplevel is an unexpected EOF error.""" + pass + + +class MetaError(Exception): + + """A wrapper around a real Python exception for including a copy of + the context.""" + + def __init__(self, contexts, exc): + Exception.__init__(self, exc) + self.contexts = contexts + self.exc = exc + + def __str__(self): + backtrace = map(lambda x: str(x), self.contexts) + return "%s: %s (%s)" % (self.exc.__class__, self.exc, \ + (string.join(backtrace, ', '))) + + +class Subsystem: + + """The subsystem class defers file creation so that it can create + Unicode-wrapped files if desired (and possible).""" + + def __init__(self): + self.useUnicode = False + self.inputEncoding = None + self.outputEncoding = None + self.errors = None + + def initialize(self, inputEncoding=None, outputEncoding=None, \ + inputErrors=None, outputErrors=None): + self.useUnicode = True + try: + unicode + import codecs + except (NameError, ImportError): + raise SubsystemError, "Unicode subsystem unavailable" + defaultEncoding = sys.getdefaultencoding() + if inputEncoding is None: + inputEncoding = defaultEncoding + self.inputEncoding = inputEncoding + if outputEncoding is None: + outputEncoding = defaultEncoding + self.outputEncoding = outputEncoding + if inputErrors is None: + inputErrors = DEFAULT_ERRORS + self.inputErrors = inputErrors + if outputErrors is None: + outputErrors = DEFAULT_ERRORS + self.outputErrors = outputErrors + + def assertUnicode(self): + if not self.useUnicode: + raise SubsystemError, "Unicode subsystem unavailable" + + def open(self, name, mode=None): + if self.useUnicode: + return self.unicodeOpen(name, mode) + else: + return self.defaultOpen(name, mode) + + def defaultOpen(self, name, mode=None): + if mode is None: + mode = 'r' + return open(name, mode) + + def unicodeOpen(self, name, mode=None): + import codecs + if mode is None: + mode = 'rb' + if mode.find('w') >= 0 or mode.find('a') >= 0: + encoding = self.outputEncoding + errors = self.outputErrors + else: + encoding = self.inputEncoding + errors = self.inputErrors + return codecs.open(name, mode, encoding, errors) + +theSubsystem = Subsystem() + + +class Stack: + + """A simple stack that behaves as a sequence (with 0 being the top + of the stack, not the bottom).""" + + def __init__(self, seq=None): + if seq is None: + seq = [] + self.data = seq + + def top(self): + """Access the top element on the stack.""" + try: + return self.data[-1] + except IndexError: + raise StackUnderflowError, "stack is empty for top" + + def pop(self): + """Pop the top element off the stack and return it.""" + try: + return self.data.pop() + except IndexError: + raise StackUnderflowError, "stack is empty for pop" + + def push(self, object): + """Push an element onto the top of the stack.""" + self.data.append(object) + + def filter(self, function): + """Filter the elements of the stack through the function.""" + self.data = filter(function, self.data) + + def purge(self): + """Purge the stack.""" + self.data = [] + + def clone(self): + """Create a duplicate of this stack.""" + return self.__class__(self.data[:]) + + def __nonzero__(self): return len(self.data) != 0 + def __len__(self): return len(self.data) + def __getitem__(self, index): return self.data[-(index + 1)] + + def __repr__(self): + return '<%s instance at 0x%x [%s]>' % \ + (self.__class__, id(self), \ + string.join(map(repr, self.data), ', ')) + + +class AbstractFile: + + """An abstracted file that, when buffered, will totally buffer the + file, including even the file open.""" + + def __init__(self, filename, mode='w', buffered=False): + # The calls below might throw, so start off by marking this + # file as "done." This way destruction of a not-completely- + # initialized AbstractFile will generate no further errors. + self.done = True + self.filename = filename + self.mode = mode + self.buffered = buffered + if buffered: + self.bufferFile = StringIO.StringIO() + else: + self.bufferFile = theSubsystem.open(filename, mode) + # Okay, we got this far, so the AbstractFile is initialized. + # Flag it as "not done." + self.done = False + + def __del__(self): + self.close() + + def write(self, data): + self.bufferFile.write(data) + + def writelines(self, data): + self.bufferFile.writelines(data) + + def flush(self): + self.bufferFile.flush() + + def close(self): + if not self.done: + self.commit() + self.done = True + + def commit(self): + if self.buffered: + file = theSubsystem.open(self.filename, self.mode) + file.write(self.bufferFile.getvalue()) + file.close() + else: + self.bufferFile.close() + + def abort(self): + if self.buffered: + self.bufferFile = None + else: + self.bufferFile.close() + self.bufferFile = None + self.done = True + + +class Diversion: + + """The representation of an active diversion. Diversions act as + (writable) file objects, and then can be recalled either as pure + strings or (readable) file objects.""" + + def __init__(self): + self.file = StringIO.StringIO() + + # These methods define the writable file-like interface for the + # diversion. + + def write(self, data): + self.file.write(data) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def flush(self): + self.file.flush() + + def close(self): + self.file.close() + + # These methods are specific to diversions. + + def asString(self): + """Return the diversion as a string.""" + return self.file.getvalue() + + def asFile(self): + """Return the diversion as a file.""" + return StringIO.StringIO(self.file.getvalue()) + + +class Stream: + + """A wrapper around an (output) file object which supports + diversions and filtering.""" + + def __init__(self, file): + self.file = file + self.currentDiversion = None + self.diversions = {} + self.filter = file + self.done = False + + def write(self, data): + if self.currentDiversion is None: + self.filter.write(data) + else: + self.diversions[self.currentDiversion].write(data) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def flush(self): + self.filter.flush() + + def close(self): + if not self.done: + self.undivertAll(True) + self.filter.close() + self.done = True + + def shortcut(self, shortcut): + """Take a filter shortcut and translate it into a filter, returning + it. Sequences don't count here; these should be detected + independently.""" + if shortcut == 0: + return NullFilter() + elif type(shortcut) is types.FunctionType or \ + type(shortcut) is types.BuiltinFunctionType or \ + type(shortcut) is types.BuiltinMethodType or \ + type(shortcut) is types.LambdaType: + return FunctionFilter(shortcut) + elif type(shortcut) is types.StringType: + return StringFilter(filter) + elif type(shortcut) is types.DictType: + raise NotImplementedError, "mapping filters not yet supported" + else: + # Presume it's a plain old filter. + return shortcut + + def last(self): + """Find the last filter in the current filter chain, or None if + there are no filters installed.""" + if self.filter is None: + return None + thisFilter, lastFilter = self.filter, None + while thisFilter is not None and thisFilter is not self.file: + lastFilter = thisFilter + thisFilter = thisFilter.next() + return lastFilter + + def install(self, shortcut=None): + """Install a new filter; None means no filter. Handle all the + special shortcuts for filters here.""" + # Before starting, execute a flush. + self.filter.flush() + if shortcut is None or shortcut == [] or shortcut == (): + # Shortcuts for "no filter." + self.filter = self.file + else: + if type(shortcut) in (types.ListType, types.TupleType): + shortcuts = list(shortcut) + else: + shortcuts = [shortcut] + # Run through the shortcut filter names, replacing them with + # full-fledged instances of Filter. + filters = [] + for shortcut in shortcuts: + filters.append(self.shortcut(shortcut)) + if len(filters) > 1: + # If there's more than one filter provided, chain them + # together. + lastFilter = None + for filter in filters: + if lastFilter is not None: + lastFilter.attach(filter) + lastFilter = filter + lastFilter.attach(self.file) + self.filter = filters[0] + else: + # If there's only one filter, assume that it's alone or it's + # part of a chain that has already been manually chained; + # just find the end. + filter = filters[0] + lastFilter = filter.last() + lastFilter.attach(self.file) + self.filter = filter + + def attach(self, shortcut): + """Attached a solitary filter (no sequences allowed here) at the + end of the current filter chain.""" + lastFilter = self.last() + if lastFilter is None: + # Just install it from scratch if there is no active filter. + self.install(shortcut) + else: + # Attach the last filter to this one, and this one to the file. + filter = self.shortcut(shortcut) + lastFilter.attach(filter) + filter.attach(self.file) + + def revert(self): + """Reset any current diversions.""" + self.currentDiversion = None + + def create(self, name): + """Create a diversion if one does not already exist, but do not + divert to it yet.""" + if name is None: + raise DiversionError, "diversion name must be non-None" + if not self.diversions.has_key(name): + self.diversions[name] = Diversion() + + def retrieve(self, name): + """Retrieve the given diversion.""" + if name is None: + raise DiversionError, "diversion name must be non-None" + if self.diversions.has_key(name): + return self.diversions[name] + else: + raise DiversionError, "nonexistent diversion: %s" % name + + def divert(self, name): + """Start diverting.""" + if name is None: + raise DiversionError, "diversion name must be non-None" + self.create(name) + self.currentDiversion = name + + def undivert(self, name, purgeAfterwards=False): + """Undivert a particular diversion.""" + if name is None: + raise DiversionError, "diversion name must be non-None" + if self.diversions.has_key(name): + diversion = self.diversions[name] + self.filter.write(diversion.asString()) + if purgeAfterwards: + self.purge(name) + else: + raise DiversionError, "nonexistent diversion: %s" % name + + def purge(self, name): + """Purge the specified diversion.""" + if name is None: + raise DiversionError, "diversion name must be non-None" + if self.diversions.has_key(name): + del self.diversions[name] + if self.currentDiversion == name: + self.currentDiversion = None + + def undivertAll(self, purgeAfterwards=True): + """Undivert all pending diversions.""" + if self.diversions: + self.revert() # revert before undiverting! + names = self.diversions.keys() + names.sort() + for name in names: + self.undivert(name) + if purgeAfterwards: + self.purge(name) + + def purgeAll(self): + """Eliminate all existing diversions.""" + if self.diversions: + self.diversions = {} + self.currentDiversion = None + + +class NullFile: + + """A simple class that supports all the file-like object methods + but simply does nothing at all.""" + + def __init__(self): pass + def write(self, data): pass + def writelines(self, lines): pass + def flush(self): pass + def close(self): pass + + +class UncloseableFile: + + """A simple class which wraps around a delegate file-like object + and lets everything through except close calls.""" + + def __init__(self, delegate): + self.delegate = delegate + + def write(self, data): + self.delegate.write(data) + + def writelines(self, lines): + self.delegate.writelines(data) + + def flush(self): + self.delegate.flush() + + def close(self): + """Eat this one.""" + pass + + +class ProxyFile: + + """The proxy file object that is intended to take the place of + sys.stdout. The proxy can manage a stack of file objects it is + writing to, and an underlying raw file object.""" + + def __init__(self, bottom): + self.stack = Stack() + self.bottom = bottom + + def current(self): + """Get the current stream to write to.""" + if self.stack: + return self.stack[-1][1] + else: + return self.bottom + + def push(self, interpreter): + self.stack.push((interpreter, interpreter.stream())) + + def pop(self, interpreter): + result = self.stack.pop() + assert interpreter is result[0] + + def clear(self, interpreter): + self.stack.filter(lambda x, i=interpreter: x[0] is not i) + + def write(self, data): + self.current().write(data) + + def writelines(self, lines): + self.current().writelines(lines) + + def flush(self): + self.current().flush() + + def close(self): + """Close the current file. If the current file is the bottom, then + close it and dispose of it.""" + current = self.current() + if current is self.bottom: + self.bottom = None + current.close() + + def _testProxy(self): pass + + +class Filter: + + """An abstract filter.""" + + def __init__(self): + if self.__class__ is Filter: + raise NotImplementedError + self.sink = None + + def next(self): + """Return the next filter/file-like object in the sequence, or None.""" + return self.sink + + def write(self, data): + """The standard write method; this must be overridden in subclasses.""" + raise NotImplementedError + + def writelines(self, lines): + """Standard writelines wrapper.""" + for line in lines: + self.write(line) + + def _flush(self): + """The _flush method should always flush the sink and should not + be overridden.""" + self.sink.flush() + + def flush(self): + """The flush method can be overridden.""" + self._flush() + + def close(self): + """Close the filter. Do an explicit flush first, then close the + sink.""" + self.flush() + self.sink.close() + + def attach(self, filter): + """Attach a filter to this one.""" + if self.sink is not None: + # If it's already attached, detach it first. + self.detach() + self.sink = filter + + def detach(self): + """Detach a filter from its sink.""" + self.flush() + self._flush() # do a guaranteed flush to just to be safe + self.sink = None + + def last(self): + """Find the last filter in this chain.""" + this, last = self, self + while this is not None: + last = this + this = this.next() + return last + +class NullFilter(Filter): + + """A filter that never sends any output to its sink.""" + + def write(self, data): pass + +class FunctionFilter(Filter): + + """A filter that works simply by pumping its input through a + function which maps strings into strings.""" + + def __init__(self, function): + Filter.__init__(self) + self.function = function + + def write(self, data): + self.sink.write(self.function(data)) + +class StringFilter(Filter): + + """A filter that takes a translation string (256 characters) and + filters any incoming data through it.""" + + def __init__(self, table): + if not (type(table) == types.StringType and len(table) == 256): + raise FilterError, "table must be 256-character string" + Filter.__init__(self) + self.table = table + + def write(self, data): + self.sink.write(string.translate(data, self.table)) + +class BufferedFilter(Filter): + + """A buffered filter is one that doesn't modify the source data + sent to the sink, but instead holds it for a time. The standard + variety only sends the data along when it receives a flush + command.""" + + def __init__(self): + Filter.__init__(self) + self.buffer = '' + + def write(self, data): + self.buffer = self.buffer + data + + def flush(self): + if self.buffer: + self.sink.write(self.buffer) + self._flush() + +class SizeBufferedFilter(BufferedFilter): + + """A size-buffered filter only in fixed size chunks (excepting the + final chunk).""" + + def __init__(self, bufferSize): + BufferedFilter.__init__(self) + self.bufferSize = bufferSize + + def write(self, data): + BufferedFilter.write(self, data) + while len(self.buffer) > self.bufferSize: + chunk, self.buffer = \ + self.buffer[:self.bufferSize], self.buffer[self.bufferSize:] + self.sink.write(chunk) + +class LineBufferedFilter(BufferedFilter): + + """A line-buffered filter only lets data through when it sees + whole lines.""" + + def __init__(self): + BufferedFilter.__init__(self) + + def write(self, data): + BufferedFilter.write(self, data) + chunks = string.split(self.buffer, '\n') + for chunk in chunks[:-1]: + self.sink.write(chunk + '\n') + self.buffer = chunks[-1] + +class MaximallyBufferedFilter(BufferedFilter): + + """A maximally-buffered filter only lets its data through on the final + close. It ignores flushes.""" + + def __init__(self): + BufferedFilter.__init__(self) + + def flush(self): pass + + def close(self): + if self.buffer: + BufferedFilter.flush(self) + self.sink.close() + + +class Context: + + """An interpreter context, which encapsulates a name, an input + file object, and a parser object.""" + + DEFAULT_UNIT = 'lines' + + def __init__(self, name, line=0, units=DEFAULT_UNIT): + self.name = name + self.line = line + self.units = units + self.pause = False + + def bump(self, quantity=1): + if self.pause: + self.pause = False + else: + self.line = self.line + quantity + + def identify(self): + return self.name, self.line + + def __str__(self): + if self.units == self.DEFAULT_UNIT: + return "%s:%s" % (self.name, self.line) + else: + return "%s:%s[%s]" % (self.name, self.line, self.units) + + +class Hook: + + """The base class for implementing hooks.""" + + def __init__(self): + self.interpreter = None + + def register(self, interpreter): + self.interpreter = interpreter + + def deregister(self, interpreter): + if interpreter is not self.interpreter: + raise Error, "hook not associated with this interpreter" + self.interpreter = None + + def push(self): + self.interpreter.push() + + def pop(self): + self.interpreter.pop() + + def null(self): pass + + def atStartup(self): pass + def atReady(self): pass + def atFinalize(self): pass + def atShutdown(self): pass + def atParse(self, scanner, locals): pass + def atToken(self, token): pass + def atHandle(self, meta): pass + def atInteract(self): pass + + def beforeInclude(self, name, file, locals): pass + def afterInclude(self): pass + + def beforeExpand(self, string, locals): pass + def afterExpand(self, result): pass + + def beforeFile(self, name, file, locals): pass + def afterFile(self): pass + + def beforeBinary(self, name, file, chunkSize, locals): pass + def afterBinary(self): pass + + def beforeString(self, name, string, locals): pass + def afterString(self): pass + + def beforeQuote(self, string): pass + def afterQuote(self, result): pass + + def beforeEscape(self, string, more): pass + def afterEscape(self, result): pass + + def beforeControl(self, type, rest, locals): pass + def afterControl(self): pass + + def beforeSignificate(self, key, value, locals): pass + def afterSignificate(self): pass + + def beforeAtomic(self, name, value, locals): pass + def afterAtomic(self): pass + + def beforeMulti(self, name, values, locals): pass + def afterMulti(self): pass + + def beforeImport(self, name, locals): pass + def afterImport(self): pass + + def beforeClause(self, catch, locals): pass + def afterClause(self, exception, variable): pass + + def beforeSerialize(self, expression, locals): pass + def afterSerialize(self): pass + + def beforeDefined(self, name, locals): pass + def afterDefined(self, result): pass + + def beforeLiteral(self, text): pass + def afterLiteral(self): pass + + def beforeEvaluate(self, expression, locals): pass + def afterEvaluate(self, result): pass + + def beforeExecute(self, statements, locals): pass + def afterExecute(self): pass + + def beforeSingle(self, source, locals): pass + def afterSingle(self): pass + +class VerboseHook(Hook): + + """A verbose hook that reports all information received by the + hook interface. This class dynamically scans the Hook base class + to ensure that all hook methods are properly represented.""" + + EXEMPT_ATTRIBUTES = ['register', 'deregister', 'push', 'pop'] + + def __init__(self, output=sys.stderr): + Hook.__init__(self) + self.output = output + self.indent = 0 + + class FakeMethod: + """This is a proxy method-like object.""" + def __init__(self, hook, name): + self.hook = hook + self.name = name + + def __call__(self, **keywords): + self.hook.output.write("%s%s: %s\n" % \ + (' ' * self.hook.indent, \ + self.name, repr(keywords))) + + for attribute in dir(Hook): + if attribute[:1] != '_' and \ + attribute not in self.EXEMPT_ATTRIBUTES: + self.__dict__[attribute] = FakeMethod(self, attribute) + + +class Token: + + """An element of expansion.""" + + def run(self, interpreter, locals): + raise NotImplementedError + + def string(self): + raise NotImplementedError + + def __str__(self): return self.string() + +class NullToken(Token): + """A chunk of data not containing markups.""" + def __init__(self, data): + self.data = data + + def run(self, interpreter, locals): + interpreter.write(self.data) + + def string(self): + return self.data + +class ExpansionToken(Token): + """A token that involves an expansion.""" + def __init__(self, prefix, first): + self.prefix = prefix + self.first = first + + def scan(self, scanner): + pass + + def run(self, interpreter, locals): + pass + +class WhitespaceToken(ExpansionToken): + """A whitespace markup.""" + def string(self): + return '%s%s' % (self.prefix, self.first) + +class LiteralToken(ExpansionToken): + """A literal markup.""" + def run(self, interpreter, locals): + interpreter.write(self.first) + + def string(self): + return '%s%s' % (self.prefix, self.first) + +class PrefixToken(ExpansionToken): + """A prefix markup.""" + def run(self, interpreter, locals): + interpreter.write(interpreter.prefix) + + def string(self): + return self.prefix * 2 + +class CommentToken(ExpansionToken): + """A comment markup.""" + def scan(self, scanner): + loc = scanner.find('\n') + if loc >= 0: + self.comment = scanner.chop(loc, 1) + else: + raise TransientParseError, "comment expects newline" + + def string(self): + return '%s#%s\n' % (self.prefix, self.comment) + +class ContextNameToken(ExpansionToken): + """A context name change markup.""" + def scan(self, scanner): + loc = scanner.find('\n') + if loc >= 0: + self.name = string.strip(scanner.chop(loc, 1)) + else: + raise TransientParseError, "context name expects newline" + + def run(self, interpreter, locals): + context = interpreter.context() + context.name = self.name + +class ContextLineToken(ExpansionToken): + """A context line change markup.""" + def scan(self, scanner): + loc = scanner.find('\n') + if loc >= 0: + try: + self.line = int(scanner.chop(loc, 1)) + except ValueError: + raise ParseError, "context line requires integer" + else: + raise TransientParseError, "context line expects newline" + + def run(self, interpreter, locals): + context = interpreter.context() + context.line = self.line + context.pause = True + +class EscapeToken(ExpansionToken): + """An escape markup.""" + def scan(self, scanner): + try: + code = scanner.chop(1) + result = None + if code in '()[]{}\'\"\\': # literals + result = code + elif code == '0': # NUL + result = '\x00' + elif code == 'a': # BEL + result = '\x07' + elif code == 'b': # BS + result = '\x08' + elif code == 'd': # decimal code + decimalCode = scanner.chop(3) + result = chr(string.atoi(decimalCode, 10)) + elif code == 'e': # ESC + result = '\x1b' + elif code == 'f': # FF + result = '\x0c' + elif code == 'h': # DEL + result = '\x7f' + elif code == 'n': # LF (newline) + result = '\x0a' + elif code == 'N': # Unicode character name + theSubsystem.assertUnicode() + import unicodedata + if scanner.chop(1) != '{': + raise ParseError, r"Unicode name escape should be \N{...}" + i = scanner.find('}') + name = scanner.chop(i, 1) + try: + result = unicodedata.lookup(name) + except KeyError: + raise SubsystemError, \ + "unknown Unicode character name: %s" % name + elif code == 'o': # octal code + octalCode = scanner.chop(3) + result = chr(string.atoi(octalCode, 8)) + elif code == 'q': # quaternary code + quaternaryCode = scanner.chop(4) + result = chr(string.atoi(quaternaryCode, 4)) + elif code == 'r': # CR + result = '\x0d' + elif code in 's ': # SP + result = ' ' + elif code == 't': # HT + result = '\x09' + elif code in 'u': # Unicode 16-bit hex literal + theSubsystem.assertUnicode() + hexCode = scanner.chop(4) + result = unichr(string.atoi(hexCode, 16)) + elif code in 'U': # Unicode 32-bit hex literal + theSubsystem.assertUnicode() + hexCode = scanner.chop(8) + result = unichr(string.atoi(hexCode, 16)) + elif code == 'v': # VT + result = '\x0b' + elif code == 'x': # hexadecimal code + hexCode = scanner.chop(2) + result = chr(string.atoi(hexCode, 16)) + elif code == 'z': # EOT + result = '\x04' + elif code == '^': # control character + controlCode = string.upper(scanner.chop(1)) + if controlCode >= '@' and controlCode <= '`': + result = chr(ord(controlCode) - ord('@')) + elif controlCode == '?': + result = '\x7f' + else: + raise ParseError, "invalid escape control code" + else: + raise ParseError, "unrecognized escape code" + assert result is not None + self.code = result + except ValueError: + raise ParseError, "invalid numeric escape code" + + def run(self, interpreter, locals): + interpreter.write(self.code) + + def string(self): + return '%s\\x%02x' % (self.prefix, ord(self.code)) + +class SignificatorToken(ExpansionToken): + """A significator markup.""" + def scan(self, scanner): + loc = scanner.find('\n') + if loc >= 0: + line = scanner.chop(loc, 1) + if not line: + raise ParseError, "significator must have nonblank key" + if line[0] in ' \t\v\n': + raise ParseError, "no whitespace between % and key" + # Work around a subtle CPython-Jython difference by stripping + # the string before splitting it: 'a '.split(None, 1) has two + # elements in Jython 2.1). + fields = string.split(string.strip(line), None, 1) + if len(fields) == 2 and fields[1] == '': + fields.pop() + self.key = fields[0] + if len(fields) < 2: + fields.append(None) + self.key, self.valueCode = fields + else: + raise TransientParseError, "significator expects newline" + + def run(self, interpreter, locals): + value = self.valueCode + if value is not None: + value = interpreter.evaluate(string.strip(value), locals) + interpreter.significate(self.key, value) + + def string(self): + if self.valueCode is None: + return '%s%%%s\n' % (self.prefix, self.key) + else: + return '%s%%%s %s\n' % (self.prefix, self.key, self.valueCode) + +class ExpressionToken(ExpansionToken): + """An expression markup.""" + def scan(self, scanner): + z = scanner.complex('(', ')', 0) + try: + q = scanner.next('$', 0, z, True) + except ParseError: + q = z + try: + i = scanner.next('?', 0, q, True) + try: + j = scanner.next('!', i, q, True) + except ParseError: + try: + j = scanner.next(':', i, q, True) # DEPRECATED + except ParseError: + j = q + except ParseError: + i = j = q + code = scanner.chop(z, 1) + self.testCode = code[:i] + self.thenCode = code[i + 1:j] + self.elseCode = code[j + 1:q] + self.exceptCode = code[q + 1:z] + + def run(self, interpreter, locals): + try: + result = interpreter.evaluate(self.testCode, locals) + if self.thenCode: + if result: + result = interpreter.evaluate(self.thenCode, locals) + else: + if self.elseCode: + result = interpreter.evaluate(self.elseCode, locals) + else: + result = None + except SyntaxError: + # Don't catch syntax errors; let them through. + raise + except: + if self.exceptCode: + result = interpreter.evaluate(self.exceptCode, locals) + else: + raise + if result is not None: + interpreter.write(str(result)) + + def string(self): + result = self.testCode + if self.thenCode: + result = result + '?' + self.thenCode + if self.elseCode: + result = result + '!' + self.elseCode + if self.exceptCode: + result = result + '$' + self.exceptCode + return '%s(%s)' % (self.prefix, result) + +class StringLiteralToken(ExpansionToken): + """A string token markup.""" + def scan(self, scanner): + scanner.retreat() + assert scanner[0] == self.first + i = scanner.quote() + self.literal = scanner.chop(i) + + def run(self, interpreter, locals): + interpreter.literal(self.literal) + + def string(self): + return '%s%s' % (self.prefix, self.literal) + +class SimpleExpressionToken(ExpansionToken): + """A simple expression markup.""" + def scan(self, scanner): + i = scanner.simple() + self.code = self.first + scanner.chop(i) + + def run(self, interpreter, locals): + interpreter.serialize(self.code, locals) + + def string(self): + return '%s%s' % (self.prefix, self.code) + +class ReprToken(ExpansionToken): + """A repr markup.""" + def scan(self, scanner): + i = scanner.next('`', 0) + self.code = scanner.chop(i, 1) + + def run(self, interpreter, locals): + interpreter.write(repr(interpreter.evaluate(self.code, locals))) + + def string(self): + return '%s`%s`' % (self.prefix, self.code) + +class InPlaceToken(ExpansionToken): + """An in-place markup.""" + def scan(self, scanner): + i = scanner.next(':', 0) + j = scanner.next(':', i + 1) + self.code = scanner.chop(i, j - i + 1) + + def run(self, interpreter, locals): + interpreter.write("%s:%s:" % (interpreter.prefix, self.code)) + try: + interpreter.serialize(self.code, locals) + finally: + interpreter.write(":") + + def string(self): + return '%s:%s::' % (self.prefix, self.code) + +class StatementToken(ExpansionToken): + """A statement markup.""" + def scan(self, scanner): + i = scanner.complex('{', '}', 0) + self.code = scanner.chop(i, 1) + + def run(self, interpreter, locals): + interpreter.execute(self.code, locals) + + def string(self): + return '%s{%s}' % (self.prefix, self.code) + +class CustomToken(ExpansionToken): + """A custom markup.""" + def scan(self, scanner): + i = scanner.complex('<', '>', 0) + self.contents = scanner.chop(i, 1) + + def run(self, interpreter, locals): + interpreter.invokeCallback(self.contents) + + def string(self): + return '%s<%s>' % (self.prefix, self.contents) + +class ControlToken(ExpansionToken): + + """A control token.""" + + PRIMARY_TYPES = ['if', 'for', 'while', 'try', 'def'] + SECONDARY_TYPES = ['elif', 'else', 'except', 'finally'] + TERTIARY_TYPES = ['continue', 'break'] + GREEDY_TYPES = ['if', 'elif', 'for', 'while', 'def', 'end'] + END_TYPES = ['end'] + + IN_RE = re.compile(r"\bin\b") + + def scan(self, scanner): + scanner.acquire() + i = scanner.complex('[', ']', 0) + self.contents = scanner.chop(i, 1) + fields = string.split(string.strip(self.contents), ' ', 1) + if len(fields) > 1: + self.type, self.rest = fields + else: + self.type = fields[0] + self.rest = None + self.subtokens = [] + if self.type in self.GREEDY_TYPES and self.rest is None: + raise ParseError, "control '%s' needs arguments" % self.type + if self.type in self.PRIMARY_TYPES: + self.subscan(scanner, self.type) + self.kind = 'primary' + elif self.type in self.SECONDARY_TYPES: + self.kind = 'secondary' + elif self.type in self.TERTIARY_TYPES: + self.kind = 'tertiary' + elif self.type in self.END_TYPES: + self.kind = 'end' + else: + raise ParseError, "unknown control markup: '%s'" % self.type + scanner.release() + + def subscan(self, scanner, primary): + """Do a subscan for contained tokens.""" + while True: + token = scanner.one() + if token is None: + raise TransientParseError, \ + "control '%s' needs more tokens" % primary + if isinstance(token, ControlToken) and \ + token.type in self.END_TYPES: + if token.rest != primary: + raise ParseError, \ + "control must end with 'end %s'" % primary + break + self.subtokens.append(token) + + def build(self, allowed=None): + """Process the list of subtokens and divide it into a list of + 2-tuples, consisting of the dividing tokens and the list of + subtokens that follow them. If allowed is specified, it will + represent the list of the only secondary markup types which + are allowed.""" + if allowed is None: + allowed = SECONDARY_TYPES + result = [] + latest = [] + result.append((self, latest)) + for subtoken in self.subtokens: + if isinstance(subtoken, ControlToken) and \ + subtoken.kind == 'secondary': + if subtoken.type not in allowed: + raise ParseError, \ + "control unexpected secondary: '%s'" % subtoken.type + latest = [] + result.append((subtoken, latest)) + else: + latest.append(subtoken) + return result + + def run(self, interpreter, locals): + interpreter.invoke('beforeControl', type=self.type, rest=self.rest, \ + locals=locals) + if self.type == 'if': + info = self.build(['elif', 'else']) + elseTokens = None + if info[-1][0].type == 'else': + elseTokens = info.pop()[1] + for secondary, subtokens in info: + if secondary.type not in ('if', 'elif'): + raise ParseError, \ + "control 'if' unexpected secondary: '%s'" % secondary.type + if interpreter.evaluate(secondary.rest, locals): + self.subrun(subtokens, interpreter, locals) + break + else: + if elseTokens: + self.subrun(elseTokens, interpreter, locals) + elif self.type == 'for': + sides = self.IN_RE.split(self.rest, 1) + if len(sides) != 2: + raise ParseError, "control expected 'for x in seq'" + iterator, sequenceCode = sides + info = self.build(['else']) + elseTokens = None + if info[-1][0].type == 'else': + elseTokens = info.pop()[1] + if len(info) != 1: + raise ParseError, "control 'for' expects at most one 'else'" + sequence = interpreter.evaluate(sequenceCode, locals) + for element in sequence: + try: + interpreter.assign(iterator, element, locals) + self.subrun(info[0][1], interpreter, locals) + except ContinueFlow: + continue + except BreakFlow: + break + else: + if elseTokens: + self.subrun(elseTokens, interpreter, locals) + elif self.type == 'while': + testCode = self.rest + info = self.build(['else']) + elseTokens = None + if info[-1][0].type == 'else': + elseTokens = info.pop()[1] + if len(info) != 1: + raise ParseError, "control 'while' expects at most one 'else'" + atLeastOnce = False + while True: + try: + if not interpreter.evaluate(testCode, locals): + break + atLeastOnce = True + self.subrun(info[0][1], interpreter, locals) + except ContinueFlow: + continue + except BreakFlow: + break + if not atLeastOnce and elseTokens: + self.subrun(elseTokens, interpreter, locals) + elif self.type == 'try': + info = self.build(['except', 'finally']) + if len(info) == 1: + raise ParseError, "control 'try' needs 'except' or 'finally'" + type = info[-1][0].type + if type == 'except': + for secondary, _tokens in info[1:]: + if secondary.type != 'except': + raise ParseError, \ + "control 'try' cannot have 'except' and 'finally'" + else: + assert type == 'finally' + if len(info) != 2: + raise ParseError, \ + "control 'try' can only have one 'finally'" + if type == 'except': + try: + self.subrun(info[0][1], interpreter, locals) + except FlowError: + raise + except Exception, e: + for secondary, tokens in info[1:]: + exception, variable = interpreter.clause(secondary.rest) + if variable is not None: + interpreter.assign(variable, e) + if isinstance(e, exception): + self.subrun(tokens, interpreter, locals) + break + else: + raise + else: + try: + self.subrun(info[0][1], interpreter, locals) + finally: + self.subrun(info[1][1], interpreter, locals) + elif self.type == 'continue': + raise ContinueFlow, "control 'continue' without 'for', 'while'" + elif self.type == 'break': + raise BreakFlow, "control 'break' without 'for', 'while'" + elif self.type == 'def': + signature = self.rest + definition = self.substring() + code = 'def %s:\n' \ + ' r"""%s"""\n' \ + ' return %s.expand(r"""%s""", locals())\n' % \ + (signature, definition, interpreter.pseudo, definition) + interpreter.execute(code, locals) + elif self.type == 'end': + raise ParseError, "control 'end' requires primary markup" + else: + raise ParseError, \ + "control '%s' cannot be at this level" % self.type + interpreter.invoke('afterControl') + + def subrun(self, tokens, interpreter, locals): + """Execute a sequence of tokens.""" + for token in tokens: + token.run(interpreter, locals) + + def substring(self): + return string.join(map(str, self.subtokens), '') + + def string(self): + if self.kind == 'primary': + return '%s[%s]%s%s[end %s]' % \ + (self.prefix, self.contents, self.substring(), \ + self.prefix, self.type) + else: + return '%s[%s]' % (self.prefix, self.contents) + + +class Scanner: + + """A scanner holds a buffer for lookahead parsing and has the + ability to scan for special symbols and indicators in that + buffer.""" + + # This is the token mapping table that maps first characters to + # token classes. + TOKEN_MAP = [ + (None, PrefixToken), + (' \t\v\r\n', WhitespaceToken), + (')]}', LiteralToken), + ('\\', EscapeToken), + ('#', CommentToken), + ('?', ContextNameToken), + ('!', ContextLineToken), + ('%', SignificatorToken), + ('(', ExpressionToken), + (IDENTIFIER_FIRST_CHARS, SimpleExpressionToken), + ('\'\"', StringLiteralToken), + ('`', ReprToken), + (':', InPlaceToken), + ('[', ControlToken), + ('{', StatementToken), + ('<', CustomToken), + ] + + def __init__(self, prefix, data=''): + self.prefix = prefix + self.pointer = 0 + self.buffer = data + self.lock = 0 + + def __nonzero__(self): return self.pointer < len(self.buffer) + def __len__(self): return len(self.buffer) - self.pointer + def __getitem__(self, index): return self.buffer[self.pointer + index] + + def __getslice__(self, start, stop): + if stop > len(self): + stop = len(self) + return self.buffer[self.pointer + start:self.pointer + stop] + + def advance(self, count=1): + """Advance the pointer count characters.""" + self.pointer = self.pointer + count + + def retreat(self, count=1): + self.pointer = self.pointer - count + if self.pointer < 0: + raise ParseError, "can't retreat back over synced out chars" + + def set(self, data): + """Start the scanner digesting a new batch of data; start the pointer + over from scratch.""" + self.pointer = 0 + self.buffer = data + + def feed(self, data): + """Feed some more data to the scanner.""" + self.buffer = self.buffer + data + + def chop(self, count=None, slop=0): + """Chop the first count + slop characters off the front, and return + the first count. If count is not specified, then return + everything.""" + if count is None: + assert slop == 0 + count = len(self) + if count > len(self): + raise TransientParseError, "not enough data to read" + result = self[:count] + self.advance(count + slop) + return result + + def acquire(self): + """Lock the scanner so it doesn't destroy data on sync.""" + self.lock = self.lock + 1 + + def release(self): + """Unlock the scanner.""" + self.lock = self.lock - 1 + + def sync(self): + """Sync up the buffer with the read head.""" + if self.lock == 0 and self.pointer != 0: + self.buffer = self.buffer[self.pointer:] + self.pointer = 0 + + def unsync(self): + """Undo changes; reset the read head.""" + if self.pointer != 0: + self.lock = 0 + self.pointer = 0 + + def rest(self): + """Get the remainder of the buffer.""" + return self[:] + + def read(self, i=0, count=1): + """Read count chars starting from i; raise a transient error if + there aren't enough characters remaining.""" + if len(self) < i + count: + raise TransientParseError, "need more data to read" + else: + return self[i:i + count] + + def check(self, i, archetype=None): + """Scan for the next single or triple quote, with the specified + archetype. Return the found quote or None.""" + quote = None + if self[i] in '\'\"': + quote = self[i] + if len(self) - i < 3: + for j in range(i, len(self)): + if self[i] == quote: + return quote + else: + raise TransientParseError, "need to scan for rest of quote" + if self[i + 1] == self[i + 2] == quote: + quote = quote * 3 + if quote is not None: + if archetype is None: + return quote + else: + if archetype == quote: + return quote + elif len(archetype) < len(quote) and archetype[0] == quote[0]: + return archetype + else: + return None + else: + return None + + def find(self, sub, start=0, end=None): + """Find the next occurrence of the character, or return -1.""" + if end is not None: + return string.find(self.rest(), sub, start, end) + else: + return string.find(self.rest(), sub, start) + + def last(self, char, start=0, end=None): + """Find the first character that is _not_ the specified character.""" + if end is None: + end = len(self) + i = start + while i < end: + if self[i] != char: + return i + i = i + 1 + else: + raise TransientParseError, "expecting other than %s" % char + + def next(self, target, start=0, end=None, mandatory=False): + """Scan for the next occurrence of one of the characters in + the target string; optionally, make the scan mandatory.""" + if mandatory: + assert end is not None + quote = None + if end is None: + end = len(self) + i = start + while i < end: + newQuote = self.check(i, quote) + if newQuote: + if newQuote == quote: + quote = None + else: + quote = newQuote + i = i + len(newQuote) + else: + c = self[i] + if quote: + if c == '\\': + i = i + 1 + else: + if c in target: + return i + i = i + 1 + else: + if mandatory: + raise ParseError, "expecting %s, not found" % target + else: + raise TransientParseError, "expecting ending character" + + def quote(self, start=0, end=None, mandatory=False): + """Scan for the end of the next quote.""" + assert self[start] in '\'\"' + quote = self.check(start) + if end is None: + end = len(self) + i = start + len(quote) + while i < end: + newQuote = self.check(i, quote) + if newQuote: + i = i + len(newQuote) + if newQuote == quote: + return i + else: + c = self[i] + if c == '\\': + i = i + 1 + i = i + 1 + else: + if mandatory: + raise ParseError, "expecting end of string literal" + else: + raise TransientParseError, "expecting end of string literal" + + def nested(self, enter, exit, start=0, end=None): + """Scan from i for an ending sequence, respecting entries and exits + only.""" + depth = 0 + if end is None: + end = len(self) + i = start + while i < end: + c = self[i] + if c == enter: + depth = depth + 1 + elif c == exit: + depth = depth - 1 + if depth < 0: + return i + i = i + 1 + else: + raise TransientParseError, "expecting end of complex expression" + + def complex(self, enter, exit, start=0, end=None, skip=None): + """Scan from i for an ending sequence, respecting quotes, + entries and exits.""" + quote = None + depth = 0 + if end is None: + end = len(self) + last = None + i = start + while i < end: + newQuote = self.check(i, quote) + if newQuote: + if newQuote == quote: + quote = None + else: + quote = newQuote + i = i + len(newQuote) + else: + c = self[i] + if quote: + if c == '\\': + i = i + 1 + else: + if skip is None or last != skip: + if c == enter: + depth = depth + 1 + elif c == exit: + depth = depth - 1 + if depth < 0: + return i + last = c + i = i + 1 + else: + raise TransientParseError, "expecting end of complex expression" + + def word(self, start=0): + """Scan from i for a simple word.""" + length = len(self) + i = start + while i < length: + if not self[i] in IDENTIFIER_CHARS: + return i + i = i + 1 + else: + raise TransientParseError, "expecting end of word" + + def phrase(self, start=0): + """Scan from i for a phrase (e.g., 'word', 'f(a, b, c)', 'a[i]', or + combinations like 'x[i](a)'.""" + # Find the word. + i = self.word(start) + while i < len(self) and self[i] in '([{': + enter = self[i] + if enter == '{': + raise ParseError, "curly braces can't open simple expressions" + exit = ENDING_CHARS[enter] + i = self.complex(enter, exit, i + 1) + 1 + return i + + def simple(self, start=0): + """Scan from i for a simple expression, which consists of one + more phrases separated by dots.""" + i = self.phrase(start) + length = len(self) + while i < length and self[i] == '.': + i = self.phrase(i) + # Make sure we don't end with a trailing dot. + while i > 0 and self[i - 1] == '.': + i = i - 1 + return i + + def one(self): + """Parse and return one token, or None if the scanner is empty.""" + if not self: + return None + if not self.prefix: + loc = -1 + else: + loc = self.find(self.prefix) + if loc < 0: + # If there's no prefix in the buffer, then set the location to + # the end so the whole thing gets processed. + loc = len(self) + if loc == 0: + # If there's a prefix at the beginning of the buffer, process + # an expansion. + prefix = self.chop(1) + assert prefix == self.prefix + first = self.chop(1) + if first == self.prefix: + first = None + for firsts, factory in self.TOKEN_MAP: + if firsts is None: + if first is None: + break + elif first in firsts: + break + else: + raise ParseError, "unknown markup: %s%s" % (self.prefix, first) + token = factory(self.prefix, first) + try: + token.scan(self) + except TransientParseError: + # If a transient parse error occurs, reset the buffer pointer + # so we can (conceivably) try again later. + self.unsync() + raise + else: + # Process everything up to loc as a null token. + data = self.chop(loc) + token = NullToken(data) + self.sync() + return token + + +class Interpreter: + + """An interpreter can process chunks of EmPy code.""" + + # Constants. + + VERSION = __version__ + SIGNIFICATOR_RE_SUFFIX = SIGNIFICATOR_RE_SUFFIX + SIGNIFICATOR_RE_STRING = None + + # Types. + + Interpreter = None # define this below to prevent a circular reference + Hook = Hook # DEPRECATED + Filter = Filter # DEPRECATED + NullFilter = NullFilter # DEPRECATED + FunctionFilter = FunctionFilter # DEPRECATED + StringFilter = StringFilter # DEPRECATED + BufferedFilter = BufferedFilter # DEPRECATED + SizeBufferedFilter = SizeBufferedFilter # DEPRECATED + LineBufferedFilter = LineBufferedFilter # DEPRECATED + MaximallyBufferedFilter = MaximallyBufferedFilter # DEPRECATED + + # Tables. + + ESCAPE_CODES = {0x00: '0', 0x07: 'a', 0x08: 'b', 0x1b: 'e', 0x0c: 'f', \ + 0x7f: 'h', 0x0a: 'n', 0x0d: 'r', 0x09: 't', 0x0b: 'v', \ + 0x04: 'z'} + + ASSIGN_TOKEN_RE = re.compile(r"[_a-zA-Z][_a-zA-Z0-9]*|\(|\)|,") + + DEFAULT_OPTIONS = {BANGPATH_OPT: True, + BUFFERED_OPT: False, + RAW_OPT: False, + EXIT_OPT: True, + FLATTEN_OPT: False, + OVERRIDE_OPT: True, + CALLBACK_OPT: False} + + _wasProxyInstalled = False # was a proxy installed? + + # Construction, initialization, destruction. + + def __init__(self, output=None, argv=None, prefix=DEFAULT_PREFIX, \ + pseudo=None, options=None, globals=None, hooks=None): + self.interpreter = self # DEPRECATED + # Set up the stream. + if output is None: + output = UncloseableFile(sys.__stdout__) + self.output = output + self.prefix = prefix + if pseudo is None: + pseudo = DEFAULT_PSEUDOMODULE_NAME + self.pseudo = pseudo + if argv is None: + argv = [DEFAULT_SCRIPT_NAME] + self.argv = argv + self.args = argv[1:] + if options is None: + options = {} + self.options = options + # Initialize any hooks. + self.hooksEnabled = None # special sentinel meaning "false until added" + self.hooks = [] + if hooks is None: + hooks = [] + for hook in hooks: + self.register(hook) + # Initialize callback. + self.callback = None + # Finalizers. + self.finals = [] + # The interpreter stacks. + self.contexts = Stack() + self.streams = Stack() + # Now set up the globals. + self.globals = globals + self.fix() + self.history = Stack() + # Install a proxy stdout if one hasn't been already. + self.installProxy() + # Finally, reset the state of all the stacks. + self.reset() + # Okay, now flatten the namespaces if that option has been set. + if self.options.get(FLATTEN_OPT, False): + self.flatten() + # Set up old pseudomodule attributes. + if prefix is None: + self.SIGNIFICATOR_RE_STRING = None + else: + self.SIGNIFICATOR_RE_STRING = prefix + self.SIGNIFICATOR_RE_SUFFIX + self.Interpreter = self.__class__ + # Done. Now declare that we've started up. + self.invoke('atStartup') + + def __del__(self): + self.shutdown() + + def __repr__(self): + return '<%s pseudomodule/interpreter at 0x%x>' % \ + (self.pseudo, id(self)) + + def ready(self): + """Declare the interpreter ready for normal operations.""" + self.invoke('atReady') + + def fix(self): + """Reset the globals, stamping in the pseudomodule.""" + if self.globals is None: + self.globals = {} + # Make sure that there is no collision between two interpreters' + # globals. + if self.globals.has_key(self.pseudo): + if self.globals[self.pseudo] is not self: + raise Error, "interpreter globals collision" + self.globals[self.pseudo] = self + + def unfix(self): + """Remove the pseudomodule (if present) from the globals.""" + UNWANTED_KEYS = [self.pseudo, '__builtins__'] + for unwantedKey in UNWANTED_KEYS: + if self.globals.has_key(unwantedKey): + del self.globals[unwantedKey] + + def update(self, other): + """Update the current globals dictionary with another dictionary.""" + self.globals.update(other) + self.fix() + + def clear(self): + """Clear out the globals dictionary with a brand new one.""" + self.globals = {} + self.fix() + + def save(self, deep=True): + if deep: + copyMethod = copy.deepcopy + else: + copyMethod = copy.copy + """Save a copy of the current globals on the history stack.""" + self.unfix() + self.history.push(copyMethod(self.globals)) + self.fix() + + def restore(self, destructive=True): + """Restore the topmost historic globals.""" + if destructive: + fetchMethod = self.history.pop + else: + fetchMethod = self.history.top + self.unfix() + self.globals = fetchMethod() + self.fix() + + def shutdown(self): + """Declare this interpreting session over; close the stream file + object. This method is idempotent.""" + if self.streams is not None: + try: + self.finalize() + self.invoke('atShutdown') + while self.streams: + stream = self.streams.pop() + stream.close() + finally: + self.streams = None + + def ok(self): + """Is the interpreter still active?""" + return self.streams is not None + + # Writeable file-like methods. + + def write(self, data): + self.stream().write(data) + + def writelines(self, stuff): + self.stream().writelines(stuff) + + def flush(self): + self.stream().flush() + + def close(self): + self.shutdown() + + # Stack-related activity. + + def context(self): + return self.contexts.top() + + def stream(self): + return self.streams.top() + + def reset(self): + self.contexts.purge() + self.streams.purge() + self.streams.push(Stream(self.output)) + if self.options.get(OVERRIDE_OPT, True): + sys.stdout.clear(self) + + def push(self): + if self.options.get(OVERRIDE_OPT, True): + sys.stdout.push(self) + + def pop(self): + if self.options.get(OVERRIDE_OPT, True): + sys.stdout.pop(self) + + # Higher-level operations. + + def include(self, fileOrFilename, locals=None): + """Do an include pass on a file or filename.""" + if type(fileOrFilename) is types.StringType: + # Either it's a string representing a filename ... + filename = fileOrFilename + name = filename + file = theSubsystem.open(filename, 'r') + else: + # ... or a file object. + file = fileOrFilename + name = "<%s>" % str(file.__class__) + self.invoke('beforeInclude', name=name, file=file, locals=locals) + self.file(file, name, locals) + self.invoke('afterInclude') + + def expand(self, data, locals=None): + """Do an explicit expansion on a subordinate stream.""" + outFile = StringIO.StringIO() + stream = Stream(outFile) + self.invoke('beforeExpand', string=data, locals=locals) + self.streams.push(stream) + try: + self.string(data, '<expand>', locals) + stream.flush() + expansion = outFile.getvalue() + self.invoke('afterExpand', result=expansion) + return expansion + finally: + self.streams.pop() + + def quote(self, data): + """Quote the given string so that if it were expanded it would + evaluate to the original.""" + self.invoke('beforeQuote', string=data) + scanner = Scanner(self.prefix, data) + result = [] + i = 0 + try: + j = scanner.next(self.prefix, i) + result.append(data[i:j]) + result.append(self.prefix * 2) + i = j + 1 + except TransientParseError: + pass + result.append(data[i:]) + result = string.join(result, '') + self.invoke('afterQuote', result=result) + return result + + def escape(self, data, more=''): + """Escape a string so that nonprintable characters are replaced + with compatible EmPy expansions.""" + self.invoke('beforeEscape', string=data, more=more) + result = [] + for char in data: + if char < ' ' or char > '~': + charOrd = ord(char) + if Interpreter.ESCAPE_CODES.has_key(charOrd): + result.append(self.prefix + '\\' + \ + Interpreter.ESCAPE_CODES[charOrd]) + else: + result.append(self.prefix + '\\x%02x' % charOrd) + elif char in more: + result.append(self.prefix + '\\' + char) + else: + result.append(char) + result = string.join(result, '') + self.invoke('afterEscape', result=result) + return result + + # Processing. + + def wrap(self, callable, args): + """Wrap around an application of a callable and handle errors. + Return whether no error occurred.""" + try: + apply(callable, args) + self.reset() + return True + except KeyboardInterrupt, e: + # Handle keyboard interrupts specially: we should always exit + # from these. + self.fail(e, True) + except Exception, e: + # A standard exception (other than a keyboard interrupt). + self.fail(e) + except: + # If we get here, then either it's an exception not derived from + # Exception or it's a string exception, so get the error type + # from the sys module. + e = sys.exc_type + self.fail(e) + # An error occurred if we leak through to here, so do cleanup. + self.reset() + return False + + def interact(self): + """Perform interaction.""" + self.invoke('atInteract') + done = False + while not done: + result = self.wrap(self.file, (sys.stdin, '<interact>')) + if self.options.get(EXIT_OPT, True): + done = True + else: + if result: + done = True + else: + self.reset() + + def fail(self, error, fatal=False): + """Handle an actual error that occurred.""" + if self.options.get(BUFFERED_OPT, False): + try: + self.output.abort() + except AttributeError: + # If the output file object doesn't have an abort method, + # something got mismatched, but it's too late to do + # anything about it now anyway, so just ignore it. + pass + meta = self.meta(error) + self.handle(meta) + if self.options.get(RAW_OPT, False): + raise + if fatal or self.options.get(EXIT_OPT, True): + sys.exit(FAILURE_CODE) + + def file(self, file, name='<file>', locals=None): + """Parse the entire contents of a file-like object, line by line.""" + context = Context(name) + self.contexts.push(context) + self.invoke('beforeFile', name=name, file=file, locals=locals) + scanner = Scanner(self.prefix) + first = True + done = False + while not done: + self.context().bump() + line = file.readline() + if first: + if self.options.get(BANGPATH_OPT, True) and self.prefix: + # Replace a bangpath at the beginning of the first line + # with an EmPy comment. + if string.find(line, BANGPATH) == 0: + line = self.prefix + '#' + line[2:] + first = False + if line: + scanner.feed(line) + else: + done = True + self.safe(scanner, done, locals) + self.invoke('afterFile') + self.contexts.pop() + + def binary(self, file, name='<binary>', chunkSize=0, locals=None): + """Parse the entire contents of a file-like object, in chunks.""" + if chunkSize <= 0: + chunkSize = DEFAULT_CHUNK_SIZE + context = Context(name, units='bytes') + self.contexts.push(context) + self.invoke('beforeBinary', name=name, file=file, \ + chunkSize=chunkSize, locals=locals) + scanner = Scanner(self.prefix) + done = False + while not done: + chunk = file.read(chunkSize) + if chunk: + scanner.feed(chunk) + else: + done = True + self.safe(scanner, done, locals) + self.context().bump(len(chunk)) + self.invoke('afterBinary') + self.contexts.pop() + + def string(self, data, name='<string>', locals=None): + """Parse a string.""" + context = Context(name) + self.contexts.push(context) + self.invoke('beforeString', name=name, string=data, locals=locals) + context.bump() + scanner = Scanner(self.prefix, data) + self.safe(scanner, True, locals) + self.invoke('afterString') + self.contexts.pop() + + def safe(self, scanner, final=False, locals=None): + """Do a protected parse. Catch transient parse errors; if + final is true, then make a final pass with a terminator, + otherwise ignore the transient parse error (more data is + pending).""" + try: + self.parse(scanner, locals) + except TransientParseError: + if final: + # If the buffer doesn't end with a newline, try tacking on + # a dummy terminator. + buffer = scanner.rest() + if buffer and buffer[-1] != '\n': + scanner.feed(self.prefix + '\n') + # A TransientParseError thrown from here is a real parse + # error. + self.parse(scanner, locals) + + def parse(self, scanner, locals=None): + """Parse and run as much from this scanner as possible.""" + self.invoke('atParse', scanner=scanner, locals=locals) + while True: + token = scanner.one() + if token is None: + break + self.invoke('atToken', token=token) + token.run(self, locals) + + # Medium-level evaluation and execution. + + def tokenize(self, name): + """Take an lvalue string and return a name or a (possibly recursive) + list of names.""" + result = [] + stack = [result] + for garbage in self.ASSIGN_TOKEN_RE.split(name): + garbage = string.strip(garbage) + if garbage: + raise ParseError, "unexpected assignment token: '%s'" % garbage + tokens = self.ASSIGN_TOKEN_RE.findall(name) + # While processing, put a None token at the start of any list in which + # commas actually appear. + for token in tokens: + if token == '(': + stack.append([]) + elif token == ')': + top = stack.pop() + if len(top) == 1: + top = top[0] # no None token means that it's not a 1-tuple + elif top[0] is None: + del top[0] # remove the None token for real tuples + stack[-1].append(top) + elif token == ',': + if len(stack[-1]) == 1: + stack[-1].insert(0, None) + else: + stack[-1].append(token) + # If it's a 1-tuple at the top level, turn it into a real subsequence. + if result and result[0] is None: + result = [result[1:]] + if len(result) == 1: + return result[0] + else: + return result + + def significate(self, key, value=None, locals=None): + """Declare a significator.""" + self.invoke('beforeSignificate', key=key, value=value, locals=locals) + name = '__%s__' % key + self.atomic(name, value, locals) + self.invoke('afterSignificate') + + def atomic(self, name, value, locals=None): + """Do an atomic assignment.""" + self.invoke('beforeAtomic', name=name, value=value, locals=locals) + if locals is None: + self.globals[name] = value + else: + locals[name] = value + self.invoke('afterAtomic') + + def multi(self, names, values, locals=None): + """Do a (potentially recursive) assignment.""" + self.invoke('beforeMulti', names=names, values=values, locals=locals) + # No zip in 1.5, so we have to do it manually. + i = 0 + try: + values = tuple(values) + except TypeError: + raise TypeError, "unpack non-sequence" + if len(names) != len(values): + raise ValueError, "unpack tuple of wrong size" + for i in range(len(names)): + name = names[i] + if type(name) is types.StringType: + self.atomic(name, values[i], locals) + else: + self.multi(name, values[i], locals) + self.invoke('afterMulti') + + def assign(self, name, value, locals=None): + """Do a potentially complex (including tuple unpacking) assignment.""" + left = self.tokenize(name) + # The return value of tokenize can either be a string or a list of + # (lists of) strings. + if type(left) is types.StringType: + self.atomic(left, value, locals) + else: + self.multi(left, value, locals) + + def import_(self, name, locals=None): + """Do an import.""" + self.invoke('beforeImport', name=name, locals=locals) + self.execute('import %s' % name, locals) + self.invoke('afterImport') + + def clause(self, catch, locals=None): + """Given the string representation of an except clause, turn it into + a 2-tuple consisting of the class name, and either a variable name + or None.""" + self.invoke('beforeClause', catch=catch, locals=locals) + if catch is None: + exceptionCode, variable = None, None + elif string.find(catch, ',') >= 0: + exceptionCode, variable = string.split(string.strip(catch), ',', 1) + variable = string.strip(variable) + else: + exceptionCode, variable = string.strip(catch), None + if not exceptionCode: + exception = Exception + else: + exception = self.evaluate(exceptionCode, locals) + self.invoke('afterClause', exception=exception, variable=variable) + return exception, variable + + def serialize(self, expression, locals=None): + """Do an expansion, involving evaluating an expression, then + converting it to a string and writing that string to the + output if the evaluation is not None.""" + self.invoke('beforeSerialize', expression=expression, locals=locals) + result = self.evaluate(expression, locals) + if result is not None: + self.write(str(result)) + self.invoke('afterSerialize') + + def defined(self, name, locals=None): + """Return a Boolean indicating whether or not the name is + defined either in the locals or the globals.""" + self.invoke('beforeDefined', name=name, local=local) + if locals is not None: + if locals.has_key(name): + result = True + else: + result = False + elif self.globals.has_key(name): + result = True + else: + result = False + self.invoke('afterDefined', result=result) + + def literal(self, text): + """Process a string literal.""" + self.invoke('beforeLiteral', text=text) + self.serialize(text) + self.invoke('afterLiteral') + + # Low-level evaluation and execution. + + def evaluate(self, expression, locals=None): + """Evaluate an expression.""" + if expression in ('1', 'True'): return True + if expression in ('0', 'False'): return False + self.push() + try: + self.invoke('beforeEvaluate', \ + expression=expression, locals=locals) + if locals is not None: + result = eval(expression, self.globals, locals) + else: + result = eval(expression, self.globals) + self.invoke('afterEvaluate', result=result) + return result + finally: + self.pop() + + def execute(self, statements, locals=None): + """Execute a statement.""" + # If there are any carriage returns (as opposed to linefeeds/newlines) + # in the statements code, then remove them. Even on DOS/Windows + # platforms, + if string.find(statements, '\r') >= 0: + statements = string.replace(statements, '\r', '') + # If there are no newlines in the statements code, then strip any + # leading or trailing whitespace. + if string.find(statements, '\n') < 0: + statements = string.strip(statements) + self.push() + try: + self.invoke('beforeExecute', \ + statements=statements, locals=locals) + if locals is not None: + exec statements in self.globals, locals + else: + exec statements in self.globals + self.invoke('afterExecute') + finally: + self.pop() + + def single(self, source, locals=None): + """Execute an expression or statement, just as if it were + entered into the Python interactive interpreter.""" + self.push() + try: + self.invoke('beforeSingle', \ + source=source, locals=locals) + code = compile(source, '<single>', 'single') + if locals is not None: + exec code in self.globals, locals + else: + exec code in self.globals + self.invoke('afterSingle') + finally: + self.pop() + + # Hooks. + + def register(self, hook, prepend=False): + """Register the provided hook.""" + hook.register(self) + if self.hooksEnabled is None: + # A special optimization so that hooks can be effectively + # disabled until one is added or they are explicitly turned on. + self.hooksEnabled = True + if prepend: + self.hooks.insert(0, hook) + else: + self.hooks.append(hook) + + def deregister(self, hook): + """Remove an already registered hook.""" + hook.deregister(self) + self.hooks.remove(hook) + + def invoke(self, _name, **keywords): + """Invoke the hook(s) associated with the hook name, should they + exist.""" + if self.hooksEnabled: + for hook in self.hooks: + hook.push() + try: + method = getattr(hook, _name) + apply(method, (), keywords) + finally: + hook.pop() + + def finalize(self): + """Execute any remaining final routines.""" + self.push() + self.invoke('atFinalize') + try: + # Pop them off one at a time so they get executed in reverse + # order and we remove them as they're executed in case something + # bad happens. + while self.finals: + final = self.finals.pop() + final() + finally: + self.pop() + + # Error handling. + + def meta(self, exc=None): + """Construct a MetaError for the interpreter's current state.""" + return MetaError(self.contexts.clone(), exc) + + def handle(self, meta): + """Handle a MetaError.""" + first = True + self.invoke('atHandle', meta=meta) + for context in meta.contexts: + if first: + if meta.exc is not None: + desc = "error: %s: %s" % (meta.exc.__class__, meta.exc) + else: + desc = "error" + else: + desc = "from this context" + first = False + sys.stderr.write('%s: %s\n' % (context, desc)) + + def installProxy(self): + """Install a proxy if necessary.""" + # Unfortunately, there's no surefire way to make sure that installing + # a sys.stdout proxy is idempotent, what with different interpreters + # running from different modules. The best we can do here is to try + # manipulating the proxy's test function ... + try: + sys.stdout._testProxy() + except AttributeError: + # ... if the current stdout object doesn't have one, then check + # to see if we think _this_ particularly Interpreter class has + # installed it before ... + if Interpreter._wasProxyInstalled: + # ... and if so, we have a proxy problem. + raise Error, "interpreter stdout proxy lost" + else: + # Otherwise, install the proxy and set the flag. + sys.stdout = ProxyFile(sys.stdout) + Interpreter._wasProxyInstalled = True + + # + # Pseudomodule routines. + # + + # Identification. + + def identify(self): + """Identify the topmost context with a 2-tuple of the name and + line number.""" + return self.context().identify() + + def atExit(self, callable): + """Register a function to be called at exit.""" + self.finals.append(callable) + + # Context manipulation. + + def pushContext(self, name='<unnamed>', line=0): + """Create a new context and push it.""" + self.contexts.push(Context(name, line)) + + def popContext(self): + """Pop the top context.""" + self.contexts.pop() + + def setContextName(self, name): + """Set the name of the topmost context.""" + context = self.context() + context.name = name + + def setContextLine(self, line): + """Set the name of the topmost context.""" + context = self.context() + context.line = line + + setName = setContextName # DEPRECATED + setLine = setContextLine # DEPRECATED + + # Globals manipulation. + + def getGlobals(self): + """Retrieve the globals.""" + return self.globals + + def setGlobals(self, globals): + """Set the globals to the specified dictionary.""" + self.globals = globals + self.fix() + + def updateGlobals(self, otherGlobals): + """Merge another mapping object into this interpreter's globals.""" + self.update(otherGlobals) + + def clearGlobals(self): + """Clear out the globals with a brand new dictionary.""" + self.clear() + + def saveGlobals(self, deep=True): + """Save a copy of the globals off onto the history stack.""" + self.save(deep) + + def restoreGlobals(self, destructive=True): + """Restore the most recently saved copy of the globals.""" + self.restore(destructive) + + # Hook support. + + def areHooksEnabled(self): + """Return whether or not hooks are presently enabled.""" + if self.hooksEnabled is None: + return True + else: + return self.hooksEnabled + + def enableHooks(self): + """Enable hooks.""" + self.hooksEnabled = True + + def disableHooks(self): + """Disable hooks.""" + self.hooksEnabled = False + + def getHooks(self): + """Get the current hooks.""" + return self.hooks[:] + + def clearHooks(self): + """Clear all hooks.""" + self.hooks = [] + + def addHook(self, hook, prepend=False): + """Add a new hook; optionally insert it rather than appending it.""" + self.register(hook, prepend) + + def removeHook(self, hook): + """Remove a preexisting hook.""" + self.deregister(hook) + + def invokeHook(self, _name, **keywords): + """Manually invoke a hook.""" + apply(self.invoke, (_name,), keywords) + + # Callbacks. + + def getCallback(self): + """Get the callback registered with this interpreter, or None.""" + return self.callback + + def registerCallback(self, callback): + """Register a custom markup callback with this interpreter.""" + self.callback = callback + + def deregisterCallback(self): + """Remove any previously registered callback with this interpreter.""" + self.callback = None + + def invokeCallback(self, contents): + """Invoke the callback.""" + if self.callback is None: + if self.options.get(CALLBACK_OPT, False): + raise Error, "custom markup invoked with no defined callback" + else: + self.callback(contents) + + # Pseudomodule manipulation. + + def flatten(self, keys=None): + """Flatten the contents of the pseudo-module into the globals + namespace.""" + if keys is None: + keys = self.__dict__.keys() + self.__class__.__dict__.keys() + dict = {} + for key in keys: + # The pseudomodule is really a class instance, so we need to + # fumble use getattr instead of simply fumbling through the + # instance's __dict__. + dict[key] = getattr(self, key) + # Stomp everything into the globals namespace. + self.globals.update(dict) + + # Prefix. + + def getPrefix(self): + """Get the current prefix.""" + return self.prefix + + def setPrefix(self, prefix): + """Set the prefix.""" + self.prefix = prefix + + # Diversions. + + def stopDiverting(self): + """Stop any diverting.""" + self.stream().revert() + + def createDiversion(self, name): + """Create a diversion (but do not divert to it) if it does not + already exist.""" + self.stream().create(name) + + def retrieveDiversion(self, name): + """Retrieve the diversion object associated with the name.""" + return self.stream().retrieve(name) + + def startDiversion(self, name): + """Start diverting to the given diversion name.""" + self.stream().divert(name) + + def playDiversion(self, name): + """Play the given diversion and then purge it.""" + self.stream().undivert(name, True) + + def replayDiversion(self, name): + """Replay the diversion without purging it.""" + self.stream().undivert(name, False) + + def purgeDiversion(self, name): + """Eliminate the given diversion.""" + self.stream().purge(name) + + def playAllDiversions(self): + """Play all existing diversions and then purge them.""" + self.stream().undivertAll(True) + + def replayAllDiversions(self): + """Replay all existing diversions without purging them.""" + self.stream().undivertAll(False) + + def purgeAllDiversions(self): + """Purge all existing diversions.""" + self.stream().purgeAll() + + def getCurrentDiversion(self): + """Get the name of the current diversion.""" + return self.stream().currentDiversion + + def getAllDiversions(self): + """Get the names of all existing diversions.""" + names = self.stream().diversions.keys() + names.sort() + return names + + # Filter. + + def resetFilter(self): + """Reset the filter so that it does no filtering.""" + self.stream().install(None) + + def nullFilter(self): + """Install a filter that will consume all text.""" + self.stream().install(0) + + def getFilter(self): + """Get the current filter.""" + filter = self.stream().filter + if filter is self.stream().file: + return None + else: + return filter + + def setFilter(self, shortcut): + """Set the filter.""" + self.stream().install(shortcut) + + def attachFilter(self, shortcut): + """Attach a single filter to the end of the current filter chain.""" + self.stream().attach(shortcut) + + +class Document: + + """A representation of an individual EmPy document, as used by a + processor.""" + + def __init__(self, ID, filename): + self.ID = ID + self.filename = filename + self.significators = {} + + +class Processor: + + """An entity which is capable of processing a hierarchy of EmPy + files and building a dictionary of document objects associated + with them describing their significator contents.""" + + DEFAULT_EMPY_EXTENSIONS = ('.em',) + SIGNIFICATOR_RE = re.compile(SIGNIFICATOR_RE_STRING) + + def __init__(self, factory=Document): + self.factory = factory + self.documents = {} + + def identifier(self, pathname, filename): return filename + + def clear(self): + self.documents = {} + + def scan(self, basename, extensions=DEFAULT_EMPY_EXTENSIONS): + if type(extensions) is types.StringType: + extensions = (extensions,) + def _noCriteria(x): + return True + def _extensionsCriteria(pathname, extensions=extensions): + if extensions: + for extension in extensions: + if pathname[-len(extension):] == extension: + return True + return False + else: + return True + self.directory(basename, _noCriteria, _extensionsCriteria, None) + self.postprocess() + + def postprocess(self): + pass + + def directory(self, basename, dirCriteria, fileCriteria, depth=None): + if depth is not None: + if depth <= 0: + return + else: + depth = depth - 1 + filenames = os.listdir(basename) + for filename in filenames: + pathname = os.path.join(basename, filename) + if os.path.isdir(pathname): + if dirCriteria(pathname): + self.directory(pathname, dirCriteria, fileCriteria, depth) + elif os.path.isfile(pathname): + if fileCriteria(pathname): + documentID = self.identifier(pathname, filename) + document = self.factory(documentID, pathname) + self.file(document, open(pathname)) + self.documents[documentID] = document + + def file(self, document, file): + while True: + line = file.readline() + if not line: + break + self.line(document, line) + + def line(self, document, line): + match = self.SIGNIFICATOR_RE.search(line) + if match: + key, valueS = match.groups() + valueS = string.strip(valueS) + if valueS: + value = eval(valueS) + else: + value = None + document.significators[key] = value + + +def expand(_data, _globals=None, \ + _argv=None, _prefix=DEFAULT_PREFIX, _pseudo=None, _options=None, \ + **_locals): + """Do an atomic expansion of the given source data, creating and + shutting down an interpreter dedicated to the task. The sys.stdout + object is saved off and then replaced before this function + returns.""" + if len(_locals) == 0: + # If there were no keyword arguments specified, don't use a locals + # dictionary at all. + _locals = None + output = NullFile() + interpreter = Interpreter(output, argv=_argv, prefix=_prefix, \ + pseudo=_pseudo, options=_options, \ + globals=_globals) + if interpreter.options.get(OVERRIDE_OPT, True): + oldStdout = sys.stdout + try: + result = interpreter.expand(_data, _locals) + finally: + interpreter.shutdown() + if _globals is not None: + interpreter.unfix() # remove pseudomodule to prevent clashes + if interpreter.options.get(OVERRIDE_OPT, True): + sys.stdout = oldStdout + return result + +def environment(name, default=None): + """Get data from the current environment. If the default is True + or False, then presume that we're only interested in the existence + or non-existence of the environment variable.""" + if os.environ.has_key(name): + # Do the True/False test by value for future compatibility. + if default == False or default == True: + return True + else: + return os.environ[name] + else: + return default + +def info(table): + DEFAULT_LEFT = 28 + maxLeft = 0 + maxRight = 0 + for left, right in table: + if len(left) > maxLeft: + maxLeft = len(left) + if len(right) > maxRight: + maxRight = len(right) + FORMAT = ' %%-%ds %%s\n' % max(maxLeft, DEFAULT_LEFT) + for left, right in table: + if right.find('\n') >= 0: + for right in right.split('\n'): + sys.stderr.write(FORMAT % (left, right)) + left = '' + else: + sys.stderr.write(FORMAT % (left, right)) + +def usage(verbose=True): + """Print usage information.""" + programName = sys.argv[0] + def warn(line=''): + sys.stderr.write("%s\n" % line) + warn("""\ +Usage: %s [options] [<filename, or '-' for stdin> [<argument>...]] +Welcome to EmPy version %s.""" % (programName, __version__)) + warn() + warn("Valid options:") + info(OPTION_INFO) + if verbose: + warn() + warn("The following markups are supported:") + info(MARKUP_INFO) + warn() + warn("Valid escape sequences are:") + info(ESCAPE_INFO) + warn() + warn("The %s pseudomodule contains the following attributes:" % \ + DEFAULT_PSEUDOMODULE_NAME) + info(PSEUDOMODULE_INFO) + warn() + warn("The following environment variables are recognized:") + info(ENVIRONMENT_INFO) + warn() + warn(USAGE_NOTES) + else: + warn() + warn("Type %s -H for more extensive help." % programName) + +def invoke(args): + """Run a standalone instance of an EmPy interpeter.""" + # Initialize the options. + _output = None + _options = {BUFFERED_OPT: environment(BUFFERED_ENV, False), + RAW_OPT: environment(RAW_ENV, False), + EXIT_OPT: True, + FLATTEN_OPT: environment(FLATTEN_ENV, False), + OVERRIDE_OPT: not environment(NO_OVERRIDE_ENV, False), + CALLBACK_OPT: False} + _preprocessing = [] + _prefix = environment(PREFIX_ENV, DEFAULT_PREFIX) + _pseudo = environment(PSEUDO_ENV, None) + _interactive = environment(INTERACTIVE_ENV, False) + _extraArguments = environment(OPTIONS_ENV) + _binary = -1 # negative for not, 0 for default size, positive for size + _unicode = environment(UNICODE_ENV, False) + _unicodeInputEncoding = environment(INPUT_ENCODING_ENV, None) + _unicodeOutputEncoding = environment(OUTPUT_ENCODING_ENV, None) + _unicodeInputErrors = environment(INPUT_ERRORS_ENV, None) + _unicodeOutputErrors = environment(OUTPUT_ERRORS_ENV, None) + _hooks = [] + _pauseAtEnd = False + _relativePath = False + if _extraArguments is not None: + _extraArguments = string.split(_extraArguments) + args = _extraArguments + args + # Parse the arguments. + pairs, remainder = getopt.getopt(args, 'VhHvkp:m:frino:a:buBP:I:D:E:F:', ['version', 'help', 'extended-help', 'verbose', 'null-hook', 'suppress-errors', 'prefix=', 'no-prefix', 'module=', 'flatten', 'raw-errors', 'interactive', 'no-override-stdout', 'binary', 'chunk-size=', 'output=' 'append=', 'preprocess=', 'import=', 'define=', 'execute=', 'execute-file=', 'buffered-output', 'pause-at-end', 'relative-path', 'no-callback-error', 'no-bangpath-processing', 'unicode', 'unicode-encoding=', 'unicode-input-encoding=', 'unicode-output-encoding=', 'unicode-errors=', 'unicode-input-errors=', 'unicode-output-errors=']) + for option, argument in pairs: + if option in ('-V', '--version'): + sys.stderr.write("%s version %s\n" % (__program__, __version__)) + return + elif option in ('-h', '--help'): + usage(False) + return + elif option in ('-H', '--extended-help'): + usage(True) + return + elif option in ('-v', '--verbose'): + _hooks.append(VerboseHook()) + elif option in ('--null-hook',): + _hooks.append(Hook()) + elif option in ('-k', '--suppress-errors'): + _options[EXIT_OPT] = False + _interactive = True # suppress errors implies interactive mode + elif option in ('-m', '--module'): + _pseudo = argument + elif option in ('-f', '--flatten'): + _options[FLATTEN_OPT] = True + elif option in ('-p', '--prefix'): + _prefix = argument + elif option in ('--no-prefix',): + _prefix = None + elif option in ('-r', '--raw-errors'): + _options[RAW_OPT] = True + elif option in ('-i', '--interactive'): + _interactive = True + elif option in ('-n', '--no-override-stdout'): + _options[OVERRIDE_OPT] = False + elif option in ('-o', '--output'): + _output = argument, 'w', _options[BUFFERED_OPT] + elif option in ('-a', '--append'): + _output = argument, 'a', _options[BUFFERED_OPT] + elif option in ('-b', '--buffered-output'): + _options[BUFFERED_OPT] = True + elif option in ('-B',): # DEPRECATED + _options[BUFFERED_OPT] = True + elif option in ('--binary',): + _binary = 0 + elif option in ('--chunk-size',): + _binary = int(argument) + elif option in ('-P', '--preprocess'): + _preprocessing.append(('pre', argument)) + elif option in ('-I', '--import'): + for module in string.split(argument, ','): + module = string.strip(module) + _preprocessing.append(('import', module)) + elif option in ('-D', '--define'): + _preprocessing.append(('define', argument)) + elif option in ('-E', '--execute'): + _preprocessing.append(('exec', argument)) + elif option in ('-F', '--execute-file'): + _preprocessing.append(('file', argument)) + elif option in ('-u', '--unicode'): + _unicode = True + elif option in ('--pause-at-end',): + _pauseAtEnd = True + elif option in ('--relative-path',): + _relativePath = True + elif option in ('--no-callback-error',): + _options[CALLBACK_OPT] = True + elif option in ('--no-bangpath-processing',): + _options[BANGPATH_OPT] = False + elif option in ('--unicode-encoding',): + _unicodeInputEncoding = _unicodeOutputEncoding = argument + elif option in ('--unicode-input-encoding',): + _unicodeInputEncoding = argument + elif option in ('--unicode-output-encoding',): + _unicodeOutputEncoding = argument + elif option in ('--unicode-errors',): + _unicodeInputErrors = _unicodeOutputErrors = argument + elif option in ('--unicode-input-errors',): + _unicodeInputErrors = argument + elif option in ('--unicode-output-errors',): + _unicodeOutputErrors = argument + # Set up the Unicode subsystem if required. + if _unicode or \ + _unicodeInputEncoding or _unicodeOutputEncoding or \ + _unicodeInputErrors or _unicodeOutputErrors: + theSubsystem.initialize(_unicodeInputEncoding, \ + _unicodeOutputEncoding, \ + _unicodeInputErrors, _unicodeOutputErrors) + # Now initialize the output file if something has already been selected. + if _output is not None: + _output = apply(AbstractFile, _output) + # Set up the main filename and the argument. + if not remainder: + remainder.append('-') + filename, arguments = remainder[0], remainder[1:] + # Set up the interpreter. + if _options[BUFFERED_OPT] and _output is None: + raise ValueError, "-b only makes sense with -o or -a arguments" + if _prefix == 'None': + _prefix = None + if _prefix and type(_prefix) is types.StringType and len(_prefix) != 1: + raise Error, "prefix must be single-character string" + interpreter = Interpreter(output=_output, \ + argv=remainder, \ + prefix=_prefix, \ + pseudo=_pseudo, \ + options=_options, \ + hooks=_hooks) + try: + # Execute command-line statements. + i = 0 + for which, thing in _preprocessing: + if which == 'pre': + command = interpreter.file + target = theSubsystem.open(thing, 'r') + name = thing + elif which == 'define': + command = interpreter.string + if string.find(thing, '=') >= 0: + target = '%s{%s}' % (_prefix, thing) + else: + target = '%s{%s = None}' % (_prefix, thing) + name = '<define:%d>' % i + elif which == 'exec': + command = interpreter.string + target = '%s{%s}' % (_prefix, thing) + name = '<exec:%d>' % i + elif which == 'file': + command = interpreter.string + name = '<file:%d (%s)>' % (i, thing) + target = '%s{execfile("""%s""")}' % (_prefix, thing) + elif which == 'import': + command = interpreter.string + name = '<import:%d>' % i + target = '%s{import %s}' % (_prefix, thing) + else: + assert 0 + interpreter.wrap(command, (target, name)) + i = i + 1 + # Now process the primary file. + interpreter.ready() + if filename == '-': + if not _interactive: + name = '<stdin>' + path = '' + file = sys.stdin + else: + name, file = None, None + else: + name = filename + file = theSubsystem.open(filename, 'r') + path = os.path.split(filename)[0] + if _relativePath: + sys.path.insert(0, path) + if file is not None: + if _binary < 0: + interpreter.wrap(interpreter.file, (file, name)) + else: + chunkSize = _binary + interpreter.wrap(interpreter.binary, (file, name, chunkSize)) + # If we're supposed to go interactive afterwards, do it. + if _interactive: + interpreter.interact() + finally: + interpreter.shutdown() + # Finally, if we should pause at the end, do it. + if _pauseAtEnd: + try: + raw_input() + except EOFError: + pass + +def main(): + invoke(sys.argv[1:]) + +if __name__ == '__main__': main() diff --git a/py-bin/lib/jon/__init__.py b/py-bin/lib/jon/__init__.py new file mode 100644 index 0000000..1f42b03 --- /dev/null +++ b/py-bin/lib/jon/__init__.py @@ -0,0 +1,3 @@ +# $Id: __init__.py,v 1.7 2004/03/04 17:29:18 jribbens Exp $ + +__version__ = "0.06" diff --git a/py-bin/lib/jon/cgi.py b/py-bin/lib/jon/cgi.py new file mode 100644 index 0000000..a1771c4 --- /dev/null +++ b/py-bin/lib/jon/cgi.py @@ -0,0 +1,673 @@ +# $Id: cgi.py,v 1.31 2004/03/24 12:14:31 jribbens Exp $ + +import sys, re, os, Cookie, errno +try: + import cStringIO as StringIO +except ImportError: + import StringIO + +"""Object-oriented CGI interface.""" + + +class Error(Exception): + """The base class for all exceptions thrown by this module.""" + pass + +class SequencingError(Error): + """The exception thrown when functions are called out of order.""" + """ + For example, if you try to call a function altering the headers of your + output when the headers have already been sent. + """ + pass + + +_url_encre = re.compile(r"[^A-Za-z0-9_.!~*()-]") # RFC 2396 section 2.3 +_url_decre = re.compile(r"%([0-9A-Fa-f]{2})") +_html_encre = re.compile("[&<>\"'+]") +# '+' is encoded because it is special in UTF-7, which the browser may select +# automatically if the content-type header does not specify the character +# encoding. This is paranoia and is not bulletproof, but it does no harm. See +# section 4 of www.microsoft.com/technet/security/news/csoverv.mspx +_html_encodes = { "&": "&", "<": "<", ">": ">", "\"": """, + "'": "'", "+": "+" } + +def html_encode(raw): + """Return the string parameter HTML-encoded.""" + """ + Specifically, the following characters are encoded as entities: + & < > " ' + + """ + if not isinstance(raw, unicode): + raw = str(raw) + return re.sub(_html_encre, lambda m: _html_encodes[m.group(0)], raw) + +def url_encode(raw): + """Return the string parameter URL-encoded.""" + if not isinstance(raw, unicode): + raw = str(raw) + return re.sub(_url_encre, lambda m: "%%%02X" % ord(m.group(0)), raw) + +def url_decode(enc): + """Return the string parameter URL-decoded (including '+' -> ' ').""" + s = enc.replace("+", " ") + return re.sub(_url_decre, lambda m: chr(int(m.group(1), 16)), s) + + +__UNDEF__ = [] + +def _lookup(name, frame, locls): + if name in locls: + return "local", locls[name] + if name in frame.f_globals: + return "global", frame.f_globals[name] + if "__builtins__" in frame.f_globals and \ + hasattr(frame.f_globals["__builtins__"], name): + return "builtin", getattr(frame.f_globals["__builtins__"], name) + return None, __UNDEF__ + + +def _scanvars(reader, frame, locls): + import tokenize, keyword + vrs = [] + lasttoken = None + parent = None + prefix = "" + for ttype, token, start, end, line in tokenize.generate_tokens(reader): + if ttype == tokenize.NEWLINE: + break + elif ttype == tokenize.NAME and token not in keyword.kwlist: + if lasttoken == ".": + if parent is not __UNDEF__: + value = getattr(parent, token, __UNDEF__) + vrs.append((prefix + token, prefix, value)) + else: + (where, value) = _lookup(token, frame, locls) + vrs.append((token, where, value)) + elif token == ".": + prefix += lasttoken + "." + parent = value + else: + parent = None + prefix = "" + lasttoken = token + return vrs + + +def _tb_encode(s): + return html_encode(s).replace(" ", " ") + + +def traceback(req, html=0): + import traceback, time, types, linecache, inspect, repr + repr = repr.Repr() + repr.maxdict = 10 + repr.maxlist = 10 + repr.maxtuple = 10 + repr.maxother = 200 + repr.maxstring = 200 + repr = repr.repr + (etype, evalue, etb) = sys.exc_info() + if type(etype) is types.ClassType: + etype = etype.__name__ + if html: + try: + req.clear_headers() + req.clear_output() + req.set_header("Content-Type", "text/html; charset=iso-8859-1") + except SequencingError: + req.write("</font></font></font></script></object></blockquote></pre>" + "</table></table></table></table></table></table></font></font>") + req.write("""\ +<?xml version="1.0" encoding="iso-8859-1"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head><title>jonpy traceback: %s</title> +<style type="text/css"><!-- +BODY { background-color: #f0f0f8; font-family: helveta, arial, sans-serif } +.tb_head { background-color: #6622aa; color: #ffffff } +.tb_title { font-size: x-large } +.tb_frame { background-color: #d8bbff } +.tb_lineno { font-size: smaller } +.tb_codehigh { background-color: #ffccee } +.tb_code { color: #909090 } +.tb_dump { color: #909090; font-size: smaller } +--></style> +</head><body> +<table width="100%%" cellspacing="0" cellpadding="0" border="0"> +<tr class="tb_head"> +<td valign="bottom" class="tb_title"> <br /><strong>%s</strong></td> +<td align="right" valign="bottom">%s<br />%s</td></tr></table> +<p>A problem occurred in a Python script. Here is the sequence of +function calls leading up to the error, with the most recent first.</p> +""" % (_tb_encode(etype), _tb_encode(etype), + "Python %s: %s" % (sys.version.split()[0], sys.executable), + time.ctime(time.time()))) + req.error("jonpy error: %s at %s\n" % (etype, time.ctime(time.time()))) + # this code adapted from the standard cgitb module + # unfortunately we cannot use that module directly, + # mainly because it won't allow us to output to the log + if html: + req.write("<p><strong>%s</strong>: %s" % (_tb_encode(etype), + _tb_encode(evalue))) + req.error("%s: %s\n" % (etype, evalue)) + #if type(evalue) is types.InstanceType: + # for name in dir(evalue): + # if html: + # req.write("\n<br /><tt> </tt>%s = %s" % + # (_tb_encode(name), _tb_encode(repr(getattr(evalue, name))))) + # req.error(" %s = %s\n" % (name, repr(getattr(evalue, name)))) + if html: + req.write("</p>\n") + frames = [] + records = inspect.getinnerframes(etb, 7) + records.reverse() + for frame, fn, lnum, func, lines, index in records: + if html: + req.write("""\ +<table width="100%" cellspacing="0" cellpadding="0" border="0">""") + fn = fn and os.path.abspath(fn) or "?" + args, varargs, varkw, locls = inspect.getargvalues(frame) + if func != "?": + fav = inspect.formatargvalues(args, varargs, varkw, locls) + if html: + req.write("<tr><td class=\"tb_frame\">%s in <strong>%s</strong>%s</td>" + "</tr>\n" % (_tb_encode(fn), _tb_encode(func), _tb_encode(fav))) + req.error("%s in %s%s\n" % (fn, func, fav)) + else: + if html: + req.write("<tr><td class=\"tb_head\">%s</td></tr>\n" % + (_tb_encode(fn),)) + req.error("%s\n" % (fn,)) + highlight = {} + def reader(lnum=[lnum]): + highlight[lnum[0]] = 1 + try: + return linecache.getline(fn, lnum[0]) + finally: + lnum[0] += 1 + vrs = _scanvars(reader, frame, locls) + if index is not None: + i = lnum - index + for line in lines: + if html: + if i in highlight: + style = "tb_codehigh" + else: + style = "tb_code" + req.write("<tr><td class=\"%s\"><code><span class=\"tb_lineno\">" + "%s</span> %s</code></td></tr>\n" % + (style, " " * (5 - len(str(i))) + str(i), _tb_encode(line))) + req.error("%s %s" % (" " * (5-len(str(i))) + str(i), line)) + i += 1 + done = {} + dump = [] + htdump = [] + for name, where, value in vrs: + if name in done: + continue + done[name] = 1 + if value is not __UNDEF__: + if where == "global": + dump.append("global %s = %s" % (name, repr(value))) + htdump.append("<em>global</em> <strong>%s</strong> = %s" % + (_tb_encode(name), _tb_encode(repr(value)))) + elif where == "builtin": + dump.append("builtin %s = %s" % (name, repr(value))) + htdump.append("<em>builtin</em> <strong>%s</strong> = %s" % + (_tb_encode(name), _tb_encode(repr(value)))) + elif where == "local": + dump.append("%s = %s" % (name, repr(value))) + htdump.append("<strong>%s</strong> = %s" % + (_tb_encode(name), _tb_encode(repr(value)))) + else: + dump.append("%s%s = %s" % (where, name.split(".")[-1], repr(value))) + htdump.append("%s<strong>%s</strong> = %s" % + (_tb_encode(where), _tb_encode(name.split(".")[-1]), + _tb_encode(repr(value)))) + else: + dump.append("%s undefined" % (name,)) + htdump.append("%s <em>undefined</em>" % (_tb_encode(name,))) + if html: + req.write("<tr><td class=\"tb_dump\">%s</td></tr>\n" % + (", ".join(htdump),)) + req.error(", ".join(dump) + "\n") + if html: + req.write("</table>\n") + if html: + req.write("</body></html>\n") + linecache.clearcache() + + +class Request(object): + """All the information about a CGI-style request, including how to respond.""" + """Headers are buffered in a list before being sent. They are either sent + on request, or when the first part of the body is sent. If requested, the + body output can be buffered as well.""" + + def __init__(self, handler_type): + """Create a Request object which uses handler_type as its handler.""" + """An object of type handler_type, which should be a subclass of + Handler, will be used to handle requests.""" + self._handler_type = handler_type + + def _init(self): + self._doneHeaders = 0 + self._headers = [] + self._bufferOutput = 1 + self._output = StringIO.StringIO() + self._pos = 0 + self.closed = 0 + try: + del self.params + except AttributeError: + pass + self.cookies = Cookie.SimpleCookie() + if self.environ.has_key("HTTP_COOKIE"): + self.cookies.load(self.environ["HTTP_COOKIE"]) + self.aborted = 0 + self.set_header("Content-Type", "text/html; charset=iso-8859-1") + + def __getattr__(self, name): + if name == "params": + self.params = {} + self._read_cgi_data(self.environ, self.stdin) + return self.__dict__["params"] + raise AttributeError, "%s instance has no attribute %s" % \ + (self.__class__.__name__, `name`) + + def close(self): + """Closes the output stream.""" + if not self.closed: + self.flush() + self._close() + self.closed = 1 + + def _check_open(self): + if self.closed: + raise ValueError, "I/O operation on closed file" + + def output_headers(self): + """Output the list of headers.""" + self._check_open() + if self._doneHeaders: + raise SequencingError, "output_headers() called twice" + for pair in self._headers: + self._write("%s: %s\r\n" % pair) + self._write("\r\n") + self._doneHeaders = 1 + + def clear_headers(self): + """Clear the list of headers.""" + self._check_open() + if self._doneHeaders: + raise SequencingError, "cannot clear_headers() after output_headers()" + self._headers = [] + + def add_header(self, hdr, val): + """Add a header to the list of headers.""" + self._check_open() + if self._doneHeaders: + raise SequencingError, \ + "cannot add_header(%s) after output_headers()" % `hdr` + self._headers.append((hdr, val)) + + def set_header(self, hdr, val): + """Add a header to the list of headers, replacing any existing values.""" + self._check_open() + if self._doneHeaders: + raise SequencingError, \ + "cannot set_header(%s) after output_headers()" % `hdr` + self.del_header(hdr) + self._headers.append((hdr, val)) + + def get_header(self, hdr, index=0): + """Retrieve a header from the list of headers.""" + i = 0 + hdr = hdr.lower() + for pair in self._headers: + if pair[0].lower() == hdr: + if i == index: + return pair[1] + i += 1 + return None + + def del_header(self, hdr): + """Removes all values for a header from the list of headers.""" + self._check_open() + if self._doneHeaders: + raise SequencingError, \ + "cannot del_header(%s) after output_headers()" % `hdr` + hdr = hdr.lower() + while 1: + for s in self._headers: + if s[0].lower() == hdr: + self._headers.remove(s) + break + else: + break + + def set_buffering(self, f): + """Specifies whether or not body output is buffered.""" + self._check_open() + if self._output.tell() > 0 and not f: + self.flush() + self._bufferOutput = f + + def flush(self): + """Flushes the body output.""" + self._check_open() + if not self._doneHeaders: + self.output_headers() + self._write(self._output.getvalue()) + self._pos += self._output.tell() + self._output.seek(0, 0) + self._output.truncate() + self._flush() + + def clear_output(self): + """Discards the contents of the body output buffer.""" + self._check_open() + if not self._bufferOutput: + raise SequencingError, "cannot clear output when not buffering" + self._output.seek(0, 0) + self._output.truncate() + + def error(self, s): + """Records an error message from the program.""" + """The output is logged or otherwise stored on the server. It does not + go to the client. + + Must be overridden by the sub-class.""" + raise NotImplementedError, "error must be overridden" + + def _write(self, s): + """Sends some data to the client.""" + """Must be overridden by the sub-class.""" + raise NotImplementedError, "_write must be overridden" + + def _flush(self): + """Flushes data to the client.""" + """May be overridden by the sub-class.""" + pass + + def _close(self): + """Closes the output stream.""" + """May be overridden by the sub-class.""" + pass + + def write(self, s): + """Sends some data to the client.""" + self._check_open() + s = str(s) + if self._bufferOutput: + self._output.write(s) + else: + if not self._doneHeaders: + self.output_headers() + self._pos += len(s) + self._write(s) + + def tell(self): + return self._pos + self._output.tell() + + def seek(self, offset, whence=0): + self._check_open() + currentpos = self._pos + self._output.tell() + currentlen = self._pos + len(self._output.getvalue()) + if whence == 0: + newpos = offset + elif whence == 1: + newpos = currentpos + offset + elif whence == 2: + newpos = currentlen + offset + else: + raise ValueError, "Bad 'whence' argument to seek()" + if newpos == currentpos: + return + elif newpos < self._pos: + raise ValueError, "Cannot seek backwards into already-sent data" + elif newpos <= currentlen: + self._output.seek(newpos - self._pos) + else: + if self._bufferOutput: + self._output.seek(newpos - self._pos) + else: + self._write("\0" * (newpos - self._pos)) + + def _mergevars(self, encoded): + """Parse variable-value pairs from a URL-encoded string.""" + """Extract the variable-value pairs from the URL-encoded input string and + merge them into the output dictionary. Variable-value pairs are separated + from each other by the '&' character. Missing values are allowed. + + If the variable name ends with a '*' character, then the value that is + placed in the dictionary will be a list. This is useful for multiple-value + fields.""" + for pair in encoded.split("&"): + if pair == "": + continue + nameval = pair.split("=", 1) + name = url_decode(nameval[0]) + if len(nameval) > 1: + val = url_decode(nameval[1]) + else: + val = None + if name.endswith("!") or name.endswith("!*"): + continue + if name.endswith("*"): + if self.params.has_key(name): + self.params[name].append(val) + else: + self.params[name] = [val] + else: + self.params[name] = val + + def _mergemime(self, contenttype, encoded): + """Parses variable-value pairs from a MIME-encoded input stream.""" + """Extract the variable-value pairs from the MIME-encoded input file and + merge them into the output dictionary. + + If the variable name ends with a '*' character, then the value that is + placed in the dictionary will be a list. This is useful for multiple-value + fields. If the variable name ends with a '!' character (before the '*' if + present) then the value will be a mime.Entity object.""" + import mime + headers = "Content-Type: %s\n" % contenttype + for entity in mime.Entity(encoded.read(), mime=1, headers=headers).entities: + if not entity.content_disposition: + continue + if entity.content_disposition[0] != 'form-data': + continue + name = entity.content_disposition[1].get("name") + if name[-1:] == "*": + if self.params.has_key(name): + if name[-2:-1] == "!": + self.params[name].append(entity) + else: + self.params[name].append(entity.body) + else: + if name[-2:-1] == "!": + self.params[name] = [entity] + else: + self.params[name] = [entity.body] + elif name[-1:] == "!": + self.params[name] = entity + else: + self.params[name] = entity.body + + def _read_cgi_data(self, environ, inf): + """Read input data from the client and set up the object attributes.""" + if environ.has_key("QUERY_STRING"): + self._mergevars(environ["QUERY_STRING"]) + if environ.get("REQUEST_METHOD") == "POST": + if environ.get("CONTENT_TYPE", "").startswith("multipart/form-data"): + self._mergemime(environ["CONTENT_TYPE"], inf) + else: + self._mergevars(inf.read(int(environ.get("CONTENT_LENGTH", "-1")))) + + def traceback(self): + traceback(self) + try: + self.clear_headers() + self.clear_output() + self.set_header("Content-Type", "text/html; charset=iso-8859-1") + except SequencingError: + pass + self.write("""\ +<html><head><title>Error</title></head> +<body><h1>Error</h1> +<p>Sorry, an error occurred. Please try again later.</p> +</body></html>""") + + +class GZipMixIn(object): + def _init(self): + self._gzip = None + self._gzip_level = 6 + super(GZipMixIn, self)._init() + + def _close(self): + if self._gzip: + import struct + super(GZipMixIn, self)._write(self._gzip.flush(self._gzip_zlib.Z_FINISH)) + super(GZipMixIn, self)._write( + struct.pack("<II", self._gzip_crc, self._gzip_length)) + super(GZipMixIn, self)._flush() + self._gzip = None + super(GZipMixIn, self)._close() + + def gzip_level(self, level=6): + """Enable/disable gzip output compression.""" + if self._gzip_level == level: + return + if self._doneHeaders: + raise SequencingError, "Cannot adjust compression - headers already sent" + self._gzip_level = level + + def _write(self, s): + if not self._gzip: + super(GZipMixIn, self)._write(s) + return + self._gzip_crc = self._gzip_zlib.crc32(s, self._gzip_crc) + self._gzip_length += len(s) + super(GZipMixIn, self)._write(self._gzip.compress(s)) + + def output_headers(self): + if self._gzip_level == 0: + super(GZipMixIn, self).output_headers() + return + gzip_ok = 0 + if self.environ.has_key("HTTP_ACCEPT_ENCODING"): + encodings = [[a.strip() for a in x.split(";", 1)] + for x in self.environ["HTTP_ACCEPT_ENCODING"].split(",")] + for encoding in encodings: + if encoding[0].lower() == "gzip": + if len(encoding) == 1: + gzip_ok = 1 + break + else: + q = [x.strip() for x in encoding[1].split("=")] + if len(q) == 2 and q[0].lower() == "q" and q[1] != "0": + gzip_ok = 1 + break + if gzip_ok: + try: + import zlib + encoding = "gzip" + encoding = self.get_header("Content-Encoding") + if encoding is None: + encoding = "gzip" + else: + encoding += ", gzip" + self.set_header("Content-Encoding", encoding) + self.del_header("Content-Length") + super(GZipMixIn, self).output_headers() + self._gzip = zlib.compressobj(self._gzip_level, 8, -15) + self._gzip_zlib = zlib + self._gzip_crc = self._gzip_length = 0 + super(GZipMixIn, self)._write( + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03") + return + except ImportError: + pass + super(GZipMixIn, self).output_headers() + + def _flush(self): + if self._gzip: + super(GZipMixIn, self)._write( + self._gzip.flush(self._gzip_zlib.Z_SYNC_FLUSH)) + super(GZipMixIn, self)._flush() + + +class CGIRequest(Request): + """An implementation of Request which uses the standard CGI interface.""" + + def _init(self): + self.__out = sys.stdout + self.__err = sys.stderr + self.environ = os.environ + self.stdin = sys.stdin + super(CGIRequest, self)._init() + + def process(self): + """Read the CGI input and create and run a handler to handle the request.""" + self._init() + try: + handler = self._handler_type() + except: + self.traceback() + else: + try: + handler.process(self) + except: + handler.traceback(self) + self.close() + + def error(self, s): + self.__err.write(s) + + def _write(self, s): + if not self.aborted: + try: + self.__out.write(s) + except IOError, x: + # Ignore EPIPE, caused by the browser having gone away + if x[0] != errno.EPIPE: + raise + self.aborted = 1 + + def _flush(self): + if not self.aborted: + try: + self.__out.flush() + except IOError, x: + # Ignore EPIPE, caused by the browser having gone away + if x[0] != errno.EPIPE: + raise + self.aborted = 1 + + +class GZipCGIRequest(GZipMixIn, CGIRequest): + pass + + +class Handler(object): + """Handle a request.""" + def process(self, req): + """Handle a request. req is a Request object.""" + raise NotImplementedError, "handler process function must be overridden" + + def traceback(self, req): + """Display a traceback, req is a Request object.""" + req.traceback() + + +class DebugHandlerMixIn(object): + def traceback(self, req): + """Display a traceback, req is a Request object.""" + traceback(req, html=1) + + +class DebugHandler(DebugHandlerMixIn, Handler): + pass diff --git a/py-bin/lib/jon/session.py b/py-bin/lib/jon/session.py new file mode 100644 index 0000000..7a253ab --- /dev/null +++ b/py-bin/lib/jon/session.py @@ -0,0 +1,209 @@ +# $Id: session.py,v 1.9 2003/01/02 23:24:44 jribbens Exp $ + +import time, hmac, sha, Cookie, re, random, os, errno, fcntl +try: + import cPickle as pickle +except ImportError: + import pickle + + +class Error(Exception): + pass + + +class Session(dict): + def _make_hash(self, sid, secret): + """Create a hash for 'sid' + + This function may be overridden by subclasses.""" + return hmac.new(secret, sid, sha).hexdigest()[:8] + + def _create(self, secret): + """Create a new session ID and, optionally hash + + This function must insert the new session ID (which must be 8 hexadecimal + characters) into self["id"]. + + It may optionally insert the hash into self["hash"]. If it doesn't, then + _make_hash will automatically be called later. + + This function may be overridden by subclasses. + """ + rnd = str(time.time()) + str(random.random()) + \ + str(self._req.environ.get("UNIQUE_ID")) + self["id"] = sha.new(rnd).hexdigest()[:8] + + def _load(self): + """Load the session dictionary from somewhere + + This function may be overridden by subclasses. + It should return 1 if the load was successful, or 0 if the session could + not be found. Any other type of error should raise an exception as usual.""" + return 1 + + def save(self): + """Save the session dictionary to somewhere + + This function may be overridden by subclasses.""" + pass + + def tidy(): + pass + tidy = staticmethod(tidy) + + def __init__(self, req, secret, cookie="jonsid", url=0, root="", + referer=None, sid=None, shash=None): + dict.__init__(self) + self["id"] = None + self._req = req + self.relocated = 0 + self.new = 0 + + # try and determine existing session id + + if sid is not None: + self["id"] = sid + if shash is None: + self["hash"] = self._make_hash(self["id"], secret) + else: + self["hash"] = shash + if self["hash"] != self._make_hash(self["id"], secret): + self["id"] = None + + if cookie and self._req.cookies.has_key(cookie): + self["id"] = self._req.cookies[cookie].value[:8] + self["hash"] = self._req.cookies[cookie].value[8:] + if self["hash"] != self._make_hash(self["id"], secret): + self["id"] = None + + if url: + for i in xrange(1, 4): + requrl = self._req.environ.get("REDIRECT_" * i + "SESSION") + if requrl: + break + if requrl and self["id"] is None: + self["id"] = requrl[:8] + self["hash"] = requrl[8:] + if self["hash"] != self._make_hash(self["id"], secret): + self["id"] = None + + # check the session + + if referer: + if self._req.environ.has_key("HTTP_REFERER"): + if self._req.environ["HTTP_REFERER"].find(referer) == -1: + self["id"] = None + + # try and load the session + + if self["id"] is not None: + if not self._load(): + self["id"] = None + + # if no session was available and loaded, create a new one + + if self["id"] is None: + if self.has_key("hash"): + del self["hash"] + self.created = time.time() + self.new = 1 + self._create(secret) + if not self.has_key("hash"): + self["hash"] = self._make_hash(self["id"], secret) + if cookie: + c = Cookie.SimpleCookie() + c[cookie] = self["id"] + self["hash"] + c[cookie]["path"] = root + "/" + self._req.add_header("Set-Cookie", c[cookie].OutputString()) + + # if using url-based sessions, redirect if necessary + + if url: + if not requrl or self["id"] != requrl[:8] or self["hash"] != requrl[8:]: + requrl = self._req.environ["REQUEST_URI"][len(root):] + requrl = re.sub("^/[A-Fa-f0-9]{16}/", "/", requrl) + self._req.add_header("Location", "http://" + + self._req.environ["SERVER_NAME"] + root + "/" + self["id"] + + self["hash"] + requrl) + self.relocated = 1 + self.surl = root + "/" + self["id"] + self["hash"] + "/" + +class FileSession(Session): + def _create(self, secret): + while 1: + Session._create(self, secret) + try: + os.lstat("%s/%s" % (self.basedir, self["id"][:2])) + except OSError, x: + if x[0] == errno.ENOENT: + os.mkdir("%s/%s" % (self.basedir, self["id"][:2]), 0700) + try: + fd = os.open("%s/%s/%s" % (self.basedir, self["id"][:2], + self["id"][2:]), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0700) + except OSError, x: + if x[0] != errno.EEXIST: + raise + continue + f = os.fdopen(fd, "wb") + f.write("%d\n" % self.created) + pickle.dump({}, f, 1) + f.flush() + break + + def _load(self): + try: + f = open("%s/%s/%s" % (self.basedir, self["id"][:2], self["id"][2:]), + "r+b") + except IOError, x: + if x[0] != errno.ENOENT: + raise + return 0 + fcntl.lockf(f.fileno(), fcntl.LOCK_EX) + self.created = int(f.readline().strip()) + self.update(pickle.load(f)) + return 1 + + def save(self): + f = open("%s/%s/%s" % (self.basedir, self["id"][:2], self["id"][2:]), "r+b") + fcntl.lockf(f.fileno(), fcntl.LOCK_EX) + f.write("%d\n" % self.created) + pickle.dump(self.copy(), f, 1) + f.flush() + f.truncate() + + def tidy(cls, max_idle=0, max_age=0, basedir=None): + if not max_idle and not max_age: + return + basedir = cls._find_basedir(basedir) + now = time.time() + for d in os.listdir(basedir): + if len(d) != 2 or not d.isalnum(): + continue + for f in os.listdir("%s/%s" % (basedir, d)): + if len(f) != 6 or not f.isalnum(): + continue + p = "%s/%s/%s" % (basedir, d, f) + if (max_idle and os.lstat(p).st_mtime < now - max_idle) or \ + (max_age and int(open(p, "rb").readline().strip()) < now - max_age): + os.remove(p) + tidy = classmethod(tidy) + + def _find_basedir(basedir): + if basedir is None: + basedir = os.environ.get("TMPDIR", "/tmp") + while basedir[-1] == "/": + basedir = basedir[:-1] + basedir = "%s/jon-sessions-%d" % (basedir, os.getuid()) + try: + st = os.lstat(basedir) + if st[4] != os.getuid(): + raise Error, "Sessions basedir is not owned by user %d" % os.getuid() + except OSError, x: + if x[0] == errno.ENOENT: + os.mkdir(basedir, 0700) + return basedir + _find_basedir = staticmethod(_find_basedir) + + def __init__(self, req, secret, basedir=None, **kwargs): + self.basedir = self._find_basedir(basedir) + Session.__init__(self, req, secret, **kwargs) diff --git a/py-bin/login.py b/py-bin/login.py new file mode 100644 index 0000000..2fd85b3 --- /dev/null +++ b/py-bin/login.py @@ -0,0 +1,24 @@ +#login + +import config + +class LoginMixIn: + def login_form(self, req): + self.render_template(req, "login_form.em") + login_form.web_callable = True + + def login_process(self, req): + email = req.params.get("email", "") + jabberpw = req.params.get("jabberpw", "") + + success, status_or_user = self.jman.login(email, jabberpw) + if not success: + return self.failed_page(req, status_or_user) + + self.redirect_to(req, config.script_url + "?cmd=setup_main") + login_process.web_callable = True + + def failed_page(self, req, reason): + self.render_template(req, "login_fail.em", dict(reason=reason)) + return None + diff --git a/py-bin/mail_auth.py b/py-bin/mail_auth.py new file mode 100644 index 0000000..27607e8 --- /dev/null +++ b/py-bin/mail_auth.py @@ -0,0 +1,111 @@ +#mail authentication + +import smtplib, hmac, sha, re, time, logging +from utils import empy_render +import subprocess +import config + +class MailAuthMixIn: + def mail_form(self, req): + self.render_template(req, "mail_form.em") + mail_form.web_callable = True + + def mail_process(self, req): + email = req.params.get("email", "") + + success, status_or_token = self.jman.generate_token(email) + if not success: + self.render_template(req, "mail_error.em", dict(reason=status_or_token)) + return + token = status_or_token + + if not self.__send_mail(email, token): + msg = "Unerwarteter Fehler beim Versenden des Mails." + self.render_template(req, "mail_trylater.em", dict(reason=msg)) + return + + success, status = self.jman.prepare_user(email, token) + if not success: + self.render_template(req, "mail_error.em", dict(reason=status)) + return + + msg = "Mail erfolgreich versandt." + self.render_template(req, "mail_success.em", dict(status=msg, email=email)) + mail_process.web_callable = True + + def mail_pw_form(self, req): + user_id = req.params.get("uid", "") + token = req.params.get("tok", "") + last_error = req.params.get("error", "") + + token_ok, status_or_user = self.jman.validate_token(user_id, token) + if token_ok: + user_id = status_or_user.get_user_id() + self.render_template(req, "set_pw_form.em", + dict(user_id=user_id, error=last_error, command="mail_pw_process")) + else: + self.error_page(req, status_or_user) + mail_pw_form.web_callable = True + + def mail_pw_process(self, req): + password = req.params.get("password", "") + password2 = req.params.get("password2", "") + + ok, status = self.jman.is_acceptable_password(password, password2) + if not ok: + url = self.make_url([("cmd","mail_pw_form"), ("error", status)]) + self.redirect_to(req, url) + return + + ok, status_or_user = self.jman.activate_user(password) + if ok: + self.redirect_to(req, config.script_url + "?cmd=setup_main") + else: + self.error_page(req, status_or_user) + mail_pw_process.web_callable = True + + def __send_mail(self, email, token): + date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) + url = self.make_url([("cmd","mail_pw_form"), ("uid",email), ("tok",token)]) + ctx = dict(from_addr=config.mail_from_addr, to_addr=email, + date=date, reg_link=url) + message = empy_render("mail_message.em", context_raw = ctx) + + if hasattr(config,"use_sendmail") and config.use_sendmail: + return self.__sendmail_send_mail(email, token, message) + else: + return self.__smtp_send_mail(email, token, message) + + def __smtp_send_mail(self, email, token, message): + try: + server = smtplib.SMTP(config.smtp_server) + server.ehlo() + server.starttls() + server.ehlo() + if hasattr(config, "smtp_pass"): + server.login(config.smtp_user, config.smtp_pass) + server.sendmail(config.mail_from_addr, [email], message) + server.quit() + logging.info("Sent mail to %s." % email) + except Exception, e: + logging.error("Error sending mail to %s: %s" % (email, str(e))) + return False + + return True + + def __sendmail_send_mail(self, email, token, message): + try: + params = ["/usr/sbin/sendmail", "-f" + config.mail_from_addr, email] + p = subprocess.Popen(params, stdin=subprocess.PIPE) + p.communicate(message) + result = p.wait() + if result != 0: + logging.error("Error queuing mail for %s: return code %s" % + (email, str(result))) + return False + logging.info("Queued mail for %s." % email) + except Exception, e: + logging.error("Error queuing mail for %s: %s" % (email, str(e))) + return False + return True + diff --git a/py-bin/main.py b/py-bin/main.py new file mode 100644 index 0000000..39294e0 --- /dev/null +++ b/py-bin/main.py @@ -0,0 +1,31 @@ +#main url mapper + +from utils import BasicHandler, process_request, set_logging_defaults +from jabberman import JabberManager +from login import LoginMixIn +from mail_auth import MailAuthMixIn +from setup import SetupMixIn + +set_logging_defaults() + +class MainHandler(BasicHandler, MailAuthMixIn, LoginMixIn, SetupMixIn): + def do_process(self, req): + command = req.params.get("cmd", "") + + if command == "": + self.login_form(req) + else: + if hasattr(self, command): + method = getattr(self, command) + if hasattr(method, 'web_callable') and method.web_callable: + self.jman = JabberManager(self.session) + method(req) + else: + self.invalid_page(req) + else: + self.invalid_page(req) + + def invalid_page(self, req): + self.error_page(req, "Ungueltiger Request.") + +process_request(MainHandler)
\ No newline at end of file diff --git a/py-bin/setup.py b/py-bin/setup.py new file mode 100644 index 0000000..1978dd1 --- /dev/null +++ b/py-bin/setup.py @@ -0,0 +1,140 @@ +#jabber setup +from lib.jon.cgi import html_encode, url_encode +import config + +class SetupMixIn: + def setup_main(self, req): + user = self.__authenticate(req) + if not user: + return + + paras = dict(user_id=user.get_user_id(), jabber_id=user.get_default_jabber_id()) + acc_list = map(self.__get_delete_tuple, user.get_extra_account_list()) + self.render_template(req, "setup_main.em", paras, dict(account_list=acc_list)) + setup_main.web_callable = True + + def __get_delete_tuple(self, account): + url = self.make_url([("cmd","delete_account_ask"), ("account",account)]) + return (html_encode(account), url) + + def set_pw_form(self, req): + user = self.__authenticate(req) + if not user: + return + + last_error = req.params.get("error", "") + self.render_template(req, "set_pw_form.em", + dict(user_id=user.get_user_id(), error=last_error)) + set_pw_form.web_callable = True + + def set_pw_process(self, req): + user = self.__authenticate(req) + if not user: + return + + password = req.params.get("password", "") + password2 = req.params.get("password2", "") + + ok, status = self.jman.is_acceptable_password(password, password2) + if not ok: + url = self.make_url([("cmd","set_pw_form"), ("error", status)]) + self.redirect_to(req, url) + return + + self.jman.change_password(password) + self.__redirect_to_main(req) + set_pw_process.web_callable = True + + def add_account_form(self, req): + user = self.__authenticate(req) + if not user: + return + + last_err = req.params.get("error", "") + domains = config.extra_domains + + self.render_template(req, "add_account_form.em", + dict(user_id=user.get_user_id(), domains=domains, error=last_err)) + add_account_form.web_callable = True + + def add_account_process(self, req): + user = self.__authenticate(req) + if not user: + return + + domain = req.params.get("domain", "") + account = req.params.get("name", "") + "@" + domain + + if domain not in config.extra_domains: + self.error_page(req, "Zugriff verweigert.") + return + + ok, status = self.jman.add_account(account) + if not ok: + url = self.make_url([("cmd","add_account_form"), ("error", status)]) + self.redirect_to(req, url) + return + + self.__redirect_to_main(req) + add_account_process.web_callable = True + + def delete_account_ask(self, req): + user = self.__authenticate(req) + if not user: + return + + account = req.params.get("account", "") + + raw = dict(account_urlenc = url_encode(account)) + self.render_template(req, "delete_account_ask.em", + dict(account=account, user_id=user.get_user_id()), raw) + delete_account_ask.web_callable = True + + def delete_account_process(self, req): + user = self.__authenticate(req) + if not user: + return + + account = req.params.get("account", "") + + ok, status = self.jman.remove_account(account) + if not ok: + self.error_page(req, status) + return + + self.__redirect_to_main(req) + delete_account_process.web_callable = True + + def help(self, req): + user = self.__authenticate(req) + if not user: + return + + self.render_template(req, "setup_help.em", + dict(user_id=user.get_user_id(),jabber_id=user.get_default_jabber_id())) + help.web_callable = True + + def logout(self, req): + user = self.__authenticate(req) + if not user: + return + + self.jman.logout() + self.render_template(req, "logged_out.em") + logout.web_callable = True + + def test(self, req): + ctx = dict(user_id="alice@immerda.ch") + self.render_template(req, "test.em", ctx) + test.web_callable = True + + def __redirect_to_main(self, req): + self.redirect_to(req, self.make_url([("cmd","setup_main")])) + + def __authenticate(self, req): + ok, status_or_user = self.jman.authenticate() + if not ok: + self.redirect_to(req, config.script_url) + return None + return status_or_user +
\ No newline at end of file diff --git a/py-bin/templates/add_account_form.em b/py-bin/templates/add_account_form.em new file mode 100644 index 0000000..ae88204 --- /dev/null +++ b/py-bin/templates/add_account_form.em @@ -0,0 +1,17 @@ +@{em_inherit = "jman_setup_base.em"} +@{title = "Jabber Konto Hinzufügen"} + +<form method="post" action="main.py?cmd=add_account_process"> + <p>Name: <input type="text" name="name"/></p> + <p>Domain: + <select name="domain"> + @[for domain in domains] + <option value="@domain">@domain</option> + @[end for] + </select> + </p> + <p><input type="reset" value="Zurücksetzen"/> + <input type="submit" name="submitted" value="Erstellen"/></p> +</form> + +<p><a href="main.py?cmd=setup_main">Zurück</a></p> diff --git a/py-bin/templates/delete_account_ask.em b/py-bin/templates/delete_account_ask.em new file mode 100644 index 0000000..d28fb6a --- /dev/null +++ b/py-bin/templates/delete_account_ask.em @@ -0,0 +1,8 @@ +@{em_inherit = "jman_setup_base.em"} +@{title = "Jabber Konto Löschen"} + +<p>Jabber Konto: @account</p> + +<p><a href="main.py?cmd=setup_main">Abbrechen</a> + | <a href="main.py?cmd=delete_account_process&account=@account_urlenc">Löschen</a> +</p> diff --git a/py-bin/templates/error.em b/py-bin/templates/error.em new file mode 100644 index 0000000..850e13e --- /dev/null +++ b/py-bin/templates/error.em @@ -0,0 +1,13 @@ +@{em_inherit = "jman_base.em"} +@{title = "Schwerer Fehler"} + +<div class="carbonbar"> + <h2>Unbehebbarer Fehler aufgetreten</h2> +</div> + +<div id="content"> + <p>Fehlermeldung: <b>@message</b></p> + <p>Bitte wende dich an den technischen Support: + <a href="mailto:jabber AT immerda.ch"> jabber AT immerda.ch</a> (AT durch @@ ersetzen) + </p> +</div>
\ No newline at end of file diff --git a/py-bin/templates/jman_base.em b/py-bin/templates/jman_base.em new file mode 100644 index 0000000..35494a4 --- /dev/null +++ b/py-bin/templates/jman_base.em @@ -0,0 +1,94 @@ +<html> +<head> +<style type="text/css"><!-- +body { + font-family:Sans-serif; + font-size:14px; + background-color:#000000; + color:#ffffff; + text-align:center; + margin:0px; + padding:0px; +} + +h1 {font-size:24px; margin:0px;} +h2 {font-size:18px; margin:0px;} +h3 {font-size:16px; margin:0px;} +p {margin: 6px 0px;} +b { + color: #cc1f1f; +} +a:link, a:visited, a:active, a:hover { + text-decoration:underline; + font-weight:bold; + color:#ffA900; +} +a:active, a:hover { + color:ffdd00; + text-shadow: #ffA900 1px 1px 6px; +} + +#wholepage { + width: 840px; + margin: 0px auto; + text-align: left; +} +#maincolumn { + width:640px; + float:left; +} +#leftshadow, #rightshadow { + width: 100px; + height: 100%; + float: left; +} +#leftshadow {background: url(images/left-sm.png) repeat-y;} +#rightshadow {background: url(images/right-sm.png) repeat-y;} + +#title {padding:5px 5px;} +#logo { + float: right; + width: 180px; + height: 75px; + padding-right: 5px; +} + +.carbonbar { + border-top: 2px solid #cc0000; + border-bottom: 2px solid #cc0000; + padding: 3px 5px; + clear: right; + background-image: url(images/carbon.png); +} +#userbar { + padding-top: 3px; + float: right; +} +#bottombar {text-align: center;} + +#content { + padding: 5px; + clear: right; +} +//--></style> +<title>Jabber Verwaltung - @title</title> +</head> + +<body> + <div id="wholepage"> + <div id="leftshadow"></div> + <div id="maincolumn"> + <img id="logo" src="images/jabber_logo.png" alt="Logo" /> + <h1 id="title">Jabber Verwaltung</h1> + + @em_child_content + + <div id="bottombar" class="carbonbar"> + Copyleft (<a href="http://creativecommons.org" target="_blank">cc</a>) 2007 + <a href="http://www.immerda.ch" target="_blank">Immerda Projekt</a> + </div> + </div> + <div id="rightshadow"></div> + </div> +</body> +</html>
\ No newline at end of file diff --git a/py-bin/templates/jman_setup_base.em b/py-bin/templates/jman_setup_base.em new file mode 100644 index 0000000..329c3b3 --- /dev/null +++ b/py-bin/templates/jman_setup_base.em @@ -0,0 +1,18 @@ +@{em_inherit = "jman_base.em"} + +<div class="carbonbar"> + <div id="userbar"> + @[if "user_id" in locals()] + <i>@user_id</i> | + <a href="main.py?cmd=set_pw_form">Passwort ändern</a> | + <a href="main.py?cmd=logout">Ausloggen</a> + @[end if] + </div> + <h2>@title</h2> +</div> + +<div id="content"> + @[if "error" in locals() and error != ""] <p>Fehler: <b>@error</b></p> @[end if] + @em_child_content +</div> +
\ No newline at end of file diff --git a/py-bin/templates/logged_out.em b/py-bin/templates/logged_out.em new file mode 100644 index 0000000..f096597 --- /dev/null +++ b/py-bin/templates/logged_out.em @@ -0,0 +1,5 @@ +@{em_inherit = "jman_setup_base.em"} +@{title = "Ausgeloggt"} + +<p><b>Auf wiedersehen und viel Spass mit Jabber!</b></p> +<p><a href="main.py">Neu einloggen</a></p> diff --git a/py-bin/templates/login_fail.em b/py-bin/templates/login_fail.em new file mode 100644 index 0000000..bc20d65 --- /dev/null +++ b/py-bin/templates/login_fail.em @@ -0,0 +1,22 @@ +@{em_inherit = "jman_base.em"} +@{title = "Login fehlgeschlagen"} + +<div class="carbonbar"> + <h2>@title</h2> +</div> + +<div id="content"> + <p>Grund: <b>@reason</b></p> + <p>Bitte logge dich mit den folgenden Angaben ein:</p> + <ul> + <li><b>E-Mail</b> Adresse (z.B. <i>alice@@immerda.ch</i>)</li> + <li><b>Jabber</b> Passwort</li> + </ul> + <p>Es kann auf den ersten Blick verwirren, dass du deine E-Mail + statt deiner Jabber Adresse angeben musst. Das ist aber nötig, da eine + Person mehrere Jabber Benutzerkonten haben kann. Somit brauchen wir deine + E-Mail Adresse, damit du nach dem Login alle deine Jabber Konten verwalten + kannst. + </p> + <p><a href="main.py">Nochmals versuchen</a></p> +</div>
\ No newline at end of file diff --git a/py-bin/templates/login_form.em b/py-bin/templates/login_form.em new file mode 100644 index 0000000..a90dcfb --- /dev/null +++ b/py-bin/templates/login_form.em @@ -0,0 +1,26 @@ +@{em_inherit = "jman_base.em"} +@{title = "Login"} + +<div class="carbonbar"> + <h2>Registrierung</h2> +</div> + +<div id="content"> + <p>Bist du <b>neu hier</b>? Dann kannst du dich da + <a href="main.py?cmd=mail_form">registrieren</a>. + </p> +</div> + +<div class="carbonbar"> + <h2>Login</h2> +</div> + +<div id="content"> + <p>Hast du bereits ein Jabber Benutzerkonto? Dann logge dich hier ein.</p> + <form method="post" action="main.py?cmd=login_process"> + <p>E-Mail: <input type="text" name="email"/></p> + <p>Jabber Passwort: <input type="password" name="jabberpw"/></p> + <p><input type="reset" value="Zurücksetzen"/> + <input type="submit" name="submitted" value="Login"/></p> + </form> +</div>
\ No newline at end of file diff --git a/py-bin/templates/mail_error.em b/py-bin/templates/mail_error.em new file mode 100644 index 0000000..01b70a9 --- /dev/null +++ b/py-bin/templates/mail_error.em @@ -0,0 +1,16 @@ +@{em_inherit = "jman_base.em"} +@{title = "Fehler bei Registrierung"} + +<div class="carbonbar"> + <h2>@title</h2> +</div> + +<div id="content"> + <p>Grund: <b>@reason</b></p> + <p>Um dich registrieren zu können, musst du eine <b>gültige E-Mail Adresse</b> angeben. + Beachte zudem, dass nur Domains erlaubt sind, welche <b>in Verbindung mit dem + Immerda Projekt</b> stehen. Wenn du glaubst, dass ein gewisser Domain zu Unrecht nicht + erlaubt ist, dann schreib ein E-Mail an + <a href="mailto:jabber AT immerda.ch"> jabber AT immerda.ch</a> (AT durch @@ ersetzen). + </p> +</div>
\ No newline at end of file diff --git a/py-bin/templates/mail_form.em b/py-bin/templates/mail_form.em new file mode 100644 index 0000000..604b8ab --- /dev/null +++ b/py-bin/templates/mail_form.em @@ -0,0 +1,24 @@ +@{em_inherit = "jman_base.em"} +@{title = "E-Mail Formular"} + +<div class="carbonbar"> + <h2>@title</h2> +</div> + +<div id="content"> + <p>Zur Registration wird deine <b>E-Mail Adresse</b> benötigt. Damit nicht x-beliebige + Leute hier Jabber Konten registrieren, sind die Mail Domains beschränkt auf + <i>Immerda</i>, <i>Cronopios</i> und <i>Einfachsicher</i>. Erlaubte Adressen sind + also z.B.: + </p> + <ul> + <li><i>alice@@immerda.ch</i></li> + <li><i>bob@@cronopios.org</i></li> + <li><i>charlie@@einfachsicher.ch</i></li> + </ul> + <form method="post" action="main.py?cmd=mail_process"> + <p>E-Mail: <input type="text" name="email"/></p> + <p><input type="reset" value="Zurücksetzen"/> + <input type="submit" name="submitted" value="Registrieren"/></p> + </form> +</div> diff --git a/py-bin/templates/mail_message.em b/py-bin/templates/mail_message.em new file mode 100644 index 0000000..134a4a2 --- /dev/null +++ b/py-bin/templates/mail_message.em @@ -0,0 +1,21 @@ +From: Jabber Web Registration <@from_addr> +To: @to_addr +Date: @date +Subject: Freischalt-Code + +Hallo und willkommen beim Jabber Netzwerk von Immerda! + +Du hast auf der Jabber Webseite des Immerda-Projekts ein Jabber +Benutzerkonto registriert. + +Um das Benutzerkonto zu aktivieren und dein Jabber Passwort zu setzen, +musst du dem unten angegebenen Link folgen. Alle weiteren Schritte +sind auf dieser Seite beschrieben. + +@reg_link + +Falls du den Link nicht anklicken kannst, dann kopiere ihn einfach +_vollständig_ (wichtig!) in die Adresszeile deines Browsers. + +Viel Spass wünscht dir +Das Immerda Team
\ No newline at end of file diff --git a/py-bin/templates/mail_success.em b/py-bin/templates/mail_success.em new file mode 100644 index 0000000..4efe73a --- /dev/null +++ b/py-bin/templates/mail_success.em @@ -0,0 +1,15 @@ +@{em_inherit = "jman_base.em"} +@{title = "E-Mail versandt"} + +<div class="carbonbar"> + <h2>@title</h2> +</div> + +<div id="content"> + <p>Status: <b>@status</b></p> + <p>Ein Jabber Benutzerkonto ist nun für dich reserviert, doch du musst es erst noch + <b>aktivieren</b> und das <b>Passwort setzen</b>. In dem Mail, das in kürze bei dir + eintreffen wird, ist alles weitere beschrieben... + </p> + <p>Rufe nun deine Mailbox (<i>@email</i>) ab.</p> +</div>
\ No newline at end of file diff --git a/py-bin/templates/mail_trylater.em b/py-bin/templates/mail_trylater.em new file mode 100644 index 0000000..6f7421b --- /dev/null +++ b/py-bin/templates/mail_trylater.em @@ -0,0 +1,16 @@ +@{em_inherit = "jman_base.em"} +@{title = "Fehler bei Registrierung"} + +<div class="carbonbar"> + <h2>@title</h2> +</div> + +<div id="content"> + <p>Grund: <b>@reason</b></p> + <p> + Vermutlich ist der Mail-Server momentan gerade überlastet. Versuche es also einfach + zu einem späteren Zeitpunkt <a href="main.py?cmd=mail_form">nochmals</a>. + Wenn der Fehler wiederholt auftritt, dann schreib ein E-Mail an + <a href="mailto:jabber AT immerda.ch"> jabber AT immerda.ch</a> (AT durch @@ ersetzen). + </p> +</div>
\ No newline at end of file diff --git a/py-bin/templates/set_pw_form.em b/py-bin/templates/set_pw_form.em new file mode 100644 index 0000000..7e7a3b7 --- /dev/null +++ b/py-bin/templates/set_pw_form.em @@ -0,0 +1,29 @@ +@{em_inherit = "jman_base.em"} +@{title = "Passwort setzen"} + +@{if not "command" in locals(): command = "set_pw_process"} + +<div class="carbonbar"> + <div id="userbar"> + <i>@user_id</i> + @[if command == "set_pw_process"] + | + <a href="main.py?cmd=logout">Ausloggen</a> + @[end if] + </div> + <h2>@title</h2> +</div> + +<div id="content"> + @[if "error" in locals() and error != ""] <p>Fehler: <b>@error</b></p> @[end if] + <form method="post" action="main.py?cmd=@command"> + <p>Passwort: <input type="password" name="password"/></p> + <p>Passwort bestätigen: <input type="password" name="password2"/></p> + <p><input type="reset" value="Zurücksetzen"/> + <input type="submit" name="submitted" value="Passwort setzen"/></p> + </form> + + @[if command == "set_pw_process"] + <p><a href="main.py?cmd=setup_main">Zurück</a></p> + @[end if] +</div> diff --git a/py-bin/templates/setup_help.em b/py-bin/templates/setup_help.em new file mode 100644 index 0000000..ac41f80 --- /dev/null +++ b/py-bin/templates/setup_help.em @@ -0,0 +1,75 @@ +@{em_inherit = "jman_base.em"} +@{title = "Hilfe"} + +<div class="carbonbar"> + <div id="userbar"> + <i>@user_id</i> | + <a href="main.py?cmd=set_pw_form">Passwort ändern</a> | + <a href="main.py?cmd=logout">Ausloggen</a> + </div> + <h2>@title</h2> +</div> + +<div id="content"> + <p>Laut den Regeln des Immerda Projekts haben alle E-Mail NutzerInnen + von <i>Immerda</i>, <i>Cronopios</i> und <i>Einfachsicher</i> + ein Anrecht auf ein Jabber Benutzerkonto. Damit kein Chaos oder + gar Streitigkeiten bei der Namensvergabe enstehen, du jedoch trotzdem + eine Wahlmöglichkeit hast, werden zwei Optionen angeboten: + </p> +</div> + +<div class="carbonbar"> + <h3>Jabber Adressen <i>@@jabber.immerda.ch</i> usw.:</h3> +</div> + +<div id="content"> + <p>Diese Jabber Adressen werden nach einem fixen Schema <b>automatisch + aus den E-Mail adressen abgeleitet</b>. Die folgenden Beispiele zeigen, + wie das funktioniert: + </p> + <ul> + <li><i>alice@@immerda.ch</i> kriegt <i>alice@@<b>jabber</b>.immerda.ch</i></li> + <li><i>bob@@cronopios.org</i> kriegt <i>bob@@<b>jabber</b>.cronopios.org</i></li> + <li><i>charlie@@einfachsicher.ch</i> <i>kriegt charlie@@<b>jabber</b>.einfachsicher.ch</i></li> + </ul> + <p>Wenn du nun die Adresse <i>@jabber_id</i> verwendest, hast du folgende Vorteile:</p> + <ul> + <li>Leute, die deine E-Mail Adresse kennen, werden dich auch im Jabber finden.</li> + <li>Deine Gesprächspartner im Jabber können sicher sein, dass sie wirklich mit + <b>dir</b> reden, da nur <b>du als Besitzer der zugehörigen E-Mail Adresse</b> + diese Jabber Adresse haben kannst! + </li> + </ul> +</div> + +<div class="carbonbar"> + <h3>Jabber Adressen <i>@@imsg.ch</i>:</h3> +</div> + +<div id="content"> + <p>Während die Jabber Adressen <i>@@jabber.immerda.ch</i> etc. also das Vertrauen unter + Immerda-Leuten fördern können, kann es auch ein <b>Nachteil</b> sein, wenn deine Jabber + Adresse <b>in Verbindung mit deiner E-Mail</b> bei Immerda <b>gebracht</b> werden kann. Deshalb + hast du auch die Möglichkeit, eine Jabber Adresse <i>@@imsg.ch</i> auszuwählen, + solange diese noch frei ist. Hier gilt: "first come - first served". + </p> +</div> + +<div class="carbonbar"> + <h3>Fazit</h3> +</div> + +<div id="content"> + <p>Welche Variante für dich besser geeignet ist, kann sich <b>je nach Verwendung</b> von + Fall zu Fall ändern. Die beiden Varianten schliessen sich aber auch nicht gegenseitig + aus! Ausserdem kannst du auch späteren noch bei Bedarf auf dieser Seite zusätzliche + Jabber Adressen erstellen. + </p> + <p>Bei weiteren Fragen wende dich an + <a href="mailto:jabber AT immerda.ch"> jabber AT immerda.ch</a> (AT durch @@ ersetzen). + </p> + <a href="main.py?cmd=setup_main">Zurück</a> +</div> + + diff --git a/py-bin/templates/setup_main.em b/py-bin/templates/setup_main.em new file mode 100644 index 0000000..3109f4b --- /dev/null +++ b/py-bin/templates/setup_main.em @@ -0,0 +1,22 @@ +@{em_inherit = "jman_setup_base.em"} +@{title = "Übersicht"} + +<p>Jabber Konten: [<a href="main.py?cmd=add_account_form">Hinzufügen</a>]</p> +<ul> + <li><b><i>@jabber_id</i></b> (Standard)</li> + @[for jabber_id, url in account_list] + <li><i>@jabber_id</i> [<a href="@url">Löschen</a>]</li> + @[end for] +</ul> + +@[if len(account_list) == 0] +<p> + Im Moment ist dies deine einzige Jabber-Adresse. + Mit ihr kannst du Jabber uneingeschränkt benützen. Wenn dir diese Adresse jedoch + nicht gefällt, oder wenn du aus einem anderen Grund weitere Jabber Adressen + möchtest, so klicke oben auf <i>Hinzufügen</i>. +</p> +@[end if] +<p>Eine ausführliche Erklärung zu den Adressen findest du in der + <a href="main.py?cmd=help">Hilfe</a>. +</p>
\ No newline at end of file diff --git a/py-bin/utils.py b/py-bin/utils.py new file mode 100644 index 0000000..b13949e --- /dev/null +++ b/py-bin/utils.py @@ -0,0 +1,71 @@ +#helper functions/classes + +import string, logging, re, os, urllib +import lib.jon.cgi as cgi, lib.jon.session as session +import lib.em as em +import config + +def xmap(function, data): + if isinstance(data, str): + return function(data) + if isinstance(data, int) or isinstance(data, long) or isinstance(data, float): + return function(str(data)) + if isinstance(data, list): + return map(lambda x: xmap(function, x), data) + if isinstance(data, tuple): + return tuple(xmap(function, list(data))) + +def html_encode_struct(data): + return xmap(cgi.html_encode, data) + +def __empy_render_step(interpreter, filename, context_raw): + path = os.path.join(config.template_dir, filename) + out = interpreter.expand(file(path).read(), context_raw) + if "em_inherit" in context_raw: + filename = context_raw["em_inherit"] + del(context_raw["em_inherit"]) + context_raw["em_child_content"] = out + out = __empy_render_step(interpreter, filename, context_raw) + return out + +def empy_render(filename, context={}, context_raw={}): + for k in context.keys(): + context[k] = html_encode_struct(context[k]) + context_raw.update(context) + interpreter = em.Interpreter() + out = __empy_render_step(interpreter, filename, context_raw) + interpreter.shutdown() + return out + +if config.debugmode: + HandlerBaseClass = cgi.DebugHandler +else: + HandlerBaseClass = cgi.Handler +class BasicHandler(HandlerBaseClass): + def process(self, req): + self.session = session.FileSession(req, config.the_secret, config.session_dir) + req.set_header("Content-Type", "text/html; charset=utf-8") + self.do_process(req) + self.session.save() + + def render_template(self, req, filename, context={}, context_raw={}): + req.write(empy_render(filename, context, context_raw)) + + def error_page(self, req, message): + self.render_template(req, "error.em", dict(message=message)) + + def make_url(self, params): + return config.script_url + "?" + urllib.urlencode(params) + + def redirect_to(self, req, url): + req.set_header("Status", "302 Temporarily moved") + req.set_header("Location", url) + +def process_request(handler): + cgi.CGIRequest(handler).process() + +def set_logging_defaults(): + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%d.%m.%Y %H:%M:%S', + filename=config.logfile_path)
\ No newline at end of file |