"""
$RCSfile: ExternalFile.py,v $

Implements ExternalFile class as a Zope product
ExternalFile encapsulates a file in the filesystem for
use as a Zope object.

Author: <a href="mailto:cstrong@arielpartners.com">Craeg Strong</a>
Release: 1.2
"""

__cvstag__  = '$Name:  $'[6:-2]
__date__    = '$Date: 2003/04/21 02:16:10 $'[6:-2]
__version__ = '$Revision: 1.80 $'[10:-2]

# Python builtins
from cStringIO import StringIO
import mimetypes, os, os.path, string

# Zope builtins
from Globals import MessageDialog
import Globals               # InitializeClass (security stuff)
import OFS                   # we grab his _property settings
from DateTime.DateTime import DateTime
from webdav.common import rfc1123_date
from AccessControl import ClassSecurityInfo
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate

# Add our base classes.  Derived classes will get this stuff automatically
from OFS.SimpleItem import SimpleItem  # The SimpleItem module is a Zope builtin that provides
                                       # cut/paste, management views, webdav/ftp, undo, ownership,
                                       # traversal, acquisition, and persistence
from Products.ZCatalog.CatalogAwareness import CatalogAware # Reqd for ZCatalog support
from OFS.PropertyManager import PropertyManager  # Reqd for proper property mgmt
from IExternalFile import IExternalFile

# Local modules
from FileUtils import copy_file
from ProductProperties import getMimeTypeList, getMimeTypeFromFile, getDisplayTypeFromMimeType

################################################################
# Permissions
################################################################
# By defining these constants, no new permissions will be created when
# misspelling them in the declareProtected() call

PERM_VIEW    = "View"
PERM_EDIT    = "Change External Files"
PERM_FTP     = "FTP access"
PERM_SEARCH  = "Search ZCatalog"
PERM_CONTENT = "Access contents information"

################################################################
# Container Methods
################################################################

def addInstance(folder, id, title='', description='',
                target_filepath='', basedir='',
                upload_file = None, REQUEST=None, RESPONSE=None):                
    """
    This is a convenience factory method for creating an instance of
    ExternalFile.  It returns the object created, and may therefore be
    more convenient than the manage_add() method it calls.  It is
    used by the unit testing programs.
    """
    folder.manage_addProduct['ExternalFile'].addExternalFile(
        id, title, description, target_filepath,
        basedir, upload_file)
    return folder[id]

################################################################
# Contructors
################################################################

# globals() is standard python for passing the current namespace
manage_addExternalFileForm = PageTemplateFile('www/create.pt', globals())

def addExternalFile(folder, id, title='', description='',
                    target_filepath='', basedir='',
                    upload_file = None):
    """

    Factory method to actually create an instance of ExternalFile.
    ExternalFile.  This method assumes all parameters are correct (it
    does no error checking).  It is called from CreationDialog.py once
    all of the confirmation and error checking steps have been taken.
    
    You should call this method directly if you are creating an
    instance of ExternalFile programmatically and have 'vetted' all of
    your parameters for correctness.

    """
    if not id:
        raise Exception('Required fields must not be blank')

    fully_resolved_target_filepath = os.path.join(basedir,target_filepath)    
    if upload_file:
        copy_file(upload_file, fully_resolved_target_filepath)        
    
    folder._setObject(id, ExternalFile(id, title, description,
                                     fully_resolved_target_filepath))
    folder._getOb(id).reindex_object()

################################################################
# ExternalFile class
################################################################

