<?php
// Copyright 2011 Toby Zerner, Simon Zerner
// This file is part of esoTalk. Please see the included license file for usage information.

if (!defined("IN_ESOTALK")) exit;

/**
 * A class that defines a controller, and provides methods and properties to dispatch requests and render
 * the page. A controller takes user input from a request, handles it, and responds by rendering content/data.
 *
 * @package esoTalk
 */
class ETController extends ETPluggable {


/**
 * The master view that will be used to render the page. The master view is a "wrapper" that contains all
 * the common elements of a page (header, footer, etc.) and that will render actual view within it.
 * @var string
 */
public $masterView = "default.master";


/**
 * An array of data that will be passed to the view as a local variable when the page is rendered. This
 * should be used as the primary transport of data between a controller and a view.
 * @var array
 */
public $data = array();


/**
 * The title of the page. The default master view will use this in the <title> tag.
 * @var string
 */
public $title = "";


/**
 * An array of JavaScript files to be included in the head of the page.
 * @var array
 */
private $jsFiles = array("global" => array(), "local" => array(), "remote" => array());


/**
 * An array of CSS files to be included in the head of the page.
 * @var array
 */
private $cssFiles = array("global" => array(), "local" => array(), "remote" => array());


/**
 * An array of language definition codes to make accessible to JavaScript.
 * @var array
 */
private $jsLanguage = array();


/**
 * An array of data to make accessible to JavaScript.
 * @var array
 */
private $jsData = array();


/**
 * A string of HTML to append before the </head> tag. This could contain <meta> tags or other code.
 * @var string
 */
private $head = "";


/**
 * An array of ETMenu objects for common menus (such as "user", "main", and "meta".)
 * @var array
 */
public $menus = array();


/**
 * An array of data to output in a JSON response.
 * @var array
 */
public $json = array();


/**
 * The content type to use for the response.
 * @var string
 */
public $contentType = "text/html";


/**
 * The response type. This can be any of the RESPONSE_TYPE_* constants (see config.defaults.php.)
 * @var string
 */
public $responseType = RESPONSE_TYPE_DEFAULT;


/**
 * Class(es) to apply to the <body> tag.
 * @var string
 */
public $bodyClass = "";


/**
 * When $this->pushNavigation() is called, we store the navigation ID in here so that we can use it when we
 * later call ET::$session->getNavigation().
 * @var string
 */
protected $navigationId = false;


/**
 * The URL to the request for the current page.
 * @var string
 */
public $selfURL = "";


/**
 * The canonical URL for the current page. This should be set by a controller method.
 * @var string
 */
public $canonicalURL = "";


/**
 * An array of messages to show on the page.
 * @var string
 */
private $messages = array();


/**
 * Class constructor.
 *
 * @return void
 */
public function __construct()
{
	// Pull any messages stored into the session into the controller's $messages property.
	$messages = ET::$session->get("messages");
	if (is_array($messages)) $this->messages = $messages;

	if (ET::$session->errorCount()) $this->messages(ET::$session->errors(), "warning");
}


/**
 * Dispatch a request to $method, passing along $arguments.
 *
 * @param string $method The name of the controller method.
 * @param array $arguments An array of arguments to pass to the method.
 * @return void
 */
public function dispatch($method, $arguments)
{
	// Create an array of arguments where the first item is $this.
	$eventArguments = array_merge(array(&$this), $arguments);
	$eventName = $this->className."_".$method;

	// Trigger a "before" event for this method.
	ET::trigger($eventName."_before", $eventArguments);

	// Go through plugins and look for a handler for this controller/method.
	$called = false;
	foreach (ET::$plugins as $plugin) {
		$actionName = "action_".$eventName;
		if (method_exists($plugin, $actionName)) {
			call_user_func_array(array($plugin, $actionName), $eventArguments);
			$called = true;
			break;
		}
	}

	// If one wasn't found, call the method on $this.
	if (!$called) call_user_func_array(array($this, "action_".$method), $arguments);

	// Trigger an "after" event for this method.
	ET::trigger($eventName."_after", $eventArguments);
}


/**
 * Add a message to be displayed on the page. The messages will also be stored in the session so that if the
 * controller redirects instead of rendering, they will be displayed on the next response.
 *
 * @param string $message The message text.
 * @param mixed $options An array of options. Possible keys include:
 * 		id: a unique ID for the message. If specified, this message will overwrite any previous messages with
 * 			the same ID.
 * 		className: the CSS class to apply to the message.
 * 		callback: a JavaScript function to run when the message is dismissed.
 * 		If $options is a string, it will be used as the className.
 * @return void
 */
public function message($message, $options = "")
{
	if (!is_array($options)) $options = array("className" => $options);
	$options["message"] = $message;
	if (!empty($options["id"])) $this->messages[$options["id"]] = $options;
	else $this->messages[] = $options;
	ET::$session->store("messages", $this->messages);
}


/**
 * Add an array of messages to be displayed on the page. This is the same as looping through an array and
 * calling message() for each item.
 *
 * @param array $messages An array of messages. Any non-numeric keys will be used as the ID for their message.
 * @param mixed $options An array of options; see message() for a full description. These options will be used
 * 		for all of the messages.
 * @return void
 */
public function messages($messages, $options = "")
{
	if (!is_array($options)) $options = array("className" => $options);
	foreach ($messages as $id => $message) {
		$options["id"] = !is_numeric($id) ? $id : null;
		$this->message(T("message.$message", $message), $options);
	}
}


/**
 * Given an array of notifications, add messages to the controller to display the notifications in the
 * messages area.
 *
 * @param array $notifications An array of notifications, typically from ETActivityModel::getNotifications(-1).
 * @return void
 */
public function notificationMessages($notifications)
{
	// Only show the first 3 notifications.
	$notifications = array_slice($notifications, 0, 3);
	
	foreach ($notifications as $notification) {

		// If we've already shown this notification as a message before, don't show it again.
		if ($notification["time"] <= ET::$session->preference("notificationCheckTime")) continue;

		$avatar = avatar(array(
			"memberId" => $notification["fromMemberId"],
			"avatarFormat" => $notification["avatarFormat"],
			"avatarTime" => $notification["avatarTime"],
			"email" => $notification["email"]
		), "thumb");
		$this->message("<a href='".$notification["link"]."' class='messageLink'><span class='action'>".$avatar.$notification["body"]."</span></a>", "popup notificationMessage autoDismiss hasSprite");
	}

	// Update the user's "notificationCheckTime" preference so these notifications won't be shown again.
	ET::$session->setPreferences(array("notificationCheckTime" => time()));
}


/**
 * Common initialization for all controllers, called on every page load. This will add basic user links to
 * the "user" menu, and add core JS files and language definitions.
 *
 * If this is overridden, parent::init() should be called to maintain consistency between controllers.
 *
 * @return void
 */
public function init()
{
	// Check for updates to the esoTalk software, but only if we're the root admin and we haven't checked in
	// a while.
	if (ET::$session->userId == C("esoTalk.rootAdmin") and C("esoTalk.admin.lastUpdateCheckTime") + C("esoTalk.updateCheckInterval") < time())
		ET::upgradeModel()->checkForUpdates();

	if ($this->responseType === RESPONSE_TYPE_DEFAULT) {

		// If the user IS NOT logged in, add the 'login' and 'sign up' links to the bar.
		if (!ET::$session->user) {
			$this->addToMenu("user", "join", "<a href='".URL("user/join?return=".urlencode($this->selfURL))."' class='link-join'>".T("Sign Up")."</a>");
			$this->addToMenu("user", "login", "<a href='".URL("user/login?return=".urlencode($this->selfURL))."' class='link-login'>".T("Log In")."</a>");
		}

		// If the user IS logged in, we want to display their name and appropriate links.
		else {
			$this->addToMenu("user", "user", "<a href='".URL("member/me")."'>".avatar(ET::$session->user, "thumb").name(ET::$session->user["username"])."</a>");

			$this->addToMenu("user", "settings", "<a href='".URL("settings")."' class='link-settings'>".T("Settings")."</a>");

			if (ET::$session->isAdmin())
				$this->addToMenu("user", "administration", "<a href='".URL("admin")."' class='link-administration'>".T("Administration")."</a>");

			$this->addToMenu("user", "logout", "<a href='".URL("user/logout?token=".ET::$session->token)."' class='link-logout'>".T("Log Out")."</a>");
		}

		// Get the number of members currently online and add it as a statistic.
		if (C("esoTalk.members.visibleToGuests") or ET::$session->user) {
			$online = ET::SQL()
				->select("COUNT(*)")
				->from("member")
				->where("UNIX_TIMESTAMP()-:seconds<lastActionTime")
				->bind(":seconds", C("esoTalk.userOnlineExpire"))
				->exec()
				->result();
			$stat = Ts("statistic.online", "statistic.online.plural", number_format($online));
			$stat = "<a href='".URL("members/online")."' class='link-membersOnline'>$stat</a>";
			$this->addToMenu("statistics", "statistic-online", $stat);
		}

		$this->addToMenu("meta", "copyright", "<a href='https://eso.towerlight.top/' target='_blank'>".T("Powered by")." esoTalk 中文优化版</a>");

		// Set up some default JavaScript files and language definitions.
		$this->addJSFile("core/js/lib/jquery.js", true);
		$this->addJSFile("core/js/lib/jquery.migrate.js", true);
		$this->addJSFile("core/js/lib/jquery.misc.js", true);
		$this->addJSFile("core/js/lib/jquery.history.js", true);
		$this->addJSFile("core/js/lib/jquery.scrollTo.js", true);
		$this->addJSFile("core/js/global.js", true);
		$this->addJSLanguage("message.ajaxRequestPending", "message.ajaxDisconnected", "Loading...", "Notifications");
		$this->addJSVar("notificationCheckInterval", C("esoTalk.notificationCheckInterval"));

	}

	$this->trigger("init");
}


/**
 * Redirect to another location.
 *
 * If the response type is AJAX or JSON, this function will render the page with a "redirect" key set in the
 * response data. The esoTalk JavaScript will set window.location upon receiving this data.
 *
 * @param string $url The URL to redirect to.
 * @param int $code The HTTP response code to respond with. This will usually be either 302 (temporary) or
 * 		301 (permanent).
 */
public function redirect($url, $code = 302)
{
	if ($this->responseType === RESPONSE_TYPE_AJAX or $this->responseType === RESPONSE_TYPE_JSON or $this->responseType === RESPONSE_TYPE_VIEW) {
		if ($this->responseType === RESPONSE_TYPE_VIEW) $this->responseType = RESPONSE_TYPE_AJAX;
		$this->json("redirect", $url);
		$this->render();
		exit;
	}
	else redirect($url, $code);
}


/**
 * Push an item onto the top of the navigation (breadcrumb) stack.
 *
 * This is simply a layer on top of ETSession::pushNavigation() which stores the navigation ID. Later in the
 * controller's life, the navigation ID is used to create a "back" button with ETSession::getNavigation().
 *
 * @see ETSession::pushNavigation()
 * @param string $id The navigation ID.
 * @param string $type The type of page this is.
 * @param string $url The URL to this page.
 * @return void
 */
public function pushNavigation($id, $type, $url)
{
	$this->navigationId = $id;
	ET::$session->pushNavigation($id, $type, $url);
}


/**
 * Add a piece of data to be rendered in a JSON response.
 *
 * @param string $key The JSON key.
 * @param mixed $value The value.
 * @return void
 */
public function json($key, $value)
{
	$this->json[$key] = $value;
}


/**
 * Add a piece of data to be transported to the view when it is rendered.
 *
 * @param string $key The data key.
 * @param mixed $value The data value.
 * @return void
 */
public function data($key, $value)
{
	$this->data[$key] = $value;
}


/**
 * Render the specified view, in the format according to the controller's set response type.
 *
 * @param string $view The view to render. This can be left blank if we know the response type is one that
 * 		doesn't require a view, such as JSON or ATOM.
 * @return void
 */
public function render($view = "")
{
	$this->trigger("renderBefore");

	if ($this->responseType == RESPONSE_TYPE_DEFAULT and ET::$session->user) {
		
		// Fetch all unread notifications so we have a count for the notifications button.
		$notifications = ET::activityModel()->getNotifications(-1);
		$count = count($notifications);
		$this->addToMenu("user", "notifications", "<a href='".URL("settings/notifications")."' id='notifications' class='button popupButton ".($count ? "new" : "")."'><span>$count</span></a>");

		// Show messages with these notifications.
		$this->notificationMessages($notifications);
		
	}

	// Set up the master view, content type, and other stuff depending on the response type.
	switch ($this->responseType) {

		// For an ATOM response, set the master view and the content type.
		case RESPONSE_TYPE_ATOM:
			$this->masterView = "atom.master";
			$this->contentType = "application/atom+xml";
			break;

		// For an AJAX or JSON response, set the master view and the content type.
		// If it's an AJAX response, set one of the JSON parameters to the specified view's contents.
		case RESPONSE_TYPE_AJAX:
			if ($view) $this->json("view", $this->getViewContents($view, $this->data));

		case RESPONSE_TYPE_JSON:
			$this->masterView = "json.master";
			$this->contentType = "application/json";

	}

	// Set a content-type header.
	header("Content-type: ".$this->contentType."; charset=".T("charset", "utf-8"));

	// If we're just outputting the view on its own, do that now.
	if ($this->responseType === RESPONSE_TYPE_VIEW) {
		$this->renderView($view, $this->data);
	}

	// Otherwise, set up the master view and render it.
	else {

		// Make a new data array for the master view.
		$data = array();

		// For any master views but the JSON and ATOM ones, give the view some data that will be useful in
		// rendering a HTML page.
		if ($this->masterView != "json.master" and $this->masterView != "atom.master") {

			// Fetch the content of the view, passing the data collected in the controller.
			if ($view) $data["content"] = $this->getViewContents($view, $this->data);

			// If config/custom.css contains something, add it to be included in the page.
			if (file_exists($file = PATH_CONFIG."/custom.css") and filesize($file) > 0) {
				$this->addCSSFile("config/custom.css", true);
			}

			// Add the <head> contents and the page title.
			$data["head"] = $this->head();
			$titleParts = array();
			if ($this->title) $titleParts[] = $this->title;
			if ($t = C("esoTalk.forumTitle")) $titleParts[] = $t;
			$data["pageTitle"] = implode(" - ", $titleParts);

			// Add the forum title, or logo if the forum has one.
			$logo = C("esoTalk.forumLogo");
			$title = C("esoTalk.forumTitle");
			if ($logo) $size = getimagesize($logo);
			$data["forumTitle"] = $logo ? "<img src='".C("esoTalk.cdnURL").getWebPath($logo)."' {$size[3]} alt='$title'/>" : $title;

			// Add the forum favicon
			$favicon = C("esoTalk.forumFavicon");
			$data["forumFavicon"] = "<link href='".C("esoTalk.cdnURL").getWebPath($favicon)."' rel='shortcut icon'/>";

			// Add the details for the "back" button.
			$data["backButton"] = ET::$session->getNavigation($this->navigationId);

			// Get common menu items.
			foreach ($this->menus as $menu => $items)
				$data[$menu."MenuItems"] = $items->getContents();

			// Add the body class.
			$data["bodyClass"] = $this->bodyClass;

			// Get messages.
			$data["messages"] = $this->getMessages();

		}

		$this->renderView($this->masterView, $data);

	}

	$this->trigger("renderAfter");
}


/**
 * Render a simple message sheet with an 'OK' button. This can be used to easily display, for example, a
 * "you do not have permission to be here" message.
 *
 * @param string $title The title to use in the message sheet.
 * @param string $message The message text.
 * @return void
 */
public function renderMessage($title, $message)
{
	// Add the title and message to be passed to the view.
	$this->data("title", $title);
	$this->data("message", $message);

	// If the response type is anything other than default, just make it an AJAX response and set a JSON
	// parameter so the esoTalk JavaScript knows to display a modal message sheet.
	if ($this->responseType !== RESPONSE_TYPE_DEFAULT) {
		$this->responseType = RESPONSE_TYPE_AJAX;
		$this->json("modalMessage", true);
	}

	$this->render("message");
}


/**
 * Render a "Page Not Found" message sheet, and send a 404 header with the response. This can be used to
 * easily display, for example, a "this conversation was not found" message.
 *
 * @param string $message The message text.
 * @return void
 */
public function render404($message = "", $showLogin = false)
{
	header("HTTP/1.1 404 Not Found");

	// If the user isn't logged in, we might want to show a login form to them.
	// To do this, we create an ETUserController instance, set a message to display on the login form,
	// and then run the "login" method.
	if (!ET::$session->user and $showLogin) {
		$_GET["return"] = $this->selfURL;
		$controller = ETFactory::make("userController");
		$controller->init();
		$controller->loginMessage = $message;
		$controller->dispatch("login", array());
	}

	// If they are logged in, however, we'll just show a page not found message.
	else {
		$this->renderMessage(T("Page Not Found"), $message);
	}
}


/**
 * Validate an input token. If it's invalid, show a "no permission" message.
 *
 * @param string $token The token to validate. If false, the token will automatically be taken from the
 * 		request input.
 * @return bool true if the token is valid, false if it isn't.
 */
public function validateToken($token = false)
{
	if ($token === false) $token = R("token");

	if (!ET::$session->validateToken($token)) {
		$this->renderMessage(T("Error"), T("message.noPermission"));
		return false;
	}
	return true;
}


/**
 * Make sure that the user is logged in, or the specified configuration key is true. If not, redirect
 * to the login page.
 *
 * This is generally used to make sure the user is allowed to view the forum (i.e. they are logged in
 * or the forum is visible to guests.)
 *
 * @param string $key The configuration key which determines whether this page is visible to guests.
 * @return bool true if the user is allowed to view this page, false if they are not.
 */
public function allowed($key = "esoTalk.visibleToGuests")
{
	if (ET::$session->user or C($key)) return true;

	$url = ltrim($this->selfURL, "/");
	$this->redirect(URL("user/login".($url ? "?return=$url" : "")));
	return false;
}


/**
 * Renders a view, and captures and returns the output.
 *
 * @param string $view The name of the view to get.
 * @param array $data An array of data to pass to the view.
 * @return string The output of the view.
 */
public function getViewContents($view, $data = array())
{
	ob_start();
	$this->renderView($view, $data);
	$content = ob_get_clean();
	return $content;
}


/**
 * Renders a view.
 *
 * @param string $view The name of the view to render.
 * @param array $data An array of data to pass to the view.
 * @return void
 */
public function renderView($view, $data = array())
{
	ob_start();
	include $this->getViewPath($view);
	$content = ob_get_clean();

	$this->trigger("renderView", array($view, &$content, $data));

	echo $content;
}


/**
 * Gets the full filepath to the specified view.
 *
 * @param string $view The name of the view to get the filepath of.
 * @return string The filepath of the view.
 */
public function getViewPath($view)
{
	// If the view has a file extension, assume it contains the full file path and use it as is.
	if (pathinfo($view, PATHINFO_EXTENSION) == "php") return $view;

	// Check the skin to see if it contains this view.
	if (file_exists($skinView = ET::$skin->view($view))) return $skinView;

	// Check loaded plugins to see if one of them contains the view.
	foreach (ET::$plugins as $k => $v) {
		if (file_exists($pluginView = $v->view($view))) return $pluginView;
	}

	// Otherwise, just return the default view.
	return PATH_VIEWS."/$view.php";
}


/**
 * Get all of the controller's messages, and remove them from the session storage.
 *
 * @return array An array of the controller's messages.
 */
public function getMessages()
{
	ET::$session->remove("messages");

	return $this->messages;
}


/**
 * Set a language definition(s) to be accessible by JavaScript code on the page, as a property of the
 * esoTalk.language object.
 *
 * @param string $key,... Unlimited number of language definition keys to make accessible to JavaScript.
 * @return void
 */
public function addJSLanguage()
{
	$args = func_get_args();
	foreach ($args as $k) $this->jsLanguage[$k] = T($k);
}


/**
 * Set a variable that can be accessed by JavaScript code on the page, as a property of the esoTalk object.
 *
 * @param string $key The key to make $val accessible under.
 * @param mixed $val The value.
 * @return void
 */
public function addJSVar($key, $val)
{
	$this->jsData[$key] = $val;
}


/**
 * Add a JavaScript file to be included in the page header.
 *
 * @param string $file The relative or absolute path to the JavaScript file.
 * @param bool $global Whether or not this file is included globally (on every interface of the application.)
 * 		If true, we will aggregate this with other global files to get consistency, encouraging the browser
 * 		to cache the aggregated file.
 * @return void
 */
public function addJSFile($file, $global = false)
{
	if (strpos($file, "://") !== false) $key = "remote";
	$key = $global ? "global" : "local";
	if (!in_array($file, $this->jsFiles[$key])) $this->jsFiles[$key][] = $file;
}


/**
 * Add a CSS file, or files, to be included on the page.
 *
 * @param string $file The relative or absolute path to the CSS file.
 * @param bool $global Whether or not this file is included globally (on every interface of the application.)
 * 		If true, we will aggregate this with other global files to get consistency, encouraging the browser
 * 		to cache the aggregated file.
 * @return void
 */
public function addCSSFile($file, $global = false)
{
	if (strpos($file, "://") !== false) $key = "remote";
	else $key = $global ? "global" : "local";
	if (!in_array($file, $this->cssFiles[$key])) $this->cssFiles[$key][] = $file;
}


/**
 * Add a string of HTML to be outputted inside of the <head> tag. This can be used to add things to the page
 * like <meta> tags.
 *
 * @param string $string The string to add.
 * @return void
 */
public function addToHead($string)
{
	$this->head .= "\n$string";
}


/**
 * Take a collection of CSS or JS files and create and return the filename of an aggregation file which
 * contains all of their individual contents.
 *
 * @param array $files An array of files to aggregate.
 * @param string $type The type of files we are aggregating ("css" or "js").
 * @return array An array containing a single element, which is the path to the aggregation file.
 */
protected function aggregateFiles($files, $type)
{
	// Construct an array of filenames, and get the maximum last modifiction time of all the files.
	$filenames = array();
	$lastModTime = 0;
	foreach ($files as $filename) {
		$filenames[] = str_replace(".", "", pathinfo($filename, PATHINFO_FILENAME));
		$lastModTime = max($lastModTime, filemtime(PATH_ROOT."/".$filename));
	}

	// Construct a filename for the aggregation file based on the individual filenames.
	$file = PATH_ROOT."/cache/$type/".implode(",", $filenames).".$type";

	// If this file doesn't exist, or if it is out of date, generate and write it.
	if (!file_exists($file) or filemtime($file) < $lastModTime) {
		$contents = "";

		// Get the contents of each of the files, fixing up image URL paths for CSS files.
		foreach ($files as $f) {
			$content = file_get_contents(PATH_ROOT."/".$f);
			if ($type == "css") $content = preg_replace("/url\(('?)/i", "url($1".getResource(pathinfo($f, PATHINFO_DIRNAME)."/"), $content);
			$contents .= $content." ";
		}

		// Minify and write the contents.
		file_force_contents($file, $type == "css" ? minifyCSS($contents) : minifyJS($contents));
	}

	return array($file);
}


/**
 * Generate all of the HTML to be outputted inside of the <head> tag.
 *
 * @return string The HTML to go inside <head>.
 */
public function head()
{
	$head = "<!-- This page was generated by esoTalk (http://esotalk.org) -->\n";

	// Add the canonical URL tag.
	if (!empty($this->canonicalURL))
		$head .= "<link rel='canonical' href='$this->canonicalURL'>\n";

	// Add remote stylesheets.
	if (!empty($this->cssFiles["remote"])) {
		foreach ($this->cssFiles["remote"] as $url) {
			$head .= "<link rel='stylesheet' href='$url'>\n";
		}
	}
	unset($this->cssFiles["remote"]);

	// Go through CSS stylesheets and aggregate them, then add appropriate tags to the header.
	// Here we loop through "groups" of CSS files (usually "global" and "local".)
	foreach ($this->cssFiles as $key => $files) {

		// If CSS aggregation is enabled, and there's more than one file in this "group", proceed with aggregation.
		if (count($files) > 1 and C("esoTalk.aggregateCSS") and !(ET::$controller instanceof ETAdminController))
			$files = $this->aggregateFiles($files, "css");

		// Otherwise, we need to prepend the full path to each of the files.
		else foreach ($files as &$file) $file = PATH_ROOT."/".$file;
		unset($file);

		// For each of the files that we need to include in the page, add a <link> tag.
		foreach ($files as $file)
			$head .= "<link rel='stylesheet' href='".C("esoTalk.cdnURL").getResource($file)."?".@filemtime($file)."'>\n";

	}

	// Output all necessary config variables and language definitions, as well as other variables.
	$esoTalkJS = array(
		"webPath" => ET::$webPath.((C("esoTalk.urls.friendly") and !C("esoTalk.urls.rewrite")) ? "/index.php" : ""),
		"userId" => ET::$session->user ? (int)ET::$session->userId : false,
		"token" => ET::$session->token,
		"debug" => C("esoTalk.debug"),
		"language" => $this->jsLanguage
	) + (array)$this->jsData;
	$head .= "<script>var ET=".json_encode($esoTalkJS)."</script>\n";

	// Add remote JavaScript.
	if (!empty($this->jsFiles["remote"])) {
		foreach ($this->jsFiles["remote"] as $url) {
			$head .= "<script src='$url'></script>\n";
		}
	}
	unset($this->jsFiles["remote"]);

	// Same thing as above, but with JavaScript!
	foreach ($this->jsFiles as $files) {

		// If JS aggregation is enabled, and there's more than one file in this "group", proceed with aggregation.
		if (count($files) > 1 and C("esoTalk.aggregateJS") and !(ET::$controller instanceof ETAdminController))
			$files = $this->aggregateFiles($files, "js");

		// Otherwise, we need to prepend the full path to each of the files.
		else foreach ($files as &$file) $file = PATH_ROOT."/".$file;
		unset($file);

		// For each of the files that we need to include in the page, add a <script> tag.
		foreach ($files as $file)
			$head .= "<script src='".C("esoTalk.cdnURL").getResource($file)."?".filemtime($file)."'></script>\n";
	}

	// Finally, append the custom HTML string constructed via $this->addToHead().
	$head .= $this->head;

	$this->trigger("head", array(&$head));

	return $head;
}


/**
 * Add an item to one of the master view's menus.
 *
 * @param string $menu The name of the menu.
 * @param string $id The name of this menu item.
 * @param string $html The content of this menu item.
 * @param mixed $position Where to put this menu item relative to the others.
 * @see addToArray()
 * @return void
 */
public function addToMenu($menu, $id, $html, $position = false)
{
	if (empty($this->menus[$menu])) $this->menus[$menu] = ETFactory::make("menu");
	$this->menus[$menu]->add($id, $html, $position);
}

}
