<?php

/*
 * Copyright 2005 - 2025 Centreon (https://www.centreon.com/)
 *
 * Licensed 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
 *
 * https://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.
 *
 * For more information : contact@centreon.com
 *
 */

require_once _CENTREON_PATH_ . 'www/class/centreonDB.class.php';
require_once _CENTREON_PATH_ . 'www/class/centreonGMT.class.php';

/**
 * Class
 *
 * @class CentreonUtils
 */
class CentreonUtils
{
    /**
     * Remove all <script> data
     */
    public const ESCAPE_LEGACY_METHOD = 0;

    /**
     * Convert all html tags into HTML entities except links
     */
    public const ESCAPE_ALL_EXCEPT_LINK = 1;

    /**
     * Convert all html tags into HTML entities
     */
    public const ESCAPE_ALL = 2;

    /**
     * Remove all specific characters defined in the configuration > pollers > engine > admin, illegal characters field
     */
    public const ESCAPE_ILLEGAL_CHARS = 4;

    /**
     * Defines all self-closing html tags allowed
     *
     * @var string[]
     */
    public static $selfclosingHtmlTagsAllowed = ['br', 'hr'];

    /**
     * Converts Object into Array
     *
     * @param mixed $arrObjData
     * @param array $arrSkipIndices
     *
     * @return array|string
     */
    public function objectIntoArray($arrObjData, $arrSkipIndices = [])
    {
        $arrData = [];

        if (is_object($arrObjData)) {
            $arrObjData = get_object_vars($arrObjData);
        }

        if (is_array($arrObjData)) {
            foreach ($arrObjData as $index => $value) {
                if (is_object($value) || is_array($value)) {
                    $value = self::objectIntoArray($value, $arrSkipIndices);
                }
                if (in_array($index, $arrSkipIndices)) {
                    continue;
                }
                $arrData[$index] = $value;
            }
        }
        if ($arrData === []) {
            $arrData = '';
        }

        return $arrData;
    }

    /**
     * Condition Builder
     * Creates condition with WHERE or AND or OR
     *
     * @param string $query
     * @param string $condition
     * @param bool $or
     * @return string
     */
    public static function conditionBuilder($query, $condition, $or = false)
    {
        if (preg_match('/ WHERE /', $query)) {
            if ($or === true) {
                $query .= ' OR ';
            } else {
                $query .= ' AND ';
            }
        } else {
            $query .= ' WHERE ';
        }
        $query .= $condition . ' ';

        return $query;
    }

    /**
     * Get datetime timestamp
     *
     * @param string $datetime
     * @throws Exception
     * @return int
     */
    public static function getDateTimeTimestamp($datetime)
    {
        static $db;
        static $centreonGmt;

        $invalidString = 'Date format is not valid';
        if (! isset($db)) {
            $db = new CentreonDB();
        }
        if (! isset($centreonGmt)) {
            $centreonGmt = new CentreonGMT();
        }
        $centreonGmt->getMyGMTFromSession(session_id(), $db);
        $datetime = trim($datetime);
        $res = explode(' ', $datetime);
        if (count($res) != 2) {
            throw new Exception($invalidString);
        }
        $res1 = explode('/', $res[0]);
        if (count($res1) != 3) {
            throw new Exception($invalidString);
        }
        $res2 = explode(':', $res[1]);
        if (count($res2) != 2) {
            throw new Exception($invalidString);
        }
        $timestamp = $centreonGmt->getUTCDateFromString($datetime);

        return $timestamp;
    }

    /**
     * Convert operand to Mysql format
     *
     * @param string $str
     * @return string
     */
    public static function operandToMysqlFormat($str)
    {
        $result = '';
        switch ($str) {
            case 'gt':
                $result = '>';
                break;
            case 'lt':
                $result = '<';
                break;
            case 'gte':
                $result = '>=';
                break;
            case 'lte':
                $result = '<=';
                break;
            case 'eq':
                $result = '=';
                break;
            case 'ne':
                $result = '!=';
                break;
            case 'like':
                $result = 'LIKE';
                break;
            case 'notlike':
                $result = 'NOT LIKE';
                break;
            case 'regex':
                $result = 'REGEXP';
                break;
            case 'notregex':
                $result = 'NOT REGEXP';
                break;
            default:
                $result = '';
                break;
        }

        return $result;
    }

