##############################################################################
# 
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
# 
# Copyright (c) Digital Creations.  All rights reserved.
# 
# This license has been certified as Open Source(tm).
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 
# 1. Redistributions in source code must retain the above copyright
#    notice, this list of conditions, and the following disclaimer.
# 
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions, and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
# 
# 3. Digital Creations requests that attribution be given to Zope
#    in any manner possible. Zope includes a "Powered by Zope"
#    button that is installed by default. While it is not a license
#    violation to remove this button, it is requested that the
#    attribution remain. A significant investment has been put
#    into Zope, and this effort will continue if the Zope community
#    continues to grow. This is one way to assure that growth.
# 
# 4. All advertising materials and documentation mentioning
#    features derived from or use of this software must display
#    the following acknowledgement:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    In the event that the product being advertised includes an
#    intact Zope distribution (with copyright and license included)
#    then this clause is waived.
# 
# 5. Names associated with Zope or Digital Creations must not be used to
#    endorse or promote products derived from this software without
#    prior written permission from Digital Creations.
# 
# 6. Modified redistributions of any form whatsoever must retain
#    the following acknowledgment:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    Intact (re-)distributions of any official Zope release do not
#    require an external acknowledgement.
# 
# 7. Modifications are encouraged but must be packaged separately as
#    patches to official Zope releases.  Distributions that do not
#    clearly separate the patches from the original work must be clearly
#    labeled as unofficial distributions.  Modifications which do not
#    carry the name Zope may be packaged in any form, as long as they
#    conform to all of the clauses above.
# 
# 
# Disclaimer
# 
#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
#   EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
#   PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
#   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
#   USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
#   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
#   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#   SUCH DAMAGE.
# 
# 
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations.  Specific
# attributions are listed in the accompanying credits file.
# 
##############################################################################
__doc__="""Copy interface"""
__version__='$Revision: 1.55 $'[11:-2]

import sys, string, Globals, Moniker, tempfile, ExtensionClass
from marshal import loads, dumps
from urllib import quote, unquote
from zlib import compress, decompress
from App.Dialogs import MessageDialog
from AccessControl import getSecurityManager


CopyError='Copy Error'

