##############################################################################
# 
# 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__='''Application support


$Id: Application.py,v 1.135.12.7 2001/03/01 15:22:03 brian Exp $'''
__version__='$Revision: 1.135.12.7 $'[11:-2]

import Globals,Folder,os,sys,App.Product, App.ProductRegistry, misc_
import time, traceback, os, string, Products
from string import strip, lower, find, rfind, join
from DateTime import DateTime
from AccessControl.User import UserFolder
from App.ApplicationManager import ApplicationManager
from webdav.NullResource import NullResource
from FindSupport import FindSupport
from urllib import quote
from cStringIO import StringIO
from AccessControl.PermissionRole import PermissionRole
from App.ProductContext import ProductContext
from misc_ import Misc_
import ZDOM
from zLOG import LOG, ERROR, WARNING, INFO
from HelpSys.HelpSys import HelpSys


_standard_error_msg='''\
<dtml-var standard_html_header>

<dtml-if error_message>
 <dtml-var error_message>
<dtml-else>

<TABLE BORDER="0" WIDTH="100%">
<TR VALIGN="TOP">

<TD WIDTH="10%" ALIGN="CENTER">
<IMG SRC="<dtml-var BASE1>/p_/ZButton" ALT="Zope">
</TD>

<TD WIDTH="90%">
  <H2>Zope Error</H2>
  <P>Zope has encountered an error while publishing this resource.
  </P>
  
  <P>
  <STRONG>Error Type: <dtml-var error_type html_quote></STRONG><BR>
  <STRONG>Error Value: <dtml-var error_value html_quote></STRONG><BR> 
  </P>
 
  <HR NOSHADE>
 
  <P>Troubleshooting Suggestions</P>

  <UL>
  <dtml-if "error_type in ('KeyError','NameError')">
  <LI>This resource may be trying to reference a
  nonexistent object or variable <STRONG><dtml-var error_value></STRONG>.</LI>
  </dtml-if>
  <LI>The URL may be incorrect.</LI>
  <LI>The parameters passed to this resource may be incorrect.</LI>
  <LI>A resource that this resource relies on may be encountering an error.</LI>
  </UL>

  <P>For more detailed information about the error, please
  refer to the HTML source for this page.
  </P>

  <P>If the error persists please contact the site maintainer.
  Thank you for your patience.
  </P>
</TD></TR>
</TABLE>

<dtml-comment>
 Here, events like logging and other actions may also be performed, such as
 sending mail automatically to the administrator.
</dtml-comment>

