<?php
/*
** Job Arranger Manager
** Copyright (C) 2023 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** Licensed to the Apache Software Foundation (ASF) under one or more
** contributor license agreements. See the NOTICE file distributed with
** this work for additional information regarding copyright ownership.
** The ASF licenses this file to you under the Apache License, Version 2.0
** (the "License"); you may not use this file except in compliance with
** the License. You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
**
**/

namespace App\Controllers;

use App\Utils\Controller;

class SetupRequire extends Controller
{

	const MIN_PHP_VERSION = '8.0.0';
	const MAX_PHP_VERSION = '8.3.8';
	const MIN_PHP_MEMORY_LIMIT = '134217728'; // 128 * ZBX_MEBIBYTE;
	const MIN_PHP_POST_MAX_SIZE = '16777216'; // 16 * ZBX_MEBIBYTE;
	const MIN_PHP_UPLOAD_MAX_FILESIZE = '2097152'; // 2 * ZBX_MEBIBYTE;
	const MIN_PHP_MAX_EXECUTION_TIME = 300;
	const MIN_PHP_MAX_INPUT_TIME = 300;
	const MIN_PHP_GD_VERSION = '2.0';
	const MIN_PHP_LIBXML_VERSION = '2.6.15';
	const REQUIRED_PHP_ARG_SEPARATOR_OUTPUT = '&';

	/**
	 * Check OK, setup can continue.
	 */
	const CHECK_OK = 1;

	/**
	 * Check failed, but setup can still continue. A warning will be displayed.
	 */
	const CHECK_WARNING = 2;

	/**
	 * Check failed, setup cannot continue. An error will be displayed.
	 */
	const CHECK_FATAL = 3;

	/**
	 * Perform all requirements checks.
	 *
	 * @return array
	 */
	public function checkRequirements()
	{
		$result = [];

		$result[] = $this->checkPhpVersion();
		$result[] = $this->checkPhpMemoryLimit();
		$result[] = $this->checkPhpUploadMaxFilesize();
		$result[] = $this->checkPhpMbstring();
		if (extension_loaded('mbstring')) {
			$result[] = $this->checkPhpMbstringFuncOverload();
		}
		$result[] = $this->checkPhpLibxml();
		$result[] = $this->checkPhpXmlWriter();
		$result[] = $this->checkPhpXmlReader();
		$result[] = $this->checkPhpOpenSsl();
		$result[] = $this->checkPhpSession();
		$result[] = $this->checkPhpGettext();

		// $result[] = $this->checkPhpMaxExecutionTime();
		// $result[] = $this->checkPhpMaxInputTime();
		// $result[] = $this->checkPhpDatabases();
		// $result[] = $this->checkPhpBcmath();
		// $result[] = $this->checkPhpPostMaxSize();
		// $result[] = $this->checkPhpSockets();
		// $result[] = $this->checkPhpGd();
		// $result[] = $this->checkPhpGdPng();
		// $result[] = $this->checkPhpGdJpeg();
		// $result[] = $this->checkPhpGdGif();
		// $result[] = $this->checkPhpGdFreeType();
		// $result[] = $this->checkPhpLdapModule();
		// $result[] = $this->checkPhpCtype();
		// $result[] = $this->checkPhpSessionAutoStart();
		// $result[] = $this->checkPhpArgSeparatorOutput();
		return $result;
	}

	/**
	 * Checks for minimum required PHP version.
	 *
	 * @return array
	 */
	public function checkPhpVersion()
	{
		$check = version_compare(PHP_VERSION, self::MIN_PHP_VERSION, '>=');
		// if($check >= 0){
		// 	$check = version_compare(self::MAX_PHP_VERSION,PHP_VERSION, '>=');
		// }
		return [
			'name' => _('PHP version'),
			'current' => PHP_VERSION,
			'required' => self::MIN_PHP_VERSION,
			'result' => $check ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _s('Minimum required PHP version is %1$s. Maximum required PHP version is 8.3.X', self::MIN_PHP_VERSION)
		];
	}