_marker=[]
class CopyContainer(ExtensionClass.Base):
    """Interface for containerish objects which allow cut/copy/paste"""

    __ac_permissions__=(
        ('View management screens',
         ('manage_cutObjects', 'manage_copyObjects', 'manage_pasteObjects',
          'manage_renameForm', 'manage_renameObjects', 'manage_renameObjects',)),
        )


    # The following three methods should be overridden to store sub-objects
    # as non-attributes.
    def _setOb(self, id, object): setattr(self, id, object)
    def _delOb(self, id): delattr(self, id)
    def _getOb(self, id, default=_marker):
        if hasattr(self, 'aq_base'): self=self.aq_base
        if default is _marker: return getattr(self, id)
        try: return getattr(self, id)
        except: return default


    def manage_CopyContainerFirstItem(self, REQUEST):
        return self._getOb(REQUEST['ids'][0])

    def manage_CopyContainerAllItems(self, REQUEST):
        return map(lambda i, s=self: s._getOb(i), tuple(REQUEST['ids']))

    def manage_cutObjects(self, ids, REQUEST=None):
        """Put a reference to the objects named in ids in the clip board"""
        if type(ids) is type(''):
            ids=[ids]
        oblist=[]
        for id in ids:
            ob=self._getOb(id)
            if not ob.cb_isMoveable():
                raise CopyError, eNotSupported % id
            m=Moniker.Moniker(ob)
            oblist.append(m.dump())
        cp=(1, oblist)
        cp=_cb_encode(cp)
        if REQUEST is not None:
            resp=REQUEST['RESPONSE']
            resp.setCookie('__cp', cp, path='%s' % REQUEST['SCRIPT_NAME'])
            return self.manage_main(self, REQUEST, cb_dataValid=1)
        return cp
    
    def manage_copyObjects(self, ids, REQUEST=None, RESPONSE=None):
        """Put a reference to the objects named in ids in the clip board"""
        if type(ids) is type(''):
            ids=[ids]
        oblist=[]
        for id in ids:
            ob=self._getOb(id)
            if not ob.cb_isCopyable():
                raise CopyError, eNotSupported % id
            m=Moniker.Moniker(ob)
            oblist.append(m.dump())
        cp=(0, oblist)
        cp=_cb_encode(cp)
        if REQUEST is not None:
            resp=REQUEST['RESPONSE']
            resp.setCookie('__cp', cp, path='%s' % REQUEST['SCRIPT_NAME'])
            return self.manage_main(self, REQUEST, cb_dataValid=1)
        return cp

    def _get_id(self, id):
        # Allow containers to override the generation of
        # object copy id by attempting to call its _get_id
        # method, if it exists.
        n=0
        if (len(id) > 8) and (id[8:]=='copy_of_'):
            n=1
        orig_id=id
        while 1:
            if self._getOb(id, None) is None:
                return id
            id='copy%s_of_%s' % (n and n+1 or '', orig_id)
            n=n+1

    def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
        """Paste previously copied objects into the current object.
           If calling manage_pasteObjects from python code, pass
           the result of a previous call to manage_cutObjects or
           manage_copyObjects as the first argument."""
        cp=None
        if cb_copy_data is not None:
            cp=cb_copy_data
        else:
            if REQUEST and REQUEST.has_key('__cp'):
                cp=REQUEST['__cp']
        if cp is None:
            raise CopyError, eNoData
        
        try:    cp=_cb_decode(cp)
        except: raise CopyError, eInvalid

        oblist=[]
        op=cp[0]
        app = self.getPhysicalRoot()

        for mdata in cp[1]:
            m = Moniker.loadMoniker(mdata)
            try: ob = m.bind(app)
            except: raise CopyError, eNotFound
            self._verifyObjectPaste(ob)
            oblist.append(ob)

        if op==0:
            # Copy operation
            for ob in oblist:
                if not ob.cb_isCopyable():
                    raise CopyError, eNotSupported % absattr(ob.id)
                try:    ob._notifyOfCopyTo(self, op=0)
                except: raise CopyError, MessageDialog(
                    title='Copy Error',
                    message=sys.exc_info()[1],
                    action ='manage_main')
                ob=ob._getCopy(self)
                ob.manage_afterClone(ob)
                id=self._get_id(absattr(ob.id))
                ob._setId(id)
                self._setObject(id, ob)

            if REQUEST is not None:
                return self.manage_main(self, REQUEST, update_menu=1,
                                        cb_dataValid=1)

        if op==1:
            # Move operation
            for ob in oblist:
                id=absattr(ob.id)
                if not ob.cb_isMoveable():
                    raise CopyError, eNotSupported % id
                try:    ob._notifyOfCopyTo(self, op=1)
                except: raise CopyError, MessageDialog(
                    title='Move Error',
                    message=sys.exc_info()[1],
                    action ='manage_main')
                if not sanity_check(self, ob):
                    raise CopyError, 'This object cannot be pasted into itself'

                # try to make ownership explicit so that it gets carried
                # along to the new location if needed.
                ob.manage_changeOwnershipType(explicit=1)
                
                ob.aq_parent._delObject(id)
                if hasattr(ob, 'aq_base'):
                    ob=ob.aq_base
                id=self._get_id(id)
                ob._setId(id)

                self._setObject(id, ob, set_owner=0)

                # try to make ownership implicit if possible
                ob=self._getOb(id)
                ob.manage_changeOwnershipType(explicit=0)

            if REQUEST is not None:
                REQUEST['RESPONSE'].setCookie('cp_', 'deleted',
                                    path='%s' % REQUEST['SCRIPT_NAME'],
                                    expires='Wed, 31-Dec-97 23:59:59 GMT')
                return self.manage_main(self, REQUEST, update_menu=1,
                                        cb_dataValid=0)
        return ''


    manage_renameForm=Globals.HTMLFile('renameForm', globals())

    def manage_renameObjects(self, ids, new_ids, REQUEST=None):
        """Rename several sub-objects"""
        if len(ids) != len(new_ids):
            raise 'BadRequst','Please rename each listed object.'
        for i in range(len(ids)):
            if ids[i] != new_ids[i]:
                self.manage_renameObject(ids[i], new_ids[i], REQUEST)
        if REQUEST is not None:
            return self.manage_main(self, REQUEST, update_menu=1)
        return None

    def manage_renameObject(self, id, new_id, REQUEST=None):
        """Rename a particular sub-object"""
        try: self._checkId(new_id)
        except: raise CopyError, MessageDialog(
                      title='Invalid Id',
                      message=sys.exc_info()[1],
                      action ='manage_main')
        ob=self._getOb(id)
        if not ob.cb_isMoveable():
            raise CopyError, eNotSupported % id            
        self._verifyObjectPaste(ob)
        try:    ob._notifyOfCopyTo(self, op=1)
        except: raise CopyError, MessageDialog(
                      title='Rename Error',
                      message=sys.exc_info()[1],
                      action ='manage_main')
        self._delObject(id)
        if hasattr(ob, 'aq_base'):
            ob=ob.aq_base
        ob._setId(new_id)
        
        # Note - because a rename always keeps the same context, we
        # can just leave the ownership info unchanged.
        self._setObject(new_id, ob, set_owner=0)

        if REQUEST is not None:
            return self.manage_main(self, REQUEST, update_menu=1)
        return None

    # Why did we give this a manage_ prefix if its really
    # supposed to be public since it does its own auth ?
    #
    # Because it's still a "management" function.
    manage_clone__roles__=None
    def manage_clone(self, ob, id, REQUEST=None):
        # Clone an object, creating a new object with the given id.
        if not ob.cb_isCopyable():
            raise CopyError, eNotSupported % absattr(ob.id)            
        try: self._checkId(id)
        except: raise CopyError, MessageDialog(
                      title='Invalid Id',
                      message=sys.exc_info()[1],
                      action ='manage_main')
        self._verifyObjectPaste(ob)
        try:    ob._notifyOfCopyTo(self, op=0)
        except: raise CopyError, MessageDialog(
                      title='Clone Error',
                      message=sys.exc_info()[1],
                      action ='manage_main')
        ob=ob._getCopy(self)
        ob._setId(id)
        self._setObject(id, ob)
        ob=ob.__of__(self)
        #ob._postCopy(self, op=0)
        return ob

    def cb_dataValid(self):
        # Return true if clipboard data seems valid.
        try:    cp=_cb_decode(self.REQUEST['__cp'])
        except: return 0
        return 1

    def cb_dataItems(self):
        # List of objects in the clip board
        try:    cp=_cb_decode(self.REQUEST['__cp'])
        except: return []
        oblist=[]

        app = self.getPhysicalRoot()
        for mdata in cp[1]:
            m = Moniker.loadMoniker(mdata)
            oblist.append(m.bind(app))
        return oblist

    validClipData=cb_dataValid

    def _verifyObjectPaste(self, object, validate_src=1):
        # Verify whether the current user is allowed to paste the
        # passed object into self. This is determined by checking
        # to see if the user could create a new object of the same
        # meta_type of the object passed in and checking that the
        # user actually is allowed to access the passed in object
        # in its existing context.
        #
        # Passing a false value for the validate_src argument will skip
        # checking the passed in object in its existing context. This is
        # mainly useful for situations where the passed in object has no 
        # existing context, such as checking an object during an import
        # (the object will not yet have been connected to the acquisition
        # heirarchy).
        if not hasattr(object, 'meta_type'):
            raise CopyError, MessageDialog(
                  title='Not Supported',
                  message='The object <EM>%s</EM> does not support this ' \
                          'operation' % absattr(object.id),
                  action='manage_main')
        mt=object.meta_type
        if not hasattr(self, 'all_meta_types'):
            raise CopyError, MessageDialog(
                  title='Not Supported',
                  message='Cannot paste into this object.',
                  action='manage_main')

        method_name=None
        meta_types=absattr(self.all_meta_types)
        for d in meta_types:
            if d['name']==mt:
                method_name=d['action']
                break

        if method_name is not None:
            meth=self.unrestrictedTraverse(method_name)
            if getSecurityManager().validateValue(meth):
                # Ensure the user is allowed to access the object on the
                # clipboard.
                if not validate_src:
                    return
                if getSecurityManager().validateValue(object):
                    return

        raise CopyError, MessageDialog(
              title='Not Supported',
              message='The object <EM>%s</EM> does not support this ' \
                      'operation' % absattr(object.id),
              action='manage_main')