</dtml-if>
<dtml-var standard_html_footer>'''


class Application(Globals.ApplicationDefaultPermissions,
                  ZDOM.Root, Folder.Folder,
                  App.ProductRegistry.ProductRegistry, FindSupport):
    """Top-level system object"""
    title    ='Zope'
    #__roles__=['Manager', 'Anonymous']
    __defined_roles__=('Manager','Anonymous','Owner')
    web__form__method='GET'
    isTopLevelPrincipiaApplicationObject=1
    _isBeingUsedAsAMethod_=0

    # Create the help system object
    HelpSys=HelpSys('HelpSys')

    p_=misc_.p_
    misc_=misc_.misc_

    _reserved_names=('standard_html_header',
                     'standard_html_footer',
                     'standard_error_message',
                     'Control_Panel')

    # This class-default __allow_groups__ ensures that the
    # emergency user can still access the system if the top-level
    # UserFolder is deleted. This is necessary to allow people
    # to replace the top-level UserFolder object.
    
    __allow_groups__=UserFolder()

    def title_and_id(self): return self.title
    def title_or_id(self): return self.title

    def __init__(self):
        # Initialize users
        uf=UserFolder()
        self.__allow_groups__=uf
        self._setObject('acl_users', uf)

        # Initialize control panel
        cpl=ApplicationManager()
        cpl._init()
        self._setObject('Control_Panel', cpl)

        # Note that this may happen before products are
        # installed, so we have to use addDocument as stand-alone.
        import Document
        Document.manage_addDocument(
            self,
            'standard_html_header',
            'Standard Html Header',
            '<HTML><HEAD><TITLE><dtml-var title_or_id>' \
            '</TITLE></HEAD><BODY BGCOLOR="#FFFFFF">')
        Document.manage_addDocument(
            self,
            'standard_html_footer',
            'Standard Html Footer',
            '</BODY></HTML>')
        Document.manage_addDocument(
            self,
            'standard_error_message',
            'Standard Error Message',
            _standard_error_msg)

    def id(self):
        try:    return self.REQUEST['SCRIPT_NAME'][1:]
        except: return self.title

    def __class_init__(self): Globals.default__class_init__(self)

    def PrincipiaRedirect(self,destination,URL1):
        """Utility function to allow user-controlled redirects"""
        if find(destination,'//') >= 0: raise 'Redirect', destination
        raise 'Redirect', ("%s/%s" % (URL1, destination))
    Redirect=ZopeRedirect=PrincipiaRedirect

    def __bobo_traverse__(self, REQUEST, name=None):

        try: return getattr(self, name)
        except AttributeError: pass
        try: return self[name]
        except KeyError: pass
        method=REQUEST.get('REQUEST_METHOD', 'GET')
        if not method in ('GET', 'POST'):
            return NullResource(self, name, REQUEST).__of__(self)

        # Waaa. unrestrictedTraverse calls us with a fake REQUEST.
        # There is proabably a better fix for this.
        try: REQUEST.RESPONSE.notFoundError("%s\n%s" % (name, method))
        except AttributeError:
            raise KeyError, name

    def PrincipiaTime(self, *args):
        """Utility function to return current date/time"""
        return apply(DateTime, args)
    ZopeTime=PrincipiaTime

    ZopeAttributionButton__roles__=None
    def ZopeAttributionButton(self):
        """Returns an HTML fragment that displays the 'powered by zope'
        button along with a link to the Zope site."""
        return '<a href="http://www.zope.org/Credits" target="_top"><img ' \
               'src="%s/p_/ZopeButton" width="115" height="50" ' \
               'border="0" alt="Powered by Zope" /></a>' % self.REQUEST.BASE1


    def DELETE(self, REQUEST, RESPONSE):
        """Delete a resource object."""
        self.dav__init(REQUEST, RESPONSE)
        raise 'Forbidden', 'This resource cannot be deleted.'

    def MOVE(self, REQUEST, RESPONSE):
        """Move a resource to a new location."""
        self.dav__init(REQUEST, RESPONSE)
        raise 'Forbidden', 'This resource cannot be moved.'
    
    test_url___allow_groups__=None
    test_url=ZopeAttributionButton

    def absolute_url(self, relative=0):
        """Return an absolute url to the object. Note that the url
        will reflect the acquisition path of the object if the object
        has been acquired."""
        if relative: return ''
        return self.aq_acquire('REQUEST')['BASE1']

    def getPhysicalPath(self):
        '''Returns a path that can be used to access this object again
        later, for example in a copy/paste operation.  Designed to
        be used with getPhysicalRoot().
        '''
        # We're at the base of the path.
        return ('',)

    def getPhysicalRoot(self): return self

    fixupZClassDependencies__roles__=()
    def fixupZClassDependencies(self, rebuild=0):
        # Note that callers should not catch exceptions from this method
        # to ensure that the transaction gets aborted if the registry
        # cannot be rebuilt for some reason. Returns true if any ZClasses
        # were registered as a result of the call or the registry was
        # rebuilt.
        jar=self._p_jar
        result=0

        if rebuild:
            import BTree
            jar.root()['ZGlobals']=BTree.BTree()
            result=1

        zglobals =jar.root()['ZGlobals']
        reg_has_key=zglobals.has_key

        products=self.Control_Panel.Products
        for product in products.objectValues():
            items=list(product.objectItems())
            finished=[]
            idx=0
            while(idx < len(items)):
                name, ob = items[idx]
                base=getattr(ob, 'aq_base', ob)
                if base in finished:
                    idx=idx+1
                    continue
                finished.append(base)
                try:
                    # Try to re-register ZClasses if they need it.
                    if hasattr(ob, '_register') and hasattr(ob, '_zclass_'):
                        class_id=getattr(ob._zclass_, '__module__', None)
                        if class_id and not reg_has_key(class_id):
                            ob._register()
                            result=1
                            if not rebuild:
                                LOG('Zope', INFO,
                                    'Registered ZClass: %s' % ob.id
                                    )
                    # Include subobjects.
                    if hasattr(ob, 'objectItems'):
                        m = list(ob.objectItems())
                        items.extend(m)
                    # Try to find ZClasses-in-ZClasses.
                    if hasattr(ob, 'propertysheets'):
                        ps = ob.propertysheets
                        if (hasattr(ps, 'methods') and
                            hasattr(ps.methods, 'objectItems')):
                            m = list(ps.methods.objectItems())
                            items.extend(m)
                except:
                    LOG('Zope', WARNING,
                        'Broken objects exist in product %s.' % product.id)
                idx = idx + 1

        return result

    checkGlobalRegistry__roles__=()
    def checkGlobalRegistry(self):
        """Check the global (zclass) registry for problems, which can
        be caused by things like disk-based products being deleted.
        Return true if a problem is found"""
        try:    keys=list(self._p_jar.root()['ZGlobals'].keys())
        except: return 1
        return 0



class Expired(Globals.Persistent):
    icon='p_/broken'

    def __setstate__(self, s={}):
        dict=self.__dict__
        if s.has_key('id'):
            dict['id']=s['id']
        elif s.has_key('__name__'):
            dict['id']=s['__name__']
        else: dict['id']='Unknown'
        dict['title']='** Expired **'

    def __save__(self):
        pass

    __inform_commit__=__save__

def initialize(app):
    # Initialize the application

    # Initialize the cache:
    app.Control_Panel.initialize_cache()

    # The following items marked b/c are backward compatibility hacks
    # which make sure that expected system objects are added to the
    # bobobase. This is required because the bobobase in use may pre-
    # date the introduction of certain system objects such as those
    # which provide Lever support.

    # b/c: Ensure that Control Panel exists.
    if not hasattr(app, 'Control_Panel'):
        cpl=ApplicationManager()
        cpl._init()
        app._setObject('Control_Panel', cpl)
        get_transaction().note('Added Control_Panel')
        get_transaction().commit()

    # b/c: Ensure that a ProductFolder exists.
    if not hasattr(app.Control_Panel.aq_base, 'Products'):
        app.Control_Panel.Products=App.Product.ProductFolder()
        get_transaction().note('Added Control_Panel.Products')
        get_transaction().commit()

    # b/c: Ensure that std err msg exists.
    if not hasattr(app, 'standard_error_message'):
        import Document
        Document.manage_addDocument(
            app,
            'standard_error_message',
            'Standard Error Message',
            _standard_error_msg)
        get_transaction().note('Added standard_error_message')
        get_transaction().commit()

    # b/c: Ensure that Owner role exists.
    if hasattr(app, '__ac_roles__') and not ('Owner' in app.__ac_roles__):
        app.__ac_roles__=app.__ac_roles__ + ('Owner',)
        get_transaction().note('Added Owner role')
        get_transaction().commit()

    # Make sure we have Globals
    root=app._p_jar.root()
    if not root.has_key('ZGlobals'):
        import BTree
        app._p_jar.root()['ZGlobals']=BTree.BTree()
        get_transaction().note('Added Globals')
        get_transaction().commit()

    # Install the initial user.
    if hasattr(app, 'acl_users'):
        users = app.acl_users
        if hasattr(users, '_createInitialUser'):
            app.acl_users._createInitialUser()
            get_transaction().note('Created initial user')
            get_transaction().commit()

    install_products(app)

    # Note that the code from here on only runs if we are not a ZEO
    # client, or if we are a ZEO client and we've specified by way
    # of env variable that we want to force products to load.
    if (os.environ.get('ZEO_CLIENT') and
        not os.environ.get('FORCE_PRODUCT_LOAD')):
        return

    # Check for dangling pointers (broken zclass dependencies) in the
    # global class registry. If found, rebuild the registry. Note that
    # if the check finds problems but fails to successfully rebuild the 
    # registry we abort the transaction so that we don't leave it in an
    # indeterminate state.

    did_fixups=0
    bad_things=0
    try:
        if app.checkGlobalRegistry():
            app.fixupZClassDependencies(rebuild=1)
            did_fixups=1
            LOG('Zope', INFO,
                'A broken ZClass dependency was found in the global ' \
                'class registry. This is probably due to a product ' \
                'being uninstalled. The registry has successfully ' \
                'been rebuilt.')
            get_transaction().note('Rebuilt global product registry')
            get_transaction().commit()
    except:
        bad_things=1
        LOG('Zope', ERROR,
            'A problem was found in the global product registry but '
            'the attempt to rebuild the registry failed.',
            error=sys.exc_info())
        get_transaction().abort()

    # Now we need to see if any (disk-based) products were installed
    # during intialization. If so (and the registry has no errors),
    # there may still be zclasses dependent on a base class in the
    # newly installed product that were previously broken and need to
    # be fixed up. If any really Bad Things happened (dangling pointers
    # were found in the registry but it couldn't be rebuilt), we don't
    # try to do anything to avoid making the problem worse.
    if (not did_fixups) and (not bad_things):

        # App.Product.initializeProduct will set this if a disk-based
        # product was added or updated and we are not a ZEO client.
        if getattr(Globals, '__disk_product_installed__', 0):
            try:
                if app.fixupZClassDependencies():
                    get_transaction().commit()
            except:
                LOG('Zope', ERROR,
                    'Attempt to fixup ZClass dependencies after detecting ' \
                    'an updated disk-based product failed.',
                    error=sys.exc_info())
                get_transaction().abort()


def import_products(_st=type('')):
    # Try to import each product, checking for and catching errors.
    path_join=os.path.join
    isdir=os.path.isdir
    exists=os.path.exists
    DictType=type({})
    global_dict=globals()
    silly=('__doc__',)
    modules=sys.modules
    have_module=modules.has_key
    done={}

    for product_dir in Products.__path__:

        product_names=os.listdir(product_dir)
        product_names.sort()

        for product_name in product_names:

            if done.has_key(product_name): continue
            done[product_name]=1

            package_dir=path_join(product_dir, product_name)
            if not isdir(package_dir): continue
            if not exists(path_join(package_dir, '__init__.py')):
                if not exists(path_join(package_dir, '__init__.pyc')):
                    continue

            pname="Products.%s" % product_name
            try:
                product=__import__(pname, global_dict, global_dict, silly)
                if hasattr(product, '__module_aliases__'):
                    for k, v in product.__module_aliases__:
                        if not have_module(k):
                            if type(v) is _st and have_module(v): v=modules[v]
                            modules[k]=v
            except:
                LOG('Zope',ERROR,'Couldn\'t import %s' % pname,
                    error=sys.exc_info())
                f=StringIO()
                traceback.print_exc(100,f)
                f=f.getvalue()
                try: modules[pname].__import_error__=f
                except: pass


def install_products(app):
    # Install a list of products into the basic folder class, so
    # that all folders know about top-level objects, aka products

    path_join=os.path.join
    isdir=os.path.isdir
    exists=os.path.exists
    DictType=type({})

    from Folder import Folder
    folder_permissions={}
    for p in Folder.__ac_permissions__:
        permission, names = p[:2]
        folder_permissions[permission]=names

    meta_types=[]

    global_dict=globals()
    silly=('__doc__',)

    done={}

    get_transaction().note('Prior to product installs')
    get_transaction().commit()

    for product_dir in Products.__path__:

        product_names=os.listdir(product_dir)
        product_names.sort()

        for product_name in product_names:
            # For each product, we will import it and try to call the
            # intialize() method in the product __init__ module. If
            # the method doesnt exist, we put the old-style information
            # together and do a default initialization.
            if done.has_key(product_name):
                continue
            done[product_name]=1

            package_dir=path_join(product_dir, product_name)
            __traceback_info__=product_name
            if not isdir(package_dir): continue
            if not exists(path_join(package_dir, '__init__.py')):
                if not exists(path_join(package_dir, '__init__.pyc')):
                    continue
            try:
                product=__import__("Products.%s" % product_name,
                                   global_dict, global_dict, silly)

                # Install items into the misc_ namespace, used by products
                # and the framework itself to store common static resources
                # like icon images.
                misc_=pgetattr(product, 'misc_', {})
                if misc_:
                    if type(misc_) is DictType:
                        misc_=Misc_(product_name, misc_)
                    Application.misc_.__dict__[product_name]=misc_

                # Here we create a ProductContext object which contains
                # information about the product and provides an interface
                # for registering things like classes and help topics that
                # should be associated with that product. Products are
                # expected to implement a method named 'initialize' in
                # their __init__.py that takes the ProductContext as an
                # argument. 
                productObject=App.Product.initializeProduct(
                    product, product_name, package_dir, app)
                context=ProductContext(productObject, app, product)

                # Look for an 'initialize' method in the product. If it does
                # not exist, then this is an old product that has never been
                # updated. In that case, we will analyze the product and
                # build up enough information to do initialization manually.
                initmethod=pgetattr(product, 'initialize', None)
                if initmethod is not None:
                    initmethod(context)                    

                # Support old-style product metadata. Older products may
                # define attributes to name their permissions, meta_types,
                # constructors, etc.
                permissions={}
                new_permissions={}
                for p in pgetattr(product, '__ac_permissions__', ()):
                    permission, names, default = (
                        tuple(p)+('Manager',))[:3]
                    if names:
                        for name in names:
                            permissions[name]=permission
                    elif not folder_permissions.has_key(permission):
                        new_permissions[permission]=()

                for meta_type in pgetattr(product, 'meta_types', ()):
                    # Modern product initialization via a ProductContext
                    # adds 'product' and 'permission' keys to the meta_type
                    # mapping. We have to add these here for old products.
                    pname=permissions.get(meta_type['action'], None)
                    if pname is not None:
                        meta_type['permission']=pname
                    meta_type['product']=productObject.id
                    meta_types.append(meta_type)

                for name,method in pgetattr(
                    product, 'methods', {}).items():
                    if not hasattr(Folder, name):
                        setattr(Folder, name, method)
                        if name[-9:]!='__roles__': # not Just setting roles
                            if (permissions.has_key(name) and
                                not folder_permissions.has_key(
                                    permissions[name])):
                                permission=permissions[name]
                                if new_permissions.has_key(permission):
                                    new_permissions[permission].append(name)
                                else:
                                    new_permissions[permission]=[name]

                if new_permissions:
                    new_permissions=new_permissions.items()
                    for permission, names in new_permissions:
                        folder_permissions[permission]=names
                    new_permissions.sort()
                    Folder.__dict__['__ac_permissions__']=tuple(
                        list(Folder.__ac_permissions__)+new_permissions)

                if (os.environ.get('ZEO_CLIENT') and
                    not os.environ.get('FORCE_PRODUCT_LOAD')):
                    # we don't want to install products from clients
                    # (unless FORCE_PRODUCT_LOAD is defined).
                    get_transaction().abort()
                else:
                    get_transaction().note('Installed product '+product_name)
                    get_transaction().commit()

            except:
                LOG('Zope',ERROR,'Couldn\'t install %s' % product_name,
                    error=sys.exc_info())
                get_transaction().abort()

    Products.meta_types=Products.meta_types+tuple(meta_types)

    Globals.default__class_init__(Folder)


def pgetattr(product, name, default=install_products, __init__=0):
    if not __init__ and hasattr(product, name): return getattr(product, name)
    if hasattr(product, '__init__'):
        product=product.__init__
        if hasattr(product, name): return getattr(product, name)

    if default is not install_products: return default

    raise AttributeError, name
