<?php
class MY_Controller extends CI_Controller
{
	protected $content       = "";
	public $scripts          = array();
	public $less             = array();
	public $enable_jquery    = false;
	public $enable_jquery_ui = false;
	protected $messages      = array();
	protected $errors        = array();

	public function __construct()
	{
		parent::__construct();

		$this->load->driver('cache', array('adapter' => 'file', 'backup' => 'dummy'));

		# Load LESS.PHP processor.
		require_once(ABSOLUTE_PATH . "/application/third_party/lessphp/lib/Less/Autoloader.php");
		Less_Autoloader::register();

		// Force SSL if the constant is set but the HTTPS server variable is not set or set but false.
		if(defined("FORCE_SSL") && FORCE_SSL && preg_match('/^http:/', current_url()) && !is_cli())
		{
			if(defined("LOGGING")) { error_log(PRODUCT_NAME . ": Redirecting to HTTPS."); }
			redirect(str_replace("http://", "https://", current_url()), 'auto', 301);
		}

		// The landing page will be loaded (without a template applied) under the following conditions:
		// 	- The site has not been installed yet.
		// 	- The site fails to initialize.
		// 	- The landing page is enabled.
		if((INSTALLED || defined("INSTALL_COMPLETE")) && $this->initialize())
		{
			if(Mainframe::site()->show_landing == 1 && uri_string() == "")
			{
				if(defined("LOGGING")) { error_log(PRODUCT_NAME . ": Landing page is enabled. Showing landing page."); }
				$this->load->view("landing");
				die();
			}
		}
		// Product is not installed and this is the not the installation screen.  Show landing page.
		else if(!preg_match('/install\/?/', current_url()))
		{
			if(defined("LOGGING")) { error_log(PRODUCT_NAME . ": Product not installed yet. Showing landing page."); }
			$this->load->view("landing");
			die();
		}
	}

	protected function initialize()
	{
		require_once(APPLICATION_PATH . "/third_party/feral.php");

		$this->load->model(array("template", "page", "module", "moduleinstance"));

		if(!Mainframe::init())
		{
			if(defined("LOGGING")) { error_log(PRODUCT_NAME . ": Failed to initialize the Mainframe."); }
			return false;
		}

		$site     = Mainframe::site();
		$template = new Template();
		$template->Load($site->template_id);
		Mainframe::setTemplate($template);

		if(!$template->template_id || !file_exists(TEMPLATE_PATH . "/$template->directory/index.php"))
		{
			if(defined("LOGGING")) { error_log(PRODUCT_NAME . ": No template selected or template's index.php file doesn't exist."); }
			return false;
		}

		// If a user is logged in, refresh the expiration on their cookie.
		$user = get_user();

		if($user->user_id)
		{
			$this->load->library("encryption");
			setcookie("user_id", $this->encryption->encrypt($user->user_id), time() + $this->config->item("sess_expiration"), $this->config->item("cookie_path"), $this->config->item("cookie_domain"), $this->config->item("cookie_secure"), $this->config->item("cookie_httponly"));
		}

		ob_start();

		return true;
	}

	protected function start()
	{
		// Not needed
		// This happens in the constructor now.
		// Left here for backwards compatibility.
		return;
	}

	protected function template()
	{
		$site     = Mainframe::site();
		$template = Mainframe::template();
		$page     = Mainframe::page();

		if(!$this->acl->checkPage($page->page_id, Mainframe::user(), "read"))
		{
			if($site->login_page_id)
			{
				$login_page = new Page();
				$login_page->Load($site->login_page_id);

				// Ampersand used intentionally in URL below
				$login_url = "/" . $login_page->url . "?redirect=" . rawurlencode($page->url);

				header("Location: $login_url");
				exit();
			}
			else
			{
				// Not authorized
				show_error("You are not authorized to view this page.", 403, "Access Denied");
			}
		}

		//Start a new output buffer, include our template, and capture the output.
		ob_start();
		require(TEMPLATE_PATH . "/$template->directory/index.php");
		$this->content = ob_get_contents();
		ob_end_clean();
	}