Globals.default__class_init__(CopyContainer)



class CopySource:
    """Interface for objects which allow themselves to be copied."""
    
    def _canCopy(self, op=0):
        """Called to make sure this object is copyable. The op var
        is 0 for a copy, 1 for a move."""
        return 1

    def _notifyOfCopyTo(self, container, op=0):
        """Overide this to be pickly about where you go! If you dont
        want to go there, raise an exception. The op variable is
        0 for a copy, 1 for a move."""
        pass

    def _getCopy(self, container):
        # Ask an object for a new copy of itself.
        f=tempfile.TemporaryFile()
        self._p_jar.exportFile(self._p_oid,f)
        f.seek(0)
        ob=container._p_jar.importFile(f)
        f.close()
        return ob

    def _postCopy(self, container, op=0):
        # Called after the copy is finished to accomodate special cases.
        # The op var is 0 for a copy, 1 for a move.
        pass
    
    def _setId(self, id):
        # Called to set the new id of a copied object.
        self.id=id

    def cb_isCopyable(self):
        # Is object copyable? Returns 0 or 1
        if not (hasattr(self, '_canCopy') and self._canCopy(0)):
            return 0
        if hasattr(self, '_p_jar') and self._p_jar is None:
            return 0
        return 1

    def cb_isMoveable(self):
        # Is object moveable? Returns 0 or 1
        if not (hasattr(self, '_canCopy') and self._canCopy(1)):
            return 0
        if hasattr(self, '_p_jar') and self._p_jar is None:
            return 0
        try:    n=self.aq_parent._reserved_names
        except: n=()
        if absattr(self.id) in n:
            return 0
        return 1



