"""
BaseCacheRule is the base class for ContentCacheRule,
PolicyHTTPCacheManagerCacheRule and TemplateCacheRule.

$Id: base_cache_rule.py 64217 2008-05-03 03:31:12Z newbery $
"""

__authors__ = 'Geoff Davis <geoff@geoffdavis.net>'
__docformat__ = 'restructuredtext'

from urllib import quote

from zope.interface import implements
from zope.tales.tales import CompilerError

from Acquisition import aq_inner
from DateTime import DateTime
from AccessControl import ClassSecurityInfo
from AccessControl.User import nobody
from Products.PageTemplates.Expressions import getEngine, SecureModuleImporter
from ZopeUndo.Prefix import Prefix

from Products.Archetypes.interfaces import IBaseContent
from Products.Archetypes.atapi import BaseContent
from Products.Archetypes.atapi import DisplayList
from Products.Archetypes.atapi import registerType
from Products.Archetypes.atapi import Schema

from Products.Archetypes.atapi import IntegerField
from Products.Archetypes.atapi import LinesField
from Products.Archetypes.atapi import StringField

from Products.Archetypes.atapi import IntegerWidget
from Products.Archetypes.atapi import LinesWidget
from Products.Archetypes.atapi import MultiSelectionWidget
from Products.Archetypes.atapi import SelectionWidget
from Products.Archetypes.atapi import StringWidget

from Products.CMFCore import Expression
from Products.CMFCore.utils import getToolByName
from Products.CMFCore import permissions

from Products.CMFPlone.interfaces import IBrowserDefault

from Products.CacheSetup.utils import base_hasattr
from Products.CacheSetup.config import *
from Products.CacheSetup.interfaces import ICacheRule
from nocatalog import NoCatalog

schema = BaseContent.schema.copy()

header_set_schema = Schema((

    LinesField(
        'cacheStop',
        default=('portal_status_message','statusmessages'),
        write_permission = permissions.ManagePortal,
        widget=LinesWidget(
            label='Cache Preventing Request Values',
            description='Values in the request that prevent caching if present')),

    StringField(
        'predicateExpression',
        required=0,
        edit_accessor='getPredicateExpression',
        write_permission = permissions.ManagePortal,
        widget=StringWidget(
            label='Predicate',
            size=80,
            description='A TALES expression for determining whether this rule applies. '
                        'Available variables = request, object, view (ID of the current '
                        'template), member (None if anonymous).')),

    StringField(
        'headerSetIdAnon',
        default='cache-with-etag',
        vocabulary='getHeaderSetVocabulary',
        enforce_vocabulary = 1,
        write_permission = permissions.ManagePortal,
        widget=SelectionWidget(
            label='Header Set for Anonymous Users',
            description='Header set for anonymous users.')),

    StringField(
        'headerSetIdAuth',
        default='cache-with-etag',
        vocabulary='getHeaderSetVocabulary',
        enforce_vocabulary = 1,
        write_permission = permissions.ManagePortal,
        widget=SelectionWidget(
            label='Header Set for Authenticated Users',
            description='Header set for authenticated users.')),

    StringField(
        'headerSetIdExpression',
        required=0,
        edit_accessor='getHeaderSetIdExpression',
        write_permission = permissions.ManagePortal,
        widget=StringWidget(
            label='Header Set Expression',
            size=80,
            description='A TALES expression that returns the ID of the header set '
                        'to be used.  Applied if the header set specified above is '
                        'set to "Expression".  Available variables = request, object, '
                        'view (ID of the current template), member (None if anonymous)')),

    StringField(
        'lastModifiedExpression',
        default='python:object.modified()',
        edit_accessor='getLastModifiedExpression',
        write_permission = permissions.ManagePortal,
        widget=StringWidget(
            label='Last-Modified Expression',
            size=80,
            description='An expression used to obtain the last-modified time for '
                        'the page.  Used in setting the Last-Modified header')),

    StringField(
        'varyExpression',
        required=0,
        default='python: rule.portal_cache_settings.getVaryHeader()',
        edit_accessor='getVaryExpression',
        write_permission = permissions.ManagePortal,
        widget=StringWidget(
            label='Vary Expression',
            size=80,
            description='A TALES expression that will be used to generate the Vary '
                        'header.  Available values: request, object, view '
                        '(the template ID), member (None if Anonymous)')),
    ))

