# -*- coding: utf-8 -*- """ pocoo.pkg.core.acl ~~~~~~~~~~~~~~~~~~ Pocoo ACL System. :copyright: 2006-2007 by Armin Ronacher. :license: GNU GPL, see LICENSE for more details. """ from pocoo.db import meta from pocoo.pkg.core.forum import Site, Forum, Thread from pocoo.pkg.core.user import User, Group from pocoo.pkg.core.db import users, groups, group_members, privileges, \ forums, posts, acl_mapping, acl_subjects, acl_objects class AclManager(object): """ Manager object to manage ALCs. """ STRONG_NO = -1 WEAK_NO = 0 WEAK_YES = 1 STRONG_YES = 2 def __init__(self, ctx, subject): self.ctx = ctx self.subject = subject if isinstance(subject, User): self._type = 'user' elif isinstance(subject, Group): self._type = 'group' else: raise ValueError('neither user or group specified') def allow(self, privilege, obj, force=False): """Allows the subject privilege on obj.""" return self._set(privilege, obj, 1 + bool(force)) def default(self, privilege, obj): """Sets the state for privilege on obj back to weak yes.""" return self._set(privilege, obj, 0) def deny(self, privilege, obj, force=False): """Denies the subject privilege on obj.""" return self._set(privilege, obj, -1 - bool(force)) def can_access(self, privilege, obj): """Checks if the current subject with the required privilege somehow. Either directly or when the subject is a user and one of its groups can access it.""" #XXX: maybe this could be one big query instead of 4 #XXX: this currently does not work correctly, therefore return True return True if not isinstance(obj, (Forum, Thread, Site.__class__)): raise TypeError('obj must be a forum, thread or site') privilege = privilege.upper() s = self._get_subject_join().alias('s').c def do_check(obj, tendency): db = self.ctx.engine o = self._get_object_join(obj).alias('o').c # self check r = db.execute(meta.select([acl_mapping.c.state], (acl_mapping.c.priv_id == privileges.c.priv_id) & (acl_mapping.c.subject_id == s.subject_id) & (acl_mapping.c.object_id == o.object_id) & (privileges.c.name == privilege) )) row = r.fetchone() if row is not None: if row['state'] in (self.STRONG_NO, self.STRONG_YES): return row['state'] == self.STRONG_YES tendency = row['state'] # if the controlled subject is a user check all groups if isinstance(self.subject, User): r = db.execute(meta.select([acl_mapping.c.state], (acl_mapping.c.object_id == o.object_id) & (acl_mapping.c.subject_id == groups.c.subject_id) & (groups.c.group_id == group_members.c.group_id) & (group_members.c.user_id == self.subject.user_id) )) while True: row = r.fetchone() if row is None: break state = row[0] if state in (self.STRONG_YES, self.STRONG_NO): return state == self.STRONG_YES if tendency is None: tendency = state elif tendency == self.WEAK_NO and state == self.WEAK_YES: tendency = self.WEAK_YES # check related objects if isinstance(obj, Thread): return do_check(obj.forum, tendency) elif isinstance(obj, Forum): return do_check(Site, tendency) else: return tendency return do_check(obj, None) in (self.WEAK_YES, self.STRONG_YES) def _set(self, privilege, obj, state): """Helper functions for settings privileges.""" privilege = privilege.upper() if self.subject.subject_id is None: self._bootstrap() if obj.object_id is None: self._bootstrap_object(obj) # special state "0" which means delete if not state: p = meta.select([privileges.c.priv_id], privileges.c.name == privilege) self.ctx.engine.execute(acl_mapping.delete( (acl_mapping.c.priv_id == p.c.priv_id) & (acl_mapping.c.subject_id == self.subject.subject_id) & (acl_mapping.c.object_id == obj.object_id) )) return # touch privilege and check existing mapping priv_id = self._fetch_privilege(privilege) r = self.ctx.engine.execute(meta.select([acl_mapping.c.state], (acl_mapping.c.priv_id == priv_id) & (acl_mapping.c.subject_id == self.subject.subject_id) & (acl_mapping.c.object_id == obj.object_id) )) row = r.fetchone() if row is not None: # this rule exists already if row['state'] == state: return # goddamn, same rule - different state, delete old first self._set(privilege, obj, 0) # insert new rule self.ctx.engine.execute(acl_mapping.insert(), priv_id = priv_id, subject_id = self.subject.subject_id, object_id = obj.object_id, state = state ) def _bootstrap(self): """This method is automatically called when subject_id is None and an subject_id is required.""" r = self.ctx.engine.execute(acl_subjects.insert(), subject_type = self._type ) self.subject.subject_id = r.last_inserted_ids()[0] self.subject.save() def _bootstrap_object(self, obj): """Like _bootstrap but works for objects.""" objtype = self._get_object_type(obj) r = self.ctx.engine.execute(acl_objects.insert(), object_type = objtype ) obj.object_id = r.last_inserted_ids()[0] obj.save() def _get_object_type(self, obj): if isinstance(obj, Forum): return 'forum' elif isinstance(obj, Thread): return 'thread' elif obj is Site: return 'site' raise TypeError('obj isn\'t a forum or thread') def _get_object_join(self, obj): """Returns a subjoin for the object id.""" t = self._get_object_type(obj) if t == 'forum': return meta.select([forums.c.object_id], forums.c.forum_id == obj.forum_id ) elif t == 'thread': return meta.select([posts.c.object_id], posts.c.post_id == obj.post_id ) else: # XXX: it works ^^ # i really want something like meta.select('0 as group_id') class Fake(object): def alias(self, n): class _C(object): class c(object): object_id = 0 return _C return Fake() def _get_subject_join(self): """Returns a subjoin for the subject id.""" if self._type == 'user': return meta.select([users.c.subject_id], users.c.user_id == self.subject.user_id ) return meta.select([groups.c.subject_id], groups.c.group_id == self.subject.group_id ) def _fetch_privilege(self, name): """Returns the priv_id for the given privilege. If it doesn\'t exist by now the system will create a new privilege.""" r = self.ctx.engine.execute(meta.select([privileges.c.priv_id], privileges.c.name == name )) row = r.fetchone() if row is not None: return row[0] r = self.ctx.engine.execute(privileges.insert(), name = name ) return r.last_inserted_ids()[0] def __repr__(self): if self._type == 'user': id_ = self.subject.user_id else: id_ = self.subject.group_id if self.subject.subject_id is None: return '<%s %s:%d inactive>' % ( self.__class__.__name__, self._type, id_ ) return '<%s %s:%d active as %d>' % ( self.__class__.__name__, self._type, id_, self.subject.subject_id ) # -*- coding: utf-8 -*- """ pocoo.pkg.core.auth ~~~~~~~~~~~~~~~~~~~ Default authentication module. :copyright: 2006-2007 by Armin Ronacher. :license: GNU GPL, see LICENSE for more details. """ from datetime import datetime from pocoo.context import Component from pocoo.utils.net import IP from pocoo.application import RequestWrapper from pocoo.settings import cfg from pocoo.pkg.core.user import User, check_login_data class AuthProvider(Component): @property def auth_name(self): """ has to return the name of the auth module for the configuration file. This name defaults to the classname. """ return self.__class__.__name__ def get_user(self, req): """ This method should either return a valid `User object`_ or ``None``. .. _User object: pocoo.pkg.core.user """ def get_user_id(self, session_dict): """ This method should either return the user_id of the user or ``None``. """ def do_login(self, req, username, password): """ This method should update the user session so that the auth provider can recognize the user in the ``get_user`` method. It has to return a valid ``HttpResponse``, for redirecting to external login scripts or ``False``, to display an error message (login failed). If it returns ``True`` pocoo will redirect to the last visited page. """ def do_logout(self, req): """ This method should return a valid ``Response`` for redirecting to external scripts or ``None``. """ class SessionAuth(AuthProvider): def get_user(self, req): try: user_id = req.session['user_id'] return User(self.ctx, user_id) except (KeyError, User.NotFound): return None def do_login(self, req, username, password): user_id = check_login_data(req.ctx, username, password) if user_id is not None: req.session['user_id'] = user_id return True return False def do_logout(self, req): if 'user_id' in req.session: req.session.pop('user_id') def get_user_id(self, session_dict): return session_dict.get('user_id') class AuthWrapper(RequestWrapper): def get_priority(self): # after SessionWrapper return 3 def process_request(self, req): # XXX: what to do with uid? uid = req.session.get('user_id', -1) req.auth = AuthController(req) req.user = req.auth.get_user() def process_response(self, req, resp): return resp def get_auth_provider_mapping(ctx): """Returns a list of auth providers.""" providers = {} for comp in ctx.get_components(AuthProvider): providers[comp.auth_name] = comp return providers def get_auth_provider(ctx): """Returns the enabled auth provider.""" if 'auth/provider' not in ctx._cache: providers = get_auth_provider_mapping(ctx) provider = providers[ctx.cfg.get('general', 'auth_module')] ctx._cache['auth/provider'] = provider return ctx._cache['auth/provider'] class AuthController(object): auth_provider = cfg.str('general', 'auth_module') def __init__(self, req): self.ctx = req.ctx self.req = req self.provider = get_auth_provider(req.ctx) def get_user(self): """ Returns the user for this request """ user = self.provider.get_user(self.req) if user is not None: user.ip = IP(self.req.environ['REMOTE_ADDR']) return user # return anonymous user return User(self.ctx, -1) def do_login(self, username, password): """ Returns a valid ``Response``, for redirecting to external login scripts or ``False``, to display an error message (login failed). If it returns ``True`` pocoo should redirect to the last visited page. """ rv = self.provider.do_login(self.req, username, password) if rv is not False: self.req.user = self.get_user() return rv return False def do_logout(self): """ Loggs the user out. Can eiter return None or a Response for external redirects. """ # update last login time self.req.user.last_login = datetime.now() self.req.user.save() self.provider.do_logout(self.req) #XXX: maybe a bit slow self.req.user = self.get_user() # -*- coding: utf-8 -*- """ pocoo.pkg.core.bbcode ~~~~~~~~~~~~~~~~~~~~~ Pocoo BBCode parser. :copyright: 2006-2007 by Georg Brandl, Armin Ronacher. :license: GNU GPL, see LICENSE for more details. """ import re from pocoo import Component from pocoo.pkg.core.textfmt import MarkupFormat from pocoo.pkg.core.smilies import get_smiley_buttons, replace_smilies from pocoo.utils.html import escape_html, translate_color from pocoo.utils.activecache import Node, CallbackNode, NodeList tag_re = re.compile(r'(\[(/?[a-zA-Z0-9]+)(?:=(".+?"|.+?))?\])') class EndOfText(Exception): """Raise when the end of the text is reached.""" class TokenList(list): """A subclass of a list for tokens which allows to flatten the tokens so that the original bbcode is the return value.""" def flatten(self): return u''.join(token.raw for token in self) def __repr__(self): return '<%s %s>' % ( self.__class__.__name__, list.__repr__(self) ) class Token(object): """Token Baseclass""" def __repr__(self): return '<%s %s>' % ( self.__class__.__name__, self.raw ) class TextToken(Token): """A token for plain text.""" def __init__(self, data): self.data = self.raw = data class TagToken(Token): """A token for tags.""" def __init__(self, raw, tagname, attr): self.raw = raw self.name = tagname self.attr = attr class Parser(object): """ BBCode Parser Class """ def __init__(self, ctx, text, handlers, allowed_tags): self.ctx = ctx self._tokens = tag_re.split(text) self._tokens.reverse() self._is_text = True self._cache = [] self._handlers = handlers self._allowed_tags = allowed_tags def tag_allowed(self, tagname): """ Check if a tagname is allowed for this parser. """ if self._allowed_tags is None: return True return tagname in self._allowed_tags def get_next_token(self): """ Fetch the next raw token from the text Raise ``EndOfText`` if not further token exists. """ if self._cache: return self._cache.pop() get_token = self._tokens.pop if not self._tokens: raise EndOfText() if self._is_text: self._is_text = False return TextToken(get_token()) else: self._is_text = True raw = get_token() tagname = get_token().lower() attr = get_token() if attr and attr[:6] == attr[-6:] == '"': attr = attr[6:-6] return TagToken(raw, tagname, attr) def push_token(self, token): """ Pushes the last fetched token in a cache so that the next time you call ``get_next_token`` returns the pushed token. """ self._cache.append(token) def parse(self, needle=None, preserve_needle=False): """ Parses the text until ``needle`` or the end of text if not defined. If it finds the needle it will delete the needle token. If you want the needle token too set ``preserve_needle`` to ``True``. In comparison with the ``get_tokens`` method this method will call the node handlers for each node. """ result = NodeList() try: while True: token = self.get_next_token() if isinstance(token, TagToken) and token.name == needle: if preserve_needle: self.push_token(token) break result.append(self.get_node(token)) except EndOfText: pass return result def get_tokens(self, needle=None, preserve_needle=False): """ Like ``parse`` but returns an unparsed TokenList. Basically you would never need this method except for preserved areas like Code blocks etc. """ result = TokenList() try: while True: token = self.get_next_token() if isinstance(token, TagToken) and token.name == needle: if preserve_needle: self.push_token(token) break result.append(token) except EndOfText: pass return result def get_node(self, token): """ Return the node for a token. If the token was a ``TextToken`` the resulting node will call ``get_text_node`` which returns a \n to <br/> replaced version of the token value wrapped in a plain ``Node``. In all other cases it will try to lookup the node in the list of registered token handlers. If this fails it wraps the raw token value in a ``Node``. """ if isinstance(token, TextToken): return self.get_text_node(token.data) if self.tag_allowed(token.name): for handler in self._handlers: rv = handler.get_node(token, self) if rv is not None: if isinstance(rv, Node): return rv return Node(rv) return self.get_text_node(token.raw) def get_text_node(self, data): """ Newline replaces the text and wraps it in an ``Node``. """ text = replace_smilies(self.ctx, data) return Node(re.sub(r'\r?\n', '<br />\n', text)) def wrap_render(self, tag, parse_until): """ Renders untile ``parse_until`` and wraps it in the html tag ``tag``. """ return NodeList(Node('<%s>' % tag), self.parse(parse_until), Node('</%s>' % tag)) def joined_render(self, *args): """ Takes a number of arguments which are either strings, unicode objects or nodes. It creates a new newlist, iterates over all arguments and converts all to nodes if not happened by now. """ result = NodeList() for arg in args: if isinstance(arg, Node): result.append(arg) else: result.append(Node(arg)) return result def callback(self, callback, data): """ Returns a new ``CallbackNode``. Don't create callback nodes on your own, this method might do some further magic in the future. """ return CallbackNode(callback, *data) class BBCodeTagProvider(Component): #: list of handled tags tags = [] #: list of callbacks callbacks = [] def get_node(self, token, parser): """ Is called when a tag is found. It must return a valid ``Node`` or a string which is automatically wrapped into a plain ``Node``. """ def render_callback(self, req, callback, data): """ Has to handle a callback for ``callback`` with ``data`` and return a string """ return u'' def get_buttons(self, req): """ Return a valid button definition for "tagname" or None if no button is required. A valid button definition is a dict in the following form:: {'name': _('Bold'), 'description': _('Insert bold text'), 'icon': self.ctx.make_url('!cobalt/...'), 'insert': '[b]{text}[/b]'} """ return () class BBCode(MarkupFormat): """ BBCode markup format. """ name = 'bbcode' editor_javascript = '!cobalt/core/pocoo/app/BBCodeEditor.js' def __init__(self, ctx): super(BBCode, self).__init__(ctx) self.handlers = {} self.callbacks = {} for comp in ctx.get_components(BBCodeTagProvider): for tag in comp.tags: self.handlers.setdefault(tag, []).append(comp) for callback in comp.callbacks: self.callbacks[callback] = comp def get_signature_tags(self): """Returns the allowed signature tags or None if all""" if not hasattr(self, '_signature_tags'): r = self.ctx.cfg.get('board', 'bbcode_signature_tags', 'ALL') if r == 'ALL': self._signature_tags = None else: self._signature_tags = [s.strip().lower() for s in r.split(',')] return self._signature_tags def parse(self, text, signature): handlers = self.ctx.get_components(BBCodeTagProvider) allowed_tags = None if signature: allowed_tags = self.get_signature_tags() p = Parser(self.ctx, escape_html(text), handlers, allowed_tags) return p.parse() def render_callback(self, req, callback, data): """Redirect the callback to the BBCode Provider.""" for comp in self.ctx.get_components(BBCodeTagProvider): rv = comp.render_callback(req, callback, data) if rv is not None: return rv raise Exception('unhandled callback %r' % callback) def quote_text(self, req, text, username=None): if username is None: return '[quote]%s[/quote]' % text return '[quote="%s"]%s[/quote]' % (username, text) def get_editor_options(self, req, signature): buttons = [] if signature: signature_tags = self.get_signature_tags() for comp in self.ctx.get_components(BBCodeTagProvider): for button in comp.get_buttons(req): if signature and button['tagname'] not in signature_tags: continue buttons.append(button) return { 'buttons': buttons, 'smilies': get_smiley_buttons(req.ctx) } class BasicBBCodeTagProvider(BBCodeTagProvider): tags = ['b', 'i', 'u', 's', 'url', 'email', 'color', 'size', 'code', 'quote', 'list'] callbacks = ['quote', 'list'] def get_node(self, token, parser): ctx = self.ctx if token.name == 'b': if token.attr: return return parser.wrap_render('strong', '/b') if token.name == 'i': if token.attr: return return parser.wrap_render('em', '/i') if token.name == 'u': if token.attr: return return parser.wrap_render('ins', '/u') if token.name == 's': if token.attr: return return parser.wrap_render('del', '/s') if token.name == 'url': if token.attr: content = parser.parse('/url') url = token.attr else: tokenlist = parser.get_tokens('/url') content = url = tokenlist.flatten() if url.startswith('javascript:'): url = url[11:] return parser.joined_render('<a href="', url, '">', content, '</a>') if token.name == 'email': if token.attr: content = parser.parse('/email') mail = token.attr else: tokenlist = parser.get_tokens('/email') mail = content = tokenlist.flatten() return parser.joined_render('<a href="mailto:"', mail, '">', content, '</a>') if token.name == 'color': content = parser.parse('/color') try: color = translate_color(token.attr) except ValueError: return token.raw return parser.joined_render('<span style="color: ', color, '">', content, '</span>') if token.name == 'size': content = parser.parse('/size') if not token.attr or not token.attr.isdigit() or len(token.attr) > 2: return token.raw return parser.joined_render('<span style="font-size: ', token.attr, 'px">', content, '</span>') if token.name == 'img': if token.attr: return tokenlist = parser.get_tokens('/img') url = tokenlist.flatten() if url.startswith('javascript:'): url = url[11:] return u'<img src="%s" />' % url if token.name == 'code': if token.attr: return return u'<pre>%s</pre>' % parser.get_tokens('/code').flatten() if token.name == 'quote': return parser.callback('quote', (token.attr or u'', parser.parse('/quote'))) if token.name == 'list': return parser.callback('list', (token.attr or u'*', parser.parse('/list'))) def render_callback(self, req, callback, data): if callback == 'quote': _ = req.gettext written, body = data if written: if not written.endswith(':'): written = (_('%s wrote') % written) + u':' written = u'<div class="written_by">%s</div>' % written return u'<blockquote>%s%s</blockquote>' % ( written, body.render(req, self) ) if callback == 'list': type, body = data lines = [] for line in re.split(r'^\s*\[\*\](?m)', body.render(req, self)): line = line.strip() if line: lines.append(u'<li>%s</li>' % line) return u'<ul>%s</ul>' % u'\n'.join(lines) def get_buttons(self, req): _ = req.gettext make_url = self.ctx.make_url #XXX: themeable icon_url = lambda x: make_url('!cobalt/core/default/img/bbcode/' + x) return [ {'tagname': 'b', 'name': _('Bold'), 'description': _('Insert bold text'), 'insert': '[b]{text}[/b]', 'icon': icon_url('bold.png')}, {'tagname': 'i', 'name': _('Italic'), 'description': _('Insert italic text'), 'insert': '[i]{text}[/i]', 'icon': icon_url('italic.png')}, {'tagname': 'u', 'name': _('Underline'), 'description': _('Insert underlined text'), 'insert': '[u]{text}[/u]', 'icon': icon_url('underline.png')}, {'tagname': 's', 'name': _('Strikethrough'), 'description': _('Insert striked text'), 'insert': '[i]{text}[/i]', 'icon': icon_url('strikethrough.png')}, {'tagname': 'size', 'name': _('Font Size'), 'description': _('Change the font size'), 'insert': '[size={attr}]{text}[/size]', 'values': [ (8, _('Tiny')), (11, _('Small')), (13, _('Normal')), (18, _('Big')), (24, _('Huge')) ]}, {'tagname': 'color', 'name': _('Font Color'), 'description': _('Change Font Color'), 'insert': '[color={attr}]{text}[/size]', 'values': [ ('black', _('Black')), ('blue', _('Blue')), ('brown', _('Brown')), ('cyan', _('Cyan')), ('gray', _('Gray')), ('green', _('Green')), ('magenta', _('Magenta')), ('purple', _('Purple')), ('red', _('Red')), ('white', _('White')), ('yellow', _('Yellow')) ]}, {'tagname': 'url', 'name': _('Link'), 'description': _('Create a Link'), 'icon': icon_url('link.png'), 'insert': '[url]{text}[/url]'}, {'tagname': 'img', 'name': _('Image'), 'description': _('Insert an image'), 'icon': icon_url('img.png'), 'insert': '[img]{text}[/img]'}, {'tagname': 'code', 'name': _('Code'), 'description': _('Insert a codeblock'), 'icon': icon_url('code.png'), 'insert': '[code]{text}[/code]'}, {'tagname': 'quote', 'name': _('Quote'), 'description': _('Insert a blockquote'), 'icon': icon_url('quote.png'), 'insert': '[quote]{text}[/quote]'} ] # -*- coding: utf-8 -*- """ pocoo.pkg.core.cache ~~~~~~~~~~~~~~~~~~~~ Provides a very simple caching system for persistent processes. :copyright: 2006-2007 by Armin Ronacher. :license: GNU GPL, see LICENSE for more details. """ from pocoo.application import RequestWrapper from pocoo.exceptions import PocooRuntimeError from pocoo.utils.cache import Cache # This is currently unused. class CacheSystem(RequestWrapper): def __init__(self, ctx): self.cache = Cache(autoprune=ctx.cfg.get('cache', 'autoprune', False)) self.uri2key = {} RequestWrapper.__init__(self, ctx) def get_priority(self): # caching has highest priority return 1 def process_request(self, req): req.cache_control = None req.cache = self.cache if req.environ['REQUEST_METHOD'] != 'GET': return if req.environ['REQUEST_URI'] not in self.uri2key: return key = self.uri2key[req.environ['REQUEST_URI']] return self.cache.fetch(key, None) def process_response(self, req, resp): if not req.cache_control: return resp action, key = req.cache_control if action == 'set': self.cache.dump(key, resp) self.uri2key[req.environ['REQUEST_URI']] = key elif action == 'update': if isinstance(key, basestring): self.cache.remove(key) else: for k in key: self.cache.remove(k) else: raise PocooRuntimeError('req.cache_control invalid')