| <?php
/**
 *
 * Simple password manager written in PHP with Bootstrap and PDO database connections
 *
 *  File name: functions.lib.php
 *  Last Modified: 4.01.23 ?., 23:56 ?.
 *
 *  @link          https://blacktiehost.com
 *  @since         1.0.0
 *  @version       2.4.0
 *  @author        Milen Karaganski <[email protected] >
 *
 *  @license       GPL-3.0+
 *  @license       http://www.gnu.org/licenses/gpl-3.0.txt
 *  @copyright     Copyright (c)  2020 - 2022 blacktiehost.com
 *
 */
/**
 * \file        functions.lib.php
 * \ingroup     Password Manager
 * \brief       File to hold global functions
 */
declare(strict_types=1);
const PM_LOG_EMERG = 0;
const PM_LOG_ALERT = 1;
const PM_LOG_CRIT = 2;
const PM_LOG_ERR = 3;
const PM_LOG_WARNING = 4;
const PM_LOG_NOTICE = 5;
const PM_LOG_INFO = 6;
const PM_LOG_DEBUG = 7;
/**
 *  Write log message into outputs. Possible outputs can be:
 *  This must not use any call to other function calling pm_syslog (avoid infinite loop).
 *
 * @param string $message                         Line to log. ''=Show nothing
 * @param int    $level                           Log level
 *                                                On Windows PM_LOG_ERR=4, PM_LOG_WARNING=5, PM_LOG_NOTICE=PM_LOG_INFO=6, PM_LOG_DEBUG=6
 *                                                On Linux   PM_LOG_ERR=3, PM_LOG_WARNING=4, PM_LOG_NOTICE=5, PM_LOG_INFO=6, PM_LOG_DEBUG=7
 *
 * @return    void
 * @throws Exception
 */
function pm_syslog(string $message, int $level)
{
    global $user;
    if (empty($level)) {
        $level = PM_LOG_DEBUG;
    }
    if (!empty($message)) {
        // Test log level
        $log_levels = [
            PM_LOG_EMERG   => 'EMERG',
            PM_LOG_ALERT   => 'ALERT',
            PM_LOG_CRIT    => 'CRITICAL',
            PM_LOG_ERR     => 'ERR',
            PM_LOG_WARNING => 'WARN',
            PM_LOG_NOTICE  => 'NOTICE',
            PM_LOG_INFO    => 'INFO',
            PM_LOG_DEBUG   => 'DEBUG',
        ];
        if (!array_key_exists($level, $log_levels)) {
            throw new Exception('Incorrect log level');
        }
        $data = [
            'message' => $message,
            'script'  => (isset($_SERVER['PHP_SELF']) ? basename($_SERVER['PHP_SELF'], '.php') : false),
            'level'   => $level,
            'user'    => ((is_object($user) && isset($user->id)) ? $user->username : false),
            'ip'      => false,
        ];
        $remoteip = getUserRemoteIP();
        // Get ip when page run on a web server
        if (!empty($remoteip)) {
            $data['ip'] = $remoteip;
            // This is when server run behind a reverse proxy
            if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR'] != $remoteip) {
                $data['ip'] = $_SERVER['HTTP_X_FORWARDED_FOR'] . ' -> ' . $data['ip'];
            } elseif (!empty($_SERVER['HTTP_CLIENT_IP']) && $_SERVER['HTTP_CLIENT_IP'] != $remoteip) {
                $data['ip'] = $_SERVER['HTTP_CLIENT_IP'] . ' -> ' . $data['ip'];
            }
        } elseif (!empty($_SERVER['SERVER_ADDR'])) {
            // This is when PHP session is running inside a web server but not inside a client request (example: init code of apache)
            $data['ip'] = $_SERVER['SERVER_ADDR'];
        } elseif (!empty($_SERVER['COMPUTERNAME'])) {
            // This is when PHP session is running outside a web server, like from Windows command line (Not always defined, but useful if OS defined it).
            $data['ip'] = $_SERVER['COMPUTERNAME'] . (empty($_SERVER['USERNAME']) ? '' : '@' . $_SERVER['USERNAME']);
        } elseif (!empty($_SERVER['LOGNAME'])) {
            // This is when PHP session is running outside a web server, like from Linux command line (Not always defined, but useful if OS defined it).
            $data['ip'] = '???@' . $_SERVER['LOGNAME'];
        }
        pm_export($data);
        unset($data);
    }
}
/**
 * Export the message
 *
 * @param array $content Array containing the info about the message
 *
 * @return    void
 */