etag_schema = Schema((

    LinesField(
        'etagComponents',
        default=('member','skin','language','gzip','catalog_modified'),
        multiValued=1,
        enforce_vocabulary = 1,
        write_permission = permissions.ManagePortal,
        vocabulary=DisplayList((
            ('member','Current member\'s ID'),
            ('roles','Current member\'s roles'),
            ('permissions','Current member\'s permissions'),
            ('skin','Current skin'),
            ('language','Browser\'s preferred language'),
            ('user_language','User\'s preferred language'),
            ('gzip','Browser can receive gzipped content'),
            ('last_modified','Context modification time'),
            ('catalog_modified', 'Time of last catalog change'))),
        widget=MultiSelectionWidget(
            label='ETag Components',
            format='checkbox',
            description='Items used to construct the ETag')),

    LinesField(
        'etagRequestValues',
        default=(),
        write_permission = permissions.ManagePortal,
        widget=LinesWidget(
            label='ETag Request Values',
            description='Request values used to construct the ETag')),

    IntegerField(
        'etagTimeout',
        required=0,
        default=3600,
        write_permission = permissions.ManagePortal,
        widget=IntegerWidget(
            label='ETag Timeout',
            description='Maximum amount of time an ETag is valid (leave blank for forever)')),

    StringField(
        'etagExpression',
        required=0,
        edit_accessor='getEtagExpression',
        write_permission = permissions.ManagePortal,
        widget=StringWidget(
            label='ETag Expression',
            size=80,
            description='A TALES expression that will be appended to the ETag '
                        'generated with the above settings when "The expression '
                        'below" is selected.  Available values: request, object, '
                        'view (the template ID), member (None if Anonymous)')),
    ))

