<?php
/**
 * File for content classes
 *
 * @package eFront
*/



/**
 * Efront content exceptions
 * 
 * This class extends Exception to provide the exceptions related to content
 * @package eFront
 * 
 * @since 3.5.0
 */
class EfrontContentException extends Exception
{
    /**
     * The id provided is not valid, for example it is not a number or it is 0
     * @since 3.5.0
     */
    const INVALID_ID = 501;
    /**
     * The unit requested does not exist
     * @since 3.5.0
     */
    const UNIT_NOT_EXISTS = 502;
    /**
     * The unit can not be inserted for some reason
     * @since 3.5.0
     */
    const CANNOT_INSERT_UNIT = 503;
    /**
     * The project requested does not exist
     * @since 3.5.0
     */
    const PROJECT_NOT_EXISTS = 504;
    /**
     * The user login provided is not valid or does not exist
     * @since 3.5.0
     */
    const INVALID_LOGIN = 505;
    /**
     * The score is not valid, for example it is not numeric
     * @since 3.5.0
     */
    const INVALID_SCORE = 506;
    /**
     * The data provided is not valid, for example quotes or other illegal characters
     * @since 3.5.0
     */
    const INVALID_DATA = 507;
    /**
     * An error originating in database actions
     * @since 3.5.0
     */
    const DATABASE_ERROR = 508;    
    /**
     * An unspecific error
     * @since 3.5.0
     */
    const GENERAL_ERROR = 599;
}

/**
 * This class represents a content unit in eFront
 * 
 * @package eFront
 * @since 3.5.0
 */
class EfrontUnit extends ArrayObject
{
    /**
     * The maximum length for unit names. After that, the names appear truncated
     */
    const MAXIMUM_NAME_LENGTH = 50;
    
    /**
     * Class constructor
     * 
     * This function is used to instantiate the unit object
     * Since the class inherits from ArrayObject, normally
     * an array should be provided for instantiation. However,
     * the choice of using a unit id has been added for greater
     * flexibility, but still you are advised to avoid doing so,
     * since it might lead to big and unnecessary database overhead
     * <br/>Example:
     * <code>
     * $content = eF_getTableData("content", "*");
     * $unit = new EfrontUnit($content[4]);             //The best way: instantiate unit using existing information 
     * $unit = new EfrontUnit(7);                       //The bad way: Let class to retrieve information. Should be avoided unless we are dealing with a single unit
     * </code>
     *
     * @param mixed $array Either a unit information array, or a unit id
     * @since 3.5.0
     * @access public 
     */
    function __construct($array) {
        if (!is_array($array)) {
            if (eF_checkParameter($array, 'id')) {
                $result = eF_getTableData("content", "*", "id=$array");
                if (sizeof($result) == 0) {
                    throw new EfrontContentException(_UNITDOESNOTEXIST.": $array", EfrontContentException :: UNIT_NOT_EXISTS);
                } else {
                    $array = $result[0];
                }
            } else {
                throw new EfrontContentException(_INVALIDID.": $array", EfrontContentException :: INVALID_ID);
            }
        }

        if (unserialize($array['options'])) {
            $array['options'] = unserialize($array['options']);
        } else {
             $array['options'] = false;
        }
        parent :: __construct($array);
    }
    
    /**
     * Store changed values to the database
     * 
     * This unit is used to stored any changed values to the database
     * <br/>Example:
     * <code>
     * $unit['name'] = 'new name';
     * $unit -> persist();
     * </code>
     * 
     * @return boolean true if everything is ok
     * @since 3.5.0
     * @access public
     */
    public function persist() {
        $fields = array('name'                => $this['name'],
                        'data'                => ($this['data']),
                        'parent_content_ID'   => $this['parent_content_ID'],
                        'lessons_ID'          => $this['lessons_ID'],
                        'timestamp'           => $this['timestamp'],
                        'ctg_type'            => $this['ctg_type'],
                        'active'              => $this['active'],
                        'previous_content_ID' => $this['previous_content_ID'],
                        'options'             => serialize($this['options']),
                        'metadata'            => $this['metadata']);      
        
		return eF_updateTableData("content", $fields, "id=".$this['id']);
    }
    
    
    /**
     * Set search keywords
     * 
     * This function updates the search keywords related to this unit's name and content.
     * It should be executed when and only when there is a change in any of the above fields,
     * since it performs excessive database queries.
     * <br>Example:
     * <code>
     * $unit = new EfrontUnit(34);			//Instantiate unit with id 34
     * $unit['name'] = 'New unit name';	    //Change unit name
     * $unit -> persist();					//Store new data
     * $unit -> setSearchKeywords();		//Update keywords
     * </code>
     *
     * @return boolean true if everything is ok
     * @since 3.5.2
     * @access public
     */
    public function setSearchKeywords() {
		EfrontSearch :: removeText('content', $this['id'], 'data');										//Refresh the search keywords
		EfrontSearch :: insertText($this['data'], $this['id'], "content", "data");
		EfrontSearch :: removeText('content', $this['id'], 'title');										//Refresh the search keywords
		EfrontSearch :: insertText($this['name'], $this['id'], "content", "title");

		return eF_updateTableData("content", $fields, "id=".$this['id']);
    }
    
    /**
     * Delete unit
     * 
     * This function is used to delete the current unit.
     * <br/>Example:
     * <code>
     * $unit -> delete();
     * </code>
     * 
     * @return boolean true if everything is ok
     * @since 3.5.0
     * @access public
     */
    public function delete() {
        if ($this['ctg_type'] == 'tests') {
            $result = eF_getTableData("tests", "id", "content_ID=".$this['id']);
            if (sizeof($result) > 0) {
                $test  = new EfrontTest($result[0]);
                $test -> delete();
            }
        } 
        eF_deleteTableData("content", "id=".$this['id']);                                               //Delete Unit from database   
        eF_deleteTableData("scorm_data", "content_ID=".$this['id']);                                    //Delete Unit from scorm_data
		EfrontSearch :: removeText('content', $this['id'], '');											//Delete keywords
    }	
    
    /**
     * Activate unit
     * 
     * This function is used to activate the current unit.
     * If the unit is a test unit, the correspoding test is also activated
     * <br/>Example:
     * <code>
     * $unit = new EfrontUnit(43);				//Instantiate object for unit with id 43
     * $unit -> activate();						//Activate unit
     * </code>
     *
     * @since 3.5.0
     * @access public
     */
    public function activate() {
        $this['active'] = 1;
        $this -> persist();   
        
        if ($this['ctg_type'] == 'tests') {
            $result = eF_getTableData("tests", "id", "content_ID=".$this['id']);
            if (sizeof($result) > 0) {
                $test  = new EfrontTest($result[0]['id']);
                if (!$test -> test['active']) {
                    $test -> activate();
                }
            }
        } 
    }
    
    /**
     * Deactivate unit
     * 
     * This function is used to deactivate the current unit.
     * If the unit is a test unit, the correspoding test is also deactivated
     * <br/>Example:
     * <code>
     * $unit = new EfrontUnit(43);				//Instantiate object for unit with id 43
     * $unit -> deactivate();					//Deactivate unit
     * </code>
     *
     * @since 3.5.0
     * @access public
     */
    public function deactivate() {
        if ($this['ctg_type'] == 'tests') {
            $result = eF_getTableData("tests", "id", "content_ID=".$this['id']);
            if (sizeof($result) > 0) {
                $test  = new EfrontTest($result[0]['id']);
                if ($test -> test['active']) {
                    $test -> deactivate();
                }
            }
        } 
        $this['active'] = 0;
        $this -> persist();   
    }
    
    /**
     * Get unit questions
     * 
     * This function returns a list with all the questions
     * that belong to this unit. If $returnObjects is true, then 
     * Question objects are returned.
     * <br/>Example:
     * <code>
     * $questions = $this -> getQuestions();            //Get a simple list of questions
     * $questions = $this -> getQuestions(true);        //Get a list of Question objects
     * </code>
     * 
     * @param boolean $returnObjects Whether to return Question objects
     * @return array An array of questions
     * @since 3.5.0
     * @access public
     */
    public function getQuestions($returnObjects = false) {
        $questions = array();
        $result    = eF_getTableData("questions", "*", "content_ID=".$this['id']);
        if (sizeof($result) > 0) {
            foreach ($result as $value) {
                $returnObjects ? $questions[$value['id']] = QuestionFactory :: factory($value) : $questions[$value['id']] = $value;
            }
        }
        
        return $questions;
    }    
    
