<?php
/** @package    verysimple::Phreeze */

/** import supporting libraries */
require_once("verysimple/HTTP/RequestUtil.php");
require_once("verysimple/HTTP/Context.php");
require_once("Phreezer.php");
require_once("Criteria.php");
require_once("IRouter.php");
require_once("GenericRouter.php");
require_once("verysimple/Authentication/IAuthenticatable.php");

/**
 * Controller is a base controller object used for an MVC pattern
 * This controller uses Phreeze ORM and RenderEngine Template Engine
 * This controller could be extended to use a differente ORM and
 * Rendering engine as long as they implement compatible functions.
 *
 * @package    verysimple::Phreeze
 * @author     VerySimple Inc.
 * @copyright  1997-2011 VerySimple, Inc.
 * @license    http://www.gnu.org/licenses/lgpl.html  LGPL
 * @version    3.1
 */
abstract class Controller
{
	protected $Phreezer;
	protected $RenderEngine;

	/**
	 * @var string ModelName is used by the base Controller class for certain functions in which
	 * require knowledge of what Model is being used.  For example, when validating user input.
	 * This may be defined in Init() if any of thes base Controller features will be used.
	 */
	protected $ModelName;

	protected $Context;

	/** @deprecated use RenderEngine */
	protected $Smarty;

	private $_router;
	private $_cu;
	public $GUID;
	public $DebugOutput = "";
	public $UnitTestMode = false;
	public $CaptureOutputMode = false;
	
	private $_terminate = false;

	/** Prefix this to the name of the view templates */
	static $SmartyViewPrefix = "View";

	/** the default mode used when calling 'Redirect' */
	static $DefaultRedirectMode = "client";

	/**
	 * Constructor initializes the controller.  This method cannot be overriden.  If you need
	 * to do something during construction, add it to Init
	 *
	 * @param Phreezer $phreezer Object persistance engine
	 * @param IRenderEngine $renderEngine rendering engine
	 * @param Context (optional) a context object for persisting the state of the current page
	 * @param Router (optional) a custom writer for URL formatting
	 */
	final function __construct(Phreezer &$phreezer, &$renderEngine, &$context = null, IRouter &$router = null)
	{
		$this->Phreezer =& $phreezer;
		$this->RenderEngine =& $renderEngine;

		// for backwards compatibility
		$this->Smarty =& $renderEngine;

		$ra = RequestUtil::GetRemoteHost();
		$this->GUID = $this->Phreezer->DataAdapter->GetDBName() . "_" . str_replace(".","_", $ra);

		$this->_router = $router ? $router : new GenericRouter();

		if ($context)
		{
			$this->Context =& $context;
		}
		else
		{
			$this->Context = new Context();
			$this->Context->GUID = "CTX_" . $this->GUID;
		}

		if ($this->RenderEngine)
		{
			// assign some variables globally for the views
			$this->Assign("CURRENT_USER",$this->GetCurrentUser());
			$this->Assign("URL",$this->GetRouter());
			$this->Assign("BROWSER_DEVICE",$this->GetDevice());
	
			// if feedback was persisted, set it
			$this->Assign("feedback",$this->Context->Get("feedback"));
			$this->Context->Set("feedback",null);
		}

		$this->Init();
	}
	
	/**
	 * Calling Terminate in Init will tell the dispatcher to halt execution
	 * without calling the requested method/route
	 */
	protected function Terminate()
	{
		$this->_terminate = true;
	}
	
	/**
	 * Returns true if Terminate() has been fired
	 * @return bool
	 */
	public function IsTerminated()
	{
		return $this->_terminate;
	}

	/**
	 * Returns the router object used to convert url/uri to controller method
	 * @return IRouter
	 */
	protected function GetRouter()
	{
		return $this->_router;
	}

	/**
	 * Init is called by the base constructor immediately after construction.
	 * This method must be implemented and provided an oportunity to
	 * set any class-wide variables such as ModelName, implement
	 * authentication for this Controller or any other class-wide initialization
	 */
	abstract protected function Init();

