From ianb at codespeak.net Tue Jul 1 19:07:33 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 1 Jul 2008 19:07:33 +0200 (CEST) Subject: [z3-checkins] r56206 - in z3/deliverance/sandbox/ianb/deliverance/trunk: . deliverance deliverance/tests deliverance/tests/example-files Message-ID: <20080701170733.9C2A616A0F8@codespeak.net> Author: ianb Date: Tue Jul 1 19:07:32 2008 New Revision: 56206 Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/index.html (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example.py (contents, props changed) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py Log: Added an example setup, and a bunch of fixes and tweaks to work that out Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py Tue Jul 1 19:07:32 2008 @@ -8,20 +8,34 @@ import logging from lxml.etree import tostring, _Element +from tempita import HTMLTemplate, html_quote, html + +NOTIFY = (logging.INFO + logging.WARN) / 2 + +logging.addLevelName(NOTIFY, 'NOTIFY') class SavingLogger(object): """ Logger that saves all its messages locally. """ - def __init__(self, request, description=True): + def __init__(self, request, middleware, description=None): self.messages = [] + if description is None: + description = 'deliv_log' in request.GET if description: self.descriptions = [] self.describe = self.add_description else: + self.descriptions = None self.describe = None + self.middleware = middleware + self.request = request + # This is writable: + self.theme_url = None + def add_description(self, msg): self.descriptions.append(msg) + def message(self, level, el, msg, *args, **kw): if args: msg = msg % args @@ -30,23 +44,104 @@ self.messages.append((level, el, msg)) return msg def debug(self, el, msg, *args, **kw): - self.message(logging.DEBUG, el, msg, *args, **kw) + return self.message(logging.DEBUG, el, msg, *args, **kw) def info(self, el, msg, *args, **kw): - self.message(logging.INFO, el, msg, *args, **kw) + return self.message(logging.INFO, el, msg, *args, **kw) def notify(self, el, msg, *args, **kw): - self.message(logging.INFO+1, el, msg, *args, **kw) + return self.message(logging.NOTIFY, el, msg, *args, **kw) def warn(self, el, msg, *args, **kw): - self.message(logging.WARN, el, msg, *args, **kw) + return self.message(logging.WARN, el, msg, *args, **kw) warning = warn def error(self, el, msg, *args, **kw): - self.message(logging.ERROR, el, msg, *args, **kw) + return self.message(logging.ERROR, el, msg, *args, **kw) def fatal(self, el, msg, *args, **kw): - self.message(logging.FATAL, el, msg, *args, **kw) + return self.message(logging.FATAL, el, msg, *args, **kw) + + def finish_request(self, req, resp): + if 'deliv_log' in req.GET: + resp.body += self.format_html_log() + resp.cache_expires() + return resp + + log_template = HTMLTemplate('''\ +

Deliverance Information

+ + {{if log.descriptions:}} + {{div}} + {{h2}}What happened? + {{div_inner}} +
    + {{for desc in log.descriptions}} +
  1. {{desc}}
  2. + {{endfor}} +
+ + {{endif}} + + {{if log.messages}} + {{div}} + {{h2}}Log + {{div_inner}} + + + + + {{for level, level_name, el, message in log.resolved_messages():}} + {{py:color, bgcolor = log.color_for_level(level)}} + + {{td}}{{level_name}} + {{td}}{{message}} + {{td}}{{log.obj_as_html(el) | html}} + + {{endfor}} +
LevelMessageContext
+ + {{else}} + {{h2}}No Log Messages + {{endif}} + ''', name='deliverance.log.SavingLogger.log_template') + + tags = dict( + h2=html('

'), + div=html('
'), + div_inner=html('
'), + td=html(''), + ) + + def format_html_log(self): + return self.log_template.substitute( + log=self, middleware=self.middleware, **self.tags) + + def resolved_messages(self): + for level, el, msg in self.messages: + level_name = logging.getLevelName(level) + yield level, level_name, el, msg + + def obj_as_html(self, el): + ## FIXME: another magic method? + if hasattr(el, 'log_description'): + return el.log_description(self) + elif isinstance(el, _Element): + return html_quote(tostring(el)) + else: + return html_quote(unicode(el)) + + def color_for_level(self, level): + return { + logging.DEBUG: ('#666', '#fff'), + logging.INFO: ('#333', '#fff'), + NOTIFY: ('#000', '#fff'), + logging.WARNING: ('#600', '#fff'), + logging.ERROR: ('#fff', '#600'), + logging.CRITICAL: ('#000', '#f33')}[level] + + def link_to(self, url, source=False, line=None, selector=None): + return self.middleware.link_to(self.request, url, source=source, line=line, selector=selector) class PrintingLogger(SavingLogger): - def __init__(self, request, description=True, print_level=logging.DEBUG): - super(PrintingLogger, self).__init__(request, description=description) + def __init__(self, request, middleware, description=True, print_level=logging.DEBUG): + super(PrintingLogger, self).__init__(request, middleware, description=description) self.print_level = print_level def add_description(self, msg): Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py Tue Jul 1 19:07:32 2008 @@ -1,6 +1,14 @@ -from webob import Request +from webob import Request, Response +from webob import exc +from wsgiproxy.exactproxy import proxy_exact_request from deliverance.log import SavingLogger -import urllib2 +import urllib +import hmac +import sha +from pygments import highlight as pygments_highlight +from pygments.lexers import XmlLexer, HtmlLexer, guess_lexer_for_filename +from pygments.formatters import HtmlFormatter +from tempita import HTMLTemplate, html_quote, html class DeliveranceMiddleware(object): @@ -10,39 +18,170 @@ self.log_factory = log_factory self.log_factory_kw = log_factory_kw + def log_description(self, log=None): + return 'Deliverance' + def __call__(self, environ, start_response): ## FIXME: copy_get?: - orig_req = Request(environ.copy()) req = Request(environ) - log = self.log_factory(req, **self.log_factory_kw) + req.environ['deliverance.base_url'] = req.application_url + orig_req = Request(environ.copy()) + log = self.log_factory(req, self, **self.log_factory_kw) + def resource_fetcher(url): + return self.get_resource(url, orig_req, log) + if req.path_info_peek() == '.deliverance': + req.path_info_pop() + resp = self.internal_app(req, resource_fetcher) + return resp(environ, start_response) resp = req.get_response(self.app) ## FIXME: also XHTML? if resp.content_type != 'text/html': return resp(environ, start_response) - def get_resource(url): - assert url is not None - ## FIXME: should this return a webob.Response object? - if url.startswith(orig_req.application_url + '/'): - subreq = orig_req.copy_get() - subreq.environ['deliverance.subrequest_original_environ'] = orig_req.environ - new_path_info = url[len(orig_req.application_url):] - assert new_path_info.startswith('/') - subreq.path_info = new_path_info - subresp = subreq.get_response(self.app) - ## FIXME: error if not HTML? - ## FIXME: handle redirects? - ## FIXME: handle non-200? - return subresp.body - else: - ## FIXME: pluggable subrequest handler? - f = urllib2.urlopen(url) - body = f.read() - f.close() - return body - rule_set = self.rule_getter(get_resource, self.app, orig_req) - resp = rule_set.apply_rules(req, resp, get_resource, log) + rule_set = self.rule_getter(resource_fetcher, self.app, orig_req) + resp = rule_set.apply_rules(req, resp, resource_fetcher, log) + resp = log.finish_request(req, resp) return resp(environ, start_response) + def get_resource(self, url, orig_req, log): + assert url is not None + ## FIXME: should this return a webob.Response object? + if url.startswith(orig_req.application_url + '/'): + subreq = orig_req.copy_get() + subreq.environ['deliverance.subrequest_original_environ'] = orig_req.environ + new_path_info = url[len(orig_req.application_url):] + query_string = '' + if '?' in new_path_info: + new_path_info, query_string = new_path_info.split('?') + new_path_info = urllib.unquote(new_path_info) + assert new_path_info.startswith('/') + subreq.path_info = new_path_info + subreq.query_string = query_string + subresp = subreq.get_response(self.app) + ## FIXME: error if not HTML? + ## FIXME: handle redirects? + ## FIXME: handle non-200? + log.debug(self, 'Internal request for %s: %s content-type: %s', + url, subresp.status, subresp.content_type) + return subresp + else: + ## FIXME: pluggable subrequest handler? + subreq = Request.blank(url) + resp = subreq.get_response(proxy_exact_request) + log.debug(self, 'External request for %s: %s content-type: %s', + url, subresp.status, subresp.content_type) + return subresp + + + def link_to(self, req, url, source=False, line=None, selector=None): + base = req.environ['deliverance.base_url'] + base += '/.deliverance/view' + source = int(bool(source)) + args = {'url': url} + if source: + args['source'] = '1' + if line: + args['line'] = str(line) + if selector: + args['selector'] = selector + url = base + '?' + urllib.urlencode(args) + if selector: + url += '#deliverance-selection' + if line: + ## FIXME: figure out line anchor + pass + return url + + def internal_app(self, req, resource_fetcher): + segment = req.path_info_peek() + method = 'action_%s' % segment + method = getattr(self, method, None) + if not method: + return exc.HTTPNotFound('There is no %r action' % segment) + try: + return method(req, resource_fetcher) + except exc.HTTPException, e: + return e + + def action_view(self, req, resource_fetcher): + url = req.GET['url'] + source = int(req.GET.get('source', '0')) + line = int(req.GET.get('line', '0')) or '' + selector = req.GET.get('selector', '') + subresp = resource_fetcher(url) + if source: + ct = subresp.content_type + if ct.content_type.startswith('application/xml'): + lexer = XmlLexer() + elif ct == 'text/html': + lexer = HtmlLexer() + else: + ## FIXME: what then? + lexer = HtmlLexer() + text = pygments_highlight( + subresp.body, lexer, + HtmlFormatter(linenos=linenos, lineanchors='code')) + else: + from deliverance.selector import Selector + from lxml.html import fromstring, document_fromstring, tostring + doc = document_fromstring(subresp.body) + selector = Selector.parse(selector) + type, elements, attributes = selector(doc) + if not elements: + template = self._not_found_template + else: + template = self._found_template + all_elements = [] + for index, el in enumerate(elements): + anchor = 'deliverance-selection' + if index: + anchor += '-%s' % index + ## FIXME: is a better? + el.set('id', anchor) + ## FIXME: add :target CSS rule + ## FIXME: or better, some Javascript + all_elements.append((anchor, el)) + style = el.get('style', '') + if style: + style += '; ' + style += '/* deliverance */ border: 2px dotted #f00' + el.set('style', style) + def format_tag(tag): + return tostring(tag).split('>')[0]+'>' + text = template.substitute( + elements=all_elements, selector=selector, format_tag=format_tag) + message = fromstring(self._message_template.substitute(message=text)) + if doc.body.text: + message.tail = doc.body.text + doc.body.text = '' + doc.body.insert(0, message) + text = tostring(doc) + resp = Response(text) + return resp + + _not_found_template = HTMLTemplate('''\ + There were no elements that matched the selector {{selector}} + ''', 'deliverance.middleware.DeliveranceMiddleware._not_found_template') + + _found_template = HTMLTemplate('''\ + {{if len(elements) == 1}} + One element matched the selector {{selector}}; + jump to element + {{else}} + {{len(elements)}} elements matched the selector {{selector}}: +
    + {{for anchor, el in elements}} +
  1. {{format_tag(el)}}
  2. + {{endfor}} +
+ {{endif}} + ''', 'deliverance.middleware.DeliveranceMiddleware._found_template') + + _message_template = HTMLTemplate('''\ +
+ {{message|html}} +
''', 'deliverance.middleware.DeliveranceMiddleware._message_template') + + class SubrequestRuleGetter(object): def __init__(self, url): @@ -52,7 +191,16 @@ from lxml.etree import XML, XMLSyntaxError import urlparse url = urlparse.urljoin(orig_req.url, self.url) - doc_text = get_resource(url) + doc_resp = get_resource(url) + if doc_resp.status_int != 200: + ## FIXME: better error + assert 0, "Bad response: %r" % doc_resp + ## FIXME: better content-type detection + if doc_resp.content_type != 'application/xml': + ## FIXME: better error + assert 0, "Bad response content-type: %s (from response %r)" % ( + doc_resp.content_type, doc_resp) + doc_text = doc_resp.body try: doc = XML(doc_text, base_url=url) except XMLSyntaxError, e: Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Tue Jul 1 19:07:32 2008 @@ -7,6 +7,7 @@ from deliverance.util.converters import asbool, html_quote from deliverance.selector import Selector from lxml import etree +from tempita import html CONTENT_ATTRIB = 'x-a-marker-attribute-for-deliverance' @@ -197,32 +198,7 @@ move_supported = True def describe_self(self): - """ - A text description of this rule, for use in log messages and errors - """ - parts = ['<%s' % self.name] - if getattr(self, 'content', None): - parts.append('content="%s"' % html_quote(self.content)) - if getattr(self, 'content_href', None): - parts.append('href="%s"' % html_quote(self.content_href)) - if self.move_supported and not getattr(self, 'move', False): - parts.append('move="1"') - v = getattr(self, 'nocontent', 'warn') - if v != 'warn': - parts.append(self.format_error('nocontent', v)) - v = getattr(self, 'manycontent', ('warn', None)) - if v != ('warn', None): - parts.append(self.format_error('manycontent', v)) - if getattr(self, 'theme', None): - parts.append('theme="%s"' % html_quote(self.theme)) - v = getattr(self, 'notheme', 'warn') - if v != 'warn': - parts.append(self.format_error('notheme', v)) - v = getattr(self, 'manytheme', ('warn', None)) - if v != ('warn', None): - parts.append(self.format_error('manytheme', v)) - ## FIXME: add source_location - return ' '.join(parts) + ' />' + return self.log_description(log=None) def __str__(self): return self.describe_self() @@ -295,6 +271,52 @@ elements.remove(el) return type, elements, attributes + def log_description(self, log=None): + """ + A text description of this rule, for use in log messages and errors + """ + def linked_item(url, body, source=None, line=None, selector=None): + body = html_quote(body) + if log is None or url is None: + return body + link = log.link_to(url, source=source, line=line, selector=selector) + return '%s' % (html_quote(link), body) + if log: + request_url = log.request.url + else: + request_url = None + parts = ['<%s' % linked_item(self.source_location, self.name)] + if getattr(self, 'content', None): + body = 'content="%s"' % html_quote(self.content) + parts.append(linked_item(request_url, body, selector=self.content)) + if getattr(self, 'content_href', None): + dest = self.content_href + if request_url: + dest = urlparse.urljoin(request_url, dest) + body = 'href="%s"' % html_quote(self.content_href) + parts.append(linked_item(dest, body, source=True)) + if self.move_supported and not getattr(self, 'move', False): + parts.append('move="1"') + v = getattr(self, 'nocontent', 'warn') + if v != 'warn': + parts.append(self.format_error('nocontent', v)) + v = getattr(self, 'manycontent', ('warn', None)) + if v != ('warn', None): + parts.append(self.format_error('manycontent', v)) + if getattr(self, 'theme', None): + body = 'theme="%s"' % html_quote(self.theme) + theme_url = getattr(log, 'theme_url', None) + parts.append(linked_item(theme_url, body, selector=self.theme)) + v = getattr(self, 'notheme', 'warn') + if v != 'warn': + parts.append(self.format_error('notheme', v)) + v = getattr(self, 'manytheme', ('warn', None)) + if v != ('warn', None): + parts.append(self.format_error('manytheme', v)) + ## FIXME: add source_location + return html(' '.join(parts) + ' />') + + class TransformAction(AbstractAction): # Abstract class for the rules that move from the content to the theme (replace, append, prepend) @@ -342,7 +364,8 @@ """ describe = log.describe if self.content_href: - content_doc = resource_fetcher(self.content_href) + ## FIXME: check response type + content_doc = resource_fetcher(self.content_href).body if not self.if_content_matches(content_doc, log): return content_type, content_els, content_attributes = self.select_elements(self.content, content_doc, theme=False) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py Tue Jul 1 19:07:32 2008 @@ -1,4 +1,4 @@ -from deliverance.exceptions import AbortTheme +from deliverance.exceptions import AbortTheme, DeliveranceSyntaxError from deliverance.pagematch import run_matches, Match from deliverance.rules import Rule, remove_content_attribs from lxml.html import tostring, document_fromstring @@ -55,10 +55,12 @@ resp.body = tostring(theme_doc) return resp - def get_theme(self, url, resource_getter, log): + def get_theme(self, url, resource_fetcher, log): log.info(self, 'Fetching theme from %s' % url) + log.theme_url = url ## FIXME: should do caching - doc = self.parse_document(resource_getter(url), url) + ## FIXME: check response status + doc = self.parse_document(resource_fetcher(url).body, url) doc.make_links_absolute() return doc @@ -82,8 +84,10 @@ ## FIXME: Add parse error default_theme = el.get('href') else: - ## FIXME: better error - assert 0 + ## FIXME: source location? + raise DeliveranceSyntaxError( + "Invalid tag %s (unknown tag name %r)" % (tostring(el).split('>', 1)[0]+'>', el.tag), + element=el) rules_by_class = {} for rule in rules: for class_name in rule.classes: @@ -116,6 +120,7 @@ standard_rule = Rule.parse_xml(XML('''\ + Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/index.html ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/index.html Tue Jul 1 19:07:32 2008 @@ -0,0 +1,10 @@ + + +My Site + + + + +This is my awesome site! + + Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml Tue Jul 1 19:07:32 2008 @@ -0,0 +1,7 @@ + + + + + + + Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css Tue Jul 1 19:07:32 2008 @@ -0,0 +1,9 @@ +body { + font-family: sans-serif; +} + +#footer { + border-top: 1px solid #999; + font-size: 70%; + margin-top: 1em; +} Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html Tue Jul 1 19:07:32 2008 @@ -0,0 +1,14 @@ + + +A theme + + + + +

A Page

+ +
Some content
+ + + + Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example.py Tue Jul 1 19:07:32 2008 @@ -0,0 +1,16 @@ +""" +This sets up an example site +""" + +import os +from paste.urlparser import StaticURLParser +from paste.httpserver import serve +from weberror.evalexception import EvalException +from deliverance.middleware import DeliveranceMiddleware, SubrequestRuleGetter + +base_path = os.path.join(os.path.dirname(__file__), 'example-files') +app = StaticURLParser(base_path) +deliv_app = DeliveranceMiddleware(app, SubrequestRuleGetter('/rules.xml')) + +if __name__ == '__main__': + serve(EvalException(deliv_app)) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py Tue Jul 1 19:07:32 2008 @@ -30,6 +30,10 @@ tests_require=['nose'], install_requires=[ "lxml", + "WebOb", + "WSGIProxy", + "Tempita", + "Pygments", ], entry_points=""" """, From ianb at codespeak.net Tue Jul 1 19:40:41 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 1 Jul 2008 19:40:41 +0200 (CEST) Subject: [z3-checkins] r56210 - in z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance: . tests Message-ID: <20080701174041.9CDBE168509@codespeak.net> Author: ianb Date: Tue Jul 1 19:40:41 2008 New Revision: 56210 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_middleware.txt z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_selection.txt Log: Remove the description stuff (use only normal logging) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py Tue Jul 1 19:40:41 2008 @@ -18,24 +18,13 @@ """ Logger that saves all its messages locally. """ - def __init__(self, request, middleware, description=None): + def __init__(self, request, middleware): self.messages = [] - if description is None: - description = 'deliv_log' in request.GET - if description: - self.descriptions = [] - self.describe = self.add_description - else: - self.descriptions = None - self.describe = None self.middleware = middleware self.request = request # This is writable: self.theme_url = None - def add_description(self, msg): - self.descriptions.append(msg) - def message(self, level, el, msg, *args, **kw): if args: msg = msg % args @@ -66,18 +55,6 @@ log_template = HTMLTemplate('''\

Deliverance Information

- {{if log.descriptions:}} - {{div}} - {{h2}}What happened?

- {{div_inner}} -
    - {{for desc in log.descriptions}} -
  1. {{desc}}
  2. - {{endfor}} -
- - {{endif}} - {{if log.messages}} {{div}} {{h2}}Log @@ -140,14 +117,10 @@ class PrintingLogger(SavingLogger): - def __init__(self, request, middleware, description=True, print_level=logging.DEBUG): - super(PrintingLogger, self).__init__(request, middleware, description=description) + def __init__(self, request, middleware, print_level=logging.DEBUG): + super(PrintingLogger, self).__init__(request, middleware) self.print_level = print_level - def add_description(self, msg): - print 'description:', msg - super(PrintingLogger, self).add_description(msg) - def message(self, level, el, msg, *args, **kw): msg = super(PrintingLogger, self).message(level, el, msg, *args, **kw) if level >= self.print_level: Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Tue Jul 1 19:40:41 2008 @@ -186,9 +186,6 @@ if ((not matched and not self.if_content.inverted) or (matched and self.if_content.inverted)): log.info(self, 'skipping rule because if-content="%s" does not match', self.if_content) - if log.describe: - log.describe('skipping rule %s because if-content="%s" does not match anything' - % (self, self.if_content)) return False return True @@ -362,7 +359,6 @@ """ Applies this action to the theme_doc. """ - describe = log.describe if self.content_href: ## FIXME: check response type content_doc = resource_fetcher(self.content_href).body @@ -378,10 +374,6 @@ else: log_meth = log.warn log_meth(self, 'skipping rule because no content matches rule content="%s"', self.content) - if describe: - describe( - 'skipping rule %s because content="%s" does not match anything' - % (self.describe_self(), html_quote(self.content))) return theme_type, theme_els, theme_attributes = self.select_elements(self.theme, theme_doc, theme=True) attributes = self.join_attributes(content_attributes, theme_attributes) @@ -393,9 +385,6 @@ else: log_meth = log.warn log_meth(self, 'skipping rule because no theme element matches rule theme="%s"', self.theme) - if describe: - describe('skipping rule %s because theme="%s" does not match anything' - % (self.describe_self(), html_quote(self.content))) return if len(theme_els) > 1: if self.manytheme[0] == 'warn': @@ -448,7 +437,6 @@ name = 'replace' def apply_transformation(self, content_type, content_els, attributes, theme_type, theme_el, log): - describe = log.describe if theme_type == 'children': existing_children = len(theme_el) or theme_el.text theme_el[:] = [] @@ -463,27 +451,10 @@ for el in content_els: el.tail = None theme_el.extend(content_els) - if describe: - if existing_children: - extra = ' and removed its children' - else: - extra = '' - describe( - "Rule %s moved elements %s into element %s%s" - % (self.describe_self(), self.describe_content_elements(content_els), self.describe_theme_element(theme_el), extra)) elif content_type == 'children': text, els = self.prepare_content_children(content_els) add_text(theme_el, text) theme_el.extend(els) - if describe: - if existing_children: - extra = ' and removed its children' - else: - extra = '' - describe( - "Rule %s moved the children of elements %s into element %s%s" - % (self.describe_self(), self.describe_content_elements(content_els, children=True), - self.describe_theme_element(theme_el), extra)) if self.move: # Since we moved just the children of the content elements, we still need to remove the parent # elements. @@ -577,7 +548,6 @@ ] def apply_transformation(self, content_type, content_els, attributes, theme_type, theme_el, log): - describe = log.describe if theme_type == 'children': if content_type == 'elements': if self.move: @@ -702,7 +672,6 @@ self.notheme = self.convert_error('notheme', notheme) def apply(self, content_doc, theme_doc, resource_fetcher, log): - describe = log.describe if not self.if_content_matches(content_doc, log): return for doc, selector, error, name in [(theme_doc, self.theme, self.notheme, 'theme'), (content_doc, self.content, self.nocontent, 'content')]: Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_middleware.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_middleware.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_middleware.txt Tue Jul 1 19:40:41 2008 @@ -92,7 +92,7 @@ >>> import logging >>> deliv = DeliveranceMiddleware(app, SubrequestRuleGetter('/rules.xml'), ... PrintingLogger, - ... log_factory_kw=dict(print_level=logging.WARNING, description=False)) + ... log_factory_kw=dict(print_level=logging.WARNING)) Now lets look at some plain content and its deliverated equivalent Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt Tue Jul 1 19:40:41 2008 @@ -11,7 +11,7 @@ >>> def match(matcher, request, response_headers, show_log=True): ... if isinstance(matcher, basestring): ... matcher = make(matcher) - ... log = SavingLogger(None) + ... log = SavingLogger(None, None) ... if isinstance(response_headers, list): ... response_headers = HeaderDict(response_headers) ... result = matcher(request, response_headers, log) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_selection.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_selection.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_selection.txt Tue Jul 1 19:40:41 2008 @@ -95,7 +95,7 @@ ... rule = parse_action(rule, None) ... theme_copy = copy.deepcopy(theme) ... theme_copy.make_links_absolute() - ... logger = SavingLogger(request=None) + ... logger = SavingLogger(request=None, middleware=None) ... content_copy = copy.deepcopy(content) ... rule.apply(content_copy, theme_copy, None, logger) ... remove_content_attribs(theme_copy) From ianb at codespeak.net Tue Jul 1 22:21:16 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 1 Jul 2008 22:21:16 +0200 (CEST) Subject: [z3-checkins] r56219 - in z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance: . tests/example-files Message-ID: <20080701202116.3250E169F10@codespeak.net> Author: ianb Date: Tue Jul 1 22:21:15 2008 New Revision: 56219 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml Log: Improved the log formatting, and the selection previewing Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py Tue Jul 1 22:21:15 2008 @@ -9,6 +9,8 @@ from pygments.lexers import XmlLexer, HtmlLexer, guess_lexer_for_filename from pygments.formatters import HtmlFormatter from tempita import HTMLTemplate, html_quote, html +from lxml.etree import _Element +from lxml.html import fromstring, document_fromstring, tostring class DeliveranceMiddleware(object): @@ -87,8 +89,7 @@ if selector: url += '#deliverance-selection' if line: - ## FIXME: figure out line anchor - pass + url += '#code-%s' % line return url def internal_app(self, req, resource_fetcher): @@ -110,7 +111,7 @@ subresp = resource_fetcher(url) if source: ct = subresp.content_type - if ct.content_type.startswith('application/xml'): + if ct.startswith('application/xml'): lexer = XmlLexer() elif ct == 'text/html': lexer = HtmlLexer() @@ -119,10 +120,9 @@ lexer = HtmlLexer() text = pygments_highlight( subresp.body, lexer, - HtmlFormatter(linenos=linenos, lineanchors='code')) + HtmlFormatter(full=True, linenos=True, lineanchors='code')) else: from deliverance.selector import Selector - from lxml.html import fromstring, document_fromstring, tostring doc = document_fromstring(subresp.body) selector = Selector.parse(selector) type, elements, attributes = selector(doc) @@ -131,25 +131,44 @@ else: template = self._found_template all_elements = [] + els_in_head = False for index, el in enumerate(elements): + el_in_head = self._el_in_head(el) + if el_in_head: + els_in_head = True anchor = 'deliverance-selection' if index: anchor += '-%s' % index + if el.get('id'): + anchor = el.get('id') ## FIXME: is a better? - el.set('id', anchor) + if not el_in_head: + el.set('id', anchor) + else: + anchor = None ## FIXME: add :target CSS rule ## FIXME: or better, some Javascript all_elements.append((anchor, el)) - style = el.get('style', '') - if style: - style += '; ' - style += '/* deliverance */ border: 2px dotted #f00' - el.set('style', style) + if not el_in_head: + style = el.get('style', '') + if style: + style += '; ' + style += '/* deliverance */ border: 2px dotted #f00' + el.set('style', style) + else: + el.set('DELIVERANCE-MATCH', '1') + def highlight(html_code): + if isinstance(html_code, _Element): + html_code = tostring(html_code) + return html(pygments_highlight(html_code, HtmlLexer(), + HtmlFormatter(noclasses=True))) def format_tag(tag): - return tostring(tag).split('>')[0]+'>' + return highlight(tostring(tag).split('>')[0]+'>') text = template.substitute( - elements=all_elements, selector=selector, format_tag=format_tag) - message = fromstring(self._message_template.substitute(message=text)) + els_in_head=els_in_head, doc=doc, + elements=all_elements, selector=selector, + format_tag=format_tag, highlight=highlight) + message = fromstring(self._message_template.substitute(message=text, url=url)) if doc.body.text: message.tail = doc.body.text doc.body.text = '' @@ -158,6 +177,13 @@ resp = Response(text) return resp + def _el_in_head(self, el): + while el is not None: + if el.tag == 'head': + return True + el = el.getparent() + return False + _not_found_template = HTMLTemplate('''\ There were no elements that matched the selector {{selector}} ''', 'deliverance.middleware.DeliveranceMiddleware._not_found_template') @@ -165,19 +191,37 @@ _found_template = HTMLTemplate('''\ {{if len(elements) == 1}} One element matched the selector {{selector}}; - jump to element + {{if elements[0][0]}} + jump to element + {{else}} + element is in head: {{highlight(elements[0][1])}} + {{endif}} {{else}} {{len(elements)}} elements matched the selector {{selector}}:
    {{for anchor, el in elements}} -
  1. {{format_tag(el)}}
  2. + {{if anchor}} +
  3. {{format_tag(el)}}
  4. + {{else}} +
  5. {{format_tag(el)}}
  6. + {{endif}} {{endfor}}
{{endif}} + {{if els_in_head}} +
+ Elements matched in head. Showing head:
+
+ {{highlight(doc.head)}} +
+
+ {{endif}} ''', 'deliverance.middleware.DeliveranceMiddleware._found_template') _message_template = HTMLTemplate('''\
+ + Viewing {{url}}
{{message|html}}
''', 'deliverance.middleware.DeliveranceMiddleware._message_template') Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Tue Jul 1 22:21:15 2008 @@ -7,7 +7,9 @@ from deliverance.util.converters import asbool, html_quote from deliverance.selector import Selector from lxml import etree +from lxml.html import document_fromstring from tempita import html +import copy CONTENT_ATTRIB = 'x-a-marker-attribute-for-deliverance' @@ -137,6 +139,8 @@ assert isinstance(value, tuple), ( "Unexpected value: %r (for attribute %s)" % ( value, attr)) + if value == ('warn', 'first'): + return None handler, pos = value if pos == 'last': text = '%s:%s' % (handler, pos) @@ -194,29 +198,8 @@ # Set to true in subclasses if the move attribute means something: move_supported = True - def describe_self(self): - return self.log_description(log=None) - def __str__(self): - return self.describe_self() - - def describe_content_elements(self, els, children=False): - """ - A text description of a list of content elements, for use in - log messages and errors. - """ - text = ', '.join(el.tag for el in els) - if children: - return 'children of %s' % text - else: - return text - - def describe_theme_element(self, el): - """ - A text description of a theme element, for use in log messages - and errors. - """ - return el.tag + return self.log_description(log=None) @classmethod def compile_selector(cls, el, attr, source_location): @@ -259,6 +242,7 @@ originating in the content are not selectable). """ type, elements, attributes = selector(doc) + orig_elements = list(elements) if theme: bad_els = [] for el in elements: @@ -277,15 +261,22 @@ if log is None or url is None: return body link = log.link_to(url, source=source, line=line, selector=selector) - return '%s' % (html_quote(link), body) + return '%s' % (html_quote(link), body) if log: request_url = log.request.url else: request_url = None - parts = ['<%s' % linked_item(self.source_location, self.name)] + parts = ['<%s' % linked_item(self.source_location, self.name, source=True)] if getattr(self, 'content', None): body = 'content="%s"' % html_quote(self.content) - parts.append(linked_item(request_url, body, selector=self.content)) + if self.content_href: + if request_url: + content_url = urlparse.urljoin(request_url, self.content_href) + else: + content_url = self.content_href + else: + content_url = request_url + parts.append(linked_item(content_url, body, selector=self.content)) if getattr(self, 'content_href', None): dest = self.content_href if request_url: @@ -293,12 +284,12 @@ body = 'href="%s"' % html_quote(self.content_href) parts.append(linked_item(dest, body, source=True)) if self.move_supported and not getattr(self, 'move', False): - parts.append('move="1"') + parts.append('move="0"') v = getattr(self, 'nocontent', 'warn') if v != 'warn': parts.append(self.format_error('nocontent', v)) v = getattr(self, 'manycontent', ('warn', None)) - if v != ('warn', None): + if v != ('warn', 'first'): parts.append(self.format_error('manycontent', v)) if getattr(self, 'theme', None): body = 'theme="%s"' % html_quote(self.theme) @@ -308,12 +299,44 @@ if v != 'warn': parts.append(self.format_error('notheme', v)) v = getattr(self, 'manytheme', ('warn', None)) - if v != ('warn', None): + if v != ('warn', 'first'): parts.append(self.format_error('manytheme', v)) ## FIXME: add source_location return html(' '.join(parts) + ' />') + def format_tags(self, elements, include_name=True): + if not elements: + if include_name: + return 'no elements' + else: + return '(none)' + text = ', '.join('<%s>' % el.tag for el in elements) + if include_name: + if len(elements) > 1: + return 'elements %s' % text + else: + return 'element %s' % text + else: + return text + def format_tag(self, tag, include_name=False): + return self.format_tags([tag], include_name=include_name) + + def format_attribute_names(self, attributes, include_name=True): + if not attributes: + if include_name: + return 'no attributes' + else: + return '(none)' + text = ', '.join(attributes) + if include_name: + if len(attributes) > 1: + return 'attributes %s' % text + else: + return 'attribute %s' % text + else: + return text + class TransformAction(AbstractAction): # Abstract class for the rules that move from the content to the theme (replace, append, prepend) @@ -361,13 +384,17 @@ """ if self.content_href: ## FIXME: check response type - content_doc = resource_fetcher(self.content_href).body + ## FIXME: Join content_href with request url? + content_resp = resource_fetcher(self.content_href) + log.debug(self, 'Fetching resource from href="%s": %s', + self.content_href, content_resp.status) + content_doc = document_fromstring(content_resp.body, base_url=self.content_href) if not self.if_content_matches(content_doc, log): return content_type, content_els, content_attributes = self.select_elements(self.content, content_doc, theme=False) if not content_els: if self.nocontent == 'abort': - log.debug(self, 'aborting theme because no content matches rule content="%s"', self.content) + log.debug(self, 'aborting theming because no content matches rule content="%s"', self.content) raise AbortTheme('No content matches content="%s"' % self.content) elif self.nocontent == 'ignore': log_meth = log.debug @@ -379,6 +406,7 @@ attributes = self.join_attributes(content_attributes, theme_attributes) if not theme_els: if self.notheme == 'abort': + log.debug(self, 'aborting theming because no theme elements match rule theme="%s"', self.theme) raise AbortTheme('No theme element matches theme="%s"' % self.theme) elif self.notheme == 'ignore': log_meth = log.debug @@ -387,19 +415,25 @@ log_meth(self, 'skipping rule because no theme element matches rule theme="%s"', self.theme) return if len(theme_els) > 1: - if self.manytheme[0] == 'warn': - log.warn(self, '%s elements match theme="%s", using the %s match', - len(theme_els), self.theme, self.manytheme[1]) - pass - elif self.manytheme[0] == 'abort': + if self.manytheme[0] == 'abort': + log.debug(self, 'aborting theming because %i elements (%s) match theme="%s"', + len(theme_els), self.format_tags(theme_els, include_name=False), self.theme) raise AbortTheme('Many elements match theme="%s"' % self.theme) + elif self.manytheme[0] == 'warn': + log_meth = log.warn + else: + log_meth = log.debug if self.manytheme[1] == 'first': - theme_els = [theme_els[0]] + theme_el = theme_els[0] else: - theme_els = [theme_els[-1]] - theme_el = theme_els[0] + theme_el = theme_els[-1] + log_meth(self, '%s elements match theme="%s", using the %s match', + len(theme_els), self.theme, self.manytheme[1]) + else: + theme_el = theme_els[0] if not self.move and theme_type in ('children', 'elements'): - self.log.debug(self, 'content elements are being copied into theme (not moved)') + ## FIXME: is this message necessary? + log.debug(self, 'content elements are being copied into theme (not moved)') content_els = copy.deepcopy(content_els) mark_content_els(content_els) self.apply_transformation(content_type, content_els, attributes, theme_type, theme_el, log) @@ -439,6 +473,13 @@ def apply_transformation(self, content_type, content_els, attributes, theme_type, theme_el, log): if theme_type == 'children': existing_children = len(theme_el) or theme_el.text + theme_empty = not len(theme_el) and not theme_el.text + if len(theme_el): + log_text = 'and removed the chilren and text of the theme element' + elif theme_el.text: + log_text = 'and removed the text content of the theme element' + else: + log_text = '(the theme was already empty)' theme_el[:] = [] theme_el.text = '' if content_type == 'elements': @@ -446,11 +487,15 @@ # If we aren't working with copies then we have to move the tails up as we remove the elements: for el in reversed(content_els): move_tail_upward(el) + verb = 'Moving' else: # If we are working with copies, then we can just throw away the tails: for el in content_els: el.tail = None + verb = 'Copying' theme_el.extend(content_els) + log.debug(self, '%s %s from content into theme element %s %s', + verb, self.format_tags(content_els), self.format_tag(theme_el), log_text) elif content_type == 'children': text, els = self.prepare_content_children(content_els) add_text(theme_el, text) @@ -460,6 +505,12 @@ # elements. for el in content_els: el.getparent().remove(el) + log.debug(self, 'Moving children of content %s into theme element %s, ' + 'and removing now-empty content elements %s', + self.format_tags(content_els), self.format_tag(theme_el), log_text) + else: + log.debug(self, 'Copying children of content %s into theme element %s %s', + self.format_tags(content_els), self.format_tag(theme_el), log_text) else: assert 0 @@ -471,10 +522,14 @@ if self.move: for el in reversed(content_els): move_tail_upwards(el) + verb = 'moved' else: for el in content_els: el.tail = None + verb = 'copied' parent[pos:pos+1] = content_els + log.debug(self, 'Replaced the theme element %s with the content %s (%s)', + self.format_tag(theme_el), self.format_tags(content_els), verb) elif content_type == 'children': text, els = self.prepare_content_children(content_els) if pos == 0: @@ -485,6 +540,12 @@ if self.move: for el in content_els: el.getparent().remove(el) + log.debug(self, 'Replaced the theme element %s with the children of the content %s, ' + 'and removed the now-empty content element(s)', + self.format_tag(theme_el), self.format_tags(content_els)) + else: + log.debug(self, 'Replaced the theme element %s with copies of the children of the content %s', + self.format_tag(theme_el), self.format_tags(content_els)) else: assert 0 @@ -493,20 +554,25 @@ assert content_type == 'attributes' if len(content_els) > 1: if self.manycontent[0] == 'abort': - log.debug(self, 'aborting because %s elements in the content match content="%s"', - len(content_els), self.content) + log.debug(self, 'aborting because %i elements in the content (%s) match content="%s"', + len(content_els), self.format_tags(content_els, include_name=False), self.content) raise AbortTheme() else: if self.manycontent[0] == 'warn': log_meth = log.warn else: log_meth = log.debug - log_meth(self, '%s elements match content="%s" (but only one expected), using the %s match', - len(content_els, self.content, self.manycontent[1])) + log_meth(self, '%s elements match content="%s" (%s) when only one is expected, using the %s match', + len(content_els), self.content, self.format_tags(content_els, include_name=False), self.manycontent[1]) if self.manycontent[1] == 'first': content_els = [content_els[0]] else: content_els = [content_els[-1]] + if theme_el.attrib: + log_text = ' and cleared all existing theme attributes' + else: + log_text = '' + ## FIXME: should this only clear the named attribute? (when attributes are named) theme_el.attrib.clear() if attributes: c_attrib = content_els[0].attrib @@ -517,17 +583,31 @@ for name in attributes: if name in c_attrib: del c_attrib[name] + log_text += ' and removed the attributes from the content' + ## FIXME: only list attributes that were actually found? + log.debug(self, 'Copied the %s from the content element %s to the ' + 'theme element %s%s', + self.format_attribute_names(attributes), self.format_tag(content_els[0]), + self.format_tag(theme_el), log_text) else: theme_el.attrib.update(content_els[0].attrib) if self.move: content_els[0].attrib.clear() + log_text += ' and removed all attributes from the content' + log.debug(self, 'Moved all the attributes from the content element %s to the ' + 'theme element %s%s', + self.format_tag(content_els[0]), self.format_tag(theme_el), log_text) if theme_type == 'tag': + ## FIXME: warn about manycontent assert content_type == 'tag' + old_tag = theme_el.tag theme_el.tag = content_els[0].tag theme_el.attrib.clear() theme_el.attrib.update(content_els[0].attrib) # "move" in this case doesn't mean anything + log.debug(self, 'Changed the tag name of the theme element <%s> to the name of the content element: %s', + old_tag, self.format_tag(content_els[0])) _actions['replace'] = Replace @@ -553,15 +633,21 @@ if self.move: for el in reversed(content_els): move_tail_upwards(el) + verb = 'Moving' else: for el in content_els: el.tail = None + verb = 'Copying' if self._append: theme_el.extend(content_els) + pos_text = 'end' else: add_tail(content_els[-1], theme_el.text) theme_el.text = None theme_el[:0] = content_els + pos_text = 'beginning' + log.debug(self, '%s content %s to the %s of theme element %s', + verb, self.format_tags(content_els), pos_text, self.format_tag(theme_el)) elif content_type == 'children': text, els = self.prepare_content_children(content_els) if self._append: @@ -570,10 +656,23 @@ else: add_text(theme_el, text) theme_el.extend(els) + pos_text = 'end' else: add_tail(els[-1], theme_el.text) theme_el.text = text theme_el[:0] = els + pos_text = 'beginning' + if self.move: + for el in reversed(content_els): + move_tail_upwards(el) + el.getparent().remove(el) + verb = 'Moving' + log_text = ' and removing the now-empty content element(s)' + else: + verb = 'Copying' + log_text = '' + log.debug(self, '%s the children of content %s to the %s of the theme element %s%s', + verb, self.format_tags(content_els), pos_text, self.format_tag(theme_el), log_text) else: assert 0 @@ -584,61 +683,120 @@ if self.move: for el in reversed(content_els): move_tail_upwards(el) + verb = 'Moving' else: for el in content_els: el.tail = None + verb = 'Copying' if self._append: parent[pos+1:pos+1] = content_els + pos_text = 'after' else: parent[pos:pos] = content_els + pos_text = 'before' + log.debug(self, '%s content %s %s the theme element %s', + verb, self.format_tags(content_els), pos_text, self.format_tag(theme_el)) elif content_type == 'children': text, els = self.prepare_content_children(content_els) if self._append: add_tail(theme_el, text) parent[pos+1:pos+1] = content_els + pos_text = 'after' else: if pos == 0: add_text(parent, text) else: add_tail(parent[pos-1], text) parent[pos:pos] = content_els + pos_text = 'before' + if self.move: + for el in reversed(content_els): + move_tail_upwards(el) + el.getparent().remove(el) + verb = 'Moving' + log_text = ' and removing the now-empty content element(s)' + else: + verb = 'Copying' + log_text = '' + log.debug(self, '%s the children of content %s %s the theme element %s%s', + verb, self.format_tags(content_els), pos_text, self.format_tag(theme_el), log_text) if theme_type == 'attributes': ## FIXME: handle named attributes assert content_type == 'attributes' if len(content_els) > 1: if self.manycontent[0] == 'abort': - log.debug(self, 'aborting because %s elements in the content match content="%s"', - len(content_els), self.content) + log.debug(self, 'aborting because %i elements in the content (%s) match content="%s"', + len(content_els), self.format_tags(content_els), self.content) raise AbortTheme() else: if self.manycontent[0] == 'warn': log_meth = log.warn else: log_meth = log.debug - log_meth(self, '%s elements match content="%s" (but only one expected), using the %s match', - len(content_els, self.content, self.manycontent[1])) + log_meth(self, '%s elements match content="%s" (%s) but only one is expected, using the %s match', + len(content_els), self.content, self.format_tags(content_els), self.manycontent[1]) if self.manycontent[1] == 'first': content_els = [content_els[0]] else: content_els = [content_els[-1]] content_attrib = content_els[0].attrib theme_attrib = theme_el.attrib + if self.move: + verb = 'Moved' + else: + verb = 'Copied' if self._append: if attributes: + avoided_attrs = [] + copied_attrs = [] for name in attributes: if name in content_attrib: - theme_attrib.setdefault(name, content_attrib[name]) + if name in theme_attrib: + avoided_attrs.append(name) + else: + theme_attrib[name] = content_attrib[name] + copied_attrs.append(name) + if avoided_attrs: + log.debug(self, '%s %s from the content element %s to the theme element %s, ' + 'and did not copy the %s because they were already ' + 'present in the theme element', + verb, self.format_attribute_names(copied_attrs), self.format_tag(content_els[0]), + self.format_tag(theme_el), self.format_attribute_names(avoided_attrs)) + else: + log.debug(self, '%s %s from the content element %s to the theme ' + 'element %s', + verb, self.format_attribute_names(copied_attrs), self.format_tag(content_els[0]), + self.format_tag(theme_el)) else: + avoided_attrs = [] + copied_attrs = [] for key, value in content_attrib.items(): - theme_attrib.setdefault(key, value) + if key in theme_attrib: + avoided_attrs.append(key) + else: + copied_attrs.append(key) + theme_attrib[key] = value + if avoided_attrs: + log.debug(self, '%s %s from the content element %s to the theme element %s, ' + 'and did not copy the %s because they were already present in the theme element', + verb, self.format_attribute_names(copied_attrs), self.format_tag(content_els[0]), + self.format_tag(theme_el), self.format_attribute_names(avoided_attrs)) + else: + log.debug(self, '%s %s from the content element %s to the theme element %s', + verb, self.format_attribute_names(copied_attrs), self.format_tag(content_els[0]), + self.format_tab(theme_el)) else: if attributes: for name in attributes: if name in content_attrib: theme_attrib.set(name, content_attrib[name]) + log.debug(self, '%s %s from the content element %s to the theme element %s', + verb, self.format_attribute_names(attributes), self.format_tag(content_els[0]), self.format_tag(theme_el)) else: theme_attrib.update(content_attrib) + log.debug(self, '%s all the attributes from the content element %s to the theme element %s', + verb, self.format_tag(content_els[0]), self.format_tag(theme_el)) if self.move: if attributes: for name in attributes: @@ -692,30 +850,43 @@ for el in els: move_tail_upwards(el) el.getparent().remove(el) + log.debug(self, 'Dropping %s %s', name, self.format_tags(els)) elif sel_type == 'children': - el[:] = [] - el.text = '' + for el in els: + el[:] = [] + el.text = '' + log.debug(self, 'Dropping the children of %s %s', name, self.format_tags(els)) elif sel_type == 'attributes': - attrib = el.attrib + for el in els: + attrib = el.attrib + if attributes: + for name in attributes: + if name in attrib: + del attrib[name] + else: + attrib.clear() if attributes: - for name in attributes: - if name in attrib: - del attrib[name] + log.debug(self, 'Dropping the %s from the %s %s', + self.format_attribute_names(attributes), name, self.format_tags(els)) else: - attrib.clear() + log.debug(self, 'Dropping all the attributes of %s %s', + name, self.format_tags(els)) elif sel_type == 'tag': - children = list(el) - if children: - add_tail(children[-1], el.tail) - else: - add_text(el, el.tail) - parent = el.getparent() - pos = parent.index(el) - if pos == 0: - add_text(parent, el.text) - else: - add_tail(parent[pos-1], el.text) - parent[pos:pos+1] = children + for el in els: + children = list(el) + if children: + add_tail(children[-1], el.tail) + else: + add_text(el, el.tail) + parent = el.getparent() + pos = parent.index(el) + if pos == 0: + add_text(parent, el.text) + else: + add_tail(parent[pos-1], el.text) + parent[pos:pos+1] = children + log.debug(self, 'Dropping the tag (flattening the element) of %s %s', + name, self.format_tags(els)) else: assert 0 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py Tue Jul 1 22:21:15 2008 @@ -9,10 +9,12 @@ class RuleSet(object): - def __init__(self, matchers, rules_by_class, default_theme=None): + def __init__(self, matchers, rules_by_class, default_theme=None, + source_location=None): self.matchers = matchers self.rules_by_class = rules_by_class self.default_theme = default_theme + self.source_location = source_location def apply_rules(self, req, resp, resource_fetcher, log): extra_headers = parse_meta_headers(resp.body) @@ -67,6 +69,14 @@ def parse_document(self, s, url): return document_fromstring(s, base_url=url) + def log_description(self, log=None): + if log is None: + name = 'ruleset' + else: + name = 'ruleset' % log.link_to(self.source_location, source=True) + desc = '<%s>' % name + return desc + @classmethod def parse_xml(cls, doc, source_location): assert doc.tag == 'ruleset' @@ -94,7 +104,8 @@ rules_by_class.setdefault(class_name, []).append(rule) if default_theme: default_theme = urlparse.urljoin(doc.base, default_theme) - return cls(matchers, rules_by_class, default_theme=default_theme) + return cls(matchers, rules_by_class, default_theme=default_theme, + source_location=source_location) _meta_tag_re = re.compile(r'', re.I | re.S) _http_equiv_re = re.compile(r'http-equiv=(?:"([^"]*)"|([^\s>]*))', re.I|re.S) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml Tue Jul 1 22:21:15 2008 @@ -2,6 +2,6 @@ - + From ianb at codespeak.net Tue Jul 1 23:21:08 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 1 Jul 2008 23:21:08 +0200 (CEST) Subject: [z3-checkins] r56220 - z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance Message-ID: <20080701212108.67A71169E21@codespeak.net> Author: ianb Date: Tue Jul 1 23:21:06 2008 New Revision: 56220 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Log: Add some links to the log; remove an unnecessary log message Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py Tue Jul 1 23:21:06 2008 @@ -55,6 +55,12 @@ log_template = HTMLTemplate('''\

Deliverance Information

+
+ theme: {{log.theme_url}} + | unthemed content + | content source +
+ {{if log.messages}} {{div}} {{h2}}Log @@ -86,8 +92,20 @@ ) def format_html_log(self): + content_source = self.link_to(self.request.url, source=True) return self.log_template.substitute( - log=self, middleware=self.middleware, **self.tags) + log=self, middleware=self.middleware, + unthemed_url=self._add_notheme(self.request.url), + theme_url=self._add_notheme(self.theme_url), + content_source=content_source, + **self.tags) + + def _add_notheme(self, url): + if '?' in url: + url += '&' + else: + url += '?' + return url + 'deliv_notheme' def resolved_messages(self): for level, el, msg in self.messages: Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py Tue Jul 1 23:21:06 2008 @@ -26,6 +26,8 @@ def __call__(self, environ, start_response): ## FIXME: copy_get?: req = Request(environ) + if 'deliv_notheme' in req.GET: + return self.app(environ, start_response) req.environ['deliverance.base_url'] = req.application_url orig_req = Request(environ.copy()) log = self.log_factory(req, self, **self.log_factory_kw) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Tue Jul 1 23:21:06 2008 @@ -432,8 +432,6 @@ else: theme_el = theme_els[0] if not self.move and theme_type in ('children', 'elements'): - ## FIXME: is this message necessary? - log.debug(self, 'content elements are being copied into theme (not moved)') content_els = copy.deepcopy(content_els) mark_content_els(content_els) self.apply_transformation(content_type, content_els, attributes, theme_type, theme_el, log) From ianb at codespeak.net Wed Jul 2 02:57:27 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Wed, 2 Jul 2008 02:57:27 +0200 (CEST) Subject: [z3-checkins] r56223 - in z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance: . tests/example-files Message-ID: <20080702005727.071BD2A018D@codespeak.net> Author: ianb Date: Wed Jul 2 02:57:25 2008 New Revision: 56223 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/index.html z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html Log: Added some more hooks for the element. Fixed the href attribute on rules. Fancied up the example Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py Wed Jul 2 02:57:25 2008 @@ -56,9 +56,9 @@