    /**
     * Query if the unit is a test
     * 
     * This function returns true if the unit corresponds to a test,
     * otherwise it returns false
     * <br/>Example:
     * <code>
     * $unit = new EfrontUnit(7);
     * $flg = $unit->isTest();
     * </code>
     * 
     *
     * @return boolean A flag to indicate if the unit is test
     * @since 3.5.0
     * @access public
     */
    public function isTest(){
        if (parent::offsetGet('ctg_type') == "tests"){
            return true;
        }
        else{
            return false;
        }
    }
    
    
    public function toXML(){
        $xml  =  '<?xml version="1.0" encoding="UTF-8"?>' ."\n";
        $xml .= "\t" . '<unit>';
        $xml .= "\t\t<name>".parent::offsetGet('name')."</name>\n";
        $xml .= "\t\t<ctg_type>".parent::offsetGet('ctg_type')."</ctg_type>\n";
        $xml .= "\t</unit>";
        return $xml;
    }
    
    public function fromXML($xmlstr){
        $xml = new SimpleXMLElement($xmlstr);
        parent::offsetSet('name', (string)$xml->unit->name);
        parent::offsetSet('ctg_type', (string)$xml->unit->ctg_type);
    }
    
    /**
     * Get the id of prerequisite unit for this unit
     * 
     * This function returns false if there is no prerequisite unit,
     * otherwise returns the id of the prerequisite unit
     * <br/>Example:
     * <code>
     * $unit = new EfrontUnit(7);
     * $pid = $unit->getPrerequisite();
     * </code>
     * 
     *
     * @return mixed An integer id if there is a prerequisite, or false otherwise
     * @since 3.5.0
     * @access public
     */
    public function getPrerequisite(){
        if (parent::offsetGet('previous_content_ID') != "0"){
            return parent::offsetGet('previous_content_ID');
        }
        else
            return false;
    }
    
   /**
     * Get the lesson files
     * 
     * This function returns an array of the file ids or paths which are used by this unit
     * <br/>Example:
     * <code>
     * $unit = new EfrontUnit(7);
     * $files = $unit -> getFiles();
     * </code>
     * 
     * @return array An array with the file ids
     * @since 3.5.0
     * @access public
     */
    public function getFiles($returnObjects = false) {        
        $data     = parent :: offsetGet('data');
        preg_match_all("/view_file\.php\?file=(\d+)/", $data, $matchesId);
        $filesId = $matchesId[1];
        preg_match_all("#content/lessons/(.*)\"#U", $data, $matchesPath);
        $filesPath = $matchesPath[1];
        
        foreach ($filesId as $file) {
            $returnObjects ? $files[] = new EfrontFile($file) : $files[] = $file;
        }
        foreach ($filesPath as $file) {
            $returnObjects ? $files[] = new EfrontFile(G_LESSONSPATH.html_entity_decode($file)) : $files[] = G_LESSONSPATH.html_entity_decode($file);
        }
        
        return $files;
    }
    
    
    /**
     * Create a new unit
     * 
     * This function is used to create a new unit.
     * <br/>Example:
     * <code>
     * $fields = array('name' => 'new unit', 'ctg_type' => 'theory');
     * $unit = EfrontUnit :: createUnit($fields);
     * </code>
     * 
     * @param array $fields The new unit fields
     * @return EfrontUnit The newly created unit
     * @since 3.5.0
     * @access public
     */
    public static function createUnit($fields = array()) {
        if (!isset($fields['lessons_ID'])) {
            return false;
        }
        !isset($fields['name'])      ? $fields['name']      = 'Default unit' : null;
        !isset($fields['timestamp']) ? $fields['timestamp'] = time()         : null;
        !isset($fields['ctg_type'])  ? $fields['ctg_type']  = 'theory'       : null;
        if (!isset($fields['metadata'])) {
            $defaultMetadata = array('title'       => $fields['name'],
                                  	 'creator'     => $GLOBALS['currentUser'] -> user['name'].' '.$GLOBALS['currentUser'] -> user['surname'],
                                  	 'publisher'   => $GLOBALS['currentUser'] -> user['name'].' '.$GLOBALS['currentUser'] -> user['surname'],
                                  	 'contributor' => $GLOBALS['currentUser'] -> user['name'].' '.$GLOBALS['currentUser'] -> user['surname'],
                                  	 'date'        => date("Y/m/d", $fields['timestamp']),
                                     'type'        => 'content');
            $fields['metadata']  = serialize($defaultMetadata);
        }

        $newId  = eF_insertTableData("content", $fields);
        $result = eF_getTableData("content", "*", "id=".$newId);                                            //We perform an extra step/query for retrieving data, sinve this way we make sure that the array fields will be in correct order (forst id, then name, etc)
        $unit   = new EfrontUnit($result[0]);
		EfrontSearch :: insertText($fields['name'], $unit['id'], "content", "title");
        return $unit;
    }
}

/**
 * This class represents the content tree and extends EfrontTree class 
 * @package eFront
 * @since 3.5.0 
 */
class EfrontContentTree extends EfrontTree
{   
    /**
     * The lesson id
     *
     * @var int
     * @since 3.5.0
     * @access public
     */
    public $lessonId = 0;
    
    /**
     * Content rules. The array is initialized only after the call to getRules()
     *
     * @since 3.5.0
     * @var array
     * @access public
     * @see getRules()
     */
    protected $rules = false;
    
    /**
     * Instantiate tree object
     * 
     * The constructor instantiates the tree based on the lesson id
     * <br/>Example:
     * <code>
     * $tree = new EfrontContentTree(23);                   //23 is the lesson id
     * $lesson = new EfrontLesson(23);                      //23 is the lesson id
     * $tree = new EfrontContentTree($lesson);              //Content may be alternatively instantiated using the lesson object
     * </code>
     * 
     * @param mixed $lesson Either The lesson id or an EfrontLesson object
     * @since 3.5.0
     * @access public 
     */
    function __construct($lesson) {
        if ($lesson instanceof EfrontLesson) {
            $lessonId = $lesson -> lesson['id'];
        } elseif (!eF_checkParameter($lesson, 'id')) {
            throw new EfrontContentException(_INVALIDLESSONID.': '.$lesson, EfrontContentException :: INVALID_ID);
        } else {
            $lessonId = $lesson;
        }

        $this -> lessonId = $lessonId;                        //Set the lesson id
        $this -> reset();                                     //Initialize content tree 
        $firstUnit = $this -> getFirstNode();
        $this -> currentUnitId = $firstUnit['id'];
    }

