<?php if (!defined('VB_ENTRY')) die('Access denied.');
/*======================================================================*\
|| #################################################################### ||
|| # vBulletin 4.0.0 Alpha 5
|| # ---------------------------------------------------------------- # ||
|| # Copyright ©2000-2009 Jelsoft Enterprises Ltd. All Rights Reserved. ||
|| # This file may not be redistributed in whole or significant part. # ||
|| # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html # ||
|| #################################################################### ||
\*======================================================================*/

/**
 * CMS Content Data Manager class.
 * Manages the CMS content nodes.
 *
 * Node: The nodes use the nested set model.  The DM is also responsible for
 * deleting and moving nodes and maintaining the integrity of the tree structure
 * as well as managing the nodes' associated information.
 *
 * @TODO: Provide move methods and support the various move types as defined by the
 * self::MOVE_ constants in update.
 *
 * @package vBulletin
 * @author vBulletin Development Team
 * @version $Revision: 28694 $
 * @since $Date: 2008-12-04 16:12:22 +0000 (Thu, 04 Dec 2008) $
 * @copyright Jelsoft Enterprises Ltd.
 */
class vBCms_DM_Node extends vBCMS_DM_Content
{
	/*Constants=====================================================================*/

	/**
	 * Move actions for orphaned nodes after a delete.
	 */
	const MOVE_PARENT = 1;
	const MOVE_ROOT = 2;
	const MOVE_REMOVE = 3;



	/*Properties====================================================================*/

	/**
	* Field definitions.
	* The field definitions are in the form:
	*	array(fieldname => array(VF_TYPE, VF_REQ, VF_METHOD, VF_VERIFY)).
	*
	* @var array string => array(int, int, mixed)
	*/
	protected $fields = array(
		'nodeid' => 			array(vB_Input::TYPE_UINT,		self::REQ_INC,	self::VM_TYPE),
		'contenttypeid' => 		array(vB_Input::TYPE_UINT,		self::REQ_YES,	self::VM_CALLBACK,	array('$this', 'validateContentTypeID')),
		'contentid' => 			array(vB_Input::TYPE_NOHTMLCOND,self::REQ_NO,	self::VM_TYPE),
		'item_id' => 			array(vB_Input::TYPE_NOHTMLCOND,self::REQ_NO,	self::VM_TYPE),
		'url' => 				array(vB_Input::TYPE_STR,		self::REQ_NO,	self::VM_CALLBACK,	array('$this', 'validateURL')),
		'nodeleft' =>			array(vB_Input::TYPE_NOCLEAN,	self::REQ_AUTO),
		'noderight' =>			array(vB_Input::TYPE_NOCLEAN,	self::REQ_AUTO),
		'parentnode' => 		array(vB_Input::TYPE_UINT,		self::REQ_NO,	self::VM_CALLBACK,	array('$this', 'validateParent')),
		'styleid' =>			array(vB_Input::TYPE_UINT,		self::REQ_NO,	self::VM_TYPE),
		'layoutid' =>			array(vB_Input::TYPE_UINT,		self::REQ_NO,	self::VM_CALLBACK,	array('$this', 'validateLayoutID')),
		'userid' =>				array(vB_Input::TYPE_UINT,		self::REQ_NO),
		'publicpreview' =>	array(vB_Input::TYPE_UINT,		self::REQ_NO),
		'comments_enabled' =>	array(vB_Input::TYPE_UINT,		self::REQ_NO),
		'publicpreview' =>	array(vB_Input::TYPE_UINT,		self::REQ_NO),
		'permissionsfrom' =>	array(vB_Input::TYPE_UINT,		self::REQ_NO),
		'auto_displayorder' =>	array(vB_Input::TYPE_UINT,		self::REQ_NO),
		'creationdate' =>		array(vB_Input::TYPE_UNIXTIME,	self::REQ_AUTO),
		'lastupdated' =>		array(vB_Input::TYPE_UNIXTIME,	self::REQ_NO),
		'publishdate' =>		array(vB_Input::TYPE_UNIXTIME,	self::REQ_NO),
		'setpublish' =>		array(vB_Input::TYPE_BOOL,		self::REQ_NO),
		'issection' =>			array(vB_Input::TYPE_BOOL,		self::REQ_NO),
		'description' =>		array(vB_Input::TYPE_NOHTMLCOND,self::REQ_NO),
		'title' => 				array(vB_Input::TYPE_NOHTMLCOND,self::REQ_NO),
		'html_title' => 		array(vB_Input::TYPE_NOHTMLCOND,self::REQ_NO),
		'viewcount' =>			array(vB_Input::TYPE_UINT,		self::REQ_AUTO),
		'workflowid' => 		array(vB_Input::TYPE_UINT,		self::REQ_AUTO),
		'workflowdate' =>		array(vB_Input::TYPE_UNIXTIME,	self::REQ_AUTO),
		'workflowstatus' => 	array(vB_Input::TYPE_STR,		self::REQ_AUTO),
		'workflowcheckedout' => array(vB_Input::TYPE_BOOL,		self::REQ_AUTO),
		'workflowpending' => 	array(vB_Input::TYPE_BOOL, 		self::REQ_AUTO),
		'workflowlevelid' => 	array(vB_Input::TYPE_UINT,		self::REQ_AUTO),
		'associatedthreadid' => 	array(vB_Input::TYPE_UINT,		self::REQ_NO),
		'keywords' => 	array(vB_Input::TYPE_STR,		self::REQ_NO),
		'config' => 			array(vB_Input::TYPE_NOCLEAN,	self::REQ_NO),
		'navigation' =>			array(vB_Input::TYPE_ARRAY_INT,	self::REQ_NO)
	);
	/**
	 * Map of table => field for fields that can automatically be updated with their
	 * set value.
	 *
	 * @var array (tablename => array(fieldnames))
	 */
	protected $table_fields = array(
		'cms_node' => 		array('nodeid', 'contenttypeid', 'contentid', 'url', 'nodeleft', 'noderight',
								  'parentnode', 'styleid', 'layoutid', 'userid', 'publishdate', 'setpublish', 'issection',
								  'lastupdated', 'publicpreview',  'comments_enabled', 'auto_displayorder', 'permissionsfrom'),
		'cms_nodeinfo' =>	array('nodeid', 'description', 'viewcount', 'creationdate',
								  'workflowid', 'workflowdate', 'workflowstatus', 'title', 'html_title',
								  'workflowcheckedout', 'workflowpending', 'workflowlevelid', 'associatedthreadid', 'keywords')
	);

