<?php


namespace gp{
  use function \PHP81_BC\strftime;
	defined('is_running') or die('Not an entry point...');

	class tool{

		/**
		 * Return the type of response was requested by the client
		 * @since 3.5b2
		 * @return string
		 */
		public static function RequestType(){

			$types = ['body', 'flush', 'json', 'content', 'admin'];

			if( isset($_REQUEST['gpreq']) && in_array($_REQUEST['gpreq'], $types) ){
				return $_REQUEST['gpreq'];
			}

			return 'template';
		}


		/**
		 * Send a 304 Not Modified Response to the client if HTTP_IF_NONE_MATCH matched $etag and headers have not already been sent
		 * Othewise, send the etag
		 * @param string $etag The calculated etag for the current page
		 *
		 */
		public static function Send304($etag){
			global $config;

			if( !$config['etag_headers'] ){
				return;
			}

			if( headers_sent() ){
				return;
			}

			//always send the etag
			header('ETag: "' . $etag . '"');

			if( empty($_SERVER['HTTP_IF_NONE_MATCH']) ||
				trim($_SERVER['HTTP_IF_NONE_MATCH'], '"') != $etag
			){
				return;
			}

			//don't use ob_get_level() in while loop to prevent endless loops;
			$level = ob_get_level();
			while( $level > 0 ){
				@ob_end_clean();
				$level--;
			}

			// 304 should not have a response body or Content-Length header
			//header('Not Modified', true, 304);
			self::status_header(304, 'Not Modified');
			header('Connection: close');
			exit();
		}


		/**
		 * Set HTTP status header.
		 * Modified From Wordpress
		 *
		 * @since 2.3.3
		 * @uses apply_filters() Calls 'status_header' on status header string, HTTP
		 *		HTTP code, HTTP code description, and protocol string as separate
		 *		parameters.
		 *
		 * @param int $header HTTP status code
		 * @param string $text HTTP status
		 * @return unknown
		 */
		public static function status_header($header, $text){

			$protocol = '';
			if( isset($_SERVER['SERVER_PROTOCOL']) ){
				$protocol = $_SERVER['SERVER_PROTOCOL'];
			}
			if( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol ){
				$protocol = 'HTTP/1.0';
			}

			$status_header = "$protocol $header $text";
			return @header($status_header, true, $header);
		}


		public static function GenEtag(){
			global $dirPrefix, $dataDir;
			$etag = '';
			$args = func_get_args();
			$args[] = $dataDir . $dirPrefix;
			foreach($args as $arg){
				if( !ctype_digit((string)$arg)){
					$arg = crc32( $arg );
					$arg = sprintf("%u\n", $arg);
				}
				$etag .= base_convert($arg, 10, 36);
			}
			return $etag;
		}


		/**
		 * Generate an etag from the filemtime and filesize of each file
		 * @param array $files
		 *
		 */
		public static function FilesEtag($files){
			$modified				= 0;
			$content_length			= 0;
			foreach($files as $file){
				$content_length		+= @filesize($file);
				$modified			= max($modified, @filemtime($file));
			}

			return self::GenEtag($modified, $content_length);
		}


		public static function CheckTheme(){
			global $page;
			if( $page->theme_name === false ){
				$page->SetTheme();
			}
		}


		/**
		 * Return an array of information about the layout
		 * @param string $layout the layout key
		 * @param bool $check_existence Whether or not to check for the existence of the template.php file
		 * @return mixed false | array
		 */
		public static function LayoutInfo($layout, $check_existence=true){
			global $gpLayouts, $dataDir;

			if( !isset($gpLayouts[$layout]) ){
				return false;
			}

			$layout_info = $gpLayouts[$layout];
			$layout_info += ['is_addon' => false];
			$layout_info['theme_name'] = self::DirName($layout_info['theme']);
			$layout_info['theme_color'] = basename($layout_info['theme']);

			$relative = '/themes/';
			if( $layout_info['is_addon'] ){
				$relative = '/data/_themes/';
			}
			$layout_info['path'] = $relative . $layout_info['theme'];

			$layout_info['dir'] = $dataDir . $relative . $layout_info['theme_name'];
			if( $check_existence && !file_exists($layout_info['dir'] . '/template.php') ){
				return false;
			}

			return $layout_info;
		}


		/**
		 * Returns the layout id used by the current page
		 * @return string $layout_id
		 *
		 */
		public static function GetCurrentLayoutId(){
			global $page, $config;

			if( !is_object($page) || $page->pagetype === 'admin_display' ){
				return '';
			}

			if( isset($page->TitleInfo['gpLayout']) ){
				// page uses a custom layout
				return $page->TitleInfo['gpLayout'];
			}

			$inheritance = \gp\admin\Menu\Tools::Inheritance_Info();

			if( isset($inheritance[$page->gp_index]['parent_layout']) ){
				// page inherits the layout from main menu parent
				return $inheritance[$page->gp_index]['parent_layout'];
			}

			// page uses the default layout
			return $config['gpLayout'];
		}


		/**
		 * Returns detailed information of the layout used by the current page
		 * @return array $layout_info
		 *
		 */
		public static function GetCurrentLayoutInfo(){
			global $gpLayouts;
		
			$layout_id = self::GetCurrentLayoutId();
			if( empty($layout_id) ){
				return [];
			}
			$layout_info = $gpLayouts[$layout_id];
			$layout_info['layout_id'] = $layout_id;

			return $layout_info;
		}


		/*
		 *
		 *
		 * Entry Functions
		 *
		 *
		 */

		public static function EntryPoint($level=0, $expecting='index.php', $sessions=true){

			self::CheckRequest();

			clearstatcache();

			$ob_gzhandler = false;
			if( !self::IniGet('zlib.output_compression') && 'ob_gzhandler' != ini_get('output_handler') ){
				@ob_start('ob_gzhandler'); //ini_get() does not always work for this test
				$ob_gzhandler = true;
			}

			self::SetGlobalPaths($level, $expecting);
			spl_autoload_register(['\\gp\\tool', 'Autoload']);

			includeFile('tool/functions.php');
			if( $sessions ){
				ob_start(['\\gp\\tool\\Output', 'BufferOut']);
			}elseif( !$ob_gzhandler ){
				ob_start();
			}

			self::RequestLevel();
			self::GetConfig();
			self::SetLinkPrefix();
			self::SetCookieArgs();
			if( $sessions ){
				self::sessions();
			}
		}


		/**
		 * Setup SPL Autoloading
		 *
		 */
		public static function Autoload($class){
			global $config, $dataDir;

			$class		= trim($class, '\\');
			$parts		= explode('\\', $class);
			$part_0		= array_shift($parts);

			if( !$parts ){
				return;
			}

			//gp namespace
			if( $part_0 === 'gp' ){
				$path	= $dataDir . '/include/' . implode('/', $parts) . '.php';
				if( file_exists($path) ){
					include_once( $path );
				}else{
					trigger_error('Autoload for gp namespace failed. Class: ' . $class . ' path: ' . $path);
				}
				return;
			}

			//look for addon namespace
			if( $part_0 === 'Addon' ){

				$namespace = array_shift($parts);
				if( !$parts ){
					return;
				}

				foreach($config['addons'] as $addon_key => $addon){
					if( isset($addon['Namespace']) && $addon['Namespace'] == $namespace ){

						\gp\tool\Plugins::SetDataFolder($addon_key);
						$path = \gp\tool\Plugins::$current['code_folder_full'] . '/' . implode('/', $parts) . '.php';

						if( file_exists($path) ){
							include_once($path);
						}else{
							trigger_error('Autoload for addon namespace failed. Class: ' . $class . ' path: ' . $path);
						}

						\gp\tool\Plugins::ClearDataFolder();
					}
				}
				return;
			}

			//thirdparty
			$path = $dataDir . '/include/thirdparty/' . str_replace('\\', '/', $class) . '.php';
			if( file_exists($path) ){
				include_once($path);
			}
		}