    /**
     * Construct content tree structure
     * 
     * Creates a tree-like representation of the content, using arrays as EfrontUnit,
     * a class that extends ArrayObject.
     * Each unit is represented as an array with the appropriate fields
     * (id, name, timestamp etc). If the unit has children units, then
     * these are subarrays of the current unit array. All keys correspond 
     * to unit ids.
     * If, for some reason, there are units with invalid succession data, 
     * (parent or previous content ids), these are appended at the end of
     * the content tree.
     * <br/>Example:
     * <code>
     * $content = new EfrontContentTree(4);                                 //Initialize content tree for lesson with id 4
     * //Do some nasty stuff with content tree
     * $content -> reset();                                                 //Reset content tree to its original state
     * </code>
     * 
     * @since 3.5.0
     * @access public
     */
    public function reset() {
        $units = eF_getTableData("content", "*", "lessons_ID = '".$this -> lessonId."'");
        //$units   = eF_getTableData("content", "id,name,parent_content_ID,lessons_ID,timestamp,ctg_type,active,previous_content_ID", "lessons_ID = '".$this -> lessonId."'");
        if (sizeof($units) == 0) {
            $this -> tree = new RecursiveArrayIterator(array());
            return;
        }

        $rejected = array();
        foreach ($units as $node) {             //Assign previous content ids as keys to the previousNodes array, which will be used for sorting afterwards
            $node = new EfrontUnit($node);        //We convert arrays to array objects, which is best for manipulating data through iterators
            if (!isset($previousNodes[$node['previous_content_ID']])) {          
                $previousNodes[$node['previous_content_ID']] = $node;
            } else {
                $rejected[$node['id']] = $node;                                                                            //$rejected holds cut off units, which do not have a valid previous_content_ID
            }
        }
        
        $node  = 0;
        $count = 0;       
        $nodes = array();                                                                          //$count is used to prevent infinite loops
        while (sizeof($previousNodes) > 0 && isset($previousNodes[$node]) && $count++ < 1000) {    //Order the nodes array according to previous_content_ID information. if $previousNodes[$node] is not set, it means that there are illegal previous content id entries in the array (for example, a unit reports as previous a non-existent unit). In this case, all the remaining units in the $previousNodes array are rejected
            $nodes[$previousNodes[$node]['id']] = $previousNodes[$node];                           //Assign the previous node to be the array key
            $newNode = $previousNodes[$node]['id'];                                     
            unset($previousNodes[$node]);
            $node    = $newNode; 
        }
        if (sizeof($previousNodes) > 0) {                                                          //If $previousNodes is not empty, it means there are invalid (orphan) units in the array, so append them to the $rejected list
            foreach ($previousNodes as $value) {
                $rejected[$value['id']] = $value;
            }
        }
        
        $tree  = $nodes;
        $count = 0;                                                                          //$count is used to prevent infinite loops
        while (sizeof($tree) > 1 && $count++ < 10000) {                                      //We will merge all branches under the main tree branch, the 0 node, so its size will become 1                              
            foreach ($nodes as $key => $value) {
                if ($value['parent_content_ID'] == 0 || in_array($value['parent_content_ID'], array_keys($nodes))) {        //If the unit parent is in the $nodes array keys - which are the unit ids- or it is 0, then it is  valid
                    $parentNodes[$value['parent_content_ID']][]      = $value;               //Find which nodes have children and assign them to $parentNodes
                    $tree[$value['parent_content_ID']][$value['id']] = array();              //We create the "slots" where the node's children will be inserted. This way, the ordering will not be lost
                } else {
                    $rejected = $rejected + array($value['id'] => $value);                   //Append units with invalid parents to $rejected list
                    unset($nodes[$key]);                                                     //Remove the invalid unit from the units array, as well as from the parentUnits, in case a n entry for it was created earlier
                    unset($parentNodes[$value['parent_content_ID']]);
                }
            }
            if (isset($parentNodes)) {                                                       //If the unit was rejected, there won't be a $parentNodes array
                $leafNodes = array_diff(array_keys($nodes), array_keys($parentNodes));       //Now, it's easy to see which nodes are leaf nodes, just by subtracting $parentNodes from the whole set
                foreach ($leafNodes as $leaf) {
                    $parent_id = $nodes[$leaf]['parent_content_ID'];                         //Get the leaf's parent
                    $tree[$parent_id][$leaf] = $tree[$leaf];                                 //Append the leaf to its parent's tree branch
                    unset($tree[$leaf]);                                                     //Remove the leaf from the main tree branch
                    unset($nodes[$leaf]);                                                    //Remove the leaf from the nodes set
                }
                unset($parentNodes);                                                         //Reset $parentNodes; new ones will be calculated at the next loop
            }
        }

        if (sizeof($tree) > 0 && !isset($tree[0])) {                                         //This is a special case, where only one node exists in the tree
            $tree = array($tree);
        }
        isset($tree[0]) ? $tree = $tree[0] : $tree = array();

        if (sizeof($rejected) > 0) {                                            //Append rejected nodes to the end of the tree array, updating their parent/previous information
            foreach (new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($tree), RecursiveIteratorIterator :: SELF_FIRST)) as $lastUnit);    //Advance to the last tree node
            isset($lastUnit) ? $previousId = $lastUnit['id'] : $previousId = 0;              //There is a chance that no normal units exist in the tree. In this case, there will be no $lastUnit
            foreach ($rejected as $id => $node) {                                //Update broken nodes
                $node['parent_content_ID']   = 0;
                $node['previous_content_ID'] = $previousId;
                $node -> persist();                                                //Persist changes to the database
                $tree[$id]  = $node;
                $previousId = $id;
            }
        }

        $this -> tree = new RecursiveArrayIterator($tree);        
    }

    /**
     * Remove unit
     * 
     * This function is used to remove a unit from the content tree
     * <br/>Example:
     * <code>
     * $content = new EfrontContentTree(4);                                 //Initialize content tree for lesson with id 4
     * $content -> removeNode(57);                                          //Remove the unit 57 and all of its subunits
     * </code>
     * 
     * @param int $removeId The unit id that will be removed
     * @since 3.5.0
     * @access public
     */
    public function removeNode($removeId) {
        $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator($this -> tree, RecursiveIteratorIterator :: SELF_FIRST));    //Get an iterator for the current tree. This iterator returns only whole unit arrays and not unit members separately (such as id, timestamp etc)
        $iterator -> rewind();                                                //Initialize iterator

        while ($iterator -> valid() && $iterator -> key() != $removeId) {        //Forward iterator index until you reach the designated element, which has an index equal to the unit id that will be removed
            $iterator -> next();
        }
        
        if ($iterator -> valid()) {
            $iterator -> current() -> delete();                                  //Delete the current unit from the database
            $previousUnit = $this -> getPreviousNode($iterator -> key());        //Get the deleted unit's previous unit
            $iterator -> offsetUnset($removeId);                                 //Delete the unit from the content tree
            if ($previousUnit) {                                                 //If we are deleting the first unit, there is no previous unit
                $nextUnit = $this -> getNextNode($previousUnit['id']);           //Get the previous unit's next unit, which still points to the old unit
                if ($nextUnit) {                                                 //If we are deleting the last unit, there is not next unit
                    $nextUnit['previous_content_ID'] = $previousUnit['id'];      //Update the next unit to point at the deleted unit' previous unit
                    $nextUnit -> persist();                                      //Persist these changes to the database
                }
            } else {
                $firstUnit = $this -> getFirstNode();                            //If we deleted the first unit, then we need to set the new first unit to have 0 as previous unit
                if ($firstUnit) {                                                //...Unless the deleted unit was the last content unit
                    $firstUnit['previous_content_ID'] = 0;
                    $firstUnit -> persist();                                         //Persist these changes to the database
                }
            }
        } else {
            throw new EfrontContentException(_UNITDOESNOTEXIST.': '.$removeId, EfrontContentException :: UNIT_NOT_EXISTS); 
        }
    }    
    
    /**
     * Insert unit to tree
     * 
     * This function is used to insert a new unit to the content tree
     * <br/>Example:
     * <code>
     * $unit = array("id"                  => 99,                           //Create the array of the new unit
     *               "name"                => "Test Insert Unit", 
     *               "lessons_ID"          => 1, 
     *               "timestamp"           => time(), 
     *               "ctg_type"            => "theory", 
     *               "active"              => 1, 
     *               "parent_content_ID"   => 5, 
     *               "previous_content_ID" => 5);
     * 
     * $content = new EfrontContentTree(4);                                 //Initialize content tree for lesson with id 4
     * $content -> insertNode($unit);                                       //Insert the new unit
     * </code>
     * 
     * @param array $unit The unit array
     * @param int $parentUnit The parent of the specified node, if it is not set inside the node (not used for the moment)
     * @param int $previousUnit The previous of the specified node, if it is not set inside the node (not used for the moment)
     * @return EfrontUnit The new unit
     * @since 3.5.0
     * @access public
     * @todo implement $parentUnit/$preciousUnit functionality, when not present inside $unit
     */
    public function insertNode($unit, $parentUnit = false, $previousUnit = false) {
        if (!isset($unit['id'])) {
            if (!isset($unit['previous_content_ID'])) {
                $unit['parent_content_ID'] ? $children = $this -> getNodeChildren($unit['parent_content_ID']) : $children = $this -> tree;      //Get the new unit's parent children. If the parent unit is 0, then we will append it to the end of the tree
                foreach ($iterator = new EfrontAttributeFilterIterator(new RecursiveIteratorIterator($children, RecursiveIteratorIterator :: SELF_FIRST), 'id') as $lastUnitId) {}    //Iterate through the parent's children until you reach the last one
                $lastUnitId ? $unit['previous_content_ID'] = $lastUnitId : $unit['previous_content_ID'] = 0;                            //The new unit's parent last unit
            }
            $unit = EfrontUnit :: createUnit($unit);
        }
        
        $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator($this -> tree, RecursiveIteratorIterator :: SELF_FIRST));    //Get an iterator for the current tree. This iterator returns only whole unit arrays and not unit members separately (such as id, timestamp etc)
        $iterator -> rewind();                                                //Initialize iterator

        while ($iterator -> valid() && $iterator -> key() != $unit['previous_content_ID']) {        //Forward iterator index until you reach the unit's previous unit
            $iterator -> next();
        }
        $iterator -> next();                                                            //Advance iterator once more to get the next unit

        if ($iterator -> valid()) {                                                    //If there is a next unit
            $lastUnit = $unit;        //Set the current last unit to be the inserted unit
            foreach (new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($unit), RecursiveIteratorIterator :: SELF_FIRST)) as $lastUnit);    //If the inserted unit is actually a tree branch with children, advance to the last one
            $iterator -> current() -> offsetSet('previous_content_ID', $lastUnit['id']);    //The current unit will have the $lastUnit as previous
            $iterator -> current() -> persist();                                            //Store value to the database
        }
        
        $this -> reset();                                                               //Rebuild content tree, so that the unit may appear to the right place
        return $this -> seekNode($unit['id']);
    }

    /**
     * Append unit to the end of the content tree
     * 
     * This function is the same as insertNode(), only that it
     * resets parent and previous unit information so that the 
     * unit is appended to the end of the content tree.
     * 
     * @param array $unit The unit array
     * @since 3.5.0
     * @access public
     */
    public function appendUnit($unit) {
        $lastUnit = $this -> getLastNode();
        $unit['parent_content_ID']   = 0;
        $unit['previous_content_ID'] = $lastUnit['id'];
        $newUnit = $this -> insertNode($unit);
        return $newUnit;
    }
    
    /**
     * Get the current unit of the tree
     * 
     * This function returns the current unit, in a flat array
     * <br/>Example:
     * <code>
     * $content = new EfrontContentTree(4);             //Create the content tree for lesson with id 4
     * $unit = $content -> getCurrentNode();                //$unit now holds the first unit of the tree
     * </code>
     * 
     * @param int $queryUnit A unit id, to get its array
     * @return array The current unit
     * @since 3.5.0
     * @access public
     * @todo Correct it!
     */
    public function getCurrentNode($queryUnit = false){
        $queryUnit === false ? $unitId = $this -> currentUnitId : $unitId = $queryUnit;
        $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree), RecursiveIteratorIterator :: SELF_FIRST));    //Create iterators for the tree
        $iterator -> rewind();                                                                 //Initialize iterator
        while ($iterator -> valid() && $iterator -> key() != $unitId) {                        //Advance iterator until we reach the designated unit
            $iterator -> next();
        }
        if ($iterator -> valid()) {     
            $flatTree[] = $this -> filterOutChildren($iterator -> current());
        }
        return $flatTree;
    }
    
    
    /**
     * Get the next units in the tree
     * 
     * This function returns the next units, in a flat array.
     * <br/>Example:
     * <code>
     * $content = new EfrontContentTree(4);             //Create the content tree for lesson with id 4
     * $units = $content -> getNextNodes();             //$units now holds all the next units of the current unit
     * $units = $content -> getNextNodes(32);           //$units now holds all the next units of unit 32
     * </code>
     * 
     * @param int $queryUnit A unit id, to get its next units
     * @return array The next units array
     * @since 3.5.0
     * @access public
     * @todo Correct it!
     */
    public function getNextNodes($queryUnit = false) {
        $queryUnit === false ? $unitId = $this -> currentUnitId : $unitId = $queryUnit;        //If queryUnit is not specified, use the current unit
        
        $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree), RecursiveIteratorIterator :: SELF_FIRST));    //Create iterators for the tree
        $iterator -> rewind();                                                                 //Initialize iterator
        while ($iterator -> valid() && $iterator -> key() != $unitId) {                        //Advance iterator until we reach the designated unit
            $iterator -> next();
        }
         
        if ($iterator -> valid()) {                                                            //If we found the designated unit, avance the iterator once more to get its next unit
            $iterator -> next();
        } else {
            throw new EfrontContentException(_UNITDOESNOTEXIST.': '.$unitId, EfrontContentException :: UNIT_NOT_EXISTS);
        }
        
        if ($iterator -> valid()) {                                                            //If there is a next unit, get it
            while ($iterator -> valid()) {                                                     //Advance iterator until we reach the designated unit
                $flatTree[] = $this -> filterOutChildren($iterator -> current());
                $iterator -> next();
            }
            return $flatTree;
        } else {                                                                               //The unit was apparently the last unit; return an empty array
            return array();
        }
    }

    /**
     * Get the previous units 
     * 
     * This function returns the previous units, in a flat array.
     * <br/>Example:
     * <code>
     * $units = $content -> getPreviousNodes();             //$units now holds all the previous units of the current unit
     * $units = $content -> getPreviousNodes(32);           //$units now holds all the previous units of unit 32
     * </code>
     * 
     * @param int $queryUnit A unit id, to get its previous units
     * @return array The previous units array
     * @since 3.5.0
     * @access public
     * @todo Correct it!
     */
    public function getPreviousNodes($queryUnit = false) {
        $queryUnit === false ? $unitId = $this -> currentUnitId : $unitId = $queryUnit;        //If queryUnit is not specified, use the current unit
        
        $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree), RecursiveIteratorIterator :: SELF_FIRST));    //Create iterators for the tree
        $iterator -> rewind();                                                                 //Initialize iterator
        while ($iterator -> valid() && $iterator -> key() != $unitId) {                        //Advance iterator until we reach the designated unit
            $flatTree[] = $this -> filterOutChildren($iterator -> current());                  //Assign units in each loop to the flat array (without children)
            $iterator -> next();
        }
         
        if ($iterator -> valid()) {                                                            //We reached the designated unit, so return 
            return $flatTree;
        } else {                                                                    
            if (!isset($flatTree)) {                                                           //If iterator value is not valid, and $flatTree is not set, this means that the designated unit was the first, so return empty array
                return array();
            } else {                                                                           //If $flatTree is set and iterator is invalid, the unit speify did not exist
                throw new EfrontContentException(_UNITDOESNOTEXIST.': '.$unitId, EfrontContentException :: UNIT_NOT_EXISTS);
            }
        }        
    }
       
    /**
     * Get content rules
     * 
     * This function retrieves the content rules
     * <br/>Example:
     * <code>
     * $content -> getRules();              //Returns an array with lesson rules
     * $content -> getRules(43);            //Returns an array with lesson rules that refer to unit with id 43
     * </code>
     * 
     * @param int $queryUnit If set, return rules for this unit only
     * @return array The lesson rules
     * @since 3.5.0
     * @access public
     */
    public function getRules($queryUnit = false) {
        if ($this -> rules === false) {
            $contentIds = array();
            foreach (new EfrontAttributeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree)), 'id') as $key => $id) {
                $contentIds[] = $id;
            }
            $rules = array();
            if (sizeof($contentIds) > 0) {
                $rules = eF_getTableData("rules", "*", "content_ID in (".implode(",", $contentIds).") or lessons_ID=".$this -> lessonId);
            }
            if (sizeof($rules) > 0) {
                foreach ($rules as $value) {
                    $this -> rules[$value['id']] = $value;
                }
            } else {
                $this -> rules = array();
            }
        }        
        
        if ($queryUnit) {
            $unitRules = array();
            foreach ($this -> rules as $key => $rule) {
                if ($rule['content_ID'] == $queryUnit || $rule['content_ID'] == 0) {
                    $unitRules[$key] = $rule;
                }
            }
            return $unitRules;
        } else {
            return $this -> rules;
        }
    }
    
    /**
     * Delete rules
     * 
     * This function can be used to delete one or more rules
     * <br/>Example:
     * <code>
     * $content -> deleteRules(54);                                 //Delete rule with id 54
     * $content -> deleteRules(array(54,34,76));                    //Delete rules with specified ids
     * </code>
     * 
     * @param mixed $rules one or more rule ids to delete
     * @return The content rules left
     * @since 3.5.0
     * @access public
     */
    public function deleteRules($rules) {
        if ($this -> rules === false) {                            //Initialize rules, if you haven't done so
            $this -> getRules();
        }
        if (!is_array($rules)) {                                    //Convert single rule to array
            $rules = array($rules);
        }
        foreach ($rules as $ruleId) {
            if (eF_checkParameter($ruleId, 'id') && in_array($ruleId, array_keys($this -> rules))) {
                eF_deleteTableData("rules", "id=$ruleId");            
                unset($this -> rules[$ruleId]);    
            }
        }
        return $this -> rules;
    }
    
    /**
     * Check if the user can access the specified unit
     *
     * @param int $queryUnit The unit to check for
     * @param unknown_type $seenUnits
     * @return unknown
     * @since 3.5.0
     * @access public
     */
    public function checkRules($queryUnit, $seenContent) {
        $rules = $this -> getRules($queryUnit);
        $allow = true;

        foreach ($rules as $id => $rule) {
            switch ($rule['rule_type']) {
                case 'always':
                    return _YOUHAVEBEENEXCLUDEDBYPROFESSOR;
                    break;
                case 'hasnot_seen':
                    if (!in_array($rule['rule_content_ID'], array_keys($seenContent))) {
                        try {
                            $ruleUnit = $this -> seekNode($rule['rule_content_ID']);
                            if ($ruleUnit['active']) {
                                return _MUSTFIRSTREADUNIT.' <a href = "student.php?ctg=content&view_unit='.$ruleUnit['id'].'">'.$ruleUnit['name'].'</a><br/>';
                            }
                        } catch (EfrontContentException $e) {}
                    }
                    break;
                case 'hasnot_passed':
                    if (!in_array($rule['rule_content_ID'], array_keys($seenContent)) || $seenContent[$rule['rule_content_ID']] / 100 < $rule['rule_option']) {
                        try {
                            $ruleUnit = $this -> seekNode($rule['rule_content_ID']);
                            if ($ruleUnit['active']) {
                                return _MUSTFIRSTTAKEATLEAST.' '.($rule['rule_option'] * 100).' % '._ATTEST.' <a href="student.php?ctg=tests&view_unit='.$ruleUnit['id'].'">'.$ruleUnit['name'].'</a><br/>';
                            }
                        } catch (EfrontContentException $e) {}
                    }
                    break;
                case 'serial':
                    //Get the visitable units                    
                    foreach ($iterator = new EfrontVisitableFilterIterator(new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree), RecursiveIteratorIterator :: SELF_FIRST))) as $key => $value) {
                        $visitableContentIds[$key] = $key;                                                    //Get the not-test unit ids for this content
                    }                                        
                    //Locate the first active previous unit
                    $previousUnit = $this -> getPreviousNode($queryUnit);
                    while ($previousUnit && !in_array($previousUnit['id'], $visitableContentIds)) {
                        $previousUnit = $this -> getPreviousNode($previousUnit['id']);
                    }
                    //A previous active unit exists. Is it complete?
                    if ($previousUnit && !in_array($previousUnit['id'], array_keys($seenContent))) {
                        return _MUSTSEECONTENTSERIALSOMUSTFIRSTREADUNIT.' <a href = "student.php?ctg=content&view_unit='.$previousUnit['id'].'">'.$previousUnit['name'].'</a><br/>';
                    }
                    
                    break;
                case 'tree':                    
                    break;
                default: break;
            }
        }
        return true;
    }
    
    /**
     * Get content comments
     * 
     * This function is used to retrive all the comments that have been posted
     * to the current content.
     * <br/>Example:
     * <code>
     * $content = new EfrontContentTree(34);		//Initialize content for lesson with id 34
     * $comments = $content -> getComments();		//Return an array of arrays, each of which holds the comment data
     * </code>
     *
     * @param int $queryUnit If this parameter is specified, then only comments regarding the specified unit are returned
     * @return array The content comments
     * @since 3.5.2
     * @access public
     */
    public function getComments($queryUnit = false) {
        if ($queryUnit && eF_checkParameter($queryUnit, 'id')) {
            $result = eF_getTableData("comments", "*", "content_ID = ".$queryUnit);
        } else {
            //Get all ids
            $contentIds = array();
            foreach (new EfrontAttributeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree)), 'id') as $key => $id) {
                $contentIds[] = $id;
            }            
            if (sizeof($contentIds) > 0) {
                $result = eF_getTableData("comments", "*", "content_ID in (".implode(",", $contentIds).")");
            } else {
                $result = array();
            }
        }
        $comments = array();
        foreach ($result as $value) {
            $comments[$value['id']] = $value;
        }
        
        return $comments;
    }
    
    /**
     * Delete comments from content
     * 
     * This function is used to delete the specified comments from the 
     * content.
     * <br/>Example:
     * <code>
     * $content = new EfrontContentTree(34);		//Initialize content for lesson with id 34
     * $content -> deleteComments(4);	//Delete comment with ids 4
     * $content -> deleteComments(array(4,6,2));	//Delete comments with ids 4,6,2
     * $content -> deleteComments(array_keys($content -> getComments()));	//Delete all comments
     * </code>
     *
     * @param mixed $comments A comment id or an array of comment ids
     * @since 3.5.2
     * @access public
     */
    public function deleteComments($comments) {
        if (!is_array($comments)) {
            $comments = array($comments);
        }
        $contentComments = array_keys($this -> getComments());
        
        $ids = array();
        foreach ($comments as $key => $value) {
            if (in_array($value, $contentComments)) {
                $ids[] = $value;
            }
        }
        if (sizeof($ids) > 0) {
            eF_deleteTableData("comments", "id in (".implode(",", $ids).")");        
        } 
    }

    /**
     * Repair tree
     * 
     * This function is the "last resort": It rearranges all the 
     * lesson's units so that they are visible to the system. It 
     * revokes any succession information and creates a flat
     * tree, where units are arbitrarily sorted.
     * <br/>Example:
     * <code>
     * $content = new EfrontContentTree(4);     //Create the content tree for lesson with id 4
     * $content -> repairTree();                //Repair tree. Now, the content tree will be flat, containing all the lesson units
     * </code>
     *  
     * @since 3.5.0
     * @access public
     * @static
     */
    public function repairTree() {
        $units    = eF_getTableData("content", "*", "lessons_ID=".$this -> lessonId);                    //Get all lesson units
        $previous = 0;
        foreach ($units as $key => $value) {
            eF_updateTableData("content", array("previous_content_ID" => $previous, "parent_content_ID" => 0), "id=".$value['id']);  //Update succession information and erase parent information 
            $previous = $value['id'];
        }
    }
    
    /**
     * Mark seen nodes
     * 
     * This function gets the units that the user has seen
     * and sets the 'seen' property either to 1 or to 0.
     * <br/>Example:
     * <code>
     * $currentContent = new EfrontContentTree(5);           //Initialize content for lesson with id 5
     * $currentContent -> markSeenNodes($currentUser);       //Mark the seen content for user in object $currentUser
     * </code>
     *
     * @param mixed $user Either a user login or an EfrontUser object
     * @since 3.5.0
     * @access public
     */
    public function markSeenNodes($user) {
        $user instanceof EfrontUser ? $login = $user -> user['login'] : $login = $user;

        $seenContent = EfrontStats :: getStudentsSeenContent($this -> lessonId, $login);
        $seenNodes   = array_keys($seenContent[$this -> lessonId][$login]);

        $resultScorm   = eF_getTabledataFlat("scorm_data", "content_ID, lesson_status", "users_LOGIN='$login'");
        $resultScorm   = array_combine($resultScorm['content_ID'], $resultScorm['lesson_status']);
        
        $result        = eF_getTableData("content c, completed_tests ct, tests t", "t.content_ID, ct.status, ct.timestamp", "c.id = t.content_ID and c.lessons_ID = ".$this -> lessonId." and ct.tests_ID = t.id and ct.users_LOGIN='$login'");       
        foreach ($result as $value) {
            $resultTests[$value['content_ID']]      = $value['status'];
            $resultTestsTimes[$value['content_ID']] = $value['timestamp'];
        }

        $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree), RecursiveIteratorIterator :: SELF_FIRST));
        foreach ($iterator as $key => $value) {
            in_array($key, $seenNodes) || $resultScorm[$key] == 'completed' || $resultScorm[$key] == 'passed' ? $value['seen'] = 1 : $value['seen'] = 0;
            
            $resultScorm[$key] == 'incomplete' ? $value['incomplete'] = 1 : $value['incomplete'] = 0;
            $resultScorm[$key] == 'failed'     ? $value['failed']     = 1 : $value['failed']     = 0;
            
            $resultTests[$key] == 'incomplete' ? $value['incomplete'] = $resultTestsTimes[$key] : $value['incomplete'] = 0;
            $resultTests[$key] == 'failed'     ? $value['failed']     = 1 : $value['failed']     = 0;
        }        
    }
    
    /**
     * Copy test
     * 
     * This function copies a test into the current content tree
     * <br/>Example:
     * <code>
     * $currentContent = new EfrontContentTree(5);           //Initialize content for lesson with id 5
     * $currentContent -> copyTest(3, false);   //Copy the corresponding test into the content tree (at its end)
     * </code>
     *
     * @param int $testId The id of the test to be copied
     * @param mixed $targetUnit The id of the parent unit (or the parent EfrontUnit)in which the new unit will be copied, or false (the unit will be appended at the end)
     * @return EfrontUnit The newly created test unit object
     * @since 3.5.0
     * @access public
     */
    public function copyTest($testId, $targetUnit = false){
        $oldTest                 = new EfrontTest($testId);
        $oldUnit                 = $oldTest -> getUnit();
        $testarray               = $oldTest -> test;
        $newUnit                 = $this -> copySimpleUnit($oldUnit, $targetUnit);
        $newTest                 = EfrontTest::createTest($newUnit, $testarray);
        $questions               = $oldTest -> getQuestions(true);
        $questions_in            = array();
        foreach ($questions as $oldQuestion){
            $question_array = $oldQuestion -> question;
            $question_array['content_ID'] = $newUnit -> offsetGet('id');
			$question_array['lessons_ID'] = $newUnit -> offsetGet('lessons_ID');
            unset($question_array['id']);
            unset($question_array['weight']);
            unset($question_array['previous_question_ID']);
            unset($question_array['type_icon']);
            unset($question_array['explanation']);
            unset($question_array['plain_text']);
            $newQuestion = Question :: createQuestion($question_array);
            $qid = $newQuestion -> question['id'];   
            $questions_in[$qid] = $oldTest -> getAbsoluteQuestionWeight($oldQuestion -> question['id']);   
        }
        $newTest -> addQuestions($questions_in);
        return $newUnit;
    }

    /**
     * Copy unit
     * 
     * This function copies a unit (along with its children)into the current content tree
     * If the unit corresponds to a test, then it copies the corresponding test
     * <br/>Example:
     * <code>
     * $currentContent = new EfrontContentTree(5);           //Initialize content for lesson with id 5
     * $sourceUnit = new EfrontUnit(20);                     //Get the unit with id = 20
     * $currentContent -> copyUnit($sourceUnit, false);   //Copy the source unit into the content tree (at its end)
     * </code>
     *
     * @param EfrontUnit $sourceUnit The unit object to be copied
     * @param mixed $targetUnit The id of the parent unit (or the parent EfrontUnit)in which the new unit will be copied, or false (the unit will be appended at the end)
     * @return EfrontUnit The newly created unit object
     * @since 3.5.0
     * @access public
     */
    public function copyUnit($sourceUnit, $targetUnit = false, $previousContentId = false) {
        if (!($sourceUnit instanceof EfrontUnit)) {
            $sourceUnit = new EfrontUnit($sourceUnit);
        }
        if ($sourceUnit -> offsetGet('ctg_type') == 'tests'){
            $tid      = eF_getTableData("tests, content", "tests.id as id", "tests.content_ID = content.id and content.id =".$sourceUnit -> offsetGet('id'));
            $testUnit = $this -> copyTest($tid[0]['id'], $targetUnit);    
            return $testUnit;
        } else if ($sourceUnit -> offsetGet('ctg_type') == "scorm"){
            $sid       = eF_getTableData("scorm_data, content", "scorm_data.id", "scorm_data.content_ID = content.id and content.id =".$sourceUnit -> offsetGet('id')." and scorm_data.users_LOGIN is null");
            $scormUnit = $this -> copyScorm($sid[0]['id'], $sourceUnit, $targetUnit);
            return $scormUnit;
        } else {
            if ($targetUnit && $targetUnit != 0){
                if (!($targetUnit instanceof EfrontUnit)) {
                    $targetUnit = new EfrontUnit($targetUnit);
                }

                $newParentUnit = $this -> copySimpleUnit($sourceUnit, $targetUnit, $previousContentId);
            }
            else{
                $newParentUnit = $this -> copySimpleUnit($sourceUnit, false);
            }
            
            $sourceTree    = new EfrontContentTree($sourceUnit -> offsetGet('lessons_ID'));
            $children      = $sourceTree -> getNodeChildren($sourceUnit);   //$children is a RecursiveArrayIterator
            while ($children -> valid()) {
                $childUnit = $children -> current();
                if ($childUnit instanceof EfrontUnit){
                    $this -> copyUnit($childUnit, $newParentUnit);
                }
                $children -> next();
            }
            return $newParentUnit;
        }
    }


    /**
     * Copy simple unit
     * 
     * This function copies a unit (NOT its children) into the current content tree
     * <br/>Example:
     * <code>
     * $currentContent = new EfrontContentTree(5);           //Initialize content for lesson with id 5
     * $sourceUnit = new EfrontUnit(20);                     //Get the unit with id = 20
     * $currentContent -> copySimpleUnit($sourceUnit, false);   //Copy the source unit into the content tree (at its end)
     * </code>
     *
     * @param EfrontUnit $sourceUnit The unit object to be copied
     * @param mixed $targetUnit The id of the parent unit (or the parent EfrontUnit)in which the new unit will be copied, or false (the unit will be appended at the end)
     * @param mixed $previousUnit The id of the previous unit (or the unit itself) of the new unit, or false (the unit will be put to the end of the units)
     * @return EfrontUnit The newly created unit object
     * @since 3.5.0
     * @access public
     */
    public function copySimpleUnit($sourceUnit, $targetUnit = false, $previousUnit = false){
    	if (!($sourceUnit instanceof EfrontUnit)) {
    		$sourceUnit = new EfrontUnit($sourceUnit);
    	}
    	$newUnit['name']       = $sourceUnit -> offsetGet('name');
        $newUnit['ctg_type']   = $sourceUnit -> offsetGet('ctg_type');
        $newUnit['data']       = $sourceUnit -> offsetGet('data');
        $newUnit['lessons_ID'] = $this -> lessonId;       
         
        if ($targetUnit) {
            if ($targetUnit instanceOf EfrontUnit) {
                $newUnit['parent_content_ID'] = $targetUnit -> offsetGet('id');
            } else if (eF_checkParameter($targetUnit, 'id')) {
                $newUnit['parent_content_ID'] = $targetUnit;    
            }
            if ($previousUnit instanceOf EfrontUnit) {
                $newUnit['previous_content_ID'] = $previousUnit -> offsetGet('id');
            } else if (eF_checkParameter($previousUnit, 'id')) {
                $newUnit['previous_content_ID'] = $previousUnit;    
            } 
            $unit = $this -> insertNode($newUnit);
        } else {
            $unit = $this -> appendUnit($newUnit);
        }       

        if ($copyFiles){
            $files  = $unit -> getFiles();
            $lesson = new EfrontLesson($this -> lessonId);
            $data   = $unit -> offsetGet('data');
            foreach ($files as $file){                
                try {
                    $sourceFile = new EfrontFile($file);
                    $copiedFile = $sourceFile -> copy($lesson -> getDirectory().'/'.basename($sourceFile['path']), false);
                    str_replace("view_file.php?file=".$file, "view_file.php?file=".$copiedFile -> offsetGet('id'), $data);
                    $data = preg_replace("#content/lessons/".$sourceUnit['lessons_ID']."/(.*)#", "content/lessons/".$this -> lessonId.'/${1}', $data);
                } catch (EfrontFileException $e) {
                    if ($e -> getCode() == EfrontFileException :: FILE_ALREADY_EXISTS) {
                        $copiedFile = new EfrontFile($lesson -> getDirectory().'/'.basename($sourceFile['path']));
                        str_replace("view_file.php?file=".$file, "view_file.php?file=".$copiedFile -> offsetGet('id'), $data);
                        $data = preg_replace("#content/lessons/".$sourceUnit['lessons_ID']."/(.*)#", "content/lessons/".$this -> lessonId.'/${1}', $data);                    
                    }
                } //this means that the file already exists            
    
            }
            $unit -> offsetSet('data', $data);
        }

        $unit -> persist();
        return $unit;
    }
    
    /**
     * Copy a simple scorm unit or test
     * 
     * This function copies a scorm unit/test (NOT its children) into the current content tree
     * <br/>Example:
     * <code>
     * $currentContent = new EfrontContentTree(5);           //Initialize content for lesson with id 5
     * $currentContent -> copySimpleUnit(3, false);   //Copy the scorm unit with id = 3 into the content tree (at its end)
     * </code>
     *
     * @param int The id of the scorm unit/test (in the scorm_data table)
     * @param EfrontUnit $sourceUnit The unit object to be copied
     * @param mixed $targetUnit The id of the parent unit (or the parent EfrontUnit)in which the new unit will be copied, or false (the unit will be appended at the end)
     * @return EfrontUnit The newly created unit object
     * @since 3.5.0
     * @access public
     */
    public function copyScorm($sid, $sourceUnit, $targetUnit){
        $newUnit = $this -> copySimpleUnit($sourceUnit, $targetUnit, false);
        $data = ef_getTableData("scorm_data", "*", "id = ".$sid);
        $scorm_array = $data[0];
        unset($scorm_array['id']);
        $scorm_array['content_ID'] = $newUnit -> offsetGet('id');
        ef_insertTableData("scorm_data", $scorm_array);
        return $newUnit;
    }



    public function fromXMLNode($node){
        for ($i = 0; $i < sizeof($node->content->unit); $i++){
            importUnitFromXML($xml->unit[$i], 0);
        }
        
        $this -> reset();
    }

    public function fromXML($xmlfile){
        $xml = simplexml_load_file($xmlfile);
        for ($i = 0; $i < sizeof($xml->content->unit); $i++){
            importUnitFromXML($xml->unit[$i], 0);
        }
        
        $this -> reset();
    }
    
    private function importUnitFromXML($unitelement, $parentid){
        $fields = array();
        $fields['name'] = (string) $unitelement->name;
        $fields['data'] = (string) $unitelement->data;
        $fields['ctg_type'] = (string) $unitelement->ctg_type; 
        $fields['parent_content_ID'] = $parentid;
        $uid = ef_insertTableData("content", $fields);
		EfrontSearch :: insertText($fields['name'], $uid, "content", "title");
		EfrontSearch :: insertText($fields['data'], $uid, "content", "data");
        if ($fields['ctg_type'] == 'tests'){
            $testfields = array();
            $testfields['content_id'] = (string) $unitelement->id;
            $testfields['duration'] = (string) $unitelement->test[0]->duration;
            $testfields['redoable'] = (string) $unitelement->test[0]->redoable;
            $testfields['onebyone'] = (string) $unitelement->test[0]->onebyone;
            $testfields['answers'] = (string) $unitelement->test[0]->answers;
            $testfields['description'] = (string) $unitelement->test[0]->description;
            $testfields['shuffle_questions'] = (string) $unitelement->test[0]->shuffle_questions;
            $testfields['shuffle_answers'] = (string) $unitelement->test[0]->shuffle_answers;
            $testfields['given_answers'] = (string) $unitelement->test[0]->given_answers;
            $tid = ef_insertTableData("tests", $testfields);
        }
        //import the subunits
        for ($i = 0; $i < sizeof($unitelement->unit); $i++){
            importUnitFromXML($unitelement->unit[$i]);
        }
    }

    /**
     * Create HTML representation of the content tree
     * 
     * This function is used to create an HTML representation of the content tree
     * The representation is based on javascript library drag_drop_folder_tree.js
     * If an iterator is not specified, then the tree displayed corrresponds to the default 
     * tree (excluding inactive units). Otherwise, the specified iterator is used.
     * $treeId should be specified in case there will be more than one trees on the same page.
     * $options specifies the appearance and behaviour of the tree. Possible options are:
     * <br/>- edit   (true/false)
     * <br/>- delete (true/false)
     * <br/>- activate (true/false)
     * <br/>- drag (true/false)
     * <br/>- noclick (true/false)
     * <br/>- selectedNode (node id)
     * <br/>- truncateNames (maximum length)
     * <br/>- expand (true/false)
     * <br/>- tree_root (true/false) (whether to display a root node)
     * <br/>- show_hide (true/false) (whether to display the show/hide header)
     * <br/>- onclick (function)
     * <br/>- custom (any custom code to be put next to each unit name) 
     * <br/>Example:
     * <code>
     * echo $content -> toHTML();                               //Display tree with all defaults
     * $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($content -> tree), RecursiveIteratorIterator :: SELF_FIRST), array('ctg_type' => 'theory'));
     * echo $content -> toHTML($iterator);                      //Display tree with only theory nodes
     * echo $content -> toHTML($iterator, 'theory_tree')        //Display tree with only theory nodes, having id 'theory_tree'
     * echo $content -> toHTML($iterator, 'theory_tree', array('edit' => 1, 'delete' => 1))     //Display tree with only theory nodes, having id 'theory_tree', and which is editable and deleteable
     * </code>
     *  
     * @param RecursiveIteratorIterator $iterator The content tree iterator
     * @param string $treeId The HTML id that will be assigned to the tree
     * @param array $options Behaviour options for the tree
     * @return string The HTML code
     * @since 3.5.0
     * @access public
     */
    public function toHTML($iterator = false, $treeId = false, $options = array()) {
        if (!$iterator) {
            $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree), RecursiveIteratorIterator :: SELF_FIRST), array('active' => 1));    //Default iterator excludes non-active units
        }
        if (!$treeId) {
            $treeId = 'dhtml_tree';
        }

        $iterator   -> rewind();
        $current    = $iterator -> current();
        $depth      = $iterator -> getDepth();
        $initDepth  = $depth;
        $treeString = '';
        $count      = 0;                                //Counts the total number of nodes, used to signify whether the tree has content
        while ($iterator -> valid()) {
            $iterator -> next();
            
            $current['ctg_type'] == 'tests' ? $contentType = 'tests' : $contentType = 'content';
            $linkClass = array();
            $liClass   = array();
            $tooltip   = array();
            $fullName  = $current['name'];                                                    //Full name will be needed for the link title, which is never truncated
            
            //Decide whether the unit name will be truncated
            $unitName = $current['name'];
            if (isset($options['truncateNames']) && mb_strlen($current['name']) > $options['truncateNames']) {
                $unitName = mb_substr($current['name'], 0, $options['truncateNames']).'...&nbsp;&nbsp;&nbsp;';
            }

            //Create the activate/deactivate link
            //isset($options['activate'])      && $options['activate']                                                                             ? $activateLink    = '<a href = "'.basename($_SERVER['PHP_SELF']).'?ctg='.$contentType.'&op=unit_order&activate_unit='.$current['id'].'"><img style = "vertical-align:middle" src = "images/16x16/trafficlight_green.png" title = "'._ACTIVATE.'" alt = "'._ACTIVATE.'" border = "0" /></a>'  : $activateLink = '';
            $activateLink = '';
            if (isset($options['activate']) && $options['activate']) {
                if ($current['active']) {
                    $image = 'trafficlight_green.png';
                    $title = _DEACTIVATE;
                } else {
                    $image = 'trafficlight_red.png';
                    $title = _ACTIVATE;
                }
                $activateLink = '<a href = "javascript:void(0)" onclick = "JSTreeObj.activateUnit(null, $(\'node'.$current['id'].'\').down().next().next())"><img style = "vertical-align:middle" src = "images/16x16/'.$image.'" title = "'.$title.'" alt = "'.$title.'" border = "0" /></a>';
            }
            
            //Create the edit link
            $editLink = '';
            if (isset($options['edit']) && $options['edit']) {
                $editLink = '<a href = "'.basename($_SERVER['PHP_SELF']).'?ctg='.$contentType.'&edit_unit='.$current['id'].'"><img style = "vertical-align:middle" src = "images/16x16/edit.png" title = "'._EDIT.'" alt = "'._EDIT.'" border = "0" /></a>';
            }
            
            //isset($options['delete'])        && $options['delete']                                                                               ? $deleteLink      = '<a href = "'.basename($_SERVER['PHP_SELF']).'?ctg='.$contentType.'&op=unit_order&delete_unit='.$current['id'].'" onclick = "return confirm(\''._IRREVERSIBLEACTIONDELETESUBUNITSAREYOUSURE.'\')"><img style = "position:absolute;vertical-align:middle" src = "images/16x16/delete.png" title = "'._DELETE.'" alt = "'._DELETE.'" border = "0" /></a>'  : $deleteLink   = '';
            //Create the delete link
            $deleteLink = '';
            if (isset($options['delete']) && $options['delete']) {
                $deleteLink = '<a href = "javascript:void(0)" onclick = "JSTreeObj.deleteUnit(null, $(\'node'.$current['id'].'\').down().next().next())"><img style = "vertical-align:middle" src = "images/16x16/delete.png" title = "'._DELETE.'" alt = "'._DELETE.'" border = "0" /></a>';
            }

            //Create the target link
            $targetLink = basename($_SERVER['PHP_SELF']).'?view_unit='.$current['id'];
            if (isset($options['noclick']) && $options['noclick']) {
                $targetLink = 'javascript:void(0)';         
            }
            
            //Decide whether the tree is draggable            
            isset($options['drag']) && $options['drag'] ? $nodrag = 'false' : $nodrag = 'true'; 
            
            //Should the tree expand           
            isset($options['expand']) && $options['expand'] ? $expand = $options['expand'] : null;         
            
            //Show the expand all/collapse all link
            !isset($options['show_hide']) || $options['show_hide'] ? $showHide = true : $showHide = false;
            
            //Set the selected node
            if (isset($options['selectedNode'])  && $options['selectedNode'] == $current['id']) {
                $linkClass[] = 'drag_tree_current';
            }            
            //Set the display style according to whether the unit has data
            if ($current['data'] == '' && $current['ctg_type'] != 'tests') {
                $linkClass[] = 'treeNoContent';
                $fullName.= ' ('._EMPTYUNIT.')';
                $targetLink  = 'javascript:void(0)';
            }
            //Set the class name, based on the unit status
            
            if (isset($current['incomplete']) && $current['incomplete']) {
                $liClass[] = $current['ctg_type'].'_incomplete';
                $tooltip[] = '('._TESTSTARTEDAT.': '.formatTimestamp($current['incomplete'], 'time').')';
            } else if (isset($current['failed']) && $current['failed']) {
                $liClass[] = $current['ctg_type'].'_failed';
            } else if ((isset($current['seen']) && $current['seen']) || isset($current['completed']) && $current['completed']) {
                $liClass[] = $current['ctg_type'].'_seen';
            } else {
                $liClass[] = $current['ctg_type'];
            }

            //$toolsString = '<span class = "toolsDiv" style = "position:absolute">'.$activateLink.$editLink.$deleteLink.'</span>';
            $toolsString  = '<span>'.$activateLink.$editLink.$deleteLink.$options['custom'][$current['id']].'</span>';            
            $treeString  .= '
                <li class = "'.implode(" ", $liClass).'" id = "node'.$current['id'].'" noDrag = "'.$nodrag.'" noRename = "true" noDelete = "true">
                    <a '.($options['onclick'] ? 'onclick = "'.$options['onclick'].'"' : null).' class = "'.(!$current['active'] ? 'treeinactive' : '').' treeLink '.implode(" ", $linkClass).'" href = "'.$targetLink.'" title = "'.$fullName.' '.implode(" ", $tooltip).'">'.$unitName."</a>&nbsp;".$toolsString;
            
            $iterator -> getDepth() > $depth ? $treeString .= '<ul>' : $treeString .= '</li>';

            for ($i = $depth; $i > $iterator -> getDepth() && $i > $initDepth; $i--) {
                $treeString .= '</ul></li>';
            }
            $current = $iterator -> current();
            $depth   = $iterator -> getDepth();
            $count++;
        }

        if ($showHide) {
            $str .= '
                <div id = "expand_collapse_div" '.(isset($expand) ? 'expand = "'.$expand.'"' : null).'>
                    <b><a href = "javascript:void(0)" onclick = "treeObj.setTreeId(\''.$treeId.'\');treeObj.collapseAll();">'._HIDEALL.'</a></b> /
                    <b><a href = "javascript:void(0)" onclick = "treeObj.setTreeId(\''.$treeId.'\');treeObj.expandAll();"  >'._SHOWALL.'</a></b><br/>
                </div>';            
        }
        $str .= '
            <table>
            	<tr><td>
            		<ul id = "'.$treeId.'" class = "dhtmlgoodies_tree" selectedNode = "'.$options['selectedNode'].'">';
        if ($options['tree_root']) {
            $str .= '
        			<li style = "white-space:nowrap;" class = "theory" id = "0" noDrag = "false">
                        <a class = "treeactive treeLink" href = "javascript:void(0)" title = "'._TREEROOT.'">'._TREEROOT."</a>&nbsp;";
        }            		
        $str .=         $treeString.'
            		</ul>
            	</td></tr>
            </table>';

        return $str;
    }
    

    /**
     * Create array to be used for HTML options
     * 
     * This function is used to create a structure that can be used
     * in select lists. The array is of the form [content id] => [content name string]
     * where content name is prepended with spaces and special characters "&raquo;" that 
     * denote its depth.
     * <br/>Example:
     * <code>
     * $optionsArray = $content -> toHTMLSelectOptions();
     * </code>
     * An iterator may be optionally specified, in order to display specific units (by default 
     * all active units are used).
     * Note that unit names more than 50 characters long are truncated. 
     *
     * @param RecursiveIteratorIterator $iterator The tree iterator to be used
     * @return array The options array
     * @since 3.5.0 
     * @access public
     */
    public function toHTMLSelectOptions($iterator = false) {
        if (!$iterator) {
            $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree), RecursiveIteratorIterator :: SELF_FIRST), array('active' => 1));    //Default iterator excludes non-active units
        }
        $optionsArray = array();
        foreach ($iterator as $value) {
            mb_strlen($value['name']) > 50 ? $value['name'] = mb_substr($value['name'], 0, 50).'...' : null;
            if ($iterator -> getDepth()) {
                $optionsArray[$value['id']] = implode("", array_fill(0, $iterator -> getDepth(), "&nbsp;&nbsp;")).'&raquo;&nbsp;'.$value['name'];    //This line prints spaces and a >> in front of every unit. The spaces number depend on the depth of the unit
            } else {
                $optionsArray[$value['id']] = $value['name'];
            }
        }
        
        return $optionsArray;
    }
    
    /**
     * Return path strings for each unit
     *
     * This function returns path strings for each unit, depicting its position in the content tree and 
     * its parents.
     * <br>Example:
     * <code>
     * $content = new EfrontContentTree(4);                                 //Initialize content tree for lesson with id 4
     * $patString = $content -> toPathStrings();
     * //Returns an array with contents like:
     * //[364] => Unit 1: Overview
     * //[360] => Unit 2: Introduction to mathematics
     * //[361] => Unit 2: Introduction to mathematics » Unit 2.1: Geometry 
     * </code>
     * 
     * @param RecursiveIteratorIterator $iterator The tree iterator to be used
     * @return array The path strings array
     * @since 3.5.2
     * @access public
     */
    public function toPathStrings($iterator = false) {
        if (!$iterator) {
            $iterator = new EfrontNodeFilterIterator(new RecursiveIteratorIterator(new RecursiveArrayIterator($this -> tree), RecursiveIteratorIterator :: SELF_FIRST), array('active' => 1));    //Default iterator excludes non-active units
        }
        $pathStrings = array();
        foreach ($iterator as $value) {
            mb_strlen($value['name']) > 50 ? $value['name'] = mb_substr($value['name'], 0, 50).'...' : null;
            foreach ($this -> getNodeAncestors($value['id']) as $key => $node) {
                 $pathStrings[$value['id']][$node['id']] = $node['name'];
            }             
            $pathStrings[$value['id']] = implode("&nbsp;&raquo;&nbsp;", array_reverse($pathStrings[$value['id']]));            
        }
        
        return $pathStrings;
    }
}