	/**
	 * Table name of the primary table.
	 *
	 * @var string
	 */
	protected $primary_table = 'cms_node';

	/**
	 * A primary id for REQ_INC fields.
	 * @see vB_DM::save()
	 *
	 * @var mixed
	 */
	protected $primary_id = 'nodeid';

	/**
	 * vB_Item Class.
	 * Class of the vB_Item that this DM is responsible for updating and/or
	 * creating.  This is used to instantiate the item when lazy loading based on an
	 * item id.
	 *
	 * @var string
	 */
	protected $item_class = 'vBCms_Item_Content';

	/** This is used in child classes to save type-specific information ****/
	protected $type_table = false;

	/** This is used in child classes to save type-specific information ****/
	protected $type_fields = array();

	/** This is used in child classes to save type-specific information. These are fields
	* we can save automatically. ****/
	protected $type_table_fields = array();

	/** This is used in child classes to save type-specific information ****/
	protected $type_set_fields = array();

	/**
	 * Whether the insert id is required for further queries during an insert.
	 * This can be set manually, or left to be resolved with
	 * vB_DM::requireAutoIncrementId().
	 *
	 * @var bool
	 */
	protected $require_auto_increment_id = true;


	/**
	 * The nearest parent section node.
	 * This may not be the parentnode id given, but the nearest parent that is a
	 * section.
	 * @see vBCms_DM_Node::validateParent()
	 *
	 * @var int
	 */
	protected $section;

	//This will load and save the basic form data that's in the system-wide sections
	public function saveFromForm($nodeid)
	{
		vB::$vbulletin->input->clean_array_gpc('r', array(
			'title' => vB_Input::TYPE_STR,
			'html_title' => vB_Input::TYPE_STR,
			'url' => vB_Input::TYPE_STR,
			'description' => vB_Input::TYPE_STR,
			'layoutid' => vB_Input::TYPE_INT,
			'styleid' => vB_Input::TYPE_INT,
			'setpublish' => vB_Input::TYPE_INT,
			'publishdate' => vB_Input::TYPE_STR,
			'publicpreview' => vB_Input::TYPE_INT,
			'comments_enabled' => vB_Input::TYPE_INT,
			'keywords' => vB_Input::TYPE_STR,
			'section_menu_inherit' => TYPE_BOOL,
			'section_menu_sections' => TYPE_ARRAY_INT
		));
		$this->set('nodeid', $nodeid);
		//set the values
		
		if (vB::$vbulletin->GPC_exists['url'] and vB::$vbulletin->GPC['url'] != '')
		{
			$this->set('url', vB::$vbulletin->GPC['url']);
		}

		if (vB::$vbulletin->GPC_exists['description'])
		{
			$this->set('description', vB::$vbulletin->GPC['description']);
		}

		if (vB::$vbulletin->GPC_exists['title'])
		{
			$this->set('title', vB::$vbulletin->GPC['title']);
		}

		if (vB::$vbulletin->GPC_exists['html_title'])
		{
			$this->set('html_title', vB::$vbulletin->GPC['html_title']);
		}

		if (vB::$vbulletin->GPC_exists['setpublish'])
		{
			$this->set('setpublish', vB::$vbulletin->GPC['setpublish']);
		}

		if (vB::$vbulletin->GPC_exists['publishdate'])
		{
			$str_date = str_replace('-', '/', vB::$vbulletin->GPC['publishdate']);
			$this->set('publishdate', strtotime($str_date,0));
		}

		if (vB::$vbulletin->GPC_exists['keywords'])
		{
			$this->set('keywords', vB::$vbulletin->GPC['keywords']);
		}

		if (vB::$vbulletin->GPC_exists['styleid'])
		{
			$this->set('styleid', vB::$vbulletin->GPC['styleid']);
		}

		if (vB::$vbulletin->GPC_exists['layoutid'])
		{
			$this->set('layoutid', vB::$vbulletin->GPC['layoutid']);
		}

		if (vB::$vbulletin->GPC_exists['publicpreview'])
		{
			$this->set('publicpreview', vB::$vbulletin->GPC['publicpreview']);
		}

		if (vB::$vbulletin->GPC_exists['comments_enabled'])
		{
			$this->set('comments_enabled', vB::$vbulletin->GPC['comments_enabled']);
		}

		if (vB::$vbulletin->GPC['section_menu_inherit'])
		{
			$section_menu = array();
		}
		else
		{
			$section_menu = vB::$vbulletin->GPC['section_menu_sections'];
		}

		$this->set('navigation', $section_menu);

		// add node info
		$this->setNodeTitle($title);
		$result = $this->save();

		//That's the main data. Creating an associatedthreadid, if necessary,
		// is handled in vbcms/dm/content.php
		return $result;


	}