	/**
	 * Requires 401 Authentication.  If authentication fails, this function
	 * terminates with a 401 header.  If success, sets CurrentUser and returns null.
	 * @param IAuthenticatable any IAuthenticatable object
	 * @param string http realm (basically the login message shown to the user)
	 * @param string username querystring field (optional) if provided, the username can be passed via querystring instead of through the auth headers
	 * @param string password querystring field (optional) if provided, the password can be passed via querystring instead of through the auth headers
	 */
	protected function Require401Authentication(IAuthenticatable $authenticatable, $realm = "Login Required", $qs_username_field = "", $qs_password_field = "")
	{
		require_once("verysimple/Authentication/Auth401.php");
		
		$user = $this->Get401Authentication($authenticatable,$qs_username_field, $qs_password_field);

		// we only want to output 401 headers if the user is not already authenticated
		if (!$user)
		{
			if( $this->Get401AuthUsername($qs_username_field) )
			{
				// a username was provided, which means login failed
				Auth401::OutputHeaders("Invalid Login");
			}
			else
			{
				// no username provided, which means prompt for username
				Auth401::OutputHeaders($realm);
			}
		}
	}
	
	/**
	 * Start observing messages from Phreeze.  If no observer is provided, then
	 * an ObserveToBrowser will be used and debug messages will be output to the browser
	 * @param IObserver $observer
	 * @param bool $with_styles if true then basic styles will be output to the browser
	 */
	protected function StartObserving($observer = null, $with_styles = true)
	{
		if ($observer == null)
		{
			require_once "ObserveToBrowser.php";
			$observer = new ObserveToBrowser();
		}
		
		if ($with_styles)
		{
			$this->PrintOut("<style>.debug, .query, .info {font-family: courier new; border-bottom: solid 1px #999;} .debug {color: blue;} .query {color: green;}</style>");
		}
		
		$this->Phreezer->AttachObserver($observer);
	}

	/**
	 * accept username passed in either headers or querystring.  if a querystring parameter name is
	 * provided, that will be checked first before the 401 auth headers
	 * @param string $qs_username_field the querystring parameter to check for username (optional)
	 */
	protected function Get401AuthUsername($qs_username_field = "")
	{
		$qsv = $qs_username_field ? RequestUtil::Get($qs_username_field) : '';
		return $qsv ? $qsv : Auth401::GetUsername();
	}

	/**
	* accept password passed in either headers or querystring.  if a querystring parameter name is
	* provided, that will be checked first before the 401 auth headers
	* @param string $qs_password_field the querystring parameter to check for password (optional)
	*/
	protected function Get401AuthPassword($qs_password_field = "")
	{
		$qsv = $qs_password_field ? RequestUtil::Get($qs_password_field) : '';
		return $qsv ? $qsv : Auth401::GetPassword();
	}

	/**
	* Gets the user from 401 auth headers (or optionally querystring).  There are three scenarios
	*   - The user is already logged authenticated = IAuthenticatable is returned
	*   - The user was not logged in and valid login credentials were provided = SetCurrentUser is called and IAuthenticatable is returned
	*   - The user was not logged in and invalid (or no) credentials were provided = NULL is returned
	* @param IAuthenticatable any IAuthenticatable object
	* @param string username querystring field (optional) if provided, the username can be passed via querystring instead of through the auth headers
	* @param string password querystring field (optional) if provided, the password can be passed via querystring instead of through the auth headers
	* @return IAuthenticatable or NULL
	*/
	protected function Get401Authentication(IAuthenticatable $authenticatable, $qs_username_field = "", $qs_password_field = "")
	{
		$user = null;
		$username = $this->Get401AuthUsername($qs_username_field);

		if( $username )
		{
			// username was provided so let's attempt a login
			$password = $this->Get401AuthPassword($qs_password_field);

			if ( $authenticatable->Login($username,$password) )
			{
				$user = $authenticatable;
				$this->SetCurrentUser($authenticatable);
			}
		}
		else
		{
			// no login info was provided so return whatever is in the session
			// (which will be null if the user is not authenticated)
			$user = $this->GetCurrentUser();
		}

		return $user;

	}

	/**
	 * LoadFromForm should load the object specified by primary key = $pk, or
	 * create a new instance of the object.  Then should overwrite any applicable
	 * properties with user input.
	 *
	 * This method is used by ValidateInput for automation AJAX server-side validation.
	 *
	 * @param variant $pk the primary key (optional)
	 * @return Phreezable a phreezable object
	 */
	protected function LoadFromForm($pk = null)
	{
		return null;
	}