Deliverance Information

- theme: {{log.theme_url}} - | unthemed content - | content source + theme: {{log.theme_url}} + | unthemed content + | content source
{{if log.messages}} Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py Wed Jul 2 02:57:25 2008 @@ -70,7 +70,7 @@ else: ## FIXME: pluggable subrequest handler? subreq = Request.blank(url) - resp = subreq.get_response(proxy_exact_request) + subresp = subreq.get_response(proxy_exact_request) log.debug(self, 'External request for %s: %s content-type: %s', url, subresp.status, subresp.content_type) return subresp Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py Wed Jul 2 02:57:25 2008 @@ -33,7 +33,8 @@ """ Creates an instance of Match from the given parsed XML element. """ - assert el.tag == 'match' + assert (el.tag == 'match' + or el.tag == 'rule') classes = el.get('class', '').split() abort = asbool(el.get('abort')) if not abort and not classes: Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Wed Jul 2 02:57:25 2008 @@ -6,10 +6,13 @@ from deliverance.exceptions import add_exception_info, DeliveranceSyntaxError from deliverance.util.converters import asbool, html_quote from deliverance.selector import Selector +from deliverance.pagematch import Match +from deliverance.themeref import Theme from lxml import etree from lxml.html import document_fromstring from tempita import html import copy +import urlparse CONTENT_ATTRIB = 'x-a-marker-attribute-for-deliverance' @@ -18,13 +21,17 @@ This represents everything in a section. """ - def __init__(self, classes, actions, theme, suppress_standard, source_location): + def __init__(self, classes, actions, theme, match, suppress_standard, source_location): self.classes = classes self._actions = actions self.theme = theme + self.match = match self.suppress_standard = suppress_standard self.source_location = source_location + match_attrs = set([ + 'path', 'domain', 'request-header', 'response-header', 'environ']) + @classmethod def parse_xml(cls, el, source_location): """ @@ -40,14 +47,27 @@ for el in el.iterchildren(): if el.tag == 'theme': ## FIXME: error if more than one theme - ## FIXME: error if no href - theme = el.get('href') + theme = Theme.parse_xml(el, source_location) continue if el.tag is etree.Comment: continue action = parse_action(el, source_location) actions.append(action) - return cls(classes, actions, theme, suppress_standard, source_location) + match = None + for attr in cls.match_attrs: + if el.get(attr): + match = Match.parse_xml(el, source_location) + if match.abort: + raise DeliveranceSyntaxError( + "You cannot have an abort attribute on elements", + element=el) + if match.last: + ## FIXME: is last a good alternative to suppress-standard? + raise DeliveranceSyntaxError( + "You cannot have a last attribute on elements", + element=el) + break + return cls(classes, actions, theme, match, suppress_standard, source_location) def apply(self, content_doc, theme_doc, resource_fetcher, log): """ @@ -383,11 +403,15 @@ Applies this action to the theme_doc. """ if self.content_href: - ## FIXME: check response type - ## FIXME: Join content_href with request url? - content_resp = resource_fetcher(self.content_href) + ## FIXME: Is this a weird way to resolve the href? + href = urlparse.urljoin(log.request.url, self.content_href) + content_resp = resource_fetcher(href) log.debug(self, 'Fetching resource from href="%s": %s', - self.content_href, content_resp.status) + href, content_resp.status) + if content_resp.status_int != 200: + log.warn(self, 'Resource %s returned the status %s; skipping rule', + href, content_resp.status) + return content_doc = document_fromstring(content_resp.body, base_url=self.content_href) if not self.if_content_matches(content_doc, log): return Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py Wed Jul 2 02:57:25 2008 @@ -1,6 +1,7 @@ from deliverance.exceptions import AbortTheme, DeliveranceSyntaxError from deliverance.pagematch import run_matches, Match from deliverance.rules import Rule, remove_content_attribs +from deliverance.themeref import Theme from lxml.html import tostring, document_fromstring from lxml.etree import XML import re @@ -26,6 +27,8 @@ classes = run_matches(self.matchers, req, response_headers, log) except AbortTheme: return resp + if 'X-Deliverance-Page-Class' in resp.headers: + classes.extend(resp.headers['X-Deliverance-Page-Class'].strip().split()) if not classes: classes = ['default'] rules = [] @@ -42,10 +45,16 @@ theme = self.default_theme ## FIXME: error if not theme still assert theme is not None - theme_doc = self.get_theme(theme, resource_fetcher, log) + theme_href = theme.resolve_href(req, resp) + theme_doc = self.get_theme(theme_href, resource_fetcher, log) content_doc = self.parse_document(resp.body, req.url) run_standard = True for rule in rules: + if rule.match is not None: + matches = rule.match(req, response_headers, log) + if not matches: + log.debug(rule, "Skipping ") + continue rule.apply(content_doc, theme_doc, resource_fetcher, log) if rule.suppress_standard: run_standard = False @@ -92,7 +101,7 @@ rules.append(rule) elif el.tag == 'theme': ## FIXME: Add parse error - default_theme = el.get('href') + default_theme = Theme.parse_xml(el, source_location) else: ## FIXME: source location? raise DeliveranceSyntaxError( @@ -102,8 +111,8 @@ for rule in rules: for class_name in rule.classes: rules_by_class.setdefault(class_name, []).append(rule) - if default_theme: - default_theme = urlparse.urljoin(doc.base, default_theme) + if default_theme and default_theme.href: + default_theme.href = urlparse.urljoin(doc.base, default_theme.href) return cls(matchers, rules_by_class, default_theme=default_theme, source_location=source_location) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/index.html ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/index.html (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/index.html Wed Jul 2 02:57:25 2008 @@ -5,6 +5,20 @@ -This is my awesome site! +