	/*Set===========================================================================*/

	/**
	 * Prepare fields before saving.
	 */
	protected function prepareFields()
	{
		parent::prepareFields();
		//The parent DM uses item_id, but the actual database field is $contentid. Let's make
		// sure both are set.
		if (isset($this->set_fields['contentid']) AND !isset($this->item_id))
		{
			$this->item_id = $this->set_fields['contentid'];
		}

		if ($this->isUpdating() AND count($this->set_fields))
		{
			$this->set_fields['lastupdated'] = TIMENOW;
		}

		if (!$this->isUpdating() AND ($contenttype = $this->set_fields['contenttypeid']))
		{

			if ($controller = vB_Types::instance()->getContentTypeController($contenttype));
			{
				$this->set_fields['issection'] = (bool)$controller->isSection();
			}

			if (!isset($this->set_fields['title']))
			{
				$this->set_fields['title'] = vB_Types::instance()->getUntitledContentTypeTitle($contenttype);
			}
		}
	}



	/*Set===========================================================================*/

	/**
	 * Sets a field value.
	 * Set the nearest valid section parent as the parentnode.
	 * @see vBCms_DM_Node::validateParent()
	 *
	 * @param string $fieldname					- The name of the field to set
	 * @param mixed $value						- The value to set
	 */
	protected function setField($fieldname, $value)
	{
		if ($fieldname == 'parentnode')
		{
			$this->set_fields[$fieldname] = $this->section;
		}
		else
		{
			parent::setField($fieldname, $value);
		}
	}



	/*Validate======================================================================*/

	/**
	 * Ensures that a contenttypeid is valid.
	 *
	 * @param mixed $value						- The value to validate
	 * @param mixed $error						- The var to assign an error to
	 * @return mixed | bool						- The filtered value or boolean false
	 */
	protected function validateContentTypeID($value, &$error)
	{
		if (vB_Types::instance()->contentTypeEnabled($value))
		{
			return $value;
		}

		return false;
	}


	/**
	 * Validates the URL segment
	 *
	 * @param mixed $value						- The value to validate
	 * @param mixed $error						- The var to assign an error to
	 * @return mixed | bool						- The filtered value or boolean false
	 */
	protected function validateURL($value, &$error)
	{
		if (!isset($this->set_fields['url']))
		{
			return $value;
		}

		$value = trim(preg_replace('#([^a-zA-Z0-9_-])+#', '-', $value), ' -');
		$nodeid =  $this->set_fields['nodeid'];

		if (($length = vbstrlen($value)) > 256)
		{
			// too long
			$error = new vB_Phrase('error', 'validation_toolong_x_y', $length, 256);
			return false;
		}

		//First thing- let's make sure this URL is not already in use.
		if ( $record = vB::$vbulletin->db->query_first($sql = "SELECT nodeid FROM " . TABLE_PREFIX .
			"cms_node WHERE lower(url) = '" . vB::$vbulletin->db->escape_string(strtolower($this->set_fields['url'])) .
			(isset($this->set_fields['nodeid']) ? "' AND nodeid <> $nodeid;" : "' ") ))
		{
			throw (new vB_Exception_Model($vbphrase['url_in_use'] ));
		}

		return $value;
	}


	/**
	 * Validates a parentid.
	 * Checks if the parent exists.
	 *
	 * @param mixed $value						- The value to validate
	 * @param mixed $error						- The var to assign an error to
	 * @return mixed | bool						- The filtered value or boolean false
	 */
	protected function validateParent($value, &$error)
	{
		$parent = new vBCms_Item_Content($value);

		if (!$parent->isValid())
		{
			return false;
		}

		$this->section = $parent->getSectionId();

		return $value;
	}


	/**
	 * Validates a layout id
	 *
	 * @param mixed $value						- The value to validate
	 * @param mixed $error						- The var to assign an error to
	 * @return mixed | bool						- The filtered value or boolean false
	 */
	protected function validateLayoutID($value, &$error)
	{
		$layout = new vBCms_Item_Layout($value);

		if ($layout->isValid())
		{
			return $value;
		}

		return false;
	}