    /**
     * Merge with initial values
     *
     * @param HTML_QuickFormCustom $form
     * @param string $key
     *
     * @throws InvalidArgumentException
     * @return array
     */
    public static function mergeWithInitialValues($form, $key)
    {
        $init = [];
        try {
            $initForm = $form->getElement('initialValues');
            $initForm = HtmlAnalyzer::sanitizeAndRemoveTags($initForm->getValue());
            $initialValues = unserialize($initForm, ['allowed_classes' => false]);
            if (! empty($initialValues) && isset($initialValues[$key])) {
                $init = $initialValues[$key];
            }
            $result = array_merge((array) $form->getSubmitValue($key), $init);
        } catch (HTML_QuickForm_Error $e) {
            $result = (array) $form->getSubmitValue($key);
        }

        return $result;
    }

    /**
     * Transforms an array into a string with the following format
     * '1','2','3' or '' if the array is empty
     *
     * @param array $arr
     * @param bool $transformKey | string will be formed with keys when true,
     *                           otherwise values will be used
     * @return string
     */
    public static function toStringWithQuotes($arr = [], $transformKey = true)
    {
        $string = '';
        $first = true;
        foreach ($arr as $key => $value) {
            if ($first) {
                $first = false;
            } else {
                $string .= ', ';
            }
            $string .= $transformKey ? "'" . $key . "'" : "'" . $value . "'";
        }
        if ($string == '') {
            $string = "''";
        }

        return $string;
    }

    /**
     * @param string $currentVersion Original version
     * @param string $targetVersion Version to compare
     * @param string $delimiter Indicates the delimiter parameter for version
     *
     * @param int $depth Indicates the depth of comparison, if 0 it means "unlimited"
     */
    public static function compareVersion($currentVersion, $targetVersion, $delimiter = '.', $depth = 0)
    {
        $currentVersionExplode = explode($delimiter, $currentVersion);
        $targetVersionExplode = explode($delimiter, $targetVersion);
        $isCurrentSuperior = false;
        $isCurrentEqual = false;

        $maxRecursion = $depth == 0 ? count($currentVersionExplode) : $depth;

        for ($i = 0; $i < $maxRecursion; $i++) {
            if ($currentVersionExplode[$i] > $targetVersionExplode[$i]) {
                $isCurrentSuperior = true;
                $isCurrentEqual = false;
                break;
            }
            if ($currentVersionExplode[$i] < $targetVersionExplode[$i]) {
                $isCurrentSuperior = false;
                $isCurrentEqual = false;
                break;
            }
            $isCurrentEqual = true;

        }

        if ($isCurrentSuperior) {
            return 1;
        }
        if (($isCurrentSuperior === false) && $isCurrentEqual) {
            return 2;
        }

        return 0;
    }

    /**
     * Converted a HTML string according to the selected method
     *
     * @param string $stringToEscape String to escape
     * @param int $escapeMethod Escape method (default: ESCAPE_LEGACY_METHOD)
     *
     * @return string|false Escaped string
     * @see CentreonUtils::ESCAPE_LEGACY_METHOD
     * @see CentreonUtils::ESCAPE_ALL_EXCEPT_LINK
     * @see CentreonUtils::ESCAPE_ALL
     * @see CentreonUtils::ESCAPE_ILLEGAL_CHARS
     */
    public static function escapeSecure(
        $stringToEscape,
        $escapeMethod = self::ESCAPE_LEGACY_METHOD,
    ) {
        switch ($escapeMethod) {
            case self::ESCAPE_LEGACY_METHOD:
                // Remove script and input tags by default
                return preg_replace(["/<script.*?\/script>/si", "/<input[^>]+\>/si"], '', $stringToEscape ?? '');
            case self::ESCAPE_ALL_EXCEPT_LINK:
                return self::escapeAllExceptLink($stringToEscape);
            case self::ESCAPE_ALL:
                return self::escapeAll($stringToEscape);
            case self::ESCAPE_ILLEGAL_CHARS:
                $chars = (string) $_SESSION['centreon']->Nagioscfg['illegal_object_name_chars'];

                return str_replace(str_split($chars), '', $stringToEscape);
            default: return false;
        }
    }

    /**
     * Convert all html tags into HTML entities
     *
     * @param string $stringToEscape String to escape
     * @return string Converted string
     */
    public static function escapeAll($stringToEscape)
    {
        return htmlentities($stringToEscape ?? '', ENT_QUOTES, 'UTF-8');
    }