	/**
	 * Use as an alterative to print in order to capture debug output
	 * @param string text to print
	 * @param mime content type (example text/plain)
	 */
	protected function PrintOut($text,$contentType = null)
	{
		if ($this->CaptureOutputMode)
		{
			$this->DebugOutput .= $text;
		}
		else
		{
			if ($contentType) header("Content-type: " . $contentType);
			print $text;
		}
	}

	/**
	 * Returns a BrowserDevice object with information about the browser
	 * that is being used to view/execute this code.
	 * @return BrowserDevice
	 */
	public function GetDevice()
	{
		require_once("verysimple/HTTP/BrowserDevice.php");
		return BrowserDevice::GetInstance();
	}

	/**
	 * Displays the ListAll view for the primary model object.  Because the
	 * datagrid is populated via ajax, no model data is populated here
	 */
	public function ListAll()
	{
		if (!$this->ModelName)
		{
			throw new Exception("ModelName must be defined in " . get_class($this) . "::ListAll");
		}

		// capture output instead of rendering if specified
		if ($this->CaptureOutputMode)
		{
			$this->DebugOutput = $this->RenderEngine->fetch("View" . $this->ModelName .  "ListAll.tpl");
		}
		else
		{
			$this->RenderEngine->display("View" . $this->ModelName .  "ListAll.tpl");
		}
		//$this->_ListAll(null, Request::Get("page",1), Request::Get("limit",20));
	}

	/**
	 * Displays the ListAll view for the primary model object in the event that
	 * ajax will not be used.  The model data is populated
	 *
	 * @param Criteria $criteria
	 * @param int $current_page number of the current page (for pagination)
	 * @param int $limit size of the page (for pagination)
	 */
	protected function _ListAll(Criteria $criteria, $current_page, $limit)
	{
		if (!$this->ModelName)
		{
			throw new Exception("ModelName must be defined in " . get_class($this) . "::_ListAll.");
		}

		$page = $this->Phreezer->Query($this->ModelName,$criteria)->GetDataPage($current_page,$limit);
		$this->RenderEngine->assign($this->ModelName . "DataPage", $page);
		$this->RenderEngine->display("View" . $this->ModelName .  "ListAll.tpl");
	}