	/*** This function validates type-specific field information. It's taken
	* directly from vb/dm.php, modified to use the type_ variables
	* ***/
	protected function validateTypeField($fieldname, $value, &$error)
	{
		if (!isset($this->type_fields[$fieldname]))
		{
			throw (new vB_Exception_DM('Field \'' . htmlspecialchars($fieldname) . '\' checked for validation in DM \'' . get_class($this) . '\' is undefined'));
		}

		$field = $this->type_fields[$fieldname];

		// Clean the value according to it's type
		$value = vB_Input::clean($value, $this->type_fields[$fieldname][self::VF_TYPE]);

		// If no validation method has been specified then we're done
		if (!isset($field[self::VF_METHOD]) OR (self::VM_TYPE == $field[self::VF_METHOD]))
		{
			return $value;
		}

		if (self::VM_LAMBDA == $field[self::VF_METHOD])
		{
			$lambda = create_function('$value', '&$error', $field[self::VF_VERIFY]);
			$value = $lambda($value, $error);
		}
		else if (self::VM_CALLBACK == $field[self::VF_METHOD])
		{
			// ensure a callback is specified
			if (!is_array($field[self::VF_VERIFY]) OR !sizeof($field[self::VF_VERIFY] >= 2))
			{
				throw (new vB_Exception_DM('Invalid callback function specified for field \'' . htmlspecialchars($fieldname) . '\' in DM \'' . get_class($this) . '\''));
			}

			// check for extra parameters
			if (sizeof($field[self::VF_VERIFY] > 2))
			{
				// extract callback
				$callback = array_slice($field[self::VF_VERIFY], 0, 2);

				// add value and error reference as the first paramaters
				$params = array($value);
				$params[] =& $error;

				// extract defined parameters
				$params = array_merge($params, array_slice($field[self::VF_VERIFY], 2));

				// check if callback is this
				if ($callback[0] == '$this')
				{
					$value = call_user_func_array(array($this, $callback[1]), $params);
				}
				else
				{
					// call user func
					$value = call_user_func_array($callback, $params);
				}
			}
			else
			{
				// no extra parameters in field definition
				$value = call_user_func($field[self::VF_VERIFY], $value, $error);
			}
		}
		else
		{
			throw (new vB_Exception_DM('Unknown verify method given for dm field \'' . htmlspecialchars($fieldname) . '\' in dm \'' . get_class($this) . '\''));
		}

		if (false !== $value)
		{
			return $value;
		}

		if (!$error)
		{
			$error = new vB_Phrase('error', 'invalid_dm_value_x_y', $fieldname, htmlspecialchars($value));
		}


		return $value;
	}

	/**** We need to set type-specific fields like we do the generic fields.
	* If this is a type-specific field we handle it here. If not we pass
	* to the generic DM.
	* *****/

	public function set($fieldname, $value)
	{

		if (!$this->type_table OR ! count($this->type_fields)
			or ! isset($this->type_fields[$fieldname]) )
		{
			return parent::set($fieldname, $value);
		}

		//if we got here, we're validating a type-specific field.
		$error = false;
		if (false === $value)
		{
			$value = '';
		}

		if (false === ($value = $this->validateTypeField($fieldname, $value, $error)))
		{
			if ($this->strict)
			{
				throw (new vB_Exception_DM('Value given to set DM \'' . get_class($this) . '\' field \'' . hmtlspecialchars($fieldname) . '\' is not valid: ' . $error));
			}

			$this->error($error, $fieldname);
		}

		if ($fieldname == 'contentid')
		{
			//no more checking.
			$this->type_set_fields['contentid'] = $value;
		}

		else
		{
			if (self::REQ_AUTO == $this->type_fields[$fieldname][self::VF_REQ])
			{
				throw (new vB_Exception_DM('Cannot set the value of automatic field \'' . htmlspecialchars($fieldname) . '\' in DM \'' . get_class($this) . '\''));
			}

			$this->type_set_fields[$fieldname] = $value;
		}

	}