	/**
	 * Checks for minimum PHP memory limit.
	 *
	 * @return array
	 */
	public function checkPhpMemoryLimit()
	{
		$current = ini_get('memory_limit');
		$check = ($current == '-1' || str2mem($current) >= self::MIN_PHP_MEMORY_LIMIT);

		return [
			'name' => _s('PHP option "%1$s"', 'memory_limit'),
			'current' => $current,
			'required' => mem2str(self::MIN_PHP_MEMORY_LIMIT),
			'result' => $check ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _s(
				'Minimum required PHP memory limit is %1$s (configuration option "memory_limit").',
				mem2str(self::MIN_PHP_MEMORY_LIMIT)
			)
		];
	}

	/**
	 * Checks for minimum PHP post max size.
	 *
	 * @return array
	 */
	public function checkPhpPostMaxSize()
	{
		$current = ini_get('post_max_size');

		return [
			'name' => _s('PHP option "%1$s"', 'post_max_size'),
			'current' => $current,
			'required' => mem2str(self::MIN_PHP_POST_MAX_SIZE),
			'result' => (str2mem($current) >= self::MIN_PHP_POST_MAX_SIZE) ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _s(
				'Minimum required size of PHP post is %1$s (configuration option "post_max_size").',
				mem2str(self::MIN_PHP_POST_MAX_SIZE)
			)
		];
	}

	/**
	 * Checks for minimum PHP upload max filesize.
	 *
	 * @return array
	 */
	public function checkPhpUploadMaxFilesize()
	{
		$current = ini_get('upload_max_filesize');

		return [
			'name' => _s('PHP option "%1$s"', 'upload_max_filesize'),
			'current' => $current,
			'required' => mem2str(self::MIN_PHP_UPLOAD_MAX_FILESIZE),
			'result' => (str2mem($current) >= self::MIN_PHP_UPLOAD_MAX_FILESIZE) ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _s(
				'Minimum required PHP upload filesize is %1$s (configuration option "upload_max_filesize").',
				mem2str(self::MIN_PHP_UPLOAD_MAX_FILESIZE)
			)
		];
	}

	/**
	 * Checks for minimum PHP max execution time.
	 *
	 * Value of "max_execution_time" is used to set up OS level timer that fires signal after specified number of
	 * seconds has passed. Handler of this signal interrupts execution. On *nix platforms this is done with call
	 * to "setitimer()" (http://linux.die.net/man/2/setitimer) and in this case integer value of "max_input_time"
	 * is used to specify how many seconds timer has to wait before sending signal:
	 * - Value "0" is valid because in this case timer is removed and will not fire and stop execution.
	 * - Value "-1" is valid because this signed integer and is used as value for "it_value.tv_sec" for "new_value"
	 *   argument in call to "setitimer()". As negative values for "tv_sec" are not allowed the call will fail. Errors
	 *   are not checked and timer is not set.
	 * - Any value bigger than "0" is valid and sets up timer to fire after specified number of seconds.
	 *
	 * @return array
	 */
	public function checkPhpMaxExecutionTime()
	{
		$current = ini_get('max_execution_time');

		$currentIsValid = ($current === '0' || $current === '-1' || $current >= self::MIN_PHP_MAX_EXECUTION_TIME);

		return [
			'name' => _s('PHP option "%1$s"', 'max_execution_time'),
			'current' => $current,
			'required' => self::MIN_PHP_MAX_EXECUTION_TIME,
			'result' => $currentIsValid ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _s(
				'Minimum required limit on execution time of PHP scripts is %1$s (configuration option "max_execution_time").',
				self::MIN_PHP_MAX_EXECUTION_TIME
			)
		];
	}

	/**
	 * Checks for minimum PHP max input time.
	 *
	 * @see checkPhpMaxExecutionTime()
	 *
	 * @return array
	 */
	public function checkPhpMaxInputTime()
	{
		$current = ini_get('max_input_time');

		$currentIsValid = ($current === '0' || $current === '-1' || $current >= self::MIN_PHP_MAX_INPUT_TIME);

		return [
			'name' => _s('PHP option "%1$s"', 'max_input_time'),
			'current' => $current,
			'required' => self::MIN_PHP_MAX_INPUT_TIME,
			'result' => $currentIsValid ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _s(
				'Minimum required limit on input parse time for PHP scripts is %1$s (configuration option "max_input_time").',
				self::MIN_PHP_MAX_INPUT_TIME
			)
		];
	}