function pm_export(array $content)
{
    $logfile = PM_MAIN_DOCUMENT_ROOT . '/pm-log.log';
    /*
        //Unlock file for writing
        if (isset($_SERVER['WINDIR'])) {
            // Host OS is Windows
            exec('attrib -R ' . escapeshellarg($logfile), $res);
            $res = $res[0];
        } else {
            // Host OS is *nix
            chmod($logfile, 0755);
        }
        */
    $filefd = fopen($logfile, 'a+');
    if (!$filefd) {
        // Do not break usage if log fails
        //throw new Exception('Failed to open log file '.basename($logfile));
        print 'Failed to open log file ' . basename($logfile);
    } else {
        $log_levels = [
            PM_LOG_EMERG   => 'EMERG',
            PM_LOG_ALERT   => 'ALERT',
            PM_LOG_CRIT    => 'CRIT',
            PM_LOG_ERR     => 'ERR',
            PM_LOG_WARNING => 'WARNING',
            PM_LOG_NOTICE  => 'NOTICE',
            PM_LOG_INFO    => 'INFO',
            PM_LOG_DEBUG   => 'DEBUG',
        ];
        $message = strftime('%Y-%m-%d %H:%M:%S', time()) . ' ' . sprintf('%-7s', $log_levels[$content['level']]) . ' ' . sprintf('%-15s', $content['ip']) . ' ' . $content['message'];
        fwrite($filefd, $message . "\n");
        fclose($filefd);
        /*
            //Lock file as read only
            if (isset($_SERVER['WINDIR'])) {
                // Host OS is Windows
                exec('attrib +R ' . escapeshellarg($logfile), $res);
                $res = $res[0];
            } else {
                // Host OS is *nix
                chmod($logfile, 0444);
            }
            */
    }
}
/**
 * Return the IP of remote user.
 * Take HTTP_X_FORWARDED_FOR (defined when using proxy)
 * Then HTTP_CLIENT_IP if defined (rare)
 * Then REMOTE_ADDR (no way to be modified by user but may be wrong if user is using a proxy)
 *
 * @return    string        Ip of remote user.
 */
function getUserRemoteIP(): string
{
    if (empty($_SERVER['HTTP_X_FORWARDED_FOR']) || preg_match('/[^0-9.:,\[\]]/', $_SERVER['HTTP_X_FORWARDED_FOR'])) {
        if (empty($_SERVER['HTTP_CLIENT_IP']) || preg_match('/[^0-9.:,\[\]]/', $_SERVER['HTTP_CLIENT_IP'])) {
            if (empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
                $ip = (empty($_SERVER['REMOTE_ADDR']) ? '' : $_SERVER['REMOTE_ADDR']);
            } else {
                $ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
            }
        } else {
            $ip = $_SERVER['HTTP_CLIENT_IP'];
        }
    } else {
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    }
    return $ip;
}
/**
 *  Return value of a param into GET or POST super variable.
 *
 * @param string   $paramname    Name of parameter to found
 * @param string   $check        Type of check
 *                               ''=no check (deprecated)
 *                               'none'=no check (only for param that should have very rich content)
 *                               'array', 'array:restricthtml' or 'array:aZ09' to check it's an array
 *                               'int'=check it's numeric (integer or float)
 *                               'intcomma'=check it's integer+comma ('1,2,3,4...')
 *                               'alpha'=Same than alphanohtml since v13
 *                               'alphawithlgt'=alpha with lgt
 *                               'alphanohtml'=check there is no html content and no " and no ../
 *                               'aZ'=check it's a-z only
 *                               'aZ09'=check it's simple alpha string (recommended for keys)
 *                               'san_alpha'=Use filter_var with FILTER_SANITIZE_STRING (do not use this for free text string)
 *                               'nohtml'=check there is no html content and no " and no ../
 *                               'restricthtml'=check html content is restricted to some tags only
 *                               'custom'= custom filter specify $filter and $options)
 * @param int      $method       Type of method (0 = get then post, 1 = only get, 2 = only post, 3 = post then get)
 * @param int|null $filter       Filter to apply when $check is set to 'custom'. (See http://php.net/manual/en/filter.filters.php for details)
 * @param mixed    $options      Options to pass to filter_var when $check is set to 'custom'
 *
 * @return string|array         Value found (string or array), or '' if check fails
 */
