<?php namespace HashOver;

// Copyright (C) 2010-2019 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 Misc
{
	// Allowed JavaScript constructors
	protected static $objects = array (
		'HashOver',
		'HashOverCountLink',
		'HashOverLatest'
	);

	// XSS-unsafe characters to search for
	protected static $searchXSS = array (
		'&',
		'<',
		'>',
		'"',
		"'",
		'/',
		'\\'
	);

	// XSS-safe replacement character entities
	protected static $replaceXSS = array (
		'&amp;',
		'&lt;',
		'&gt;',
		'&quot;',
		'&#x27;',
		'&#x2F;',
		'&#92;'
	);

	// Return JSON or JSONP function call
	public static function jsonData ($data, $self_error = false)
	{
		// Encode JSON data
		$json = json_encode ($data);

		// Return JSON as-is if the request isn't for JSONP
		if (!isset ($_GET['jsonp']) or !isset ($_GET['jsonp_object'])) {
			return $json;
		}

		// Otherwise, make JSONP callback index XSS safe
		$index = self::makeXSSsafe ($_GET['jsonp']);

		// Make JSONP object constructor name XSS safe
		$object = self::makeXSSsafe ($_GET['jsonp_object']);

		// Check if constructor is allowed, if not use default
		$allowed_object = in_array ($object, self::$objects, true);
		$object = $allowed_object ? $object : 'HashOver';

		// Check if the JSONP index contains a numeric value
		if (is_numeric ($index) or $self_error === true) {
			// If so, cast index to positive integer
			$index = ($self_error === true) ? 0 : (int)(abs ($index));

			// Construct JSONP function call
			$jsonp = sprintf ('%s.jsonp[%d] (%s);', $object, $index, $json);

			// And return the JSONP script
			return $jsonp;
		}

		// Otherwise, return an error
		return self::jsonData (array (
			'message' => 'JSONP index must have a numeric value.',
			'type' => 'error'
		), true);
	}

	// Makes a string XSS-safe by removing harmful characters
	public static function makeXSSsafe ($string)
	{
		return str_replace (self::$searchXSS, self::$replaceXSS, $string);
	}

	// Returns error in HTML paragraph
	public static function displayError ($error = 'Something went wrong!', $mode = 'php')
	{
		// Initial error message data
		$data = array ();

		// Make error message XSS safe
		$xss_safe = self::makeXSSsafe ($error);

		// Treat JSONP as JavaScript
		if ($mode === 'json' and isset ($_GET['jsonp'])) {
			$mode = 'javascript';
		}

		// Decide how to display error
		switch ($mode) {
			// Minimal JavaScript to display error message on page
			case 'javascript': {
				$data[] = 'var hashover = document.getElementById (\'hashover\') || document.body;';
				$data[] = 'var error = \'<p><b>HashOver</b>: ' . $xss_safe . '</p>\';' . PHP_EOL;
				$data[] = 'hashover.innerHTML += error;';

				break;
			}

			// RSS XML to indicate error
			case 'rss': {
				$data[] = '<?xml version="1.0" encoding="UTF-8"?>';
				$data[] = '<error>HashOver: ' . $xss_safe . '</error>';

				break;
			}

			// JSON to indicate error
			case 'json': {
				$data[] = self::jsonData (array (
					'message' => $error,
					'type' => 'error'
				));

				break;
			}

			// Default just return the error message
			default: {
				$data[] = '<p><b>HashOver</b>: ' . $error . '</p>';
				break;
			}
		}

		// Convert error message data to string
		$message = implode (PHP_EOL, $data);

		return $message;
	}

	// Returns error in HTML paragraph
	public static function displayException (\Exception $error, $mode = 'php')
	{
		return self::displayError ($error->getMessage (), $mode);
	}

	// Returns an array item or a given default value
	public static function getArrayItem (array $data, $key)
	{
		return !empty ($data[$key]) ? $data[$key] : false;
	}
}