def sanity_check(c, ob):
    # This is called on cut/paste operations to make sure that
    # an object is not cut and pasted into itself or one of its
    # subobjects, which is an undefined situation.
    ob=getattr(ob, 'aq_base', ob)
    while 1:
        if getattr(c, 'aq_base', c) is ob:
            return 0
        if not hasattr(c, 'aq_parent'):
            return 1
        c=c.aq_parent

def absattr(attr):
    if callable(attr): return attr()
    return attr

def _cb_encode(d):
    return quote(compress(dumps(d), 9))

def _cb_decode(s):
    return loads(decompress(unquote(s)))




fMessageDialog=Globals.HTML("""
<HTML>
<HEAD>
<TITLE><dtml-var title></TITLE>
</HEAD>
<BODY BGCOLOR="#FFFFFF">
<FORM ACTION="<dtml-var action>" METHOD="GET" <dtml-if
 target>TARGET="<dtml-var target>"</dtml-if>>
<TABLE BORDER="0" WIDTH="100%%" CELLPADDING="10">
<TR>
  <TD VALIGN="TOP">
  <BR>
  <CENTER><B><FONT SIZE="+6" COLOR="#77003B">!</FONT></B></CENTER>
  </TD>
  <TD VALIGN="TOP">
  <BR><BR>
  <CENTER>
  <dtml-var message>
  </CENTER>
  </TD>
</TR>
<TR>
  <TD VALIGN="TOP">
  </TD>
  <TD VALIGN="TOP">
  <CENTER>
  <INPUT TYPE="SUBMIT" VALUE="   Ok   ">
  </CENTER>
  </TD>
</TR>
</TABLE>
</FORM>
</BODY></HTML>""", target='', action='manage_main', title='Changed')


eNoData=MessageDialog(
        title='No Data',
        message='No clipboard data found.',
        action ='manage_main',)

eInvalid=MessageDialog(
         title='Clipboard Error',
         message='The data in the clipboard could not be read, possibly due ' \
         'to cookie data being truncated by your web browser. Try copying ' \
         'fewer objects.',
         action ='manage_main',)

eNotFound=MessageDialog(
          title='Item Not Found',
          message='One or more items referred to in the clipboard data was ' \
          'not found. The item may have been moved or deleted after you ' \
          'copied it.',
          action ='manage_main',)

eNotSupported=fMessageDialog(
              title='Not Supported',
              message='The item <EM>%s</EM> does not support this operation.',
              action ='manage_main',)