    /**
     * Protect a string and return it with single quotes around.
     *
     * This is the same behaviour as {@see PDO::quote}.
     *
     * @see https://dev.mysql.com/doc/refman/5.7/en/mysql-real-escape-string.html
     * @see https://www.php.net/manual/fr/mysqli.real-escape-string.php
     *
     * @param null|int|float|bool|string|Stringable $value
     *
     * @return string
     */
    public static function quote(null|int|float|bool|string|Stringable $value): string
    {
        $pairs = [
            "\x00" => '\x00', // \0 (ASCII 0)
            "\n" => '\n', // \n
            "\r" => '\r', // \r
            '\\' => '\\\\', // \
            "'" => "\'", // '
            '"' => '\"', // "
            "\x1a" => '\x1a', // Control-Z
        ];

        return "'" . strtr((string) $value, $pairs) . "'";
    }

    /**
     * Convert all HTML tags into HTML entities except those defined in parameter
     *
     * @param string $stringToEscape String (HTML) to escape
     * @param string[] $tagsNotToEscape List of tags not to escape
     *
     * @return string HTML escaped
     */
    public static function escapeAllExceptSelectedTags(
        $stringToEscape,
        $tagsNotToEscape = [],
    ) {
        if (! is_array($tagsNotToEscape)) {
            // Do nothing if the tag list is empty
            return $stringToEscape;
        }

        $tagOccurences = [];
        /*
         * Before to escape HTML, we will search and replace all HTML tags
         * allowed by specific tags to avoid they are processed
         */
        $counter = count($tagsNotToEscape);
        for ($indexTag = 0; $indexTag < $counter; $indexTag++) {
            $linkToken = "{{__TAG{$indexTag}x__}}";
            $currentTag = $tagsNotToEscape[$indexTag];
            if (! in_array($currentTag, self::$selfclosingHtmlTagsAllowed)) {
                // The current tag is not self-closing tag allowed
                $index = 0;
                $tagsFound = [];

                // Specific process for not self-closing HTML tags
                while ($occurence = self::getHtmlTags($currentTag, $stringToEscape)) {
                    $tagsFound[$index] = $occurence['tag'];
                    $linkTag = str_replace('x', $index, $linkToken);
                    $stringToEscape = substr_replace(
                        $stringToEscape,
                        $linkTag,
                        $occurence['start'],
                        $occurence['length']
                    );
                    $index++;
                }
            } else {
                $linkToken = '{{__' . strtoupper($currentTag) . '__}}';
                // Specific process for self-closing tag
                $stringToEscape = preg_replace(
                    '~< *(' . $currentTag . ')+ *\/?>~im',
                    $linkToken,
                    $stringToEscape
                );
                $tagsFound = ["<{$currentTag}/>"];
            }
            $tagOccurences[$linkToken] = $tagsFound;
        }

        $escapedString = htmlentities($stringToEscape, ENT_QUOTES, 'UTF-8');

        /*
         * After we escaped all unauthorized HTML tags, we will search and
         * replace all previous specifics tags by their original tag
         */
        foreach ($tagOccurences as $linkToken => $tagsFound) {
            $counter = count($tagsFound);
            for ($indexTag = 0; $indexTag < $counter; $indexTag++) {
                $linkTag = str_replace('x', $indexTag, $linkToken);
                $escapedString = str_replace($linkTag, $tagsFound[$indexTag], $escapedString);
            }
        }

        return $escapedString;
    }

    /**
     * Convert all html tags into HTML entities except links (<a>...</a>)
     *
     * @param string $stringToEscape String (HTML) to escape
     * @return string HTML escaped (except links)
     */
    public static function escapeAllExceptLink($stringToEscape)
    {
        return self::escapeAllExceptSelectedTags($stringToEscape, ['a']);
    }

    /**
     * Return all occurences of a html tag found in html string
     *
     * @param string $tag HTML tag to find
     * @param string $html HTML to analyse
     *
     * @return array (('tag'=> html tag; 'start' => start position of tag,
     *               'length'=> length between start and end of tag), ...)
     */
    public static function getHtmlTags($tag, $html)
    {
        $occurrences = false;
        $start = 0;
        if (
            ($start = stripos($html, "<{$tag}", $start)) !== false
            && ($end = stripos($html, "</{$tag}>", strlen("</{$tag}>")))
        ) {
            if (! is_array($occurrences[$tag])) {
                $occurrences[$tag] = [];
            }
            $occurrences
                = ['tag' => substr(
                    $html,
                    $start,
                    $end + strlen("</{$tag}>") - $start
                ), 'start' => $start, 'length' => $end + strlen("</{$tag}>") - $start];
        }

        return $occurrences;
    }

    /**
     * @param string $coords -90.0,180.0
     *
     * @return bool
     */
    public static function validateGeoCoords($coords): bool
    {
        return (bool) (
            preg_match(
                '/^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/',
                $coords
            )
        );
    }
}