class BaseCacheRule(NoCatalog, BaseContent):
    """
    """
    __implements__ = ICacheRule, BaseContent.__implements__
    implements(IBaseContent)

    security = ClassSecurityInfo()
    archetype_name = 'Base Cache Rule'
    portal_type = meta_type = 'BaseCacheRule'
    schema = schema
    global_allow = 0
    _at_rename_after_creation = True

    def _validate_expression(self, expression):
        try:
            getEngine().compile(expression)
        except CompilerError, e:
            return 'Bad expression:', str(e)
        except:
            raise

    def getPredicateExpression(self):
        expression = self.getField('predicateExpression').get(self)
        if expression:
            return expression.text
        
    def setPredicateExpression(self, expression):
        if expression is None:
            expression = ''
        return self.getField('predicateExpression').set(self, Expression.Expression(expression))

    def validate_predicateExpression(self, expression):
        return self._validate_expression(expression)

    def testPredicate(self, expr_context):
        expression = self.getField('predicateExpression').get(self)
        if expression:
            if not expression.text:  # empty expression
                return True
            return expression(expr_context)

    def getEtagExpression(self):
        expression = self.getField('etagExpression').get(self)
        if expression:
            return expression.text
        
    def setEtagExpression(self, expression):
        if expression is None:
            expression = ''
        return self.getField('etagExpression').set(self, Expression.Expression(expression))

    def validate_etagExpression(self, expression):
        return self._validate_expression(expression)

    def getEtagExpressionValue(self, expr_context):
        expression = self.getField('etagExpression').get(self)
        if expression:
            return expression(expr_context)

    def getHeaderSetIdExpression(self):
        expression = self.getField('headerSetIdExpression').get(self)
        if expression:
            return expression.text
        
    def setHeaderSetIdExpression(self, expression):
        if expression is None:
            expression = ''
        return self.getField('headerSetIdExpression').set(self, Expression.Expression(expression))

    def validate_headerSetIdExpression(self, expression):
        return self._validate_expression(expression)

    def getHeaderSetIdExpressionValue(self, expr_context):
        expression = self.getField('headerSetIdExpression').get(self)
        if expression:
            return expression(expr_context)

    def getLastModifiedExpression(self):
        expression = self.getField('lastModifiedExpression').get(self)
        if expression:
            return expression.text
        
    def setLastModifiedExpression(self, expression):
        if expression is None:
            expression = ''
        return self.getField('lastModifiedExpression').set(self, Expression.Expression(expression))

    def validate_lastModifiedExpression(self, expression):
        return self._validate_expression(expression)

    def getLastModified(self, expr_context):
        expression = self.getField('lastModifiedExpression').get(self)
        if expression:
            return expression(expr_context)

    def lastDate(self, *dates):
        if len(dates) == 0:
            return self.portal_cache_settings.getChangeDate()
        dates = list(dates)
        timeout = self.getEtagTimeout()
        if timeout:
            time = DateTime()
            time = timeout * (int(time.timeTime()/timeout) - 1)
            time = DateTime(time)
            dates.append(time)
        dates.sort()
        return dates[-1]

    def getLastTransactionDate(self, context=None):
        spec = {}
        if context is None:
            context = getToolByName(self, 'portal_url').getPortalObject()
        path = '/'.join(context.getPhysicalPath())
        spec['description'] = Prefix(path)
        lastTransaction = self._p_jar.db().undoInfo(0, 1, spec)
        if len(lastTransaction) == 0:
            return DateTime(self.Control_Panel.process_start)
        return DateTime(lastTransaction[0]['time'])

    def getVaryExpression(self):
        expression = self.getField('varyExpression').get(self)
        if expression:
            return expression.text
        
    def setVaryExpression(self, expression):
        if expression is None:
            expression = ''
        return self.getField('varyExpression').set(self, Expression.Expression(expression))

    def validate_varyExpression(self, expression):
        return self._validate_expression(expression)

    def getVary(self, expr_context):
        expression = self.getField('varyExpression').get(self)
        if expression:
            return expression(expr_context)

    def _getExpressionContext(self, request, object, view, member, keywords=(), time=None):
        """Construct an expression context for TALES expressions used in cache rules and header sets"""
        if time is None:
            time = DateTime()
        data = {'rule'     : self,
                'request'  : request,
                'object'   : object,
                'view'     : view,
                'member'   : member,
                'time'     : time,
                'keywords' : keywords,
                'modules'  : SecureModuleImporter,
                'nothing'  : None
               }
        return getEngine().getContext(data)

    def _associateTemplate(self, object, template_id):
        try:
            template = object.unrestrictedTraverse(template_id)
        except (AttributeError, KeyError):
            template = None 
            # XXX should log the fact that this template can't be found
        if template is not None:
            manager_id = getattr(template, 'ZCacheable_getManagerId', None)
            if manager_id is not None:
                if manager_id() is None:
                    template.ZCacheable_setManagerId(PAGE_CACHE_MANAGER_ID)

    def _getHeaderSet(self, request, object, view, member):
        stop_items = self.getCacheStop()
        if stop_items:
            for item in stop_items:
                if request.get(item, None) is not None:
                    return None
                
        expr_context = self._getExpressionContext(request, object, view, member)
        if not self.testPredicate(expr_context):
            return None
                
        if member is None:
            header_set_id = self.getHeaderSetIdAnon()
        else:
            header_set_id = self.getHeaderSetIdAuth()
        if header_set_id == 'expression':
            header_set_id = self.getHeaderSetIdExpressionValue(expr_context)
        if header_set_id == 'None':
            header_set_id = None
        if header_set_id:
            pcs = getToolByName(self, CACHE_TOOL_ID)
            return pcs.getHeaderSetById(header_set_id)

    def getHeaderSetVocabulary(self):
        pcs = getToolByName(self, CACHE_TOOL_ID)
        display_id = pcs.getDisplayPolicy().getId()
        headers = pcs.getHeaderSets(display_id)
        vocabulary = [('expression', 'Use expression below')] + \
                     [(hs.getId(), hs.Title()) for hs in headers.objectValues()] + \
                     [('None', 'Rule does not apply')]
        return DisplayList(tuple(vocabulary))
        
    def getObjectDefaultView(self, obj):
        """Get the id of an object's default view"""
        context = aq_inner(obj)
        browserDefault = IBrowserDefault(context, None)
        
        if browserDefault is not None:
            try:
                return browserDefault.defaultView()
            except AttributeError:
                # Might happen if FTI didn't migrate yet.
                pass

        fti = context.getTypeInfo()
        try:
            # XXX: This isn't quite right since it assumes the action starts with ${object_url}
            action = fti.getActionInfo('object/view')['url'].split('/')[-1]
        except ValueError:
            # If the action doesn't exist, stop
            return None

        # Try resolving method aliases because we need a real template_id here
        if action:
            action = fti.queryMethodID(action, default = action, context = context)
        else:
            action = fti.queryMethodID('(Default)', default = action, context = context)

        # Strip off leading / and/or @@
        if action and action[0] == '/':
            action = action[1:]
        if action and action.startswith('@@'):
            action = action[2:]
        return action
        
    def _addEtagComponent(self, etag, component):
        etag += '|' + str(component).replace(',',';')  # commas are used to separate etags in if-none-match headers
        return etag

    security.declarePublic('getEtag')
    def getEtag(self, request, object, view, member, time=None):
        # note: member may come in as None if the member is anonymous
        portal = getToolByName(self, 'portal_url').getPortalObject()
        pcs = getattr(portal, CACHE_TOOL_ID)
        etag = ''
        values = self.getEtagComponents()
        if 'member' in values:
            if member is not None:
                username = member.getUserName()
            else:
                username = ''
            etag = self._addEtagComponent(etag, username)
        if 'roles' in values or 'permissions' in values:
            m = member
            if m is None:
                m = portal.portal_membership.wrapUser(nobody)
            roles = list(m.getRolesInContext(object))
            roles.sort()
            etag = self._addEtagComponent(etag, ';'.join(roles))
            if 'permissions' in values:
                etag = self._addEtagComponent(etag, pcs.getPermissionCount())
        if 'skin' in values:
            try:
                skin_name = self.getCurrentSkinName()
            except AttributeError:
                stool = getToolByName(self, 'portal_skins')
                skin_name = self.getSkinNameFromRequest(request)
                
                if skin_name is None:
                    # Use default skin
                    skin_name = stool.getDefaultSkin()

            etag = self._addEtagComponent(etag, skin_name)
        if 'language' in values:
            etag = self._addEtagComponent(etag, request.get('HTTP_ACCEPT_LANGUAGE', ''))
        if 'user_language' in values:
            ltool = getToolByName(self, 'portal_languages', None)
            if ltool is None:
                ptool = getToolByName(self, 'portal_properties')
                lang = ptool.site_properties.default_language
            else:
                lang = ltool.getPreferredLanguage()
            etag = self._addEtagComponent(etag, lang)
        if 'gzip' in values:
            (enable_compression, force, gzip_capable) = pcs.isGzippable(0, 0, request)
            etag = self._addEtagComponent(etag, int(force or (enable_compression and gzip_capable)))
        if 'last_modified' in values:
            etag = self._addEtagComponent(etag, object.modified().timeTime())
        if 'catalog_modified' in values:
            etag = self._addEtagComponent(etag, pcs.getCatalogCount())
        if self.getEtagExpression():
            expr_context = self._getExpressionContext(request, object, view, member)
            etag = self._addEtagComponent(etag, self.getEtagExpressionValue(expr_context))
            
        marker = []
        req_values = self.getEtagRequestValues()
        if req_values:
            for rv in req_values:
                v = request.get(rv, marker)
                if v is not marker:
                    etag = self._addEtagComponent(etag, quote(str(v)))
                else:
                    etag = self._addEtagComponent(etag, '')
                    
        timeout = self.getEtagTimeout()
        if timeout:
            if time is None:
                time = DateTime()
            etag = self._addEtagComponent(etag, int(time.timeTime()/timeout))
        return etag

    security.declarePublic('getRelativeUrlsToPurge')
    def getRelativeUrlsToPurge(self, object, urls):
        return urls

registerType(BaseCacheRule, PROJECT_NAME)

__all__ = (
    'header_set_schema',
    'etag_schema',
    'BaseCacheRule',
)