	protected function modules()
	{
		$page = Mainframe::page();

		//loop each module for this page and insert it into the module position div
		//modules are loaded in reverse sort order because they will be inserted in reverse order
		$modules = Mainframe::LoadModules($page->page_id);

		foreach($modules as $module)
		{
			if(!$module->position ||
			   !$module->published ||
			   !$this->acl->checkModule($module->module_instance_id, Mainframe::user(), "read"))
			{
				continue;
			}

			// The module's main file doesn't exist - this is a bad module.
			if(!file_exists(MODULE_PATH . "/$module->file_name/$module->file_name" . ".php"))
			{
				continue;
			}
			// If the module contains a LESS file by the same name, automatically include it.
			if(file_exists(MODULE_PATH . "/$module->file_name/$module->file_name" . ".less"))
			{
				$this->less[] = MODULE_PATH . "/$module->file_name/$module->file_name" . ".less";
			}
			require_once(MODULE_PATH . "/$module->file_name/$module->file_name" . ".php");

			$m = new $module->class_name;
			// Some modules need to be loaded before they know what scripts to include.
			// Otherwise, we could avoid calling Load() entirely when a module is cached.
			$m->Load($module->module_instance_id);

			if($m->enable_jquery())
			{
				$this->enable_jquery = true;
			}
			if($m->enable_jquery_ui())
			{
				$this->enable_jquery_ui = true;
			}

			foreach($m->scripts() as $script)
			{
				$this->scripts[] = $script;
			}

			$key = $m->getCacheKey();

			// If the module does not allow caching or the module is not yet cached, generate its content.
			if(!$m->allowCaching() || ($module_content = $this->cache->get($key)) === false)
			{
				//Start a new output buffer, render our module, and capture the output.
				$module_content = "<div class=\"module\">";
				ob_start();
				$m->render();
				$module_content .= ob_get_contents();
				ob_end_clean();
				$module_content .= "</div>";

				$this->cache->save($key, $module_content, $m->getCacheTime());
			}

			$this->content = $this->str_replace_one("<div id=\"module_position_$module->position\">", "<div id=\"module_position_$module->position\">" . $module_content, $this->content);
		}

		$more_modules = true;

		// Continually loop our page content until there are no more module placeholders to replace
		// <img class="module_placeholder" id="module_2" />
		while($more_modules)
		{
			preg_match_all('/<img [^>]*?class="module_placeholder"[^>]*?[^>]*?id="module_([0-9]+)"[^>]*?\/>/', $this->content, $placeholders);

			$more_modules = false;
			$count        = count($placeholders[0]);

			for($i=0; $i<$count; $i++)
			{
				$code 		= $placeholders[0][$i];
				$module_id 	= $placeholders[1][$i];
				$module 	= new ModuleInstance();

				if($module->Load($module_id))
				{
					if(!$module->published ||
					   !$this->acl->checkModule($module->module_instance_id, Mainframe::user(), "read"))
					{
						continue;
					}

					// The module's main file doesn't exist - this is a bad module.
					if(!file_exists(MODULE_PATH . "/$module->file_name/$module->file_name" . ".php"))
					{
						continue;
					}
					// If the module contains a LESS file by the same name, automatically include it.
					if(file_exists(MODULE_PATH . "/$module->file_name/$module->file_name" . ".less"))
					{
						$this->less[] = MODULE_PATH . "/$module->file_name/$module->file_name" . ".less";
					}
					require_once(MODULE_PATH . "/$module->file_name/$module->file_name" . ".php");

					$m = new $module->class_name;
					// Some modules need to be loaded before they know what scripts to include.
					// Otherwise, we could avoid calling Load() entirely when a module is cached.
					$m->Load($module_id);

					if($m->enable_jquery())
					{
						$this->enable_jquery = true;
					}
					if($m->enable_jquery_ui())
					{
						$this->enable_jquery_ui = true;
					}

					foreach($m->scripts() as $script)
					{
						$this->scripts[] = $script;
					}

					$key = $m->getCacheKey();

					// If the module does not allow caching or the module is not yet cached, generate its content.
					if(!$m->allowCaching() || ($module_content = $this->cache->get($key)) === false)
					{
						//Start a new output buffer, render our module, and capture the output.
						$module_content = "<div class=\"module\">";
						ob_start();
						$m->render();
						$module_content .= ob_get_contents();
						ob_end_clean();
						$module_content .= "</div>";

						$this->cache->save($key, $module_content, $m->getCacheTime());
					}

					$this->content = $this->str_replace_one($code, $module_content, $this->content);

					// Since we've inserted a new module, we need to loop again in case any new module placeholders have been inserted
					$more_modules = true;
				}
			}
		}
	}

