# -*- coding: iso-8859-1 -*-
"""
    MoinMoin Access Control Lists

    @copyright: 2003 by Thomas Waldmann, http://linuxwiki.de/ThomasWaldmann
    @copyright: 2003 by Gustavo Niemeyer, http://moin.conectiva.com.br/GustavoNiemeyer
    @license: GNU GPL, see COPYING for details.
"""

import re
from MoinMoin import user

class AccessControlList:
    ''' Access Control List

    Control who may do what on or with a wiki page.

    Syntax of an ACL string:
    
        [+|-]User[,User,...]:[right[,right,...]] [[+|-]SomeGroup:...] ...
        ... [[+|-]Known:...] [[+|-]All:...]

        "User" is a user name and triggers only if the user matches. Up
        to version 1.2 only WikiNames were supported, as of version 1.3,
        any name can be used in acl lines, including name with spaces
        using esoteric languages.

        "SomeGroup" is a page name matching cfg.page_group_regex with
         some lines in the form " * Member", defining the group members.

        "Known" is a group containing all valid / known users.

        "All" is a group containing all users (Known and Anonymous users).

        "right" may be an arbitrary word like read, write, delete, admin.
        Only words in cfg.acl_validrights are accepted, others are
        ignored. It is allowed to specify no rights, which means that no
        rights are given.

    How ACL is processed

        When some user is trying to access some ACL-protected resource,
        the ACLs will be processed in the order they are found. The first
        matching ACL will tell if the user has access to that resource
        or not.

        For example, the following ACL tells that SomeUser is able to
        read and write the resources protected by that ACL, while any
        member of SomeGroup (besides SomeUser, if part of that group)
        may also admin that, and every other user is able to read it.

            SomeUser:read,write SomeGroup:read,write,admin All:read

        In this example, SomeUser can read and write but can not admin,
        revert or delete pages. Rights that are NOT specified on the
        right list are automatically set to NO.

    Using Prefixes
        
        To make the system more flexible, there are also two modifiers:
        the prefixes "+" and "-". 

            +SomeUser:read -OtherUser:write

        The acl line above will grant SomeUser read right, and OtherUser
        write right, but will NOT block automatically all other rights
        for these users. For example, if SomeUser ask to write, the
        above acl line does not define if he can or can not write. He
        will be able to write if acl_rights_before or acl_rights_after
        allow this (see configuration options).
        
        Using prefixes, this acl line:
        
            SomeUser:read,write SomeGroup:read,write,admin All:read

        Can be written as:
        
            -SomeUser:admin SomeGroup:read,write,admin All:read

        Or even:

            +All:read -SomeUser:admin SomeGroup:read,write,admin

        Notice that you probably will not want to use the second and
        third examples in ACL entries of some page. They are very
        useful on the moin configuration entries though.

   Configuration options
   
       cfg.acl_enabled
           If true will enable ACL support.
           Default: 0

       cfg.acl_rights_default
           It is is ONLY used when no other ACLs are given.
           Default: "Known:read,write,delete All:read,write",

       cfg.acl_rights_before
           When the page has ACL entries, this will be inserted BEFORE
           any page entries.
           Default: ""

       cfg.acl_rights_after
           When the page has ACL entries, this will be inserted AFTER
           any page entries.
           Default: ""
       
       cfg.acl_rights_valid
           These are the acceptable (known) rights (and the place to
           extend, if necessary).
           Default: ["read", "write", "delete", "admin"]
    '''

    special_users = ["All", "Known", "Trusted"]

    def __init__(self, request, lines=[]):
        """Initialize an ACL, starting from <nothing>.
        """
        self.setLines(request.cfg, lines)

    def setLines(self, cfg, lines=[]):
        self.clean()
        self.addBefore(cfg)
        if not lines:
            self.addDefault(cfg)
        else:
            for line in lines:
                self.addLine(cfg, line)
        self.addAfter(cfg)

    def clean(self):
        self.acl = [] # [ ('User', {"read": 0, ...}), ... ]
        self.acl_lines = []
        self._is_group = {}

    def addBefore(self, cfg):
        self.addLine(cfg, cfg.acl_rights_before, remember=0)
    def addDefault(self, cfg):
        self.addLine(cfg, cfg.acl_rights_default, remember=0)
    def addAfter(self, cfg):
        self.addLine(cfg, cfg.acl_rights_after, remember=0)

    def addLine(self, cfg, aclstring, remember=1):
        """ Add another ACL line

        This can be used in multiple subsequent calls to process longer
        lists.

        @param cfg: current config
        @param aclstring: acl string from page or cfg
        @param remember: should add the line to self.acl_lines
        """
        group_re = re.compile(cfg.page_group_regex)

        # Remember lines
        if remember:
            self.acl_lines.append(aclstring)

        # Iterate over entries and rights, parsed by acl string iterator
        acliter = ACLStringIterator(cfg.acl_rights_valid, aclstring)
        for modifier, entries, rights in acliter:
            if entries == ['Default']:
                self.addDefault(cfg)
                continue
            
            for entry in entries:
                if group_re.search(entry):
                    self._is_group[entry] = 1
                rightsdict = {}
                if modifier:
                    # Only user rights are added to the right dict.
                    # + add rights with value of 1
                    # - add right with value of 0
                    for right in rights:
                        rightsdict[right] = (modifier == '+')    
                else:
                    # All rights from acl_rights_valid are added to the
                    # dict, user rights with value of 1, and other with
                    # value of 0
                    for right in cfg.acl_rights_valid:
                        rightsdict[right] = (right in rights)
                self.acl.append((entry, rightsdict))

    def may(self, request, name, dowhat):
        """May <name> <dowhat>?
           Returns boolean answer.
        """
        if not request.cfg.acl_enabled:
            # everybody may read and write:
            if dowhat in ["read", "write",]:
                return 1
            # only known users may do some more dangerous things:
            if request.user.valid:
                if dowhat in ["delete", "revert",]:
                    return 1
            # in any other case, we better disallow it:
            return 0
        
        is_group_member = request.dicts.has_member

        allowed = None
        for entry, rightsdict in self.acl:
            if entry in self.special_users:
                handler = getattr(self, "_special_"+entry, None)
                allowed = handler(request, name, dowhat, rightsdict)
            elif self._is_group.get(entry) and is_group_member(entry, name):
                allowed = rightsdict.get(dowhat)
            elif entry == name:
                allowed = rightsdict.get(dowhat)
            if allowed is not None:
                return allowed
        return 0

    def getString(self, b='#acl ', e='\n'):
        """print the acl strings we were fed with"""
        return ''.join(["%s%s%s" % (b,l,e) for l in self.acl_lines])

    def _special_All(self, request, name, dowhat, rightsdict):
        return rightsdict.get(dowhat)

    def _special_Known(self, request, name, dowhat, rightsdict):
        """ check if user <name> is known to us,
            that means that there is a valid user account present.
            works for subscription emails.
        """
        if user.getUserId(request, name): # is a user with this name known?
            return rightsdict.get(dowhat)
        return None

    def _special_Trusted(self, request, name, dowhat, rightsdict):
        """ check if user <name> is known AND even has logged in using a password.
            does not work for subsription emails that should be sent to <user>,
            as he is not logged in in that case.
        """
        if request.user.trusted and name == request.user.name:
            return rightsdict.get(dowhat)
        return None

    def __eq__(self, other):
        return self.acl_lines == other.acl_lines
    def __ne__(self, other):
        return self.acl_lines != other.acl_lines