	/**
	 * Renders a datapage as XML for use with a datagrid.  The optional $additionalProps allows
	 * retrieval of properties from foreign relationships
	 *
	 * @param DataPage $page
	 * @param Array $additionalProps (In the format Array("GetObjName1"=>"PropName","GetObjName2"=>"PropName1,PropName2")
	 * @param Array $supressProps (In the format Array("PropName1","PropName2")
	 * @param bool noMap set to true to render this DataPage regardless of whether there is a FieldMap
	 */
	protected function RenderXML($page,$additionalProps = null, $supressProps = null, $noMap = false)
	{
		require_once("verysimple/String/VerySimpleStringUtil.php");
		
		if (!is_array($supressProps)) $supressProps = array();

		// never include these props
		$suppressProps[] = "NoCache";
		$suppressProps[] = "CacheLevel";
		$suppressProps[] = "IsLoaded";
		$suppressProps[] = "IsPartiallyLoaded";


		$xml = "";
		$xml .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n";

		$xml .= "<DataPage>\r\n";
		$xml .= "<ObjectName>".htmlspecialchars($page->ObjectName)."</ObjectName>\r\n";
		$xml .= "<ObjectKey>".htmlspecialchars($page->ObjectKey)."</ObjectKey>\r\n";
		$xml .= "<TotalRecords>".htmlspecialchars($page->TotalResults)."</TotalRecords>\r\n";
		$xml .= "<TotalPages>".htmlspecialchars($page->TotalPages)."</TotalPages>\r\n";
		$xml .= "<CurrentPage>".htmlspecialchars($page->CurrentPage)."</CurrentPage>\r\n";
		$xml .= "<PageSize>".htmlspecialchars($page->PageSize)."</PageSize>\r\n";

		$xml .= "<Records>\r\n";

		// get the fieldmap for this object type unless not specified
		if ($noMap)
		{
			$fms = array();
		}
		else
		{
			try
			{
				$fms = $this->Phreezer->GetFieldMaps($page->ObjectName);
			}
			catch (exception $ex)
			{
				throw new Exception("The objects contained in this DataPage do not have a FieldMap.  Set noMap argument to true to supress this error: " . $ex->getMessage());
			}
		}

		foreach ($page->Rows as $obj)
		{
			$xml .= "<" . htmlspecialchars($page->ObjectName) . ">\r\n";
			foreach (get_object_vars($obj) as $var => $val)
			{
				if (!in_array($var,$supressProps))
				{
					// depending on what type of field this is, do some special formatting
					$fm = isset($fms[$var]) ? $fms[$var]->FieldType : FM_TYPE_UNKNOWN;

					if ($fm == FM_TYPE_DATETIME)
					{
						$val = strtotime($val) ? date("m/d/Y h:i A",strtotime($val)) : $val;
					}
					elseif ($fm == FM_TYPE_DATE)
					{
						$val = strtotime($val) ? date("m/d/Y",strtotime($val)) : $val;
					}

					// if the developer has added a property that is not a simple type
					// we need to serialize it
					if (is_array($val) || is_object($val))
					{
						$val = serialize($val);
					}

					$val = VerySimpleStringUtil::EncodeSpecialCharacters($val, true, true);

					$xml .= "<" . htmlspecialchars($var) . ">" . $val . "</" . htmlspecialchars($var) . ">\r\n";
				}
			}


			// Add any properties that we want from child objects
			if ($additionalProps)
			{
				foreach ($additionalProps as $meth => $propPair)
				{
					$props = explode(",",$propPair);
					foreach ($props as $prop)
					{
						$xml .= "<" . htmlspecialchars($meth . $prop) . ">" . htmlspecialchars($obj->$meth()->$prop) . "</" . htmlspecialchars($meth . $prop) . ">\r\n";
					}
				}
		}

			$xml .= "</" . htmlspecialchars($page->ObjectName) . ">\r\n";
		}
		$xml .= "</Records>\r\n";

		$xml .= "</DataPage>\r\n";

		// capture output instead of rendering if specified
		if ($this->CaptureOutputMode)
		{
			$this->DebugOutput = $xml;
		}
		else
		{
			header('Content-type: text/xml');
			print $xml;
		}

	}

	/**
	 * Render an array of IRSSFeedItem objects as an RSS feed
	 * @param array $feedItems array of IRSSFeedItem objects
	 * @param string $feedTitle
	 * @param string $feedDescription
	 */
	protected function RenderRSS(Array $feedItems, $feedTitle = "RSS Feed", $feedDescription = "RSS Feed")
	{
		require_once('verysimple/RSS/Writer.php');
		require_once('verysimple/RSS/IRSSFeedItem.php');

		$baseUrl = RequestUtil::GetBaseURL();
		$rssWriter = new RSS_Writer($feedTitle,$baseUrl,$feedDescription);
		$rssWriter->setLanguage('us-en');
		$rssWriter->addCategory("Items");

		if (count($feedItems))
		{
			$count = 0;
			foreach ($feedItems as $item)
			{
				$count++;

				if ($item instanceof IRSSFeedItem)
				{
					$rssWriter->addItem(
						$item->GetRSSTitle(), 						// title
						$item->GetRSSLink($baseUrl), 				// link
						$item->GetRSSDescription(), 				// description
						$item->GetRSSAuthor(), 						// author
						date(DATE_RSS, $item->GetRSSPublishDate()), // date
						null, 										// source
						$item->GetRSSGUID() 						// guid
					);
				}
				else
				{
					$rssWriter->addItem("Item $count doesn't implment IRSSFeedItem","about:blank",'','Error',date(DATE_RSS) );
				}
			}
		}
		else
		{
			$rssWriter->addItem("No Items","about:blank",'','No Author',date(DATE_RSS) );
		}

		$rssWriter->writeOut();
	}

	/**
	 * @deprecated use Controller->Context->Set instead
	 */
	protected function Set($var,$val)
	{
		return $this->Context->Set($var,$val);
	}

	/**
	 * @deprecated use Controller->Context->Get instead
	 */
	protected function Get($var,$default=null)
	{
		return $this->Context->Get($var,$default);
	}