		/**
		 * Reject Invalid Requests
		 *
		 */
		public static function CheckRequest(){

			if( count($_POST) == 0 ){
				return;
			}

			if( !isset($_SERVER['CONTENT_LENGTH']) ){
				header('HTTP/1.1 503 Service Temporarily Unavailable');
				header('Status: 503 Service Temporarily Unavailable');
				header('Retry-After: 300');//300 seconds
				die();
			}

			if( function_exists('getallheaders') ){

				$headers = getallheaders();
				if( !isset($headers['Content-Length']) ){
					header('HTTP/1.1 503 Service Temporarily Unavailable');
					header('Status: 503 Service Temporarily Unavailable');
					header('Retry-After: 300');//300 seconds
					die();
				}

			}
		}


		public static function SetGlobalPaths($DirectoriesAway, $expecting){
			global $dataDir, $dirPrefix, $rootDir;

			$rootDir = self::DirName(__FILE__, 2);

			// dataDir, make sure it contains $expecting. Some servers using cgi do not set this properly
			// required for the Multi-Site plugin
			$dataDir = self::GetEnv('SCRIPT_FILENAME', $expecting);
			if( $dataDir !== false ){
				$dataDir = self::ReduceGlobalPath($dataDir, $DirectoriesAway);
			}else{
				$dataDir = $rootDir;
			}
			if( $dataDir == '/' ){
				$dataDir = '';
			}

			//$dirPrefix
			$dirPrefix = self::GetEnv('SCRIPT_NAME', $expecting);
			if( $dirPrefix === false ){
				$dirPrefix = self::GetEnv('PHP_SELF', $expecting);
			}

			//remove everything after $expecting, $dirPrefix can at times include the PATH_INFO
			$pos = strpos($dirPrefix,$expecting);
			$dirPrefix = substr($dirPrefix, 0, $pos+strlen($expecting));

			$dirPrefix = self::ReduceGlobalPath($dirPrefix, $DirectoriesAway);
			if( $dirPrefix == '/' ){
				$dirPrefix = '';
			}
		}


		/**
		 * Convert backslashes to forward slashes
		 *
		 */
		public static function WinPath($path){
			return str_replace('\\', '/', $path);
		}


		/**
		 * Returns parent directory's path with forward slashes
		 * php's dirname() method may change slashes from / to \
		 *
		 */
		public static function DirName($path, $dirs=1){
			for($i = 0; $i < $dirs; $i++){
				$path = dirname($path);
			}
			return self::WinPath( $path );
		}


		/**
		 * Determine if this installation is supressing index.php in urls or not
		 *
		 */
		public static function SetLinkPrefix(){
			global $linkPrefix, $dirPrefix, $config;

			$linkPrefix = $dirPrefix;

			// gp_rewrite = 'On' and gp_rewrite = 'gpuniq' are deprecated since 4.1
			// gp_rewrite = bool will still be used internally
			if( isset($_SERVER['gp_rewrite']) ){

				if( $_SERVER['gp_rewrite'] === true || $_SERVER['gp_rewrite'] == 'On' ){
					$_SERVER['gp_rewrite'] = true;
				}elseif( $_SERVER['gp_rewrite'] == @substr($config['gpuniq'], 0, 7) ){
					$_SERVER['gp_rewrite'] = true;
				}

			}elseif( isset($_REQUEST['gp_rewrite']) ){
				$_SERVER['gp_rewrite'] = true;

			// gp_indexphp is deprecated since 4.1
			}elseif( defined('gp_indexphp') ){

				if( \gp_indexphp === false ){
					$_SERVER['gp_rewrite'] = true;
				}

			}

			unset($_GET['gp_rewrite']);
			unset($_REQUEST['gp_rewrite']);

			if( !isset($_SERVER['gp_rewrite']) ){
				$_SERVER['gp_rewrite'] = false;
			}

			if( !$_SERVER['gp_rewrite'] ){
				$linkPrefix .= '/index.php';
			}
		}


		/**
		 * Get the environment variable and make sure it contains an expected value
		 *
		 * @param string $var The key of the requested environment variable
		 * @param string $expected Optional string that is expected as part of the environment variable value
		 *
		 * @return mixed Returns false if $expected is not found, otherwise it returns the environment value.
		 *
		 */
		public static function GetEnv($var, $expecting=false){
			$value = false;
			if( isset($_SERVER[$var]) ){
				$value = $_SERVER[$var];
			}else{
				$value = getenv($var);
			}
			if( $expecting && strpos($value, $expecting) === false ){
				return false;
			}
			return $value;
		}


		/**
		 * Get the ini value and return a boolean casted value when appropriate: On, Off, 1, 0, True, False, Yes, No
		 *
		 */
		public static function IniGet($key){
			$value = ini_get($key);
			if( empty($value) ){
				return false;
			}

			$lower_value = trim(strtolower($value));
			switch($lower_value){
				case 'true':
				case 'yes':
				case 'on':
				case '1':
					return true;

				case 'false':
				case 'no':
				case 'off':
				case '0':
					return false;
			}

			return $value;
		}


		public static function ReduceGlobalPath($path, $DirectoriesAway){
			return self::DirName($path, $DirectoriesAway + 1);
		}


		//use dirPrefix to find requested level
		public static function RequestLevel(){
			global $dirPrefixRel, $dirPrefix;

			$path = $_SERVER['REQUEST_URI'];

			//strip the query string.. in case it contains "/"
			$pos = mb_strpos($path, '?');
			if( $pos > 0 ){
				$path =  mb_substr($path, 0, $pos);
			}

			//dirPrefix will be percent-decoded
			$path = rawurldecode($path); //%20 ...

			if( !empty($dirPrefix) ){
				$pos = mb_strpos($path, $dirPrefix);
				if( $pos !== false ){
					$path = mb_substr($path, $pos + mb_strlen($dirPrefix));
				}
			}

			$path = ltrim($path, '/');
			$count = substr_count($path, '/');
			if( $count == 0 ){
				$dirPrefixRel = '.';
			}else{
				$dirPrefixRel = str_repeat('../', $count);
				$dirPrefixRel = rtrim($dirPrefixRel, '/'); //GetDir() arguments always start with /
			}
		}


		/**
		 * Escape ampersands in hyperlink attributes and other html tag attributes
		 *
		 * @param string $str The string value of an html attribute
		 * @return string The escaped string
		 */
		public static function Ampersands($str){
			return preg_replace('/&(?![#a-zA-Z0-9]{2,9};)/S', '&amp;', $str);
		}


		/**
		 * Similar to htmlspecialchars, but designed for labels
		 * Does not convert existing ampersands "&"
		 *
		 */
		public static function LabelSpecialChars($string){
			return str_replace(
				['<',		'>',	'"',		"'"],
				['&lt;',	'&gt;',	'&quot;',	'&#39;'],
				$string
			);
		}


		/**
		 * Return an html hyperlink
		 *
		 * @param string $href The href value relative to the installation root (without index.php)
		 * @param string $label Text or html to be displayed within the hyperlink
		 * @param string $query Optional query to be used with the href
		 * @param string|array $attr Optional string of attributes like title=".." and class=".."
		 * @param mixed $nonce_action If false, no nonce will be added to the query. Given a string, it will be used as the first argument in \gp\tool\Nonce::Create()
		 *
		 * @return string The formatted html hyperlink
		 */
		public static function Link($href='', $label='', $query='', $attr='', $nonce_action=false){
			return '<a href="' . self::GetUrl($href, $query, true, $nonce_action) . '" '
				. self::LinkAttr($attr,$label) . '>'
				. self::Ampersands($label)
				. '</a>';
		}


