##############################################################################
#
# 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.
#
##############################################################################
"""Application support

$Id: Application.py 80016 2007-09-25 15:13:02Z tseaver $
"""

import os, sys, traceback
from logging import getLogger
from cgi import escape
from StringIO import StringIO
from warnings import warn

import Globals, Products, App.Product, App.ProductRegistry
import transaction
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl.User import UserFolder
from Acquisition import aq_base
from App.ApplicationManager import ApplicationManager
from App.config import getConfiguration
from App.Product import doInstall
from App.ProductContext import ProductContext
from DateTime import DateTime
from HelpSys.HelpSys import HelpSys
from webdav.NullResource import NullResource
from zExceptions import Redirect as RedirectException, Forbidden

from zope.interface import implements

import Folder
import misc_
import ZDOM
from FindSupport import FindSupport
from interfaces import IApplication
from misc_ import Misc_

LOG = getLogger('Application')

class Application(Globals.ApplicationDefaultPermissions,
                  ZDOM.Root, Folder.Folder,
                  App.ProductRegistry.ProductRegistry, FindSupport):

    """Top-level system object"""

    implements(IApplication)

    security = ClassSecurityInfo()

    title ='Zope'
    __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=('Control_Panel',
                     'browser_id_manager',
                     'temp_folder')

    # 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()

    # Set the universal default method to index_html
    _object_manager_browser_default_id = 'index_html'

    _initializer_registry = None

    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)
        transaction.get().note("Created Zope Application")

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

    def title_and_id(self):
        return self.title

    def title_or_id(self):
        return self.title

    def __class_init__(self):
        InitializeClass(self)

    def PrincipiaRedirect(self, destination, URL1):
        """Utility function to allow user-controlled redirects"""
        if destination.find('//') >= 0:
            raise RedirectException, destination
        raise RedirectException, ("%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

    security.declarePublic('ZopeAttributionButton')
    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>' % escape(self.REQUEST.BASE1, 1)


    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):
        """The absolute URL of the root object is BASE1 or "/".
        """
        if relative: return ''
        try:
            # Take advantage of computed URL cache
            return self.REQUEST['BASE1']
        except (AttributeError, KeyError):
            return '/'

    def absolute_url_path(self):
        """The absolute URL path of the root object is BASEPATH1 or "/".
        """
        try:
            return self.REQUEST['BASEPATH1'] or '/'
        except (AttributeError, KeyError):
            return '/'

    def virtual_url_path(self):
        """The virtual URL path of the root object is empty.
        """
        return ''

    def getPhysicalRoot(self):
        return self

    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.
        """
        # We're at the base of the path.
        return ('',)

    security.declarePrivate('fixupZClassDependencies')
    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:
            from BTrees.OOBTree import OOBTree
            jar.root()['ZGlobals'] = OOBTree()
            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_dict={}
            finished = finished_dict.has_key
            while items:
                name, ob = items.pop()
                base=aq_base(ob)
                if finished(id(base)):
                    continue
                finished_dict[id(base)] = None
                try:
                    # Try to re-register ZClasses if they need it.
                    if hasattr(base,'_register') and hasattr(base,'_zclass_'):
                        class_id=getattr(base._zclass_, '__module__', None)
                        if class_id and not reg_has_key(class_id):
                            ob._register()
                            result=1
                            if not rebuild:
                                LOG.info('Registered ZClass: %s' % ob.id)
                    # Include subobjects.
                    if hasattr(base, 'objectItems'):
                        m = list(ob.objectItems())
                        items.extend(m)
                    # Try to find ZClasses-in-ZClasses.
                    if hasattr(base, 'propertysheets'):
                        ps = ob.propertysheets
                        if (hasattr(ps, 'methods') and
                            hasattr(ps.methods, 'objectItems')):
                            m = list(ps.methods.objectItems())
                            items.extend(m)
                except:
                    LOG.warn('Broken objects exist in product %s.' % product.id,
                             exc_info=sys.exc_info())

        return result

    security.declarePrivate('checkGlobalRegistry')
    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:
            LOG.error(
                'A problem was found when checking the global product '\
                'registry.  This is probably due to a Product being '\
                'uninstalled or renamed.  The traceback follows.',
                exc_info=sys.exc_info())
            return 1
        return 0

    security.declarePrivate('_setInitializerFlag')
    def _setInitializerFlag(self, flag):
        if self._initializer_registry is None:
            self._initializer_registry = {}
        self._initializer_registry[flag] = 1

    security.declarePrivate('_getInitializerFlag')
    def _getInitializerFlag(self, flag):
        reg = self._initializer_registry
        if reg is None:
            reg = {}
        return reg.get(flag)

InitializeClass(Application)


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):
    initializer = AppInitializer(app)
    initializer.initialize()


class AppInitializer:

    """ Initialze an Application object (called at startup) """

    def __init__(self, app):
        self.app = (app,)

    def getApp(self):
        # this is probably necessary, but avoid acquisition anyway
        return self.app[0]

    def commit(self, note):
        transaction.get().note(note)
        transaction.commit()

    def initialize(self):
        app = self.getApp()
        # make sure to preserve relative ordering of calls below.
        self.install_cp_and_products()
        self.install_tempfolder_and_sdc()
        self.install_session_data_manager()
        self.install_browser_id_manager()
        self.install_required_roles()
        self.install_zglobals()
        self.install_inituser()
        self.install_errorlog()
        self.install_products()
        self.install_standards()
        self.install_virtual_hosting()
        self.check_zglobals()

    def install_cp_and_products(self):
        app = self.getApp()

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

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

    def install_tempfolder_and_sdc(self):
        app = self.getApp()
        from Products.ZODBMountPoint.MountedObject import manage_addMounts,\
             MountedObject
        from Products.ZODBMountPoint.MountedObject import getConfiguration as \
             getDBTabConfiguration

        dbtab_config = getDBTabConfiguration()

        tf = getattr(app, 'temp_folder', None)

        if getattr(tf, 'meta_type', None) == MountedObject.meta_type:
            # tf is a MountPoint object.  This means that the temp_folder
            # couldn't be mounted properly (the meta_type would have been
            # the meta type of the container class otherwise).  The
            # MountPoint object writes a message to zLOG so we don't
            # need to.
            return

        if tf is None:
            # do nothing if we've already installed one
            if not app._getInitializerFlag('temp_folder'):
                if dbtab_config is None:
                    # DefaultConfiguration, do nothing
                    return
                mount_paths = [ x[0] for x in dbtab_config.listMountPaths() ]
                if not '/temp_folder' in mount_paths:
                    # we won't be able to create the mount point properly
                    LOG.error('Could not initialze a Temporary Folder because '
                              'a database was not configured to be mounted at '
                              'the /temp_folder mount point')
                    return
                try:
                    manage_addMounts(app, ('/temp_folder',))
                    app._setInitializerFlag('temp_folder')
                    self.commit('Added temp_folder')
                    tf = app.temp_folder
                except:
                    LOG.error('Could not add a /temp_folder mount point due to an '
                              'error.', exc_info=sys.exc_info())
                    return

        # Ensure that there is a transient object container in the temp folder
        config = getConfiguration()

        if not hasattr(aq_base(tf), 'session_data'):
            from Products.Transience.Transience import TransientObjectContainer
            addnotify = getattr(config, 'session_add_notify_script_path', None)
            delnotify = getattr(config, 'session_delete_notify_script_path',
                                None)
            default_limit = 1000
            default_period_secs = 20
            default_timeout_mins = 20

            limit = getattr(config, 'maximum_number_of_session_objects',
                            default_limit)
            timeout_spec = getattr(config, 'session_timeout_minutes',
                                   default_timeout_mins)
            period_spec = getattr(config, 'session_resolution_seconds',
                                  default_period_secs)

            if addnotify and app.unrestrictedTraverse(addnotify, None) is None:
                LOG.warn('failed to use nonexistent "%s" script as '
                         'session-add-notify-script-path' % addnotify)
                addnotify=None

            if delnotify and app.unrestrictedTraverse(delnotify, None) is None:
                LOG.warn('failed to use nonexistent "%s" script as '
                         'session-delete-notify-script-path' % delnotify)
                delnotify=None

            if 1:  # Preserve indentation for diff
                toc = TransientObjectContainer('session_data',
                                               'Session Data Container',
                                               timeout_mins = timeout_spec,
                                               addNotification = addnotify,
                                               delNotification = delnotify,
                                               limit=limit,
                                               period_secs = period_spec)

            tf._setObject('session_data', toc)
            tf_reserved = getattr(tf, '_reserved_names', ())
            if 'session_data' not in tf_reserved:
                tf._reserved_names = tf_reserved + ('session_data',)
            self.commit('Added session_data to temp_folder')
            return tf # return the tempfolder object for test purposes

    def install_browser_id_manager(self):
        app = self.getApp()
        if app._getInitializerFlag('browser_id_manager'):
            # do nothing if we've already installed one
            return
        # Ensure that a browser ID manager exists
        if not hasattr(app, 'browser_id_manager'):
            from Products.Sessions.BrowserIdManager import BrowserIdManager
            bid = BrowserIdManager('browser_id_manager', 'Browser Id Manager')
            app._setObject('browser_id_manager', bid)
            # FIXME explicitely call manage_afterAdd, as sometimes
            # events are initialized too late
            browser_id_manager = app.browser_id_manager
            browser_id_manager.manage_afterAdd(browser_id_manager, app)
            app._setInitializerFlag('browser_id_manager')
            self.commit('Added browser_id_manager')

    def install_session_data_manager(self):
        app = self.getApp()
        if app._getInitializerFlag('session_data_manager'):
            # do nothing if we've already installed one
            return
        # Ensure that a session data manager exists
        if not hasattr(app, 'session_data_manager'):
            from Products.Sessions.SessionDataManager import SessionDataManager
            sdm = SessionDataManager('session_data_manager',
                title='Session Data Manager',
                path='/temp_folder/session_data',
                requestName='SESSION')
            app._setObject('session_data_manager', sdm)
            # FIXME explicitely call manage_afterAdd, as sometimes
            # events are initialized too late
            session_data_manager = app.session_data_manager
            session_data_manager.manage_afterAdd(session_data_manager, app)
            app._setInitializerFlag('session_data_manager')
            self.commit('Added session_data_manager')

    def install_required_roles(self):
        app = self.getApp()

        # Ensure that Owner role exists.
        if hasattr(app, '__ac_roles__') and not ('Owner' in app.__ac_roles__):
            app.__ac_roles__=app.__ac_roles__ + ('Owner',)
            self.commit('Added Owner role')

        # ensure the Authenticated role exists.
        if hasattr(app, '__ac_roles__'):
            if not 'Authenticated' in app.__ac_roles__:
                app.__ac_roles__=app.__ac_roles__ + ('Authenticated',)
                self.commit('Added Authenticated role')

    def install_zglobals(self):
        app = self.getApp()

        # Make sure we have ZGlobals
        root=app._p_jar.root()
        if not root.has_key('ZGlobals'):
            from BTrees.OOBTree import OOBTree
            root['ZGlobals'] = OOBTree()
            self.commit('Added ZGlobals')

    def install_inituser(self):
        app = self.getApp()

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

    def install_errorlog(self):
        app = self.getApp()
        if app._getInitializerFlag('error_log'):
            # do nothing if we've already installed one
            return

        # Install an error_log
        if not hasattr(app, 'error_log'):
            from Products.SiteErrorLog.SiteErrorLog import SiteErrorLog
            error_log = SiteErrorLog()
            app._setObject('error_log', error_log)
            # FIXME explicitely call manage_afterAdd, as sometimes
            # events are initialized too late
            error_log = app.error_log
            error_log.manage_afterAdd(error_log, app)
            app._setInitializerFlag('error_log')
            self.commit('Added site error_log at /error_log')

    def install_virtual_hosting(self):
        app = self.getApp()
        if app._getInitializerFlag('virtual_hosting'):
            return
        if not app.objectIds('Virtual Host Monster') and not hasattr(app, 'virtual_hosting'):
            from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster
            vhm=VirtualHostMonster()
            vhm.id='virtual_hosting'
            vhm.addToContainer(app)
            app._setInitializerFlag('virtual_hosting')
            self.commit('Added virtual_hosting')

    def check_zglobals(self):
        if not doInstall():
            return

        app = self.getApp()

        # 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():
                LOG.info(
                    'Beginning attempt to rebuild the global ZClass registry.')
                app.fixupZClassDependencies(rebuild=1)
                did_fixups=1
                LOG.info(
                    'The global ZClass registry has successfully been rebuilt.')
                transaction.get().note('Rebuilt global product registry')
                transaction.commit()
        except:
            bad_things=1
            LOG.error('The attempt to rebuild the registry failed.',exc_info=True)
               
            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__', None):
                try:
                    LOG.info('New disk product detected, determining if we need '
                             'to fix up any ZClasses.')
                    if app.fixupZClassDependencies():
                        LOG.info('Repaired broken ZClass dependencies.')
                        self.commit('Repaired broked ZClass dependencies')
                except:
                    LOG.error('Attempt to fixup ZClass dependencies after '
                              'detecting an updated disk-based product failed.',
                              exc_info=sys.exc_info())
                    transaction.abort()

    def install_products(self):
        app = self.getApp()
        # this defers to a function for b/c reasons
        return install_products(app)

    def install_standards(self):
        app = self.getApp()
        # this defers to a  function for b/c reasons
        return install_standards(app)

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

    folder_permissions = get_folder_permissions()
    meta_types=[]
    done={}

    debug_mode = App.config.getConfiguration().debug_mode

    transaction.get().note('Prior to product installs')
    transaction.commit()

    products = get_products()

    for priority, product_name, index, product_dir in products:
        # 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
        install_product(app, product_dir, product_name, meta_types,
                        folder_permissions, raise_exc=debug_mode)

    # Delayed install of packages-as-products
    for module, init_func in getattr(Products, '_packages_to_initialize', []):
        install_package(app, module, init_func, raise_exc=debug_mode)
    if hasattr(Products, '_packages_to_initialize'):
        del Products._packages_to_initialize

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

def get_products():
    """ Return a list of tuples in the form:
    [(priority, dir_name, index, base_dir), ...] for each Product directory
    found, sort before returning """
    products = []
    i = 0
    for product_dir in Products.__path__:
        product_names=os.listdir(product_dir)
        for name in product_names:
            fullpath = os.path.join(product_dir, name)
            # Products must be directories
            if os.path.isdir(fullpath):
                # Products must be directories with an __init__.py[co]
                if ( os.path.exists(os.path.join(fullpath, '__init__.py')) or
                     os.path.exists(os.path.join(fullpath, '__init__.pyo')) or
                     os.path.exists(os.path.join(fullpath, '__init__.pyc')) ):
                    # import PluginIndexes 1st (why?)
                    priority = (name != 'PluginIndexes')
                    # i is used as sort ordering in case a conflict exists
                    # between Product names.  Products will be found as
                    # per the ordering of Products.__path__
                    products.append((priority, name, i, product_dir))
        i = i + 1
    products.sort()
    return products

def import_products():
    # Try to import each product, checking for and catching errors.
    done={}

    products = get_products()
    debug_mode = App.config.getConfiguration().debug_mode

    for priority, product_name, index, product_dir in products:
        if done.has_key(product_name):
            LOG.warn('Duplicate Product name: '
                     'After loading Product %s from %s, '
                     'I skipped the one in %s.' % (
                    `product_name`, `done[product_name]`, `product_dir`) )
            continue
        done[product_name]=product_dir
        import_product(product_dir, product_name, raise_exc=debug_mode)
    return done.keys()

def import_product(product_dir, product_name, raise_exc=0, log_exc=1):
    path_join=os.path.join
    isdir=os.path.isdir
    exists=os.path.exists
    _st=type('')
    global_dict=globals()
    silly=('__doc__',)
    modules=sys.modules
    have_module=modules.has_key

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

        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:
            exc = sys.exc_info()
            if log_exc:
                LOG.error('Could not import %s' % pname,
                          exc_info=exc)
            f=StringIO()
            traceback.print_exc(100,f)
            f=f.getvalue()
            try: modules[pname].__import_error__=f
            except: pass
            if raise_exc:
                raise exc[0], exc[1], exc[2]
    finally:
        exc = None


def get_folder_permissions():
    folder_permissions={}
    for p in Folder.Folder.__ac_permissions__:
        permission, names = p[:2]
        folder_permissions[permission]=names
    return folder_permissions


def install_product(app, product_dir, product_name, meta_types,
                    folder_permissions, raise_exc=0, log_exc=1):

    path_join=os.path.join
    isdir=os.path.isdir
    exists=os.path.exists
    global_dict=globals()
    silly=('__doc__',)

    if 1:  # Preserve indentation for diff :-)
        package_dir=path_join(product_dir, product_name)
        __traceback_info__=product_name
        if not isdir(package_dir): return
        if not exists(path_join(package_dir, '__init__.py')):
            if not exists(path_join(package_dir, '__init__.pyc')):
                if not exists(path_join(package_dir, '__init__.pyo')):
                    return
        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 isinstance(misc_, dict):
                    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={}
            if pgetattr(product, '__ac_permissions__', None) is not None:
                warn('__init__.py of %s has a long deprecated '
                     '\'__ac_permissions__\' attribute. '
                     '\'__ac_permissions__\' will be ignored by '
                     'install_product in Zope 2.11. Please use registerClass '
                     'instead.' % product.__name__,
                     DeprecationWarning)
            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]=()

            if pgetattr(product, 'meta_types', None) is not None:
                warn('__init__.py of %s has a long deprecated \'meta_types\' '
                     'attribute. \'meta_types\' will be ignored by '
                     'install_product in Zope 2.11. Please use registerClass '
                     'instead.' % product.__name__,
                     DeprecationWarning)
            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_type['visibility'] = 'Global'
                meta_types.append(meta_type)

            if pgetattr(product, 'methods', None) is not None:
                warn("__init__.py of %s has a long deprecated 'methods' "
                     "attribute. 'methods' support might be removed in Zope "
                     "2.11 or a later feature release. Please use the "
                     "'legacy' argument of registerClass instead if the "
                     "methods are constructors. Or refactor the product "
                     "using adapters." % product.__name__,
                     DeprecationWarning)
            for name,method in pgetattr(
                product, 'methods', {}).items():
                if not hasattr(Folder.Folder, name):
                    setattr(Folder.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.Folder.__ac_permissions__=tuple(
                    list(Folder.Folder.__ac_permissions__)+new_permissions)

            if not doInstall():
                transaction.abort()
            else:
                transaction.get().note('Installed product '+product_name)
                transaction.commit()

        except:
            if log_exc:
                LOG.error('Couldn\'t install %s' % product_name,
                           exc_info=sys.exc_info())
            transaction.abort()
            if raise_exc:
                raise


def install_package(app, module, init_func, raise_exc=False, log_exc=True):
    """Installs a Python package like a product."""
    try:
        product = App.Product.initializeProduct(module,
                                                module.__name__,
                                                module.__path__[0],
                                                app)
        product.package_name = module.__name__
        if init_func is not None:
            newContext = ProductContext(product, app, module)
            init_func(newContext)

        if not doInstall():
            transaction.abort()
        else:
            transaction.get().note('Installed package %s' % module.__name__)
            transaction.commit()

    except:
        if log_exc:
            LOG.error("Couldn't install %s" % module.__name__,
                      exc_info=True)
        transaction.abort()
        if raise_exc:
            raise


def install_standards(app):
    # Check to see if we've already done this before
    # Don't do it twice (Casey)
    if getattr(app, '_standard_objects_have_been_added', 0):
        return

    # Install the replaceable standard objects
    from Products.PageTemplates.PageTemplateFile import PageTemplateFile
    std_dir = os.path.join(Globals.package_home(globals()), 'standard')
    wrote = 0
    for fn in os.listdir(std_dir):
        base, ext = os.path.splitext(fn)
        if ext == '.dtml':
            ob = Globals.DTMLFile(base, std_dir)
            fn = base
            if hasattr(app, fn):
                continue
            app.manage_addProduct['OFSP'].manage_addDTMLMethod(
                id=fn, file=open(ob.raw))
        elif ext in ('.pt', '.zpt'):
            ob = PageTemplateFile(fn, std_dir, __name__=fn)
            if hasattr(app, fn):
                continue
            app.manage_addProduct['PageTemplates'].manage_addPageTemplate(
                id=fn, title='', text=open(ob.filename))
        elif ext in ('.ico', '.gif', '.png'):
            if hasattr(app, fn):
                continue
            app.manage_addProduct['OFSP'].manage_addImage(
                id=fn, title='', file=open(os.path.join(std_dir, fn)))
        else:
            continue
        wrote = 1
        # Below is icky and sneaky since it makes these impossible to delete
        #ob.__replaceable__ = Globals.REPLACEABLE
        #setattr(Application, fn, ob)
    if wrote:
        app._standard_objects_have_been_added = 1
        transaction.get().note('Installed standard objects')
        transaction.commit()

def reinstall_product(app, product_name):
    folder_permissions = get_folder_permissions()
    meta_types=[]

    transaction.get().note('Prior to product reinstall')
    transaction.commit()

    for product_dir in Products.__path__:
        product_names=os.listdir(product_dir)
        product_names.sort()
        if product_name in product_names:
            removeProductMetaTypes(product_name)
            install_product(app, product_dir, product_name, meta_types,
                            folder_permissions, raise_exc=1, log_exc=0)
            break

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


def reimport_product(product_name):
    for product_dir in Products.__path__:
        product_names=os.listdir(product_dir)
        product_names.sort()
        if product_name in product_names:
            import_product(product_dir, product_name,
                           raise_exc=1, log_exc=0)
            break


def removeProductMetaTypes(pid):
    """Unregisters the meta types registered by a product.
    """
    meta_types = Products.meta_types
    new_mts = []
    changed = 0
    for meta_type in meta_types:
        if meta_type.get('product', None) == pid:
            # Remove this meta type.
            changed = 1
        else:
            new_mts.append(meta_type)
    if changed:
        Products.meta_types = tuple(new_mts)


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