	protected function finalize()
	{
		$site                = Mainframe::site();
		$template            = Mainframe::template();
		$page                = Mainframe::page();
		$custom_css_position = strpos($this->content, "<!--CSS-->");
		$custom_js_position  = strpos($this->content, "<!--JS-->");
		$head_script         = "";
		$body_script         = "";
		$end_script          = "";
		$css_src             = "";

		// Inject reCAPTCHA v3.
		if($site->recaptcha && $site->recaptcha_public_key)
		{
			array_unshift($this->scripts, "\n<script src=\"https://www.google.com/recaptcha/api.js?render=" . $site->recaptcha_public_key . "\"></script>");
			array_unshift($this->scripts, "\n<script>const RECAPTCHA_PUBLIC_KEY='" . $site->recaptcha_public_key . "';</script><style>.grecaptcha-badge{display:none}</style>");
		}

		// Inject Google Analytics tracking code.
		if($site->ga_code != "")
		{
			$ga_script = "\n<!-- Google tag (gtag.js) -->
				<script async src=\"https://www.googletagmanager.com/gtag/js?id=" . $site->ga_code . "\"></script>
				<script>
				window.dataLayer = window.dataLayer || [];
				function gtag(){dataLayer.push(arguments);}
				gtag('js', new Date());

				gtag('config', '" . $site->ga_code . "');
				</script>\n";
			array_unshift($this->scripts, $ga_script);
		}

		// Inject retina images scripts.
		if($site->enable_retina)
		{
			//two scripts are required for retina display support.  one directly after <head> and one directly after <body>
			$head_script 	.= "\n<script>(function(w){var dpr=((w.devicePixelRatio===undefined)?1:w.devicePixelRatio);var r=new XMLHttpRequest();r.open('GET','/retinaimages.php?devicePixelRatio='+dpr);r.send();})(window)</script>\n";
			$body_script 	.= "\n" . '<noscript><style id="devicePixelRatio" media="only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)">#devicePixelRatio{background-image:url("/retinaimages.php?devicePixelRatio=2")}</style></noscript>' . "\n";
		}

		// Inject Facebook SDK.
		if($site->facebook_sdk)
		{
			$body_script .= "\n" . '<div id="fb-root"></div><script src="https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.5&appId="></script>
	<script>(function(d, s, id) {
	  var js, fjs = d.getElementsByTagName(s)[0];
	  if (d.getElementById(id)) return;
	  js = d.createElement(s); js.id = id;
	  js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.5&appId=";
	  fjs.parentNode.insertBefore(js, fjs);
	}(document, "script", "facebook-jssdk"));</script>' . "\n";
		}

		// Include Bootstrap
		array_unshift($this->scripts, '<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">');
		array_unshift($this->scripts, '<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>');
		array_unshift($this->scripts, '<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>');

		// Inject jQuery and jQuery UI (in reverse order).
		if($this->enable_jquery_ui)
		{
			array_unshift($this->scripts, '<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>');
			array_unshift($this->scripts, '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.min.css">');
		}
		if($this->enable_jquery || $this->enable_jquery_ui)
		{
			array_unshift($this->scripts, '<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>');
		}

		if($site->shop)
		{
			// $end_script .= "\n" . '<script src="/js/min/shop.min.js"></script>';
			$end_script .= "\n" . '<script src="/js/shop.js"></script>';
			$site->enable_vue = true;
		}

		if($site->enable_vue)
		{
			if(ENVIRONMENT == "development")
			{
				array_unshift($this->scripts, '<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>');
			}
			else
			{
				array_unshift($this->scripts, '<script src="https://cdn.jsdelivr.net/npm/vue"></script>');
			}
		}

		// Include Font Awesome
		if($site->fontawesome == 1)
		{
			array_unshift($this->scripts, '<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous">');
		}
		else if($site->fontawesome == 2)
		{
			array_unshift($this->scripts, '<link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.15.4/css/all.css" integrity="sha384-rqn26AG5Pj86AF4SO72RK5fyefcQ/x32DNQfChxWvbXIyXFePlEktwD18fEz+kQU" crossorigin="anonymous">');
		}

		// Process dynamic LESS theming
		$temp_file 	= ABSOLUTE_PATH . "/temp/page" . $page->page_id . ".css";
		$less_data 	= "";
		$recompile 	= !file_exists($temp_file);
		$temp_time 	= ($recompile ? 0 : filemtime($temp_file));	// Recompile here would mean the file doesn't exist.

		// Include font package, if selected.
		// This goes first in case there's an @import at the top.
		if($site->font)
		{
			$less_data .= file_get_contents(ABSOLUTE_PATH . "/css/themes/fonts/" . $site->font . ".less");
			$recompile = $recompile || (filemtime(ABSOLUTE_PATH . "/css/themes/fonts/" . $site->font . ".less") > $temp_time);
		}

		// Include colour theme, if selected.
		if($site->theme)
		{
			$less_data .= file_get_contents(ABSOLUTE_PATH . "/css/themes/colours/" . $site->theme . ".less");
			$recompile = $recompile || (filemtime(ABSOLUTE_PATH . "/css/themes/colours/" . $site->theme . ".less") > $temp_time);

			// Dynamically inject site-specific colours, if specified.
			// This is done by replacing the colour variable definitions in the colour theme file.
			if($site->colour1)
			{
				$less_data = preg_replace('/@color1:(.*?);/', '@color1: ' . $site->colour1 . ';', $less_data);
			}
			if($site->colour2)
			{
				$less_data = preg_replace('/@color2:(.*?);/', '@color2: ' . $site->colour2 . ';', $less_data);
			}
			if($site->colour3)
			{
				$less_data = preg_replace('/@color3:(.*?);/', '@color3: ' . $site->colour3 . ';', $less_data);
			}
			if($site->colour4)
			{
				$less_data = preg_replace('/@color4:(.*?);/', '@color4: ' . $site->colour4 . ';', $less_data);
			}
			if($site->colour5)
			{
				$less_data = preg_replace('/@color5:(.*?);/', '@color5: ' . $site->colour5 . ';', $less_data);
			}
		}

		// Process any module LESS files.
		foreach($this->less as $l)
		{
			$less_data .= file_get_contents($l) . "\n";
			$recompile = $recompile || (filemtime($l) > $temp_time);
		}

		// common.less will fail if there is no colour theme selected.
		// Themes are required now, but sites being upgraded will not have one.
		if($site->theme && $site->common_less)
		{
			// Include common file, which includes all the glory.
			$less_data .= file_get_contents(ABSOLUTE_PATH . "/css/themes/common.less");
			$recompile = $recompile || (filemtime(ABSOLUTE_PATH . "/css/themes/common.less") > $temp_time);
		}

		if(file_exists(ABSOLUTE_PATH . "/templates/$template->directory/css/$template->directory" . ".less"))
		{
			$less_data .= file_get_contents(ABSOLUTE_PATH . "/templates/$template->directory/css/$template->directory" . ".less");
			$recompile = $recompile || (filemtime(ABSOLUTE_PATH . "/templates/$template->directory/css/$template->directory" . ".less") > $temp_time);
		}

		// Include any site specific LESS overrides.
		if($site->less_css)
		{
			$less_data .= $site->less_css;
		}

		if($recompile)
		{
			// Try to compile the LESS CSS.
			// If it fails, we'll assume there was a syntax error and continue without it.
			try
			{
				$parser = new Less_Parser();
				$parser->parse($less_data);
				$css = $parser->getCss();

				file_put_contents($temp_file, $css);
			}
			catch(Exception $e)
			{
				if(ENVIRONMENT == "development")
				{
					devecho($e->getMessage());
				}
				@unlink($temp_file);
			}
		}
		// Specifically stick this one on the end so that it can override any other CSS files.
		$this->scripts[] = '<link rel="stylesheet" href="/temp/page' . $page->page_id . '.css" />';

		$this->css_scripts = array();
		// $this->js_scripts = array();
		$script_order 	= array("fonts.google", "<style", ".css", "<script", ".js");
		$script_type 	= "css";

		//scripts from above, now that we aren't using a DOM objects, lets insert them
		//script_order allows us to loop X times, first including css and then js files (better page speed)
		foreach($script_order as $script_ext)
		{
			foreach($this->scripts as $script)
			{
				$script_type = (preg_match('/<script/', $script) ? "js" : "css");

				// For local scripts, check if there are any overrides in js/overrides/{path to JS file}
				if(preg_match('/<script/', $script))
				{
					preg_match_all('/src="(.*?)"/', $script, $matches);

					foreach($matches[1] as $match)
					{
						$override = $match;
						$override = str_replace(LIVE_SITE, "", $override);

						// Skip external scripts
						if(preg_match('/^http/', $override))
						{
							continue;
						}

						$override = '/js/overrides' . str_replace('js/', '', $override);

						if(file_exists(ABSOLUTE_PATH . $override))
						{
							if(defined("LOGGING")) { error_log(PRODUCT_NAME . ": JavaScript file `" . str_replace(ABSOLUTE_PATH, "", $override) . "` has an override."); }
							$script = '<script src="' . $override . '"></script>';
						}
					}
				}

				$exists = (strpos($this->content, $script) !== false) || (strpos($head_script, $script) !== false);

				if(!$exists && strpos($script, $script_ext) !== false)
				{
					if($script_type == "css")
					{
						$css_src .= $script . "\n";
						$this->css_scripts[] = $script;
					}
					else
					{
						$head_script .= $script . "\n";
						// $this->js_scripts[] = $script;
					}
				}
			}
		}

		if($site->combine_css)
		{
			preg_match_all('/(<link [^>]*?\.css[^>]*?\/?>)/i', $this->content, $matches);
			$this->css_scripts = array_merge($matches[1], $this->css_scripts);
			$combined_css = "";

			if($recompile || !file_exists($temp_file . ".concat.css"))
			{
				foreach($this->css_scripts as $css)
				{
					preg_match('/href=("|\')(.*?)("|\')/i', $css, $href);

					if(isset($href[2]))
					{
						$combined_css .= $this->fetch_css($href[2]) . "\n";

						// Remove CSS from original page.
						$this->content = $this->str_replace_one($css, "", $this->content);
					}
				}

				// Find any @imports and fetch those as well.
				preg_match_all('/@import url\((.*?)?\);/i', $combined_css, $imports);

				if(isset($imports[0]))
				{
					// Remove all @imports from the combined CSS.
					foreach($imports[0] as $import)
					{
						$combined_css = $this->str_replace_one($import, "", $combined_css);
					}
					// Fetch all @imports.
					foreach($imports[1] as $import)
					{
						// If surrounded by quotes, strip the first and last character.
						if($import[0] == "'" || $import[0] == '"')
						{
							$import = substr($import, 1, strlen($import)-2);
						}

						$combined_css .= $this->fetch_css($import) . "\n";
					}
				}

				file_put_contents($temp_file . ".concat.css", $combined_css);
			}
			else
			{
				// Remove all CSS from the original page.
				foreach($this->css_scripts as $css)
				{
					preg_match('/href=("|\')(.*?)("|\')/i', $css, $href);

					if(isset($href[2]))
					{
						$this->content = $this->str_replace_one($css, "", $this->content);
					}
				}
			}

			$css_src = '<link rel="stylesheet" href="/temp/page' . $page->page_id . '.css.concat.css" />';
		}

		/*if($site->combine_js)
		{
			$temp_file 	= ABSOLUTE_PATH . "/temp/page" . $page->page_id . ".js";

			if(true || !file_exists($temp_file))
			{
				preg_match_all('/(<script .*\.js.*><\/script>?)/i', $this->content, $matches);
				$this->js_scripts = array_merge($matches[1], $this->js_scripts);
				$combined_js = "";

				usort($this->js_scripts, array("MY_Controller", "js_sort"));

				foreach($this->js_scripts as $js)
				{
					preg_match('/src=("|\')(.*?)("|\')/i', $js, $href);

					if(isset($href[2]))
					{
						// devecho(htmlentities($js));
						// devecho($href[2]);
						if(preg_match('/^(https?|\/\/)/i', $href[2]))
						{
							// This is a URL starting with // or http or https
							// We need to cURL it.
							$ch = curl_init($href[2]);
							curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
							curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
							curl_setopt($ch, CURLOPT_TIMEOUT, 3);
							$combined_js .= curl_exec($ch) . "\n";
							curl_close($ch);
						}
						else if(preg_match('/\.css/i', $href[2]))
						{
							// This should be a local file.
							$combined_js .= file_get_contents(str_replace("//", "/", ABSOLUTE_PATH . "/" . $href[2])) . "\n";
						}
						else
						{
							// This could be a <script> block. For safety we'll just include its content.
							$combined_js .= $href[2];
						}

						$this->content = $this->str_replace_one($js, "", $this->content);
					}
				}

				file_put_contents($temp_file, $combined_js);
			}
			$head_script .= "\n" . '<script src="/temp/page' . $page->page_id . '.js"></script>';
		}*/

		if($site->loading_overlay)
		{
			$css_src .= '
<style type="text/css">
#spinner {
	display: block;
	opacity: 1;
	transition: all 1s ease-out;
	background: #fff;
	position: fixed;
	z-index: 500000;
	top: 0px;
	left: 0px;
	width: 100%;
	height: 100%;
	padding-top: 20%;
	box-sizing: border-box;
	text-align: center;
}
.spinner {
	text-align: center;
}
.spinner > div {
	width: 30px;
	height: 30px;
	background-color: ' . ($site->colour1 ? $site->colour1 : "#08c") . ';
	border-radius: 100%;
	display: inline-block;
	-webkit-animation: sk-bouncedelay 1.6s infinite ease-in-out both;
	animation: sk-bouncedelay 1.6s infinite ease-in-out both;
}
.spinner .bounce1 {
	-webkit-animation-delay: -0.48s;
	animation-delay: -0.48s;
}
.spinner .bounce2 {
	-webkit-animation-delay: -0.24s;
	animation-delay: -0.24s;
}
@-webkit-keyframes sk-bouncedelay {
	0%, 80%, 100% { -webkit-transform: scale(0) }
	40% { -webkit-transform: scale(1.0) }
}
@keyframes sk-bouncedelay {
	0%, 80%, 100% {
	-webkit-transform: scale(0);
	transform: scale(0);
	} 40% {
	-webkit-transform: scale(1.0);
	transform: scale(1.0);
	}
}
</style>';

			$body_script .= "\n" . '<div class="spinner" id="spinner"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>';

			$end_script .= '
<script>
var nvLoaded = function()
{
	jQuery("#spinner").css({"opacity":"0"});
	setTimeout(function()
	{
		jQuery("#spinner").css({"display":"none"});
	}, 1000);
}

var nvTimeout = setTimeout(function()
{
	nvLoaded();
}, 5000);

jQuery(window).on("load", function()
{
	nvLoaded();
	clearTimeout(nvTimeout);
});
</script>
<noscript><style> #spinner {display: none;} </style></noscript>';
		}

