<?php
/**
 * Project: 
 * Contenido Content Management System
 * 
 * Description: 
 * Custom properties
 * 
 * Requirements: 
 * @con_php_req 5.0
 * 
 *
 * @package    Contenido Backend classes
 * @version    1.2
 * @author     Timo A. Hummel
 * @copyright  four for business AG <www.4fb.de>
 * @license    http://www.contenido.org/license/LIZENZ.txt
 * @link       http://www.4fb.de
 * @link       http://www.contenido.org
 * @since      file available since contenido release <= 4.6
 * 
 * {@internal 
 *   created 2003-12-21
 *   modified 2008-06-30, Dominik Ziegler, add security fix
 *   modified 2009-09-27, Dominik Ziegler, fixed wrong (un)escaping
 *   modified 2011-02-05, Murat Purc, cleanup, formatting and documentation.
 *   modified 2011-03-14, Murat Purc, adapted to new GenericDB, partly ported to PHP 5, formatting
 *
 *   $Id$:
 * }}
 * 
 */

if (!defined('CON_FRAMEWORK')) {
    die('Illegal call');
}


/* Custom properties
 * -----------------
 *
 * Custom properties are properties which can be assigned to virtually any element
 * in Contenido and underlaying websites.
 *
 *
 * Table structure
 * ---------------
 *
 * Field        Size            Description
 * -----        ----            -----------
 * idproperty   int(10)         idproperty (automatically handled by this class)
 * itemtype     varchar(32)     Custom item type (e.g. idcat, idart, idartlang, custom)
 * itemid       varchar(32)     ID of the item
 * type         varchar(32)     Property type
 * name         varchar(32)     Property name
 * value        text            Property value
 * author       varchar(32)     Author (md5-hash of the username)
 * created      datetime        Created date and time
 * modified     datetime        Modified date and time
 * modifiedby   varchar(32)     Modified by (md5-hash of the username)
 *
 *
 * Example:
 * --------
 * A module needs to store custom properties for categories. Modifying the database
 * would be a bad thing, since the changes might get lost during an upgrade or
 * reinstall.
 *
 * If the custom property for a category would be the path to a category image,
 * we would fill a row as follows:
 *
 * itemtype: idcat
 * itemid:   <number of your category>
 * type:     category
 * name:     image
 * value:    images/category01.gif
 *
 * idproperty, author, created, modified and modifiedby are automatically handled by
 * the class.
 */


class PropertyCollection extends ItemCollection
{
    public $client;

    /**
     * Constructor Function
     * @param none
     */
    public function __construct()
    {
        global $cfg, $client;
        $this->client = Contenido_Security::toInteger($client);
        parent::__construct($cfg['tab']['properties'], 'idproperty');
        $this->_setItemClass('PropertyItem');
    }

    /**
     * Creates a new property item.
     *
     * Example:
     *
     * $properties->create('idcat', 27, 'visual', 'image', 'images/tool.gif');
     *
     * @param   mixed  $itemtype     Type of the item (example: idcat)
     * @param   mixed  $itemid       ID of the item (example: 31)
     * @param   mixed  $type         Type of the data to store (arbitary data)
     * @param   mixed  $name         Entry name
     * @param   mixed  $value        Value
     * @param   bool   $bInternally  Optionally default false (on internal call do not escape parameters again
     * @return  PropertyItem
     */
    public function create($itemtype, $itemid, $type, $name, $value, $bInternally = false)
    {
        global $cfg, $auth;

        $item = parent::createNewItem();

        if (!$bInternally) {
            $itemtype   = Contenido_Security::escapeDB($itemtype, null);
            $itemid     = Contenido_Security::escapeDB($itemid, null);
            $value      = Contenido_Security::escapeDB($value, null);
            $type       = Contenido_Security::escapeDB($type, null);
            $name       = Contenido_Security::escapeDB($name, null);
        }

        $item->set('idclient', $this->client);
        $item->set('itemtype', $itemtype, false);
        $item->set('itemid', $itemid, false);
        $item->set('type', $type);
        $item->set('name', $name);
        $item->set('value', $value);

        $item->set('created', date('Y-m-d H:i:s'), false);
        $item->set('author', Contenido_Security::escapeDB($auth->auth['uid'], null));
        $item->store();

        return ($item);
    }