function GETPOST(string $paramname, string $check = 'alphanohtml', int $method = 0, int $filter = null, $options = null)
{
    if (empty($paramname)) {
        return 'BadFirstParameterForGETPOST';
    }
    if (empty($method)) {
        $out = $_GET[$paramname] ?? ($_POST[$paramname] ?? '');
    } elseif ($method == 1) {
        $out = $_GET[$paramname] ?? '';
    } elseif ($method == 2) {
        $out = $_POST[$paramname] ?? '';
    } elseif ($method == 3) {
        $out = $_POST[$paramname] ?? ($_GET[$paramname] ?? '');
    } else {
        return 'BadThirdParameterForGETPOST';
    }
    // Check rule
    if (preg_match('/^array/', $check)) {
        // If 'array' or 'array:restricthtml' or 'array:aZ09'
        if (!is_array($out) || empty($out)) {
            $out = [];
        } else {
            $tmparray = explode(':', $check);
            if (!empty($tmparray[1])) {
                $tmpcheck = $tmparray[1];
            } else {
                $tmpcheck = 'alphanohtml';
            }
            foreach ($out as $outkey => $outval) {
                $out[$outkey] = checkVal($outval, $tmpcheck, $filter, $options);
            }
        }
    } else {
        $out = checkVal($out, $check, $filter, $options);
    }
    return $out;
}
/**
 *  Return a value after checking on a rule. A sanitization may also have been done.
 *
 * @param string   $out     Value to check/clear.
 * @param string   $check   Type of check/sanitizing
 * @param int|null $filter  Filter to apply when $check is set to 'custom'. (See http://php.net/manual/en/filter.filters.php for details)
 * @param mixed    $options Options to pass to filter_var when $check is set to 'custom'
 *
 * @return string|array         Value sanitized (string or array). It may be '' if format check fails.
 */