/**
 * Iterator Filter for traversing only visitable units, incuding empty ones
 * 
 * @package eFront
 * @version 3.5.0
 */
class EfrontVisitableAndEmptyFilterIterator extends FilterIterator
{
    /**
     * Accepts only units that may be visited, i.e. are active 
     *
     * @return boolean
     * @since 3.5.0
     * @access public
     */
    function accept() {
        $current = $this -> current();
        return $current instanceof ArrayObject && ($current['active'] == 1 && $current['publish'] == 1);
    }
}

/**
 * Iterator Filter for traversing only visitable units
 * 
 * @package eFront
 * @version 3.5.0
 */
class EfrontVisitableFilterIterator extends FilterIterator
{
    /**
     * Accepts only units that may be visited, i.e. are active and either have content or are tests
     *
     * @return boolean
     * @since 3.5.0
     * @access public
     */
    function accept() {
        $current = $this -> current();
        return $current instanceof ArrayObject && ($current['active'] == 1 && $current['publish'] == 1 && ($current['data'] != '' || $current['ctg_type'] == 'tests' || $current['ctg_type'] == 'scorm_test'));
    }
}

/**
 * Iterator Filter for traversing only SCORM units
 *
 * @package eFront
 * @version 3.5.0
 */
class EfrontSCORMFilterIterator extends FilterIterator
{
    /**
     * Accepts only SCORM units
     *
     * @return boolean
     * @since 3.5.0
     * @access public
     */
    function accept() {
        if ($this -> current() -> offsetGet('ctg_type') == 'scorm' || $this -> current() -> offsetGet('ctg_type') == 'scorm_test') {
            return true;
        }
    }
}