    /**
     * Returns the value for a given item.
     *
     * Example:
     *
     * $file = $properties->getValue('idcat', 27, 'visual', 'image');
     *
     * @param   mixed  $itemtype  Type of the item (example: idcat)
     * @param   mixed  $itemid    ID of the item (example: 31)
     * @param   mixed  $type      Type of the data to store (arbitary data)
     * @param   mixed  $name      Entry name
     * @return  mixed  Value
     */
    public function getValue($itemtype, $itemid, $type, $name, $default = false)
    {
        $itemtype = Contenido_Security::escapeDB($itemtype, null);
        $itemid   = Contenido_Security::escapeDB($itemid, null);
        $type     = Contenido_Security::escapeDB($type, null);
        $name     = Contenido_Security::escapeDB($name, null);
        
        if($mValue = cPropertyCache::getProp($itemtype, $itemid, $type, $name)) {
            return $mValue;
        }

        if (isset($this->client)) {
            $this->select("idclient = '".$this->client."' AND itemtype = '".$itemtype."' AND itemid = '".$itemid."' AND type = '".$type."' AND name = '".$name."'");
        } else {
            $this->select("itemtype = '".$itemtype."' AND itemid = '".$itemid."' AND type = '".$type."' AND name = '".$name."'");
        }

        if ($item = $this->next()) {
            cPropertyCache::addProp($item);
            return (Contenido_Security::unescapeDB($item->get('value')));
        }

        //return $default;
    }


    /**
     * Returns the value for a given item.
     *
     * Example:
     *
     * $file = $properties->getValuesByType('idcat', 27, 'visual');
     *
     * @param   mixed  $itemtype  Type of the item (example: idcat)
     * @param   mixed  $itemid    ID of the item (example: 31)
     * @param   mixed  $type      Type of the data to store (arbitary data)
     * @return  array  Value
     **/
    public function getValuesByType($itemtype, $itemid, $type)
    {
        $aResult  = array();
        $itemtype = Contenido_Security::escapeDB($itemtype, null);
        $itemid   = Contenido_Security::escapeDB($itemid, null);
        $type     = Contenido_Security::escapeDB($type, null);

        if (isset($this->client)) {
            $this->select("idclient = '".$this->client."' AND itemtype = '".$itemtype."' AND itemid = '".$itemid."' AND type = '".$type."'");
        } else {
            $this->select("itemtype = '".$itemtype."' AND itemid = '".$itemid."' AND type = '".$type."'");
        }

        while ($item = $this->next()) {
            cPropertyCache::addProp($item);
            $aResult[$item->get('name')] = Contenido_Security::unescapeDB($item->get('value'));
        }

        return $aResult;
    }


    /**
     * Sets a property item. Handles creation and updating.
     *
     * Example:
     *
     * $properties->setValue('idcat', 27, 'visual', 'image', 'images/tool.gif');
     *
     * @param   mixed  $itemtype  Type of the item (example: idcat)
     * @param   mixed  $itemid    ID of the item (example: 31)
     * @param   mixed  $type      Type of the data to store (arbitary data)
     * @param   mixed  $name      Entry name
     * @param   mixed  $value     Value
     * @param   int    $idProp    Id of database record (if set, update on this basis (possiblity to update name value and type))
     */
    public function setValue($itemtype, $itemid, $type, $name, $value, $idProp = 0)
    {
        $itemtype = Contenido_Security::escapeDB($itemtype, null);
        $itemid   = Contenido_Security::escapeDB($itemid, null);
        $type     = Contenido_Security::escapeDB($type, null);
        $name     = Contenido_Security::escapeDB($name, null);
        $value    = Contenido_Security::escapeDB($value, null);
        $idProp   = Contenido_Security::toInteger($idProp);

        if ($idProp == 0) {
            $this->select("idclient = '".$this->client."' AND itemtype = '".$itemtype."' AND itemid = '".$itemid."' AND type = '".$type."' AND name = '".$name."'");
        } else {
            $this->select("idclient = '".$this->client."' AND itemtype = '".$itemtype."' AND itemid = '".$itemid."' AND idproperty = '".$idProp."'");
        }

        if ($item = $this->next()) {
            $item->set('value', $value);
            $item->set('name', $name);
            $item->set('type', $type);
            $item->store();
        } else {
            $this->create($itemtype, $itemid, $type, $name, $value, true);
        }
    }