	/*** We save the type-specific data, if there is any.
	****/
	protected function saveTypeData()
	{
		$error = '';
		$valid_values = array();
		if ($this->type_table AND count($this->type_set_fields))
		{
			foreach($this->type_set_fields as $fieldname => $value)
			{

			//the contentid field gets special handling.
				$valid_values[$fieldname] = ((vB_Input::TYPE_STR == $this->type_fields[$fieldname][self::VF_TYPE]
					or vB_Input::TYPE_NOHTMLCOND == $this->type_fields[$fieldname][self::VF_TYPE]
					or vB_Input::TYPE_NOTRIM == $this->type_fields[$fieldname][self::VF_TYPE]
					or vB_Input::TYPE_NOHTML == $this->type_fields[$fieldname][self::VF_TYPE])?
				"'" . vB::$vbulletin->db->escape_string($value) . "'"
				: $value);
			}
		}

		//validation errors raise an exception, so if we got here the fields are O.K.
		if (!count ($valid_values))
		{
			return true;
		}


		//We have data to save. If we have a contentid we do an update. Otherwise we
		// do an insert.
		if (isset($this->set_fields['contentid']) AND intval($this->set_fields['contentid']))
		{
			//this is an update
			$sql = "UPDATE " . TABLE_PREFIX . $this->type_table . " set ";
			$updates = array();

			foreach ($valid_values as $field => $value )
			{
				$updates[] = "$field = $value";
			}

			$sql .= implode(', ', $updates);
			//get the "where clause
			if (method_exists($this, 'getTypeConditionSQL'))
			{
				$sql .= " WHERE " .$this->getTypeConditionSQL($type_table);
			}
			else
			{
				$where = " WHERE contentid = " . $this->set_fields['contentid'];
			}
			vB::$vbulletin->db->query_write($sql);
			return $this->set_fields['contentid'];
		}
		else
		{
			$sql = "INSERT INTO ". TABLE_PREFIX . $this->type_table . " ("
				. implode(', ', array_keys($valid_values)) . ") values(" .
				implode(', ', $valid_values) . ") ";
			//we are doing an insert
			vB::$vbulletin->db->query_write($sql);
			$this->set('contentid', vB::$vbulletin->db->insert_id());
			return vB::$vbulletin->db->insert_id();
		}

	}

	protected function preSave()
	{
		//now, if we have type-specific data we need to save it.
		$item_id = $this->saveTypeData();

		parent::preSave();
		return $item_id;
	}
	/*Save==========================================================================*/

	/**
	* Resolves the condition SQL to be used in update queries.
	* This method is abstract and must be defined as there should always be a
	* condition for an existing item.
	*
	* @param string $table						- The table to get the condition for
	* @return string							- The resolved sql
	*/
	protected function getConditionSQL($table)
	{
		$this->assertItem();

		return 'nodeid = ' . intval($this->item->getNodeId());
	}


