<?php
namespace Froxlor\Api;

/**
 * This file is part of the Froxlor project.
 * Copyright (c) 2010 the Froxlor Team (see authors).
 *
 * For the full copyright and license information, please view the COPYING
 * file that was distributed with this source code. You can also view the
 * COPYING file online at http://files.froxlor.org/misc/COPYING.txt
 *
 * @copyright (c) the authors
 * @author Froxlor team <team@froxlor.org> (2010-)
 * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
 * @package API
 * @since 0.10.0
 *       
 */
abstract class ApiCommand extends ApiParameter
{

	/**
	 * debug flag
	 *
	 * @var boolean
	 */
	private $debug = false;

	/**
	 * is admin flag
	 *
	 * @var boolean
	 */
	private $is_admin = false;

	/**
	 * internal user data array
	 *
	 * @var array
	 */
	private $user_data = null;

	/**
	 * logger interface
	 *
	 * @var \Froxlor\FroxlorLogger
	 */
	private $logger = null;

	/**
	 * mail interface
	 *
	 * @var \Froxlor\System\Mailer
	 */
	private $mail = null;

	/**
	 * whether the call is an internal one or not
	 *
	 * @var boolean
	 */
	private $internal_call = false;

	/**
	 * language strings array
	 *
	 * @var array
	 */
	protected $lng = null;

	/**
	 * froxlor version
	 *
	 * @var string
	 */
	protected $version = null;

	/**
	 * froxlor dbversion
	 *
	 * @var int
	 */
	protected $dbversion = null;

	/**
	 * froxlor version-branding
	 *
	 * @var string
	 */
	protected $branding = null;

	/**
	 *
	 * @param array $header
	 *        	optional, passed via API
	 * @param array $params
	 *        	optional, array of parameters (var=>value) for the command
	 * @param array $userinfo
	 *        	optional, passed via WebInterface (instead of $header)
	 * @param boolean $internal
	 *        	optional whether called internally, default false
	 *        	
	 * @throws \Exception
	 */
	public function __construct($header = null, $params = null, $userinfo = null, $internal = false)
	{
		parent::__construct($params);

		$this->version = \Froxlor\Froxlor::VERSION;
		$this->dbversion = \Froxlor\Froxlor::DBVERSION;
		$this->branding = \Froxlor\Froxlor::BRANDING;

		if (! empty($header)) {
			$this->readUserData($header);
		} elseif (! empty($userinfo)) {
			$this->user_data = $userinfo;
			$this->is_admin = (isset($userinfo['adminsession']) && $userinfo['adminsession'] == 1 && $userinfo['adminid'] > 0) ? true : false;
		} else {
			throw new \Exception("Invalid user data", 500);
		}
		$this->logger = \Froxlor\FroxlorLogger::getInstanceOf($this->user_data);

		// check whether the user is deactivated
		if ($this->getUserDetail('deactivated') == 1) {
			$this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_INFO, "[API] User '" . $this->getUserDetail('loginnname') . "' tried to use API but is deactivated");
			throw new \Exception("Account suspended", 406);
		}

		$this->initLang();

		/**
		 * Initialize the mailingsystem
		 */
		$this->mail = new \Froxlor\System\Mailer(true);

