Subversion Repositories oidplus

Rev

Rev 1385 | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * OIDplus 2.0
  5.  * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
  6.  *
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *     http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  */
  19.  
  20. use ViaThinkSoft\OIDplus\OIDplus;
  21. use ViaThinkSoft\OIDplus\OIDplusException;
  22.  
  23. /**
  24.  * @param string $privKey
  25.  * @return bool
  26.  */
  27. function is_privatekey_encrypted(string $privKey): bool {
  28.         return strpos($privKey,'BEGIN ENCRYPTED PRIVATE KEY') !== false;
  29. }
  30.  
  31. /**
  32.  * @param string $privKey
  33.  * @param string $pubKey
  34.  * @return bool
  35.  */
  36. function verify_private_public_key(string $privKey, string $pubKey): bool {
  37.         if (!function_exists('openssl_public_encrypt')) return false;
  38.         try {
  39.                 if (empty($privKey)) return false;
  40.                 if (empty($pubKey)) return false;
  41.                 $data = generateRandomString(25);
  42.                 $encrypted = '';
  43.                 $decrypted = '';
  44.                 if (!@openssl_public_encrypt($data, $encrypted, $pubKey)) return false;
  45.                 if (!@openssl_private_decrypt($encrypted, $decrypted, $privKey)) return false;
  46.                 return $decrypted == $data;
  47.         } catch (\Exception $e) {
  48.                 return false;
  49.         }
  50. }
  51.  
  52. /**
  53.  * @param string $privKeyOld
  54.  * @param string|null $passphrase_old
  55.  * @param string|null $passphrase_new
  56.  * @return false|string
  57.  */
  58. function change_private_key_passphrase(string $privKeyOld, string $passphrase_old=null, string $passphrase_new=null) {
  59.         $pkey_config = array(
  60.             //"digest_alg" => "sha512",
  61.             //"private_key_bits" => 2048,
  62.             //"private_key_type" => OPENSSL_KEYTYPE_RSA,
  63.             "config" => class_exists(OIDplus::class) ? OIDplus::getOpenSslCnf() : @getenv('OPENSSL_CONF')
  64.         );
  65.         $privKeyNew = @openssl_pkey_get_private($privKeyOld, $passphrase_old);
  66.         if ($privKeyNew === false) return false;
  67.         if (!@openssl_pkey_export($privKeyNew, $privKeyNewExport, $passphrase_new, $pkey_config)) return false;
  68.         if ($privKeyNewExport === "") return false;
  69.         return "$privKeyNewExport";
  70. }
  71.  
  72. /**
  73.  * @param string $privKey
  74.  * @param string $passphrase
  75.  * @return false|string
  76.  */
  77. function decrypt_private_key(string $privKey, string $passphrase) {
  78.         return change_private_key_passphrase($privKey, $passphrase, null);
  79. }
  80.  
  81. /**
  82.  * @param string $privKey
  83.  * @param string $passphrase
  84.  * @return false|string
  85.  */
  86. function encrypt_private_key(string $privKey, string $passphrase) {
  87.         return change_private_key_passphrase($privKey, null, $passphrase);
  88. }
  89.  
  90. /**
  91.  * @param string $data
  92.  * @return int
  93.  */
  94. function smallhash(string $data): int { // get 31 bits from SHA1. Values 0..2147483647
  95.         return (hexdec(substr(sha1($data),-4*2)) & 0x7FFFFFFF);
  96. }
  97.  
  98. /**
  99.  * @param string $name
  100.  * @return array
  101.  */
  102. function split_firstname_lastname(string $name): array {
  103.         $ary = explode(' ', $name);
  104.         $last_name = array_pop($ary);
  105.         $first_name = implode(' ', $ary);
  106.         return array($first_name, $last_name);
  107. }
  108.  
  109. /**
  110.  * @return void
  111.  */
  112. function originHeaders() {
  113.         // CORS
  114.         // Author: Till Wehowski
  115.         // TODO: add to class OIDplus
  116.  
  117.         header("Access-Control-Allow-Credentials: true");
  118.         header("Access-Control-Allow-Origin: ".strip_tags(((isset($_SERVER['HTTP_ORIGIN'])) ? $_SERVER['HTTP_ORIGIN'] : "*")));
  119.  
  120.         header("Access-Control-Allow-Headers: If-None-Match, X-Requested-With, Origin, X-Frdlweb-Bugs, Etag, X-Forgery-Protection-Token, X-CSRF-Token");
  121.  
  122.         if (isset($_SERVER['HTTP_ORIGIN'])) {
  123.                 header('X-Frame-Options: ALLOW-FROM '.$_SERVER['HTTP_ORIGIN']);
  124.         } else {
  125.                 header_remove("X-Frame-Options");
  126.         }
  127.  
  128.         $expose = array('Etag', 'X-CSRF-Token');
  129.         foreach (headers_list() as $num => $header) {
  130.                 $h = explode(':', $header);
  131.                 $expose[] = trim($h[0]);
  132.         }
  133.         header("Access-Control-Expose-Headers: ".implode(',',$expose));
  134.  
  135.         header("Vary: Origin");
  136. }
  137.  
  138. if (!function_exists('mb_wordwrap')) {
  139.         /**
  140.          * @param string $str
  141.          * @param int $width
  142.          * @param string $break
  143.          * @param bool $cut
  144.          * @return string
  145.          */
  146.         function mb_wordwrap(string $str, int $width = 75, string $break = "\n", bool $cut = false): string {
  147.                 // https://stackoverflow.com/a/4988494/488539
  148.                 assert(strlen($break) > 0);
  149.                 $lines = explode("$break", $str);
  150.                 foreach ($lines as &$line) {
  151.                         $line = rtrim($line);
  152.                         if (mb_strlen($line) <= $width) {
  153.                                 continue;
  154.                         }
  155.                         $words = explode(' ', $line);
  156.                         $line = '';
  157.                         $actual = '';
  158.                         foreach ($words as $word) {
  159.                                 if (mb_strlen($actual.$word) <= $width) {
  160.                                         $actual .= $word.' ';
  161.                                 } else {
  162.                                         if ($actual != '') {
  163.                                                 $line .= rtrim($actual).$break;
  164.                                         }
  165.                                         $actual = $word;
  166.                                         if ($cut) {
  167.                                                 while (mb_strlen($actual) > $width) {
  168.                                                         $line .= mb_substr($actual, 0, $width).$break;
  169.                                                         $actual = mb_substr($actual, $width);
  170.                                                 }
  171.                                         }
  172.                                         $actual .= ' ';
  173.                                 }
  174.                         }
  175.                         $line .= trim($actual);
  176.                 }
  177.                 return implode($break, $lines);
  178.         }
  179. }
  180.  
  181. /**
  182.  * @param string $out
  183.  * @param string $contentType
  184.  * @param string $filename
  185.  * @return void
  186.  */
  187. function httpOutWithETag(string $out, string $contentType, string $filename='') {
  188.         $etag = md5($out);
  189.         header("Etag: $etag");
  190.         header("Content-MD5: $etag"); // RFC 2616 clause 14.15
  191.         if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && (trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag)) {
  192.                 if (PHP_SAPI != 'cli') @http_response_code(304); // 304 Not Modified
  193.         } else {
  194.                 header("Content-Type: $contentType");
  195.                 if (!empty($filename)) {
  196.                         header('Content-Disposition:inline; filename="'.$filename.'"');
  197.                 }
  198.                 echo $out;
  199.         }
  200.         die();
  201. }
  202.  
  203. /**
  204.  * @param string $str
  205.  * @param array $args
  206.  * @return string
  207.  */
  208. function my_vsprintf(string $str, array $args): string {
  209.         $n = 1;
  210.         foreach ($args as $val) {
  211.                 $str = str_replace("%$n", $val, $str);
  212.                 $n++;
  213.         }
  214.         return str_replace("%%", "%", $str);
  215. }
  216.  
  217. /**
  218.  * @param string $str
  219.  * @param mixed ...$sprintfArgs
  220.  * @return string
  221.  * @throws \ViaThinkSoft\OIDplus\OIDplusConfigInitializationException
  222.  * @throws \ViaThinkSoft\OIDplus\OIDplusException
  223.  */
  224. function _L(string $str, ...$sprintfArgs): string {
  225.         static $translation_array = array();
  226.         static $translation_loaded = null;
  227.  
  228.         $str = trim($str);
  229.  
  230.         if (!class_exists(OIDplus::class)) {
  231.                 return my_vsprintf($str, $sprintfArgs);
  232.         }
  233.  
  234.         $lang = OIDplus::getCurrentLang();
  235.         $ta = OIDplus::getTranslationArray($lang);
  236.         $res = $ta[$lang][$str] ?? $str;
  237.  
  238.         $res = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $res);
  239.  
  240.         return my_vsprintf($res, $sprintfArgs);
  241. }
  242.  
  243. /**
  244.  * @param array $params
  245.  * @param string $key
  246.  * @return void
  247.  * @throws \ViaThinkSoft\OIDplus\OIDplusConfigInitializationException
  248.  * @throws \ViaThinkSoft\OIDplus\OIDplusException
  249.  */
  250. function _CheckParamExists(array $params, string $key) {
  251.         if (class_exists(OIDplusException::class)) {
  252.                 if (!isset($params[$key])) throw new OIDplusException(_L('Parameter %1 is missing', $key));
  253.         } else {
  254.                 if (!isset($params[$key])) throw new Exception(_L('Parameter %1 is missing', $key));
  255.         }
  256. }
  257.  
  258. /**
  259.  * @param string $cont
  260.  * @return array
  261.  */
  262. function extractHtmlContents(string $cont): array {
  263.         // make sure the program works even if the user provided HTML is not UTF-8
  264.         $cont = convert_to_utf8_no_bom($cont);
  265.  
  266.         $out_js = '';
  267.         $m = array();
  268.         preg_match_all('@<script[^>]*>(.+)</script>@ismU', $cont, $m);
  269.         foreach ($m[1] as $x) {
  270.                 $out_js = $x . "\n\n";
  271.         }
  272.  
  273.         $out_css = '';
  274.         $m = array();
  275.         preg_match_all('@<style[^>]*>(.+)</style>@ismU', $cont, $m);
  276.         foreach ($m[1] as $x) {
  277.                 $out_css = $x . "\n\n";
  278.         }
  279.  
  280.         $out_html = $cont;
  281.         $out_html = preg_replace('@^(.+)<body[^>]*>@isU', '', $out_html);
  282.         $out_html = preg_replace('@</body>.+$@isU', '', $out_html);
  283.         $out_html = preg_replace('@<title>.+</title>@isU', '', $out_html);
  284.         $out_html = preg_replace('@<h1>.+</h1>@isU', '', $out_html, 1);
  285.         $out_html = preg_replace('@<script[^>]*>(.+)</script>@ismU', '', $out_html);
  286.         $out_html = preg_replace('@<style[^>]*>(.+)</style>@ismU', '', $out_html);
  287.  
  288.         return array($out_html, $out_js, $out_css);
  289. }
  290.  
  291. /**
  292.  * @param string $password
  293.  * @param bool $raw_output
  294.  * @return string
  295.  * @throws Exception
  296.  */
  297. function sha3_512(string $password, bool $raw_output=false): string {
  298.         if (hash_supported_natively('sha3-512')) {
  299.                 return hash('sha3-512', $password, $raw_output);
  300.         } else {
  301.                 return \bb\Sha3\Sha3::hash($password, 512, $raw_output);
  302.         }
  303. }
  304.  
  305. /**
  306.  * @param string $message
  307.  * @param string $key
  308.  * @param bool $raw_output
  309.  * @return string
  310.  */
  311. function sha3_512_hmac(string $message, string $key, bool $raw_output=false): string {
  312.         // RFC 2104 HMAC
  313.         if (hash_hmac_supported_natively('sha3-512')) {
  314.                 return hash_hmac('sha3-512', $message, $key, $raw_output);
  315.         } else {
  316.                 return \bb\Sha3\Sha3::hash_hmac($message, $key, 512, $raw_output);
  317.         }
  318. }
  319.  
  320. /**
  321.  * @param string $password
  322.  * @param string $salt
  323.  * @param int $iterations
  324.  * @param int $length
  325.  * @param bool $binary
  326.  * @return string
  327.  */
  328. function sha3_512_pbkdf2(string $password, string $salt, int $iterations, int $length=0, bool $binary=false): string {
  329.         if (hash_pbkdf2_supported_natively('sha3-512')) {
  330.                 return hash_pbkdf2('sha3-512', $password, $salt, $iterations, $length, $binary);
  331.         } else {
  332.                 return \bb\Sha3\Sha3::hash_pbkdf2($password, $salt, $iterations, 512, $length, $binary);
  333.         }
  334. }
  335.  
  336. /**
  337.  * @param bool $require_ssl
  338.  * @param string|null $reason
  339.  * @return bool
  340.  * @throws OIDplusException
  341.  * @throws \ViaThinkSoft\OIDplus\OIDplusConfigInitializationException
  342.  */
  343. function url_post_contents_available(bool $require_ssl=true, string &$reason=null): bool {
  344.         if (class_exists(OIDplus::class)) {
  345.                 if (OIDplus::baseConfig()->getValue('OFFLINE_MODE', false)) {
  346.                         $reason = _L('OIDplus is running in offline mode due to the base configuration setting %1.', 'OFFLINE_MODE');
  347.                         return false;
  348.                 }
  349.         }
  350.  
  351.         if (function_exists('curl_init')) {
  352.                 return true;
  353.         } else {
  354.                 $reason = _L('Please install the PHP extension %1, so that OIDplus can connect to the Internet.', '<code>php_curl</code>');
  355.                 return false;
  356.         }
  357. }
  358.  
  359. /**
  360.  * @param string $url
  361.  * @param array $params
  362.  * @param array $extraHeaders
  363.  * @param string $userAgent
  364.  * @return string|false
  365.  * @throws \ViaThinkSoft\OIDplus\OIDplusException
  366.  */
  367. function url_post_contents(string $url, array $params=array(), array $extraHeaders=array(), string $userAgent='ViaThinkSoft-OIDplus/2.0') {
  368.         $require_ssl = str_starts_with(strtolower($url),'https:');
  369.         if (!url_post_contents_available($require_ssl, $reason)) {
  370.                 throw new OIDplusException(_L('This feature is not available, because OIDplus cannot connect to the Internet.').' '.$reason);
  371.         }
  372.  
  373.         $postFields = http_build_query($params);
  374.  
  375.         $headers = array(
  376.                 "User-Agent: $userAgent",
  377.                 "Content-Length: ".strlen($postFields)
  378.         );
  379.  
  380.         foreach ($extraHeaders as $name => $val) {
  381.                 $headers[] = "$name: $val";
  382.         }
  383.  
  384.         if (function_exists('curl_init')) {
  385.                 $ch = curl_init();
  386.                 if (class_exists(OIDplus::class)) {
  387.                         if (ini_get('curl.cainfo') == '') curl_setopt($ch, CURLOPT_CAINFO, OIDplus::localpath() . 'vendor/cacert.pem');
  388.                 }
  389.                 curl_setopt($ch, CURLOPT_URL, $url);
  390.                 curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
  391.                 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  392.                 curl_setopt($ch, CURLOPT_POST, true);
  393.                 curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
  394.                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  395.                 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  396.                 curl_setopt($ch, CURLOPT_AUTOREFERER, true);
  397.                 $res = @curl_exec($ch);
  398.                 $error_code = @curl_getinfo($ch, CURLINFO_HTTP_CODE);
  399.                 @curl_close($ch);
  400.                 if ($error_code >= 400) return false;
  401.                 if ($res === false) return false;
  402.         } else {
  403.                 $res = false;
  404.                 assert(false);
  405.         }
  406.  
  407.         return $res;
  408. }
  409.  
  410. /**
  411.  * @param bool $require_ssl
  412.  * @param string|null $reason
  413.  * @return bool
  414.  * @throws OIDplusException
  415.  * @throws \ViaThinkSoft\OIDplus\OIDplusConfigInitializationException
  416.  */
  417. function url_get_contents_available(bool $require_ssl=true, string &$reason=null): bool {
  418.         if (class_exists(OIDplus::class)) {
  419.                 if (OIDplus::baseConfig()->getValue('OFFLINE_MODE', false)) {
  420.                         $reason = _L('OIDplus is running in offline mode due to the base configuration setting %1.', 'OFFLINE_MODE');
  421.                         return false;
  422.                 }
  423.         }
  424.  
  425.         if (function_exists('curl_init')) {
  426.                 // Via cURL
  427.                 return true;
  428.         } else {
  429.                 // Via file_get_contents()
  430.                 if (!ini_get('allow_url_fopen')) {
  431.                         $reason = _L('Please install the PHP extension %1 and/or enable %2 in your PHP configuration, so that OIDplus can connect to the Internet.', '<code>php_curl</code>', '<code>allow_url_fopen</code>');
  432.                         return false;
  433.                 }
  434.                 // Use extension_loaded() instead of function_exists(), because our supplement does not help...
  435.                 if ($require_ssl && !extension_loaded('openssl')) {
  436.                         $reason = _L('Please install the PHP extension %1 and/or %2, so that OIDplus can connect to the Internet.', '<code>php_curl</code>', '<code>php_openssl</code>');
  437.                         return false;
  438.                 }
  439.                 return true;
  440.         }
  441. }
  442.  
  443. /**
  444.  * @param string $url
  445.  * @param array $extraHeaders
  446.  * @param string $userAgent
  447.  * @return string|false
  448.  */
  449. function url_get_contents(string $url, array $extraHeaders=array(), string $userAgent='ViaThinkSoft-OIDplus/2.0') {
  450.         $require_ssl = str_starts_with(strtolower($url),'https:');
  451.         if (!url_get_contents_available($require_ssl, $reason)) {
  452.                 throw new OIDplusException(_L('This feature is not available, because OIDplus cannot connect to the Internet.').' '.$reason);
  453.         }
  454.  
  455.         $headers = array("User-Agent: $userAgent");
  456.         foreach ($extraHeaders as $name => $val) {
  457.                 $headers[] = "$name: $val";
  458.         }
  459.         if (function_exists('curl_init')) {
  460.                 $ch = curl_init();
  461.                 if (class_exists(OIDplus::class)) {
  462.                         if (ini_get('curl.cainfo') == '') curl_setopt($ch, CURLOPT_CAINFO, OIDplus::localpath() . 'vendor/cacert.pem');
  463.                 }
  464.                 curl_setopt($ch, CURLOPT_URL, $url);
  465.                 curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
  466.                 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  467.                 curl_setopt($ch, CURLOPT_POST, false);
  468.                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  469.                 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  470.                 curl_setopt($ch, CURLOPT_AUTOREFERER, true);
  471.                 $res = @curl_exec($ch);
  472.                 $error_code = @curl_getinfo($ch, CURLINFO_HTTP_CODE);
  473.                 @curl_close($ch);
  474.                 if ($error_code >= 400) return false;
  475.                 if ($res === false) return false;
  476.         } else {
  477.                 // Attention: HTTPS only works if OpenSSL extension is enabled.
  478.                 // Our supplement does not help...
  479.                 $opts = [
  480.                         "http" => [
  481.                                 "method" => "GET",
  482.                                 "header" => implode("\r\n",$headers)."\r\n"
  483.                         ]
  484.                 ];
  485.                 $context = stream_context_create($opts);
  486.                 $res = @file_get_contents($url, false, $context);
  487.                 if ($res === false) return false;
  488.         }
  489.         return $res;
  490. }
  491.  
  492. /**
  493. * @param array &$rows
  494. * @param string $fieldName
  495. * @return void
  496. */
  497. function natsort_field(array &$rows, string $fieldName) {
  498.         usort($rows, function($a,$b) use($fieldName) {
  499.                 if ($a[$fieldName] == $b[$fieldName]) return 0; // equal
  500.                 $ary = array(
  501.                         -1 => $a[$fieldName],
  502.                         1 => $b[$fieldName]
  503.                 );
  504.                 natsort($ary);
  505.                 $keys = array_keys($ary);
  506.                 return $keys[0];
  507.         });
  508. }
  509.  
  510. /**
  511.  * @param array $ary
  512.  * @return \stdClass
  513.  */
  514. function array_to_stdobj(array $ary): \stdClass {
  515.         $obj = new \stdClass;
  516.         foreach ($ary as $name => $val) {
  517.                 $obj->$name = $val;
  518.         }
  519.         return $obj;
  520. }
  521.  
  522. /**
  523.  * @param \stdClass $obj
  524.  * @return array
  525.  */
  526. function stdobj_to_array(\stdClass $obj): array {
  527.         $ary = array();
  528.         foreach ($obj as $name => $val) { /* @phpstan-ignore-line */
  529.                 $ary[$name] = $val;
  530.         }
  531.         return $ary;
  532. }
  533.  
  534. /**
  535.  * @return string|false
  536.  */
  537. function get_own_username() {
  538.         $current_user = exec('whoami');
  539.         if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
  540.                 try {
  541.                         if (function_exists('mb_convert_encoding')) {
  542.                                 $current_user = @mb_convert_encoding($current_user, "UTF-8", "cp850");
  543.                         } else if (function_exists('iconv')) {
  544.                                 $current_user = @iconv("cp850", "UTF-8", $current_user);
  545.                         }
  546.                 } catch (\Exception $e) {}
  547.                 if (function_exists('mb_strtoupper')) {
  548.                         $current_user = mb_strtoupper($current_user); // just cosmetics
  549.                 }
  550.         }
  551.         if (!$current_user) {
  552.                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
  553.                         // Windows on an IIS server:
  554.                         //     getenv('USERNAME')     MARSCHALL$                (That is the "machine account", see https://docs.microsoft.com/en-us/iis/manage/configuring-security/application-pool-identities#accessing-the-network )
  555.                         //     get_current_user()     DefaultAppPool
  556.                         //     exec('whoami')         iis apppool\defaultapppool
  557.                         // Windows with XAMPP:
  558.                         //     getenv('USERNAME')     dmarschall
  559.                         //     get_current_user()     dmarschall               (even if script has a different NTFS owner!)
  560.                         //     exec('whoami')         hickelsoft\dmarschall
  561.                         $current_user = get_current_user();
  562.                         if (!$current_user) {
  563.                                 $current_user = getenv('USERNAME');
  564.                                 $current_user = mb_strtoupper($current_user); // just cosmetics
  565.                         }
  566.                 } else {
  567.                         // On Linux:
  568.                         $current_user = exec('id -un');
  569.                         if (!$current_user) {
  570.                                 // PHP'S get_current_user() will get the owner of the PHP script, not the process owner!
  571.                                 // We want the process owner, so we use posix_geteuid() preferably.
  572.                                 if (function_exists('posix_geteuid')) {
  573.                                         $uid = posix_geteuid();
  574.                                 } else {
  575.                                         $temp_file = tempnam(sys_get_temp_dir(), 'TMP');
  576.                                         if ($temp_file !== false) {
  577.                                                 $uid = fileowner($temp_file);
  578.                                                 if ($uid === false) $uid = -1;
  579.                                                 @unlink($temp_file);
  580.                                         } else {
  581.                                                 $uid = -1;
  582.                                         }
  583.                                 }
  584.                                 if ($uid >= 0) {
  585.                                         $current_user = '#' . $uid;
  586.                                         if (function_exists('posix_getpwuid')) {
  587.                                                 $userinfo = posix_getpwuid($uid); // receive username from the UID (requires read access to /etc/passwd)
  588.                                                 if ($userinfo !== false) $current_user = $userinfo['name'];
  589.                                         }
  590.                                 } else {
  591.                                         $current_user = get_current_user();
  592.                                 }
  593.                         }
  594.                 }
  595.         }
  596.         return $current_user ?: false;
  597. }
  598.  
  599. /**
  600.  * @param string $path
  601.  * @return bool
  602.  */
  603. function isFileOrPathWritable(string $path): bool {
  604.         if ($writable_file = (file_exists($path) && is_writable($path))) return true;
  605.         if ($writable_directory = (!file_exists($path) && is_writable(dirname($path)))) return true;
  606.         return false;
  607. }
  608.  
  609. /**
  610.  * @param string $html
  611.  * @return string
  612.  */
  613. function html_to_text(string $html): string {
  614.         $html = str_replace("\n", "", $html);
  615.         $html = str_ireplace('<br', "\n<br", $html);
  616.         $html = str_ireplace('<p', "\n\n<p", $html);
  617.         $html = strip_tags($html);
  618.         $html = html_entity_decode($html, ENT_QUOTES, 'UTF-8');
  619.         return $html;
  620. }
  621.  
  622. /**
  623.  * Get header Authorization
  624.  * @see https://stackoverflow.com/questions/40582161/how-to-properly-use-bearer-tokens
  625.  **/
  626. function getAuthorizationHeader(){
  627.     $headers = null;
  628.     if (isset($_SERVER['Authorization'])) {
  629.         $headers = trim($_SERVER["Authorization"]);
  630.     }
  631.     else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
  632.         $headers = trim($_SERVER["HTTP_AUTHORIZATION"]);
  633.     } elseif (function_exists('apache_request_headers')) {
  634.         $requestHeaders = apache_request_headers();
  635.         // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
  636.         $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
  637.         //print_r($requestHeaders);
  638.         if (isset($requestHeaders['Authorization'])) {
  639.             $headers = trim($requestHeaders['Authorization']);
  640.         }
  641.     }
  642.     return $headers;
  643. }
  644.  
  645. /**
  646.  * get access token from header
  647.  * @see https://stackoverflow.com/questions/40582161/how-to-properly-use-bearer-tokens
  648.  **/
  649. function getBearerToken() {
  650.     $headers = getAuthorizationHeader();
  651.     // HEADER: Get the access token from the header
  652.     if (!empty($headers)) {
  653.         if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
  654.             return $matches[1];
  655.         }
  656.     }
  657.     return null;
  658. }
  659.  
  660. /**
  661.  * @param array $struct
  662.  * @return string
  663.  */
  664. function array_to_html_ul_li(array $struct): string {
  665.         $res = '';
  666.         $res .= '<ul>';
  667.         foreach ($struct as $name => $val) {
  668.                 $res .= '<li>';
  669.                 if (is_array($val)) {
  670.                         $res .= $name . array_to_html_ul_li($val);
  671.                 } else {
  672.                         $res .= $val;
  673.                 }
  674.                 $res .= '</li>';
  675.         }
  676.         $res .= '</ul>';
  677.         return $res;
  678. }
  679.  
  680. /**
  681.  * @param mixed $mixed
  682.  * @return bool
  683.  */
  684. function oidplus_is_true($mixed): bool {
  685.         if (is_null($mixed)) {
  686.                 return false;
  687.         } else if (is_string($mixed)) {
  688.                 return (strtolower($mixed) == 'true') || ($mixed == '1') || (strtolower($mixed) == 'y') || (strtolower($mixed) == 't') || (strtolower($mixed) == 'on');
  689.         } else if (is_bool($mixed)) {
  690.                 return $mixed;
  691.         } else if (is_numeric($mixed)) {
  692.                 return $mixed != 0;
  693.         } else {
  694.                 return (bool)$mixed; // let PHP decide...
  695.         }
  696. }
  697.  
  698. /**
  699.  * @param string $data
  700.  * @param string $key
  701.  * @return string
  702.  * @throws \Exception
  703.  */
  704. function encrypt_str(string $data, string $key): string {
  705.         if (!function_exists('openssl_encrypt')) {
  706.                 throw new OIDplusException(_L('Decryption failed (OpenSSL not installed)'));
  707.         }
  708.  
  709.         $iv = random_bytes(16); // AES block size in CBC mode
  710.  
  711.         // In 2023, OWASP recommended to use 600,000 iterations for PBKDF2-HMAC-SHA256 and 210,000 for PBKDF2-HMAC-SHA512.
  712.         $version = 'V2023A';
  713.  
  714.         // Encryption
  715.         $ciphertext = openssl_encrypt(
  716.                 $data,
  717.                 'AES-256-CBC',
  718.                 hash_pbkdf2('sha512', $key, '', 210000, 32/*256bit*/, true),
  719.                 OPENSSL_RAW_DATA,
  720.                 $iv
  721.         );
  722.  
  723.         // Authentication
  724.         $hmac = sha3_512_hmac($iv . $ciphertext, $key, true);
  725.  
  726.         return $version . $hmac . $iv . $ciphertext;
  727. }
  728.  
  729. /**
  730.  * @param string $data
  731.  * @param string $key
  732.  * @return string
  733.  * @throws OIDplusException
  734.  */
  735. function decrypt_str(string $data, string $key): string {
  736.         if (!function_exists('openssl_decrypt')) {
  737.                 throw new OIDplusException(_L('Decryption failed (OpenSSL not installed)'));
  738.         }
  739.  
  740.         $version    = mb_substr($data, 0, 6, '8bit');
  741.         $hmac       = mb_substr($data, 6, 64, '8bit');
  742.         $iv         = mb_substr($data, 70, 16, '8bit');
  743.         $ciphertext = mb_substr($data, 86, null, '8bit');
  744.  
  745.         if ($version === 'V2023A') {
  746.                 // Authentication
  747.                 $hmacNew = sha3_512_hmac($iv . $ciphertext, $key, true);
  748.                 if (!hash_equals($hmac, $hmacNew)) {
  749.                         throw new OIDplusException(_L('Decryption failed (wrong password)'));
  750.                 }
  751.  
  752.                 // Decryption
  753.                 $cleartext = openssl_decrypt(
  754.                         $ciphertext,
  755.                         'AES-256-CBC',
  756.                         hash_pbkdf2('sha512', $key, '', 210000, 32/*256bit*/, true),
  757.                         OPENSSL_RAW_DATA,
  758.                         $iv
  759.                 );
  760.         } else {
  761.                 throw new OIDplusException(_L('Decryption failed (Unexpected encryption version)'));
  762.         }
  763.  
  764.         if ($cleartext === false) {
  765.                 throw new OIDplusException(_L('Decryption failed (Internal error)'));
  766.         }
  767.         return $cleartext;
  768. }
  769.  
  770. /**
  771.  * Finds a substring that is guaranteed not in $str
  772.  * @param string $str
  773.  * @return string
  774.  */
  775. function find_nonexisting_substr(string $str): string {
  776.         $i = 0;
  777.         do {
  778.                 $i++;
  779.                 $dummy = "[$i]";
  780.         } while (strpos($str, $dummy) !== false);
  781.         return $dummy;
  782. }
  783.  
  784. /**
  785.  * Works like explode(), but it respects if $separator is preceded by an backslash escape character
  786.  * @param string $separator
  787.  * @param string $string
  788.  * @param int $limit
  789.  * @return array
  790.  */
  791. function explode_with_escaping(string $separator, string $string, int $limit=PHP_INT_MAX): array {
  792.         $dummy1 = find_nonexisting_substr($string);
  793.         $dummy2 = find_nonexisting_substr($string.$dummy1);
  794.  
  795.         $string = str_replace('\\\\', $dummy2, $string);
  796.         $string = str_replace('\\'.$separator, $dummy1, $string);
  797.  
  798.         $ary = explode($separator, $string, $limit);
  799.  
  800.         foreach ($ary as &$a) {
  801.                 $a = str_replace($dummy2, '\\\\', $a);
  802.                 $a = str_replace($dummy1, '\\'.$separator, $a);
  803.         }
  804.         unset($a);
  805.  
  806.         return $ary;
  807. }
  808.