	/**
	 * Checks for databases support.
	 *
	 * @return array
	 */
	public function checkPhpDatabases()
	{
		$current = "";

		$databases = self::getSupportedDatabases();
		foreach ($databases as $name) {
			$current .= $name . ".";
		}

		return [
			'name' => _('PHP databases support'),
			'current' => empty($current) ? _('off') : $current,
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('At least one of MySQL or PostgreSQL  should be supported.')
		];
	}

	// public static function getDatabases()
	// {
	// 	$current = "";
	// 	$databases = self::getSupportedDatabases();
	// 	foreach ($databases as $name) {
	// 		$current .='<option value="'.strtolower($name).'">'.$name.'</option>';
	// 	}
	// 	return $current;
	// }

	/**
	 * Get list of supported databases.
	 *
	 * @return array
	 */
	public static function getSupportedDatabases()
	{
		$allowed_db = [];

		if (zbx_is_callable([
			'mysqli_close', 'mysqli_connect', 'mysqli_connect_error', 'mysqli_error',
			'mysqli_fetch_assoc', 'mysqli_free_result', 'mysqli_query', 'mysqli_real_escape_string'
		])) {
			$allowed_db[ZBX_DB_MYSQL] = 'MySQL';
		}

		if (zbx_is_callable([
			'pg_close', 'pg_connect', 'pg_escape_bytea', 'pg_escape_string', 'pg_fetch_assoc',
			'pg_free_result', 'pg_last_error', 'pg_parameter_status', 'pg_query', 'pg_unescape_bytea'
		])) {
			$allowed_db[ZBX_DB_POSTGRESQL] = 'PostgreSQL';
		}

		// if (zbx_is_callable([
		// 	'oci_bind_by_name', 'oci_close', 'oci_commit', 'oci_connect', 'oci_error', 'oci_execute',
		// 	'oci_fetch_assoc', 'oci_field_type', 'oci_free_statement', 'oci_new_descriptor', 'oci_parse',
		// 	'oci_rollback'
		// ])) {
		// 	$allowed_db[ZBX_DB_ORACLE] = 'Oracle';
		// }

		return $allowed_db;
	}


	/**
	 * Verify that function exists and can be called as a function.
	 *
	 * @param array		$names
	 *
	 * @return bool
	 */
	function zbx_is_callable(array $names)
	{
		foreach ($names as $name) {
			if (!is_callable($name)) {
				return false;
			}
		}

		return true;
	}



