<?php
// +---------------------------------------------------------------------+
// | PHP Version 4                                                       |
// +---------------------------------------------------------------------+
// | Copyright (c) 1997, 1998, 1999, 2000, 2001 The PHP Group            |
// +---------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license,      |
// | that is bundled with this package in the file LICENSE, and is       |
// | available at through the world-wide-web at                          |
// | http://www.php.net/license/2_02.txt.                                |
// | If you did not receive a copy of the PHP license and are unable to  |
// | obtain it through the world-wide-web, please send a note to         |
// | license@php.net so we can mail you a copy immediately.              |
// +---------------------------------------------------------------------+
// | Author: Bertrand Mansion <bmansion@mamasam.com>                     |
// +---------------------------------------------------------------------+
//
// $Id: Container.php,v 1.2 2004/09/08 23:40:22 rossta Exp $

require_once('Config.php');

/**
* Interface for Config containers
*
* @author   Bertrand Mansion <bmansion@mamasam.com>
* @package  Config
*/
class Config_Container {

    
/**
    * Container object type
    * Ex: section, directive, comment, blank
    * @var  string
    */
    
var $type;

    
/**
    * Container object name
    * @var  string
    */
    
var $name '';

    
/**
    * Container object content
    * @var  string
    */
    
var $content '';

    
/**
    * Container object children
    * @var  array
    */
    
var $children = array();

    
/**
    * Reference to container object's parent
    * @var  object
    */
    
var $parent;
    
    
/**
    * Array of attributes for this item
    * @var  array
    */
    
var $attributes;

    
/**
    * Unique id to differenciate nodes
    *
    * This is used to compare nodes
    * Will not be needed anymore when this class will use ZendEngine 2
    *
    * @var  int
    */
    
var $_id;

    
/**
    * Constructor
    *
    * @param  string  $type       Type of container object
    * @param  string  $name       Name of container object
    * @param  string  $content    Content of container object
    * @param  array   $attributes Array of attributes for container object
    */
    
function Config_Container($type 'section'$name ''$content ''$attributes null)
    {
        
$this->type       $type;
        
$this->name       $name;
        
$this->content    $content;
        
$this->attributes $attributes;
        
$this->parent     null;
        
$this->_id        uniqid($name.$typetrue);
    } 
// end constructor

    /**
    * Create a child for this item.
    * @param  string  $type       type of item: directive, section, comment, blank...
    * @param  mixed   $item       item name
    * @param  string  $content    item content
    * @param  array   $attributes item attributes
    * @param  string  $where      choose a position 'bottom', 'top', 'after', 'before'
    * @param  object  $target     needed if you choose 'before' or 'after' for where
    * @return object  reference to new item or Pear_Error
    */
    
function &createItem($type$name$content$attributes null$where 'bottom'$target null)
    {
        
$item =& new Config_Container($type$name$content$attributes);
        
$result =& $this->addItem($item$where$target);
        return 
$result;
    } 
// end func &createItem
    
    /**
    * Adds an item to this item.
    * @param  object   $item      a container object
    * @param  string   $where     choose a position 'bottom', 'top', 'after', 'before'
    * @param  object   $target    needed if you choose 'before' or 'after' in $where
    * @return mixed    reference to added container on success, Pear_Error on error
    */
    
function &addItem(&$item$where 'bottom'$target null)
    {
        if (
$this->type != 'section') {
            return 
PEAR::raiseError('Config_Container::addItem must be called on a section type object.'nullPEAR_ERROR_RETURN);
        }
        if (
is_null($target)) {
            
$target =& $this;
        }
        if (
strtolower(get_class($target)) != 'config_container') {
            return 
PEAR::raiseError('Target must be a Config_Container object in Config_Container::addItem.'nullPEAR_ERROR_RETURN);
        }

        switch (
$where) {
            case 
'before':
                
$index $target->getItemIndex();
                break;
            case 
'after':
                
$index $target->getItemIndex()+1;
                break;
            case 
'top':
                
$index 0;
                break;
            case 
'bottom':
                
$index = -1;
                break;
            default:
                return 
PEAR::raiseError('Use only top, bottom, before or after in Config_Container::addItem.'nullPEAR_ERROR_RETURN);
        }
        if (isset(
$index) && $index >= 0) {
            
array_splice($this->children$index0'tmp');
        } else {
            
$index count($this->children);
        }
        
$this->children[$index] =& $item;
        
$this->children[$index]->parent =& $this;

        return 
$item;
    } 
// end func addItem