+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Proin ullamcorper sem at orci. Nulla facilisi. Aenean bibendum suscipit diam. Maecenas vel pede vitae lorem tempor sollicitudin. Quisque pretium sapien eget dolor. In metus sem, facilisis in, laoreet in, porta luctus, odio. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam viverra felis quis mauris. Phasellus vel quam sed arcu ullamcorper bibendum. Phasellus posuere tellus pellentesque ligula. Suspendisse volutpat pede rhoncus nisi. Vivamus interdum odio eu odio. Nulla felis sapien, pretium sed, facilisis quis, fringilla et, leo. Suspendisse potenti. Praesent quis tellus. Aliquam cursus, ipsum in sollicitudin commodo, dui lacus varius ipsum, mattis fermentum sapien nisl sit amet mauris. Nullam molestie. In commodo viverra nulla. Curabitur vitae velit id erat pellentesque consectetuer. Nulla quis quam. + +

+Maecenas venenatis, lectus a luctus porttitor, ipsum pede pretium purus, et vehicula elit lacus a est. Proin ipsum magna, commodo sed, adipiscing et, imperdiet at, orci. Morbi molestie nisi in dui. Donec ultricies dui nec leo gravida lobortis. Donec lobortis dolor nec urna vulputate adipiscing. Vestibulum facilisis commodo est. Donec ornare, odio a rhoncus ornare, purus purus pellentesque dui, vitae iaculis dolor justo ultrices mauris. Donec eget libero. Ut tellus. Donec eget justo. In tempor augue sit amet ante. Donec vitae urna at eros vestibulum faucibus. + +

+Praesent quam. Curabitur nec turpis. Donec dictum. Proin pellentesque, diam luctus convallis fermentum, mi magna consectetuer sapien, quis semper arcu nunc eget arcu. Nunc suscipit semper nunc. Sed felis metus, adipiscing a, vulputate nec, dapibus vitae, diam. Sed tristique nisi in tellus. Ut odio ipsum, ultricies placerat, vehicula nec, tempor non, mi. Vestibulum tortor. Mauris mollis augue ut nunc. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Mauris lacus. Donec ac odio ac mi euismod feugiat. Duis mauris velit, porttitor aliquam, faucibus ac, hendrerit sit amet, orci. Pellentesque ultricies pede sodales metus. Vestibulum mauris. Phasellus non leo. In eu magna eu sapien molestie porttitor. Nunc malesuada bibendum ligula. Etiam sagittis tristique neque. + +

+Maecenas velit eros, accumsan porta, sodales in, ultrices auctor, felis. In metus nisi, mattis vel, bibendum id, placerat vel, elit. Vestibulum vestibulum. Sed auctor. Fusce lobortis. Vivamus porta cursus dui. Etiam neque tortor, egestas sed, ornare in, gravida eu, urna. Vestibulum a pede. In viverra sodales urna. Aliquam tempus molestie massa. Nullam congue viverra felis. + +

+Nunc lacus odio, lacinia a, mattis quis, aliquam in, lectus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed vel odio. Suspendisse ut est eu mauris auctor interdum. Sed et leo vel turpis condimentum elementum. Morbi neque mauris, sagittis eu, posuere id, molestie et, odio. Curabitur tortor. Curabitur eu pede vitae ipsum consequat porttitor. Nam at massa sed nulla lacinia sollicitudin. Donec dapibus. Duis felis justo, tincidunt sed, aliquet sit amet, rhoncus ut, neque. Duis id turpis at sem tincidunt adipiscing. Sed ante pede, facilisis a, sollicitudin sit amet, malesuada id, erat. Cras non purus. + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml Wed Jul 2 02:57:25 2008 @@ -3,5 +3,6 @@ + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css Wed Jul 2 02:57:25 2008 @@ -1,5 +1,7 @@ body { font-family: sans-serif; + padding: 0; + margin: 0; } #footer { @@ -7,3 +9,28 @@ font-size: 70%; margin-top: 1em; } + +table#table-layout { + border-spacing: 0; +} + +table#table-layout td { + vertical-align: top; +} + +td#sidebar { + background-color: #bfb; + border-right: 2px solid #0f0; + width: 8em; + padding-top: 1em; +} + +td#content { + border-top: 2px solid #0f0; + padding: 0.5em; +} + +h1#header { + background-color: #bfb; + margin-bottom: 0; +} Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html Wed Jul 2 02:57:25 2008 @@ -6,8 +6,17 @@

A Page