		$this->content 	= $this->str_replace_one("<body>", "<body>" . $body_script, $this->content);
		$this->content 	= $this->str_replace_one("</body>", $end_script . "</body>", $this->content);

		if($custom_css_position !== false)
		{
			$this->content = $this->str_replace_one("<!--CSS-->", $css_src . "<!--CSS-->", $this->content);
		}
		else
		{
			$this->content = $this->str_replace_one("</head>", $css_src . "</head>", $this->content);
		}

		if($custom_js_position !== false)
		{
			$this->content = $this->str_replace_one("<!--JS-->", $head_script . "<!--JS-->", $this->content);
		}
		else
		{
			$this->content = $this->str_replace_one("</head>", $head_script . "</head>", $this->content);
		}

		// WebP optimzations.
		if($site->webp)
		{
			$this->content = Mainframe::optimize_html_for_webp($this->content);
		}

		// Accessibility improvements.
		$this->content = Mainframe::filter_html_for_accessibility($this->content);

		// Confirm external links.
		if($site->confirm_links)
		{
			$this->content = Mainframe::filter_html_links($this->content);
		}

		// Remove whitespace between tags (basic HTML minification).
		$this->content = preg_replace('/>[\s\t\n\r]+</im', '> <', $this->content);
		// Remove 3+ consecutive non-breaking spaces.
		$this->content = preg_replace('/&nbsp;&nbsp;(&nbsp;)+/i', '&nbsp;', $this->content);