function checkVal(string $out = '', string $check = 'alphanohtml', int $filter = null, $options = null)
{
    // Check is done after replacement
    switch ($check) {
        case 'none':
            break;
        case 'int':    // Check param is a numeric value (integer but also float or hexadecimal)
            if (!is_numeric($out)) {
                $out = '';
            }
            break;
        case 'intcomma':
            if (preg_match('/[^0-9,-]+/i', $out)) {
                $out = '';
            }
            break;
        case 'san_alpha':
            $out = filter_var($out, FILTER_SANITIZE_STRING);
            break;
        case 'email':
            $out = filter_var($out, FILTER_SANITIZE_EMAIL);
            break;
        case 'aZ':
            if (!is_array($out)) {
                $out = trim($out);
                if (preg_match('/[^a-z]+/i', $out)) {
                    $out = '';
                }
            }
            break;
        case 'aZ09':
            if (!is_array($out)) {
                $out = trim($out);
                if (preg_match('/[^a-z0-9_\-.]+/i', $out)) {
                    $out = '';
                }
            }
            break;
        case 'aZ09comma':        // great to sanitize sortfield or sortorder params that can be t.abc,t.def_gh
            if (!is_array($out)) {
                $out = trim($out);
                if (preg_match('/[^a-z0-9_\-.,]+/i', $out)) {
                    $out = '';
                }
            }
            break;
        case 'nohtml':        // No html
            $out = string_nohtmltag($out, 0);
            break;
        case 'alpha':        // No html and no ../ and "
        case 'alphanohtml':    // Recommended for most scalar parameters and search parameters
            if (!is_array($out)) {
                $out = trim($out);
                do {
                    $oldstringtoclean = $out;
                    // Remove html tags
                    $out = string_nohtmltag($out, 0);
                    $out = str_ireplace(['&', '&', '&', '"', '"', '"', '"', '"', '/', '/', '\', '\', '/', '../', '..\\'], '', $out);
                } while ($oldstringtoclean != $out);
            }
            break;
        case 'custom':
            if (empty($filter)) {
                return 'BadFourthParameterForGETPOST';
            }
            $out = filter_var($out, $filter, $options);
            break;
    }
    return $out;
}
/**
 *    Clean a string from all HTML tags and entities.
 *  This function differs from strip_tags because:
 *  - <br> are replaced with \n if removelinefeed=0 or 1
 *  - if entities are found, they are decoded BEFORE the strip
 *  - you can decide to convert line feed into a space
 *
 * @param string $stringtoclean       String to clean
 * @param int    $removelinefeed      1=Replace all new lines by 1 space, 0=Only ending new lines are removed others are replaced with \n, 2=Ending new lines are removed but
 *                                    others are kept with a same number of \n than nb of <br> when there is both "...<br>\n..."
 * @param int    $strip_tags          0=Use internal strip, 1=Use strip_tags() php function (bugged when text contains a < char that is not for a html tag or when tags are not
 *                                    closed like '<img onload=aaa')
 * @param int    $removedoublespaces  Replace double space into one space
 *
 * @return string                        String cleaned
 *
 */
function string_nohtmltag(string $stringtoclean, int $removelinefeed = 1, int $strip_tags = 0, int $removedoublespaces = 1): string
{
    if ($removelinefeed == 2) {
        $stringtoclean = preg_replace('/<br[^>]*>([\n\r])+/im', '<br>', $stringtoclean);
    }
    $temp = preg_replace('/<br[^>]*>/i', "\n", $stringtoclean);
    $temp = str_replace('< ', '__ltspace__', $temp);
    if ($strip_tags) {
        $temp = strip_tags($temp);
    } else {
        $temp = str_replace('<>', '', $temp);
        // No reason to have this into a text, except if value is to try bypass the next html cleaning
        $pattern = '/<[^<>]+>/';
        // Example of $temp: <a href="/myurl" title="<u>A title</u>">0000-021</a>
        $temp = preg_replace($pattern, '', $temp);
        // pass 1 - $temp after pass 1: <a href="/myurl" title="A title">0000-021
        $temp = preg_replace($pattern, '', $temp);
        // pass 2 - $temp after pass 2: 0000-021
        // Remove '<' into remainging, so remove non closing html tags like '<abc' or '<<abc'. Note: '<123abc' is not a html tag (can be kept), but '<abc123' is (must be removed).
        $temp = preg_replace('/<+([a-z]+)/i', '\1', $temp);
    }
    // Remove also carriage returns
    if ($removelinefeed == 1) {
        $temp = str_replace(["\r\n", "\r", "\n"], ' ', $temp);
    }
    // And double quotes
    if ($removedoublespaces) {
        while (strpos($temp, '  ')) {
            $temp = str_replace('  ', ' ', $temp);
        }
    }
    $temp = str_replace('__ltspace__', '< ', $temp);
    return trim($temp);
}
/**
 *      Return a string encoded into OS filesystem encoding. This function is used to define
 *        value to pass to filesystem PHP functions.
 *
 * @param string $str String to encode (UTF-8)
 *
 * @return    string                Encoded string (UTF-8, ISO-8859-1)
 */
function get_osencode(string $str): string
{
    $tmp = ini_get('unicode.filesystem_encoding');
    if (empty($tmp) && !empty($_SERVER['WINDIR'])) {
        $tmp = 'iso-8859-1';
    }
    if (empty($tmp)) {
        $tmp = 'utf-8';
    }
    if ($tmp == 'iso-8859-1') {
        return utf8_decode($str);
    }
    return $str;
}
/**
 * Global block to redirect on logout
 *
 * @return void
 */
function pm_logout_block()
{
    global $action;
    if ($action == 'logout') {
        session_unset();
        // Destroy the session.
        session_destroy();
        header('Location: ' . PM_MAIN_URL_ROOT);
    }
}
 |