		if ($this->debug) {
			$this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_DEBUG, "[API] " . get_called_class() . ": " . json_encode($params, JSON_UNESCAPED_SLASHES));
		}

		// set internal call flag
		$this->internal_call = $internal;
	}

	/**
	 * initialize global $lng variable to have
	 * localized strings available for the ApiCommands
	 */
	private function initLang()
	{
		global $lng;

		// query the whole table
		$result_stmt = \Froxlor\Database\Database::query("SELECT * FROM `" . TABLE_PANEL_LANGUAGE . "`");

		$langs = array();
		// presort languages
		while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
			$langs[$row['language']][] = $row;
		}

		// set default language before anything else to
		// ensure that we can display messages
		$language = \Froxlor\Settings::Get('panel.standardlanguage');

		if (isset($this->user_data['language']) && isset($langs[$this->user_data['language']])) {
			// default: use language from session, #277
			$language = $this->user_data['language'];
		} elseif (isset($this->user_data['def_language'])) {
			$language = $this->user_data['def_language'];
		}

		// include every english language file we can get
		foreach ($langs['English'] as $value) {
			include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/' . $value['file']);
		}

		// now include the selected language if its not english
		if ($language != 'English') {
			if (isset($langs[$language])) {
				foreach ($langs[$language] as $value) {
					include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/' . $value['file']);
				}
			} else {
				if ($this->debug) {
					$this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_DEBUG, "[API] unable to include user-language '" . $language . "'. Not found in database.", 404);
				}
			}
		}

		// last but not least include language references file
		include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/lng/lng_references.php');

		// set array for ApiCommand
		$this->lng = $lng;
	}

	/**
	 * returns an instance of the wanted ApiCommand (e.g.
	 * Customers, Domains, etc);
	 * this is used widely in the WebInterface
	 *
	 * @param array $userinfo
	 *        	array of user-data
	 * @param array $params
	 *        	array of parameters for the command
	 * @param boolean $internal
	 *        	optional whether called internally, default false
	 *        	
	 * @return ApiCommand
	 * @throws \Exception
	 */
	public static function getLocal($userinfo = null, $params = null, $internal = false)
	{
		return new static(null, $params, $userinfo, $internal);
	}

	/**
	 * admin flag
	 *
	 * @return boolean
	 */
	protected function isAdmin()
	{
		return $this->is_admin;
	}

	/**
	 * internal call flag
	 *
	 * @return boolean
	 */
	protected function isInternal()
	{
		return $this->internal_call;
	}

	/**
	 * return field from user-table
	 *
	 * @param string $detail
	 *
	 * @return string
	 */
	protected function getUserDetail($detail = null)
	{
		return (isset($this->user_data[$detail]) ? $this->user_data[$detail] : null);
	}

	/**
	 * return user-data array
	 *
	 * @return array
	 */
	protected function getUserData()
	{
		return $this->user_data;
	}

	/**
	 * return SQL when parameter $sql_search is given via API
	 *
	 * @param array $sql_search
	 *        	optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
	 * @param array $query_fields
	 *        	optional array of placeholders mapped to the actual value which is used in the API commands when executing the statement [internal]
	 * @param boolean $append
	 *        	optional append to WHERE clause rather then create new one, default false [internal]
	 *        	
	 * @return string
	 */
	protected function getSearchWhere(&$query_fields = array(), $append = false)
	{
		$search = $this->getParam('sql_search', true, array());
		$condition = '';
		if (! empty($search)) {
			if ($append == true) {
				$condition = ' AND ';
			} else {
				$condition = ' WHERE ';
			}
			$ops = array(
				'<',
				'>',
				'='
			);
			$first = true;
			foreach ($search as $field => $valoper) {
				$cleanfield = str_replace(".", "", $field);
				$sortfield = explode('.', $field);
				foreach ($sortfield as $id => $sfield) {
					if (substr($sfield, - 1, 1) != '`') {
						$sfield .= '`';
					}
					if ($sfield[0] != '`') {
						$sfield = '`' . $sfield;
					}
					$sortfield[$id] = $sfield;
				}
				$field = implode('.', $sortfield);
				if (! $first) {
					$condition .= ' AND ';
				}
				if (! is_array($valoper) || ! isset($valoper['op']) || empty($valoper['op'])) {
					$condition .= $field . ' LIKE :' . $cleanfield;
					if (! is_array($valoper)) {
						$query_fields[':' . $cleanfield] = '%' . $valoper . '%';
					} else {
						$query_fields[':' . $cleanfield] = '%' . $valoper['value'] . '%';
					}
				} elseif (in_array($valoper['op'], $ops)) {
					$condition .= $field . ' ' . $valoper['op'] . ':' . $cleanfield;
					$query_fields[':' . $cleanfield] = $valoper['value'] ?? '';
				} else {
					continue;
				}
				if ($first) {
					$first = false;
				}
			}
		}
		return $condition;
	}

	/**
	 * return LIMIT clause when at least $sql_limit parameter is given via API
	 *
	 * @param int $sql_limit
	 *        	optional, limit resultset, default 0
	 * @param int $sql_offset
	 *        	optional, offset for limitation, default 0
	 *        	
	 * @return string
	 */
	protected function getLimit()
	{
		$limit = $this->getParam('sql_limit', true, 0);
		$offset = $this->getParam('sql_offset', true, 0);

		if (! is_numeric($limit)) {
			$limit = 0;
		}
		if (! is_numeric($offset)) {
			$offset = 0;
		}

		if ($limit > 0) {
			return ' LIMIT ' . $offset . ',' . $limit;
		}

		return '';
	}

	/**
	 * return ORDER BY clause if parameter $sql_orderby parameter is given via API
	 *
	 * @param array $sql_orderby
	 *        	optional array with index = fieldname and value = ASC|DESC
	 * @param boolean $append
	 *        	optional append to ORDER BY clause rather then create new one, default false [internal]
	 *        	
	 * @return string
	 */
	protected function getOrderBy($append = false)
	{
		$orderby = $this->getParam('sql_orderby', true, array());
		$order = "";
		if (! empty($orderby)) {
			if ($append) {
				$order .= ", ";
			} else {
				$order .= " ORDER BY ";
			}

			$nat_fields = [
				'`c`.`loginname`',
				'`a`.`loginname`',
				'`adminname`',
				'`databasename`',
				'`username`'
			];

			foreach ($orderby as $field => $by) {
				$sortfield = explode('.', $field);
				foreach ($sortfield as $id => $sfield) {
					if (substr($sfield, - 1, 1) != '`') {
						$sfield .= '`';
					}
					if ($sfield[0] != '`') {
						$sfield = '`' . $sfield;
					}
					$sortfield[$id] = $sfield;
				}
				$field = implode('.', $sortfield);
				$by = strtoupper($by);
				if (! in_array($by, [
					'ASC',
					'DESC'
				])) {
					$by = 'ASC';
				}
				if (\Froxlor\Settings::Get('panel.natsorting') == 1 && in_array($field, $nat_fields)) {
					// Acts similar to php's natsort(), found in one comment at http://my.opera.com/cpr/blog/show.dml/160556
					$order .= "CONCAT( IF( ASCII( LEFT( " . $field . ", 5 ) ) > 57,
					LEFT( " . $field . ", 1 ), 0 ),
					IF( ASCII( RIGHT( " . $field . ", 1 ) ) > 57,
						LPAD( " . $field . ", 255, '0' ),
						LPAD( CONCAT( " . $field . ", '-' ), 255, '0' )
					)) " . $by . ", ";
				} else {
					$order .= $field . " " . $by . ", ";
				}
			}
			$order = substr($order, 0, - 2);
		}

		return $order;
	}

	/**
	 * return logger instance
	 *
	 * @return \Froxlor\FroxlorLogger
	 */
	protected function logger()
	{
		return $this->logger;
	}

	/**
	 * return mailer instance
	 *
	 * @return \Froxlor\System\Mailer
	 */
	protected function mailer()
	{
		return $this->mail;
	}

	/**
	 * call an api-command internally
	 *
	 * @param string $command
	 * @param array|null $params
	 * @param boolean $internal
	 *        	optional whether called internally, default false
	 *        	
	 *        	
	 * @return array
	 */
	protected function apiCall($command = null, $params = null, $internal = false)
	{
		$_command = explode(".", $command);
		$module = __NAMESPACE__ . "\Commands\\" . $_command[0];
		$function = $_command[1];
		$json_result = $module::getLocal($this->getUserData(), $params, $internal)->{$function}();
		return json_decode($json_result, true)['data'];
	}

	/**
	 * return api-compatible response in JSON format and send corresponding http-header
	 *
	 * @param int $status
	 * @param string $status_message
	 * @param mixed $data
	 *
	 * @return string json-encoded response message
	 */
	protected function response($status, $status_message, $data = null)
	{
		if (isset($_SERVER["SERVER_PROTOCOL"]) && ! empty($_SERVER["SERVER_PROTOCOL"])) {
			$resheader = $_SERVER["SERVER_PROTOCOL"] . " " . $status;
			if (! empty($status_message)) {
				$resheader .= ' ' . str_replace("\n", " ", $status_message);
			}
			header($resheader);
		}

		$response = array();
		$response['status'] = $status;
		$response['status_message'] = $status_message;
		$response['data'] = $data;

		$json_response = json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
		return $json_response;
	}

	/**
	 * returns an array of customers the current user can access
	 *
	 * @param string $customer_hide_option
	 *        	optional, when called as customer, some options might be hidden due to the panel.customer_hide_options ettings
	 *        	
	 * @throws \Exception
	 * @return array
	 */
	protected function getAllowedCustomerIds($customer_hide_option = '')
	{
		$customer_ids = array();
		if ($this->isAdmin()) {
			// if we're an admin, list all ftp-users of all the admins customers
			// or optionally for one specific customer identified by id or loginname
			$customerid = $this->getParam('customerid', true, 0);
			$loginname = $this->getParam('loginname', true, '');

			if (! empty($customerid) || ! empty($loginname)) {
				$_result = $this->apiCall('Customers.get', array(
					'id' => $customerid,
					'loginname' => $loginname
				));
				$custom_list_result = array(
					$_result
				);
			} else {
				$_custom_list_result = $this->apiCall('Customers.listing');
				$custom_list_result = $_custom_list_result['list'];
			}
			foreach ($custom_list_result as $customer) {
				$customer_ids[] = $customer['customerid'];
			}
		} else {
			if (!$this->isInternal() && ! empty($customer_hide_option) && \Froxlor\Settings::IsInList('panel.customer_hide_options', $customer_hide_option)) {
				throw new \Exception("You cannot access this resource", 405);
			}
			$customer_ids = array(
				$this->getUserDetail('customerid')
			);
		}
		if (empty($customer_ids)) {
			throw new \Exception("Required resource unsatisfied.", 405);
		}
		return $customer_ids;
	}

	/**
	 * returns an array of customer data for customer, or by customer-id/loginname for admin/reseller
	 *
	 * @param int $customerid
	 *        	optional, required if loginname is empty
	 * @param string $loginname
	 *        	optional, required of customerid is empty
	 * @param string $customer_resource_check
	 *        	optional, when called as admin, check the resources of the target customer
	 *        	
	 * @throws \Exception
	 * @return array
	 */
	protected function getCustomerData($customer_resource_check = '')
	{
		if ($this->isAdmin()) {
			$customerid = $this->getParam('customerid', true, 0);
			$loginname = $this->getParam('loginname', true, '');
			$customer = $this->apiCall('Customers.get', array(
				'id' => $customerid,
				'loginname' => $loginname
			));
			// check whether the customer has enough resources
			if (! empty($customer_resource_check) && $customer[$customer_resource_check . '_used'] >= $customer[$customer_resource_check] && $customer[$customer_resource_check] != '-1') {
				throw new \Exception("Customer has no more resources available", 406);
			}
		} else {
			$customer = $this->getUserData();
		}
		return $customer;
	}

	/**
	 * increase/decrease a resource field for customers/admins
	 *
	 * @param string $table
	 * @param string $keyfield
	 * @param int $key
	 * @param string $operator
	 * @param string $resource
	 * @param string $extra
	 * @param int $step
	 */
	protected static function updateResourceUsage($table = null, $keyfield = null, $key = null, $operator = '+', $resource = null, $extra = null, $step = 1)
	{
		$stmt = \Froxlor\Database\Database::prepare("
			UPDATE `" . $table . "`
			SET `" . $resource . "` = `" . $resource . "` " . $operator . " " . (int) $step . " " . $extra . "
			WHERE `" . $keyfield . "` = :key
		");
		\Froxlor\Database\Database::pexecute($stmt, array(
			'key' => $key
		), true, true);
	}

	/**
	 * return email template content from database or global language file if not found in DB
	 *
	 * @param array $customerdata
	 * @param string $group
	 * @param string $varname
	 * @param array $replace_arr
	 * @param string $default
	 *
	 * @return string
	 */
	protected function getMailTemplate($customerdata = null, $group = null, $varname = null, $replace_arr = array(), $default = "")
	{
		// get template
		$stmt = \Froxlor\Database\Database::prepare("
			SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid`= :adminid
			AND `language`= :lang AND `templategroup`= :group AND `varname`= :var
		");
		$result = \Froxlor\Database\Database::pexecute_first($stmt, array(
			"adminid" => $customerdata['adminid'],
			"lang" => $customerdata['def_language'],
			"group" => $group,
			"var" => $varname
		), true, true);
		$content = $default;
		if ($result) {
			$content = $result['value'] ?? $default;
		}
		// @fixme html_entity_decode
		$content = html_entity_decode(\Froxlor\PhpHelper::replaceVariables($content, $replace_arr));
		return $content;
	}

	/**
	 * read user data from database by api-request-header fields
	 *
	 * @param array $header
	 *        	api-request header
	 *        	
	 * @throws \Exception
	 * @return boolean
	 */
	private function readUserData($header = null)
	{
		$sel_stmt = \Froxlor\Database\Database::prepare("SELECT * FROM `api_keys` WHERE `apikey` = :ak AND `secret` = :as");
		$result = \Froxlor\Database\Database::pexecute_first($sel_stmt, array(
			'ak' => $header['apikey'],
			'as' => $header['secret']
		), true, true);
		if ($result) {
			// admin or customer?
			if ($result['customerid'] == 0 && $result['adminid'] > 0) {
				$this->is_admin = true;
				$table = 'panel_admins';
				$key = "adminid";
			} elseif ($result['customerid'] > 0 && $result['adminid'] > 0) {
				$this->is_admin = false;
				$table = 'panel_customers';
				$key = "customerid";
			} else {
				// neither adminid is > 0 nor customerid is > 0 - sorry man, no way
				throw new \Exception("Invalid API credentials", 400);
			}
			$sel_stmt = \Froxlor\Database\Database::prepare("SELECT * FROM `" . $table . "` WHERE `" . $key . "` = :id");
			$this->user_data = \Froxlor\Database\Database::pexecute_first($sel_stmt, array(
				'id' => ($this->is_admin ? $result['adminid'] : $result['customerid'])
			), true, true);
			if ($this->is_admin) {
				$this->user_data['adminsession'] = 1;
			}
			return true;
		}
		throw new \Exception("Invalid API credentials", 400);
	}
}