	/**
	* Performs additional queries or tasks after saving.
	*
	* @param mixed								- The save result
	* @param bool $deferred						- Save was deferred
	* @param bool $replace						- Save used REPLACE
	* @param bool $ignore						- Save used IGNORE if inserting
	* @return bool								- Whether the save can be considered a success
	*/
	protected function postSave($result, $deferred, $replace, $ignore)
	{
		//First let's handle the configuration.
		if (isset($this->set_fields['config']))
		{
			if ($this->isUpdating())
			{
				$this->assertItem();
				$id = $this->item->getNodeId();
			}
			else
			{
				if (!$this->primary_id)
				{
					throw (new vB_Exception_DM('No primary id available for setting the node config in DM \'' . get_class($this) . '\''));
				}

				$id = $this->primary_id;
			}

			// delete the old config
			vB::$db->query_write(
				'DELETE FROM ' . TABLE_PREFIX . 'cms_nodeconfig
				 WHERE nodeid = ' . $id);

			// build the sql
			$sql = 'INSERT INTO ' . TABLE_PREFIX . 'cms_nodeconfig (nodeid, name, value, serialized) VALUES ';
			$values = array();

			// write the new config
			foreach ($this->set_fields['config'] AS $cvar => $value)
			{
				if (is_resource($value))
				{
					throw (new vB_Exception_DM('Trying to set a resource as a node config value'));
				}

				if (is_object($value) OR is_array($value))
				{
					$serialized = true;
					$value = serialize($value);
				}
				else
				{
					$serialized = false;
				}

				$values[] = '(' . $id . ', \'' . vB::$db->escape_string($cvar) . '\',\'' . vB::$db->escape_string($value) . '\',\'' . intval($serialized) . '\')';
			}
			// insert config
			vB::$db->insert_multiple($sql, $values, true);

		}

		//and set permissionsfrom the parent. Let's do this so we fix any close records.
		$nodeid = (isset($this->set_fields['nodeid']) ? $this->set_fields['nodeid'] : $this->item->getNodeId());
		$parents = array();
		if (intval($this->set_fields['permissionsfrom']))
		{
			$permissionsfrom = $this->set_fields['permissionsfrom'];
		}
		else
		{
			//we'll pull from our parent.
			$rst = vB::$vbulletin->db->query_read("SELECT parent.nodeid, parent.parentnode,
				parent.permissionsfrom, parent.nodeleft, parent.noderight
				FROM " . TABLE_PREFIX . "cms_node AS node INNER JOIN " . TABLE_PREFIX .
				"cms_node AS parent ON node.nodeleft BETWEEN parent.nodeleft AND parent.noderight
				WHERE node.nodeid = $nodeid
				ORDER BY parent.nodeleft DESC");
			while($record = vB::$vbulletin->db->fetch_array($rst))
			{
				$parents[] = $record;
				if (intval($record['permissionsfrom']))
				{
					$permissionsfrom = $record['permissionsfrom'];
					if (intval($record['permissionsfrom']) != intval($nodeid))
					{
						break;
					}
				}
			}
			//either we found a parent with a permissionsfrom, or we hit the top- which is
			// just as good.
			vB::$vbulletin->db->query_write("UPDATE " . TABLE_PREFIX . "cms_node SET permissionsfrom = " .
				$permissionsfrom . " WHERE nodeid = $nodeid" ) ;

		}
		foreach ($parents as $parent)
		{
			vB::$vbulletin->db->query_write("UPDATE " . TABLE_PREFIX . "cms_node SET permissionsfrom = " .
				$permissionsfrom . " WHERE parentnode = " . $parent['nodeid'] .
				" AND IFNULL(permissionsfrom, 0) = 0") ;
		}

		if (isset($this->set_fields['navigation']))
		{
			$nodeid = intval((isset($this->set_fields['nodeid']) ? $this->set_fields['nodeid'] : $this->item->getNodeId()));

			if (!sizeof($this->set_fields['navigation']))
			{
				vB::$vbulletin->db->query_write("
					DELETE FROM " . TABLE_PREFIX . "cms_navigation
					WHERE nodeid = $nodeid
				");
			}
			else
			{
				vB::$vbulletin->db->query_write("
					REPLACE INTO " . TABLE_PREFIX . "cms_navigation
					SET nodeid = $nodeid,
						nodelist = '" . implode(',', $this->set_fields['navigation']) . "'
				");
			}
			
			// clear the navbar cache
			vB_Cache::instance()->event(vBCms_NavBar::getCacheEventId($nodeid));
			
			// reload the navbar for the page
			vBCms_NavBar::prepareNavBar($this->item, true);
		}

		parent::postSave($result, $deferred, $replace, $ignore);
		//we should never return false if we got here.
		$result = (intval($result) ? $result : true);

		return $result;
	}



	/*Insert========================================================================*/

	/**
	 * Performs an INSERT with the set fields.
	 *
	 * @param string $table						- The table to insert into
	 * @param bool $replace						- Whether to REPLACE instead of INSERT
	 * @param bool $ignore						- Whether to IGNORE
	 * @return int								- The insert id, or affected rows if using IGNORE
	 */
	protected function execInsert($table, $replace = false, $ignore = false)
	{
		if ($table == $this->primary_table)
		{
			return $this->insertNode();
		}

		parent::execInsert($table, $replace, $ignore);
	}


	/**
	 * Inserts a new node.
	 */
	protected function insertNode()
	{
		// check we don't already have errors
		if ($this->hasErrors())
		{
			return false;
		}

		$parent = $this->set_fields['parentnode'];

		if (! $this->getSet('userid'))
		{
			$this->set_fields['userid'] = vB::$vbulletin->userinfo['userid'];
		}

		// Lock the node table :(
		vB::$db->lock_tables(array('cms_node' => 'WRITE', 'language' =>read));

		// Get the new leftnode position
		$left = vB::$db->query_first("
				SELECT noderight
				FROM " . TABLE_PREFIX . "cms_node
				WHERE nodeid = " . intval($this->set_fields['parentnode']));

		// Shouldn't happen as we already validated the parentnode with set
		if (!$left)
		{
			throw (new vB_Exception_DM('No valid parent node found for inserting a new node'));
		}

		$left = $left['noderight'] - 1;

		// Make a space for the new node
		vB::$db->query_write("
			UPDATE " . TABLE_PREFIX . "cms_node
			SET noderight = noderight + 2
			WHERE noderight > " . intval($left)
		);

		vB::$db->query_write("
			UPDATE " . TABLE_PREFIX . "cms_node
			SET nodeleft = nodeleft + 2
			WHERE nodeleft > " . intval($left)
		);

		// Fill the gap with our new leaf node
		$this->set_fields['nodeleft'] = $left + 1;
		$this->set_fields['noderight'] = $left + 2;

		// Insert the new node
		vB::$db->query_write($this->getInsertSQL($this->primary_table));

		// Get the new nodeid
		$insertid = vB::$db->insert_id();

		// Set the primary key based on the insert id
		$this->setAutoIncrementId(vB::$db->insert_id());

		// Unlock the node table :)
		vB::$db->unlock_tables();

		// Mark the tree as updated
		$this->treeUpdated();

		return $insertid;
	}



	/*Update========================================================================*/

	/**
	* Performs an UPDATE on the current item.
	*
	* @param string	$table						- The table to update
	* @param bool $deferred						- Whether to defer the update until shutdown
	* @param bool $get_affected_rows			- Whether to return the number of affected rows
	*
	* @return bool | int						- Success or affected rows
	*/
	protected function execUpdate($table, $deferred = false, $get_affected_rows = false)
	{
		// Check if the node was moved
		if (($table == $this->primary_table) AND isset($this->set_fields['parentid']))
		{
			$this->assertItem();
			$this->moveNode($this->item->getId(), $this->set_fields['parentid']);
		}

		return parent::execUpdate($table, $deferred, $get_affected_rows);
	}


	/**
	 * Moves a node to be the child of another node.
	 * @TODO: Ordering
	 *
	 * @param int $nodeid						- Id of the node to move
	 * @param int $parentid						- The parent / set root to move to
	 * @param int $order						- The order to place the node (0 first)
	 */
	public function moveNode($nodeid, $parentid, $order = false)
	{
		// Get the tree info for the src and new parent nodes
		$result = vB::$db->query("
			SELECT (nodeid = " . intval($parentid) . ") AS isparent, nodeleft, noderight, parentnode
			FROM " . TABLE_PREFIX . "cms_node
			WHERE nodeid IN (" . intval($parentid) . ", " . intval($nodeid) . ")"
		);

		$parent = $source = false;
		while ($node = vB::$db->fetch_array($result))
		{
			if ($node['isparent'])
			{
				$parent = $node;
			}
			else
			{
				$source = $node;
			}
		}

		if (!$parent OR !$source)
		{
			throw (new vB_Exception_DM('Source node \'' . htmlspecialchars($nodeid) . '\' or parent node \'' . htmlspecialchars($parentid) . '\' not valid for moving'));
		}

		if (($parent['nodeleft'] >= $node['nodeleft']) AND ($parent['nodeleft'] <= $node['nodeleft']))
		{
			throw (new vB_Exception_DM('Cannot move node \'' . $node['nodeid'] . '\': destination \'' . $parent['nodeid'] . '\' is a descendant of the source (' . intval($node['nodeid']) . ')'));
		}

		// Get the width of the subtree we're moving
		 $src_width = ($source['noderight'] - $source['nodeleft']) + 1;

		 // Lock the node tree
		 vB::$db->lock_tables(array('cms_node' => 'WRITE'));

		 // Create space for the moving node to the right of the new parent's tree
		 vB::$db->query_write("
		 	UPDATE " . TABLE_PREFIX . "cms_node
		 	SET nodeleft = IF (nodeid != " . intval($parent['nodeid']) . ", nodeleft + $src_width, nodeleft),
		 		noderight = noderight + $src_width
		 	WHERE noderight >= " . intval($parent['noderight'])
		 );

		// If the source was to the right of the new parent then it was shifted to make the gap
		if ($node['nodeleft'] > $parent['noderight'])
		{
			$node['nodeleft'] += $src_width;
			$node['noderight'] += $src_width;
		}

		 // Check the distance that the node will move.  This works in both directions.
		 $distance = ($parent['noderight'] - $node['nodeleft']);

		 // Update the moved sub tree with it's new left and right values
		 vB::$db->query_write("
		 	UPDATE " . TABLE_PREFIX . "cms_node
		 	SET nodeleft = nodeleft + " . intval($distance) . ",
		 		noderight + noderight + " . intval($distance) . "
		 		parentnode = IF(nodeid = " . intval($node['nodeid']) . ", " . intval($parent['nodeid']) . ", parentnode)
		 	WHERE nodeleft BETWEEN " . intval($node['nodeleft']) . " AND " . intval($node['noderight'])
		);

		// Close the gap where the sub tree was moved from
		vB::$db->query_write("
			UPDATE " . TABLE_PREFIX . "cms_node
			SET nodeleft = IF(nodeleft >= " . intval($node['nodeleft']) . ", nodeleft - " . intval($src_width) . ", nodeleft),
				noderight = noderight - " . intval($src_width) . "
			WHERE noderight > " . $node['noderight']
		);

		// Unlock the node tree
		vB::$db->unlock_tables();
	}


	/**
	 * Saves a timestamp of the last node tree update time.
	 * This only needs to be done when the actual structure of the tree has been
	 * modified, not it's contents.  This allows us to notify users who are viewing
	 * the nodes if any changes occured before they attempted a change of their own.
	 */
	protected function treeUpdated()
	{
		// TODO:
	}



	/*Delete========================================================================*/

	/**
	* Deletes the existing item.
	*
	* @return int								- The number of affected rows.
	*/
	public function delete($move_children = self::MOVE_PARENT)
	{
		if ($this->hasErrors())
		{
			return false;
		}

		// Get the condition
		$condition = $this->getConditionSQL($this->primary_table);

		// Check condition
		if (!$condition)
		{
			throw (new vB_Exception_DM('execDelete() was called in DM \'' . get_class($this) . '\' with no condition'));
		}

		// Lock the node table :(
		vB::$db->lock_tables(array('cms_node' => 'WRITE'));

		// Get the node info of the nodes we are deleting
		$result = vB::$db->query("
			SELECT nodeid, nodeleft, noderight, parentnode
		 	FROM " . TABLE_PREFIX . "cms_node
		 	WHERE $condition
		 ");

		$nodes = array();
		while($node = vB::$db->fetch_array($result))
		{
			$nodes[$node['nodeid']] = $node;
		}

		// Set result for affected rows based only on number of nodes being deleted
		$affected_nodes = sizeof($nodes);

		// Find any children that will be inherently removed
		if (self::MOVE_REMOVE == $move_children)
		{
			foreach ($nodes AS $nodeid => $node)
			{
				$left = $node['nodeleft'];
				$right = $node['noderight'];

				foreach ($nodes AS $childid => $child)
				{
					if (($child['nodeleft'] > $left) AND $child['noderight'] < $right)
					{
						// this node will be removed anyway
						unset($nodes[$child['nodeid']]);
					}
				}
			}
		}

		// Remove nodes
		foreach ($nodes AS $node)
		{
			$this->deleteNode($node, $move_children);
		}

		// Unlock the node table :)
		vB::$db->unlock_tables();

		return $affected_nodes;
	}


	/**
	 * Deletes a single specified node and all of it's children.
	 *
	 * @param array $node
	 */
	protected function deleteNode($node, $move_children)
	{
		if (1 == $node['nodeleft'])
		{
			throw (new vB_Exception_DM('Cannot delete the root cms node'));
		}

		// Get the size of the set being deleted
		$size = ($node['noderight'] - $node['nodeleft']) + 1;

		// Handle children
		if ($size > 2)
		{
			if (self::MOVE_REMOVE == $move_children)
			{
				// delete the set
				vB::$db->query_write("
					DELETE FROM " . TABLE_PREFIX . "cms_node
					WHERE nodeleft BETWEEN " . intval($node['nodeleft']) . " AND " . intval($node['noderight'])
				);
			}
			else if (self::MOVE_PARENT == $move_children)
			{
				// delete the node
				vB::$db->query_write("
					DELETE FROM " . TABLE_PREFIX . "cms_node
					WHERE nodeid = " . intval($node['nodeid'])
				);

				// move children up
				vB::$db->query_write("
					UPDATE " . TABLE_PREFIX . "cms_node
					SET nodeleft = nodeleft - 1,
						noderight = noderight -1
					WHERE nodeleft BETWEEN " . intval($node['nodeleft']) . " AND " . intval($node['noderight'])
				);

				// update parentnode
				vB::$db->query_write("
					UPDATE " . TABLE_PREFIX . "cms_node
					SET parentnode = " . intval($node['parentnode']) . "
					WHERE parentnode = " . intval($node['nodeid'])
				);

				// we only removed a single node
				$size = 2;
			}
			else if (self::MOVE_ROOT == $move_children)
			{
				// delete the node
				vB::$db->query_write("
					DELETE FROM " . TABLE_PREFIX . "cms_node
					WHERE nodeid = " . intval($node['nodeid'])
				);

				// get entire tree width
				$root = vB::$db->query_first("
					SELECT noderight
					FROM " . TABLE_PREFIX . "cms_node
					WHERE nodeleft = 1"
				);

				// get the distance of the required move
				$distance = $root['noderight'] - $node['nodeleft'] - 1;

				// move children to the end of the root
				vB::$db->query_write("
					UPDATE " . TABLE_PREFIX . "cms_node
					SET nodeleft = nodeleft + " . intval($distance) . ",
						noderight = noderight + " . intval($distance) . "
					WHERE nodeleft BETWEEN " . intval($node['nodeleft']) . " AND " . intval($node['noderight'])
				);

				// update the root node width
				vB::$db->query_write("
					UPDATE " . TABLE_PREFIX . "cms_node
					SET noderight = noderight + " . (intval($size) - 2) . "
					WHERE nodeleft = 1
				");

				// update parentnode to root
				vB::$db->query_write("
					UPDATE " . TABLE_PREFIX . "cms_node
					SET parentnode = 1
					WHERE parentnode = " . intval($node['nodeid'])
				);
			}
			else
			{
				throw (new vB_Exception_DM('No valid move type specified for moving orphans after deleting a node'));
			}
		}
		else
		{
			// node is a leaf
			vB::$db->query_write("
				DELETE FROM " . TABLE_PREFIX . "cms_node
				WHERE nodeid = " . intval($node['nodeid'])
			);
		}

		// Update right node left values
		vB::$db->query_write("
			UPDATE " . TABLE_PREFIX . "cms_node
			SET nodeleft = nodeleft - " . intval($size) . "
			WHERE nodeleft > " . intval($node['noderight'])
		);

		// Update right node right values
		vB::$db->query_write("
			UPDATE " . TABLE_PREFIX . "cms_node
			SET noderight = noderight - " . intval($size) . "
			WHERE noderight > " . intval($node['noderight'])
		);
	}


	/**
	* Additional tasks to perform after a delete.
	*
	* Return false to indicate that the entire delete process was not a success.
	*
	* @param mixed								- The result of execDelete()
	*/
	protected function postDelete($result)
	{
		$this->treeUpdated();

		$this->assertItem();

		vB::$db->query_write("
			DELETE FROM " . TABLE_PREFIX . "cms_nodeconfig AS config
			WHERE config.nodeid = " . intval($this->item->getId())
		);

		vB::$db->query_write("
			DELETE FROM " . TABLE_PREFIX . "cms_nodeinfo AS nodeinfo
			WHERE nodeinfo.nodeid = " . intval($this->item->getId())
		);

		vB::$db->query_write("
			DELETE FROM " . TABLE_PREFIX . "cms_navigation AS navigation
			WHERE navigation.nodeid = " . intval($this->item->getId())
		);
		
		vB_Cache::instance()->event(vBCms_NavBar::getCacheEventId($this->item->getId()));

		return $result;
	}
}

/*======================================================================*\
|| ####################################################################

|| # CVS: $RCSfile$ - $Revision: 28749 $
|| ####################################################################
\*======================================================================*/