	/**
	 * This method calls LoadFromForm to retrieve a model object populated with user
	 * input.  The input is validated and a ValidationResponse is rendered in JSON format
	 *
	 * if Request::Get("SaveInline") is set then validate will call Save instead of
	 * rendering JSON.  In which case, your Save method should render the ValidationResponse
	 */
	function ValidateInput()
	{
		require_once("ValidationResponse.php");
		$vr = new ValidationResponse();

		$save = RequestUtil::Get("SaveInline");

		$obj = $this->LoadFromForm();

		if (!is_object($obj))
		{
			$vr->Success = false;
			$vr->Errors = array("Unknown"=>"LoadFromForm does not appear to be implemented.  Unable to validate");
			$vr->Message = "LoadFromForm does not appear to be implemented.  Unable to validate";
		}
		elseif ($obj->Validate())
		{
			$vr->Success = true;
		}
		else
		{
			$vr->Success = false;
			$vr->Errors = $obj->GetValidationErrors();
			$vr->Message = "Validation Errors Occured";
		}

		// if the user requested to save inline, their Save method will take over from here
		if ($vr->Success && $save)
		{
			$this->Save();
		}
		else
		{
			$this->RenderJSON($vr);
		}
	}


	/**
	 * Stub method
	 */
	function Save()
	{
		if ( !RequestUtil::Get("SaveInline") )
		{
			throw new Exception("Save is not implemented by this controller");
		}

		require_once("ValidationResponse.php");
		$vr = new ValidationResponse();
		$vr->Success = false;
		$vr->Errors = array();
		$vr->Message = "SaveInline is not implemented by this controller";
		$this->RenderJSON($vr);
	}

	/**
	 * Returns an array of all property names in the primary model
	 *
	 * @return array
	 */
	protected function GetColumns()
	{
		if (!$this->ModelName)
		{
			throw new Exception("ModelName must be defined in " . get_class($this) . "::GetColumns");
		}

		$counter = 0;
		$props = array();
		foreach (get_class_vars($this->ModelName)as $var => $val)
		{
			$props[$counter++] = $var;
		}
		return $props;
	}

	/**
	 * Returns a unique ID for this session based on connection string and remote IP
	 * This is a reasonable variable to use as a session variable because it ensures
	 * that if other applications on the same server are running phreeze, there won't
	 * be cross-application authentication issues.  Additionally, the remote ip
	 * helps to make session hijacking more difficult
	 *
	 * @deprecated use $controller->GUID instead
	 * @return string
	 */
	private function GetGUID()
	{
		return $this->GUID;
	}

	/**
	 * Clears the current authenticated user from the session
	 */
	public function ClearCurrentUser()
	{
		require_once("verysimple/Authentication/Authenticator.php");
		
		$this->_cu = null;
		Authenticator::ClearAuthentication($this->GUID);
	}

	/**
	 * Sets the given user as the authenticatable user for this session.
	 *
	 * @param IAuthenticatable The user object that has authenticated
	 */
	protected function SetCurrentUser(IAuthenticatable $user)
	{
		$this->_cu = $user;
		Authenticator::SetCurrentUser($user,$this->GUID);

		// assign some global variables to the view
		$this->Assign("CURRENT_USER",$this->GetCurrentUser());
	}

	/**
	 * Returns the currently authenticated user, or null if a user has not been authenticated.
	 *
	 * @return IAuthenticatable || null
	 */
	protected function GetCurrentUser()
	{
		if (!$this->_cu)
		{
			require_once("verysimple/Authentication/Authenticator.php");
			
			$this->Phreezer->Observe("Loading CurrentUser from Session");
			$this->_cu = Authenticator::GetCurrentUser($this->GUID);

			if ($this->_cu )
			{
				if (get_class($this->_cu) == "__PHP_Incomplete_Class")
				{
					// this happens if the class used for authentication was not included before the session was started
					$tmp = print_r($this->_cu,1);
					$parts1 = explode("__PHP_Incomplete_Class_Name] => ",$tmp);
					$parts2 = explode("[",$parts1[1]);
					$name = trim($parts2[0]);

					Authenticator::ClearAuthentication($this->GUID);
					throw new Exception("The class definition used for authentication '$name' must be defined (included) before the session is started, for example in _app_config.php.");
				}
				else
				{
					// refresh the current user if the object supports it
					if (method_exists($this->_cu, 'Refresh')) $this->_cu->Refresh($this->Phreezer);
				}
			}
		}
		else
		{
			$this->Phreezer->Observe("Using previously loaded CurrentUser");
		}

		return $this->_cu;
	}