    /**
    * Adds a comment to this item.
    * This is a helper method that calls createItem
    *
    * @param  string    $content        Object content
    * @param  string    $where          Position : 'top', 'bottom', 'before', 'after'
    * @param  object    $target         Needed when $where is 'before' or 'after'
    * @return object  reference to new item or Pear_Error
    */
    
function &createComment($content ''$where 'bottom'$target null)
    {
        return 
$this->createItem('comment'null$contentnull$where$target);
    } 
// end func &createComment

    /**
    * Adds a blank line to this item.
    * This is a helper method that calls createItem
    *
    * @return object  reference to new item or Pear_Error
    */
    
function &createBlank($where 'bottom'$target null)
    {
        return 
$this->createItem('blank'nullnullnull$where$target);
    } 
// end func &createBlank

    /**
    * Adds a directive to this item.
    * This is a helper method that calls createItem
    *
    * @param  string    $name           Name of new directive
    * @param  string    $content        Content of new directive
    * @param  mixed     $attributes     Directive attributes
    * @param  string    $where          Position : 'top', 'bottom', 'before', 'after'
    * @param  object    $target         Needed when $where is 'before' or 'after'
    * @return object  reference to new item or Pear_Error
    */
    
function &createDirective($name$content$attributes null$where 'bottom'$target null)
    {
        return 
$this->createItem('directive'$name$content$attributes$where$target);
    } 
// end func &createDirective

    /**
    * Adds a section to this item.
    *
    * This is a helper method that calls createItem
    * If the section already exists, it won't create a new one. 
    * It will return reference to existing item.
    *
    * @param  string    $name           Name of new section
    * @param  array     $attributes     Section attributes
    * @param  string    $where          Position : 'top', 'bottom', 'before', 'after'
    * @param  object    $target         Needed when $where is 'before' or 'after'
    * @return object  reference to new item or Pear_Error
    */
    
function &createSection($name$attributes null$where 'bottom'$target null)
    {
        return 
$this->createItem('section'$namenull$attributes$where$target);
    } 
// end func &createSection

    /**
    * Tries to find the specified item(s) and returns the objects.
    *
    * Examples:
    * $directives =& $obj->getItem('directive');
    * $directive_bar_4 =& $obj->getItem('directive', 'bar', null, 4);
    * $section_foo =& $obj->getItem('section', 'foo');
    *
    * This method can only be called on an object of type 'section'.
    * Note that root is a section.
    * This method is not recursive and tries to keep the current structure.
    * For a deeper search, use searchPath()
    *
    * @param  string    $type        Type of item: directive, section, comment, blank...
    * @param  mixed     $name        Item name
    * @param  mixed     $content     Find item with this content
    * @param  array     $attributes  Find item with attribute set to the given value
    * @param  int       $index       Index of the item in the returned object list. If it is not set, will try to return the last item with this name.
    * @return mixed  reference to item found or false when not found
    * @see &searchPath()
    */
    
function &getItem($type null$name null$content null$attributes null$index = -1)
    {
        if (
$this->type != 'section') {
            return 
PEAR::raiseError('Config_Container::getItem must be called on a section type object.'nullPEAR_ERROR_RETURN);
        }
        if (!
is_null($type)) {
            
$testFields[] = 'type';
        }
        if (!
is_null($name)) {
            
$testFields[] = 'name';
        }
        if (!
is_null($content)) {
            
$testFields[] = 'content';
        }
        if (!
is_null($attributes) && is_array($attributes)) {
            
$testFields[] = 'attributes';
        }

        
$itemsArr = array();
        
$fieldsToMatch count($testFields);
        for (
$i 0$count count($this->children); $i $count$i++) {
            
$match 0;
            
reset($testFields);
            foreach (
$testFields as $field) {
                if (
$field != 'attributes') {
                    if (
$this->children[$i]->$field == ${$field}) {
                        
$match++;
                    }
                } else {
                    
// Look for attributes in array
                    
$attrToMatch count($attributes);
                    
$attrMatch 0;
                    foreach (
$attributes as $key => $value) {
                        if (isset(
$this->children[$i]->attributes[$key]) &&
                            
$this->children[$i]->attributes[$key] == $value) {
                            
$attrMatch++;
                        }
                    }
                    if (
$attrMatch == $attrToMatch) {
                        
$match++;
                    }
                }
            }
            if (
$match == $fieldsToMatch) {
                
$itemsArr[] =& $this->children[$i];
            }
        }
        if (
$index >= 0) {
            if (isset(
$itemsArr[$index])) {
                return 
$itemsArr[$index];
            } else {
                return 
false;
            }
        } else {
            if (
$count count($itemsArr)) {
                return 
$itemsArr[$count-1];
            } else {
                return 
false;
            }
        }
    } 
// end func &getItem