class ExternalFile(CatalogAware,    # ZCatalog support
                   PropertyManager, # Property support
                   SimpleItem):

    """
    ExternalFile encapsulates a file in the filesystem as a Zope object.
    """

    meta_type = 'External File' # This is the name Zope will use for
                                # the Product in the "addProduct" list

    __implements__ = IExternalFile

    behave_like_list    = [ 'DTMLDocument', 'DTMLMethod', 'File', 'Image', 'PageTemplate'  ]

    # This tuple defines our properties
    _properties = (
        {'id':'title',          'type':'string',    'mode': 'w'},
        {'id':'description',    'type':'string',    'mode': 'w'},
        {'id':'filepath',       'type':'string',    'mode': '' },
        {'id':'content_type',   'type':'selection', 'mode': 'w', 'select_variable':'getMimeTypes', },
        {'id':'behave_like',    'type':'selection', 'mode': 'w', 'select_variable':'behave_like_list', },
        )
    
    # This tuple defines a dictionary for each tab in the management interface
    # label = label of tab, action = url it links to
    manage_options = (
        {'label':'Edit', 'action': 'manage_editForm', 'help': ('ExternalFile', 'edit.stx') },
        {'label':'View', 'action': 'manage_view',     'help': ('ExternalFile', 'view.stx') },
        ) + OFS.PropertyManager.PropertyManager.manage_options + OFS.SimpleItem.SimpleItem.manage_options

    _security = ClassSecurityInfo()

    # set security for the object itself, e.g. if it is accessed in DTML code
    # This line is REQUIRED to allow access to External Files by the public (ariel DTDs, etc.)
    _security.declareObjectPublic()

    def __init__(self, id, title, description, filepath):
        """
        Initialize a new instance of ExternalFile
        """
        self.id          = id       # This is the OID translated by Zope into a URL
        self.title       = title
        self.description = description
        self.filepath    = filepath # This is the full pathname of the file
        self.__version__ = __version__

        self.content_type = getMimeTypeFromFile(filepath)
        
        #
        # By default, behave like a DTMLDocument, unless this is an
        # image, in which case, we need to behave like an Image, or
        # binary, in which case we need to behave like a File.
        #
        displaytype = getDisplayTypeFromMimeType(self.content_type)        
        if displaytype == 'image':
            self.behave_like = 'Image'
        elif displaytype == 'binary':
            self.behave_like = 'File'
        else:
            self.behave_like = 'DTMLDocument'

    # innocuous methods should be declared public
    _security.declarePublic('getMimeTypes')
    def getMimeTypes(self):
        "return list of available mime types."
        return getMimeTypeList()

    # innocuous methods should be declared public
    _security.declarePublic('getSelf')
    def getSelf(self):
        "Return this object. For use in DTML scripts"
        return self.aq_chain[0]
    
    ################################################################
    # Methods implementing the IExternalFile interface below
    ################################################################

    # protect these b/c a malicious hacker could get info about the filesystem
    _security.declareProtected(PERM_VIEW,  'getContentType')
    def getContentType(self):
        "Returns the mimetype of the file."
        return self.content_type

    # protect these b/c a malicious hacker could get info about the filesystem
    _security.declareProtected(PERM_VIEW,  'getDisplayType')
    def getDisplayType(self):
        """
        Returns 'ascii', 'image', or 'binary' according to the
        mime_type_map defaults to 'binary' if no match is found.
        """
        return getDisplayTypeFromMimeType(self.content_type)

    # protect these b/c a malicious hacker could get info about the filesystem
    _security.declareProtected(PERM_VIEW,  'getFilepath')
    def getFilepath(self, REQUEST):
        """
        Return the full pathname of the external file to which we are
        referring.  The REQUEST parameter encapsulates information
        about the environment which can be used by subclasses
        overriding this method.
        """
        return os.path.normpath(self.filepath)

    _security.declareProtected(PERM_VIEW, 'getFileModTime')
    def getFileModTime(self, REQUEST):
        """
        Return the last modification date of the external file to
        which we are referring.  The value is returned as number of
        seconds since the epoch in UTC (see python time module).  The
        REQUEST parameter encapsulates information about the
        environment which can be used by subclasses overriding this
        method.
        """
        path = self.getFilepath(REQUEST)
        return os.path.getmtime(path)

    _security.declareProtected(PERM_VIEW, 'getFileModTime')
    def getModTime(self, REQUEST):
        """
        Return the last modification date of the external file to
        which we are referring, or of our metadata which is stored
        internally in the ZODB, whichever is more recent (greater).
        The value is returned as number of seconds since the epoch in
        UTC (see python time module).  The REQUEST parameter
        encapsulates information about the environment which can be
        used by subclasses overriding this method.
        """
        tempTime = self.getFileModTime(REQUEST)
        if self._p_mtime > tempTime:
            return self._p_mtime
        else:
            return tempTime

    _security.declareProtected(PERM_VIEW, 'getContents')
    def getContents(self, REQUEST, RESPONSE=None):
        """
        Return the contents of this external file as a string.  The
        REQUEST parameter encapsulates information about the
        environment which can be used by subclasses overriding this
        method.  **The RESPONSE parameter is there for compatibility
        with other products such as ExternalEditor.**
        """
        #
        # if file does not exist, should we throw an exception?
        # right now, we return the error message as if it
        # were the contents of the file.
        #
        # We need to do something better! cks 10/31/2001
        #
        #print "getContents of:",self.getFilepath(REQUEST)
        try:
            data = StringIO()
            path = self.getFilepath(REQUEST)
            copy_file(path, data)
        except IOError, value:
            return value
        else:
            return data.getvalue()

    _security.declareProtected(PERM_EDIT, 'setContents')
    def setContents(self, newcontents, REQUEST=''):
        """
        Overwrite the previous contents of this file with the
        newcontents string.  The REQUEST parameter encapsulates
        information about the environment which can be used by
        subclasses overriding this method.
        """
      
        data = StringIO(newcontents)
        copy_file(data, self.getFilepath(REQUEST))

        # update ZCatalog
        self.reindex_object()

    ################################################################
    # Standard Zope stuff
    ################################################################

    # globals() is standard python for passing the current namespace
    _security.declareProtected(PERM_EDIT, 'manage_editForm')
    manage_editForm = PageTemplateFile('www/edit.pt', globals())    

    # globals() is standard python for passing the current namespace
    _security.declareProtected(PERM_VIEW, 'manage_view')
    manage_view = PageTemplateFile('www/view.pt',globals())

    # macros for read/only display of external file information
    _security.declareProtected(PERM_VIEW, 'fileinfo')    
    fileinfo = PageTemplateFile('www/fileinfo.pt', globals())

    _security.declarePublic('__len__')
    def __len__(self):
        """
        return length of object.  This is invoked by the Python
        builtin len(), which in turn is invoked by 'if foo:' where foo
        is an instance of this class.  Unless this method exists and
        returns a non-zero value, this locution will yield unexpected
        results.
        """
        if os.path.exists(self.filepath):
            return os.stat(self.filepath)[6]
        else:
            return 1 # non-zero to avoid unintended consequences

    _security.declarePublic('get_size')
    def get_size(self):
        """
        Return the length of the underlying file.  This method is
        REQUIRED or FTP won't work.
        """
        return len(self)

    _security.declareProtected(PERM_CONTENT, 'index_html')
    # next line is not strictly necessary, Access contents info is Anonymous by default
    # as opposed to all other protections that get Manager Role by default
    _security.setPermissionDefault(PERM_CONTENT, ('Anonymous')) 
    def index_html(self, REQUEST):
        """
        Render the document/method/Image/File/etc appropriately.  Note
        that this method is called for ExternalFile when it is
        emulating a DTMLMethod, even though a DTMLMethod itself never
        calls index_html.
        """
        return self.__call__(None, REQUEST)

    _security.declareProtected(PERM_CONTENT, '__str__')
    def __str__(self, REQUEST=None):
        "get contents as a string"
        return self.__call__(None, REQUEST)
        
    _security.declareProtected(PERM_CONTENT, '__call__')
    def __call__(self, client=None, REQUEST=None, **kw):
        """
        Render the document given a client object, REQUEST mapping,
        Response, and key word arguments.
        """
        if self.behave_like == 'File' or \
           self.behave_like == 'Image':
            if REQUEST:
                resp = REQUEST['RESPONSE']
                try:
                    resp.setHeader('Content-Type', self.content_type)
                    resp.setHeader('Last-Modified', self.getModTime(REQUEST))
                    resp.setHeader('Content-Length', self.get_size())
                    # not sure about the next one, but I believe that
                    # even though we support streaming, we can't provide
                    # ranges, and it is more compliant to say so
                    # cks 4/10/2003
                    resp.setHeader('Accept-Ranges', "none")
                    copy_file(self.getFilepath(REQUEST), resp)
                except IOError, value:
                    return value
                return
            else:
                try:
                    data = StringIO()
                    path = self.getFilepath(REQUEST)
                    copy_file(path, data)
                    return data.getvalue()                    
                except IOError, value:
                    return value

        elif self.behave_like == 'DTMLDocument':
            resultObj = OFS.DTMLDocument.DTMLDocument(
                self.getContents(REQUEST))

            resultObj.id           = self.id
            resultObj.title        = self.title
            resultObj.content_type = self.content_type
            if client is None:
                client = self
            return resultObj.__of__(client)(client, REQUEST)
        
        elif self.behave_like == 'DTMLMethod':
            resultObj = OFS.DTMLMethod.DTMLMethod(
                self.getContents(REQUEST))
            
            resultObj.id           = self.id
            resultObj.title        = self.title
            resultObj.content_type = self.content_type            

            if client is None:
                client = self
            return resultObj.__of__(client)(client, REQUEST)

        else: # PageTemplate
            resultObj = ZopePageTemplate(
                self.id,
                self.getContents(REQUEST),
                self.content_type)
            resultObj.pt_setTitle(self.title)
            
            if client is None:
                client = self
            return resultObj.__of__(client)(client, REQUEST)
            
    _size_changes = {
        # directive: (colDelta, rowDelta)
        'Narrower': (-5,  0),
        'Wider':    (5,   0),
        'Taller':   (0,   5),
        'Shorter':  (0,  -5),
        }

    _minCols = 40
    _minRows = 5

    _security.declareProtected(PERM_EDIT, 'manage_edit')
    def manage_edit(self,
                    title = '',
                    description = '',
                    externalfile_content = None,
                    SUBMIT = 'Cancel Changes',
                    edit_textarea_cols = '80',
                    edit_textarea_rows = '40',
                    upload_file = None,
                    REQUEST=None):
        """
        Called in response to edit.dtml form submit, to save changes,
        cancel changes, replace the contents of the file with uploaded
        contents, or change the size of the edit textarea form
        control.
        """
        if SUBMIT == 'Save Changes':
            self.title       = title
            self.description = description
            self.setContents(externalfile_content, REQUEST)
            message = 'Content changed. (%s)' % DateTime().ISO()
            return self.manage_editForm(manage_tabs_message = message)

        elif SUBMIT == 'Cancel Changes':
            message = 'Content changes cancelled.'
            return self.manage_editForm(externalfile_content = self.getContents(REQUEST),
                                        manage_tabs_message  = message)

        elif SUBMIT == 'Upload File':
            valid   = 0
            message = "Please specify a a non-empty file to upload."
            if upload_file and upload_file.filename:
                upload_contents = upload_file.read()
                if not upload_contents:
                    message = """Uploaded file %s is empty.
                    Please specify a non-empty file.\n""" % upload_file.filename
                else:
                    valid = 1;

            if valid:
                self.setContents(upload_contents, REQUEST)
                message = 'Content changed. (%s)' % DateTime().ISO()
                return self.manage_editForm(externalfile_content = self.getContents(REQUEST),
                                            manage_tabs_message  = message)
            else:
                message.replace('\n','<br/>')
                return MessageDialog(title   = 'ERROR',
                                     message = message,
                                     action  = 'manage_editForm')

        else: # change size of textarea form control 
            colDelta, rowDelta = self._size_changes[SUBMIT]
            cols=max(self._minCols, string.atoi(edit_textarea_cols)+colDelta)
            rows=max(self._minRows, string.atoi(edit_textarea_rows)+rowDelta)

            e=(DateTime('GMT') + 365).rfc822()

            resp = REQUEST['RESPONSE'] 
            resp.setCookie('edit_textarea_cols',str(cols),path='/',expires=e)
            resp.setCookie('edit_textarea_rows',str(rows),path='/',expires=e)
        
            return self.manage_editForm(externalfile_content = externalfile_content,
                                        edit_textarea_cols   = cols,
                                        edit_textarea_rows   = rows,
                                        manage_tabs_message  = 'Size changed.')
        
    # Saving the file back via FTP or webdav
    _security.declareProtected(PERM_EDIT, 'PUT')
    def PUT(self, REQUEST, RESPONSE):
        "Handle HTTP PUT requests"
        self.dav__init(REQUEST, RESPONSE)
        instream = REQUEST['BODYFILE']
        self.setContents(instream.read(), REQUEST)
        RESPONSE.setStatus(204)
        return RESPONSE

    #  FTP "get"
    _security.declareProtected(PERM_FTP, 'manage_FTPget')
    manage_FTPget = getContents

    _security.declareProtected(PERM_SEARCH, 'PrincipiaSearchSource')
    def PrincipiaSearchSource(self, REQUEST=None):
        "Support for full text search via ZCatalog indexing"
        if self.getDisplayType() == 'ascii':
            return self.getContents(REQUEST)
        else:
            return ""

    ################################################################
    # Support for Schema Migration
    ################################################################

    def repair(self):
        """

        Repair this object.  This method is used for schema migration,
        when the class definition changes and existing instances of
        previous versions must be updated.  See 'schema migration'
        discussion in README.txt

        """
        pass

# register security information
Globals.InitializeClass(ExternalFile)