    /**
     * Delete a property item.
     *
     * Example:
     *
     * $properties->deleteValue('idcat', 27, 'visual', 'image');
     *
     * @param  mixed  $itemtype  Type of the item (example: idcat)
     * @param  mixed  $itemid    ID of the item (example: 31)
     * @param  mixed  $type      Type of the data to store (arbitary data)
     * @param  mixed  $name      Entry name
     */
    public function deleteValue($itemtype, $itemid, $type, $name)
    {
        $itemtype = Contenido_Security::escapeDB($itemtype, null);
        $itemid   = Contenido_Security::escapeDB($itemid, null);
        $type     = Contenido_Security::escapeDB($type, null);
        $name     = Contenido_Security::escapeDB($name, null);

        if (isset($this->client)) {
            $this->select("idclient = '".$this->client."' AND itemtype = '".$itemtype."' AND itemid = '".$itemid."' AND type = '".$type."' AND name = '".$name."'");
        } else {
            $this->select("itemtype = '".$itemtype."' AND itemid = '".$itemid."' AND type = '".$type."' AND name = '".$name."'");
        }

        if ($item = $this->next()) {
            $this->delete($item->get('idproperty'));
        }
    }


    /**
     * Checks if values for a given item are available.
     *
     * @param   mixed  $itemtype  Type of the item (example: idcat)
     * @param   mixed  $itemid    ID of the item (example: 31)
     * @return  array  For each given item
     */
    public function getProperties($itemtype, $itemid)
    {
        $itemtype = Contenido_Security::escapeDB($itemtype, null);
        $itemid   = Contenido_Security::escapeDB($itemid, null);

        if (isset($this->client)) {
            $this->select("idclient = '".$this->client."' AND itemtype = '".$itemtype."' AND itemid = '".$itemid."'");
        } else {
            $this->select("itemtype = '".$itemtype."' AND itemid = '".$itemid."'");
        }

        $result[$itemid] = false;

        while ($item = $this->next()) {
            cPropertyCache::addProp($item);
            // enable accessing property values per number and field name
            $result[$item->get('itemid')][$item->get('idproperty')] = array(
                0=> $item->get('type'),  'type'=>  $item->get('type'),
                1=> $item->get('name'),  'name'=>  $item->get('name'),
                2=> $item->get('value'), 'value'=> $item->get('value')
            );
        }
        return $result;
    }


    /**
     * Returns all datasets selected by given field and value combination
     *
     * @param   mixed  $field       Field to search in
     * @param   mixed  $fieldValue  Value to search for
     * @param   Contenido_Auth  $auth  Narrow result down to user in auth objext
     * @return  array  For each given item
     */
    public function getAllValues($field, $fieldValue, $auth=NULL)
    {
        $authString = '';
        if (!is_null($auth)
                && is_countable($auth)
                && sizeof($auth) > 0) {
            $authString .= " AND author = '" . $auth->auth["uid"] . "'";
        }

        if (isset($this->client)) {
            $this->select("idclient = '" . $this->client . "' AND " . $field . " = '" . $fieldValue . "'" . $authString, '' ,'itemid');
        } else {
            $this->select($field . " = '" . $fieldValue . "'" . $authString);
        }

        $retValue = array();
        while ($item = $this->next()) {
            cPropertyCache::addProp($item);
            $dbLine = array(
                'idproperty'  => $item->get('idproperty'),
                'idclient'    => $item->get('idclient'),
                'itemtype'    => $item->get('itemtype'),
                'itemid'      => $item->get('itemid'),
                'type'        => $item->get('type'),
                'name'        => $item->get('name'),
                'value'       => $item->get('value'),
                'author'      => $item->get('author'),
                'created'     => $item->get('created'),
                'modified'    => $item->get('modified'),
                'modifiedby'  => $item->get('modifiedby')
            );
            $retValue[] = $dbLine;
        }
        return $retValue;
    }


