<?php namespace HashOver;// Copyright (C) 2010-2021 Jacob Barkdull// This file is part of HashOver.//// HashOver is free software: you can redistribute it and/or modify// it under the terms of the GNU Affero General Public License as// published by the Free Software Foundation, either version 3 of the// License, or (at your option) any later version.//// HashOver is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the// GNU Affero General Public License for more details.//// You should have received a copy of the GNU Affero General Public License// along with HashOver.  If not, see <http://www.gnu.org/licenses/>.class Setup extends Settings{	public $hasIntl;	public $scheme;	public $absolutePath;	public $commentsPath;	public $threadsPath;	public $website;	public $isMobile = false;	public $remoteAccess = false;	public $filePath;	public $urlQueryList = array ();	public $threadQueryList = array ();	public $urlQueries;	public $threadQueries;	public $threadPath;	public $threadName = '';	public $pageURL = '';	public $pageTitle = '';	public $instanceNumber = 1;	// Required extensions to check for	protected $extensions = array (		'date', 'dom', 'json', 'mbstring', 'openssl', 'pcre'	);	// Characters to convert to dashes in thread names	protected $dashFromThreads = array (		'<', '>', ':', '"', '/', '\\', '|', '?',		'&', '!', '*', '.', '=', '_', '+', ' '	);	// Characters to convert to dashes in domain names	protected $dashFromDomains = array (		'<', '>', ':', '"', '/', '\\', '|', '?',		'&', '!', '*', '=', '_', '+', ' '	);	// HashOver-specific URL queries to be ignored	protected $hashoverQueries = array (		'hashover-reply', 'hashover-edit'	);	public function __construct ()	{		// Check for required extensions, before calling parent		$this->extensionsLoaded ($this->extensions);		// Construct parent class		parent::__construct ();		// Intl extension presence indicator		$this->hasIntl = extension_loaded ('intl');		// Get connection scheme		$this->scheme = $this->isHTTPS () ? 'https' : 'http';		// Absolute path or remote access		$this->absolutePath = $this->scheme . '://' . $this->domain;		// Check if multisite support is enabled		if ($this->supportsMultisites === true) {			// If so, set website based on domain			$this->setWebsite ($this->domain);		} else {			// If not, comments directory path			$this->commentsPath = $this->commentsRoot;			// Set threads directory path			$this->threadsPath = $this->commentsRoot . '/threads';			// And set website to "all"			$this->website = 'all';		}		// Check if we have a user agent		if (!empty ($_SERVER['HTTP_USER_AGENT'])) {			// If so, get user agent			$agent = $_SERVER['HTTP_USER_AGENT'];			// And check if visitor is on mobile device based on user agent			if (preg_match ('/android|blackberry|phone|mobile|tablet/i', $agent)) {				// If so, set mobile indicator to true				$this->isMobile = true;				// And set image format to vector				$this->imageFormat = 'svg';			}		}	}	// Throws an exception if a require extension isn't loaded	public function extensionsLoaded (array $extensions)	{		// Run through given extensions		foreach ($extensions as $extension) {			// Throw exception if extension isn't loaded			if (extension_loaded ($extension) === false) {				throw new \Exception (					'Failed to detect required extension: ' . $extension . '.'				);			}		}	}	// Checks if host is localhost	public function isLocalhost ($host)	{		// "localhost" equivalent addresses		$addresses = array ('127.0.0.1', 'localhost', '::1');		// Check if we're on localhost		$is_localhost = in_array ($host, $addresses, true);		return $is_localhost;	}	// Sanitizes given data for HTML use	protected function sanitizeData ($data = '')	{		// Strip HTML tags from data		$data = strip_tags (html_entity_decode ($data, false, 'UTF-8'));		// Encode HTML characters in data		$data = htmlspecialchars ($data, false, 'UTF-8', false);		return $data;	}	// Gets sanitized data from POST or GET data	protected function requestData ($data = '', $default = false)	{		// Attempt to obtain data via GET or POST		$request = $this->getRequest ($data, $default);		// Return on success		if ($request !== $default) {			$request = $this->sanitizeData ($request);		}		return $request;	}	// Sets path to website-specific threads directory	public function setWebsite ($host = 'request')	{		// Do nothing if multisites is disabled		if ($this->supportsMultisites === false) {			return;		}		// Attempt to obtain website from POST or GET if told to		if ($host === 'request') {			$host = $this->requestData ('website');		}		// Do nothing else if host is false		if ($host === false) {			return;		}		// Remove "www." from URL host		$host = str_replace ('www.', '', $host);		// Get directory safe name of host		$host = $this->getSafeDomainName ($host);		// Set host as "localhost" on local addresses		$host = $this->isLocalhost ($host) ? 'localhost' : $host;		// Append website host to comments directory path		$this->commentsPath = $this->joinPaths ($this->commentsRoot, $host);		// Set threads directory path		$this->threadsPath = $this->commentsPath . '/threads';		// Set thread directory path		$this->threadPath = $this->threadsPath . '/' . $this->threadName;		// And set website		$this->website = $host;	}	// Gets a domain with a port from given URL	protected function getDomainWithPort ($url = '')	{		// Parse URL		$url = parse_url ($url);		// Throw exception if URL or host is empty		if ($url === false or empty ($url['host'])) {			throw new \Exception (				'Failed to obtain domain name.'			);		}		// If URL has a port, return domain with port		if (!empty ($url['port'])) {			return $url['host'] . ':' . $url['port'];		}		// Otherwise, return domain without port		return $url['host'];	}	// Enables and sets up remote access	public function setupRemoteAccess ()	{		// Set remote access indicator		$this->remoteAccess = true;		// Make HTTP root path absolute		$this->httpRoot = $this->joinPaths ($this->absolutePath, $this->httpRoot);		// Synchronize settings		$this->syncSettings ();	}	// Checks remote request against allowed domains setting	public function refererCheck ()	{		// Return true if no referer is set		if (empty ($_SERVER['HTTP_REFERER'])) {			return true;		}		// Otherwise, get HTTP referer domain with port		$domain = $this->getDomainWithPort ($_SERVER['HTTP_REFERER']);		// Return true if referer domain is the same server		if ($domain === $this->domain) {			return true;		}		// Otherwise, escape wildcard for regular expression		$sub_regex = '/^' . preg_quote ('\*\.') . '/S';		// Run through allowed domains		foreach ($this->allowedDomains as $allowed_domain) {			// Escape allowed domain for regular expression			$safe_domain = preg_quote ($allowed_domain);			// Replace subdomain wildcard with proper regular expression			$domain_regex = preg_replace ($sub_regex, '(?:.*?\.)*', $safe_domain);			// Final domain regular expression			$domain_regex = '/^' . $domain_regex . '$/iS';			// Check if script was requested from an allowed domain			if (preg_match ($domain_regex, $domain)) {				// If so, setup remote access				$this->setupRemoteAccess ();				// Connection origin				$origin = $this->scheme . '://' . $domain;				// Set remote access headers				header ('Access-Control-Allow-Origin: ' . $origin);				header ('Access-Control-Allow-Credentials: true');				// And return true				return true;			}		}		// Otherwise, throw exception		throw new \Exception (			'External use not allowed.'		);	}	// Strips magic quote escape slashes from string if present	public function stripMagicQuotes ($data)	{		// Check if magic quotes are supported		if (function_exists ('get_magic_quotes_gpc')) {			// If so, strip escape slashes from string if they are enabled			if (@get_magic_quotes_gpc ()) {				$data = stripslashes ($data);			}		}		// And return data		return $data;	}	// Processes POST or GET data depending on its type	protected function processRequest ($request)	{		// Check if GET or POST data is type string		if (gettype ($request) === 'string') {			// If so, strip escape slashes if enabled			$request = $this->stripMagicQuotes ($request);			// URL decode value			$request = urldecode ($request);		}		// And return POST or GET data		return $request;	}	// Gets value from POST or GET data	public function getRequest ($key, $default = false)	{		// Attempt to get and process POST data		if (!empty ($_POST[$key])) {			return $this->processRequest ($_POST[$key]);		}		// Attempt to get and process GET data		if (!empty ($_GET[$key])) {			return $this->processRequest ($_GET[$key]);		}		// Otherwise, return default		return $default;	}	// Gets current page URL	protected function getPageURL ()	{		// Attempt to obtain URL via POST or GET		$request = $this->getRequest ('url');		// Return URL from POST or GET if it exists		if ($request !== false) {			return $request;		}		// Otherwise, return HTTP referer if it exists		if (!empty ($_SERVER['HTTP_REFERER'])) {			return $_SERVER['HTTP_REFERER'];		}		// Otherwise, throw exception		throw new \Exception (			'Failed to obtain page URL.'		);	}	// Gets configured URL queries to be ignored	protected function getIgnoredQueries ()	{		// Initial ignored URL queries		$queries = array ();		// Ignored URL queries file		$ignored_queries = $this->getAbsolutePath ('config/ignored-queries.json');		// Check if ignored URL queries file exists		if (file_exists ($ignored_queries)) {			// If so, get ignored URL queries			$data = @file_get_contents ($ignored_queries);			// Parse ignored URL queries JSON file			$json = @json_decode ($data, true);			// Merge file with initial queries if parsed successfully			if ($json !== null) {				$queries = array_merge ($json, $queries);			}		}		return $queries;	}	// Filters URL queries for URLs and thread name usage	protected function filterURLQueries ($queries)	{		// Split queries by ampersand		$url_queries = explode ('&', $queries);		// Get configured queries to be ignored		$ignored_queries = $this->getIgnoredQueries ();		// Run through queries		for ($q = 0, $ql = count ($url_queries); $q < $ql; $q++) {			// Current URL query			$current = $url_queries[$q];			// Split current URL query by equals sign			$query_parts = explode ('=', $current);			// Current URL query name			$query = $query_parts[0];			// Skip default HashOver-specific URL queries			if (in_array ($query, $this->hashoverQueries, true)) {				continue;			}			// Store query for usage in URLs			$this->urlQueryList[] = $current;			// Skip name=value queries to be ignored in thread name			if (in_array ($current, $ignored_queries, true)) {				continue;			}			// Skip query names to be ignored entirely in thread name			if (in_array ($query, $ignored_queries, true)) {				continue;			}			// Store query thread name			$this->threadQueryList[] = $current;		}	}	// Reduces dashes to one per removed character	protected function reduceDashes ($name)	{		// Remove multiple dashes		if (mb_strpos ($name, '--') !== false) {			$name = preg_replace ('/-{2,}/', '-', $name);		}		// Remove leading and trailing dashes		$name = trim ($name, '-');		return $name;	}	// Gets an OS-agnostic safe directory name	protected function getSafeThreadName ($name)	{		// Replace reserved characters with dashes		$name = str_replace ($this->dashFromThreads, '-', $name);		// Remove multiple/leading/trailing dashes		$name = $this->reduceDashes ($name);		return $name;	}	// Gets an OS-agnostic safe directory name	protected function getSafeDomainName ($name)	{		// Replace reserved characters with dashes		$name = str_replace ($this->dashFromDomains, '-', $name);		// Remove multiple/leading/trailing dashes		$name = $this->reduceDashes ($name);		// Remove leading periods to prevent hiding in UNIX		$name = ltrim ($name, '.');		return $name;	}	// Sets comment thread to read comments from	public function setThreadName ($name = 'request')	{		// Request thread if told to		if ($name === 'request') {			$name = $this->requestData ('thread', $this->threadName);		}		// Convert thread to safe directory name		$name = $this->getSafeThreadName ($name);		// Set thread directory path		$this->threadPath = $this->joinPaths ($this->threadsPath, $name);		// And set thread name		$this->threadName = $name;	}	// Sets page URL	public function setPageURL ($url = 'request')	{		// Request page URL by default		if ($url === 'request') {			$url = $this->getPageURL ();		}		// Strip HTML tags from page URL		$url = strip_tags (html_entity_decode ($url, false, 'UTF-8'));		// Turn page URL into array		$parts = parse_url ($url);		// Throw exception if URL doesn't have a scheme or host		if (empty ($parts['scheme']) or empty ($parts['host'])) {			throw new \Exception (				'URL needs a hostname and scheme.'			);		}		// Set various paths to be website-specific		$this->setWebsite ($parts['host']);		// Check if URL has a path and is not the index		if (!empty ($parts['path']) and $parts['path'] !== '/') {			// If so, set file path property			$this->filePath = $parts['path'];			// And set thread name as path without slash			$thread_name = mb_substr ($parts['path'], 1);		} else {			// If not, set file path as a single slash			$this->filePath = '/';			// And set thread name as index			$thread_name = 'index';		}		// Check if URL has queries		if (!empty ($parts['query'])) {			// If so, filter queries in page URL			$this->filterURLQueries ($parts['query']);			// Store a string version of page URL queries			$this->urlQueries = implode ('&', $this->urlQueryList);			// Store a string version of thread name URL queries			$this->threadQueries = implode ('&', $this->threadQueryList);			// And add queries to thread name			$thread_name .= '-' . $this->threadQueries;		}		// Encode HTML characters in page URL		$url = htmlspecialchars ($url, false, 'UTF-8', false);		// Final URL scheme and host		$url = $parts['scheme'] . '://' . $parts['host'];		// Add optional port to URL		if (!empty ($parts['port'])) {			$url .= ':' . $parts['port'];		}		// Add file path to URL		$url .= $this->filePath;		// Add optional queries to URL		if (!empty ($this->urlQueries)) {			$url .= '?' . $this->urlQueries;		}		// Set thread directory name to page URL		$this->setThreadName ($thread_name);		// And set page URL property		$this->pageURL = $url;	}	// Sets page title	public function setPageTitle ($title = 'request')	{		// Check if we are getting title from POST/GET		if ($title === 'request') {			// If so, get sanitized POST/GET data			$title = $this->requestData ('title', '');		} else {			// If not, sanitize given title			$title = $this->sanitizeData ($title);		}		// And set page title		$this->pageTitle = $title;	}	// Sets instance number	public function setInstance ($instance = 'request')	{		// Request instance if told to		if ($instance === 'request') {			$instance = $this->getRequest ('instance', '');		}		// Throw exception if given instance is not numeric		if (!is_numeric ($instance)) {			throw new \Exception (				'Instance must be a number.'			);		}		// Otherwise, cast instance to actual integer		$this->instanceNumber = (int)($instance);	}}