		// Apply Content-Security-Policy, if enabled.
		if($site->csp)
		{
			$csp = Mainframe::generateCSP($this->content);
			header("Content-Security-Policy: " . $csp, true);
		}

		$this->load->view("index", array("content" => $this->content));
	}

	private function fetch_css($path)
	{
		if(preg_match('/^(https?|\/\/)/i', $path))
		{
			// This is a URL starting with // or http or https
			// We need to cURL it.
			$ch = curl_init($path);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
			curl_setopt($ch, CURLOPT_TIMEOUT, 3);
			$newcss = curl_exec($ch) . "\n";
			curl_close($ch);
			return $this->localize_css($newcss, $path);
		}
		else if(preg_match('/\.css/i', $path))
		{
			// This should be a local file.
			$newcss = file_get_contents(str_replace("//", "/", ABSOLUTE_PATH . "/" . $path)) . "\n";
			return $this->localize_css($newcss, $path);
		}
		else
		{
			// This could be a <style> block. For safety we'll just include its content.
			return $path;
		}
	}
	// public static function js_sort($a, $b)
	// {
	// 	$a1 = preg_match('/jquery/i', $a) && !preg_match('/jquery-ui/i', $a);
	// 	$a2 = !preg_match('/jquery-ui/i', $a);
	// 	$b1 = preg_match('/jquery/i', $b) && !preg_match('/jquery-ui/i', $b);
	// 	$b2 = !preg_match('/jquery-ui/i', $b);

	// 	// Put jQuery above all others.
	//     if ($a1 == $b1)
	//     {
	//     	// Put jQuery UI above everything except jQuery.
	//         if ($a2 == $b2)
	// 	    {
	// 	    	// Leave all other ordering as we found it in the page.
	// 	        return 0;
	// 	    }
	// 	    return ($a2 < $b2) ? -1 : 1;
	//     }
	//     return ($a1 > $b1) ? -1 : 1;
	// }

	private function localize_css($css, $path)
	{
		// Leave any local temp CSS alone (ie: our main compiled LESS CSS)
		if(dirname($path) == "/temp")
		{
			return $css;
		}

		// Replace local references with absolute paths. ie: Font Awesome "local" files.
		$css = preg_replace('/url\(([\'"])?(?!(https?|[\'"]?data:))/i', "url($1" . dirname($path) . "/", $css);
		// $css = str_replace("../", dirname($path) . "/../", $css);
		// $css = str_replace(array('url(images/', 'url("images/', "url('images/"),
		//                       array('url(' . dirname($path) . '/images/', 'url("' . dirname($path) . '/images/', "url('" . dirname($path) . '/images/'),
		//                       $css);
		return $css;
	}

	protected function end()
	{
		Mainframe::page()->content = ob_get_contents();
		ob_end_clean();

		$this->template();
		$this->modules();
		$this->finalize();
	}

	/**
	 * Login is required to access the page where this is called.
	 *
	 * @access public
	 * @return void
	 */
	protected function login_required()
	{
		$user = get_user();
		$page = new Page();
		$page->load(Mainframe::site()->login_page_id);

		if(!$user || $user->user_id <= 0 || !$user->has(ACTION_LOGIN))
		{
			$this->load->helper('url');

			redirect("/" . $page->url() . "?redirect=" . rawurlencode(current_url()));
		}
	}

	/**
	 * Admin login is required to access the page where this is called.
	 *
	 * @access public
	 * @return void
	 */
	protected function admin_login_required()
	{
		$this->load->helper('url');
		$user = get_user();

		if(defined("ADMIN_IP") && ADMIN_IP != "")
		{
			$ips = explode("|", ADMIN_IP);

			if(!in_array($this->input->server("REMOTE_ADDR"), $ips))
			{
				redirect("/");
			}
		}

		if(!$user || $user->user_id <= 0 || !$user->has(ACTION_ADMIN_LOGIN))
		{
			redirect("/login?redirect=" . rawurlencode(current_url()));
		}
	}

	/**
	 * Convert the passed content to JSON and send it to the browser.
	 *
	 * @access protected
	 * @param mixed $content
	 * @return void
	 */
	protected function spit_json($content, $JSON_NUMERIC_CHECK=false)
	{
		header('Content-type: application/json');

		if($JSON_NUMERIC_CHECK)
		{
			echo json_encode( $content, JSON_NUMERIC_CHECK );
		}
		else
		{
			echo json_encode( $content );
		}
	}

	protected function bare_init()
	{
		require_once(APPLICATION_PATH . "/third_party/feral.php");
	}

	public function http404()
	{
		$this->fourOhFour();
		show_404();
	}

	protected function fourOhFour()
	{
		$this->load->model("Four_oh_four");
		$fof = new Four_oh_four();
		$url = uri_string();

		// Its possible for things like the retina image processor to redirect here.
		// In that case we will use the referring URL as the 404 URL.
		if($url == "http404")
		{
			$url = urldecode($this->input->get("url"));
		}

		// Remove leading slash if present.
		if(substr($url, 0, 1) == "/")
		{
			$url = substr($url, 1);
		}

		if(!$fof->loadByURL($url))
		{
			$fof->url = $url;
		}
		$fof->count++;
		$fof->save();

		if(defined("LOGGING")) { error_log(PRODUCT_NAME . ": 404: `" . $url . "` not found."); }
	}

	protected function clean_css_cache()
	{
		// Clean up cached copies of dynamic CSS.
		$files = scandir(ABSOLUTE_PATH . "/temp");
		$files = array_splice($files, 2);			// Remove . and ..

		foreach($files as $file)
		{
			if(preg_match('/page(.*)\.css/', $file))
			{
				unlink(ABSOLUTE_PATH . "/temp/" . $file);
			}
		}
	}

	protected function clean_application_cache()
	{
		$this->db->cache_delete_all();
		$this->cache->clean();
	}

	protected function denied($data)
	{
		$this->load->view("common/header", $data);
		$this->load->view("common/message", array("messages" => array(), "errors" => array("Access Denied", "You don't have permission to access this feature.")));
		$this->load->view("common/footer", $data);
	}

	private function str_replace_one($needle, $replace, $haystack)
	{
		$pos = strpos($haystack, $needle);

		if($needle && $pos !== false)
		{
		    return substr_replace($haystack, $replace, $pos, strlen($needle));
		}

		return $haystack;
	}

	protected function cleanModuleCache($module_instance_id)
	{
		$caches = $this->cache->cache_info();

		foreach($caches as $cache)
		{
			// SiteID:PageID:ModuleInstanceID:UserID:URL
			if(preg_match('/^module:.*?:.*?:' . $module_instance_id . ':.*?/', $cache["name"]))
			{
				$this->cache->delete($cache["name"]);
			}
		}
	}

	protected function cleanPageCache($page_id)
	{
		$caches = $this->cache->cache_info();

		foreach($caches as $cache)
		{
			// SiteID:PageID:ModuleInstanceID:UserID:URL
			if(preg_match('/^module:.*?:' . $page_id . ':.*?/', $cache["name"]))
			{
				$this->cache->delete($cache["name"]);
			}
		}
	}
}