	/**
	 * Check the current user to see if they have the requested permission.
	 * If so then the function does nothing.  If not, then the user is redirected
	 * to $on_fail_action (if provided) or an AuthenticationException is thrown
	 *
	 * @param int $permission Permission ID requested
	 * @param string $on_fail_action (optional) The action to redirect if require fails
	 * @param string $not_authenticated_feedback (optional) Feedback to forward to the on_fail_action if user is not logged in
	 * @param string $permission_denied_feedback (optional) Feedback to forward to the on_fail_action if user is logged in but does not have permission
	 * @throws AuthenticationException
	 */
	protected function RequirePermission($permission, $on_fail_action = "", $not_authenticated_feedback = "Please login to access this page", $permission_denied_feedback = "You are not authorized to view this page and/or your session has expired")
	{
		$this->Phreezer->Observe("Checking For Permission '$permission'");
		$cu = $this->GetCurrentUser();

		if (!$cu || !$cu->IsAuthorized($permission))
		{
			$message = !$cu || $cu->IsAnonymous()
				? $not_authenticated_feedback
				: $permission_denied_feedback;
			
			if ($on_fail_action)
			{

				$this->Redirect($on_fail_action,$message);
			}
			else
			{
				$ex = new AuthenticationException($message,500);
				$this->Crash("Permission Denied",500,$ex);
			}
		}
	}

	/**
	 * Assigns a variable to the view
	 *
	 * @param string $varname
	 * @param variant $varval
	 */
	protected function Assign($varname,$varval)
	{
		$this->RenderEngine->assign($varname,$varval);
	}

	/**
	 * Renders the specified view
	 *
	 * @param string $view (optional) if not provided, the view is automatically bound using the class and method name
	 * @param string $format (optional) defaults to $self::SmartyViewPrefix
	 */
	protected function Render($view="",$format = null)
	{
		$isSmarty = (strpos(get_class($this->RenderEngine),"Smarty") > -1);

		if ($isSmarty && $format == null) $format = self::$SmartyViewPrefix;

		if ($format == null) $format = '';

		if ($view == "")
		{
			// automatic binding
			$backtrace = debug_backtrace();
			$view = str_replace("Controller","", $backtrace[1]['class']) . $backtrace[1]['function'];
		}

		// if the render engine is Smarty then add the '.tpl' suffix
		$viewPath = $isSmarty ? $format.$view.".tpl" : $format.$view;

		// capture output instead of rendering if specified
		if ($this->CaptureOutputMode)
		{
			$this->DebugOutput = $this->RenderEngine->fetch($viewPath);
		}
		else
		{
			$this->RenderEngine->display($viewPath);
		}
	}

	/**
	 * Renders the given value as JSON
	 *
	 * @param variant the variable, array, object, etc to be rendered as JSON
	 * @param string if a callback is provided, this will be rendered as JSONP
	 * @param bool if true then objects will be returned ->GetObject() (only supports ObjectArray or individual Phreezable or Reporter object)
	 * @param array  (only relvant if useSimpleObject is true) options array passed through to Phreezable->ToString()
	 * @param bool set to 0 to leave data untouched.  set to 1 to always force value to UTF8. set to 2 to only force UTF8 if an encoding error occurs (WARNING: options 1 or 2 will likely result in unreadable characters.  The recommended fix is to set your database charset to utf8)
	 */
	protected function RenderJSON($var, $callback = "",$useSimpleObject = false, $options = null, $forceUTF8 = 0)
	{
		$obj = null;

		if (is_a($var,'DataSet') || is_a($var,'DataPage'))
		{
			// if a dataset or datapage can be converted directly into an array without enumerating twice
			$obj = $var->ToObjectArray($useSimpleObject,$options);
		}
		else if ($useSimpleObject)
		{
			// we need to figure out what type
			if (is_array($var) || is_a($var,'SplFixedArray')  )
			{
				$obj = array();
				foreach ($var as $item)
				{
					$obj[] = $item->ToObject($options);
				}
			}
			elseif (is_a($var,'Phreezable') || is_a($var,'Reporter'))
			{
				$obj = $var->ToObject($options);
			}
			else
			{
				throw new Exception('RenderJSON could not determine the type of object to render');
			}
		}
		else
		{
			$obj = $var;
		}

		if ($forceUTF8 == 1) $this->UTF8Encode($obj);

		try
		{
			$output = json_encode($obj);
		}
		catch (Exception $ex)
		{
			if (strpos($ex->getMessage(),'Invalid UTF-8') !== false)
			{
				// a UTF encoding problem has been encountered
				if ($forceUTF8 == 2) 
				{
					$this->UTF8Encode($obj);
					$output = json_encode($obj);
				}
				else
				{
					throw new Exception('The object to be encoded contains invalid UTF-8 data.  Please verify your database character encoding or alternatively set the Controller::RenderJSON $forceUTF8 parameter to 1 or 2.');
				}
			}
			else
			{
				// we don't know what this is so don't try to handle it here
				throw $ex;
			}
		}

		if ($callback) $output = "$callback(" . $output . ")";

		// capture output instead of rendering if specified
		if ($this->CaptureOutputMode)
		{
			$this->DebugOutput = $output;
		}
		else
		{
			@header($callback ? 'Content-type: text/plain' : 'Content-type: application/json');
			print $output;
		}
	}