    /**
     * Delete all properties which match itemtype and itemid
     *
     * @param  mixed  $itemtype  Type of the item (example: idcat)
     * @param  mixed  $itemid    ID of the item (example: 31)
     */
    public function deleteProperties($itemtype, $itemid)
    { 
        $itemtype = Contenido_Security::escapeDB($itemtype, null);
        $itemid   = Contenido_Security::escapeDB($itemid, null);

        if (isset($this->client)) {
            $this->select("idclient = '".$this->client."' AND itemtype = '".$itemtype."' AND itemid = '".$itemid."'");
        } else {
            $this->select("itemtype = '".$itemtype."' AND itemid = '".$itemid."'");
        }

        $deleteProperties = array();

        while ($item = $this->next()) {
            $deleteProperties[] = $item->get('idproperty');
        }

        foreach($deleteProperties as $idproperty) {
            cPropertyCache::deleteProp($idproperty);
            $this->delete($idproperty);
        }
    }


    public function changeClient($idclient)
    {
        $this->client = $idclient;
    }
}


class PropertyItem extends Item
{
    /**
     * maximumLength: Array which stores the maximum string length of each field
     */
    public $maximumLength;

    /**
     * Constructor Function
     * @param  mixed  $mId  Specifies the ID of item to load
     */
    public function __construct($mId = false)
    {
        global $cfg;
        parent::__construct($cfg['tab']['properties'], 'idproperty');

        // Initialize maximum lengths for each column
        $this->maximumLength = array();
        $this->maximumLength['itemtype'] = 64;
        $this->maximumLength['itemid'] = 255;
        $this->maximumLength['type'] = 96;
        $this->maximumLength['name'] = 96;

        if ($mId !== false) {
            $this->loadByPrimaryKey($mId);
        }
    }

    /**
     * Stores changed PropertyItem
     */
    public function store()
    {
        global $auth;

        $this->set('modified', date('Y-m-d H:i:s'), false);
        $this->set('modifiedby', $auth->auth['uid']);

        parent::store();
    }

    /**
     * Sets value of a field
     *
	 * @param  string  $field
	 * @param  string  $value
	 * @param  bool    $safe  Flag to run filter on passed value
     */
    public function setField($field, $value, $safe = true): bool
    {
        if (array_key_exists($field, $this->maximumLength)) {
            if (strlen($value) > $this->maximumLength[$field]) {
                cWarning(__FILE__, __LINE__, "Tried to set field $field to value $value, but the field is too small. Truncated.");
            }
        }

        return parent::setField($field, $value, $safe);
    }
}

class cPropertyCache {
    
    /**
     * Property cache array
     * @var array 
     */
    public static $_aEntries;
    
    /**
     * add item to cache
     * 
     * @param object $oEntry
     */
    public static function addProp($oEntry) {
        if(!is_array(self::$_aEntries)) {
            self::$_aEntries = array();
        }
        $aData = $oEntry->toArray();
        
        self::$_aEntries[$aData['idproperty']] = $aData;
    }
    
    /**
     * Delete item from cache
     * 
     * @param int $iId id of prperty
     */
    public static function deleteProp($iId) {
        if(isset(self::$_aEntries[$iId]) && is_array(self::$_aEntries[$iId])) {
            unset(self::$_aEntries[$iId]);
        }
    }
    
    /**
     * get property from cache
     * 
     * @param string $itemtype
     * @param int $itemid
     * @param string $type
     * @param string $name
     * @return string|boolean string of entry or false
     */
    public static function getProp($itemtype, $itemid, $type, $name) {
        if(!is_array(self::$_aEntries) || empty(self::$_aEntries)) {
            return false;
        }
        foreach (self::$_aEntries as $id => $entry) {
            if ($entry['itemtype'] == $itemtype && $entry['itemid'] == $itemid && $entry['type'] == $type && $entry['name'] == $name) {
                return (string) $entry['value'];
            }
        }
        return false;
    }
}
?>