		/**
		 * @param string|array $attr
		 * @param string $label
		 */
		public static function LinkAttr($attr='', $label=''){

			$string		= '';
			$has_title	= false;

			if( is_array($attr) ){

				// update old <a name="cmd"> links to <a data-cmd="cmd">
				// @deprecated
				$attr = array_change_key_case($attr);
				$has_title = isset($attr['title']);
				if( isset($attr['name']) && !isset($attr['data-cmd']) ){
					trigger_error('deprecated use of name attribute (use data-cmd attribute instead)');
					$attr['data-cmd'] = $attr['name'];
					unset($attr['name']);
				}

				$nonce_cmds = ['creq', 'cnreq', 'postlink', 'post'];
				if( isset($attr['data-cmd']) && in_array($attr['data-cmd'], $nonce_cmds) ){
					$attr['data-nonce'] = \gp\tool\Nonce::Create('post', true);
				}
				$string = \gp\tool\HTML::Attributes($attr);

			}else{

				$string = $attr;
				if( strpos($attr, 'title="') !== false){
					$has_title = true;
				}

				// backwards compatibility hack to be removed in future releases
				// @since 3.6
				if( strpos($string, 'name="postlink"') !== false ){
					trigger_error('deprecated use of name attribute (use data-cmd attribute instead)');
					$string .= ' data-nonce="' . \gp\tool\Nonce::Create('post', true) . '"';

				// @since 4.1
				}elseif( strpos($string, 'name="cnreq"') !== false || strpos($string, 'name="creq"') !== false ){
					trigger_error('deprecated use of name attribute (use data-cmd attribute instead)');
					$string .= ' data-nonce="' . \gp\tool\Nonce::Create('post', true) . '"';
				}

			}

			if( !$has_title && !empty($label) ){
				$string .= ' title="' . self::Ampersands(strip_tags($label)) . '" ';
			}

			return trim($string);
		}


		/**
		 * Return an html hyperlink for a page
		 *
		 * @param string $title The title of the page
		 * @return string The formatted html hyperlink
		 */
		public static function Link_Page($title=''){
			global $config, $gp_index;

			if( empty($title) && !empty($config['homepath']) ){
				$title = $config['homepath'];
			}

			$label = self::GetLabel($title);

			return self::Link($title, $label);
		}


		public static function GetUrl($href='', $query='', $ampersands=true, $nonce_action=false){
			global $linkPrefix, $config;

			$filtered = \gp\tool\Plugins::Filter('GetUrl', [[$href, $query]]);
			if( is_array($filtered) ){
				list($href, $query) = $filtered;
			}

			$href = self::SpecialHref($href);

			//home page link
			if( isset($config['homepath']) && $href == $config['homepath'] ){
				$href = $linkPrefix;
				if( !$_SERVER['gp_rewrite'] ){
					$href = self::DirName($href);
				}
				$href = rtrim($href, '/') . '/';
			}else{
				$href = $linkPrefix . '/' . ltrim($href, '/');
			}

			$query = self::QueryEncode($query,$ampersands);

			if( $nonce_action ){
				$nonce = \gp\tool\Nonce::Create($nonce_action);
				if( !empty($query) ){
					$query .= '&'; //in the cases where $ampersands is false, nonces are not used
				}
				$query .= '_gpnonce=' . $nonce;
			}
			if( !empty($query) ){
				$query = '?' . ltrim($query, '?');
			}

			return self::HrefEncode($href, $ampersands) . $query;
		}


		/**
		 * translate special pages from key to title
		 *
		 */
		public static function SpecialHref($href){
			global $gp_index;

			$href2 = ''; $href .= '';
			$pos = mb_strpos($href, '/');
			if( $pos !== false ){
				$href2 = mb_substr($href, $pos);
				$href = mb_substr($href, 0, $pos);
			}

			$lower = mb_strtolower($href);
			if( !isset($gp_index[$href]) &&
				strpos($lower, 'special_') === 0 &&
				$index_title = self::IndexToTitle($lower)
			){
				$href = $index_title;
			}

			return $href . $href2;
		}


		/**
		 * RawUrlEncode but keeps the following characters: &, /, \
		 * Slash is needed for hierarchical links
		 * In case you'd like to learn about percent encoding: http://www.blooberry.com/indexdot/html/topics/urlencoding.htm
		 *
		 */
		public static function HrefEncode($href, $ampersands=true){
			$ampersand = '&';
			if( $ampersands ){
				$ampersand = '&amp;';
			}
			$href = rawurlencode($href);
			return str_replace(
				['%26amp%3B',	'%26',		'%2F',	'%5C'],
				[$ampersand,	$ampersand,	'/',	'\\'],
				$href
			);
		}


		/**
		 * RawUrlEncode parts of the query string ( characters except & and = )
		 *
		 */
		public static function QueryEncode($query, $ampersands=true){

			if( empty($query) ){
				return '';
			}

			$query = str_replace('+', '%20', $query); //in case urlencode() was used instead of rawurlencode()
			if( strpos($query, '&amp;') !== false ){
				$parts = explode('&amp;', $query);
			}else{
				$parts = explode('&', $query);
			}

			$ampersand = $query = '';
			foreach($parts as $part){
				if( strpos($part, '=') ){
					list($key,$value) = explode('=', $part, 2);
					$query .= $ampersand . rawurlencode(rawurldecode($key)) . '=' . rawurlencode(rawurldecode($value));
				}else{
					$query .= $ampersand.rawurlencode(rawurldecode($part));
				}
				if( $ampersands ){
					$ampersand = '&amp;';
				}else{
					$ampersand = '&';
				}
			}
			return $query;
		}


		public static function AbsoluteLink($href, $label, $query='', $attr=''){

			if( strpos($attr,'title="') === false){
				$attr .= ' title="' . htmlspecialchars(strip_tags($label)) . '" ';
			}

			return '<a href="' . self::AbsoluteUrl($href, $query) . '" ' . $attr . '>' . self::Ampersands($label) . '</a>';
		}