/**
 * Iterator Filter for traversing only non-SCORM units
 *
 * @package eFront
 * @version 3.5.0
 */
class EfrontNoSCORMFilterIterator extends FilterIterator
{
    /**
     * Accepts only non-SCORM units
     *
     * @return boolean
     * @since 3.5.0
     * @access public
     */
    function accept() {
        if ($this -> current() -> offsetGet('ctg_type') != 'scorm' && $this -> current() -> offsetGet('ctg_type') != 'scorm_test') {
            return true;
        }
    }
}

/**
 * Iterator Filter for traversing only Test units
 *
 * @package eFront
 * @version 3.5.0
 */
class EfrontTestsFilterIterator extends FilterIterator
{
    /**
     * Accepts only test units (normal or SCORM)
     *
     * @return boolean
     * @since 3.5.0
     * @access public
     */
    function accept() {
        if ($this -> current() -> offsetGet('ctg_type') == 'tests' || $this -> current() -> offsetGet('ctg_type') == 'scorm_test') {
            return true;
        }
    }
}

/**
 * Iterator Filter for traversing only Theory units
 *
 * @package eFront
 * @version 3.5.0
 */
class EfrontTheoryFilterIterator extends FilterIterator
{
    /**
     * Accepts only theory and SCORM units
     *
     * @return boolean
     * @since 3.5.0
     * @access public
     */
    function accept() {
        if ($this -> current() -> offsetGet('ctg_type') == 'theory' || $this -> current() -> offsetGet('ctg_type') == 'scorm') {
            return true;
        }
    }
}

/**
 * Iterator Filter for traversing only Example units
 *
 * @package eFront
 * @version 3.5.0
 */
class EfrontExampleFilterIterator extends FilterIterator
{
    /**
     * Accepts only examplesunits
     *
     * @return boolean
     * @since 3.5.0
     * @access public
     */
    function accept() {
        if ($this -> current() -> offsetGet('ctg_type') == 'examples') {
            return true;
        }
    }
}

/**
 * Iterator Filter for traversing only Content (theory, example and SCORM) units
 *
 * @package eFront
 * @version 3.5.0
 */
class EfrontContentFilterIterator extends FilterIterator
{
    /**
     * Accepts content that is not tests
     *
     * @return boolean
     * @since 3.5.0
     * @access public
     */
    function accept() {
        if ($this -> current() -> offsetGet('ctg_type') != 'tests' && $this -> current() -> offsetGet('ctg_type') != 'scorm_test') {
            return true;
        }
    }
}

?>