    /**
    * Finds a node using XPATH like format:
    * 
    * search format = array ( item,item,item,item)
    * item = 'string' .. will match the <string of the xml element
    * item = array('string',array('name'=>'xyz'))
    *       .. will match <string name="xyz">
    * 
    * @param    mixed   Search path and attributes
    * 
    * @return   mixed   Config_Container object, array of Config_Container objects or false on failure.
    * @access   public
    */
    
function &searchPath($args)
    {
        if (
$this->type != 'section') {
            return 
PEAR::raiseError('Config_Container::searchPath must be called on a section type object.'nullPEAR_ERROR_RETURN);
        }

        
$arg array_shift($args);

        if (
is_array($arg)) {
            
$name $arg[0];
            
$attributes $arg[1];
        } else {
            
$name $arg;
            
$attributes null;
        }
        
// find all the matches for first..
        
$match =& $this->getItem(null$namenull$attributes);

        if (!
$match) {
            return 
false;
        }
        if (!empty(
$args)) {
            return 
$match->searchPath($args);
        }
        return 
$match;
    } 
// end func &searchPath

    /**
    * Returns how many children this container has
    *
    * @param  string    $type    type of children counted
    * @param  string    $name    name of children counted
    * @return int  number of children found
    */
    
function countChildren($type null$name null)
    {
        if (
$this->type != 'section') {
            return 
PEAR::raiseError('Config_Container::getChildrenNum must be called on a section type object.'nullPEAR_ERROR_RETURN);
        }
        if (
is_null($type) && is_null($name)) {
            return 
count($this->children);
        }
        
$count 0;
        if (isset(
$name) && isset($type)) {
            for (
$i 0$children count($this->children); $i $children$i++) {
                if (
$this->children[$i]->name == $name && 
                    
$this->children[$i]->type == $type) {
                    
$count++;
                }
            }
            return 
$count;
        }
        if (isset(
$type)) {
            for (
$i 0$children count($this->children); $i $children$i++) {
                if (
$this->children[$i]->type == $type) {
                    
$count++;
                }
            }
            return 
$count;
        }
        if (isset(
$name)) {
            
// Some directives can have the same name
            
for ($i 0$children count($this->children); $i $children$i++) {
                if (
$this->children[$i]->name == $name) {
                    
$count++;
                }
            }
            return 
$count;
        }
    } 
// end func &countChildren

    /**
    * Deletes an item (section, directive, comment...) from the current object
    * TODO: recursive remove in sub-sections
    * @return mixed  true if object was removed, false if not, or PEAR_Error if root
    */
    
function removeItem()
    {
        if (
$this->isRoot()) {
            return 
PEAR::raiseError('Cannot remove root item in Config_Container::removeItem.'nullPEAR_ERROR_RETURN);
        }
        
$index $this->getItemIndex();
        if (!
is_null($index)) {
            
array_splice($this->parent->children$index1);
            return 
true;
        }
        return 
false;
    } 
// end func removeItem

    /**
    * Returns the item index in its parent children array.
    * @return int  returns int or null if root object
    */
    
function getItemIndex()
    {
        if (
is_object($this->parent)) {
            
// This will be optimized with Zend Engine 2
            
$pchildren =& $this->parent->children;
            for (
$i 0$count count($pchildren); $i $count$i++) {
                if (
$pchildren[$i]->_id == $this->_id) {
                    return 
$i;
                }
            }
        }
        return;
    } 
// end func getItemIndex