	/**
	 * Checks for PHP bcmath extension.
	 *
	 * @return array
	 */
	public function checkPhpBcmath()
	{
		$current = function_exists('bcadd') &&
			function_exists('bccomp') &&
			function_exists('bcdiv') &&
			function_exists('bcmod') &&
			function_exists('bcmul') &&
			function_exists('bcpow') &&
			function_exists('bcpowmod') &&
			function_exists('bcscale') &&
			function_exists('bcsqrt') &&
			function_exists('bcsub');

		return [
			'name' => _('PHP bcmath'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP bcmath extension missing (PHP configuration parameter --enable-bcmath).')
		];
	}

	/**
	 * Checks for PHP mbstring extension.
	 *
	 * @return array
	 */
	public function checkPhpMbstring()
	{
		$current = extension_loaded('mbstring');

		return [
			'name' => _('PHP mbstring'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP mbstring extension missing (PHP configuration parameter --enable-mbstring).')
		];
	}

	/**
	 * Checks for PHP mbstring.func_overload value.
	 *
	 * Note: disabling mbstring functions completely, mbstring.func_overload returns false.
	 * checkPhpMbstringFuncOverload() will be called after successful checkPhpMbstring(), to avoid duplicate
	 * error messages. mbstring.func_overload value in php.ini file represents a combination of bitmasks.
	 *
	 * @return array
	 */
	public function checkPhpMbstringFuncOverload()
	{
		$current = ini_get('mbstring.func_overload');

		return [
			'name' => _s('PHP option "%1$s"', 'mbstring.func_overload'),
			'current' => ($current & 2) ? _('on') : _('off'),
			'required' => _('off'),
			'result' => ($current & 2) ? self::CHECK_FATAL : self::CHECK_OK,
			'error' => _('PHP string function overloading must be disabled.')
		];
	}

	/**
	 * Checks for PHP sockets extension.
	 *
	 * @return array
	 */
	public function checkPhpSockets()
	{
		$current = function_exists('socket_create');

		return [
			'name' => _('PHP sockets'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP sockets extension missing (PHP configuration parameter --enable-sockets).')
		];
	}

	/**
	 * Checks for PHP GD extension.
	 *
	 * @return array
	 */
	public function checkPhpGd()
	{
		if (is_callable('gd_info')) {
			$gdInfo = gd_info();
			preg_match('/(\d\.?)+/', $gdInfo['GD Version'], $current);
			$current = $current[0];
		} else {
			$current = _('unknown');
		}
		$check = version_compare($current, self::MIN_PHP_GD_VERSION, '>=');

		return [
			'name' => _('PHP gd'),
			'current' => $current,
			'required' => self::MIN_PHP_GD_VERSION,
			'result' => $check ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP gd extension missing (PHP configuration parameter --with-gd).')
		];
	}

	/**
	 * Checks for PHP GD PNG support.
	 *
	 * @return array
	 */
	public function checkPhpGdPng()
	{
		if (is_callable('gd_info')) {
			$gdInfo = gd_info();
			$current = $gdInfo['PNG Support'];
		} else {
			$current = false;
		}

		return [
			'name' => _('PHP gd PNG support'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP gd PNG image support missing.')
		];
	}

	/**
	 * Checks for PHP GD JPEG support.
	 *
	 * @return array
	 */
	public function checkPhpGdJpeg()
	{
		if (is_callable('gd_info')) {
			$gd_info = gd_info();
			$current = $gd_info['JPEG Support'];
		} else {
			$current = false;
		}

		return [
			'name' => _('PHP gd JPEG support'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP gd JPEG image support missing.')
		];
	}

	/**
	 * Checks for PHP GD GIF support.
	 *
	 * @return array
	 */
	public function checkPhpGdGif()
	{
		$supported = (is_callable('imagetypes') && imagetypes() & IMG_GIF);

		return [
			'name' => _('PHP gd GIF support'),
			'current' => $supported ? _('on') : _('off'),
			'required' => null,
			'result' => $supported ? self::CHECK_OK : self::CHECK_WARNING,
			'error' => _('PHP gd GIF image support missing.')
		];
	}

	/**
	 * Checks for PHP GD FreeType support.
	 *
	 * @return array
	 */
	public function checkPhpGdFreeType()
	{
		if (is_callable('gd_info')) {
			$gdInfo = gd_info();
			$current = $gdInfo['FreeType Support'];
		} else {
			$current = false;
		}

		return [
			'name' => _('PHP gd FreeType support'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP gd FreeType support missing.')
		];
	}

	/**
	 * Checks for PHP libxml extension.
	 *
	 * @return array
	 */
	public function checkPhpLibxml()
	{
		if (defined('LIBXML_DOTTED_VERSION')) {
			$current = constant('LIBXML_DOTTED_VERSION');
		} else {
			$current = _('unknown');
		}
		$check = version_compare($current, self::MIN_PHP_LIBXML_VERSION, '>=');

		return [
			'name' => _('PHP libxml'),
			'current' => $current,
			'required' => self::MIN_PHP_LIBXML_VERSION,
			'result' => $check ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP libxml extension missing.')
		];
	}

	/**
	 * Checks for PHP xmlwriter extension.
	 *
	 * @return array
	 */
	public function checkPhpXmlWriter()
	{
		$current = extension_loaded('xmlwriter');

		return [
			'name' => _('PHP xmlwriter'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP xmlwriter extension missing.')
		];
	}

	/**
	 * Checks for PHP xmlreader extension.
	 *
	 * @return array
	 */
	public function checkPhpXmlReader()
	{
		$current = extension_loaded('xmlreader');

		return [
			'name' => _('PHP xmlreader'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP xmlreader extension missing.')
		];
	}

	/**
	 * Checks for PHP LDAP extension.
	 *
	 * @return array
	 */
	// public function checkPhpLdapModule() {
	// 	$current = (new CLdap())->error !== CLdap::ERR_PHP_EXTENSION;

	// 	return [
	// 		'name' => _('PHP LDAP'),
	// 		'current' => $current ? _('on') : _('off'),
	// 		'required' => null,
	// 		'result' => $current ? self::CHECK_OK : self::CHECK_WARNING,
	// 		'error' => _('PHP LDAP extension missing.')
	// 	];
	// }

	/**
	 * Checks for PHP OpenSSL extension.
	 *
	 * @return array
	 */
	public function checkPhpOpenSsl()
	{
		$current = extension_loaded('openssl');

		return [
			'name' => _('PHP OpenSSL'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_WARNING,
			'error' => _('PHP OpenSSL extension missing.')
		];
	}

	/**
	 * Checks for PHP ctype extension.
	 *
	 * @return array
	 */
	public function checkPhpCtype()
	{
		$current = function_exists('ctype_alnum') &&
			function_exists('ctype_alpha') &&
			function_exists('ctype_cntrl') &&
			function_exists('ctype_digit') &&
			function_exists('ctype_graph') &&
			function_exists('ctype_lower') &&
			function_exists('ctype_print') &&
			function_exists('ctype_punct') &&
			function_exists('ctype_space') &&
			function_exists('ctype_xdigit') &&
			function_exists('ctype_upper');

		return [
			'name' => _('PHP ctype'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP ctype extension missing (PHP configuration parameter --enable-ctype).')
		];
	}

	/**
	 * Checks for PHP session extension.
	 *
	 * @return array
	 */
	public function checkPhpSession()
	{
		$current = (function_exists('session_start') && function_exists('session_write_close'));

		return [
			'name' => _('PHP session'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP session extension missing (PHP configuration parameter --enable-session).')
		];
	}

	/**
	 * Checks for PHP session auto start.
	 *
	 * @return array
	 */
	public function checkPhpSessionAutoStart()
	{
		$current = !ini_get('session.auto_start');

		return [
			'name' => _s('PHP option "%1$s"', 'session.auto_start'),
			'current' => $current ? _('off') : _('on'),
			'required' => _('off'),
			'result' => $current ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _('PHP session auto start must be disabled (PHP directive "session.auto_start").')
		];
	}

	/**
	 * Checks for PHP gettext extension.
	 *
	 * @return array
	 */
	public function checkPhpGettext()
	{
		$current = function_exists('bindtextdomain');

		return [
			'name' => _('PHP gettext'),
			'current' => $current ? _('on') : _('off'),
			'required' => null,
			'result' => $current ? self::CHECK_OK : self::CHECK_WARNING,
			'error' => _('PHP gettext extension missing (PHP configuration parameter --with-gettext). Translations will not be available.')
		];
	}

	/**
	 * Checks for arg_separator.output
	 *
	 * @return array
	 */
	public function checkPhpArgSeparatorOutput()
	{
		$current = ini_get('arg_separator.output');

		return [
			'name' => _s('PHP option "%1$s"', 'arg_separator.output'),
			'current' => htmlspecialchars($current),
			'required' => htmlspecialchars(self::REQUIRED_PHP_ARG_SEPARATOR_OUTPUT),
			'result' => ($current === self::REQUIRED_PHP_ARG_SEPARATOR_OUTPUT) ? self::CHECK_OK : self::CHECK_FATAL,
			'error' => _s(
				'PHP option "%1$s" must be set to "%2$s"',
				'arg_separator.output',
				self::REQUIRED_PHP_ARG_SEPARATOR_OUTPUT
			)
		];
	}

	/**
	 * Checks for the SSL parameters point to files that are open for writing.
	 *
	 * @return array
	 */
	public function checkSslFiles()
	{
		global $DB;
		$writeable = [];

		foreach (['KEY_FILE', 'CERT_FILE', 'CA_FILE'] as $key) {
			if ($DB[$key] !== '' && is_writable($DB[$key])) {
				$writeable[] = $key;
			}
		}

		return [
			'name' => _('Database TLS certificate file'),
			'current' => implode(', ', $writeable),
			'required' => null,
			'result' => $writeable ? self::CHECK_FATAL : self::CHECK_OK,
			'error' => _s('Database TLS certificate files must be read-only')
		];
	}
}