	/**
	 * Send a crash message to the browser and terminate
	 *
	 * @param string $errmsg text message to display
	 * @param int $code used for support for this error
	 * @param Exception $exception if exception was thrown, can be provided for more info
	 */
	protected function Crash($errmsg = "Unknown Error", $code = 0, $exception = null)
	{
		$ex = $exception ? $exception : new Exception($errmsg, $code);
		throw $ex;
	}

	/**
	 * Redirect to the appropriate page based on the action.  This function will
	 * call "exit" so do not put any code that you wish to execute after Redirect
	 *
	 * @param string $action in the format Controller.Method
	 * @param string $feedback
	 * @param array $params
	 * @param string $mode (client | header) default = Controller::$DefaultRedirectMode
	 */
	protected function Redirect($action, $feedback = "", $params = "", $mode = "")
	{
		if (!$mode) $mode = self::$DefaultRedirectMode;

		$params = is_array($params) ? $params : array();

		if ($feedback)
		{
			// $params["feedback"] = $feedback;
			$this->Context->Set("feedback",$feedback);
		}

		// support for deprecated Controller/Method format
		list($controller,$method) = explode(".", str_replace("/",".",$action));

		$url = $this->GetRouter()->GetUrl($controller,$method,$params);

		// capture output instead of rendering if specified
		if ($this->CaptureOutputMode)
		{
			if ($mode == 'client')
			{
				$this->RenderEngine->assign("url",$url);
				$this->DebugOutput = $this->RenderEngine->fetch("_redirect.tpl");
			}
			else
			{
				$this->DebugOutput = 'Location: ' . $url;
			}
		}
		else
		{

			if ($mode == 'client')
			{
				$this->RenderEngine->assign("url",$url);
				$this->RenderEngine->display("_redirect.tpl");
			}
			else
			{
				header('Location: ' . $url) ;
			}

		}

		// don't exit if we are unit testing because it will stop all further tests
		if (!$this->UnitTestMode) exit;

	}

	/**
	 * Does a recursive UTF8 encoding on a string/array/object.  This is used
	 * when json encoding fails due to non UTF8 in the database that cannot
	 * be repaired with any other means.
	 * 
	 * NOTE: this does not have a return value.  value is passed by reference and updated
	 * 
	 * @param variant $input
	 */
	private function UTF8Encode(&$input) 
	{
		if (is_string($input)) 
		{
			// pop recursion here
			$input = utf8_encode($input);
		} 
		else if (is_array($input)) 
		{
			foreach ($input as &$value) 
			{
				$this->UTF8Encode($value);
			}
			unset($value);
		} 
		else if (is_object($input)) 
		{
			$vars = array_keys(get_object_vars($input));
			foreach ($vars as $var) 
			{
				$this->UTF8Encode($input->$var);
			}
		}
	}

    /**
    * Throw an exception if an undeclared method is accessed
    *
	* @access     public
	* @param      string $name
	* @param      variant $vars
	* @throws     Exception
	*/
	function __call($name,$vars = null)
	{
		throw new Exception(get_class($this) . "::" . $name . " is not implemented");
	}
}

?>