    /**
    * Returns the item rank in its parent children array
    * according to other items with same type and name.
    * @return int  returns int or null if root object
    */
    
function getItemPosition()
    {
        if (
is_object($this->parent)) {
            
$pchildren =& $this->parent->children;
            for (
$i 0$count count($pchildren); $i $count$i++) {
                if (
$pchildren[$i]->name == $this->name &&
                    
$pchildren[$i]->type == $this->type) {
                    
$obj[] =& $pchildren[$i];
                }
            }
            for (
$i 0$count count($obj); $i $count$i++) {
                if (
$obj[$i]->_id == $this->_id) {
                    return 
$i;
                }
            }
        }
        return;
    } 
// end func getItemPosition

    /**
    * Returns the item parent object.
    * @return object  returns reference to parent object or null if root object
    */
    
function &getParent()
    {
        return 
$this->parent;
    } 
// end func &getParent

    /**
    * Returns the item parent object.
    * @return mixed  returns reference to child object or false if child does not exist
    */
    
function &getChild($index 0)
    {
        if (!empty(
$this->children[$index])) {
            return 
$this->children[$index];
        } else {
            return 
false;
        }
    } 
// end func &getChild

    /**
    * Set this item's name.
    * @return void
    */
    
function setName($name)
    {
        
$this->name $name;
    } 
// end func setName

    /**
    * Get this item's name.
    * @return string    item's name
    */
    
function getName()
    {
        return 
$this->name;
    } 
// end func getName

    /**
    * Set this item's content.
    * @return void
    */
    
function setContent($content)
    {
        
$this->content $content;
    } 
// end func setContent
    
    /**
    * Get this item's content.
    * @return string    item's content
    */
    
function getContent()
    {
        return 
$this->content;
    } 
// end func getContent

    /**
    * Set this item's type.
    * @return void
    */
    
function setType($type)
    {
        
$this->type $type;
    } 
// end func setType

    /**
    * Get this item's type.
    * @return string    item's type
    */
    
function getType()
    {
        return 
$this->type;
    } 
// end func getType

    /**
    * Set this item's attributes.
    * @param  array    $attributes        Array of attributes
    * @return void
    */
    
function setAttributes($attributes)
    {
        
$this->attributes $attributes;
    } 
// end func setAttributes

    /**
    * Set this item's attributes.
    * @param  array    $attributes        Array of attributes
    * @return void
    */
    
function updateAttributes($attributes)
    {
        if (
is_array($attributes)) {
            foreach (
$attributes as $key => $value) {
                
$this->attributes[$key] = $value;
            }
        }
    } 
// end func updateAttributes

    /**
    * Get this item's attributes.
    * @return array    item's attributes
    */
    
function getAttributes()
    {
        return 
$this->attributes;
    } 
// end func getAttributes
    
    /**
    * Get one attribute value of this item
    * @param  string   $attribute        Attribute key
    * @return mixed    item's attribute value
    */
    
function getAttribute($attribute)
    {
        if (isset(
$this->attributes[$attribute])) {
            return 
$this->attributes[$attribute];
        }
        return 
null;
    } 
// end func getAttribute

    /**
    * Set a children directive content.
    * This is an helper method calling getItem and addItem or setContent for you.
    * If the directive does not exist, it will be created at the bottom.
    *
    * @param  string    $name        Name of the directive to look for
    * @param  mixed     $content     New content
    * @param  int       $index       Index of the directive to set,
    *                                in case there are more than one directive
    *                                with the same name
    * @return object    newly set directive
    */
    
function &setDirective($name$content$index = -1)
    {
        
$item =& $this->getItem('directive'$namenullnull$index);
        if (
$item === false || PEAR::isError($item)) {
            
// Directive does not exist, will create one
            
unset($item);
            return 
$this->createDirective($name$contentnull);
        } else {
            
// Change existing directive value
            
$item->setContent($content);
            return 
$item;
        }
    } 
// end func setDirective

    /**
    * Is this item root, in a config container object
    * @return bool    true if item is root
    */
    
function isRoot()
    {
        if (
is_null($this->parent)) {
            return 
true;
        }
        return 
false;
    } 
// end func isRoot