class ACLStringIterator:
    """ Iterator for acl string

    Parse acl string and return the next entry on each call to
    next. Implement the Iterator protocol.

    Usage:
        iter = ACLStringIterator(cfg.acl_rights_valid, 'user name:right')
        for modifier, entries, rights in iter:
            # process data
    """
    
    def __init__(self, rights, aclstring):
        """ Initialize acl iterator

        @param rights: the acl rights to consider when parsing
        @param aclstirng: string to parse
        """
        self.rights = rights
        self.rest = aclstring.strip()
        self.finished = 0

    def __iter__(self):
        """ Required by the Iterator protocol """
        return self
    
    def next(self):
        """ Return the next values from the acl string

        When the iterator is finished and you try to call next, it
        raises a StopIteration. The iterator finish as soon as the
        string is fully parsed or can not be parsed any more.

        @rtype: 3 tuple - (modifier, [entry, ...], [right, ...])
        @return: values for one item in an acl string
        """
        # Handle finished state, required by iterator protocol
        if self.rest == '':
            self.finished = 1
        if self.finished:
            raise StopIteration
        
        # Get optional modifier [+|-]entries:rights
        modifier = ''
        if self.rest[0] in ('+', '-'):
            modifier, self.rest = self.rest[0], self.rest[1:]

        # Handle the Default meta acl
        if self.rest.startswith('Default ') or self.rest == 'Default':
            self.rest = self.rest[8:]           
            entries, rights = ['Default'], []
            
        # Handle entries:rights pairs
        else:
            # Get entries
            try:
                # XXX TODO disallow : and , in usernames
                entries, self.rest = self.rest.split(':', 1)
            except ValueError:
                self.finished = 1
                raise StopIteration("Can't parse rest of string")
            if entries == '':
                entries = []
            else:
                # TODO strip each entry from blanks?
                entries = entries.split(',')            

            # Get rights
            try:         
                rights, self.rest = self.rest.split(' ', 1)
                # Remove extra white space after rights fragment,
                # allowing using multiple spaces between items.
                self.rest = self.rest.lstrip()
            except ValueError:
                rights, self.rest = self.rest, ''
            rights = [r for r in rights.split(',') if r in self.rights]

        return modifier, entries, rights


def parseACL(request, body):
    """ Parse acl lines on page and return ACL object

    Use ACL object may(request, dowhat) to get acl rights.
    """
    if not request.cfg.acl_enabled:
        return AccessControlList(request)

    acl_lines = []
    while body and body[0] == '#':
        # extract first line
        try:
            line, body = body.split('\n', 1)
        except ValueError:
            line = body
            body = ''

        # end parsing on empty (invalid) PI
        if line == "#":
            break

        # skip comments (lines with two hash marks)
        if line[1] == '#':
            continue

        tokens = line.split(None, 1)
        if tokens[0].lower() == "#acl":
            if len(tokens) == 2:
                args = tokens[1].rstrip()
            else:
                args = ""
            acl_lines.append(args)
    return AccessControlList(request, acl_lines)