		public static function AbsoluteUrl($href='', $query='', $with_schema=true, $ampersands=true, $with_port=false){

			$server = self::ServerName(false, $with_port);
			if( $server === false ){
				return self::GetUrl($href, $query, $ampersands);
			}

			$schema = '';
			if( $with_schema ){
				$schema = ( isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on' ) ? 'https://' : 'http://';
			}

			return $schema.$server.self::GetUrl($href,$query,$ampersands);
		}



		/**
		 * Return ther server name
		 *
		 */
		public static function ServerName($strip_www=false, $with_port=false){

			$add_port = '';
			if( isset($_SERVER['SERVER_NAME']) ){
				$server = self::UrlChars($_SERVER['SERVER_NAME']);
				if( $with_port && isset($_SERVER['SERVER_PORT']) ){
					$port = $_SERVER['SERVER_PORT'];
					if( $port != 80 && $port != 443 ){
						$add_port = ':' . $port;
					}
				}
			}else{
				return false;
			}

			if( $strip_www && strpos($server, 'www.') === 0 ){
				$server = substr($server, 4);
			}

			return $server . $add_port;
		}


		public static function UrlChars($string){
			$string = str_replace(' ', '%20', $string);
			return preg_replace('|[^a-z0-9-~+_.?#=!&;,/:%@$\|*\'()\[\]\\x80-\\xff]|i', '', $string);
		}


		/**
		 * Get the full path of a physical file on the server
		 * The query string component of a path should not be included but will be protected from being encoded
		 *
		 */
		public static function GetDir($dir='', $ampersands=false){
			global $dirPrefix;

			$query = '';
			$pos = mb_strpos($dir, '?');
			if( $pos !== false ){
				$query = mb_substr($dir, $pos);
				$dir = mb_substr($dir, 0, $pos);
			}
			$dir = $dirPrefix . '/' . ltrim($dir, '/');
			return self::HrefEncode($dir, $ampersands) . $query;
		}


		/**
		 * Get the label for a page from it's index
		 * @param string $index
		 * @param bool $amp Whether or not to escape ampersand characters
		 */
		public static function GetLabelIndex($index=null, $amp=false){
			global $gp_titles,$langmessage;

			$info = [];
			if( isset($gp_titles[$index]) ){
				$info = $gp_titles[$index];
			}

			if( isset($info['label']) ){
				$return = $info['label'];

			}elseif( isset($info['lang_index']) ){
				$return = $langmessage[$info['lang_index']];

			}else{
				$return = self::IndexToTitle($index);
				$return = \gp\tool\Files::CleanLabel($return);
			}
			if( $amp ){
				return str_replace('&', '&amp;', $return);
			}
			return $return;
		}


		/**
		 * Get the label for a page from it's title
		 * @param string $title
		 *
		 */
		public static function GetLabel($title=null){
			global $gp_titles, $gp_index, $langmessage;

			$return = false;
			if( isset($gp_index[$title]) ){
				$id = $gp_index[$title];
				$info =& $gp_titles[$id];

				if( isset($info['label']) ){
					$return = $info['label'];
				}elseif( isset($info['lang_index']) ){
					$return = $langmessage[$info['lang_index']];
				}
			}

			if( $return === false ){
				$return = \gp\tool\Files::CleanLabel($title);
			}

			return $return;
		}


		/**
		 * Get the browser title for a page
		 * @param string $title
		 *
		 */
		public static function GetBrowserTitle($title){
			global $gp_titles, $gp_index;

			if( !isset($gp_index[$title]) ){
				return false;
			}

			$index = $gp_index[$title];
			$title_info = $gp_titles[$index];

			if( isset($title_info['browser_title']) ){
				return $title_info['browser_title'];
			}

			$label = self::GetLabel($title);

			return strip_tags($label);
		}


		/**
		 * Add js and css components to the current web page
		 *
		 * @static
		 * @since 2.0b1
		 * @param array|string $names can be either a csv (with or without space characters) or an array. Since 5.1.1
		 */
		public static function LoadComponents($names=''){
			if( gettype($names) == 'array' ){
				$names = implode(',', $names);
			}
			\gp\tool\Output::$components .= ',' . $names . ',';
			\gp\tool\Output::$components = str_replace(',,', ',', \gp\tool\Output::$components);
			\gp\tool\Output::$components = str_replace(' ', '', \gp\tool\Output::$components);
		}


		/**
		 * Add gallery js and css to the <head> section of a page
		 *
		 */
		public static function ShowingGallery(){
			global $page, $config;

			static $showing = false;
			if( $showing ){
				return;
			}
			$showing = true;

			self::AddColorBox();
			$css = \gp\tool\Plugins::OneFilter('Gallery_Style');
			if( $css === false ){

				if( !empty($config['gallery_legacy_style']) ){
					$page->css_user[] = '/include/css/legacy_gallery.css';
					return;
				}

				$page->css_user[] = '/include/css/default_gallery.css';
				self::LoadComponents('dotdotdot');
				$page->jQueryCode .= "\n" .
					'$(".filetype-gallery .caption")'.
						'.dotdotdot({ ' .
							'watch : "window", ' .
							'callback : function(isTruncated, orgContent){ ' .
							'$(this).data("originalContent", orgContent); ' .
						'}' .
					'});';

				if( \gp\tool::LoggedIn() ){
					$page->head_script	.= "\n" .
						'var gallery_editing_options = { legacy_style : false };';
					$page->jQueryCode	.= "\n" .
						'$(document).on("editor_area:loaded", function(){ ' .
							'$(".filetype-gallery .caption").trigger("destroy.dot"); ' .
						'});';
				}

				return;
			}

			$page->head .= "\n" .
				'<link type="text/css" media="screen" rel="stylesheet" href="' . $css . '" />';
		}


		public static function AddColorBox(){
			global $page, $config, $dataDir;

			static $init = false;
			if( $init ){
				return;
			}
			$init = true;

			// use page->lang for colorbox
			if( isset($page->lang) &&
				$page->lang != $config['language'] &&
				file_exists($dataDir . '/include/languages/' . $page->lang . '.main.inc')
			){
				include($dataDir . '/include/languages/' . $page->lang . '.main.inc');
			}else{
				global $langmessage;
			}

			\gp\tool\Output::$inline_vars['colorbox_lang'] = [
				'previous'	=> $langmessage['Previous'],
				'next'		=> $langmessage['Next'],
				'close'		=> $langmessage['Close'],
				'caption'	=> $langmessage['caption'],
				'current'	=> sprintf($langmessage['Image_of'],'{current}','{total}')
			]; //'Start Slideshow'=>'slideshowStart', 'Stop Slideshow'=>'slideshowStop'

			self::LoadComponents('colorbox');
		}


		/**
		 * Set the $config array from /data/_site/config.php
		 *
		 */
		public static function GetConfig(){
			global $config, $gp_hooks;

			$config = \gp\tool\Files::Get('_site/config');

			if( !is_array($config) || !array_key_exists('gpversion', $config) ){
				self::stop();
			}

			//make sure defaults are set
			$config += [
				'maximgarea'				=> '2073600',
				'preserve_icc_profiles'		=> true,		//5.1
				'preserve_image_metadata'	=> true,		//5.1
				'maxthumbsize'				=> '300',
				'maxthumbheight'			=> '',			//5.1
				'thumbskeepaspect'			=> false,
				'check_uploads'				=> false,
				'colorbox_style'			=> 'example1',
				'gallery_legacy_style'		=> true,
				'combinecss'				=> true,
				'combinejs'					=> true,
				'minifyjs'					=> false,		//5.2
				'etag_headers'				=> true,
				'customlang'				=> [],
				'showgplink'				=> true,
				'showsitemap'				=> true,
				'showlogin'					=> true,
				'auto_redir'				=> 90,			//2.5
				'history_limit'				=> min(gp_backup_limit, 30),
				'resize_images'				=> true,		//3.5
				'addons'					=> [],
				'themes'					=> [],
				'gadgets'					=> [],
				'passhash'					=> 'sha1',
				'hooks'						=> [],
				'space_char'				=> '-',			//4.6
				'cdn'						=> '',
				'admin_ui_autohide_below'	=> '992',		//5.2
				'admin_ui_hotkey'			=> 'H',			//5.2
				'admin_ui_hotkey_code'		=> 'ctrlKey+72',	//5.2
				'homepath_auto'				=> true,		//5.2
			];

			//cdn settings
			if( isset($config['jquery']) && $config['jquery'] != 'local' ){
				$config['cdn'] = 'CloudFlare';
				unset($config['jquery']);
			}

			//shahash deprecated 4.0
			if( isset($config['shahash']) && !$config['shahash'] ){
				$config['passhash'] = 'md5';
			}

			// default gadgets
			$config['gadgets']['Contact']		= ['class'	=> '\\gp\\special\\ContactGadget'];
			$config['gadgets']['Search']		= ['method'	=> ['\\gp\\special\\Search', 'gadget']];
			$config['gadgets']['Admin_Link']	= ['method'	=> ['\\gp\\tool\\Output', 'AdminLinkGadget']];
			$config['gadgets']['Login_Link']	= ['method'	=> ['\\gp\\tool\\Output', 'LoginLinkGadget']];


			foreach($config['hooks'] as $hook => $hook_info){
				if( isset($gp_hooks[$hook]) ){
					$gp_hooks[$hook] += $hook_info;
				}else{
					$gp_hooks[$hook] = $hook_info;
				}
			}

			self::GetLangFile();
			self::GetPagesPHP();

			//upgrade?
			if( version_compare($config['gpversion'], '2.3.4', '<') ){
				new \gp\tool\Upgrade();
			}
		}


		/**
		 * Stop loading
		 * Check to see if the cms has already been installed
		 *
		 */
		public static function stop(){
			global $dataDir;

			if( !\gp\tool::Installed() ){

				if( file_exists($dataDir . '/include/install/install.php') ){
					self::SetLinkPrefix();
					includeFile('install/install.php');
					die();
				}
			}

			die(
				'<p>Notice: The site configuration did not load properly.</p>'
				. '<p>If you are the site administrator, you can troubleshoot the problem turning '
				. 'debugging "on" or bypass it by enabling ' . \CMS_NAME . ' safe mode.</p>'
				. '<p>More information is available in the '
				. '<a href="' . \CMS_DOMAIN . '/Docs/Main/Troubleshooting">Documentation</a>.</p>'
				. self::ErrorBuffer(true, false)
			);
		}


		/**
		 * Return true if
		 *
		 */
		public static function Installed(){
			global $dataDir;

			return \gp\tool\Files::Exists($dataDir . '/data/_site/config.php');
		}


		/**
		 * Set global variables ($gp_index, $gp_titles, $gp_menu and $gpLayouts) from _site/pages.php
		 *
		 */
		public static function GetPagesPHP(){
			global $gp_index, $gp_titles, $gp_menu, $gpLayouts, $config;

			$pages			= \gp\tool\Files::Get('_site/pages');
			$gpLayouts		= $pages['gpLayouts'];
			$gp_index		= $pages['gp_index'];
			$gp_titles		= $pages['gp_titles'];
			$gp_menu		= $pages['gp_menu'];

			if( !is_array($gp_menu) ){
				self::stop();
			}

			//title related configuration settings
			if( empty($config['homepath_key']) ){
				$config['homepath_key'] = key($gp_menu);
			}
			$config['homepath'] = self::IndexToTitle($config['homepath_key']);
		}


		/**
		 * Generate a new file index
		 * skip indexes that are just numeric
		 */
		public static function NewFileIndex(){
			global $gp_index, $gp_titles, $dataDir, $config;

			$last_index		= 'a';
			$num_index		= 0;

			/*prevent reusing old indexes */
			if( count($gp_index) > 0 ){
				$max = count($gp_index);
				$title = end($gp_index);
				for($i = $max; $i > 0; $i--){
					$last_index = current($gp_index);
					$type = self::SpecialOrAdmin($title);
					if( $type === 'special' ){
						$title = prev($gp_index);
						continue;
					}
					$i = 0;
				}
				reset($gp_index);
				$num_index = base_convert($last_index, 36, 10);
				$num_index++;
			}

			do{
				$index = base_convert($num_index, 10, 36);
				$num_index++;

				//check backup dir
				$backup_dir = $dataDir . '/data/_backup/pages/' . $index;
				if( file_exists($backup_dir) ){
					$index = false;
					continue;
				}

				//check for page directory
				$draft_file	= $dataDir . '/data/_pages/' . substr($config['gpuniq'], 0, 7) . '_' . $index;
				if( file_exists($draft_file) ){
					$index = false;
					continue;
				}

			}while( !$index || is_numeric($index) || isset($gp_titles[$index]) );

			return $index;
		}


		/**
		 * Return the title of file using the index
		 * Will return false for titles that are external links
		 * @param string $index The index of the file
		 */
		public static function IndexToTitle($index){
			global $gp_index;
			return array_search($index, $gp_index);
		}


		/**
		 * Traverse the $menu upwards looking for the parents of the a title given by it's index
		 * @param string $index The data index of the child title
		 * @return array
		 *
		 */
		public static function Parents($index, $menu){
			$parents = [];

			if( !isset($menu[$index]) || !isset($menu[$index]['level']) ){
				return $parents;
			}

			$checkLevel = $menu[$index]['level'];
			$menu_ids = array_keys($menu);
			$key = array_search($index, $menu_ids);
			for($i = ($key-1); $i >= 0; $i--){
				$id = $menu_ids[$i];

				//check the level
				$level = $menu[$id]['level'];
				if( $level >= $checkLevel ){
					continue;
				}
				$checkLevel = $level;

				$parents[] = $id;

				//no need to go further
				if( $level == 0 ){
					return $parents;
				}
			}

			return $parents;
		}


		/**
		 * Traverse the $menu and gather all the descendants of a title given by it's $index
		 * @param string $index The data index of the child title
		 * @param array $menu The menu to use to check for descendants
		 * @param bool $children_only Option to return a list of children instead of all descendants. Since 4.3
		 * @return array
		 */
		public static function Descendants($index, $menu, $children_only=false){

			$titles = [];

			if( !isset($menu[$index]) || !isset($menu[$index]['level']) ){
				return $titles;
			}

			$start_level	= $menu[$index]['level'];
			$menu_ids		= array_keys($menu);
			$key			= array_search($index, $menu_ids);
			$count			= count($menu);
			for($i = ($key+1); $i < $count; $i++){
				$id = $menu_ids[$i];
				$level = $menu[$id]['level'];

				if( $level <= $start_level ){
					return $titles;
				}

				if( !$children_only ){
					$titles[] = $id;
				}elseif( $level == $start_level + 1 ){
					$titles[] = $id;
				}
			}

			return $titles;
		}


		/**
		 * Return the configuration value or default if it's not set
		 *
		 * @since 1.7
		 *
		 * @param string $key The key to the $config array
		 * @param mixed $default The value to return if $config[$key] is not set
		 * @return mixed
		 */
		public static function ConfigValue($key, $default=false){
			global $config;
			if( !isset($config[$key]) ){
				return $default;
			}
			return $config[$key];
		}


		/**
		 * Generate a random alphanumeric string of variable length
		 *
		 * @param int $len length of string to return
		 * @param bool $cases Whether or not to use upper and lowercase characters
		 */
		public static function RandomString($len=40, $cases=true){

			$string = 'abcdefghijklmnopqrstuvwxyz1234567890';
			if( $cases ){
				$string .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
			}

			$string = str_repeat($string, (int)round($len / 2));
			$string = str_shuffle($string);
			$start = mt_rand(1, (strlen($string) - $len));

			return substr($string, $start, $len);
		}


		/**
		 * Include the main.inc language file for $language
		 *
		 */
		public static function GetLangFile($file='main.inc', $language=false){
			global $dataDir, $config, $langmessage;

			$language	= $language ? $language : $config['language'];
			$path		= $dataDir.'/include/languages/' . $language . '.main.inc';

			if( !file_exists($path) ){
				include($dataDir . '/include/languages/en.main.inc'); //default to en
				return false;
			}

			include($path);
			return true;
		}


		/**
		 * Get translation of a $langmessage key
		 * @since 5.2
		 * @param string key to be found in the $langmessage array
		 * @param string optional language code (de, fr, …) of the language to be loaded.
		 *					if omitted the current CMS language will be used.
		 * @return string the translation if it exists, otherwise the passed term
		 *
		 */
		public static function Translate($term, $lang=false){
			global $dataDir;

			if( empty($lang) ){
				// use CMS language
				global $langmessage;
			}else{
				$path = $dataDir . '/include/languages/' . $lang . '.main.inc';
				if( !file_exists($path) ){
					return $term;
				}
				include($path);
			}

			return isset($langmessage[$term]) ? $langmessage[$term] : $term;
		}



		/**
		 * Determine if the $title is a special or admin page
		 * @param string $title
		 * @return mixed 'admin','special' or false
		 */
		public static function SpecialOrAdmin($title){
			global $gp_index, $gp_titles;

			$lower_title = strtolower($title);

			if( $lower_title === 'admin' ){
				return 'admin';
			}elseif( strpos($lower_title, 'admin_') === 0 || strpos($lower_title, 'admin/') === 0 ){
				return 'admin';
			}

			if( strpos($lower_title, 'special_') === 0 ){
				return 'special';
			}


			$parts = explode('/', $title);
			do{
				$title = implode('/', $parts);
				if( isset($gp_index[$title]) ){
					$key = $gp_index[$title];
					$info = $gp_titles[$key];
					if( $info['type'] == 'special' ){
						return 'special';
					}
				}
				array_pop($parts);
			}while( count($parts) );

			return false;
		}


		/**
		 * Return the name of the page being requested based on $_SERVER['REQUEST_URI']
		 * May also redirect the request
		 *
		 * @return string The title to display based on the request uri
		 *
		 */
		public static function WhichPage(){
			global $config, $gp_menu;

			$path	= \gp\tool\Editing::Sanitize($_SERVER['REQUEST_URI']);
			$path	= self::CleanRequest($path);

			$pos = mb_strpos($path, '?');
			if( $pos !== false ){
				$path = mb_substr($path, 0, $pos);
			}

			$path = \gp\tool\Plugins::Filter('WhichPage', [$path]);

			//redirect if an "external link" is the first entry of the main menu
			if( empty($path) && isset($gp_menu[$config['homepath_key']]) ){
				$homepath_info = $gp_menu[$config['homepath_key']];
				if( isset($homepath_info['url']) ){
					self::Redirect($homepath_info['url'], 302);
				}
			}

			if( empty($path) ){
				return $config['homepath'];
			}

			//redirect to / for homepath request
			if( isset($config['homepath']) && $path == $config['homepath'] ){
				self::Redirect(self::GetUrl('', '', false));
			}

			return $path;
		}


		/**
		 * Redirect the request to $path with http $code
		 *
		 * @param string|array $path url to redirect to
		 * @param string $code http redirect code: 301 or 302
		 *
		 */
		public static function Redirect($path, $code=302){
			global $wbMessageBuffer, $gpAdmin;

			// if $path is an array, generate a url with \gp\tool::GetUrl($path);
			// add gpreq and jsoncallback to maintain ajax requests
			if( is_array($path) ){
				$add_query	= ['gpreq' => 1, 'jsoncallback' => 1];
				$add_query	= array_intersect_key($_REQUEST, $add_query);
				$path		+= [1=>[]];
				$path[1]	+= $add_query;

				$path		= \gp\tool::GetUrl($path[0], http_build_query($path[1], '', '&'), false);
			}


			//store any messages for display after the redirect
			if( self::LoggedIn() && count($wbMessageBuffer) ){
				$gpAdmin['message_buffer'] = $wbMessageBuffer;
			}



			//prevent a cache from creating an infinite redirect
			Header('Last-Modified: ' . gmdate('D, j M Y H:i:s') . ' GMT');
			Header('Expires: ' . gmdate('D, j M Y H:i:s', time()) . ' GMT');
			Header('Cache-Control: no-store, no-cache, must-revalidate'); // HTTP/1.1
			Header('Cache-Control: post-check=0, pre-check=0', false);
			Header('Pragma: no-cache' ); // HTTP/1.0

			switch((int)$code){
				case 301:
					self::status_header(301, 'Moved Permanently');
				break;
				case 302:
					self::status_header(302, 'Found');
				break;
			}

			header('Location: ' . $path);
			die();
		}


		/**
		 * Remove $dirPrefix and index.php from a path to get the page title
		 *
		 * @param string $path A full relative url like /install_dir/index.php/request_title
		 *
		 */
		public static function CleanRequest($path){
			global $dirPrefix;

			//use dirPrefix to find requested title
			if( !empty($dirPrefix) ){
				$pos = strpos($path, $dirPrefix);
				if( $pos !== false ){
					$path = substr($path, $pos + strlen($dirPrefix));
				}
			}

			//remove /index.php/
			$pos = strpos($path, '/index.php');
			if( $pos === 0 ){
				$path = substr($path, 11);
			}

			$path = ltrim($path, '/');

			return $path;
		}


		/**
		 * Handle admin login/logout/session_start if admin session parameters exist
		 *
		 */
		public static function sessions(){

			//alternate sessions
			if( defined('gpcom_sessions') ){
				include(gpcom_sessions);
			}

			$cmd = '';
			if( isset($_GET['cmd']) && $_GET['cmd'] == 'logout' ){
				$cmd = 'logout';
			}elseif( isset($_POST['cmd']) && $_POST['cmd'] == 'login' ){
				$cmd = $_POST['cmd'];
			}elseif( count($_COOKIE) ){
				foreach($_COOKIE as $key => $value){
					if( strpos($key, 'gpEasy_') === 0 ){
						$cmd = 'start';
						break;
					}
				}
			}

			if( empty($cmd) ){
				return;
			}

			\gp\tool\Session::Init();
		}


		/**
		 * Return true if an administrator is logged in
		 * @return bool
		 */
		public static function LoggedIn(){
			global $gpAdmin;

			$loggedin = false;
			if( isset($gpAdmin) && is_array($gpAdmin) ){
				$loggedin = true;
			}

			return \gp\tool\Plugins::Filter('LoggedIn', [$loggedin]);
		}


		/**
		 * @deprecated
		 * Create a new nonce
		 * Deprecated as of Typesetter 5.2
		 * Use \gp\tool\Nonce::Create() instead
		 */
		public static function new_nonce($action='none', $anon=false, $factor=43200){
			// trigger_error('Deprecated: \gp\tool::new_nonce(), use \gp\tool\Nonce::Create() instead', E_USER_WARNING);
			return \gp\tool\Nonce::Create($action, $anon, $factor);
		}


		/**
		 * @deprecated
		 * Verify a nonce ($check_nonce)
		 * Deprecated as of Typesetter 5.2
		 * Use \gp\tool\Nonce::Verify() instead
		 *
		 * @param string $action Should be the same $action that is passed to new_nonce()
		 * @param mixed $check_nonce The user submitted nonce or false if $_REQUEST['_gpnonce'] can be used
		 * @param bool $anon True if the nonce is being used for anonymous users
		 * @param int $factor Determines the length of time the generated nonce will be valid. The default 43200 will result in a 24hr period of time.
		 * @return mixed Return false if the $check_nonce did not pass. 1 or 2 if it passes.
		 *
		 */
		public static function verify_nonce($action='none', $check_nonce=false, $anon=false, $factor=43200){
			// trigger_error('Deprecated: \gp\tool::verify_nonce(), use \gp\tool\Nonce::Verify() instead', E_USER_WARNING);
			return \gp\tool\Nonce::Verify($action, $check_nonce, $anon, $factor);
		}


		/**
		 * @deprecated
		 * Generate a nonce hash
		 * Deprecated as of Typesetter 5.2
		 * Use \gp\tool\Nonce::Hash() instead
		 *
		 * @param string $nonce
		 * @param int $tick_offset
		 * @param int $factor Determines the length of time the generated nonce will be valid. The default 43200 will result in a 24hr period of time.
		 *
		 */
		public static function nonce_hash($nonce, $tick_offset=0, $factor=43200){
			// trigger_error('Deprecated: \gp\tool::nonce_hash(), use \gp\tool\Nonce::Hash() instead', E_USER_WARNING);
			return \gp\tool\Nonce::Hash($nonce, $tick_offset, $factor);
		}


		/**
		 * Return the command sent by the user
		 * Don't use $_REQUEST here because SetCookieArgs() uses $_GET
		 *
		 */
		public static function GetCommand($type='cmd'){
			global $gpAdmin;

			if( is_array($gpAdmin) && isset($gpAdmin['locked']) && $gpAdmin['locked'] ){
				return false;
			}

			if( isset($_POST[$type]) ){
				return $_POST[$type];
			}

			if( isset($_GET[$type]) ){
				return $_GET[$type];
			}
			return false;
		}


		/**
		 * Used for receiving arguments from javascript without having to put variables in the $_GET request
		 * nice for things that shouldn't be repeated!
		 */
		public static function SetCookieArgs(){
			static $done = false;

			if( $done || !gp_cookie_cmd ){
				return;
			}

			self::RawCookies();

			//get cookie arguments
			if( empty($_COOKIE['cookie_cmd']) ){
				return;
			}

			$test = $_COOKIE['cookie_cmd'];
			if( $test[0] === '?' ){
				$test = substr($test, 1);
			}

			parse_str($test,$cookie_args);
			if( !$cookie_args ){
				return;
			}

			//parse_str will overwrite values in $_GET/$_REQUEST
			$_GET = $cookie_args + $_GET;
			$_REQUEST = $cookie_args + $_REQUEST;

			//for requests with verification, we'll set $_POST
			if( !empty($_GET['verified']) ){
				$_POST = $cookie_args + $_POST;
			}

			$done = true;
		}


		/**
		 * Fix the $_COOKIE array if RAW_HTTP_COOKIE is set
		 * Some servers encrypt cookie values before sending them to the client
		 * Since cookies set by the client (with JavaScript) are not encrypted,
		 * the values won't be set in $_COOOKIE
		 *
		 */
		public static function RawCookies(){
			if( empty($_SERVER['RAW_HTTP_COOKIE']) ){
				return;
			}
			$csplit = explode(';', $_SERVER['RAW_HTTP_COOKIE']);
			foreach($csplit as $pair){
				if( !strpos($pair, '=') ){
					continue;
				}
				list($key, $value) = explode('=', $pair);
				$key = rawurldecode(trim($key));
				if( !array_key_exists($key, $_COOKIE) ){
					$_COOKIE[$key] = rawurldecode(trim($value));
				}
			}
		}


		/**
		 * Output Javascript code to set variable defaults
		 *
		 */
		public static function JsStart(){
			global $linkPrefix;

			//default Variables
			\gp\tool\Output::$inline_vars['isadmin']			= false;
			\gp\tool\Output::$inline_vars['gpBase']				= rtrim(self::GetDir(''), '/');
			\gp\tool\Output::$inline_vars['post_nonce']			= '';
			\gp\tool\Output::$inline_vars['req_type']			= strtolower(htmlspecialchars($_SERVER['REQUEST_METHOD']));

			if( \gpdebugjs ){
				if( is_string(\gpdebugjs) ){
					\gp\tool\Output::$inline_vars['debugjs']	= 'send';
				}else{
					\gp\tool\Output::$inline_vars['debugjs']	= true;
				}
			}else{
				\gp\tool\Output::$inline_vars['debugjs']		= false;
			}

			if( self::LoggedIn() ){

				\gp\tool\Output::$inline_vars['isadmin']			= true;
				\gp\tool\Output::$inline_vars['req_time']			= time();
				\gp\tool\Output::$inline_vars['gpBLink']			= self::HrefEncode($linkPrefix, false);
				\gp\tool\Output::$inline_vars['post_nonce']			= \gp\tool\Nonce::Create('post', true);
				\gp\tool\Output::$inline_vars['gpFinderUrl']		= \gp\tool::GetUrl('Admin/Browser');
				\gp\tool\Output::$inline_vars['hideAdminUIcfg']		= self::HideAdminUIcfg();

				\gp\tool\Session::GPUIVars();
			}

			echo 'var gplinks={}, gpinputs={}, gpresponse={}';
			foreach(\gp\tool\Output::$inline_vars as $key => $value){
				echo ',' . $key . '=' . json_encode($value);
			}
			echo ';';
		}


		/**
		 * Get config values for hiding the admin UI
		 *
		 * @return array $cfg configuration values
		 *
		 */
		public static function HideAdminUIcfg(){
			global $config, $langmessage;
			$cfg = [
				'autohide_below'	=> 0,
				'hotkey'			=> '',
				'hotkey_modkeys'	=> [],
				'hotkey_which'		=> '',
			];

			if( !empty($config['admin_ui_autohide_below']) && is_numeric($config['admin_ui_autohide_below']) ){
				$cfg['autohide_below'] = (int)$config['admin_ui_autohide_below'];
			}

			if( !empty($config['admin_ui_hotkey']) && !empty($config['admin_ui_hotkey_code']) ){
				$cfg['hotkey'] = $config['admin_ui_hotkey'];
				if( strpos($config['admin_ui_hotkey_code'], 'ctrlKey+') !== false ){
					$cfg['hotkey_modkeys'][] = 'ctrlKey';
				}
				if( strpos($config['admin_ui_hotkey_code'], 'shiftKey+') !== false ){
					$cfg['hotkey_modkeys'][] = 'shiftKey';
				}
				if( strpos($config['admin_ui_hotkey_code'], 'altKey+') !== false ){
					$cfg['hotkey_modkeys'][] = 'altKey';
				}
				if( strpos($config['admin_ui_hotkey_code'], 'metaKey+') !== false ){
					$cfg['hotkey_modkeys'][] = 'metaKey';
				}
				$parts 					= explode('+', $config['admin_ui_hotkey_code']);
				$cfg['hotkey_which']	= array_pop($parts);
			}

			return $cfg;
		}


		/**
		 * Return the hash of $arg using the appropriate hashing function for the installation
		 *
		 * @param string $arg The string to be hashed
		 * @param string $algo The hashing algorithm to be used
		 * @param int $loops The number of times to loop the $arg through the algorithm
		 *
		 */
		public static function hash($arg, $algo='sha512', $loops=1000){
			$arg = trim($arg);

			switch($algo){
				//password_hash
				case 'password_hash':
				$temp = self::hash($arg, 'sha512', 50); // 50 salted sha512, same as in /include/admin/Settings/Users.php -> SetUserPass
				return password_hash($temp, PASSWORD_DEFAULT);

				//md5
				case 'md5':
				trigger_error('md5 should not be used, please reset your password');
				return md5($arg);

				//sha1
				case 'sha1':
				return sha1($arg);
			}

			//sha512: looped with dynamic salt
			for($i = 0; $i < $loops; $i++){
				$ints			= preg_replace('#[a-f]#', '', $arg);
				$salt_start		= (int)substr($ints, 0, 1);
				$salt_len		= (int)substr($ints, 2, 1);
				$salt			= substr($arg, $salt_start, $salt_len);
				$arg			= hash($algo, $arg . $salt);
			}

			return $arg;
		}


		public static function AjaxWarning(){
			global $page,$langmessage;
			$page->ajaxReplace[] = ['gpabox', '', $langmessage['OOPS_Start_over']];
		}


		public static function IdUrl($request_cmd='cv'){
			global $config, $dataDir, $gpLayouts;

			//command
			$args				= [];
			$args['cmd']		= $request_cmd;

			$_SERVER += ['SERVER_SOFTWARE' => ''];

			//checkin
			if( isset($config['gpuniq']) ){
				$args['mdu']		= substr(md5($config['gpuniq']), 0, 20); // gpuniq won't be set before installation
			}
			$args['site']		= self::AbsoluteUrl(''); //keep full path for backwards compat
			$args['gpv']		= \gpversion;
			$args['php']		= phpversion();
			$args['se']			= $_SERVER['SERVER_SOFTWARE'];
			$args['data']		= $dataDir;
			//$args['zlib'] = (int)function_exists('gzcompress');

			//service provider
			if( defined('service_provider_id') && is_numeric(\service_provider_id) ){
				$args['provider'] = \service_provider_id;
			}

			//plugins
			$addon_ids = [];
			if( isset($config['addons']) && is_array($config['addons']) ){
				self::AddonIds($addon_ids, $config['addons']);
			}

			//themes
			if( isset($config['themes']) && is_array($config['themes']) ){
				self::AddonIds($addon_ids, $config['themes']);
			}

			//layouts
			if( is_array($gpLayouts) ){
				foreach($gpLayouts as $layout_info){
					if( !isset($layout_info['addon_id']) ){
						continue;
					}
					$addon_ids[] = $layout_info['addon_id'];
				}
			}

			$addon_ids		= array_unique($addon_ids);
			$args['as']		= implode('-', $addon_ids);

			return \addon_browse_path . '/Resources?' . http_build_query($args, '', '&');
		}


		public static function AddonIds(&$addon_ids, $array){

			foreach($array as $addon_info){
				if( !isset($addon_info['id']) ){
					continue;
				}
				$addon_id = $addon_info['id'];
				if( isset($addon_info['order']) ){
					$addon_id .= '.' . $addon_info['order'];
				}
				$addon_ids[] = $addon_id;
			}
		}


		/**
		 * Used to send error reports without affecting the display of a page
		 *
		 */
		public static function IdReq($img_path, $jquery=true){
			global $page;

			// don't send for unit testing
			if( defined('gp_unit_testing') ){
				return;
			}

			//using jquery asynchronously doesn't affect page loading
			//error function defined to prevent the default error function in main.js from firing
			if( $jquery && is_object($page) ){
				$page->head_script .= '$.ajax(' . json_encode($img_path) . ', {error:function(){}, dataType: "jsonp"});';
				return;
			}

			return '<img src="' . self::Ampersands($img_path) . '" height="1" width="1" alt="" '
				. 'style="border:0 none!important; height:1px!important; width:1px!important; '
				. 'padding:0!important; margin:0!important;"/>';
		}


		/**
		 * Return a debug message with link to online debug info
		 *
		 */
		public static function Debug($lang_key, $debug=[]){
			global $langmessage, $dataDir;

			//add backtrace info
			$backtrace = debug_backtrace();
			while( count($backtrace) > 0 && !empty($backtrace[0]['function']) && $backtrace[0]['function'] == 'Debug' ){
				array_shift($backtrace);
			}

			$debug['trace']	= array_intersect_key(
				$backtrace[0],
				[
					'file'		=> '',
					'line'		=> '',
					'function'	=> '',
					'class'		=> '',
				]
			);

			if( !empty($debug['trace']['file']) && !empty($dataDir) && strpos($debug['trace']['file'],$dataDir) === 0 ){
				$debug['trace']['file'] = substr($debug['trace']['file'], strlen($dataDir) );
			}

			//add php and cms info
			$debug['lang_key']		= $lang_key;
			$debug['phpversion']	= phpversion();
			$debug['gpversion']		= \gpversion;
			$debug['Rewrite']		= $_SERVER['gp_rewrite'];
			$debug['Server']		= isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '';

			//create string
			$debug	= json_encode($debug);
			$debug	= base64_encode($debug);
			$debug	= trim($debug, '=');
			$debug	= strtr($debug, '+/', '-_');

			$label	= isset($langmessage[$lang_key]) ? $langmessage[$lang_key] : $lang_key;

			return ' <span>' . $label . ' <a href="' . \debug_path . '?data=' . $debug . '" target="_blank">More Info...</a></span>';
		}


		//only include error buffer when admin is logged in
		public static function ErrorBuffer($check_user=true, $jquery=true){
			global $wbErrorBuffer, $config, $dataDir, $rootDir;

			if( count($wbErrorBuffer) == 0 ){
				return;
			}

			if( isset($config['Report_Errors']) && !$config['Report_Errors'] ){
				return;
			}

			if( $check_user && !self::LoggedIn() ){
				return;
			}

			$dataDir_len = strlen($dataDir);
			$rootDir_len = strlen($rootDir);
			$img_path = self::IdUrl('er');
			$i = 0;

			foreach($wbErrorBuffer as $error){

				//remove $dataDir or $rootDir from the filename
				$file_name = self::WinPath($error['ef' . $i]);
				if( $dataDir_len > 1 && strpos($file_name,$dataDir) === 0 ){
					$file_name = substr($file_name, $dataDir_len);
				}elseif( $rootDir_len > 1 && strpos($file_name, $rootDir) === 0 ){
					$file_name = substr($file_name, $rootDir_len);
				}
				$error['ef' . $i] = substr($file_name, -100);

				$new_path = $img_path . '&' . http_build_query($error, '', '&');

				//maximum length of 2000 characters
				if( strlen($new_path) > 2000 ){
					break;
				}
				$img_path = $new_path;
				$i++;
			}

			return self::IdReq($img_path, $jquery);
		}


		/**
		 * Test if function exists.  Also handles case where function is disabled via Suhosin.
		 * Modified from: http://dev.piwik.org/trac/browser/trunk/plugins/Installation/Controller.php
		 *
		 * @param string $function Function name
		 * @return bool True if function exists (not disabled); False otherwise.
		 */
		public static function function_exists($function){
			$function = strtolower($function);

			// eval() is a language construct
			if( $function == 'eval' ){
				// does not check suhosin.executor.eval.whitelist (or blacklist)
				if( extension_loaded('suhosin') && self::IniGet('suhosin.executor.disable_eval') ){
					return false;
				}
				return true;
			}

			if( !function_exists($function) ){
				return false;
			}

			$blacklist = @ini_get('disable_functions');
			if( extension_loaded('suhosin') ){
				$blacklist .= ',' . @ini_get('suhosin.executor.func.blacklist');
			}

			$blacklist = explode(',', $blacklist);
			$blacklist = array_map('trim', $blacklist);
			$blacklist = array_map('strtolower', $blacklist);
			if( in_array($function, $blacklist) ){
				return false;
			}

			return true;
		}


		/**
		 * A more functional JSON Encode function
		 * @param mixed $data
		 * @return string
		 */
		public static function JsonEncode($data){

			$search		= ['<script', '<\/script>'];
			$repl		= ['<"+"script', '<"+"\/script>'];

			$type = gettype($data);
			switch( $type ){
				case 'NULL':
					return 'null';

				case 'boolean':
					return ($data ? 'true' : 'false');

				case 'integer':
				case 'double':
				case 'float':
					return json_encode($data);

				case 'string':
					$data = htmlspecialchars_decode(htmlspecialchars($data, ENT_SUBSTITUTE, 'UTF-8'));
					$data = json_encode($data);
					return str_replace($search, $repl, $data);

				case 'object':
					$data = get_object_vars($data);
				case 'array':
					$output_index_count = 0;
					$output_indexed = [];
					$output_associative = [];
					foreach( $data as $key => $value ){
						$output_indexed[] = self::JsonEncode($value);
						$output_associative[] = self::JsonEncode($key) . ':' . self::JsonEncode($value);
						if( $output_index_count !== NULL && $output_index_count++ !== $key ){
							$output_index_count = NULL;
						}
					}
					if( $output_index_count !== NULL ){
						return '[' . implode(',', $output_indexed) . ']';
					}
					return '{' . implode(',', $output_associative) . '}';

			}

			return ''; // Not supported
		}


		/**
		 * Date format funciton, uses formatting similar to php's strftime function
		 * http://php.net/manual/en/function.strftime.php
		 *
		 */
		public static function Date($format='', $time=null){

			if( empty($format) ){
				return '';
			}

			if( is_null($time) ){
				$time = time();
			}
			$time = (int)$time;

			$match_count = preg_match_all('#%+[^\s]#', $format, $matches, PREG_OFFSET_CAPTURE);
			if( $match_count ){
				$matches = array_reverse($matches[0]);
				foreach($matches as $match){
					$len = strlen($match[0]);
					if( $len%2 ){
						$replacement = strftime($match[0], $time);
					}else{
						$piece = substr($match[0], -2, 2);
						switch($piece){
							case '%e':
								$replacement = strftime(substr($match[0], 0, -2), $time) . ltrim(strftime('%d', $time), '0');
							break;
							default:
								$replacement = strftime($match[0], $time);
							break;
						}
					}
					$format = substr_replace($format, $replacement, $match[1], strlen($match[0]));
				}
			}
			return $format;
		}


		/**
		 * Get an image's thumbnail path
		 *
		 */
		public static function ThumbnailPath($img){

			//already thumbnail path
			if( strpos($img, '/data/_uploaded/image/thumbnails') !== false ){
				return $img;
			}

			$dir_part = '/data/_uploaded/';
			$pos = strpos($img, $dir_part);
			if( $pos === false ){
				return $img;
			}

			// svg or not svg
			$nameParts = explode('.', $img);
			$type = array_pop($nameParts);
			$type = strtolower($type);
			if( strpos('svgz', $type) !== 0 ){
				$type = 'jpg';
			}

			return substr_replace($img, '/data/_uploaded/image/thumbnails/', $pos, strlen($dir_part)) . '.' . $type;
		}


		/**
		 * Generate a checksum for the $array
		 *
		 */
		public static function ArrayHash($array){
			return md5(json_encode($array));
		}


		/**
		 * Return the key of an array if found
		 * Alert if $msg is not null
		 *
		 * @param string $key
		 * @param array $array
		 * @param string $msg
		 * @return mixed
		 */
		public static function ArrayKey($key, $array, $msg=null){
			global $langmessage;

			if( !isset($array[$key]) ){

				if( !is_null($msg) ){
					msg($langmessage['OOPS'] . ' ' . $msg);
				}

				return false;
			}

			return array_search($array[$key], $array, true);
		}


		/**
		 * Convert a string representation of a byte value to an number
		 * @param string $value
		 * @return int
		 */
		public static function getByteValue($value){

			if( is_numeric($value) ){
				return (int)$value;
			}

			$lastChar = strtolower(substr($value, -1));
			$num = (int)substr($value, 0, -1);

			switch($lastChar){
				case 'g':
					$num *= 1024;
				case 'm':
					$num *= 1024;
				case 'k':
					$num *= 1024;
					break;
			}

			return $num;
		}


		/**
		 * Get the extension of the $file
		 *
		 */
		public static function Ext($file){
			$ext = pathinfo($file, PATHINFO_EXTENSION);
			return strtolower($ext);
		}


		/**
		 * @deprecated 3.0
		 * use \gp\tool\Editing::UseCK();
		 */
		public static function UseFCK($contents, $name='gpcontent'){
			trigger_error('Deprecated Function');
			\gp\tool\Editing::UseCK($contents, $name);
		}


		/**
		 * @deprecated 3.0
		 * Use \gp\tool\Editing::UseCK();
		 */
		public static function UseCK($contents, $name='gpcontent', $options=[]){
			trigger_error('Deprecated Function');
			\gp\tool\Editing::UseCK($contents, $name, $options);
		}
	}
}

namespace{
	class common extends \gp\tool{}
}