    /**
    * Call the toString methods in the container plugin
    * @param    string  $configType  Type of configuration used to generate the string
    * @param    array   $options     Specify special options used by the parser
    * @return   mixed   true on success or PEAR_ERROR
    */
    
function toString($configType$options = array())
    {
        
$configType strtolower($configType);
        if (!isset(
$GLOBALS['CONFIG_TYPES'][$configType])) {
            return 
PEAR::raiseError("Configuration type '$configType' is not registered in Config_Container::toString."nullPEAR_ERROR_RETURN);
        }
        
$includeFile $GLOBALS['CONFIG_TYPES'][$configType][0];
        
$className   $GLOBALS['CONFIG_TYPES'][$configType][1];
        include_once(
$includeFile);
        
$renderer = new $className($options);
        return 
$renderer->toString($this);
    } 
// end func toString

    /**
    * Returns a key/value pair array of the container and its children.
    *
    * Format : section[directive][index] = value
    * If the container has attributes, it will use '@' and '#'
    * index is here because multiple directives can have the same name.
    *
    * @param    bool    $useAttr        Whether to return the attributes too
    * @return array
    */
    
function toArray($useAttr true)
    {
        
$array[$this->name] = array();
        switch (
$this->type) {
            case 
'directive':
                if (
$useAttr && count($this->attributes) > 0) {
                    
$array[$this->name]['#'] = $this->content;
                    
$array[$this->name]['@'] = $this->attributes;
                } else {
                    
$array[$this->name] = $this->content;
                }
                break;
            case 
'section':
                if (
$useAttr && count($this->attributes) > 0) {
                    
$array[$this->name]['@'] = $this->attributes;
                }
                if (
$count count($this->children)) {
                    for (
$i 0$i $count$i++) {
                        
$newArr $this->children[$i]->toArray($useAttr);
                        if (!
is_null($newArr)) {
                            foreach (
$newArr as $key => $value) {
                                if (isset(
$array[$this->name][$key])) {
                                    
// duplicate name/type
                                    
if (!is_array($array[$this->name][$key]) ||
                                        !isset(
$array[$this->name][$key][0])) {
                                        
$old $array[$this->name][$key];
                                        unset(
$array[$this->name][$key]);
                                        
$array[$this->name][$key][0] = $old;
                                    }
                                    
$array[$this->name][$key][] = $value;
                                } else {
                                    
$array[$this->name][$key] = $value;
                                }
                            }
                        }
                    }
                }
                break;
            default:
                return 
null;
        }
        return 
$array;
    } 
// end func toArray
    
    /**
    * Writes the configuration to a file
    * 
    * @param  mixed  $datasrc        Info on datasource such as path to the configuraton file or dsn...
    * @param  string $configType     Type of configuration
    * @param  array  $options        Options for writer
    * @access public
    * @return mixed     true on success or PEAR_ERROR
    */
    
function writeDatasrc($datasrc$configType$options = array())
    {
        
$configType strtolower($configType);
        if (!isset(
$GLOBALS['CONFIG_TYPES'][$configType])) {
            return 
PEAR::raiseError("Configuration type '$configType' is not registered in Config_Container::writeDatasrc."nullPEAR_ERROR_RETURN);
        }
        
$includeFile $GLOBALS['CONFIG_TYPES'][$configType][0];
        
$className $GLOBALS['CONFIG_TYPES'][$configType][1];
        include_once(
$includeFile);

        
$writeMethodName = (version_compare(phpversion(), '5''<')) ? 'writedatasrc' 'writeDatasrc';
        if (
in_array($writeMethodNameget_class_methods($className))) {
            
$writer = new $className($options);
            return 
$writer->writeDatasrc($datasrc$this);
        }

        
// Default behaviour
        
$fp = @fopen($datasrc'w');
        if (
$fp) {
            
$string $this->toString($configType$options);
            
$len strlen($string);
            @
flock($fpLOCK_EX);
            @
fwrite($fp$string$len);
            @
flock($fpLOCK_UN);
            @
fclose($fp);
            return 
true;
        } else {
            return 
PEAR::raiseError('Cannot open datasource for writing.'1PEAR_ERROR_RETURN);
        }
    } 
// end func writeDatasrc
// end class Config_Container
?>