- -
Some content
+ + + + + + +
+ Some content +
From ianb at codespeak.net Wed Jul 2 03:10:52 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Wed, 2 Jul 2008 03:10:52 +0200 (CEST) Subject: [z3-checkins] r56224 - z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance Message-ID: <20080702011052.B224C2A018D@codespeak.net> Author: ianb Date: Wed Jul 2 03:10:52 2008 New Revision: 56224 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py Log: But in page previews, and fix up the anchors so that the base href doesn't effect them. Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py Wed Jul 2 03:10:52 2008 @@ -10,7 +10,8 @@ from pygments.formatters import HtmlFormatter from tempita import HTMLTemplate, html_quote, html from lxml.etree import _Element -from lxml.html import fromstring, document_fromstring, tostring +from lxml.html import fromstring, document_fromstring, tostring, Element +import posixpath class DeliveranceMiddleware(object): @@ -75,7 +76,6 @@ url, subresp.status, subresp.content_type) return subresp - def link_to(self, req, url, source=False, line=None, selector=None): base = req.environ['deliverance.base_url'] base += '/.deliverance/view' @@ -126,6 +126,9 @@ else: from deliverance.selector import Selector doc = document_fromstring(subresp.body) + el = Element('base') + el.set('href', posixpath.dirname(url) + '/') + doc.head.insert(0, el) selector = Selector.parse(selector) type, elements, attributes = selector(doc) if not elements: @@ -167,6 +170,7 @@ def format_tag(tag): return highlight(tostring(tag).split('>')[0]+'>') text = template.substitute( + base_url=req.url, els_in_head=els_in_head, doc=doc, elements=all_elements, selector=selector, format_tag=format_tag, highlight=highlight) @@ -194,7 +198,7 @@ {{if len(elements) == 1}} One element matched the selector {{selector}}; {{if elements[0][0]}} - jump to element + jump to element {{else}} element is in head: {{highlight(elements[0][1])}} {{endif}} @@ -203,7 +207,7 @@
    {{for anchor, el in elements}} {{if anchor}} -
  1. {{format_tag(el)}}
  2. +
  3. {{format_tag(el)}}
  4. {{else}}
  5. {{format_tag(el)}}
  6. {{endif}} From ianb at codespeak.net Wed Jul 2 19:30:03 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Wed, 2 Jul 2008 19:30:03 +0200 (CEST) Subject: [z3-checkins] r56244 - in z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance: . tests tests/example-files util Message-ID: <20080702173003.106962A0192@codespeak.net> Author: ianb Date: Wed Jul 2 19:30:02 2008 New Revision: 56244 Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/sidebar.html (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/importstring.py (contents, props changed) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/exceptions.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt Log: Factored out the python references. Made an abstract superclass for matching (separate from the Match/ object). Added python references to matching in addition to theming. Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/exceptions.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/exceptions.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/exceptions.py Wed Jul 2 19:30:02 2008 @@ -8,10 +8,11 @@ attached to it. Elements are the objects (maybe XML, or maybe not) that is applicable. """ - def __init__(self, msg=None, request=None, element=None): + def __init__(self, msg=None, request=None, element=None, source_location=None): Exception.__init__(self, msg) self.request = request self.element = element + self.source_location = source_location class DeliveranceSyntaxError(DeliveranceError): """ Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py Wed Jul 2 19:30:02 2008 @@ -5,64 +5,65 @@ from deliverance.exceptions import DeliveranceSyntaxError, AbortTheme from deliverance.stringmatch import compile_matcher, compile_header_matcher from deliverance.util.converters import asbool, html_quote +from deliverance.pyref import PyReference, PyArgs __all__ = ['MatchSyntaxError', 'Match'] -class Match(object): +class AbstractMatch(object): """ Represents the tags. You can call this object to apply the match """ - def __init__(self, classes, path=None, domain=None, + def __init__(self, path=None, domain=None, request_header=None, response_header=None, environ=None, - abort=False, last=False, source_location=None): - self.classes = classes + pyref=None, pyargs=None, + source_location=None): self.path = path self.domain = domain self.request_header = request_header self.response_header = response_header self.environ = environ - self.abort = abort - self.last = last + self.pyref = pyref + self.pyargs = pyargs self.source_location = source_location + @classmethod - def parse_xml(cls, el, source_location): + def parse_match_xml(cls, el, source_location): """ - Creates an instance of Match from the given parsed XML element. + Parses out the match-related arguments """ - assert (el.tag == 'match' - or el.tag == 'rule') - classes = el.get('class', '').split() - abort = asbool(el.get('abort')) - if not abort and not classes: - ## FIXME: source location - raise DeliveranceSyntaxError( - "You must provide some classes in the class attribute") - if abort and classes: - ## FIXME: source location - raise DeliveranceSyntaxError( - 'You cannot provide both abort="1" and class="%s"' - % (' '.join(classes))) path = cls._parse_attr(el, 'path', default='path') domain = cls._parse_attr(el, 'domain', default='wildcard') request_header = cls._parse_attr(el, 'request-header', default='exact', header=True) response_header = cls._parse_attr(el, 'response-header', default='exact', header=True) environ = cls._parse_attr(el, 'environ', default='exact', header=True) - last = asbool(el.get('last')) - return cls( - classes, + pyref = el.get('pyref') + if pyref: + pyref = PyReference.parse(pyref, source_location=source_location, + default_function='match_request', + default_objs=dict(AbortTheme=AbortTheme)) + pyargs = PyArgs.from_attrib(el.attrib) + if pyargs and not pyref: + raise DeliveranceSyntaxError( + 'You cannot provide arguments (like %s) unless you provide a pyref attribute' + % pyargs, + element=el, source_location=source_location) + return dict( path=path, domain=domain, request_header=request_header, response_header=response_header, environ=environ, - abort=abort, - last=last, + pyref=pyref, + pyargs=pyargs, source_location=source_location) + match_attrs = [ + 'path', 'domain', 'request-header', 'response-header', 'environ', 'pyref'] + @staticmethod def _parse_attr(el, attr, default=None, header=False): """ @@ -77,28 +78,39 @@ return compile_matcher(value, default) def __unicode__(self): - parts = [u'') return ' '.join(parts) + def _uni_early_args(self): + return [] + + def _uni_late_args(self): + return [] + def __str__(self): return unicode(self).encode('utf8') - def __call__(self, request, response_headers, log): + def debug_description(self): + raise NotImplementedError + + def log_context(self): + return self + + def __call__(self, request, resp, response_headers, log): """ Checks this match against the given request and response_headers object. @@ -106,51 +118,105 @@ :class:webob.Request object. """ result = True - if self.abort: - class_name = 'abort' - elif len(self.classes) > 1: - class_name = '(%s)' % ' '.join(self.classes) - else: - class_name = self.classes[0] + debug_name = self.debug_description() + debug_context = self.log_context() if self.path: if not self.path(request.path): - log.debug(self, 'Skipping class %s because request URL (%s) does not match path="%s"', - class_name, request.path, self.path) + log.debug(debug_context, 'Skipping %s because request URL (%s) does not match path="%s"', + debug_name, request.path, self.path) return False if self.domain: host = request.host.split(':', 1)[0] if not self.domain(host): - log.debug(self, 'Skipping class %s because request domain (%s) does not match domain="%s"', - class_name, host, self.domain) + log.debug(debug_context, 'Skipping %s because request domain (%s) does not match domain="%s"', + debug_name, host, self.domain) return False if self.request_header: result, headers = self.request_header(request.headers) if not result: - log.debug(self, 'Skipping class %s because request headers %s do not match request-header="%s"', - class_name, ', '.join(headers), self.request_header) + log.debug(debug_context, 'Skipping %s because request headers %s do not match request-header="%s"', + debug_name, ', '.join(headers), self.request_header) return False if self.response_header: result, headers = self.response_header(response_headers) if not result: ## FIXME: maybe distinguish headers and real headers? - log.debug(self, 'Skipping class %s because the response headers %s do not match response-header="%s"', - class_name, ', '.join(headers), self.response_header) + log.debug(debug_context, 'Skipping %s because the response headers %s do not match response-header="%s"', + debug_name, ', '.join(headers), self.response_header) return False if self.environ: result, keys = self.environ(request.environ) if not result: - log.debug(self, 'Skipping class %s because the request environ (keys %s) did not match environ="%s"', - class_name, ', '.join(keys), self.environ) + log.debug(debug_context, 'Skipping %s because the request environ (keys %s) did not match environ="%s"', + debug_name, ', '.join(keys), self.environ) + return False + if self.pyref: + result = self.pyref(request, resp, response_headers, log, **self.pyargs.dict) + if not result: + log.debug(debug_context, 'Skipping %s because the pyref="%s" returned false', + debug_name, self.pyref) return False return True -def run_matches(matchers, request, response_headers, log): +class Match(AbstractMatch): + + element_name = 'match' + + def __init__(self, classes=None, abort=False, last=False, **kw): + super(Match, self).__init__(**kw) + self.classes = classes + self.abort = abort + self.last = last + + @classmethod + def parse_xml(cls, el, source_location): + """ + Parses the element into a match object + """ + matchargs = cls.parse_match_xml(el, source_location) + assert el.tag == cls.element_name + classes = el.get('class', '').split() + abort = asbool(el.get('abort')) + if not abort and not classes: + ## FIXME: source location + raise DeliveranceSyntaxError( + "You must provide some classes in the class attribute") + if abort and classes: + ## FIXME: source location + raise DeliveranceSyntaxError( + 'You cannot provide both abort="1" and class="%s"' + % (' '.join(classes))) + last = asbool(el.get('last')) + return cls( + classes=classes, abort=abort, last=last, **matchargs) + + def _uni_early_args(self): + if self.classes: + return [u'class="%s"' % html_quote(' '.join(self.classes))] + else: + return [] + + def _uni_late_args(self): + parts = [] + if self.abort: + parts.append(u'abort="1"') + if self.last: + parts.append(u'last="1"') + return parts + + def debug_description(self): + if self.abort: + return 'abort' + else: + return 'class="%s"' % ' '.join(self.classes) + +def run_matches(matchers, request, resp, response_headers, log): """ Runs all the match objects in matchers, returning the list of matched classes. """ results = [] for matcher in matchers: - if matcher(request, response_headers, log): + if matcher(request, resp, response_headers, log): if matcher.abort: log.debug(matcher, ' matched request, aborting') raise AbortTheme(' matched request, aborting') Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py Wed Jul 2 19:30:02 2008 @@ -0,0 +1,181 @@ +""" +Handles loading modules or files for use with the ``pyfunc`` attribute +in , or other places with python hooks +""" +from string import Template +import os +import new +from UserDict import DictMixin +from deliverance.exceptions import DeliveranceSyntaxError +from deliverance.util.importstring import simple_import + +__all__ = ['PyReference', 'PyArgs'] + +class DefaultDict(DictMixin): + """ + A dictionary where all values have a default + """ + def __init__(self, wrapping, default=''): + self.wrapping = wrapping + self.default = default + def __getitem__(self, key): + return self.wrapping.get(key, self.default) + def __setitem__(self, key, value): + self.wrapping[key] = value + def __delitem__(self, key): + if key in self.wrapping: + del self.wrapping[key] + def keys(self): + return self.wrapping.keys() + def __contains__(self, key): + return True + +class PyReference(object): + """ + Represents a reference to a Python function that can be called + """ + + def __init__(self, module_name=None, filename=None, function_name=None, default_objs={}, source_location=None): + self.module_name = module_name + self.filename = filename + self.function_name = function_name + self.default_objs = default_objs + self.source_location = source_location + self._modules = {} + + @classmethod + def parse(cls, s, source_location, default_function=None, default_objs={}): + s = s.strip() + module = filename = None + if s.startswith('file:'): + filename = s[len('file:'):] + if ':' in filename: + filename, func = filename.split(':', 1) + else: + func = default_function + else: + # A module name + if ':' in s: + module, func = s.split(':', 1) + else: + module = s + if func is None: + raise DeliveranceSyntaxError( + "You must provide a function name", + element=s, source_location=source_location) + if filename: + full_file = cls.expand_filename(filename, source_location) + if not os.path.exists(full_file): + if full_file != filename: + raise DeliveranceSyntaxError( + "The filename %r (expanded from %r) does not exist" + % (full_file, filename), + element=s, source_location=source_location) + else: + raise DeliveranceSyntaxError( + "The filename %r does not exist" % full_file, + element=s, source_location=source_location) + return cls(module_name=module, file=filename, function=function, source_location=source_location, + default_objs=default_objs) + + def __repr__(self): + args = [repr(str(self))] + if self.default_objs: + args.append('default_objs=%r' % self.default_objs) + if self.source_location: + args.append('source_location=%r' % self.source_location) + return '%s.parse(%s)' % ( + self.__class__.__name__, ', '.join(args)) + + def __unicode__(self): + if self.file: + return 'file:%s:%s' % (self.file, self.function) + else: + return '%s:%s' % (self.module, self.function) + + def __str__(self): + return unicode(self).encode('utf8') + + @property + def module(self): + """ + Returns the instantiated module, or a module created from the filename + """ + if module_name: + if module_name not in self._modules: + new_mod = simple_import(self.module_name) + for name, value in self.default_objs.items(): + if not hasattr(new_mod, name): + setattr(new_mod, name, value) + self._modules[module_name] = new_mod + return self._modules[module_name] + else: + filename = self.expand_filename(self.filename, self.source_location) + if filename not in self._modules: + name = self.pyfile.strip('/').strip('\\') + name = os.path.splitext(name)[0] + name = name.replace('\\', '_').replace('/', '_') + new_mod = new.module(name) + new_mod.__file__ = filename + for name, value in self.default_objs.items(): + if not hasattr(new_mod, name): + setattr(new_mod, name, value) + self._modules[filename] = new_mod + return self._modules[filename] + + @property + def function(self): + """ + Returns the function object + """ + obj = self.module + for p in self.function_name.split('.'): + ## FIXME: better error handling: + obj = getattr(obj, p) + return obj + + def __call__(self, *args, **kw): + return self.function(*args, **kw) + + @staticmethod + def expand_filename(filename, source_location=None): + """ + Expand environmental variables in a filename + """ + vars = DefautDict(os.environ) + tmpl = Template(filename) + try: + return tmpl.substitute(os.environ) + except ValueError, e: + raise DeliveranceSyntaxError( + "The filename %r contains bad $ substitutions: %s" + % (filename, e), + filename, source_location=source_location) + +class PyArgs(object): + """ + Represents pyarg-* arguments + """ + def __init__(self, dict): + self.dict = dict + + def __nonzero__(self): + return bool(self.dict) + + def __unicode__(self): + return ' '.join('pyargs-%s="%s"' % (name, value) + for name, value in sorted(self.dict.items())) + + def __str__(self): + return unicode(self).encode('utf8') + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.dict) + + @classmethod + def from_attrib(cls, attrib): + kw = {} + for name in attrib: + if name.startswith('pyarg-'): + kw[name[len('pyarg-'):]] = value + return cls(kw) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Wed Jul 2 19:30:02 2008 @@ -6,7 +6,7 @@ from deliverance.exceptions import add_exception_info, DeliveranceSyntaxError from deliverance.util.converters import asbool, html_quote from deliverance.selector import Selector -from deliverance.pagematch import Match +from deliverance.pagematch import AbstractMatch from deliverance.themeref import Theme from lxml import etree from lxml.html import document_fromstring @@ -29,9 +29,6 @@ self.suppress_standard = suppress_standard self.source_location = source_location - match_attrs = set([ - 'path', 'domain', 'request-header', 'response-header', 'environ']) - @classmethod def parse_xml(cls, el, source_location): """ @@ -54,18 +51,10 @@ action = parse_action(el, source_location) actions.append(action) match = None - for attr in cls.match_attrs: + for attr in RuleMatch.match_attrs: if el.get(attr): - match = Match.parse_xml(el, source_location) - if match.abort: - raise DeliveranceSyntaxError( - "You cannot have an abort attribute on elements", - element=el) - if match.last: - ## FIXME: is last a good alternative to suppress-standard? - raise DeliveranceSyntaxError( - "You cannot have a last attribute on elements", - element=el) + match = RuleMatch.parse_xml(self, el, source_location) + ## FIXME: would last="1" be a good alternative to suppress-standard? break return cls(classes, actions, theme, match, suppress_standard, source_location) @@ -81,6 +70,25 @@ action.apply(content_doc, theme_doc, resource_fetcher, log) return theme_doc +class RuleMatch(AbstractMatch): + """ + Represents match rules in the element + """ + + element_name = 'rule' + + @classmethod + def parse_xml(cls, rule, el, source_location): + inst = cls(**cls.parse_match_xml(el, source_location)) + inst.rule = rule + return rule + + def debug_description(self): + return '' + + def log_context(self): + return self.rule + ## A dictionary mapping element names to their rule classes: _actions = {} Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py Wed Jul 2 19:30:02 2008 @@ -24,7 +24,7 @@ else: response_headers = resp.headers try: - classes = run_matches(self.matchers, req, response_headers, log) + classes = run_matches(self.matchers, req, resp, response_headers, log) except AbortTheme: return resp if 'X-Deliverance-Page-Class' in resp.headers: @@ -45,13 +45,13 @@ theme = self.default_theme ## FIXME: error if not theme still assert theme is not None - theme_href = theme.resolve_href(req, resp) + theme_href = theme.resolve_href(req, resp, log) theme_doc = self.get_theme(theme_href, resource_fetcher, log) content_doc = self.parse_document(resp.body, req.url) run_standard = True for rule in rules: if rule.match is not None: - matches = rule.match(req, response_headers, log) + matches = rule.match(req, resp, response_headers, log) if not matches: log.debug(rule, "Skipping ") continue Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/sidebar.html ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/sidebar.html Wed Jul 2 19:30:02 2008 @@ -0,0 +1,26 @@ + + + + +The sidebar + + + + +This contains links that go into the sidebar. Everything in the +following div goes into the sidebar: + + + + + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt Wed Jul 2 19:30:02 2008 @@ -2,19 +2,19 @@ >>> from deliverance.pagematch import Match >>> from lxml.etree import XML - >>> from webob import Request + >>> from webob import Request, Response >>> from webob.headerdict import HeaderDict >>> from deliverance.log import SavingLogger >>> def make(xml): ... el = XML(xml) ... return Match.parse_xml(el, source_location=None) - >>> def match(matcher, request, response_headers, show_log=True): + >>> def match(matcher, request, resp, response_headers, show_log=True): ... if isinstance(matcher, basestring): ... matcher = make(matcher) ... log = SavingLogger(None, None) ... if isinstance(response_headers, list): ... response_headers = HeaderDict(response_headers) - ... result = matcher(request, response_headers, log) + ... result = matcher(request, resp, response_headers, log) ... if show_log: ... for level, rule, message in log.messages: ... print 'log:', message @@ -35,16 +35,16 @@ Now, some matches: >>> m = make('') - >>> match(m, Request.blank('/foo'), []) + >>> match(m, Request.blank('/foo'), Response(), []) True - >>> match(m, Request.blank('/foobar'), []) - log: Skipping class a because request URL (/foobar) does not match path="path:/foo/" + >>> match(m, Request.blank('/foobar'), Response(), []) + log: Skipping class="a" because request URL (/foobar) does not match path="path:/foo/" False - >>> match(m, Request.blank('/foo/bar'), []) + >>> match(m, Request.blank('/foo/bar'), Response(), []) True >>> m = make('') - >>> match(m, Request.blank('/'), [('content-type', 'text/plain')]) - log: Skipping class x because the response headers Content-Type do not match response-header="Content-Type: contains:html" + >>> match(m, Request.blank('/'), Response(content_type='text/plain'), [('content-type', 'text/plain')]) + log: Skipping class="x" because the response headers Content-Type do not match response-header="Content-Type: contains:html" False - >>> match(m, Request.blank('/'), [('content-type', 'text/html')]) + >>> match(m, Request.blank('/'), Response(content_type='text/html'), [('content-type', 'text/html')]) True Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py Wed Jul 2 19:30:02 2008 @@ -0,0 +1,50 @@ +""" +Represents elements +""" +from deliverance.exceptions import DeliveranceSyntaxError, AbortTheme +from deliverance.pyref import PyReference, PyArgs +import urlparse + +class Theme(object): + """ + Represents the element + """ + + def __init__(self, href=None, pyref=None, pyargs=None, source_location=None): + self.href = href + self.pyref = pyref + self.pyargs = pyargs + self.source_location = source_location + + @classmethod + def parse_xml(cls, el, source_location): + assert el.tag == 'theme' + href = el.get('href') + pyref = el.get('pyref') + pyargs = PyArgs.from_attrib(el.attrib) + if not pyref and pyargs: + raise DeliveranceSyntaxError( + 'You cannot provide arguments (like %s) unless you provide a pyref attribute' + % pyargs, + element=el) + if pyref: + pyref = PythonReference.parse(pyref, source_location, + default_function='get_theme', + default_objs=dict(AbortTheme=AbortTheme)) + if not pyref and not href: + ## FIXME: also warn when pyref and href? + raise DeliveranceSyntaxError( + 'You must provide at least one of href, pymodule, or the pyfile attribute', + element=el) + return cls(href=href, pyref=pyref, + pyargs=pyargs, source_location=source_location) + + def resolve_href(self, req, resp, log): + if self.pyref: + href = self.pyref(req, resp, log, **self.pyargs.dict) + else: + href = self.href + ## FIXME: is this join a good idea? + if href: + href = urlparse.urljoin(req.url, href) + return href Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/importstring.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/importstring.py Wed Jul 2 19:30:02 2008 @@ -0,0 +1,95 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +'imports' a string -- converts a string to a Python object, importing +any necessary modules and evaluating the expression. Everything +before the : in an import expression is the module path; everything +after is an expression to be evaluated in the namespace of that +module. + +Alternately, if no : is present, then import the modules and get the +attributes as necessary. Arbitrary expressions are not allowed in +that case. +""" + +def eval_import(s): + """ + Import a module, or import an object from a module. + + A module name like ``foo.bar:baz()`` can be used, where + ``foo.bar`` is the module, and ``baz()`` is an expression + evaluated in the context of that module. Note this is not safe on + arbitrary strings because of the eval. + """ + if ':' not in s: + return simple_import(s) + module_name, expr = s.split(':', 1) + module = import_module(module_name) + obj = eval(expr, module.__dict__) + return obj + +def simple_import(s): + """ + Import a module, or import an object from a module. + + A name like ``foo.bar.baz`` can be a module ``foo.bar.baz`` or a + module ``foo.bar`` with an object ``baz`` in it, or a module + ``foo`` with an object ``bar`` with an attribute ``baz``. + """ + parts = s.split('.') + module = import_module(parts[0]) + name = parts[0] + parts = parts[1:] + last_import_error = None + while parts: + name += '.' + parts[0] + try: + module = import_module(name) + parts = parts[1:] + except ImportError, e: + last_import_error = e + break + obj = module + while parts: + try: + obj = getattr(module, parts[0]) + except AttributeError: + raise ImportError( + "Cannot find %s in module %r (stopped importing modules with error %s)" % (parts[0], module, last_import_error)) + parts = parts[1:] + return obj + +def import_module(s): + """ + Import a module. + """ + mod = __import__(s) + parts = s.split('.') + for part in parts[1:]: + mod = getattr(mod, part) + return mod + +def try_import_module(module_name): + """ + Imports a module, but catches import errors. Only catches errors + when that module doesn't exist; if that module itself has an + import error it will still get raised. Returns None if the module + doesn't exist. + """ + try: + return import_module(module_name) + except ImportError, e: + if not getattr(e, 'args', None): + raise + desc = e.args[0] + if not desc.startswith('No module named '): + raise + desc = desc[len('No module named '):] + # If you import foo.bar.baz, the bad import could be any + # of foo.bar.baz, bar.baz, or baz; we'll test them all: + parts = module_name.split('.') + for i in range(len(parts)): + if desc == '.'.join(parts[i:]): + return None + raise From z3-checkins at codespeak.net Fri Jul 4 18:07:22 2008 From: z3-checkins at codespeak.net (VIAGRA ® Official Site) Date: Fri, 4 Jul 2008 18:07:22 +0200 (CEST) Subject: [z3-checkins] Dear z3-checkins@codespeak.net SALE 84% 0FF on Pfizer Message-ID: <20080704070515.2968.qmail@cust-214-137.dsl.versateladsl.be> An HTML attachment was scrubbed... URL: http://codespeak.net/pipermail/z3-checkins/attachments/20080704/135b7359/attachment.htm From ianb at codespeak.net Tue Jul 8 21:40:16 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 8 Jul 2008 21:40:16 +0200 (CEST) Subject: [z3-checkins] r56372 - in z3/deliverance/sandbox/ianb/deliverance/trunk: . deliverance deliverance/tests deliverance/util docs Message-ID: <20080708194016.0F4792A805E@codespeak.net> Author: ianb Date: Tue Jul 8 21:40:14 2008 New Revision: 56372 Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxycommand.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/security.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-proxy-rule.xml z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/nesteddict.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/uritemplate.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/docs/proxy.txt (contents, props changed) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/exceptions.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/stringmatch.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py Log: Added a proxy command Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/exceptions.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/exceptions.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/exceptions.py Tue Jul 8 21:40:14 2008 @@ -25,6 +25,11 @@ aborted. """ +class AbortProxy(Exception): + """ + Raised (and caught) when a proxy rule should be ignored + """ + def add_exception_info(info, exc_info=None): """ Add the given information to the exception (typically context information) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py Tue Jul 8 21:40:14 2008 @@ -9,6 +9,7 @@ import logging from lxml.etree import tostring, _Element from tempita import HTMLTemplate, html_quote, html +from deliverance.security import display_logging NOTIFY = (logging.INFO + logging.WARN) / 2 @@ -47,7 +48,7 @@ return self.message(logging.FATAL, el, msg, *args, **kw) def finish_request(self, req, resp): - if 'deliv_log' in req.GET: + if 'deliv_log' in req.GET and display_logging(req): resp.body += self.format_html_log() resp.cache_expires() return resp @@ -56,7 +57,11 @@

    Deliverance Information

    - theme: {{log.theme_url}} + {{if log.theme_url}} + theme: {{log.theme_url}} + {{else}} + theme: no theme set + {{endif}} | unthemed content | content source
    @@ -101,6 +106,8 @@ **self.tags) def _add_notheme(self, url): + if url is None: + return None if '?' in url: url += '&' else: Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py Tue Jul 8 21:40:14 2008 @@ -2,6 +2,7 @@ from webob import exc from wsgiproxy.exactproxy import proxy_exact_request from deliverance.log import SavingLogger +from deliverance.security import display_logging, display_local_files import urllib import hmac import sha @@ -12,9 +13,12 @@ from lxml.etree import _Element from lxml.html import fromstring, document_fromstring, tostring, Element import posixpath +import mimetypes +import os class DeliveranceMiddleware(object): + ## FIXME: is log_factory etc very useful? def __init__(self, app, rule_getter, log_factory=SavingLogger, log_factory_kw={}): self.app = app self.rule_getter = rule_getter @@ -25,13 +29,18 @@ return 'Deliverance' def __call__(self, environ, start_response): - ## FIXME: copy_get?: req = Request(environ) if 'deliv_notheme' in req.GET: return self.app(environ, start_response) req.environ['deliverance.base_url'] = req.application_url + ## FIXME: copy_get?: orig_req = Request(environ.copy()) - log = self.log_factory(req, self, **self.log_factory_kw) + if 'deliverance.log' in req.environ: + log = req.environ['deliverance.log'] + else: + log = self.log_factory(req, self, **self.log_factory_kw) + ## FIXME: should this be put in both the orig_req and this req? + req.environ['deliverance.log'] = log def resource_fetcher(url): return self.get_resource(url, orig_req, log) if req.path_info_peek() == '.deliverance': @@ -49,8 +58,31 @@ def get_resource(self, url, orig_req, log): assert url is not None - ## FIXME: should this return a webob.Response object? - if url.startswith(orig_req.application_url + '/'): + if url.lower().startswith('file:'): + if not display_local_files(orig_req): + ## FIXME: not sure if this applies generally; some calls to get_resource might + ## be because of a more valid subrequest than displaying a file + return exc.HTTPForbidden( + "You cannot access file: URLs (like %r)" % url) + filename = '/' + url[len('file:'):].lstrip('/') + filename = urllib.unquote(filename) + if not os.path.exists(filename): + return exc.HTTPNotFound( + "The file %r was not found" % filename) + if os.path.isdir(filename): + return exc.HTTPForbidden( + "You cannot display a directory (%r)" % filename) + subresp = Response() + type, encoding = mimetypes.guess_type(filename) + if not type: + type = 'application/octet-stream' + subresp.content_type = type + ## FIXME: reading the whole thing obviously ain't great: + f = open(filename, 'rb') + subresp.body = f.read() + f.close() + return subresp + elif url.startswith(orig_req.application_url + '/'): subreq = orig_req.copy_get() subreq.environ['deliverance.subrequest_original_environ'] = orig_req.environ new_path_info = url[len(orig_req.application_url):] @@ -95,6 +127,9 @@ return url def internal_app(self, req, resource_fetcher): + if not display_logging(req): + return exc.HTTPForbidden( + "Logging is not enabled for you") segment = req.path_info_peek() method = 'action_%s' % segment method = getattr(self, method, None) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py Tue Jul 8 21:40:14 2008 @@ -5,7 +5,8 @@ from deliverance.exceptions import DeliveranceSyntaxError, AbortTheme from deliverance.stringmatch import compile_matcher, compile_header_matcher from deliverance.util.converters import asbool, html_quote -from deliverance.pyref import PyReference, PyArgs +from deliverance.pyref import PyReference +from deliverance.security import execute_pyref __all__ = ['MatchSyntaxError', 'Match'] @@ -18,15 +19,13 @@ def __init__(self, path=None, domain=None, request_header=None, response_header=None, environ=None, - pyref=None, pyargs=None, - source_location=None): + pyref=None, source_location=None): self.path = path self.domain = domain self.request_header = request_header self.response_header = response_header self.environ = environ self.pyref = pyref - self.pyargs = pyargs self.source_location = source_location @@ -40,17 +39,10 @@ request_header = cls._parse_attr(el, 'request-header', default='exact', header=True) response_header = cls._parse_attr(el, 'response-header', default='exact', header=True) environ = cls._parse_attr(el, 'environ', default='exact', header=True) - pyref = el.get('pyref') - if pyref: - pyref = PyReference.parse(pyref, source_location=source_location, - default_function='match_request', - default_objs=dict(AbortTheme=AbortTheme)) - pyargs = PyArgs.from_attrib(el.attrib) - if pyargs and not pyref: - raise DeliveranceSyntaxError( - 'You cannot provide arguments (like %s) unless you provide a pyref attribute' - % pyargs, - element=el, source_location=source_location) + pyref = PyReference.parse_xml( + el, source_location=source_location, + default_function='match_request', + default_objs=dict(AbortTheme=AbortTheme)) return dict( path=path, domain=domain, @@ -58,7 +50,6 @@ response_header=response_header, environ=environ, pyref=pyref, - pyargs=pyargs, source_location=source_location) match_attrs = [ @@ -85,12 +76,11 @@ ('domain', self.domain), ('request-header', self.request_header), ('response-header', self.response_header), - ('environ', self.environ), - ('pyref', self.pyref)]: + ('environ', self.environ)]: if value: parts.append(u'%s="%s"' % (attr, html_quote(unicode(self.path)))) - if self.pyargs: - parts.append(unicode(self.pyargs)) + if self.pyref: + parts.append(unicode(self.pyref)) parts.extend(self._uni_late_args()) parts.append(u'/>') return ' '.join(parts) @@ -151,12 +141,20 @@ debug_name, ', '.join(keys), self.environ) return False if self.pyref: - result = self.pyref(request, resp, response_headers, log, **self.pyargs.dict) - if not result: - log.debug(debug_context, 'Skipping %s because the pyref="%s" returned false', - debug_name, self.pyref) - return False - return True + if not execute_pyref(request): + log.error( + self, "Security disallows executing pyref %s") + else: + result = self.pyref(request, resp, response_headers, log) + if not result: + log.debug(debug_context, 'Skipping %s because the reference <%s> returned false', + debug_name, self.pyref) + return False + if isinstance(result, basestring): + result = result.split() + if isinstance(result, (list, tuple)): + return self.classes + list(result) + return getattr(self, 'classes', None) or True class Match(AbstractMatch): @@ -216,13 +214,14 @@ """ results = [] for matcher in matchers: - if matcher(request, resp, response_headers, log): + classes = matcher(request, resp, response_headers, log) + if classes: if matcher.abort: log.debug(matcher, ' matched request, aborting') raise AbortTheme(' matched request, aborting') log.debug(matcher, ' matched request, adding classes %s', - ', '.join(matcher.classes)) - for item in matcher.classes: + ', '.join(classes)) + for item in classes: if item not in results: results.append(item) if matcher.last: Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py Tue Jul 8 21:40:14 2008 @@ -0,0 +1,567 @@ +from deliverance.exceptions import DeliveranceSyntaxError, AbortProxy +from deliverance.pagematch import AbstractMatch +from deliverance.util.converters import asbool +from deliverance.middleware import DeliveranceMiddleware +from deliverance.ruleset import RuleSet +from deliverance.log import SavingLogger +from deliverance.util.uritemplate import uri_template_substitute +from deliverance.util.nesteddict import NestedDict +from deliverance.security import execute_pyref +from lxml.etree import tostring as xml_tostring +from lxml.html import document_fromstring, tostring +from pyref import PyReference +from webob import Request +from webob import exc +import urlparse +from wsgiproxy.exactproxy import proxy_exact_request +import re +import socket +from lxml.etree import Comment +from tempita import html_quote +import os +import string +from paste.fileapp import FileApp +import urllib +import posixpath + +class ProxySet(object): + + def __init__(self, proxies, ruleset, source_location=None): + self.proxies = proxies + self.ruleset = ruleset + self.source_location = source_location + self.deliverator = DeliveranceMiddleware(self.proxy_app, self.rule_getter) + + @classmethod + def parse_xml(cls, el, source_location): + proxies = [] + for child in el: + if child.tag == 'proxy': + proxies.append(Proxy.parse_xml(child, source_location)) + ruleset = RuleSet.parse_xml(el, source_location) + return cls(proxies, ruleset, source_location) + + def proxy_app(self, environ, start_response): + request = Request(environ) + log = environ['deliverance.log'] + for proxy in self.proxies: + ## FIXME: obviously this is wonky: + if proxy.match(request, None, None, log): + try: + return proxy.forward_request(environ, start_response) + except AbortProxy, e: + log.debug( + self, ' aborted (%s), trying next proxy' % e) + continue + ## FIXME: should also allow for AbortTheme? + log.error(self, 'No proxy matched the request; aborting with a 404 Not Found error') + ## FIXME: better error handling would be nice: + resp = exc.HTTPNotFound() + return resp(environ, start_response) + + def rule_getter(self, get_resource, app, orig_req): + return self.ruleset + + def application(self, environ, start_response): + req = Request(environ) + log = SavingLogger(req, self.deliverator) + req.environ['deliverance.log'] = log + return self.deliverator(environ, start_response) + +class Proxy(object): + + def __init__(self, match, dest, + request_modifications, response_modifications, + strip_script_name=False, keep_host=False, + source_location=None): + self.match = match + self.match.proxy = self + self.dest = dest + self.strip_script_name = strip_script_name + self.keep_host = keep_host + self.request_modifications = request_modifications + self.response_modifications = response_modifications + self.source_location = source_location + + def log_description(self, log=None): + parts = [] + if log is None: + parts.append('<proxy') + else: + parts.append('<proxy' % log.link_to(self.source_location, source=True)) + if self.strip_script_name: + parts.append('strip-script-name="1"') + if self.keep_host: + parts.append('keep-host="1"') + parts.append('/>
    \n') + parts.append(' ' + self.dest.log_description(log)) + parts.append('
    \n') + if self.request_modifications: + if len(self.request_modifications) > 1: + parts.append(' %i request modifications
    \n' % len(self.request_modifications)) + else: + parts.append(' 1 request modification
    \n') + if self.response_modifications: + if len(self.response_modifications) > 1: + parts.append(' %i response modifications
    \n' % len(self.response_modifications)) + else: + parts.append(' 1 response modification
    \n') + parts.append('</proxy>') + return ' '.join(parts) + + @classmethod + def parse_xml(cls, el, source_location): + assert el.tag == 'proxy' + match = ProxyMatch.parse_xml(el, source_location) + dest = None + request_modifications = [] + response_modifications = [] + strip_script_name = False + keep_host = False + for child in el: + if child.tag == 'dest': + if dest is not None: + raise DeliveranceSyntaxError( + "You cannot have more than one tag (second tag: %s)" + % xml_tostring(child), + element=child, source_location=source_location) + dest = ProxyDest.parse_xml(child, source_location) + elif child.tag == 'transform': + if child.get('strip-script-name'): + strip_script_name = asbool(child.get('strip-script-name')) + if child.get('keep-host'): + keep_host = asbool(child.get('keep-host')) + ## FIXME: error on other attrs + elif child.tag == 'request': + request_modifications.append( + ProxyRequestModification.parse_xml(child, source_location)) + elif child.tag == 'response': + response_modifications.append( + ProxyResponseModification.parse_xml(child, source_location)) + elif child.tag is Comment: + continue + else: + raise DeliveranceSyntaxError( + "Unknown tag in : %s" % xml_tostring(child), + element=child, source_location=source_location) + return cls(match, dest, request_modifications, response_modifications, + strip_script_name=strip_script_name, keep_host=keep_host, + source_location=source_location) + + def forward_request(self, environ, start_response): + request = Request(environ) + prefix = self.match.strip_prefix() + if prefix: + if prefix.endswith('/'): + prefix = prefix[:-1] + path_info = request.path_info + if not path_info.startswith(prefix + '/'): + log.warn(self, "The match would strip the prefix %r from the request path (%r), but they do not match" + % (prefix + '/', path_info)) + else: + request.script_name = request.script_name + prefix + request.path_info = path_info[len(prefix):] + log = request.environ['deliverance.log'] + for modifier in self.request_modifications: + request = modifier.modify_request(request, log) + if self.dest.next: + raise AbortProxy + dest = self.dest(request, log) + log.debug(self, ' matched; forwarding request to %s' % dest) + response, orig_base, proxied_base, proxied_url = self.proxy_to_dest(request, dest) + for modifier in self.response_modifications: + response = modifier.modify_response(request, response, orig_base, proxied_base, proxied_url, log) + return response(environ, start_response) + + def proxy_to_dest(self, request, dest): + # Not using request.copy because I don't want to copy wsgi.input: + # FIXME: handle file: + orig_base = request.application_url + proxy_req = Request(request.environ.copy()) + scheme, netloc, path, query, fragment = urlparse.urlsplit(dest) + assert not fragment, ( + "Unexpected fragment: %r" % fragment) + if scheme == 'file': + return self.proxy_to_file(request, dest) + proxy_req.path_info = path + request.path_info + proxy_req.server_name = netloc.split(':', 1)[0] + if ':' in netloc: + proxy_req.server_port = netloc.split(':', 1)[1] + elif scheme == 'http': + proxy_req.server_port = '80' + elif scheme == 'https': + proxy_req.server_port = '443' + else: + assert 0, "bad scheme: %r (from %r)" % (scheme, dest) + if not self.keep_host: + proxy_req.host = netloc + proxied_url = '%s://%s%s' % (scheme, netloc, proxy_req.path_qs) + if query: + if proxy_req.query_string: + proxy_req.query_string += '&' + ## FIXME: add query before or after existing query? + proxy_req.query_string += query + proxy_req.headers['X-Forwarded-For'] = request.remote_addr + proxy_req.headers['X-Forwarded-Scheme'] = request.scheme + proxy_req.headers['X-Forwarded-Server'] = request.host + ## FIXME: something with path? proxy_req.headers['X-Forwarded-Path'] + if self.strip_script_name: + proxy_req.script_name = '' + try: + resp = proxy_req.get_response(proxy_exact_request) + except socket.error, e: + ## FIXME: really wsgiproxy should handle this + ## FIXME: which error? + ## 502 HTTPBadGateway, 503 HTTPServiceUnavailable, 504 HTTPGatewayTimeout? + if isinstance(e.args, tuple) and len(e.args) > 1: + error = e.args[1] + else: + error = str(e) + resp = exc.HTTPServiceUnavailable( + 'Could not proxy the request to %s:%s : %s' % (proxy_req.server_name, proxy_req.server_port, error)) + return resp, orig_base, dest, proxied_url + + def proxy_to_file(self, request, dest): + orig_base = request.application_url + ## FIXME: security restrictions here? + assert dest.startswith('file:') + filename = urllib.unquote('/' + dest[len('file:'):].lstrip('/')) + rest = posixpath.normpath(request.path_info) + proxied_url = dest.lstrip('/') + '/' + urllib.quote(rest.lstrip('/')) + ## FIXME: handle /->/index.html + filename = filename.rstrip('/') + '/' + rest.lstrip('/') + app = FileApp(filename) + # I don't really need a copied request here, because FileApp is so simple: + resp = request.get_response(app) + return resp, orig_base, dest, proxied_url + +class ProxyMatch(AbstractMatch): + + element_name = 'proxy' + + @classmethod + def parse_xml(cls, el, source_location): + ## FIXME: this should have a way of indicating what portion of the path to strip + return cls(**cls.parse_match_xml(el, source_location)) + + def debug_description(self): + return '' + + def log_context(self): + return self.proxy + + def strip_prefix(self): + if self.path: + return self.path.strip_prefix() + return None + +class ProxyDest(object): + + def __init__(self, href=None, pyref=None, next=False, source_location=None): + self.href = href + self.pyref = pyref + self.next = next + self.source_location = source_location + + @classmethod + def parse_xml(cls, el, source_location): + href = el.get('href') + pyref = PyReference.parse_xml(el, source_location, + default_function='get_proxy_dest', default_objs=dict(AbortProxy=AbortProxy)) + next = asbool(el.get('next')) + if next and (href or pyref): + raise DeliveranceSyntaxError( + 'If you have a next="1" attribute you cannot also have an href or pyref attribute', + element=el, source_location=source_location) + return cls(href, pyref, next=next, source_location=source_location) + + def __call__(self, request, log): + assert not self.next + if self.pyref: + if not execute_pyref(request): + log.error( + self, "Security disallows executing pyref %s" % self.pyref) + else: + return self.pyref(request, log) + ## FIXME: is this nesting really needed? + ## we could just use HTTP_header keys... + vars = NestedDict(request.environ, request.headers, dict(here=posixpath.dirname(self.source_location))) + return uri_template_substitute(self.href, vars) + + def log_description(self, log=None): + parts = ['<dest'] + if self.href: + if log is not None: + parts.append('href="%s"' % html_quote(html_quote(self.href))) + else: + ## FIXME: definite security issue with the link through here: + ## FIXME: Should this be source=True? + parts.append('href="%s"' % + (html_quote(log.link_to(self.href)), html_quote(html_quote(self.href)))) + if self.pyref: + parts.append('pref="%s"' % html_quote(self.pyref)) + if self.next: + parts.append('next="1"') + parts.append('/>') + return ' '.join(parts) + +class ProxyRequestModification(object): + def __init__(self, pyref=None, header=None, content=None, + source_location=None): + self.pyref = pyref + self.header = header + self.content = content + self.source_location = source_location + + @classmethod + def parse_xml(cls, el, source_location): + assert el.tag == 'request' + pyref = PyReference.parse_xml( + el, source_location, + default_function='modify_proxy_request', + default_objs=dict(AbortProxy=AbortProxy)) + header = el.get('header') + content = el.get('content') + if (not header and content) or (not content and header): + raise DeliveranceSyntaxError( + "If you provide a header attribute you must provide a content attribute, and vice versa", + element=el, source_location=source_location) + return cls(pyref, header, content, source_location) + + def modify_request(self, request, log): + if self.pyref: + if not execute_pyref(request): + log.error( + self, "Security disallows executing pyref %s" % self.pyref) + else: + result = self.pyref(request, log) + if isinstance(result, dict): + request = Request(result) + elif isinstance(result, Request): + request = result + if self.header: + request.headers[self.header] = self.content + return request + +class ProxyResponseModification(object): + def __init__(self, pyref=None, header=None, content=None, rewrite_links=False, + source_location=None): + self.pyref = pyref + self.header = header + self.content = content + self.rewrite_links = rewrite_links + + @classmethod + def parse_xml(cls, el, source_location): + assert el.tag == 'response' + pyref = PyReference.parse_xml( + el, source_location, + default_function='modify_proxy_response', + default_objs=dict(AbortProxy=AbortProxy)) + header = el.get('header') + content = el.get('content') + if (not header and content) or (not content and header): + raise DeliveranceSyntaxError( + "If you provide a header attribute you must provide a content attribute, and vice versa", + element=el, source_location=source_location) + rewrite_links = asbool(el.get('rewrite-links')) + return cls(pyref=pyref, header=header, content=content, rewrite_links=rewrite_links, + source_location=source_location) + + _cookie_domain_re = re.compile(r'(domain="?)([a-z0-9._-]*)("?)', re.I) + + ## FIXME: instead of proxied_base/proxied_path, should I keep the modified request object? + def modify_response(self, request, response, orig_base, proxied_base, proxied_url, log): + if not proxied_base.endswith('/'): + proxied_base += '/' + if not orig_base.endswith('/'): + orig_base += '/' + assert proxied_url.startswith(proxied_base), ( + "Unexpected proxied_url %r, doesn't start with proxied_base %r" + % (proxied_url, proxied_base)) + assert request.url.startswith(orig_base), ( + "Unexpected request.url %r, doesn't start with orig_base %r" + % (request.url, orig_base)) + if self.pyref: + if not execute_pyref(request): + log.error( + self, "Security disallows executing pyref %s" % self.pyref) + else: + result = self.pyref(request, response, orig_base, proxied_base, proxied_url, log) + if isinstance(result, Response): + response = result + if self.header: + response.headers[self.header] = self.content + if self.rewrite_links: + if response.content_type != 'text/html': + log.debug(self, 'Not rewriting links in response from %s, because Content-Type is %s' % (proxied_url, response.content_type)) + return response + body_doc = document_fromstring(response.body, base_url=proxied_url) + body_doc.make_links_absolute() + def link_repl_func(link): + if not link.startswith(proxied_base): + # External link, so we don't rewrite it + return link + new = orig_base + link[len(proxied_base):] + return new + body_doc.rewrite_links(link_repl_func) + response.body = tostring(body_doc) + if response.location: + ## FIXME: if you give a proxy like http://openplans.org, and it redirects to + ## http://www.openplans.org, it won't be rewritten and that can be confusing + ## -- it *shouldn't* be rewritten, but some better log message is required + loc = urlparse.urljoin(proxied_url, response.location) + loc = link_repl_func(loc) + response.location = loc + if response.headers.get('set-cookie'): + cook = response.headers['set-cookie'] + old_domain = urlparse.urlsplit(proxied_url)[1].lower() + new_domain = req.host.split(':', 1)[0].lower() + def rewrite_domain(match): + domain = match.group(2) + if domain == old_domain: + ## FIXME: doesn't catch wildcards and the sort + return match.group(1) + new_domain + match.group(3) + else: + return match.group(0) + cook = self._cookie_domain_re.sub(rewrite_domain, cook) + response.headers['set-cookie'] = cook + return response + +class ProxySettings(object): + """ + Represents the settings for the proxy + """ + def __init__(self, server_host, execute_pyref=True, display_local_files=True, + dev_allow_ips=None, dev_deny_ips=None, dev_htpasswd=None, dev_users=None, + dev_expiration=60, + source_location=None): + self.server_host = server_host + self.execute_pyref = execute_pyref + self.display_local_files = display_local_files + self.dev_allow_ips = dev_allow_ips + self.dev_deny_ips = dev_deny_ips + self.dev_htpasswd = dev_htpasswd + self.dev_expiration = dev_expiration + self.dev_users = dev_users + self.source_location = source_location + + @classmethod + def parse_xml(cls, el, source_location, environ=None, traverse=False): + if traverse and el.tag != 'server-settings': + try: + el = el.xpath('//server-settings')[0] + except IndexError: + raise DeliveranceSyntaxError( + "There is no element", + element=el) + if environ is None: + environ = os.environ + assert el.tag == 'server-settings' + server_host = 'localhost:8080' + ## FIXME: should these defaults be passed in: + execute_pyref = True + display_local_files = True + dev_allow_ips = [] + dev_deny_ips = [] + dev_htpasswd = None + dev_expiration = 60 + dev_users = {} + for child in el: + if child.tag is Comment: + continue + ## FIXME: should some of these be attributes? + elif child.tag == 'server': + server_host = cls.substitute(child.text, environ) + elif child.tag == 'execute-pyref': + pyref = asbool(cls.substitute(child.text, environ)) + elif child.tag == 'dev-allow': + dev_allow_ips.extend(cls.substitute(child.text, environ).split()) + elif child.tag == 'dev-deny': + dev_deny_ips.extend(cls.substitute(child.text, environ).split()) + elif child.tag == 'dev-htpasswd': + dev_htpasswd = cls.substitute(child.text, environ) + elif child.tag == 'dev-expiration': + dev_expiration = cls.substitute(child.text, environ) + if dev_expiration: + dev_expiration = int(dev_expiration) + elif child.tag == 'display-local-files': + display_local_files = asbool(cls.substitute(child.text, environ)) + elif child.tag == 'dev-user': + username = cls.substitute(child.get('username', ''), environ) + ## FIXME: allow hashed password? + password = cls.substitute(child.get('password', ''), environ) + if not username or not password: + raise DeliveranceSyntaxError( + " must have both a username and password attribute", + element=child) + if username in dev_users: + raise DeliveranceSyntaxError( + ' appears more than once' % username, + element=el) + dev_users[username] = password + else: + raise DeliveranceSyntaxError( + 'Unknown element in : <%s>' % child.tag, + element=child) + if dev_users and dev_htpasswd: + raise DeliveranceSyntaxError( + "You can use or , but not both", + element=el) + ## FIXME: add a default allow_ips of 127.0.0.1? + return cls(server_host, execute_pyref=execute_pyref, display_local_files=display_local_files, + dev_allow_ips=dev_allow_ips, dev_deny_ips=dev_deny_ips, + dev_users=dev_users, dev_expiration=dev_expiration, + source_location=source_location) + + @property + def host(self): + return self.server_host.split(':', 1)[0] + + @property + def port(self): + if ':' in self.server_host: + return int(self.server_host.split(':', 1)[1]) + else: + return 80 + + @property + def base_url(self): + host = self.host + if host == '0.0.0.0' or not host: + host = '127.0.0.1' + if self.port != 80: + host += ':%s' % self.port + return 'http://' + host + + @staticmethod + def substitute(template, environ): + if environ is None: + return template + return string.Template(template).substitute(environ) + + def middleware(self, app): + """ + Wrap the given application in an appropriate DevAuth and Security instance + """ + from devauth import DevAuth, convert_ip_mask + from deliverance.security import SecurityContext + if self.dev_users: + password_checker = self.check_password + else: + password_checker = None + app = SecurityContext.middleware(app, execute_pyref=self.execute_pyref, + display_local_files=self.display_local_files) + app = DevAuth( + app, + allow=convert_ip_mask(self.dev_allow_ips), + deny=convert_ip_mask(self.dev_deny_ips), + password_file=self.dev_htpasswd, + password_checker=password_checker, + expiration=self.dev_expiration, + login_mountpoint='/.deliverance') + return app + + def check_password(self, username, password): + assert self.dev_users + return self.dev_users.get(username) == password Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxycommand.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxycommand.py Tue Jul 8 21:40:14 2008 @@ -0,0 +1,65 @@ +import sys +import os +import optparse +from deliverance.proxy import ProxySet +from deliverance.proxy import ProxySettings + +from lxml.etree import parse +from paste.httpserver import serve +import urllib + +description = """\ +Starts up a proxy server using the given rule file. +""" + +parser = optparse.OptionParser( + usage='%prog [OPTIONS] RULE.xml', + ## FIXME: get from pkg_resources: + version='0.1', + description=description, + ) +## FIXME: these should be handled by the settings (or just picked up from devauth): +parser.add_option( + '--debug', + action='store_true', + dest='debug', + help='Show debugging information about unexpected exceptions in the browser') +parser.add_option( + '--interactive-debugger', + action='store_true', + dest='interactive_debugger', + help='Use an interactive debugger (note: security hole when done publically; ' + 'if interface is not explicitly given it will be set to 127.0.0.1)') + +def run_command(rule_filename, debug=False, interactive_debugger=False): + rule_url = 'file://' + urllib.quote(os.path.abspath(rule_filename).replace(os.path.sep, '/')) + el = parse(rule_filename, base_url=rule_url).getroot() + ## FIXME: rule_filename isn't browsable in the logs + ps = ProxySet.parse_xml(el, rule_url) + settings = ProxySettings.parse_xml(el, rule_url, traverse=True) + app = ps.application + app = settings.middleware(app) + if interactive_debugger: + from weberror.evalexception import EvalException + app = EvalException(app, debug=True) + else: + from weberror.errormiddleware import ErrorMiddleware + app = ErrorMiddleware(app, debug=debug) + print 'To see logging, visit %s/.deliverance/login' % settings.base_url + serve(app, host=settings.host, port=settings.port) + +def main(args=None): + if args is None: + args = sys.argv[1:] + options, args = parser.parse_args() + if not args: + parser.error('You must provide a rule file') + if len(args) > 1: + parser.error('Only one argument (the rule file) allowed') + rule_filename = args[0] + run_command(rule_filename, + interactive_debugger=options.interactive_debugger, + debug=options.debug) + +if __name__ == '__main__': + main() Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py Tue Jul 8 21:40:14 2008 @@ -3,13 +3,14 @@ in , or other places with python hooks """ from string import Template +from tempita import html_quote import os import new from UserDict import DictMixin from deliverance.exceptions import DeliveranceSyntaxError from deliverance.util.importstring import simple_import -__all__ = ['PyReference', 'PyArgs'] +__all__ = ['PyReference'] class DefaultDict(DictMixin): """ @@ -35,16 +36,31 @@ Represents a reference to a Python function that can be called """ - def __init__(self, module_name=None, filename=None, function_name=None, default_objs={}, source_location=None): + def __init__(self, module_name=None, filename=None, function_name=None, + args={}, default_objs={}, attr_name=None, source_location=None): self.module_name = module_name self.filename = filename self.function_name = function_name + self.args = args self.default_objs = default_objs + self.attr_name = attr_name self.source_location = source_location self._modules = {} @classmethod - def parse(cls, s, source_location, default_function=None, default_objs={}): + def parse_xml(cls, el, source_location, attr_name='pyref', default_function=None, default_objs={}): + s = el.get(attr_name) + args = {} + for name, value in el.attrib.items(): + if name.startswith('pyarg-'): + args[name[len('pyarg-'):]] = value + if not s: + if args: + raise DeliveranceSyntaxError( + "You provided pyargs-* attributes (%s) but no %s attribute" + % (cls._format_args(args), attr_name), + element=el, source_location=source_location) + return None s = s.strip() module = filename = None if s.startswith('file:'): @@ -75,33 +91,16 @@ raise DeliveranceSyntaxError( "The filename %r does not exist" % full_file, element=s, source_location=source_location) - return cls(module_name=module, file=filename, function=function, source_location=source_location, - default_objs=default_objs) - - def __repr__(self): - args = [repr(str(self))] - if self.default_objs: - args.append('default_objs=%r' % self.default_objs) - if self.source_location: - args.append('source_location=%r' % self.source_location) - return '%s.parse(%s)' % ( - self.__class__.__name__, ', '.join(args)) - - def __unicode__(self): - if self.file: - return 'file:%s:%s' % (self.file, self.function) - else: - return '%s:%s' % (self.module, self.function) - - def __str__(self): - return unicode(self).encode('utf8') + return cls(module_name=module, file=filename, function_name=func, args=args, + attr_name=attr_name, default_objs=default_objs, + source_location=source_location) @property def module(self): """ Returns the instantiated module, or a module created from the filename """ - if module_name: + if self.module_name: if module_name not in self._modules: new_mod = simple_import(self.module_name) for name, value in self.default_objs.items(): @@ -135,6 +134,8 @@ return obj def __call__(self, *args, **kw): + for name, value in self.args.iteritems(): + kw.setdefault(name, value) return self.function(*args, **kw) @staticmethod @@ -152,30 +153,16 @@ % (filename, e), filename, source_location=source_location) -class PyArgs(object): - """ - Represents pyarg-* arguments - """ - def __init__(self, dict): - self.dict = dict - - def __nonzero__(self): - return bool(self.dict) - def __unicode__(self): - return ' '.join('pyargs-%s="%s"' % (name, value) - for name, value in sorted(self.dict.items())) - + if self.filename: + base = 'file:%s:%s' % (self.filename, self.function_name) + else: + base = '%s:%s' % (self.module_name, self.function_name) + parts = ['%s="%s"' % (self.attr_name, html_quote(base))] + for name, value in sorted(self.args.items()): + parts.append('pyarg-%s="%s"' % (name, html_quote(value))) + return ' '.join(parts) + def __str__(self): return unicode(self).encode('utf8') - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, self.dict) - - @classmethod - def from_attrib(cls, attrib): - kw = {} - for name in attrib: - if name.startswith('pyarg-'): - kw[name[len('pyarg-'):]] = value - return cls(kw) + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py Tue Jul 8 21:40:14 2008 @@ -3,7 +3,7 @@ from deliverance.rules import Rule, remove_content_attribs from deliverance.themeref import Theme from lxml.html import tostring, document_fromstring -from lxml.etree import XML +from lxml.etree import XML, Comment import re import urlparse from webob.headerdict import HeaderDict @@ -44,10 +44,15 @@ if theme is None: theme = self.default_theme ## FIXME: error if not theme still - assert theme is not None - theme_href = theme.resolve_href(req, resp, log) - theme_doc = self.get_theme(theme_href, resource_fetcher, log) - content_doc = self.parse_document(resp.body, req.url) + if theme is None: + log.error(self, "No theme has been defined for the request") + return resp + try: + theme_href = theme.resolve_href(req, resp, log) + theme_doc = self.get_theme(theme_href, resource_fetcher, log) + content_doc = self.parse_document(resp.body, req.url) + except AbortTheme: + return resp run_standard = True for rule in rules: if rule.match is not None: @@ -71,7 +76,12 @@ log.theme_url = url ## FIXME: should do caching ## FIXME: check response status - doc = self.parse_document(resource_fetcher(url).body, url) + resp = resource_fetcher(url) + if resp.status_int != 200: + log.fatal(self, "The resource %s was not 200 OK: %s" % (url, resp.status)) + raise AbortTheme( + "The resource %s returned an error: %s" % (url, resp.status)) + doc = self.parse_document(resp.body, url) doc.make_links_absolute() return doc @@ -102,6 +112,9 @@ elif el.tag == 'theme': ## FIXME: Add parse error default_theme = Theme.parse_xml(el, source_location) + elif el.tag in ('proxy', 'server-settings', Comment): + # Handled elsewhere, so we just ignore this element + continue else: ## FIXME: source location? raise DeliveranceSyntaxError( @@ -111,8 +124,6 @@ for rule in rules: for class_name in rule.classes: rules_by_class.setdefault(class_name, []).append(rule) - if default_theme and default_theme.href: - default_theme.href = urlparse.urljoin(doc.base, default_theme.href) return cls(matchers, rules_by_class, default_theme=default_theme, source_location=source_location) Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/security.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/security.py Tue Jul 8 21:40:14 2008 @@ -0,0 +1,79 @@ +class SecurityContext(object): + """ + This represents the security context of the Deliverance request. + This is stored in ``environ['deliverance.security_context']`` and + is local to the request. + + The three primary security-related restrictions are: + + 1. Can Python be executed using pyref attributes? + 2. Can logging messages be displayed? + 3. Can local files be displayed? + + Each of these is a method that takes the request. + + When instantiating, the default value of None means that the value + should be guessed from the environment. + + This uses the `developer auth spec + `_ for + guessing when a value is None. + """ + + def __init__(self, execute_pyref=False, display_logging=None, + display_local_files=None): + self._execute_pyref = execute_pyref + self._display_logging = display_logging + self._display_local_files = display_local_files + + @classmethod + def install(cls, environ, **kw): + """ + Instantiate the context and put it into the environment + """ + inst = cls(**kw) + environ['deliverance.security_context'] = cls(**kw) + return inst + + def display_logging(self, environ): + if self._display_logging is not None: + return self._display_logging + return self.is_developer_user(environ) + + def display_local_files(self, environ): + if self._display_logging is not None: + return self._display_logging + return self.is_developer_user(environ) + + def execute_pyref(self, environ): + return self._execute_pyref + + def is_developer_user(self, environ): + if hasattr(environ, 'environ'): + # Actually a request + environ = environ.environ + return bool(environ.get('x-wsgiorg.developer_user')) + + @classmethod + def middleware(cls, app, **settings): + """ + Wrap the application with middleware that installs settings + with the given configuration values. + """ + def replacement_app(environ, start_response): + cls.install(environ, **settings) + return app(environ, start_response) + return replacement_app + +def make_getter(meth_name): + def getter(environ): + if hasattr(environ, 'environ'): + environ = environ.environ + ## FIXME: handle case when security context isn't in place? + return getattr(environ['deliverance.security_context'], meth_name)(environ) + getter.func_name = meth_name + return getter + +display_logging = make_getter('display_logging') +display_local_files = make_getter('display_local_files') +execute_pyref = make_getter('execute_pyref') Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/stringmatch.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/stringmatch.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/stringmatch.py Tue Jul 8 21:40:14 2008 @@ -72,6 +72,12 @@ name = None + def strip_prefix(self): + """ + String prefix to strip from a matched string + """ + return None + def __init__(self, pattern): self.pattern = pattern @@ -157,6 +163,9 @@ return (s == self.pattern[:-1] or s.startswith(self.pattern)) + def strip_prefix(self): + return self.pattern + _add_matcher(PathMatcher) class ExactMatcher(Matcher): Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-proxy-rule.xml ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-proxy-rule.xml Tue Jul 8 21:40:14 2008 @@ -0,0 +1,50 @@ + + + + 127.0.0.1:8080 + true + 127.0.0.1 + + + + + + + + + + + + + + + + + + + + + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py Tue Jul 8 21:40:14 2008 @@ -2,7 +2,11 @@ Represents elements """ from deliverance.exceptions import DeliveranceSyntaxError, AbortTheme -from deliverance.pyref import PyReference, PyArgs +from deliverance.pyref import PyReference +from deliverance.security import execute_pyref +from deliverance.util.uritemplate import uri_template_substitute +from deliverance.util.nesteddict import NestedDict +import posixpath import urlparse class Theme(object): @@ -10,41 +14,48 @@ Represents the element """ - def __init__(self, href=None, pyref=None, pyargs=None, source_location=None): + def __init__(self, href=None, pyref=None, source_location=None): self.href = href self.pyref = pyref - self.pyargs = pyargs self.source_location = source_location @classmethod def parse_xml(cls, el, source_location): assert el.tag == 'theme' href = el.get('href') - pyref = el.get('pyref') - pyargs = PyArgs.from_attrib(el.attrib) - if not pyref and pyargs: - raise DeliveranceSyntaxError( - 'You cannot provide arguments (like %s) unless you provide a pyref attribute' - % pyargs, - element=el) - if pyref: - pyref = PythonReference.parse(pyref, source_location, - default_function='get_theme', - default_objs=dict(AbortTheme=AbortTheme)) + pyref = PyReference.parse_xml(el, source_location, default_function='get_theme', + default_objs=dict(AbortTheme=AbortTheme)) if not pyref and not href: ## FIXME: also warn when pyref and href? raise DeliveranceSyntaxError( 'You must provide at least one of href, pymodule, or the pyfile attribute', element=el) return cls(href=href, pyref=pyref, - pyargs=pyargs, source_location=source_location) + source_location=source_location) def resolve_href(self, req, resp, log): + substitute = True if self.pyref: - href = self.pyref(req, resp, log, **self.pyargs.dict) + if not execute_pyref(req): + log.error( + self, "Security disallows executing pyref %s" % self.pyref) + ## FIXME: this isn't very good; fatal exception?: + href = self.href + else: + href = self.pyref(req, resp, log) + substitute = False else: href = self.href + if substitute: + vars = NestedDict(req.environ, req.headers, dict(here=posixpath.dirname(self.source_location))) + new_href = uri_template_substitute(href, vars) + if new_href != href: + log.debug( + self, 'Rewrote theme href="%s" to "%s"' % (href, new_href)) + href = new_href ## FIXME: is this join a good idea? + print 'resolved value is %r (from %r) for request %r (base %r)' % ( + href, self.href, req.url, self.source_location) if href: href = urlparse.urljoin(req.url, href) return href Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/nesteddict.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/nesteddict.py Tue Jul 8 21:40:14 2008 @@ -0,0 +1,16 @@ +from UserDict import DictMixin + +class NestedDict(object): + def __init__(self, *dicts): + self.dicts = dicts + def __getitem__(self, key): + for d in self.dicts: + if key in d: + return d[key] + raise KeyError(key) + def keys(self): + keys = set() + for d in self.dicts: + keys.update(d.keys()) + return list(keys) + Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/uritemplate.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/uritemplate.py Tue Jul 8 21:40:14 2008 @@ -0,0 +1,20 @@ +""" +A simple implementation of URI templates. Note: this is incomplete! + +This only implements simple {var} substitution, not any of the other +operations in the URI template (unfinished) spec. +""" +import re + +__all__ = ['uri_template_substitute'] + +_uri_var_re = re.compile(r'\{(.*?)\}') + +def uri_template_substitute(uri_template, vars): + def subber(match): + try: + return vars[match.group(1)] + except KeyError: + raise KeyError('No variable {%s} in uri_template %r' + % (match.group(1), uri_template)) + return _uri_var_re.sub(subber, uri_template) Added: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/proxy.txt ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/proxy.txt Tue Jul 8 21:40:14 2008 @@ -0,0 +1,23 @@ +Proxying can be setup inside the rules as well: + + + + + + + + + + + + +The element takes all the same attributes that does for matching (not including class, abort, last). + +The request is proxied to a location given with the element, either a location to proxy to, or a Python callback. Any Python callback can raise AbortProxy, and the proxy will be skipped (looking for later matching proxies). It can proxy to http/https and to file locations. + +The element controls how the request is transformed when it is forwarded. By default all the standard headers -- X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Scheme -- are added. The Host header is not preserved by default, but if you use ``keep-host="1"`` it will be. As a minor matter, ``environ['SCRIPT_NAME']`` is typically just ignored. You can have it stripped off, and then X-Forwarded-Path will also be set. (FIXME: check that header name) + +You can modify both the request and the response with multiple and tags. The request can set headers to literal strings, and you can modify the request arbitrarily with ``pyref``. The response can also have headers added, and arbitrary modification with ``pyref``. You can also rewrite all links with ``rewrite-links="1"``; this is typically necessary if the X-Forwarded-\* headers aren't used to construct links. + +FIXME: should there be a way to avoid theming on a section? + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py Tue Jul 8 21:40:14 2008 @@ -29,12 +29,19 @@ test_suite='nose.collector', tests_require=['nose'], install_requires=[ - "lxml", + "lxml>=2.1alpha1", "WebOb", "WSGIProxy", "Tempita", "Pygments", - ], + "WebError", + "DevAuth", + ], + dependency_links=[ + "https://svn.openplans.org/svn/DevAuth/trunk#egg=DevAuth-dev", + ], entry_points=""" + [console_scripts] + deliverance-proxy = deliverance.proxycommand:main """, ) From ianb at codespeak.net Tue Jul 8 22:25:53 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 8 Jul 2008 22:25:53 +0200 (CEST) Subject: [z3-checkins] r56373 - in z3/deliverance/sandbox/ianb/deliverance/trunk: . deliverance docs docs/_static Message-ID: <20080708202553.ABE3416A047@codespeak.net> Author: ianb Date: Tue Jul 8 22:25:51 2008 New Revision: 56373 Added: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/_static/ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/conf.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/docs/index.txt (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/docs/news.txt (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/docs/pyref.txt (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/regen-docs (contents, props changed) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py z3/deliverance/sandbox/ianb/deliverance/trunk/docs/ (props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/docs/license.txt z3/deliverance/sandbox/ianb/deliverance/trunk/docs/page-classes.txt z3/deliverance/sandbox/ianb/deliverance/trunk/docs/proxy.txt z3/deliverance/sandbox/ianb/deliverance/trunk/docs/rules.txt Log: Setup sphinx Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py Tue Jul 8 22:25:51 2008 @@ -205,7 +205,9 @@ proxy_req.headers['X-Forwarded-Scheme'] = request.scheme proxy_req.headers['X-Forwarded-Server'] = request.host ## FIXME: something with path? proxy_req.headers['X-Forwarded-Path'] + ## (now we are only doing it with strip_script_name) if self.strip_script_name: + proxy_req.headers['X-Forwarded-Path'] = proxy_req.script_name proxy_req.script_name = '' try: resp = proxy_req.get_response(proxy_exact_request) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py Tue Jul 8 22:25:51 2008 @@ -100,6 +100,7 @@ """ Returns the instantiated module, or a module created from the filename """ + ## FIXME: this should reload the module as necessary. if self.module_name: if module_name not in self._modules: new_mod = simple_import(self.module_name) Added: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/conf.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/conf.py Tue Jul 8 22:25:51 2008 @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# +# Paste documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 22 22:08:49 2008. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# All configuration values have a default value; values that are commented out +# serve to show the default value. + +import sys + +# If your extensions are in another directory, add it here. +#sys.path.append('some/directory') + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +#extensions = ['sphinx.ext.autodoc'] +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The master toctree document. +master_doc = 'index' + +# General substitutions. +project = 'Deliverance' +copyright = '2008, The Open Planning Project' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +# +# The short X.Y version. +version = '0.2' +# The full version, including alpha/beta/rc tags. +release = '0.2' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +unused_docs = [] + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +html_style = 'default.css' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Content template for the index page. +#html_index = '' + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Deliverancedoc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +#latex_documents = [] + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True Added: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/index.txt ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/index.txt Tue Jul 8 22:25:51 2008 @@ -0,0 +1,13 @@ +Deliverance +=========== + +Deliverance is a tool to theme HTML, applying a consistent style to applications and static files regardless of how they are implemented, and separating site-wide styling from application-level templating. + +.. toctree:: + + rules + proxy + pyref + page-classes + news + license Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/license.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/docs/license.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/license.txt Tue Jul 8 22:25:51 2008 @@ -1,4 +1,4 @@ -Copyright (c) 2007 +Copyright (c) 2008 The Open Planning Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Added: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/news.txt ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/news.txt Tue Jul 8 22:25:51 2008 @@ -0,0 +1,4 @@ +News +==== + +Everything is new: no news yet! Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/page-classes.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/docs/page-classes.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/page-classes.txt Tue Jul 8 22:25:51 2008 @@ -1,4 +1,7 @@ -OK, so I feel okay about a redesign of selectors and rules. So the other thing is selecting themes and whatnot. +Page Classes +============ + +**Note:** This document was written before the code, and needs to be corrected. I still like the idea of using names and indirection, ala CSS classes. I'm thinking of calling them page-class, like "page type" that Wichert suggested, but using "class" to suggest that a page isn't exclusively one thing. @@ -6,23 +9,25 @@ Setting these isn't always easy. You have to poke around in templates to do it, and for something like styling Trac it would be nice to do it completely externally to Trac itself. So back to path matching. This seems fairly obvious: +.. code-block:: xml + There will be the tag, and these selectors as attributes: - path: a path. If it allows wildcards, I am a little concerned about people mistakenly putting an exact path and assuming it is a prefix. So I'm thinking this won't take wildcards. A trailing / won't matter -- /foo/ and /foo will be treated equivalently, and neither would match /foobar. + ``path``: a path. If it allows wildcards, I am a little concerned about people mistakenly putting an exact path and assuming it is a prefix. So I'm thinking this won't take wildcards. A trailing / won't matter -- /foo/ and /foo will be treated equivalently, and neither would match /foobar. Perhaps prefixes could allow different kinds of matches. E.g.: "wildcard:*/manage*" or "regex:/wp-admin/(post|new-post)\.php". I'd like a way to indicate whether just PATH_INFO or the full path is being matched. People are naturally inclined to view it as a full-path match. Perhaps a leading / would indicate this, and no leading / means match SCRIPT_NAME. Except that wouldn't work for wildcard and regex matches. For most installations PATH_INFO will be the full path, so it's a little academic. But that's not true for openplans, so I have to think about this. - domain: a domain, with possible wildcard. + ``domain``: a domain, with possible wildcard. request-header: this would be "Header: match", like "X-Requested-With: regex:(?i)xmlhttprequest". The header name will be parsed out. Maybe allow wildcards? The part after : will be matched like path, except maybe a case-insensitive whitespace-normalized match as the default. So "X-Requested-With: xmlhttprequest" would match a value of "XMLHttpRequest", but wouldn't match "XMLHttpRequest/foo" - response-header: just like request header, but the response. + ``response-header``: just like request header, but the response. - last: if this is "1", then if this matches, no other matches will be checked. (Not sure if this is the best name... it's like [L] in a rewrite rule) + ``last``: if this is "1", then if this matches, no other matches will be checked. (Not sure if this is the best name... it's like [L] in a rewrite rule) If you provide multiple attributes then they all must match. I'm not sure how to allow multiple attributes of the same type, like if you want to match multiple request headers. @@ -31,6 +36,8 @@ OK, so then the actual rules. We introduce a single attribute: +.. code-block:: xml + @@ -39,6 +46,8 @@ Maybe better than things like -standard, would be a way of including other classes in a rule. Like: +.. code-block:: xml + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/proxy.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/docs/proxy.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/proxy.txt Tue Jul 8 22:25:51 2008 @@ -1,23 +1,27 @@ Proxying can be setup inside the rules as well: - - - - - - - - - +.. code-block:: xml + + + + + + + + + -The element takes all the same attributes that does for matching (not including class, abort, last). +The ```` element takes all the same attributes that ```` does for matching (not including ``class``, ``abort``, ``last``). -The request is proxied to a location given with the element, either a location to proxy to, or a Python callback. Any Python callback can raise AbortProxy, and the proxy will be skipped (looking for later matching proxies). It can proxy to http/https and to file locations. +The request is proxied to a location given with the ```` element, either a location to proxy to, or a Python callback. Any Python callback can raise ``AbortProxy``, and the proxy will be skipped (looking for later matching proxies). It can proxy to http/https and to file locations. You can use URI templates for destinations as well, substituting headers and environmental variables as well as ``{here}`` which is the directory location of the rule document (e.g., ````). The element controls how the request is transformed when it is forwarded. By default all the standard headers -- X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Scheme -- are added. The Host header is not preserved by default, but if you use ``keep-host="1"`` it will be. As a minor matter, ``environ['SCRIPT_NAME']`` is typically just ignored. You can have it stripped off, and then X-Forwarded-Path will also be set. (FIXME: check that header name) -You can modify both the request and the response with multiple and tags. The request can set headers to literal strings, and you can modify the request arbitrarily with ``pyref``. The response can also have headers added, and arbitrary modification with ``pyref``. You can also rewrite all links with ``rewrite-links="1"``; this is typically necessary if the X-Forwarded-\* headers aren't used to construct links. +You can modify both the request and the response with multiple ```` and ```` tags. The request can set headers to literal strings, and you can modify the request arbitrarily with ``pyref``. The response can also have headers added, and arbitrary modification with ``pyref``. You can also rewrite all links with ``rewrite-links="1"``; this is typically necessary if the X-Forwarded-\* headers aren't used to construct links in the application. You can also use this to try theming on an existing live site. FIXME: should there be a way to avoid theming on a section? +FIXME: it would be nice to be able to put in a hard restriction on ``file:`` URLs (both to disallow, or simply to give a base directory that you can't possibly go above). + +FIXME: there should be a way of indicating what portion of the path is stripped. If you do ```` then ``/foo`` is stripped (moved to SCRIPT_NAME) before proxying, but for any other kind of matching it doesn't work. Note you can still do it with ``pyref``. Added: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/pyref.txt ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/pyref.txt Tue Jul 8 22:25:51 2008 @@ -0,0 +1,59 @@ +Python References +================= + +Many places in the configuration take ``pyref`` attributes. These reference a module/function name. The general pattern is: + +``pyref="location:function_name"`` + + The ``location`` can be a module name, or ``file:/path/to/filename.py``. If it is a literal filename, then the file is exec'd and turned into a module that way. All references are to functions (or callable objects), and so you must give the function name. Note that the ``file:`` case isn't a URL, just a path. + +``pyarg-foo="bar"`` + + You can pass extra ad hoc arguments to the function using attributes in this form. This would add the keyword argument ``foo="bar"`` to the function call. All arguments have string (well, unicode) values. + +Examples +-------- + +You can use a ```` like: + +.. code-block:: xml + + + + + + + +Then your code might look like: + +.. code-block:: python + + def get_theme(request, response, log, default_theme, + base, base_url): + host = request.host.split(':')[0] + if os.path.exists(os.path.join(base, host, 'theme.html'): + return base_url + '/' + host + '/theme.html' + elif not default_theme: + log.fatal(None, + "Theme for host %r doesn't exist and no default theme was given" + % host) + raise AbortTheme("No theme found") + else: + log.debug(None, + "Falling back to default theme") + return base_url + '/' + default_theme + '/theme.html' + +Disabling +--------- + +You can disallow Python references using ``deliverance-proxy`` with: + +.. code-block:: xml + + + false + + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/rules.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/docs/rules.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/rules.txt Tue Jul 8 22:25:51 2008 @@ -1,10 +1,12 @@ Rule Spec ========= +**Note:** This document was written before the code, and needs to be corrected (it's close to accurate, though). + Selectors --------- -Rules use selectors. Selectors look like: +Rules use selectors. Selectors look like:: type:selector1 || selector2 || ... @@ -44,13 +46,18 @@ These are optional: - href: a place to find the "content" for this rule. You can use this to include external content. - if-content: a selector; if the selector matches anything then the rule will be run. If not the rule will be skipped. No types (elements:, etc) are allowed for this, but you may prefix "not " before the selector. You may also use "&&", which binds looser than ||. - notheme: one of "ignore", "abort", "warn", where the default is "warn". This is the error action if the theme selector doesn't match anything. - nocontent: like notheme. - manytheme: if the theme selector matches more than one element, then this is used to decide the action. The default is "warn:first". If you use "warn:", then a warning will given. You can also use "abort" and "ignore", and "first" or "last". E.g., manytheme="ignore:last", then the last element selected will be used, and no warning given. If you give just "first" or "last", then ignore: is presumed. "abort" by itself aborts the theming. - manycontent: like manytheme, but for those (few) instances when the content selector must match a single element. - move: contains "0" or "1" (default "1"). If "1", then the elements are removed from the content. You can use this to take pieces out of the content; otherwise if an inner element and outer element from the content are both copied in, then the inner element will show up twice. + ``href``: a place to find the "content" for this rule. You can use this to include external content. + + ``if-content``: a selector; if the selector matches anything then the rule will be run. If not the rule will be skipped. No types (elements:, etc) are allowed for this, but you may prefix "not " before the selector. You may also use "&&", which binds looser than ||. + + ``notheme``: one of "ignore", "abort", "warn", where the default is "warn". This is the error action if the theme selector doesn't match anything. + + ``nocontent``: like notheme. + + ``manytheme``: if the theme selector matches more than one element, then this is used to decide the action. The default is "warn:first". If you use "warn:", then a warning will given. You can also use "abort" and "ignore", and "first" or "last". E.g., manytheme="ignore:last", then the last element selected will be used, and no warning given. If you give just "first" or "last", then ignore: is presumed. "abort" by itself aborts the theming. + ``manycontent``: like manytheme, but for those (few) instances when the content selector must match a single element. + + ``move``: contains "0" or "1" (default "1"). If "1", then the elements are removed from the content. You can use this to take pieces out of the content; otherwise if an inner element and outer element from the content are both copied in, then the inner element will show up twice. For drop: @@ -58,7 +65,7 @@ Optional attributes for drop: - if-content, notheme, nocontent + ``if-content``, ``notheme``, ``nocontent`` Behavior: replace ~~~~~~~~~~~~~~~~~ @@ -67,31 +74,31 @@ theme=children, content=elements: -The theme element is cleared of any content and text, and replaced with the content elements. + The theme element is cleared of any content and text, and replaced with the content elements. theme=elements, content=elements: -This element is removed, and the content element(s) are put in the same location. + This element is removed, and the content element(s) are put in the same location. theme=elements, content=children: -The theme element is removed, and replaced with all the children of the content elements (including text). + The theme element is removed, and replaced with all the children of the content elements (including text). theme=children, content=children: -The theme element is cleared and replaced with the children of the content elements. + The theme element is cleared and replaced with the children of the content elements. theme=attributes, content=attributes: -The theme element has all its attributes cleared, and replaced with the attributes of the content. + The theme element has all its attributes cleared, and replaced with the attributes of the content. -If the theme is attributes, the content must also be attributes (and vice versa). + If the theme is attributes, the content must also be attributes (and vice versa). theme=tag, content=tag: -Both the theme and content selector must match a single element. The theme tag is removed and replaced with the content tag, but the theme's children are moved into the content tag. + Both the theme and content selector must match a single element. The theme tag is removed and replaced with the content tag, but the theme's children are moved into the content tag. -Both theme and content must be tag. + Both theme and content must be tag. Behavior: append/prepend @@ -101,27 +108,27 @@ theme=children, content=elements: -The theme (a single element) has the content elements appended after all the theme's children. prepend puts the children of the contents at the start of the theme element, in their original order. For prepend any leading text in the theme goes after the children elements. + The theme (a single element) has the content elements appended after all the theme's children. prepend puts the children of the contents at the start of the theme element, in their original order. For prepend any leading text in the theme goes after the children elements. theme=elements, content=elements: -The theme (a single element) has all the elements from the content put immediate after the theme element. prepend puts the content elements immediately before the element, but the content elements themselves retain their original order. + The theme (a single element) has all the elements from the content put immediate after the theme element. prepend puts the content elements immediately before the element, but the content elements themselves retain their original order. theme=elements, content=children: -The theme element has the children of the content elements appended after it. + The theme element has the children of the content elements appended after it. theme=children, content=children: -The theme element has the children of the content appended or prepended before its children. + The theme element has the children of the content appended or prepended before its children. theme=attributes, content=attributes: -The content attributes are added to the theme attributes. When attributes overlap, the content attribute is dropped when using append. With prepend, the theme attribute is dropped. The class attribute is a special case: these are combined (space-separated). + The content attributes are added to the theme attributes. When attributes overlap, the content attribute is dropped when using append. With prepend, the theme attribute is dropped. The class attribute is a special case: these are combined (space-separated). theme=tag, or content=tag: -Disallowed. + Disallowed. Behavior: drop ~~~~~~~~~~~~~~ @@ -130,19 +137,19 @@ elements: -Drop the elements. (Doesn't include tail text) + Drop the elements. (Doesn't include tail text) children: -Clear the elements of their children (including contained text). + Clear the elements of their children (including contained text). attributes: -Clear the elements' attributes. + Clear the elements' attributes. tag: -Remove the element tags, but keep their children in place. + Remove the element tags, but keep their children in place. @@ -153,6 +160,8 @@ You want to add the class of the content's body element to the class of the resulting page: +.. code-block:: xml + @@ -162,6 +171,8 @@ You want to take content from one of several places (different products put it in different locations). In all cases it is merged into a single theme element: +.. code-block:: xml + @@ -171,6 +182,8 @@ You want to copy over content CSS, except for one annoying file: +.. code-block:: xml + @@ -182,10 +195,14 @@ You want to copy over the title from the content, but if the content has no title then use the title from the theme: +.. code-block:: xml + -You want to copy over the title from the content. The theme looks like " | Site Name", and you want to keep "| Site Name" as the tail of the title: +You want to copy over the title from the content. The theme looks like `` | Site Name``, and you want to keep ``| Site Name`` as the tail of the title: + +.. code-block:: xml @@ -193,6 +210,8 @@ You want to copy the login button from the content to a known location of the theme. It might be a login, or it might be the username. +.. code-block:: xml + @@ -201,6 +220,8 @@ The content has a class that uses a CSS style that breaks out of the theme. You want to just get rid of that class: +.. code-block:: xml + Added: z3/deliverance/sandbox/ianb/deliverance/trunk/regen-docs ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/regen-docs Tue Jul 8 22:25:51 2008 @@ -0,0 +1,10 @@ +#!/bin/sh + +mkdir -p docs/_static docs/_build +sphinx-build -E -b html docs/ docs/_build || exit 1 +if [ "$1" = "publish" ] ; then + cd docs/ + echo "Uploading files..." + #scp -r _build/* + echo "(no upload location set)" +fi From ianb at codespeak.net Tue Jul 8 22:39:26 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 8 Jul 2008 22:39:26 +0200 (CEST) Subject: [z3-checkins] r56374 - z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance Message-ID: <20080708203926.A37D816A053@codespeak.net> Author: ianb Date: Tue Jul 8 22:39:24 2008 New Revision: 56374 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py Log: remove a spurious debug print Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/themeref.py Tue Jul 8 22:39:24 2008 @@ -54,8 +54,6 @@ self, 'Rewrote theme href="%s" to "%s"' % (href, new_href)) href = new_href ## FIXME: is this join a good idea? - print 'resolved value is %r (from %r) for request %r (base %r)' % ( - href, self.href, req.url, self.source_location) if href: href = urlparse.urljoin(req.url, href) return href From ianb at codespeak.net Tue Jul 8 22:40:24 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 8 Jul 2008 22:40:24 +0200 (CEST) Subject: [z3-checkins] r56375 - z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance Message-ID: <20080708204024.8351D16A056@codespeak.net> Author: ianb Date: Tue Jul 8 22:40:23 2008 New Revision: 56375 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py Log: remove default 60min expiration of dev login Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py Tue Jul 8 22:40:23 2008 @@ -436,7 +436,7 @@ """ def __init__(self, server_host, execute_pyref=True, display_local_files=True, dev_allow_ips=None, dev_deny_ips=None, dev_htpasswd=None, dev_users=None, - dev_expiration=60, + dev_expiration=0, source_location=None): self.server_host = server_host self.execute_pyref = execute_pyref @@ -467,7 +467,7 @@ dev_allow_ips = [] dev_deny_ips = [] dev_htpasswd = None - dev_expiration = 60 + dev_expiration = 0 dev_users = {} for child in el: if child.tag is Comment: From ianb at codespeak.net Tue Jul 8 22:54:21 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 8 Jul 2008 22:54:21 +0200 (CEST) Subject: [z3-checkins] r56376 - in z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance: . tests Message-ID: <20080708205421.BB139169F66@codespeak.net> Author: ianb Date: Tue Jul 8 22:54:21 2008 New Revision: 56376 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/security.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example.py Log: get example working again; add force_dev_auth to SecurityContext Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/security.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/security.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/security.py Tue Jul 8 22:54:21 2008 @@ -18,13 +18,18 @@ This uses the `developer auth spec `_ for guessing when a value is None. + + Also if you use ``force_dev_auth=True`` then DevAuth login will + not be required, and at all times you will be logged in as a dev + user. """ def __init__(self, execute_pyref=False, display_logging=None, - display_local_files=None): + display_local_files=None, force_dev_auth=False): self._execute_pyref = execute_pyref self._display_logging = display_logging self._display_local_files = display_local_files + self._force_dev_auth = force_dev_auth @classmethod def install(cls, environ, **kw): @@ -52,6 +57,8 @@ if hasattr(environ, 'environ'): # Actually a request environ = environ.environ + if self._force_dev_auth: + return True return bool(environ.get('x-wsgiorg.developer_user')) @classmethod Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example.py Tue Jul 8 22:54:21 2008 @@ -7,10 +7,14 @@ from paste.httpserver import serve from weberror.evalexception import EvalException from deliverance.middleware import DeliveranceMiddleware, SubrequestRuleGetter +from deliverance.security import SecurityContext base_path = os.path.join(os.path.dirname(__file__), 'example-files') app = StaticURLParser(base_path) deliv_app = DeliveranceMiddleware(app, SubrequestRuleGetter('/rules.xml')) +full_app = SecurityContext.middleware(deliv_app, execute_pyref=True, display_logging=True, display_local_files=True, + force_dev_auth=True) if __name__ == '__main__': - serve(EvalException(deliv_app)) + print 'See http://127.0.0.1:8080/?deliv_log for the page with log messages' + serve(EvalException(full_app)) From ianb at codespeak.net Tue Jul 8 22:56:29 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Tue, 8 Jul 2008 22:56:29 +0200 (CEST) Subject: [z3-checkins] r56377 - z3/deliverance/sandbox/ianb/deliverance/trunk Message-ID: <20080708205629.D1F8B169F66@codespeak.net> Author: ianb Date: Tue Jul 8 22:56:29 2008 New Revision: 56377 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py Log: add a requirement Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py Tue Jul 8 22:56:29 2008 @@ -36,6 +36,7 @@ "Pygments", "WebError", "DevAuth", + "Paste", ], dependency_links=[ "https://svn.openplans.org/svn/DevAuth/trunk#egg=DevAuth-dev", From ianb at codespeak.net Wed Jul 9 08:52:30 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Wed, 9 Jul 2008 08:52:30 +0200 (CEST) Subject: [z3-checkins] r56379 - z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance Message-ID: <20080709065230.3DD6B2A013F@codespeak.net> Author: ianb Date: Wed Jul 9 08:52:30 2008 New Revision: 56379 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Log: Made logging work for rules. Handled case of append/prepend when there are no child elements (only text). Missing import for making nocontent=abort work. Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py Wed Jul 9 08:52:30 2008 @@ -3,7 +3,7 @@ puts them together """ -from deliverance.exceptions import add_exception_info, DeliveranceSyntaxError +from deliverance.exceptions import add_exception_info, DeliveranceSyntaxError, AbortTheme from deliverance.util.converters import asbool, html_quote from deliverance.selector import Selector from deliverance.pagematch import AbstractMatch @@ -297,7 +297,7 @@ parts = ['<%s' % linked_item(self.source_location, self.name, source=True)] if getattr(self, 'content', None): body = 'content="%s"' % html_quote(self.content) - if self.content_href: + if getattr(self, 'content_href', None): if request_url: content_url = urlparse.urljoin(request_url, self.content_href) else: @@ -688,8 +688,14 @@ theme_el.extend(els) pos_text = 'end' else: - add_tail(els[-1], theme_el.text) - theme_el.text = text + if len(els): + add_tail(els[-1], theme_el.text) + theme_el.text = text + else: + old_text = theme_el.text + theme_el.text = text or '' + if old_text: + theme_el.text += old_text theme_el[:0] = els pos_text = 'beginning' if self.move: @@ -853,7 +859,6 @@ ## FIXME: proper error: assert content is not None or theme is not None self.content = content - assert theme is not None self.theme = theme self.if_content = if_content self.nocontent = self.convert_error('nocontent', nocontent) From ianb at codespeak.net Wed Jul 9 08:53:12 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Wed, 9 Jul 2008 08:53:12 +0200 (CEST) Subject: [z3-checkins] r56380 - z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance Message-ID: <20080709065312.C18A42A013F@codespeak.net> Author: ianb Date: Wed Jul 9 08:53:12 2008 New Revision: 56380 Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py Log: Pick up classes from environment. Catch AbortTheme from more places. Handle case when there are no rules for a class Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py Wed Jul 9 08:53:12 2008 @@ -29,6 +29,8 @@ return resp if 'X-Deliverance-Page-Class' in resp.headers: classes.extend(resp.headers['X-Deliverance-Page-Class'].strip().split()) + if 'deliverance.page_classes' in req.environ: + classes.extend(req.environ['deliverance.page_classes']) if not classes: classes = ['default'] rules = [] @@ -36,7 +38,7 @@ for class_name in classes: ## FIXME: handle case of unknown classes ## Or do that during compilation? - for rule in self.rules_by_class[class_name]: + for rule in self.rules_by_class.get(class_name, []): if rule not in rules: rules.append(rule) if rule.theme: @@ -51,21 +53,21 @@ theme_href = theme.resolve_href(req, resp, log) theme_doc = self.get_theme(theme_href, resource_fetcher, log) content_doc = self.parse_document(resp.body, req.url) + run_standard = True + for rule in rules: + if rule.match is not None: + matches = rule.match(req, resp, response_headers, log) + if not matches: + log.debug(rule, "Skipping ") + continue + rule.apply(content_doc, theme_doc, resource_fetcher, log) + if rule.suppress_standard: + run_standard = False + if run_standard: + ## FIXME: should it be possible to put the standard rule in the ruleset? + standard_rule.apply(content_doc, theme_doc, resource_fetcher, log) except AbortTheme: return resp - run_standard = True - for rule in rules: - if rule.match is not None: - matches = rule.match(req, resp, response_headers, log) - if not matches: - log.debug(rule, "Skipping ") - continue - rule.apply(content_doc, theme_doc, resource_fetcher, log) - if rule.suppress_standard: - run_standard = False - if run_standard: - ## FIXME: should it be possible to put the standard rule in the ruleset? - standard_rule.apply(content_doc, theme_doc, resource_fetcher, log) remove_content_attribs(theme_doc) ## FIXME: handle caching? resp.body = tostring(theme_doc) From ianb at codespeak.net Wed Jul 9 08:54:06 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Wed, 9 Jul 2008 08:54:06 +0200 (CEST) Subject: [z3-checkins] r56381 - in z3/deliverance/sandbox/ianb/deliverance/trunk: . deliverance deliverance/media deliverance/tests deliverance/tests/example-files deliverance/util Message-ID: <20080709065406.2361F2A013F@codespeak.net> Author: ianb Date: Wed Jul 9 08:54:05 2008 New Revision: 56381 Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/media/ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/media/browser.js z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/deliv-sidebar.html (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/deliv-theme.html (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/fixup_openplans.py (contents, props changed) z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/filetourl.py (contents, props changed) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxycommand.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-proxy-rule.xml z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py Log: Lots of bug fixes to make the deliverance combination site work, improving proxying, adding the browser to figure out how to make selections Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py Wed Jul 9 08:54:05 2008 @@ -64,6 +64,7 @@ {{endif}} | unthemed content | content source + | browse content {{if log.messages}} @@ -98,11 +99,13 @@ def format_html_log(self): content_source = self.link_to(self.request.url, source=True) + content_browse = self.link_to(self.request.url, browse=True) return self.log_template.substitute( log=self, middleware=self.middleware, unthemed_url=self._add_notheme(self.request.url), theme_url=self._add_notheme(self.theme_url), content_source=content_source, + content_browse=content_browse, **self.tags) def _add_notheme(self, url): @@ -137,8 +140,9 @@ logging.ERROR: ('#fff', '#600'), logging.CRITICAL: ('#000', '#f33')}[level] - def link_to(self, url, source=False, line=None, selector=None): - return self.middleware.link_to(self.request, url, source=source, line=line, selector=selector) + def link_to(self, url, source=False, line=None, selector=None, browse=False): + return self.middleware.link_to(self.request, url, source=source, line=line, + selector=selector, browse=browse) class PrintingLogger(SavingLogger): Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/media/browser.js ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/media/browser.js Wed Jul 9 08:54:05 2008 @@ -0,0 +1,53 @@ +google.load("jquery", "1"); +var deliveranceSelector = null; +var selectedElement = null; + +google.setOnLoadCallback(function() { + var selector = $('#deliverance-ids').get(0); + deliveranceSelector = selector; + var uniqueClasses = {}; + var dupClasses = {}; + $("*").get().map(function (el) { + if (el.id == 'deliverance-ids' || el.id == 'deliverance-browser') { + return; + } + if (el.id) { + var option = document.createElement('option'); + option.value = '#' + el.id; + option.innerHTML = '#' + el.id; + selector.appendChild(option); + } + if (el.className) { + var allClasses = el.className.split(); + allClasses.map(function (className) { + if (! dupClasses[className]) { + if (uniqueClasses[className]) { + dupClasses[className] = true; + uniqueClasses[className] = undefined; + } else { + uniqueClasses[className] = true; + } + } + }); + } + }); + for (var className in uniqueClasses) { + if (! uniqueClasses[className]) { + continue; + } + var option = document.createElement('option'); + option.value = '.'+className; + option.innerHTML = '.'+className; + selector.appendChild(option); + } +}); + +function deliveranceChangeId() { + var option = deliveranceSelector.value; + if (selectedElement) { + selectedElement.removeClass('deliverance-highlight'); + } + selectedElement = $(option); + selectedElement.addClass('deliverance-highlight'); +} + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py Wed Jul 9 08:54:05 2008 @@ -3,6 +3,7 @@ from wsgiproxy.exactproxy import proxy_exact_request from deliverance.log import SavingLogger from deliverance.security import display_logging, display_local_files +from deliverance.util.filetourl import url_to_filename import urllib import hmac import sha @@ -64,8 +65,7 @@ ## be because of a more valid subrequest than displaying a file return exc.HTTPForbidden( "You cannot access file: URLs (like %r)" % url) - filename = '/' + url[len('file:'):].lstrip('/') - filename = urllib.unquote(filename) + filename = url_to_filename(url) if not os.path.exists(filename): return exc.HTTPNotFound( "The file %r was not found" % filename) @@ -108,7 +108,7 @@ url, subresp.status, subresp.content_type) return subresp - def link_to(self, req, url, source=False, line=None, selector=None): + def link_to(self, req, url, source=False, line=None, selector=None, browse=False): base = req.environ['deliverance.base_url'] base += '/.deliverance/view' source = int(bool(source)) @@ -119,6 +119,8 @@ args['line'] = str(line) if selector: args['selector'] = selector + if browse: + args['browse'] = '1' url = base + '?' + urllib.urlencode(args) if selector: url += '#deliverance-selection' @@ -140,83 +142,142 @@ except exc.HTTPException, e: return e + def action_media(self, req, resource_fetcher): + ## FIXME: I'm not using this currently, because the Javascript didn't work. Dunno why. + from paste.urlparser import StaticURLParser + app = StaticURLParser(os.path.join(os.path.dirname(__file__), 'media')) + ## FIXME: need to pop some segments from the req? + req.path_info_pop() + resp = req.get_response(app) + if resp.content_type == 'application/x-javascript': + resp.content_type = 'application/javascript' + return resp + def action_view(self, req, resource_fetcher): url = req.GET['url'] source = int(req.GET.get('source', '0')) + browse = int(req.GET.get('browse', '0')) line = int(req.GET.get('line', '0')) or '' selector = req.GET.get('selector', '') subresp = resource_fetcher(url) if source: - ct = subresp.content_type - if ct.startswith('application/xml'): - lexer = XmlLexer() - elif ct == 'text/html': - lexer = HtmlLexer() - else: - ## FIXME: what then? - lexer = HtmlLexer() - text = pygments_highlight( - subresp.body, lexer, - HtmlFormatter(full=True, linenos=True, lineanchors='code')) + return self.view_source(req, subresp) + elif browse: + return self.view_browse(req, subresp) + else: + return self.view_selection(req, subresp) + + def view_source(self, req, resp): + ct = resp.content_type + if ct.startswith('application/xml'): + lexer = XmlLexer() + elif ct == 'text/html': + lexer = HtmlLexer() + else: + ## FIXME: what then? + lexer = HtmlLexer() + text = pygments_highlight( + resp.body, lexer, + HtmlFormatter(full=True, linenos=True, lineanchors='code')) + return Response(text) + + def view_browse(self, req, resp): + import re + body = resp.body + f = open(os.path.join(os.path.dirname(__file__), 'media', 'browser.js')) + content = f.read() + f.close() + extra_head = ''' + + + + + + + ''' % ( + content, posixpath.dirname(req.GET['url']) + '/') + match = re.search(r'', body, re.I) + if match: + body = body[:match.end()] + extra_head + body[match.end():] else: - from deliverance.selector import Selector - doc = document_fromstring(subresp.body) - el = Element('base') - el.set('href', posixpath.dirname(url) + '/') - doc.head.insert(0, el) - selector = Selector.parse(selector) - type, elements, attributes = selector(doc) - if not elements: - template = self._not_found_template + body = extra_head + body + extra_body = ''' +
    + + View by id/class: +
    ''' + match = re.search('', body, re.I) + if match: + body = body[:match.end()] + extra_body + body[match.end():] + else: + body = extra_body + body + return Response(body) + + def view_selection(self, req, resp): + from deliverance.selector import Selector + doc = document_fromstring(resp.body) + el = Element('base') + el.set('href', posixpath.dirname(req.GET['url']) + '/') + doc.head.insert(0, el) + selector = Selector.parse(selector) + type, elements, attributes = selector(doc) + if not elements: + template = self._not_found_template + else: + template = self._found_template + all_elements = [] + els_in_head = False + for index, el in enumerate(elements): + el_in_head = self._el_in_head(el) + if el_in_head: + els_in_head = True + anchor = 'deliverance-selection' + if index: + anchor += '-%s' % index + if el.get('id'): + anchor = el.get('id') + ## FIXME: is a better? + if not el_in_head: + el.set('id', anchor) else: - template = self._found_template - all_elements = [] - els_in_head = False - for index, el in enumerate(elements): - el_in_head = self._el_in_head(el) - if el_in_head: - els_in_head = True - anchor = 'deliverance-selection' - if index: - anchor += '-%s' % index - if el.get('id'): - anchor = el.get('id') - ## FIXME: is a better? - if not el_in_head: - el.set('id', anchor) - else: - anchor = None - ## FIXME: add :target CSS rule - ## FIXME: or better, some Javascript - all_elements.append((anchor, el)) - if not el_in_head: - style = el.get('style', '') - if style: - style += '; ' - style += '/* deliverance */ border: 2px dotted #f00' - el.set('style', style) - else: - el.set('DELIVERANCE-MATCH', '1') - def highlight(html_code): - if isinstance(html_code, _Element): - html_code = tostring(html_code) - return html(pygments_highlight(html_code, HtmlLexer(), - HtmlFormatter(noclasses=True))) - def format_tag(tag): - return highlight(tostring(tag).split('>')[0]+'>') - text = template.substitute( - base_url=req.url, - els_in_head=els_in_head, doc=doc, - elements=all_elements, selector=selector, - format_tag=format_tag, highlight=highlight) - message = fromstring(self._message_template.substitute(message=text, url=url)) - if doc.body.text: - message.tail = doc.body.text - doc.body.text = '' - doc.body.insert(0, message) - text = tostring(doc) - resp = Response(text) - return resp + anchor = None + ## FIXME: add :target CSS rule + ## FIXME: or better, some Javascript + all_elements.append((anchor, el)) + if not el_in_head: + style = el.get('style', '') + if style: + style += '; ' + style += '/* deliverance */ border: 2px dotted #f00' + el.set('style', style) + else: + el.set('DELIVERANCE-MATCH', '1') + def highlight(html_code): + if isinstance(html_code, _Element): + html_code = tostring(html_code) + return html(pygments_highlight(html_code, HtmlLexer(), + HtmlFormatter(noclasses=True))) + def format_tag(tag): + return highlight(tostring(tag).split('>')[0]+'>') + text = template.substitute( + base_url=req.url, + els_in_head=els_in_head, doc=doc, + elements=all_elements, selector=selector, + format_tag=format_tag, highlight=highlight) + message = fromstring(self._message_template.substitute(message=text, url=url)) + if doc.body.text: + message.tail = doc.body.text + doc.body.text = '' + doc.body.insert(0, message) + text = tostring(doc) + return Response(text) + def _el_in_head(self, el): while el is not None: Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxy.py Wed Jul 9 08:54:05 2008 @@ -10,7 +10,7 @@ from lxml.etree import tostring as xml_tostring from lxml.html import document_fromstring, tostring from pyref import PyReference -from webob import Request +from webob import Request, Response from webob import exc import urlparse from wsgiproxy.exactproxy import proxy_exact_request @@ -23,6 +23,8 @@ from paste.fileapp import FileApp import urllib import posixpath +from deliverance.util.filetourl import filename_to_url, url_to_filename +from lxml.etree import parse class ProxySet(object): @@ -41,6 +43,12 @@ ruleset = RuleSet.parse_xml(el, source_location) return cls(proxies, ruleset, source_location) + @classmethod + def parse_file(cls, filename): + file_url = filename_to_url(filename) + el = parse(filename, base_url=file_url).getroot() + return cls.parse_xml(el, file_url) + def proxy_app(self, environ, start_response): request = Request(environ) log = environ['deliverance.log'] @@ -72,8 +80,8 @@ def __init__(self, match, dest, request_modifications, response_modifications, - strip_script_name=False, keep_host=False, - source_location=None): + strip_script_name=True, keep_host=False, + source_location=None, classes=None): self.match = match self.match.proxy = self self.dest = dest @@ -82,6 +90,7 @@ self.request_modifications = request_modifications self.response_modifications = response_modifications self.source_location = source_location + self.classes = classes def log_description(self, log=None): parts = [] @@ -89,11 +98,12 @@ parts.append('<proxy') else: parts.append('<proxy' % log.link_to(self.source_location, source=True)) - if self.strip_script_name: - parts.append('strip-script-name="1"') + ## FIXME: defaulting to true is bad + if not self.strip_script_name: + parts.append('strip-script-name="0"') if self.keep_host: parts.append('keep-host="1"') - parts.append('/>
    \n') + parts.append('>
    \n') parts.append(' ' + self.dest.log_description(log)) parts.append('
    \n') if self.request_modifications: @@ -116,7 +126,7 @@ dest = None request_modifications = [] response_modifications = [] - strip_script_name = False + strip_script_name = True keep_host = False for child in el: if child.tag == 'dest': @@ -144,9 +154,10 @@ raise DeliveranceSyntaxError( "Unknown tag in : %s" % xml_tostring(child), element=child, source_location=source_location) + classes = el.get('class', '').split() or None return cls(match, dest, request_modifications, response_modifications, strip_script_name=strip_script_name, keep_host=keep_host, - source_location=source_location) + source_location=source_location, classes=classes) def forward_request(self, environ, start_response): request = Request(environ) @@ -155,7 +166,8 @@ if prefix.endswith('/'): prefix = prefix[:-1] path_info = request.path_info - if not path_info.startswith(prefix + '/'): + if not path_info.startswith(prefix + '/') and not path_info == prefix: + log = environ['deliverance.log'] log.warn(self, "The match would strip the prefix %r from the request path (%r), but they do not match" % (prefix + '/', path_info)) else: @@ -168,6 +180,9 @@ raise AbortProxy dest = self.dest(request, log) log.debug(self, ' matched; forwarding request to %s' % dest) + if self.classes: + log.debug(self, 'Adding class="%s" to page' % ' '.join(self.classes)) + request.environ.setdefault('deliverance.page_classes', []).extend(self.classes) response, orig_base, proxied_base, proxied_url = self.proxy_to_dest(request, dest) for modifier in self.response_modifications: response = modifier.modify_response(request, response, orig_base, proxied_base, proxied_url, log) @@ -195,7 +210,6 @@ assert 0, "bad scheme: %r (from %r)" % (scheme, dest) if not self.keep_host: proxy_req.host = netloc - proxied_url = '%s://%s%s' % (scheme, netloc, proxy_req.path_qs) if query: if proxy_req.query_string: proxy_req.query_string += '&' @@ -204,11 +218,13 @@ proxy_req.headers['X-Forwarded-For'] = request.remote_addr proxy_req.headers['X-Forwarded-Scheme'] = request.scheme proxy_req.headers['X-Forwarded-Server'] = request.host + proxy_req.scheme = scheme ## FIXME: something with path? proxy_req.headers['X-Forwarded-Path'] ## (now we are only doing it with strip_script_name) if self.strip_script_name: proxy_req.headers['X-Forwarded-Path'] = proxy_req.script_name proxy_req.script_name = '' + proxied_url = '%s://%s%s' % (scheme, netloc, proxy_req.path_qs) try: resp = proxy_req.get_response(proxy_exact_request) except socket.error, e: @@ -227,14 +243,33 @@ orig_base = request.application_url ## FIXME: security restrictions here? assert dest.startswith('file:') - filename = urllib.unquote('/' + dest[len('file:'):].lstrip('/')) + if '?' in dest: + dest = dest.split('?', 1)[0] + filename = url_to_filename(dest) rest = posixpath.normpath(request.path_info) proxied_url = dest.lstrip('/') + '/' + urllib.quote(rest.lstrip('/')) ## FIXME: handle /->/index.html filename = filename.rstrip('/') + '/' + rest.lstrip('/') - app = FileApp(filename) - # I don't really need a copied request here, because FileApp is so simple: - resp = request.get_response(app) + if os.path.isdir(filename): + if not request.path.endswith('/'): + new_url = request.path + '/' + if request.query_string: + new_url += '?' + request.query_string + resp = exc.HTTPMovedPermanently(location=new_url) + return resp, orig_base, dest, proxied_url + ## FIXME: configurable? StaticURLParser? + for base in ['index.html', 'index.htm']: + if os.path.exists(os.path.join(filename, base)): + filename = os.path.join(filename, base) + break + else: + resp = exc.HTTPNotFound("There was no index.html file in the directory") + if not os.path.exists(filename): + resp = exc.HTTPNotFound("The file %s could not be found" % filename) + else: + app = FileApp(filename) + # I don't really need a copied request here, because FileApp is so simple: + resp = request.get_response(app) return resp, orig_base, dest, proxied_url class ProxyMatch(AbstractMatch): @@ -324,11 +359,13 @@ default_objs=dict(AbortProxy=AbortProxy)) header = el.get('header') content = el.get('content') + ## FIXME: the misspelling is annoying :( if (not header and content) or (not content and header): raise DeliveranceSyntaxError( "If you provide a header attribute you must provide a content attribute, and vice versa", element=el, source_location=source_location) - return cls(pyref, header, content, source_location) + return cls(pyref, header, content, + source_location=source_location) def modify_request(self, request, log): if self.pyref: @@ -378,10 +415,10 @@ proxied_base += '/' if not orig_base.endswith('/'): orig_base += '/' - assert proxied_url.startswith(proxied_base), ( + assert proxied_url.startswith(proxied_base) or proxied_url.split('?', 1)[0] == proxied_base[:-1], ( "Unexpected proxied_url %r, doesn't start with proxied_base %r" % (proxied_url, proxied_base)) - assert request.url.startswith(orig_base), ( + assert request.url.startswith(orig_base) or request.url.split('?', 1)[0] == orig_base[:-1], ( "Unexpected request.url %r, doesn't start with orig_base %r" % (request.url, orig_base)) if self.pyref: @@ -395,19 +432,24 @@ if self.header: response.headers[self.header] = self.content if self.rewrite_links: - if response.content_type != 'text/html': - log.debug(self, 'Not rewriting links in response from %s, because Content-Type is %s' % (proxied_url, response.content_type)) - return response - body_doc = document_fromstring(response.body, base_url=proxied_url) - body_doc.make_links_absolute() def link_repl_func(link): if not link.startswith(proxied_base): # External link, so we don't rewrite it return link new = orig_base + link[len(proxied_base):] return new - body_doc.rewrite_links(link_repl_func) - response.body = tostring(body_doc) + if response.content_type != 'text/html': + log.debug(self, 'Not rewriting links in response from %s, because Content-Type is %s' % (proxied_url, response.content_type)) + else: + if not response.charset: + ## FIXME: maybe we should guess the encoding? + body = response.body + else: + body = response.unicode_body + body_doc = document_fromstring(body, base_url=proxied_url) + body_doc.make_links_absolute() + body_doc.rewrite_links(link_repl_func) + response.body = tostring(body_doc) if response.location: ## FIXME: if you give a proxy like http://openplans.org, and it redirects to ## http://www.openplans.org, it won't be rewritten and that can be confusing @@ -418,7 +460,7 @@ if response.headers.get('set-cookie'): cook = response.headers['set-cookie'] old_domain = urlparse.urlsplit(proxied_url)[1].lower() - new_domain = req.host.split(':', 1)[0].lower() + new_domain = request.host.split(':', 1)[0].lower() def rewrite_domain(match): domain = match.group(2) if domain == old_domain: @@ -516,6 +558,12 @@ dev_users=dev_users, dev_expiration=dev_expiration, source_location=source_location) + @classmethod + def parse_file(cls, filename): + file_url = filename_to_url(filename) + el = parse(filename, base_url=file_url).getroot() + return cls.parse_xml(el, file_url, traverse=True) + @property def host(self): return self.server_host.split(':', 1)[0] Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxycommand.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxycommand.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/proxycommand.py Wed Jul 9 08:54:05 2008 @@ -4,7 +4,6 @@ from deliverance.proxy import ProxySet from deliverance.proxy import ProxySettings -from lxml.etree import parse from paste.httpserver import serve import urllib @@ -30,24 +29,54 @@ dest='interactive_debugger', help='Use an interactive debugger (note: security hole when done publically; ' 'if interface is not explicitly given it will be set to 127.0.0.1)') - -def run_command(rule_filename, debug=False, interactive_debugger=False): - rule_url = 'file://' + urllib.quote(os.path.abspath(rule_filename).replace(os.path.sep, '/')) - el = parse(rule_filename, base_url=rule_url).getroot() - ## FIXME: rule_filename isn't browsable in the logs - ps = ProxySet.parse_xml(el, rule_url) - settings = ProxySettings.parse_xml(el, rule_url, traverse=True) - app = ps.application - app = settings.middleware(app) +parser.add_option( + '--debug-headers', + action='count', + dest='debug_headers', + help='Show (in the console) all the incoming and outgoing headers; use twice for bodies') + +def run_command(rule_filename, debug=False, interactive_debugger=False, debug_headers=False): + settings = ProxySettings.parse_file(rule_filename) + app = ReloadingApp(rule_filename, settings) if interactive_debugger: from weberror.evalexception import EvalException app = EvalException(app, debug=True) else: from weberror.errormiddleware import ErrorMiddleware app = ErrorMiddleware(app, debug=debug) + if debug_headers: + from wsgifilter.proxyapp import DebugHeaders + app = DebugHeaders(app, show_body=debug_headers > 1) print 'To see logging, visit %s/.deliverance/login' % settings.base_url serve(app, host=settings.host, port=settings.port) +class ReloadingApp(object): + """ + This is a WSGI app that notices when the rule file changes, and + reloads it in that case. + """ + def __init__(self, rule_filename, settings): + self.rule_filename = rule_filename + self.settings = settings + self.proxy_set = None + self.proxy_set_mtime = None + self.application = None + # This gives syntax errors earlier: + self.load_proxy_set(warn=False) + + def __call__(self, environ, start_response): + if (self.proxy_set is None + or self.proxy_set_mtime < os.path.getmtime(self.rule_filename)): + self.load_proxy_set() + return self.application(environ, start_response) + + def load_proxy_set(self, warn=True): + if warn: + print 'Reloading rule file %s' % self.rule_filename + self.proxy_set = ProxySet.parse_file(self.rule_filename) + self.proxy_set_mtime = os.path.getmtime(self.rule_filename) + self.application = self.settings.middleware(self.proxy_set.application) + def main(args=None): if args is None: args = sys.argv[1:] @@ -59,7 +88,7 @@ rule_filename = args[0] run_command(rule_filename, interactive_debugger=options.interactive_debugger, - debug=options.debug) + debug=options.debug, debug_headers=options.debug_headers) if __name__ == '__main__': main() Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pyref.py Wed Jul 9 08:54:05 2008 @@ -6,9 +6,12 @@ from tempita import html_quote import os import new +import urllib from UserDict import DictMixin from deliverance.exceptions import DeliveranceSyntaxError from deliverance.util.importstring import simple_import +from deliverance.util.nesteddict import NestedDict +from deliverance.util import filetourl __all__ = ['PyReference'] @@ -91,7 +94,7 @@ raise DeliveranceSyntaxError( "The filename %r does not exist" % full_file, element=s, source_location=source_location) - return cls(module_name=module, file=filename, function_name=func, args=args, + return cls(module_name=module, filename=filename, function_name=func, args=args, attr_name=attr_name, default_objs=default_objs, source_location=source_location) @@ -112,7 +115,7 @@ else: filename = self.expand_filename(self.filename, self.source_location) if filename not in self._modules: - name = self.pyfile.strip('/').strip('\\') + name = filename.strip('/').strip('\\') name = os.path.splitext(name)[0] name = name.replace('\\', '_').replace('/', '_') new_mod = new.module(name) @@ -120,6 +123,7 @@ for name, value in self.default_objs.items(): if not hasattr(new_mod, name): setattr(new_mod, name, value) + execfile(filename, new_mod.__dict__) self._modules[filename] = new_mod return self._modules[filename] @@ -131,7 +135,12 @@ obj = self.module for p in self.function_name.split('.'): ## FIXME: better error handling: - obj = getattr(obj, p) + try: + obj = getattr(obj, p) + except AttributeError, e: + raise Exception( + "Could not get function %s: %s; existing attributes: %s" + % (p, e, ', '.join(dir(obj)))) return obj def __call__(self, *args, **kw): @@ -144,10 +153,15 @@ """ Expand environmental variables in a filename """ - vars = DefautDict(os.environ) + if source_location and source_location.startswith('file:'): + here = os.path.dirname(filetourl.url_to_filename(source_location)) + else: + ## FIXME: this is a lousy default: + here = '' + vars = NestedDict(dict(here=here), DefaultDict(os.environ)) tmpl = Template(filename) try: - return tmpl.substitute(os.environ) + return tmpl.substitute(vars) except ValueError, e: raise DeliveranceSyntaxError( "The filename %r contains bad $ substitutions: %s" Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/deliv-sidebar.html ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/deliv-sidebar.html Wed Jul 9 08:54:05 2008 @@ -0,0 +1,23 @@ + + + + +The sidebar + + + + +This contains links that go into the sidebar. Everything in the +following div goes into the sidebar: + + + + + Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/deliv-theme.html ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/deliv-theme.html Wed Jul 9 08:54:05 2008 @@ -0,0 +1,26 @@ + + + | Deliverance + + + + + + + + + + + +
    + Some content +
    + + + + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/rules.xml Wed Jul 9 08:54:05 2008 @@ -1,7 +1,7 @@ - + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/style.css Wed Jul 9 08:54:05 2008 @@ -18,19 +18,53 @@ vertical-align: top; } -td#sidebar { +#sidebar { background-color: #bfb; border-right: 2px solid #0f0; width: 8em; padding-top: 1em; + font-size: 85% } -td#content { +#sidebar li { + list-style: none; +} + +#sidebar ul { + padding-left: 0; +} + +#sidebar a, #sidebar a:link{ + text-decoration: none; + width: 100%; + display: block; +} + +#sidebar a:hover { + text-decoration: underline; + background-color: #8f8; +} + +td#theme-content { border-top: 2px solid #0f0; padding: 0.5em; } -h1#header { +#header { background-color: #bfb; margin-bottom: 0; + width: 100%; +} + +#header h1 { + margin: 0; +} + +#header a, #header a:link, #header a:visited { + text-decoration: none; + color: #000; +} + +#header a:hover { + text-decoration: underline; } Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-files/theme.html Wed Jul 9 08:54:05 2008 @@ -12,7 +12,7 @@ Some sidebar links. - + Some content Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-proxy-rule.xml ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-proxy-rule.xml (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/example-proxy-rule.xml Wed Jul 9 08:54:05 2008 @@ -7,44 +7,61 @@
    - - - - + + + + + - - + + + - + + + + + + + + + + + + + + + + + + + + - + + Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/fixup_openplans.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/fixup_openplans.py Wed Jul 9 08:54:05 2008 @@ -0,0 +1,6 @@ +def fixup_openplans_response(request, response, orig_base, proxied_base, proxied_url, log): + user_base = 'http://www.openplans.org/people' + if response.location and response.location.startswith(user_base + '/'): + user_rest = response.location[len(user_base):] + response.location = '%s/people%s' % (request.application_url, user_rest) + Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_pagematch.txt Wed Jul 9 08:54:05 2008 @@ -36,15 +36,15 @@ >>> m = make('') >>> match(m, Request.blank('/foo'), Response(), []) - True + ['a'] >>> match(m, Request.blank('/foobar'), Response(), []) log: Skipping class="a" because request URL (/foobar) does not match path="path:/foo/" False >>> match(m, Request.blank('/foo/bar'), Response(), []) - True + ['a'] >>> m = make('') >>> match(m, Request.blank('/'), Response(content_type='text/plain'), [('content-type', 'text/plain')]) log: Skipping class="x" because the response headers Content-Type do not match response-header="Content-Type: contains:html" False >>> match(m, Request.blank('/'), Response(content_type='text/html'), [('content-type', 'text/html')]) - True + ['x'] Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/filetourl.py ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/util/filetourl.py Wed Jul 9 08:54:05 2008 @@ -0,0 +1,32 @@ +import urllib +import os +import re + +drive_re = re.compile('^([a-z]):', re.I) +url_drive_re = re.compile('^([a-z])[|]', re.I) + +def filename_to_url(filename): + """ + Convert a path to a file: URL. The path will be made absolute. + """ + filename = os.path.normcase(os.path.abspath(filename)) + url = urllib.quote(filename) + if drive_re.match(url): + url = url[0] + '|' + url[2:] + url = url.replace(os.path.sep, '/') + url = url.lstrip('/') + return 'file:///' + url + +def url_to_filename(url): + """ + Convert a file: URL to a path. + """ + assert url.startswith('file:'), ( + "You can only turn file: urls into filenames (not %r)" % url) + filename = url[len('file:'):].lstrip('/') + filename = urllib.unquote(filename) + if url_drive_re.match(filename): + filename = filename[0] + ':' + filename[2:] + else: + filename = '/' + filename + return filename Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/setup.py Wed Jul 9 08:54:05 2008 @@ -37,6 +37,7 @@ "WebError", "DevAuth", "Paste", + "WSGIFilter", ], dependency_links=[ "https://svn.openplans.org/svn/DevAuth/trunk#egg=DevAuth-dev", From ianb at codespeak.net Wed Jul 9 20:57:32 2008 From: ianb at codespeak.net (ianb at codespeak.net) Date: Wed, 9 Jul 2008 20:57:32 +0200 (CEST) Subject: [z3-checkins] r56395 - in z3/deliverance/sandbox/ianb/deliverance/trunk: deliverance docs Message-ID: <20080709185732.922492A8006@codespeak.net> Author: ianb Date: Wed Jul 9 20:57:30 2008 New Revision: 56395 Added: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/configuration.txt (contents, props changed) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py z3/deliverance/sandbox/ianb/deliverance/trunk/docs/index.txt Log: configuration; the complete docs\! Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py Wed Jul 9 20:57:30 2008 @@ -20,6 +20,7 @@ def __init__(self, path=None, domain=None, request_header=None, response_header=None, environ=None, pyref=None, source_location=None): + ## FIXME: this should add response_status self.path = path self.domain = domain self.request_header = request_header @@ -35,7 +36,7 @@ Parses out the match-related arguments """ path = cls._parse_attr(el, 'path', default='path') - domain = cls._parse_attr(el, 'domain', default='wildcard') + domain = cls._parse_attr(el, 'domain', default='wildcard-insensitive') request_header = cls._parse_attr(el, 'request-header', default='exact', header=True) response_header = cls._parse_attr(el, 'response-header', default='exact', header=True) environ = cls._parse_attr(el, 'environ', default='exact', header=True) Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py ============================================================================== --- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py (original) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py Wed Jul 9 20:57:30 2008 @@ -150,6 +150,8 @@ headers.append((http_equiv, content)) return headers +# Note: these are included in the documentation; any changes should be +# reflected there as well. standard_rule = Rule.parse_xml(XML('''\ Added: z3/deliverance/sandbox/ianb/deliverance/trunk/docs/configuration.txt ============================================================================== --- (empty file) +++ z3/deliverance/sandbox/ianb/deliverance/trunk/docs/configuration.txt Wed Jul 9 20:57:30 2008 @@ -0,0 +1,587 @@ +Deliverance Configuration +========================= + +.. contents:: + +All the configuration for Deliverance goes in an XML file. This file can configure page matches, transformation rules, proxying, and server settings. Though it is all in one file, there are several major sections. A quick overview: + +`rule and theme`_: + Determine the theme and apply the actual transformations to the page. +`match and page classes`_: + In more complicated sites, page classes allow you to apply rules based on different criteria. +`proxy and server-settings`_: + Controls the proxy destinations, server, and security settings. +`request/response matching`_: + Several elements can be conditionally used only when the request or response matches conditions. All these elements share common behavior and attributes. +`pyref Python references`_: + Several elements can call Python hooks. This describes the general syntax, while the details of the hook are described alongside the element. +`developer debugging console`_: + This gives you information about what Deliverance is doing, on a page-by-page basis. + +Everything goes in a ```` tag. + +.. comment: FIXME: should this be ? + +rule and theme +-------------- + +This is the core of what Deliverance does: take a theme along with your content, and apply transformations. + +theme +~~~~~ + +The ```` element defines the theme you'll be using. The theme is given as a URL. The basic form is: + +.. code-block:: xml + + + +This defines the theme as being at ``/theme.html``. If possible it will be fetched with an internal request, though external requests are also possible (if you host the theme outside of Deliverance). + +If you have ```` at the global level (in the ```` element) then that is the default theme. You can also put it inside ```` elements, and then it will apply just to that rule set. This is useful if you are using `match and page classes`_. + +The theme element also supports `pyref Python references`_. If you use: + +.. code-block:: xml + + + +Then define a function like: + +.. code-bock:: python + + def get_theme(request, response, log): + return "/%s/theme.html" % request.host + +The ``request`` and ``response`` objects are `WebOb `_ objects. + +The default function name is ``get_theme``. + +rule +~~~~ + +The ```` element defines a set of transformations. It also supports `page classes`_ and `request/response matching`_, which you can read about in those sections. Also, as mentioned above, you can include a ```` element. But the most important thing is the transformation actions. + +The transformation actions are applied in order. The starting point for the transformation is the *theme* document, and the actions copy elements from the content into the theme. + +There are four actions: + +````: + Replaces something in the theme with elements from the content. +````: + Appends content to an element in the theme. +````: + Prepends content to an element in the theme. +````: + Removes elements from the theme or the content. + +Selection and selection types +++++++++++++++++++++++++++++++ + +Each rule depends on selecting elements from the theme and content. The most basic selection is done with CSS 3 selectors. For instance, this places the element in the content with the id ``content`` *after* the element in the theme with the id ``header``: + +.. code-block:: xml + + + +You can also use `XPath `_ selectors. Any selector starting with ``/`` is treated as an XPath expression, while everything else is treated as CSS. CSS can only select elements, and while XPath can select text or attributes Deliverance is only interested in elements. Moving elements around has some limitations, so there are different explicit types of selection: + +.. comment: FIXME: a better XPath link would be nice, like to a tutorial. + +``elements:`` + The default, this applies the rules to the elements selected. +``children:`` + A common type, this applies rules to the *children* of the elements selected (including text content of the elements). +``attributes:`` + This applies the rules to just the attributes. Also you can apply it to just specifically named attributes, for instance just to the ``class`` and ``style`` attributes with ``attributes(class,style):``. +``tag:`` + This applies the rule to the tag, but not the children of the element. For instance, dropping a tag keeps the children in the document, but removes their enclosing tag. + +You can apply any of these like ``content="children:#content"``. Not all combinations make sense, and some are not allowed. For instance, ```` does not make sense, as you can't replace elements with attributes. Generally ``elements:`` and ``children:`` work together, ``attributes:`` only works with ``attributes:``, and ``tag:`` only works with ``tag:``. + +When selecting elements you can use the ``||`` operator. This applies to both CSS and XPath selectors, and with the operator you can mix the two. The ``||`` operator takes the results of the first selector that matches anything. So ``content="#content || children:body"`` will take the element ``#content`` if there is one, and if there is not one it will take all the children of ````. You can mix ``elements:`` and ``children:`` using ``||``, though no other types can be mixed like this. + +```` ++++++++++++++ + +The ```` action replaces something in the theme with something in the content. Exactly what is replaced depends on the selection type. Some examples: + +.. code-block:: xml + + + +this replaces the elements *inside* the theme element ``#content`` with the elements inside the content element ``#content-wrapper``. The resulting document won't have any element with the id ``#content-wrapper`` (unless the theme already had an element with that id). + +.. code-block:: xml + + + + +both of these are the same (``elements:`` is the default selection type). This replaces the theme element ``#content`` with the content element ``#content-wrapper``. The resulting document has no element ``#content``. + +.. code-block:: xml + + + +this removes all the attributes (e.g., ``class``, ``onload``) from the ```` element in the theme, and moves over the attributes from the content body element. + +.. code-block:: xml + + + +this replaces the tag ``#content`` in the theme with its corresponding tag from the theme. They might not be the same tag name (e.g., the theme might be a ``

    `` and the content ``

    ``), and all attributes will be taken from the content. + +```` and ```` +++++++++++++++++++++++++++++++ + +These actions obvious are very similar; ```` puts things from te content after things in the theme, and ```` puts things from the content before things in the theme. Some examples: + +.. code-block:: xml + + + +this moves the children of the content element ``#sidenav`` to the end of the theme element ``#sidebar``, combining the navigation of the theme and content. If you wanted the content navigation to go first you'd use: + +.. code-block:: xml + + + +Another example: + +.. code-block:: xml + + + +this moves the children of ``#sidenav`` *after the element* with the element in the theme ``