##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""This module implements a simple item mix-in for objects that have a
very simple (e.g. one-screen) management interface, like documents,
Aqueduct database adapters, etc.

This module can also be used as a simple template for implementing new
item types.

$Id: SimpleItem.py 40478 2005-12-02 15:52:43Z chrisw $
"""

import warnings
import marshal, re, sys, time

import AccessControl.Role, AccessControl.Owned, App.Common
import Globals, App.Management, Acquisition, App.Undo
from AccessControl import getSecurityManager, Unauthorized
from AccessControl.ZopeSecurityPolicy import getRoles
from Acquisition import aq_base, aq_parent, aq_inner, aq_acquire
from ComputedAttribute import ComputedAttribute
from DocumentTemplate.html_quote import html_quote
from DocumentTemplate.ustr import ustr
from ExtensionClass import Base
from webdav.Resource import Resource
from zExceptions import Redirect
from zExceptions.ExceptionFormatter import format_exception
from zope.interface import implements

import ZDOM
from CopySupport import CopySource
from interfaces import IItem
from interfaces import IItemWithName
from interfaces import ISimpleItem
from Traversable import Traversable

HTML=Globals.HTML

import logging
logger = logging.getLogger()

class Item(Base, Resource, CopySource, App.Management.Tabs, Traversable,
           ZDOM.Element,
           AccessControl.Owned.Owned,
           App.Undo.UndoSupport,
           ):

    """A common base class for simple, non-container objects."""

    implements(IItem)

    isPrincipiaFolderish=0
    isTopLevelPrincipiaApplicationObject=0

    def manage_afterAdd(self, item, container):
        pass
    manage_afterAdd.__five_method__ = True

    def manage_beforeDelete(self, item, container):
        pass
    manage_beforeDelete.__five_method__ = True

    def manage_afterClone(self, item):
        pass
    manage_afterClone.__five_method__ = True

    # Direct use of the 'id' attribute is deprecated - use getId()
    id=''

    getId__roles__=None
    def getId(self):
        """Return the id of the object as a string.

        This method should be used in preference to accessing an id attribute
        of an object directly. The getId method is public.
        """
        name=getattr(self, 'id', None)
        if callable(name):
            return name()
        if name is not None:
            return name
        if hasattr(self, '__name__'):
            return self.__name__
        raise AttributeError, 'This object has no id'

    # Alias id to __name__, which will make tracebacks a good bit nicer:
    __name__=ComputedAttribute(lambda self: self.getId())

    # Name, relative to SOFTWARE_URL of icon used to display item
    # in folder listings.
    icon=''

    # Meta type used for selecting all objects of a given type.
    meta_type='simple item'

    # Default title.
    title=''

    # Default propertysheet info:
    __propsets__=()

    manage_options=(
        App.Undo.UndoSupport.manage_options
        +AccessControl.Owned.Owned.manage_options
        +({'label': 'Interfaces',
           'action': 'manage_interfaces'},)
        )

    # Attributes that must be acquired
    REQUEST=Acquisition.Acquired

    # Allow (reluctantly) access to unprotected attributes
    __allow_access_to_unprotected_subobjects__=1

    def title_or_id(self):
        """Return the title if it is not blank and the id otherwise.
        """
        title=self.title
        if callable(title):
            title=title()
        if title: return title
        return self.getId()

    def title_and_id(self):
        """Return the title if it is not blank and the id otherwise.

        If the title is not blank, then the id is included in parens.
        """
        title=self.title
        if callable(title):
            title=title()
        id = self.getId()
        return title and ("%s (%s)" % (title,id)) or id

    def this(self):
        # Handy way to talk to ourselves in document templates.
        return self

    def tpURL(self):
        # My URL as used by tree tag
        return self.getId()

    def tpValues(self):
        # My sub-objects as used by the tree tag
        return ()

    _manage_editedDialog=Globals.DTMLFile('dtml/editedDialog', globals())
    def manage_editedDialog(self, REQUEST, **args):
        return apply(self._manage_editedDialog,(self, REQUEST), args)

    def raise_standardErrorMessage(
        self, client=None, REQUEST={},
        error_type=None, error_value=None, tb=None,
        error_tb=None, error_message='',
        tagSearch=re.compile(r'[a-zA-Z]>').search,
        error_log_url=''):

        try:
            if error_type  is None: error_type =sys.exc_info()[0]
            if error_value is None: error_value=sys.exc_info()[1]

            # allow for a few different traceback options
            if tb is None and error_tb is None:
                tb=sys.exc_info()[2]
            if type(tb) is not type('') and (error_tb is None):
                error_tb = pretty_tb(error_type, error_value, tb)
            elif type(tb) is type('') and not error_tb:
                error_tb = tb

            # turn error_type into a string
            if hasattr(error_type, '__name__'):
                error_type=error_type.__name__

            if hasattr(self, '_v_eek'):
                # Stop if there is recursion.
                raise error_type, error_value, tb
            self._v_eek=1

            if str(error_type).lower() in ('redirect',):
                raise error_type, error_value, tb

            if not error_message:
                try:
                    s = ustr(error_value)
                except:
                    s = error_value
                try:
                    match = tagSearch(s)
                except TypeError:
                    match = None
                if match is not None:
                    error_message=error_value

            if client is None: client=self
            if not REQUEST: REQUEST=self.aq_acquire('REQUEST')

            try:
                if hasattr(client, 'standard_error_message'):
                    s=getattr(client, 'standard_error_message')
                else:
                    client = client.aq_parent
                    s=getattr(client, 'standard_error_message')
                kwargs = {'error_type': error_type,
                          'error_value': error_value,
                          'error_tb': error_tb,
                          'error_traceback': error_tb,
                          'error_message': error_message,
                          'error_log_url': error_log_url}

                if getattr(aq_base(s),'isDocTemp',0):
                    v = s(client, REQUEST, **kwargs)
                elif callable(s):
                    v = s(**kwargs)
                else:
                    v = HTML.__call__(s, client, REQUEST, **kwargs)
            except:
                logger.error(
                    'Exception while rendering an error message',
                    exc_info=True
                    )
                try:
                    strv = str(error_value)
                except:
                    strv = ('<unprintable %s object>' %
                            str(type(error_value).__name__))
                v = strv + (
                    (" (Also, the following error occurred while attempting "
                     "to render the standard error message, please see the "
                     "event log for full details: %s)")%(
                    html_quote(sys.exc_info()[1]),
                    ))
            raise error_type, v, tb
        finally:
            if hasattr(self, '_v_eek'): del self._v_eek
            tb=None

    def manage(self, URL1):
        """
        """
        raise Redirect, "%s/manage_main" % URL1

    # This keeps simple items from acquiring their parents
    # objectValues, etc., when used in simple tree tags.
    def objectValues(self, spec=None):
        return ()
    objectIds=objectItems=objectValues

    # FTP support methods

    def manage_FTPstat(self,REQUEST):
        """Psuedo stat, used by FTP for directory listings.
        """
        from AccessControl.User import nobody
        mode=0100000

        if (hasattr(aq_base(self),'manage_FTPget')):
            try:
                if getSecurityManager().validate(
                    None, self, 'manage_FTPget', self.manage_FTPget):
                    mode=mode | 0440
            except Unauthorized:
                pass

            if nobody.allowed(
                self.manage_FTPget,
                getRoles(self, 'manage_FTPget', self.manage_FTPget, ()),
                ):
                mode=mode | 0004

        # check write permissions
        if hasattr(aq_base(self),'PUT'):
            try:
                if getSecurityManager().validate(None, self, 'PUT', self.PUT):
                    mode=mode | 0220
            except Unauthorized:
                pass

            if nobody.allowed(
                self.PUT,
                getRoles(self, 'PUT', self.PUT, ()),
                ):
                mode=mode | 0002

        # get size
        if hasattr(aq_base(self), 'get_size'):
            size=self.get_size()
        elif hasattr(aq_base(self),'manage_FTPget'):
            size=len(self.manage_FTPget())
        else:
            size=0
        # get modification time
        if hasattr(aq_base(self), 'bobobase_modification_time'):
            mtime=self.bobobase_modification_time().timeTime()
        else:
            mtime=time.time()
        # get owner and group
        owner=group='Zope'
        if hasattr(aq_base(self), 'get_local_roles'):
            for user, roles in self.get_local_roles():
                if 'Owner' in roles:
                    owner=user
                    break
        return marshal.dumps((mode,0,0,1,owner,group,size,mtime,mtime,mtime))

    def manage_FTPlist(self,REQUEST):
        """Directory listing for FTP.

        In the case of non-Foldoid objects, the listing should contain one
        object, the object itself.
        """
        # check to see if we are being acquiring or not
        ob=self
        while 1:
            if App.Common.is_acquired(ob):
                raise ValueError('FTP List not supported on acquired objects')
            if not hasattr(ob,'aq_parent'):
                break
            ob=ob.aq_parent

        stat=marshal.loads(self.manage_FTPstat(REQUEST))
        id = self.getId()
        return marshal.dumps((id,stat))

    def __len__(self):
        return 1

    def __repr__(self):
        """Show the physical path of the object and its context if available.
        """
        try:
            path = '/'.join(self.getPhysicalPath())
        except:
            return Base.__repr__(self)
        context_path = None
        context = aq_parent(self)
        container = aq_parent(aq_inner(self))
        if aq_base(context) is not aq_base(container):
            try:
                context_path = '/'.join(context.getPhysicalPath())
            except:
                context_path = None
        res = '<%s' % self.__class__.__name__
        res += ' at %s' % path
        if context_path:
            res += ' used for %s' % context_path
        res += '>'
        return res

Globals.default__class_init__(Item)


class Item_w__name__(Item):

    """Mixin class to support common name/id functions"""

    implements(IItemWithName)

    def getId(self):
        """Return the id of the object as a string.
        """
        return self.__name__

    def title_or_id(self):
        """Return the title if it is not blank and the id otherwise.
        """
        return self.title or self.__name__

    def title_and_id(self):
        """Return the title if it is not blank and the id otherwise.

        If the title is not blank, then the id is included in parens.
        """
        t=self.title
        return t and ("%s (%s)" % (t,self.__name__)) or self.__name__

    def _setId(self, id):
        self.__name__=id

    def getPhysicalPath(self):
        """Get the physical path of the object.

        Returns a path (an immutable sequence of strings) that can be used to
        access this object again later, for example in a copy/paste operation.
        getPhysicalRoot() and getPhysicalPath() are designed to operate
        together.
        """
        path = (self.__name__,)

        p = aq_parent(aq_inner(self))
        if p is not None:
            path = p.getPhysicalPath() + path

        return path


def pretty_tb(t, v, tb, as_html=1):
    tb = format_exception(t, v, tb, as_html=as_html)
    tb = '\n'.join(tb)
    return tb


class SimpleItem(Item, Globals.Persistent,
                 Acquisition.Implicit,
                 AccessControl.Role.RoleManager,
                 ):

    # Blue-plate special, Zope Masala
    """Mix-in class combining the most common set of basic mix-ins
    """

    implements(ISimpleItem)

    manage_options=Item.manage_options+(
        {'label':'Security',
         'action':'manage_access',
         'help':('OFSP', 'Security.stx')},
        )

    __ac_permissions__=(('View', ()),)

