Subversion Repositories oidplus

Rev

Rev 1426 | Rev 1436 | Go to most recent revision | 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. namespace ViaThinkSoft\OIDplus;
  21.  
  22. // phpcs:disable PSR1.Files.SideEffects
  23. \defined('INSIDE_OIDPLUS') or die;
  24. // phpcs:enable PSR1.Files.SideEffects
  25.  
  26. class OIDplus extends OIDplusBaseClass {
  27.         /**
  28.          * @var OIDplusPagePlugin[]
  29.          */
  30.         private static /*OIDplusPagePlugin[]*/ $pagePlugins = array();
  31.         /**
  32.          * @var OIDplusAuthPlugin[]
  33.          */
  34.         private static /*OIDplusAuthPlugin[]*/ $authPlugins = array();
  35.         /**
  36.          * @var OIDplusLoggerPlugin[]
  37.          */
  38.         private static /*OIDplusLoggerPlugin[]*/ $loggerPlugins = array();
  39.         /**
  40.          * @var OIDplusObjectTypePlugin[]
  41.          */
  42.         private static /*OIDplusObjectTypePlugin[]*/ $objectTypePlugins = array();
  43.         /**
  44.          * @var string[]|OIDplusObject[] Classnames of OIDplusObject classes
  45.          */
  46.         private static /*string[]*/ $enabledObjectTypes = array();
  47.         /**
  48.          * @var string[]|OIDplusObject[] Classnames of OIDplusObject classes
  49.          */
  50.         private static /*string[]*/ $disabledObjectTypes = array();
  51.         /**
  52.          * @var OIDplusDatabasePlugin[]
  53.          */
  54.         private static /*OIDplusDatabasePlugin[]*/ $dbPlugins = array();
  55.         /**
  56.          * @var OIDplusCaptchaPlugin[]
  57.          */
  58.         private static /*OIDplusCaptchaPlugin[]*/ $captchaPlugins = array();
  59.         /**
  60.          * @var OIDplusSqlSlangPlugin[]
  61.          */
  62.         private static /*OIDplusSqlSlangPlugin[]*/ $sqlSlangPlugins = array();
  63.         /**
  64.          * @var OIDplusLanguagePlugin[]
  65.          */
  66.         private static /*OIDplusLanguagePlugin[]*/ $languagePlugins = array();
  67.         /**
  68.          * @var OIDplusDesignPlugin[]
  69.          */
  70.         private static /*OIDplusDesignPlugin[]*/ $designPlugins = array();
  71.  
  72.         /**
  73.          * @var bool
  74.          */
  75.         protected static $html = true;
  76.  
  77.         /**
  78.          * e.g. "../"
  79.          */
  80.         /*public*/ const PATH_RELATIVE = 1;
  81.  
  82.         /**
  83.          * e.g. "http://www.example.com/oidplus/"
  84.          */
  85.         /*public*/ const PATH_ABSOLUTE = 2;
  86.  
  87.         /**
  88.          * e.g. "http://www.example.org/oidplus/" (if baseconfig CANONICAL_SYSTEM_URL is set)
  89.          */
  90.         /*public*/ const PATH_ABSOLUTE_CANONICAL = 3;
  91.  
  92.         /**
  93.          * e.g. "/oidplus/"
  94.          */
  95.         /*public*/ const PATH_RELATIVE_TO_ROOT = 4;
  96.  
  97.         /**
  98.          * e.g. "/oidplus/" (if baseconfig CANONICAL_SYSTEM_URL is set)
  99.          */
  100.         /*public*/ const PATH_RELATIVE_TO_ROOT_CANONICAL = 5;
  101.  
  102.         /**
  103.          * These plugin types can contain HTML code and therefore may
  104.          * emit (non-setup) CSS/JS code via their manifest.
  105.          * Note that design plugins may only output CSS, not JS.
  106.          */
  107.         /*public*/ const INTERACTIVE_PLUGIN_TYPES = array(
  108.                 'publicPages',
  109.                 'raPages',
  110.                 'adminPages',
  111.                 'objectTypes',
  112.                 'captcha'
  113.         );
  114.  
  115.         //const UUID_NAMEBASED_NS_Base64PubKey = 'fd16965c-8bab-11ed-8744-3c4a92df8582';
  116.  
  117.         /**
  118.          * Private constructor (Singleton)
  119.          */
  120.         private function __construct() {
  121.         }
  122.  
  123.         /**
  124.          * @return bool
  125.          * @throws OIDplusException
  126.          */
  127.         private static function insideSetup(): bool {
  128.                 if (PHP_SAPI == 'cli') return false;
  129.                 if (!isset($_SERVER['REQUEST_URI'])) return false;
  130.                 return (strpos($_SERVER['REQUEST_URI'], OIDplus::webpath(null,OIDplus::PATH_RELATIVE_TO_ROOT).'setup/') === 0);
  131.         }
  132.  
  133.         // --- Static classes
  134.  
  135.         private static $baseConfig = null;
  136.         private static $oldConfigFormatLoaded = false;
  137.  
  138.         /**
  139.          * @return OIDplusBaseConfig
  140.          * @throws OIDplusException, OIDplusConfigInitializationException
  141.          */
  142.         public static function baseConfig(): OIDplusBaseConfig {
  143.                 if ($first_init = is_null(self::$baseConfig)) {
  144.                         self::$baseConfig = new OIDplusBaseConfig();
  145.                 }
  146.  
  147.                 if ($first_init) {
  148.                         if (self::insideSetup()) return self::$baseConfig;
  149.                         if ((basename($_SERVER['SCRIPT_NAME']) === 'polyfill.min.js.php') && isset($_REQUEST['noBaseConfig']) && ($_REQUEST['noBaseConfig'] == '1')) return self::$baseConfig;
  150.                         if ((basename($_SERVER['SCRIPT_NAME']) === 'oidplus.min.js.php') && isset($_REQUEST['noBaseConfig']) && ($_REQUEST['noBaseConfig'] == '1')) return self::$baseConfig;
  151.                         if ((basename($_SERVER['SCRIPT_NAME']) === 'oidplus.min.css.php') && isset($_REQUEST['noBaseConfig']) && ($_REQUEST['noBaseConfig'] == '1')) return self::$baseConfig;
  152.  
  153.                         // Include a file containing various size/depth limitations of OIDs
  154.                         // It is important to include it before userdata/baseconfig/config.inc.php was included,
  155.                         // so we can give userdata/baseconfig/config.inc.php the chance to override the values.
  156.  
  157.                         include OIDplus::localpath().'includes/oidplus_limits.inc.php';
  158.  
  159.                         // Include config file
  160.  
  161.                         $config_file = OIDplus::localpath() . 'userdata/baseconfig/config.inc.php';
  162.                         $config_file_old = OIDplus::localpath() . 'includes/config.inc.php'; // backwards compatibility
  163.  
  164.                         if (!file_exists($config_file) && file_exists($config_file_old)) {
  165.                                 $config_file = $config_file_old;
  166.                         }
  167.  
  168.                         if (file_exists($config_file)) {
  169.                                 if (self::$oldConfigFormatLoaded) {
  170.                                         // Note: We may only include it once due to backwards compatibility,
  171.                                         //       since in version 2.0, the configuration was defined using define() statements
  172.                                         // Attention: This does mean that a full re-init (e.g. for test cases) is not possible
  173.                                         //            if a version 2.0 config is used!
  174.  
  175.                                         // We need to do this, because define() cannot be undone
  176.                                         // Note: This can only happen in very special cases (e.g. test cases) where you call init() twice
  177.                                         throw new OIDplusConfigInitializationException(_L('A full re-initialization is not possible if a version 2.0 config file (containing "defines") is used. Please update to a config 2.1 file by running setup again.'));
  178.                                 } else {
  179.                                         $tmp = file_get_contents($config_file);
  180.                                         $ns = "ViaThinkSoft\OIDplus\OIDplus";
  181.                                         $uses = "use $ns;";
  182.                                         if ((strpos($tmp,'OIDplus::') !== false) && (strpos($tmp,$uses) === false)) {
  183.                                                 // Migrate config file to namespace class names
  184.                                                 // Note: Only config files version 2.1 are affected. Not 2.0 ones
  185.  
  186.                                                 $tmp = "<?php\r\n\r\n$uses /* Automatically added by migration procedure */\r\n?>$tmp";
  187.                                                 $tmp = str_replace('?><?php', '', $tmp);
  188.  
  189.                                                 $tmp = str_replace("\$ns\OIDplusCaptchaPluginRecaptcha::", "OIDplusCaptchaPluginRecaptcha::", $tmp);
  190.                                                 $tmp = str_replace("OIDplusCaptchaPluginRecaptcha::", "\$ns\OIDplusCaptchaPluginRecaptcha::", $tmp);
  191.  
  192.                                                 $tmp = str_replace('DISABLE_PLUGIN_OIDplusPagePublicRdap',
  193.                                                         'DISABLE_PLUGIN_Frdlweb\OIDplus\OIDplusPagePublicRdap', $tmp);
  194.                                                 $tmp = str_replace('DISABLE_PLUGIN_OIDplusPagePublicAltIds',
  195.                                                         'DISABLE_PLUGIN_Frdlweb\OIDplus\OIDplusPagePublicAltIds', $tmp);
  196.                                                 $tmp = str_replace('DISABLE_PLUGIN_OIDplusPagePublicUITweaks',
  197.                                                         'DISABLE_PLUGIN_TushevOrg\OIDplus\OIDplusPagePublicUITweaks', $tmp);
  198.                                                 $tmp = str_replace('DISABLE_PLUGIN_OIDplus',
  199.                                                         'DISABLE_PLUGIN_ViaThinkSoft\OIDplus\OIDplus', $tmp);
  200.  
  201.                                                 if (@file_put_contents($config_file, $tmp) === false) {
  202.                                                         eval('?>'.$tmp);
  203.                                                 } else {
  204.                                                         include $config_file;
  205.                                                 }
  206.                                         } else {
  207.                                                 include $config_file;
  208.                                         }
  209.                                 }
  210.  
  211.                                 // Backwards compatibility 2.0 => 2.1
  212.                                 if (defined('OIDPLUS_CONFIG_VERSION') && (OIDPLUS_CONFIG_VERSION == 2.0)) {
  213.                                         self::$oldConfigFormatLoaded = true;
  214.                                         foreach (get_defined_constants(true)['user'] as $name => $value) {
  215.                                                 $name = str_replace('OIDPLUS_', '', $name);
  216.                                                 if ($name == 'SESSION_SECRET') $name = 'SERVER_SECRET';
  217.                                                 if ($name == 'MYSQL_QUERYLOG') $name = 'QUERY_LOGFILE';
  218.                                                 $name = str_replace('DISABLE_PLUGIN_OIDplusPagePublicRdap',
  219.                                                         'DISABLE_PLUGIN_Frdlweb\OIDplus\OIDplusPagePublicRdap', $name);
  220.                                                 $name = str_replace('DISABLE_PLUGIN_OIDplusPagePublicAltIds',
  221.                                                         'DISABLE_PLUGIN_Frdlweb\OIDplus\OIDplusPagePublicAltIds', $name);
  222.                                                 $name = str_replace('DISABLE_PLUGIN_OIDplusPagePublicUITweaks',
  223.                                                         'DISABLE_PLUGIN_TushevOrg\OIDplus\OIDplusPagePublicUITweaks', $name);
  224.                                                 $name = str_replace('DISABLE_PLUGIN_OIDplus',
  225.                                                         'DISABLE_PLUGIN_ViaThinkSoft\OIDplus\OIDplus', $name);
  226.                                                 if ($name == 'CONFIG_VERSION') {
  227.                                                         $value = 2.1;
  228.                                                 } else if (($name == 'MYSQL_PASSWORD') || ($name == 'ODBC_PASSWORD') || ($name == 'PDO_PASSWORD') || ($name == 'PGSQL_PASSWORD')) {
  229.                                                         $value = base64_decode($value);
  230.                                                 }
  231.                                                 self::$baseConfig->setValue($name, $value);
  232.                                         }
  233.                                 }
  234.                         } else {
  235.                                 if (!is_dir(OIDplus::localpath().'setup')) {
  236.                                         throw new OIDplusConfigInitializationException(_L('File %1 is missing, but setup can\'t be started because its directory missing.',$config_file));
  237.                                 } else {
  238.                                         if (self::$html) {
  239.                                                 if (!self::insideSetup()) {
  240.                                                         header('Location:'.OIDplus::webpath(null,OIDplus::PATH_RELATIVE).'setup/');
  241.                                                         die(_L('Redirecting to setup...'));
  242.                                                 } else {
  243.                                                         return self::$baseConfig;
  244.                                                 }
  245.                                         } else {
  246.                                                 // This can be displayed in e.g. ajax.php
  247.                                                 throw new OIDplusConfigInitializationException(_L('File %1 is missing. Please run setup again.',$config_file));
  248.                                         }
  249.                                 }
  250.                         }
  251.  
  252.                         // Check important config settings
  253.  
  254.                         if (self::$baseConfig->getValue('CONFIG_VERSION') != 2.1) {
  255.                                 if (strpos($_SERVER['REQUEST_URI'], OIDplus::webpath(null,OIDplus::PATH_RELATIVE).'setup/') !== 0) {
  256.                                         throw new OIDplusConfigInitializationException(_L("The information located in %1 is outdated.",realpath($config_file)));
  257.                                 }
  258.                         }
  259.  
  260.                         if (self::$baseConfig->getValue('SERVER_SECRET', '') === '') {
  261.                                 if (strpos($_SERVER['REQUEST_URI'], OIDplus::webpath(null,OIDplus::PATH_RELATIVE).'setup/') !== 0) {
  262.                                         throw new OIDplusConfigInitializationException(_L("You must set a value for SERVER_SECRET in %1 for the system to operate secure.",realpath($config_file)));
  263.                                 }
  264.                         }
  265.                 }
  266.  
  267.                 return self::$baseConfig;
  268.         }
  269.  
  270.         private static $config = null;
  271.  
  272.         /**
  273.          * @return OIDplusConfig
  274.          * @throws OIDplusException
  275.          */
  276.         public static function config(): OIDplusConfig {
  277.                 if ($first_init = is_null(self::$config)) {
  278.                         self::$config = new OIDplusConfig();
  279.                 }
  280.  
  281.                 if ($first_init) {
  282.                         // These are important settings for base functionalities and therefore are not inside plugins
  283.                         self::$config->prepareConfigKey('system_title', 'What is the name of your RA?', 'OIDplus 2.0', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
  284.                                 if (empty($value)) {
  285.                                         throw new OIDplusException(_L('Please enter a value for the system title.'));
  286.                                 }
  287.                         });
  288.                         self::$config->prepareConfigKey('admin_email', 'E-Mail address of the system administrator', '', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
  289.                                 if (!empty($value) && !OIDplus::mailUtils()->validMailAddress($value)) {
  290.                                         throw new OIDplusException(_L('This is not a correct email address'));
  291.                                 }
  292.                         });
  293.                         self::$config->prepareConfigKey('global_cc', 'Global CC for all outgoing emails?', '', OIDplusConfig::PROTECTION_EDITABLE, function(&$value) {
  294.                                 $value = trim($value);
  295.                                 if ($value === '') return;
  296.                                 $addrs = explode(';', $value);
  297.                                 foreach ($addrs as $addr) {
  298.                                         $addr = trim($addr);
  299.                                         if (!empty($addr) && !OIDplus::mailUtils()->validMailAddress($addr)) {
  300.                                                 throw new OIDplusException(_L('%1 is not a correct email address',$addr));
  301.                                         }
  302.                                 }
  303.                         });
  304.                         self::$config->prepareConfigKey('global_bcc', 'Global BCC for all outgoing emails?', '', OIDplusConfig::PROTECTION_EDITABLE, function(&$value) {
  305.                                 $value = trim($value);
  306.                                 if ($value === '') return;
  307.                                 $addrs = explode(';', $value);
  308.                                 foreach ($addrs as $addr) {
  309.                                         $addr = trim($addr);
  310.                                         if (!empty($addr) && !OIDplus::mailUtils()->validMailAddress($addr)) {
  311.                                                 throw new OIDplusException(_L('%1 is not a correct email address',$addr));
  312.                                         }
  313.                                 }
  314.                         });
  315.                         self::$config->prepareConfigKey('objecttypes_initialized', 'List of object type plugins that were initialized once', '', OIDplusConfig::PROTECTION_READONLY, function($value) {
  316.                                 // Nothing here yet
  317.                         });
  318.                         self::$config->prepareConfigKey('objecttypes_enabled', 'Enabled object types and their order, separated with a semicolon (please reload the page so that the change is applied)', '', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
  319.                                 // TODO: when objecttypes_enabled is changed at the admin control panel, we need to do a reload of the page, so that jsTree will be updated. Is there anything we can do?
  320.  
  321.                                 $ary = explode(';',$value);
  322.                                 $uniq_ary = array_unique($ary);
  323.  
  324.                                 if (count($ary) != count($uniq_ary)) {
  325.                                         throw new OIDplusException(_L('Please check your input. Some object types are double.'));
  326.                                 }
  327.  
  328.                                 foreach ($ary as $ot_check) {
  329.                                         $ns_found = false;
  330.                                         foreach (OIDplus::getEnabledObjectTypes() as $ot) {
  331.                                                 if ($ot::ns() == $ot_check) {
  332.                                                         $ns_found = true;
  333.                                                         break;
  334.                                                 }
  335.                                         }
  336.                                         foreach (OIDplus::getDisabledObjectTypes() as $ot) {
  337.                                                 if ($ot::ns() == $ot_check) {
  338.                                                         $ns_found = true;
  339.                                                         break;
  340.                                                 }
  341.                                         }
  342.                                         if (!$ns_found) {
  343.                                                 throw new OIDplusException(_L('Please check your input. Namespace "%1" is not found',$ot_check));
  344.                                         }
  345.                                 }
  346.                         });
  347.                         self::$config->prepareConfigKey('oidplus_private_key', 'Private key for this system', '', OIDplusConfig::PROTECTION_HIDDEN, function($value) {
  348.                                 // Nothing here yet
  349.                         });
  350.                         self::$config->prepareConfigKey('oidplus_public_key', 'Public key for this system. If you "clone" your system, you must delete this key (e.g. using phpMyAdmin), so that a new one is created.', '', OIDplusConfig::PROTECTION_READONLY, function($value) {
  351.                                 // Nothing here yet
  352.                         });
  353.                         self::$config->prepareConfigKey('last_known_system_url', 'Last known System URL', '', OIDplusConfig::PROTECTION_HIDDEN, function($value) {
  354.                                 // Nothing here yet
  355.                         });
  356.                         self::$config->prepareConfigKey('last_known_version', 'Last known OIDplus Version', '', OIDplusConfig::PROTECTION_HIDDEN, function($value) {
  357.                                 // Nothing here yet
  358.                         });
  359.                         self::$config->prepareConfigKey('default_ra_auth_method', 'Default auth method used for generating password of RAs (must exist in plugins/[vendorname]/auth/)? Empty = OIDplus decides.', '', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
  360.                                 if (trim($value) === '') return; // OIDplus decides
  361.  
  362.                                 $good = true;
  363.                                 if (strpos($value,'/') !== false) $good = false;
  364.                                 if (strpos($value,'\\') !== false) $good = false;
  365.                                 if (strpos($value,'..') !== false) $good = false;
  366.                                 if (!$good) {
  367.                                         throw new OIDplusException(_L('Invalid auth plugin name. It is usually the folder name, without path, e.g. "%1"', 'A4_argon2'));
  368.                                 }
  369.  
  370.                                 OIDplus::checkRaAuthPluginAvailable($value, true);
  371.                         });
  372.                 }
  373.  
  374.                 return self::$config;
  375.         }
  376.  
  377.         private static $gui = null;
  378.  
  379.         /**
  380.          * @return OIDplusGui
  381.          */
  382.         public static function gui(): OIDplusGui {
  383.                 if (is_null(self::$gui)) {
  384.                         self::$gui = new OIDplusGui();
  385.                 }
  386.                 return self::$gui;
  387.         }
  388.  
  389.         private static $authUtils = null;
  390.  
  391.         /**
  392.          * @return OIDplusAuthUtils
  393.          */
  394.         public static function authUtils(): OIDplusAuthUtils {
  395.                 if (is_null(self::$authUtils)) {
  396.                         self::$authUtils = new OIDplusAuthUtils();
  397.                 }
  398.                 return self::$authUtils;
  399.         }
  400.  
  401.         private static $mailUtils = null;
  402.  
  403.         /**
  404.          * @return OIDplusMailUtils
  405.          */
  406.         public static function mailUtils(): OIDplusMailUtils {
  407.                 if (is_null(self::$mailUtils)) {
  408.                         self::$mailUtils = new OIDplusMailUtils();
  409.                 }
  410.                 return self::$mailUtils;
  411.         }
  412.  
  413.         private static $cookieUtils = null;
  414.  
  415.         /**
  416.          * @return OIDplusCookieUtils
  417.          */
  418.         public static function cookieUtils(): OIDplusCookieUtils {
  419.                 if (is_null(self::$cookieUtils)) {
  420.                         self::$cookieUtils = new OIDplusCookieUtils();
  421.                 }
  422.                 return self::$cookieUtils;
  423.         }
  424.  
  425.         private static $menuUtils = null;
  426.  
  427.         /**
  428.          * @return OIDplusMenuUtils
  429.          */
  430.         public static function menuUtils(): OIDplusMenuUtils {
  431.                 if (is_null(self::$menuUtils)) {
  432.                         self::$menuUtils = new OIDplusMenuUtils();
  433.                 }
  434.                 return self::$menuUtils;
  435.         }
  436.  
  437.         private static $logger = null;
  438.  
  439.         /**
  440.          * @return OIDplusLogger
  441.          */
  442.         public static function logger(): OIDplusLogger {
  443.                 if (is_null(self::$logger)) {
  444.                         self::$logger = new OIDplusLogger();
  445.                 }
  446.                 return self::$logger;
  447.         }
  448.  
  449.         // --- SQL slang plugin
  450.  
  451.         /**
  452.          * @param OIDplusSqlSlangPlugin $plugin
  453.          * @return void
  454.          * @throws OIDplusException
  455.          */
  456.         private static function registerSqlSlangPlugin(OIDplusSqlSlangPlugin $plugin) {
  457.                 $name = $plugin::id();
  458.  
  459.                 if ($name === '') {
  460.                         throw new OIDplusException(_L('Plugin %1 cannot be registered because it does not return a valid ID', $plugin->getPluginDirectory()));
  461.                 }
  462.  
  463.                 if (isset(self::$sqlSlangPlugins[$name])) {
  464.                         $plugintype_hf = _L('SQL slang');
  465.                         throw new OIDplusException(_L('Multiple %1 plugins use the ID %2', $plugintype_hf, $name));
  466.                 }
  467.  
  468.                 self::$sqlSlangPlugins[$name] = $plugin;
  469.         }
  470.  
  471.         /**
  472.          * @return OIDplusSqlSlangPlugin[]
  473.          */
  474.         public static function getSqlSlangPlugins(): array {
  475.                 return self::$sqlSlangPlugins;
  476.         }
  477.  
  478.         /**
  479.          * @param string $id
  480.          * @return OIDplusSqlSlangPlugin|null
  481.          */
  482.         public static function getSqlSlangPlugin(string $id)/*: ?OIDplusSqlSlangPlugin*/ {
  483.                 return self::$sqlSlangPlugins[$id] ?? null;
  484.         }
  485.  
  486.         // --- Database plugin
  487.  
  488.         /**
  489.          * @param OIDplusDatabasePlugin $plugin
  490.          * @return void
  491.          * @throws OIDplusException
  492.          */
  493.         private static function registerDatabasePlugin(OIDplusDatabasePlugin $plugin) {
  494.                 $name = $plugin::id();
  495.  
  496.                 if ($name === '') {
  497.                         throw new OIDplusException(_L('Plugin %1 cannot be registered because it does not return a valid ID', $plugin->getPluginDirectory()));
  498.                 }
  499.  
  500.                 if (isset(self::$dbPlugins[$name])) {
  501.                         $plugintype_hf = _L('Database');
  502.                         throw new OIDplusException(_L('Multiple %1 plugins use the ID %2', $plugintype_hf, $name));
  503.                 }
  504.  
  505.                 self::$dbPlugins[$name] = $plugin;
  506.         }
  507.  
  508.         /**
  509.          * @return OIDplusDatabasePlugin[]
  510.          */
  511.         public static function getDatabasePlugins(): array {
  512.                 return self::$dbPlugins;
  513.         }
  514.  
  515.         /**
  516.          * @return OIDplusDatabasePlugin
  517.          * @throws OIDplusException, OIDplusConfigInitializationException
  518.          */
  519.         public static function getActiveDatabasePlugin(): OIDplusDatabasePlugin {
  520.                 $db_plugin_name = OIDplus::baseConfig()->getValue('DATABASE_PLUGIN','');
  521.                 if ($db_plugin_name === '') {
  522.                         throw new OIDplusConfigInitializationException(_L('No database plugin selected in config file'));
  523.                 }
  524.                 foreach (self::$dbPlugins as $name => $plugin) {
  525.                         if (strtolower($name) == strtolower($db_plugin_name)) {
  526.                                 return $plugin;
  527.                         }
  528.                 }
  529.                 throw new OIDplusConfigInitializationException(_L('Database plugin "%1" not found',$db_plugin_name));
  530.         }
  531.  
  532.         /**
  533.          * @var OIDplusDatabaseConnection|null
  534.          */
  535.         private static $dbMainSession = null;
  536.  
  537.         /**
  538.          * @return OIDplusDatabaseConnection
  539.          * @throws OIDplusException, OIDplusConfigInitializationException
  540.          */
  541.         public static function db(): OIDplusDatabaseConnection {
  542.                 if (is_null(self::$dbMainSession)) {
  543.                         self::$dbMainSession = self::getActiveDatabasePlugin()->newConnection();
  544.                 }
  545.                 if (!self::$dbMainSession->isConnected()) self::$dbMainSession->connect();
  546.                 return self::$dbMainSession;
  547.         }
  548.  
  549.         /**
  550.          * @var OIDplusDatabaseConnection|null
  551.          */
  552.         private static $dbIsolatedSession = null;
  553.  
  554.         /**
  555.          * @return OIDplusDatabaseConnection
  556.          * @throws OIDplusException, OIDplusConfigInitializationException
  557.          */
  558.         public static function dbIsolated(): OIDplusDatabaseConnection {
  559.                 if (is_null(self::$dbIsolatedSession)) {
  560.                         self::$dbIsolatedSession = self::getActiveDatabasePlugin()->newConnection();
  561.                 }
  562.                 if (!self::$dbIsolatedSession->isConnected()) self::$dbIsolatedSession->connect();
  563.                 return self::$dbIsolatedSession;
  564.         }
  565.  
  566.         // --- CAPTCHA plugin
  567.  
  568.         /**
  569.          * @param OIDplusCaptchaPlugin $plugin
  570.          * @return void
  571.          * @throws OIDplusException
  572.          */
  573.         private static function registerCaptchaPlugin(OIDplusCaptchaPlugin $plugin) {
  574.                 $name = $plugin::id();
  575.  
  576.                 if ($name === '') {
  577.                         throw new OIDplusException(_L('Plugin %1 cannot be registered because it does not return a valid ID', $plugin->getPluginDirectory()));
  578.                 }
  579.  
  580.                 if (isset(self::$captchaPlugins[$name])) {
  581.                         $plugintype_hf = _L('CAPTCHA');
  582.                         throw new OIDplusException(_L('Multiple %1 plugins use the ID %2', $plugintype_hf, $name));
  583.                 }
  584.  
  585.                 self::$captchaPlugins[$name] = $plugin;
  586.         }
  587.  
  588.         /**
  589.          * @return OIDplusCaptchaPlugin[]
  590.          */
  591.         public static function getCaptchaPlugins(): array {
  592.                 return self::$captchaPlugins;
  593.         }
  594.  
  595.         /**
  596.          * @return string
  597.          * @throws OIDplusException, OIDplusConfigInitializationException
  598.          */
  599.         public static function getActiveCaptchaPluginId(): string {
  600.                 $captcha_plugin_name = OIDplus::baseConfig()->getValue('CAPTCHA_PLUGIN', '');
  601.  
  602.                 if (OIDplus::baseConfig()->getValue('RECAPTCHA_ENABLED', false) && ($captcha_plugin_name === '')) {
  603.                         // Legacy config file support!
  604.                         $captcha_plugin_name = 'reCAPTCHA';
  605.                 }
  606.  
  607.                 if ($captcha_plugin_name === '') $captcha_plugin_name = 'None'; // the "None" plugin is a must-have!
  608.  
  609.                 return $captcha_plugin_name;
  610.         }
  611.  
  612.         /**
  613.          * @return OIDplusCaptchaPlugin
  614.          * @throws OIDplusException, OIDplusConfigInitializationException
  615.          */
  616.         public static function getActiveCaptchaPlugin(): OIDplusCaptchaPlugin {
  617.                 $captcha_plugin_name = OIDplus::getActiveCaptchaPluginId();
  618.                 foreach (self::$captchaPlugins as $name => $plugin) {
  619.                         if (strtolower($name) == strtolower($captcha_plugin_name)) {
  620.                                 return $plugin;
  621.                         }
  622.                 }
  623.                 throw new OIDplusConfigInitializationException(_L('CAPTCHA plugin "%1" not found',$captcha_plugin_name));
  624.         }
  625.  
  626.         // --- Page plugin
  627.  
  628.         /**
  629.          * @param OIDplusPagePlugin $plugin
  630.          * @return void
  631.          */
  632.         private static function registerPagePlugin(OIDplusPagePlugin $plugin) {
  633.                 self::$pagePlugins[] = $plugin;
  634.         }
  635.  
  636.         /**
  637.          * @return OIDplusPagePlugin[]
  638.          */
  639.         public static function getPagePlugins(): array {
  640.                 return self::$pagePlugins;
  641.         }
  642.  
  643.         // --- Auth plugin
  644.  
  645.         /**
  646.          * @param string $id
  647.          * @return OIDplusAuthPlugin|null
  648.          */
  649.         public static function getAuthPluginById(string $id)/*: ?OIDplusAuthPlugin*/ {
  650.                 $plugins = OIDplus::getAuthPlugins();
  651.                 foreach ($plugins as $plugin) {
  652.                         if ($plugin->id() == $id) {
  653.                                 return $plugin;
  654.                         }
  655.                 }
  656.                 return null;
  657.         }
  658.  
  659.         /**
  660.          * @param string $plugin_id
  661.          * @param bool $must_hash
  662.          * @return void
  663.          * @throws OIDplusException
  664.          */
  665.         private static function checkRaAuthPluginAvailable(string $plugin_id, bool $must_hash) {
  666.                 // if (!wildcard_is_dir(OIDplus::localpath().'plugins/'.'*'.'/auth/'.$plugin_foldername)) {
  667.                 $plugin = OIDplus::getAuthPluginById($plugin_id);
  668.                 if (is_null($plugin)) {
  669.                         throw new OIDplusException(_L('The auth plugin "%1" does not exist in plugin directory %2',$plugin_id,'plugins/[vendorname]/auth/'));
  670.                 }
  671.  
  672.                 $reason = '';
  673.                 if (!$plugin->availableForVerify($reason)) {
  674.                         throw new OIDplusException(trim(_L('The auth plugin "%1" is not available for password verification on this system.',$plugin_id).' '.$reason));
  675.                 }
  676.                 if ($must_hash && !$plugin->availableForHash($reason)) {
  677.                         throw new OIDplusException(trim(_L('The auth plugin "%1" is not available for hashing on this system.',$plugin_id).' '.$reason));
  678.                 }
  679.         }
  680.  
  681.         /**
  682.          * @param bool $must_hash
  683.          * @return OIDplusAuthPlugin|null
  684.          * @throws OIDplusException
  685.          */
  686.         public static function getDefaultRaAuthPlugin(bool $must_hash)/*: OIDplusAuthPlugin*/ {
  687.                 // 1. Priority: Use the auth plugin the user prefers
  688.                 $def_plugin_id = OIDplus::config()->getValue('default_ra_auth_method');
  689.                 if (trim($def_plugin_id) !== '') {
  690.                         OIDplus::checkRaAuthPluginAvailable($def_plugin_id, $must_hash);
  691.                         return OIDplus::getAuthPluginById($def_plugin_id);
  692.                 }
  693.  
  694.                 // 2. Priority: If empty (i.e. OIDplus may decide), choose the best ViaThinkSoft plugin that is supported on this system
  695.                 $preferred_auth_plugins = array(
  696.                         // Sorted by preference
  697.                         'A4_argon2',  // usually Salted Argon2id
  698.                         'A3_bcrypt',  // usually Salted BCrypt
  699.                         'A5_vts_mcf', // usually SHA3-512-HMAC
  700.                         'A6_crypt'    // usually Salted SHA512 with 5000 rounds
  701.                 );
  702.                 foreach ($preferred_auth_plugins as $plugin_id) {
  703.                         $plugin = OIDplus::getAuthPluginById($plugin_id);
  704.                         if (is_null($plugin)) continue;
  705.  
  706.                         $reason = '';
  707.                         if (!$plugin->availableForHash($reason)) continue;
  708.                         if ($must_hash && !$plugin->availableForVerify($reason)) continue;
  709.                         return $plugin;
  710.                 }
  711.  
  712.                 // 3. Priority: If nothing found, take the first found plugin
  713.                 $plugins = OIDplus::getAuthPlugins();
  714.                 foreach ($plugins as $plugin) {
  715.                         $reason = '';
  716.                         if (!$plugin->availableForHash($reason)) continue;
  717.                         if ($must_hash && !$plugin->availableForVerify($reason)) continue;
  718.                         return $plugin;
  719.                 }
  720.  
  721.                 // 4. Priority: We must deny the creation of the password because we have no auth plugin!
  722.                 throw new OIDplusException(_L('Could not find a fitting auth plugin!'));
  723.         }
  724.  
  725.         /**
  726.          * @param OIDplusAuthPlugin $plugin
  727.          * @return void
  728.          * @throws OIDplusConfigInitializationException
  729.          * @throws OIDplusException
  730.          */
  731.         private static function registerAuthPlugin(OIDplusAuthPlugin $plugin) {
  732.                 $reason = '';
  733.                 if (OIDplus::baseConfig()->getValue('DEBUG') && $plugin->availableForHash($reason) && $plugin->availableForVerify($reason)) {
  734.                         $password = generateRandomString(25);
  735.  
  736.                         try {
  737.                                 $authInfo = $plugin->generate($password);
  738.                         } catch (\Exception $e) {
  739.                                 // This can happen when the AuthKey is too long for the database field
  740.                                 // Note: The constructor and setters of OIDplusRAAuthInfo() already check for length and null/false values.
  741.                                 throw new OIDplusException(_L('Auth plugin "%1" is erroneous: %2',basename($plugin->getPluginDirectory()),$e->getMessage()));
  742.                         }
  743.  
  744.                         $authInfo_AuthKeyDiff = clone $authInfo;
  745.                         $authInfo_AuthKeyDiff->setAuthKey(strrev($authInfo_AuthKeyDiff->getAuthKey()));
  746.  
  747.                         if ((!$plugin->verify($authInfo,$password)) ||
  748.                                 ($plugin->verify($authInfo_AuthKeyDiff,$password)) ||
  749.                                 ($plugin->verify($authInfo,$password.'x'))) {
  750.                                 throw new OIDplusException(_L('Auth plugin "%1" is erroneous: Generate/Verify self-test failed',basename($plugin->getPluginDirectory())));
  751.                         }
  752.                 }
  753.  
  754.                 self::$authPlugins[] = $plugin;
  755.         }
  756.  
  757.         /**
  758.          * @return OIDplusAuthPlugin[]
  759.          */
  760.         public static function getAuthPlugins(): array {
  761.                 return self::$authPlugins;
  762.         }
  763.  
  764.         // --- Language plugin
  765.  
  766.         /**
  767.          * @param OIDplusLanguagePlugin $plugin
  768.          * @return void
  769.          */
  770.         private static function registerLanguagePlugin(OIDplusLanguagePlugin $plugin) {
  771.                 self::$languagePlugins[] = $plugin;
  772.         }
  773.  
  774.         /**
  775.          * @return OIDplusLanguagePlugin[]
  776.          */
  777.         public static function getLanguagePlugins(): array {
  778.                 return self::$languagePlugins;
  779.         }
  780.  
  781.         // --- Design plugin
  782.  
  783.         /**
  784.          * @param OIDplusDesignPlugin $plugin
  785.          * @return void
  786.          */
  787.         private static function registerDesignPlugin(OIDplusDesignPlugin $plugin) {
  788.                 self::$designPlugins[] = $plugin;
  789.         }
  790.  
  791.         /**
  792.          * @return OIDplusDesignPlugin[]
  793.          */
  794.         public static function getDesignPlugins(): array {
  795.                 return self::$designPlugins;
  796.         }
  797.  
  798.         /**
  799.          * @return OIDplusDesignPlugin|null
  800.          * @throws OIDplusException
  801.          */
  802.         public static function getActiveDesignPlugin()/*: ?OIDplusDesignPlugin*/ {
  803.                 $plugins = OIDplus::getDesignPlugins();
  804.                 foreach ($plugins as $plugin) {
  805.                         if ($plugin->id() == OIDplus::config()->getValue('design','default')) {
  806.                                 return $plugin;
  807.                         }
  808.                 }
  809.                 return null;
  810.         }
  811.  
  812.         // --- Logger plugin
  813.  
  814.         /**
  815.          * @param OIDplusLoggerPlugin $plugin
  816.          * @return void
  817.          */
  818.         private static function registerLoggerPlugin(OIDplusLoggerPlugin $plugin) {
  819.                 self::$loggerPlugins[] = $plugin;
  820.         }
  821.  
  822.         /**
  823.          * @return OIDplusLoggerPlugin[]
  824.          */
  825.         public static function getLoggerPlugins(): array {
  826.                 return self::$loggerPlugins;
  827.         }
  828.  
  829.         // --- Object type plugin
  830.  
  831.         /**
  832.          * @param OIDplusObjectTypePlugin $plugin
  833.          * @return void
  834.          * @throws OIDplusException
  835.          */
  836.         private static function registerObjectTypePlugin(OIDplusObjectTypePlugin $plugin) {
  837.                 self::$objectTypePlugins[] = $plugin;
  838.  
  839.                 if (OIDplus::baseConfig()->getValue('DEBUG')) {
  840.                         // Avoid a namespace hash conflict of the OIDplus Information Object Custom UUIDs
  841.                         // see here https://github.com/danielmarschall/oidplus/blob/master/doc/oidplus_custom_guid.md
  842.                         if (!str_starts_with($plugin->getManifest()->getOid(), '1.3.6.1.4.1.37476.2.5.2.4.8.')) {
  843.                                 $coll = [];
  844.                                 for ($i = 1; $i <= 185; $i++) {
  845.                                         // No conflict between ViaThinkSoft OIDs .1 till .185
  846.                                         $block4 = dechex(hexdec(substr(sha1('1.3.6.1.4.1.37476.2.5.2.4.8.'.$i), -4)) & 0x3FFF | 0x8000);
  847.                                         $coll[] = $block4;
  848.                                 }
  849.                                 for ($i=0; $i<=0xF; $i++) {
  850.                                         // 0x8000 - 0x800F are used by the system
  851.                                         $coll[] = dechex(0x8000 + $i);
  852.                                 }
  853.                                 $block4 = dechex(hexdec(substr(sha1($plugin->getManifest()->getOid()), -4)) & 0x3FFF | 0x8000);
  854.                                 if (in_array($block4, $coll)) {
  855.                                         throw new OIDplusException(_L("A third-party vendor object type plugin with OID %1 has a hash-conflict with a ViaThinkSoft plugin. Please recommend to the developer to pick a different OID for their plugin. More information here: %2",$plugin->getManifest()->getOid(),'https://github.com/danielmarschall/oidplus/blob/master/doc/oidplus_custom_guid.md'));
  856.                                 }
  857.                         }
  858.                 }
  859.  
  860.                 $ot = $plugin::getObjectTypeClassName();
  861.                 self::registerObjectType($ot);
  862.         }
  863.  
  864.         /**
  865.          * @param string|OIDplusObject $ot Object type class name (OIDplusObject)
  866.          * @return void
  867.          * @throws OIDplusException
  868.          */
  869.         private static function registerObjectType($ot) {
  870.                 $ns = $ot::ns();
  871.                 if (empty($ns)) throw new OIDplusException(_L('ObjectType plugin %1 is erroneous: Namespace must not be empty',$ot));
  872.  
  873.                 // Currently, we must enforce that namespaces in objectType plugins are lowercase, because prefilterQuery() makes all namespaces lowercase and the DBMS should be case-sensitive
  874.                 if ($ns != strtolower($ns)) throw new OIDplusException(_L('ObjectType plugin %1 is erroneous: Namespace %2 must be lower-case',$ot,$ns));
  875.  
  876.                 $root = $ot::root();
  877.                 if (!str_starts_with($root,$ns.':')) throw new OIDplusException(_L('ObjectType plugin %1 is erroneous: Root node (%2) is in wrong namespace (needs starts with %3)!',$ot,$root,$ns.':'));
  878.  
  879.                 $ns_found = false;
  880.                 foreach (array_merge(OIDplus::getEnabledObjectTypes(), OIDplus::getDisabledObjectTypes()) as $test_ot) {
  881.                         if ($test_ot::ns() == $ns) {
  882.                                 $ns_found = true;
  883.                                 break;
  884.                         }
  885.                 }
  886.                 if ($ns_found) {
  887.                         throw new OIDplusException(_L('Attention: Two objectType plugins use the same namespace "%1"!',$ns));
  888.                 }
  889.  
  890.                 $init = OIDplus::config()->getValue("objecttypes_initialized");
  891.                 $init_ary = empty($init) ? array() : explode(';', $init);
  892.                 $init_ary = array_map('trim', $init_ary);
  893.  
  894.                 $enabled = OIDplus::config()->getValue("objecttypes_enabled");
  895.                 $enabled_ary = empty($enabled) ? array() : explode(';', $enabled);
  896.                 $enabled_ary = array_map('trim', $enabled_ary);
  897.  
  898.                 if (in_array($ns, $enabled_ary)) {
  899.                         // If it is in the list of enabled object types, it is enabled (obviously)
  900.                         $do_enable = true;
  901.                 } else {
  902.                         if (!OIDplus::config()->getValue('oobe_objects_done')) {
  903.                                 // If the OOBE wizard is NOT done, then just enable the "oid" object type by default
  904.                                 $do_enable = $ns == 'oid';
  905.                         } else {
  906.                                 // If the OOBE wizard was done (once), then
  907.                                 // we will enable all object types which were never initialized
  908.                                 // (i.e. a plugin folder was freshly added)
  909.                                 $do_enable = !in_array($ns, $init_ary);
  910.                         }
  911.                 }
  912.  
  913.                 if ($do_enable) {
  914.                         self::$enabledObjectTypes[] = $ot;
  915.                         usort(self::$enabledObjectTypes, function($a, $b) {
  916.                                 $enabled = OIDplus::config()->getValue("objecttypes_enabled");
  917.                                 $enabled_ary = explode(';', $enabled);
  918.  
  919.                                 $idx_a = array_search($a::ns(), $enabled_ary);
  920.                                 $idx_b = array_search($b::ns(), $enabled_ary);
  921.  
  922.                                 if ($idx_a == $idx_b) return 0;
  923.                                 return ($idx_a > $idx_b) ? +1 : -1;
  924.                         });
  925.                 } else {
  926.                         self::$disabledObjectTypes[] = $ot;
  927.                 }
  928.  
  929.                 if (!in_array($ns, $init_ary)) {
  930.                         // Was never initialized before, so we add it to the list of enabled object types once
  931.  
  932.                         if ($do_enable) {
  933.                                 $enabled_ary[] = $ns;
  934.                                 // Important: Don't validate the input, because the other object types might not be initialized yet! So use setValueNoCallback() instead setValue().
  935.                                 OIDplus::config()->setValueNoCallback("objecttypes_enabled", implode(';', $enabled_ary));
  936.                         }
  937.  
  938.                         $init_ary[] = $ns;
  939.                         OIDplus::config()->setValue("objecttypes_initialized", implode(';', $init_ary));
  940.                 }
  941.         }
  942.  
  943.         /**
  944.          * @return OIDplusObjectTypePlugin[]
  945.          */
  946.         public static function getObjectTypePlugins(): array {
  947.                 return self::$objectTypePlugins;
  948.         }
  949.  
  950.         /**
  951.          * @return OIDplusObjectTypePlugin[]
  952.          */
  953.         public static function getObjectTypePluginsEnabled(): array {
  954.                 $res = array();
  955.                 foreach (self::$objectTypePlugins as $plugin) {
  956.                         $ot = $plugin::getObjectTypeClassName();
  957.                         if (in_array($ot, self::$enabledObjectTypes)) $res[] = $plugin;
  958.                 }
  959.                 return $res;
  960.         }
  961.  
  962.         /**
  963.          * @return OIDplusObjectTypePlugin[]
  964.          */
  965.         public static function getObjectTypePluginsDisabled(): array {
  966.                 $res = array();
  967.                 foreach (self::$objectTypePlugins as $plugin) {
  968.                         $ot = $plugin::getObjectTypeClassName();
  969.                         if (in_array($ot, self::$disabledObjectTypes)) $res[] = $plugin;
  970.                 }
  971.                 return $res;
  972.         }
  973.  
  974.         /**
  975.          * @return string[]|OIDplusObject[] Classname of a OIDplusObject class
  976.          */
  977.         public static function getEnabledObjectTypes(): array {
  978.                 return self::$enabledObjectTypes;
  979.         }
  980.  
  981.         /**
  982.          * @return string[]|OIDplusObject[] Classname of a OIDplusObject class
  983.          */
  984.         public static function getDisabledObjectTypes(): array {
  985.                 return self::$disabledObjectTypes;
  986.         }
  987.  
  988.         // --- Plugin handling functions
  989.  
  990.         /**
  991.          * @return OIDplusPlugin[]
  992.          */
  993.         public static function getAllPlugins(): array {
  994.                 $res = array();
  995.                 $res = array_merge($res, self::$pagePlugins);
  996.                 $res = array_merge($res, self::$authPlugins);
  997.                 $res = array_merge($res, self::$loggerPlugins);
  998.                 $res = array_merge($res, self::$objectTypePlugins);
  999.                 $res = array_merge($res, self::$dbPlugins);
  1000.                 $res = array_merge($res, self::$captchaPlugins);
  1001.                 $res = array_merge($res, self::$sqlSlangPlugins);
  1002.                 $res = array_merge($res, self::$languagePlugins);
  1003.                 return array_merge($res, self::$designPlugins);
  1004.         }
  1005.  
  1006.         /**
  1007.          * @param string $oid
  1008.          * @return OIDplusPlugin|null
  1009.          */
  1010.         public static function getPluginByOid(string $oid)/*: ?OIDplusPlugin*/ {
  1011.                 $plugins = self::getAllPlugins();
  1012.                 foreach ($plugins as $plugin) {
  1013.                         if (oid_dotnotation_equal($plugin->getManifest()->getOid(), $oid)) {
  1014.                                 return $plugin;
  1015.                         }
  1016.                 }
  1017.                 return null;
  1018.         }
  1019.  
  1020.         /**
  1021.          * @param string $classname
  1022.          * @return OIDplusPlugin|null
  1023.          */
  1024.         public static function getPluginByClassName(string $classname)/*: ?OIDplusPlugin*/ {
  1025.                 $plugins = self::getAllPlugins();
  1026.                 foreach ($plugins as $plugin) {
  1027.                         if (get_class($plugin) === $classname) {
  1028.                                 return $plugin;
  1029.                         }
  1030.                 }
  1031.                 return null;
  1032.         }
  1033.  
  1034.         /**
  1035.          * Checks if the plugin is disabled
  1036.          * @return bool true if plugin is enabled, false if plugin is disabled
  1037.          * @throws OIDplusException if the class name or config file (disabled setting) does not contain a namespace
  1038.          */
  1039.         private static function pluginCheckDisabled($class_name): bool {
  1040.                 $path = explode('\\', $class_name);
  1041.  
  1042.                 if (count($path) == 1) {
  1043.                         throw new OIDplusException(_L('Plugin "%1" is erroneous',$class_name).': '._L('The plugin uses no namespaces. The new version of OIDplus requires plugin class files to be in a namespace. Please notify your plugin author and ask for an update.'));
  1044.                 }
  1045.  
  1046.                 $class_end = end($path);
  1047.                 if (OIDplus::baseConfig()->getValue('DISABLE_PLUGIN_'.$class_end, false)) {
  1048.                         throw new OIDplusConfigInitializationException(_L('Your base configuration file is outdated. Please change "%1" to "%2".','DISABLE_PLUGIN_'.$class_end,'DISABLE_PLUGIN_'.$class_name));
  1049.                 }
  1050.  
  1051.                 if (OIDplus::baseConfig()->getValue('DISABLE_PLUGIN_'.$class_name, false)) {
  1052.                         return false;
  1053.                 }
  1054.  
  1055.                 return true;
  1056.         }
  1057.  
  1058.         /**
  1059.          * @param string $pluginFolderMasks
  1060.          * @param bool $flat
  1061.          * @return OIDplusPluginManifest[]|array<string,array<string,OIDplusPluginManifest>>
  1062.          * @throws OIDplusException
  1063.          */
  1064.         public static function getAllPluginManifests(string $pluginFolderMasks='*', bool $flat=true): array {
  1065.                 $out = array();
  1066.                 // Note: glob() will sort by default, so we do not need a page priority attribute.
  1067.                 //       So you just need to use a numeric plugin directory prefix (padded).
  1068.                 $ary = array();
  1069.                 foreach (explode(',',$pluginFolderMasks) as $pluginFolderMask) {
  1070.                         $ary = array_merge($ary,glob(OIDplus::localpath().'plugins/'.'*'.'/'.$pluginFolderMask.'/'.'*'.'/manifest.xml'));
  1071.                 }
  1072.  
  1073.                 // Sort the plugins by their type and name, as if they would be in a single vendor-folder!
  1074.                 uasort($ary, function($a,$b) {
  1075.                         if ($a == $b) return 0;
  1076.  
  1077.                         $a = str_replace('\\', '/', $a);
  1078.                         $ary = explode('/',$a);
  1079.                         $bry = explode('/',$b);
  1080.  
  1081.                         // First sort by type (publicPage, auth, database, language, ...)
  1082.                         $a_type = $ary[count($ary)-1-2];
  1083.                         $b_type = $bry[count($bry)-1-2];
  1084.                         if ($a_type < $b_type) return -1;
  1085.                         if ($a_type > $b_type) return 1;
  1086.  
  1087.                         // Then sort by name (090_login, 100_whois, etc.)
  1088.                         $a_name = $ary[count($ary)-1-1];
  1089.                         $b_name = $bry[count($bry)-1-1];
  1090.                         if ($a_name < $b_name) return -1;
  1091.                         if ($a_name > $b_name) return 1;
  1092.  
  1093.                         // If it is still equal, then finally sort by vendorname
  1094.                         $a_vendor = $ary[count($ary)-1-3];
  1095.                         $b_vendor = $bry[count($bry)-1-3];
  1096.                         if ($a_vendor < $b_vendor) return -1;
  1097.                         if ($a_vendor > $b_vendor) return 1;
  1098.                         return 0;
  1099.                 });
  1100.  
  1101.                 foreach ($ary as $ini) {
  1102.                         if (!file_exists($ini)) continue;
  1103.  
  1104.                         $manifest = new OIDplusPluginManifest();
  1105.                         $manifest->loadManifest($ini);
  1106.  
  1107.                         $class_name = $manifest->getPhpMainClass();
  1108.                         if ($class_name) if (!self::pluginCheckDisabled($class_name)) continue;
  1109.  
  1110.                         if ($flat) {
  1111.                                 $out[] = $manifest;
  1112.                         } else {
  1113.                                 $vendor_folder = basename(dirname($ini, 3));
  1114.                                 $plugintype_folder = basename(dirname($ini, 2));
  1115.                                 $pluginname_folder = basename(dirname($ini));
  1116.  
  1117.                                 if (!isset($out[$plugintype_folder])) $out[$plugintype_folder] = array();
  1118.                                 if (!isset($out[$plugintype_folder][$vendor_folder])) $out[$plugintype_folder][$vendor_folder] = array();
  1119.                                 $out[$plugintype_folder][$vendor_folder][$pluginname_folder] = $manifest;
  1120.                         }
  1121.                 }
  1122.                 return $out;
  1123.         }
  1124.  
  1125.         /**
  1126.          * @param string|array $pluginDirName
  1127.          * @param string $expectedPluginClass
  1128.          * @param callable|null $registerCallback
  1129.          * @return string[]
  1130.          * @throws OIDplusConfigInitializationException
  1131.          * @throws OIDplusException
  1132.          * @throws \ReflectionException
  1133.          */
  1134.         public static function registerAllPlugins($pluginDirName, string $expectedPluginClass, callable $registerCallback=null): array {
  1135.                 $out = array();
  1136.                 if (is_array($pluginDirName)) {
  1137.                         $ary = array();
  1138.                         foreach ($pluginDirName as $pluginDirName_) {
  1139.                                 $ary = array_merge($ary, self::getAllPluginManifests($pluginDirName_, false));
  1140.                         }
  1141.                 } else {
  1142.                         $ary = self::getAllPluginManifests($pluginDirName, false);
  1143.                 }
  1144.                 $known_plugin_oids = array();
  1145.                 $known_main_classes_no_namespace = array();
  1146.                 foreach ($ary as $plugintype_folder => $bry) {
  1147.                         foreach ($bry as $vendor_folder => $cry) {
  1148.                                 foreach ($cry as $pluginname_folder => $manifest) {
  1149.                                         $class_name = $manifest->getPhpMainClass();
  1150.  
  1151.                                         // Before we load the plugin, we want to make some checks to confirm
  1152.                                         // that the plugin is working correctly.
  1153.  
  1154.                                         if (!$class_name) {
  1155.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Manifest does not declare a PHP main class'));
  1156.                                         }
  1157.                                         if (!self::pluginCheckDisabled($class_name)) {
  1158.                                                 continue; // Plugin is disabled
  1159.                                         }
  1160.  
  1161.                                         // The auto-loader of OIDplus currently does not accept PHP namespaces.
  1162.                                         // Reason: The autoloader detects the classes inside plugins/*/*/*/*.class.php, but it cannot know
  1163.                                         //         which namespace these files have, because their folder names do not reveal the namespace.
  1164.                                         //         So it just ignores the namespace and loads all classes with the same name.
  1165.                                         // TODO: Think about a solution; There was a discussion here https://github.com/frdl/frdl-oidplus-plugin-type-pen/issues/1
  1166.                                         $tmp = explode('\\',$class_name);
  1167.                                         $class_name_no_namespace = end($tmp);
  1168.                                         if (in_array($class_name_no_namespace, $known_main_classes_no_namespace)) {
  1169.                                                 // Removed check for now, since everything should work correctly
  1170.                                                 // throw new OIDplusException(_L('More than one plugin has the PHP class name "%1". This is currently no supported, not even if they are in different namespaces.', $class_name_no_namespace));
  1171.                                         }
  1172.                                         $known_main_classes_no_namespace[] = $class_name_no_namespace;
  1173.  
  1174.                                         // Do some basic checks on the plugin PHP main class
  1175.                                         if (!class_exists($class_name)) {
  1176.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Manifest declares PHP main class as "%1", but it could not be found', $class_name));
  1177.                                         }
  1178.                                         if (!is_subclass_of($class_name, $expectedPluginClass)) {
  1179.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Plugin main class "%1" is expected to be a subclass of "%2"', $class_name, $expectedPluginClass));
  1180.                                         }
  1181.                                         if (($class_name != $manifest->getTypeClass()) && (!is_subclass_of($class_name, $manifest->getTypeClass()))) {
  1182.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Plugin main class "%1" is expected to be a subclass of "%2", according to type declared in manifest', $class_name, $manifest->getTypeClass()));
  1183.                                         }
  1184.                                         if (($manifest->getTypeClass() != $expectedPluginClass) && (!is_subclass_of($manifest->getTypeClass(), $expectedPluginClass))) {
  1185.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Class declared in manifest is "%1" does not fit expected class for this plugin type "%2"', $manifest->getTypeClass(), $expectedPluginClass));
  1186.                                         }
  1187.  
  1188.                                         // Do some basic checks on the plugin OID
  1189.                                         $plugin_oid = $manifest->getOid();
  1190.                                         if (!$plugin_oid) {
  1191.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Does not have an OID'));
  1192.                                         }
  1193.                                         if (!oid_valid_dotnotation($plugin_oid, false, false, 2)) {
  1194.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Plugin OID "%1" is invalid (needs to be valid dot-notation)', $plugin_oid));
  1195.                                         }
  1196.                                         if (isset($known_plugin_oids[$plugin_oid])) {
  1197.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('The OID "%1" is already used by the plugin "%2"', $plugin_oid, $known_plugin_oids[$plugin_oid]));
  1198.                                         }
  1199.  
  1200.                                         // Additional check: Are third-party plugins using ViaThinkSoft plugin folders, OIDs or class namespaces?
  1201.                                         $full_plugin_dir = dirname($manifest->getManifestFile());
  1202.                                         $full_plugin_dir = substr($full_plugin_dir, strlen(OIDplus::localpath()));
  1203.                                         $dir_is_viathinksoft = str_starts_with($full_plugin_dir, 'plugins/viathinksoft/') || str_starts_with($full_plugin_dir, 'plugins\\viathinksoft\\');
  1204.                                         $oid_is_viathinksoft = str_starts_with($plugin_oid, '1.3.6.1.4.1.37476.2.5.2.4.'); // { iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 37476 products(2) oidplus(5) v2(2) plugins(4) }
  1205.                                         $class_is_viathinksoft = str_starts_with($class_name, 'ViaThinkSoft\\');
  1206.                                         if ($oid_is_viathinksoft != $class_is_viathinksoft) {
  1207.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Third-party plugins must not use the ViaThinkSoft PHP namespace. Please use your own vendor namespace.'));
  1208.                                         }
  1209.                                         $plugin_is_viathinksoft = $oid_is_viathinksoft && $class_is_viathinksoft;
  1210.                                         if ($dir_is_viathinksoft != $plugin_is_viathinksoft) {
  1211.                                                 throw new OIDplusException(_L('Plugin "%1" is misplaced', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('The plugin is in the wrong folder. The folder %1 can only be used by official ViaThinkSoft plugins', 'plugins/viathinksoft/'));
  1212.                                         }
  1213.  
  1214.                                         // Additional check: does the plugin define JS/CSS although it is not an interactive plugin type?
  1215.                                         $has_js = $manifest->getJSFiles();
  1216.                                         $has_css = $manifest->getCSSFiles();
  1217.                                         $is_interactive = in_array(basename($plugintype_folder), OIDplus::INTERACTIVE_PLUGIN_TYPES);
  1218.                                         $is_design = basename($plugintype_folder) === 'design';
  1219.                                         if (!$is_interactive && $has_js) {
  1220.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('%1 files are included in the manifest XML, but this plugin type does not allow such files.', 'JavaScript'));
  1221.                                         }
  1222.                                         if (!$is_interactive && !$is_design && $has_css) {
  1223.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('%1 files are included in the manifest XML, but this plugin type does not allow such files.', 'CSS'));
  1224.                                         }
  1225.  
  1226.                                         // Additional check: Check "Setup CSS" and "Setup JS" (Allowed for plugin types: database, captcha)
  1227.                                         $has_js_setup = $manifest->getJSFilesSetup();
  1228.                                         $has_css_setup = $manifest->getCSSFilesSetup();
  1229.                                         $is_database = basename($plugintype_folder) === 'database';
  1230.                                         $is_captcha = basename($plugintype_folder) === 'captcha';
  1231.                                         if (!$is_database && !$is_captcha && $has_js_setup) {
  1232.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('%1 files are included in the manifest XML, but this plugin type does not allow such files.', 'Setup JavaScript'));
  1233.                                         }
  1234.                                         if (!$is_database && !$is_captcha && $has_css_setup) {
  1235.                                                 throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('%1 files are included in the manifest XML, but this plugin type does not allow such files.', 'Setup CSS'));
  1236.                                         }
  1237.  
  1238.                                         // Additional check: Are all CSS/JS files there?
  1239.                                         $tmp = $manifest->getManifestLinkedFiles();
  1240.                                         foreach ($tmp as $file) {
  1241.                                                 if (!file_exists($file)) {
  1242.                                                         throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('File %1 was defined in manifest, but it is not existing', $file));
  1243.                                                 }
  1244.                                         }
  1245.  
  1246.                                         // For the next check, we need an instance of the object
  1247.                                         $obj = new $class_name();
  1248.  
  1249.                                         // Now we can continue
  1250.                                         $known_plugin_oids[$plugin_oid] = $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder;
  1251.                                         $out[] = $class_name;
  1252.                                         if (!is_null($registerCallback)) {
  1253.                                                 call_user_func($registerCallback, $obj);
  1254.  
  1255.                                                 // Alternative approaches:
  1256.                                                 //$registerCallback[0]::{$registerCallback[1]}($obj);
  1257.                                                 // or:
  1258.                                                 //forward_static_call($registerCallback, $obj);
  1259.                                         }
  1260.                                 }
  1261.                         }
  1262.                 }
  1263.                 return $out;
  1264.         }
  1265.  
  1266.         // --- Initialization of OIDplus
  1267.  
  1268.         /**
  1269.          * @param bool $html
  1270.          * @param bool $keepBaseConfig
  1271.          * @return void
  1272.          * @throws OIDplusConfigInitializationException|OIDplusException|\ReflectionException
  1273.          */
  1274.         public static function init(bool $html=true, bool $keepBaseConfig=true) {
  1275.                 self::$html = $html;
  1276.  
  1277.                 // Reset internal state, so we can re-init verything if required
  1278.  
  1279.                 self::$config = null;
  1280.                 if (!$keepBaseConfig) self::$baseConfig = null;  // for test cases we need to be able to control base config and setting values manually, so $keepBaseConfig needs to be true
  1281.                 self::$gui = null;
  1282.                 self::$authUtils = null;
  1283.                 self::$mailUtils = null;
  1284.                 self::$menuUtils = null;
  1285.                 self::$logger = null;
  1286.                 self::$dbMainSession = null;
  1287.                 self::$dbIsolatedSession = null;
  1288.                 self::$pagePlugins = array();
  1289.                 self::$authPlugins = array();
  1290.                 self::$loggerPlugins = array();
  1291.                 self::$objectTypePlugins = array();
  1292.                 self::$enabledObjectTypes = array();
  1293.                 self::$disabledObjectTypes = array();
  1294.                 self::$dbPlugins = array();
  1295.                 self::$captchaPlugins = array();
  1296.                 self::$sqlSlangPlugins = array();
  1297.                 self::$languagePlugins = array();
  1298.                 self::$designPlugins = array();
  1299.                 self::$system_id_cache = null;
  1300.                 self::$sslAvailableCache = null;
  1301.                 self::$translationArray = array();
  1302.  
  1303.                 // Continue...
  1304.  
  1305.                 OIDplus::baseConfig(); // this loads the base configuration located in userdata/baseconfig/config.inc.php (once!)
  1306.                 // You can do changes to the configuration afterwards using OIDplus::baseConfig()->...
  1307.  
  1308.                 // Register database types (highest priority)
  1309.  
  1310.                 // SQL slangs
  1311.  
  1312.                 self::registerAllPlugins('sqlSlang', OIDplusSqlSlangPlugin::class, array(OIDplus::class,'registerSqlSlangPlugin'));
  1313.                 foreach (OIDplus::getSqlSlangPlugins() as $plugin) {
  1314.                         $plugin->init($html);
  1315.                 }
  1316.  
  1317.                 // Database providers
  1318.  
  1319.                 self::registerAllPlugins('database', OIDplusDatabasePlugin::class, array(OIDplus::class,'registerDatabasePlugin'));
  1320.                 foreach (OIDplus::getDatabasePlugins() as $plugin) {
  1321.                         $plugin->init($html);
  1322.                 }
  1323.  
  1324.                 // Do redirect stuff etc.
  1325.  
  1326.                 self::isSslAvailable(); // This function does automatic redirects
  1327.  
  1328.                 // Construct the configuration manager
  1329.  
  1330.                 OIDplus::config(); // During the construction, various system settings are prepared if required
  1331.  
  1332.                 // Initialize public / private keys
  1333.  
  1334.                 OIDplus::getPkiStatus(true);
  1335.  
  1336.                 // Register non-DB plugins
  1337.  
  1338.                 self::registerAllPlugins(array('publicPages', 'raPages', 'adminPages'), OIDplusPagePlugin::class, array(OIDplus::class,'registerPagePlugin'));
  1339.                 self::registerAllPlugins('auth', OIDplusAuthPlugin::class, array(OIDplus::class,'registerAuthPlugin'));
  1340.                 self::registerAllPlugins('logger', OIDplusLoggerPlugin::class, array(OIDplus::class,'registerLoggerPlugin'));
  1341.                 self::logger()->reLogMissing(); // Some previous plugins might have tried to log. Repeat that now.
  1342.                 self::registerAllPlugins('objectTypes', OIDplusObjectTypePlugin::class, array(OIDplus::class,'registerObjectTypePlugin'));
  1343.                 self::registerAllPlugins('language', OIDplusLanguagePlugin::class, array(OIDplus::class,'registerLanguagePlugin'));
  1344.                 self::registerAllPlugins('design', OIDplusDesignPlugin::class, array(OIDplus::class,'registerDesignPlugin'));
  1345.                 self::registerAllPlugins('captcha', OIDplusCaptchaPlugin::class, array(OIDplus::class,'registerCaptchaPlugin'));
  1346.  
  1347.                 // Initialize non-DB plugins
  1348.  
  1349.                 foreach (OIDplus::getPagePlugins() as $plugin) {
  1350.                         $plugin->init($html);
  1351.                 }
  1352.                 foreach (OIDplus::getAuthPlugins() as $plugin) {
  1353.                         $plugin->init($html);
  1354.                 }
  1355.                 foreach (OIDplus::getLoggerPlugins() as $plugin) {
  1356.                         $plugin->init($html);
  1357.                 }
  1358.                 foreach (OIDplus::getObjectTypePlugins() as $plugin) {
  1359.                         $plugin->init($html);
  1360.                 }
  1361.                 foreach (OIDplus::getLanguagePlugins() as $plugin) {
  1362.                         $plugin->init($html);
  1363.                 }
  1364.                 foreach (OIDplus::getDesignPlugins() as $plugin) {
  1365.                         $plugin->init($html);
  1366.                 }
  1367.                 foreach (OIDplus::getCaptchaPlugins() as $plugin) {
  1368.                         $plugin->init($html);
  1369.                 }
  1370.  
  1371.                 if (PHP_SAPI != 'cli') {
  1372.  
  1373.                         // Prepare some security related response headers (default values)
  1374.  
  1375.                         $content_language =
  1376.                                 strtolower(substr(OIDplus::getCurrentLang(),0,2)) . '-' .
  1377.                                 strtoupper(substr(OIDplus::getCurrentLang(),2,2)); // e.g. 'en-US'
  1378.  
  1379.                         $http_headers = array(
  1380.                                 "X-Content-Type-Options" => "nosniff",
  1381.                                 "X-XSS-Protection" => "1; mode=block",
  1382.                                 "X-Frame-Options" => "SAMEORIGIN",
  1383.                                 "Referrer-Policy" => array(
  1384.                                         "no-referrer-when-downgrade"
  1385.                                 ),
  1386.                                 "Cache-Control" => array(
  1387.                                         "no-cache",
  1388.                                         "no-store",
  1389.                                         "must-revalidate"
  1390.                                 ),
  1391.                                 "Pragma" => "no-cache",
  1392.                                 "Content-Language" => $content_language,
  1393.                                 "Expires" => "0",
  1394.                                 "Content-Security-Policy" => array(
  1395.                                         // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
  1396.  
  1397.                                         // --- Fetch directives ---
  1398.                                         "child-src" => array(
  1399.                                                 "'self'",
  1400.                                                 "blob:"
  1401.                                         ),
  1402.                                         "connect-src" => array(
  1403.                                                 "'self'",
  1404.                                                 "blob:"
  1405.                                         ),
  1406.                                         "default-src" => array(
  1407.                                                 "'self'",
  1408.                                                 "blob:",
  1409.                                                 "https://cdnjs.cloudflare.com/"
  1410.                                         ),
  1411.                                         "font-src" => array(
  1412.                                                 "'self'",
  1413.                                                 "blob:"
  1414.                                         ),
  1415.                                         "frame-src" => array(
  1416.                                                 "'self'",
  1417.                                                 "blob:"
  1418.                                         ),
  1419.                                         "img-src" => array(
  1420.                                                 "blob:",
  1421.                                                 "data:",
  1422.                                                 "http:",
  1423.                                                 "https:"
  1424.                                         ),
  1425.                                         "manifest-src" => array(
  1426.                                                 "'self'",
  1427.                                                 "blob:"
  1428.                                         ),
  1429.                                         "media-src" => array(
  1430.                                                 "'self'",
  1431.                                                 "blob:"
  1432.                                         ),
  1433.                                         "object-src" => array(
  1434.                                                 "'none'"
  1435.                                         ),
  1436.                                         "script-src" => array(
  1437.                                                 "'self'",
  1438.                                                 "'unsafe-inline'",
  1439.                                                 "'unsafe-eval'",
  1440.                                                 "blob:",
  1441.                                                 "https://cdnjs.cloudflare.com/",
  1442.                                                 "https://polyfill.io/"
  1443.                                         ),
  1444.                                         // script-src-elem not used
  1445.                                         // script-src-attr not used
  1446.                                         "style-src" => array(
  1447.                                                 "'self'",
  1448.                                                 "'unsafe-inline'",
  1449.                                                 "https://cdnjs.cloudflare.com/"
  1450.                                         ),
  1451.                                         // style-src-elem not used
  1452.                                         // style-src-attr not used
  1453.                                         "worker-src" => array(
  1454.                                                 "'self'",
  1455.                                                 "blob:"
  1456.                                         ),
  1457.  
  1458.                                         // --- Navigation directives ---
  1459.                                         "frame-ancestors" => array(
  1460.                                                 "'none'"
  1461.                                         ),
  1462.                                 )
  1463.                         );
  1464.  
  1465.                         // Give plugins the opportunity to manipulate/extend the headers
  1466.  
  1467.                         foreach (OIDplus::getSqlSlangPlugins() as $plugin) {
  1468.                                 $plugin->httpHeaderCheck($http_headers);
  1469.                         }
  1470.                         //foreach (OIDplus::getDatabasePlugins() as $plugin) {
  1471.                         if ($plugin = OIDplus::getActiveDatabasePlugin()) {
  1472.                                 $plugin->httpHeaderCheck($http_headers);
  1473.                         }
  1474.                         foreach (OIDplus::getPagePlugins() as $plugin) {
  1475.                                 $plugin->httpHeaderCheck($http_headers);
  1476.                         }
  1477.                         foreach (OIDplus::getAuthPlugins() as $plugin) {
  1478.                                 $plugin->httpHeaderCheck($http_headers);
  1479.                         }
  1480.                         foreach (OIDplus::getLoggerPlugins() as $plugin) {
  1481.                                 $plugin->httpHeaderCheck($http_headers);
  1482.                         }
  1483.                         foreach (OIDplus::getObjectTypePlugins() as $plugin) {
  1484.                                 $plugin->httpHeaderCheck($http_headers);
  1485.                         }
  1486.                         foreach (OIDplus::getLanguagePlugins() as $plugin) {
  1487.                                 $plugin->httpHeaderCheck($http_headers);
  1488.                         }
  1489.                         foreach (OIDplus::getDesignPlugins() as $plugin) {
  1490.                                 $plugin->httpHeaderCheck($http_headers);
  1491.                         }
  1492.                         //foreach (OIDplus::getCaptchaPlugins() as $plugin) {
  1493.                         if ($plugin = OIDplus::getActiveCaptchaPlugin()) {
  1494.                                 $plugin->httpHeaderCheck($http_headers);
  1495.                         }
  1496.  
  1497.                         // Prepare to send the headers to the client
  1498.                         // The headers are sent automatically when the first output comes or the script ends
  1499.  
  1500.                         foreach ($http_headers as $name => $val) {
  1501.  
  1502.                                 // Plugins can remove standard OIDplus headers by setting the value to null.
  1503.                                 if (is_null($val)) continue;
  1504.  
  1505.                                 // Some headers can be written as arrays to make it easier for plugin authors
  1506.                                 // to manipulate/extend the contents.
  1507.                                 if (is_array($val)) {
  1508.                                         if ((strtolower($name) == 'cache-control') ||
  1509.                                                 (strtolower($name) == 'referrer-policy'))
  1510.                                         {
  1511.                                                 if (count($val) == 0) continue;
  1512.                                                 $val = implode(', ', $val);
  1513.                                         } else if (strtolower($name) == 'content-security-policy') {
  1514.                                                 if (count($val) == 0) continue;
  1515.                                                 foreach ($val as $tmp1 => &$tmp2) {
  1516.                                                         $tmp2 = array_unique($tmp2);
  1517.                                                         $tmp2 = $tmp1.' '.implode(' ', $tmp2);
  1518.                                                 }
  1519.                                                 $val = implode('; ', $val);
  1520.                                         } else {
  1521.                                                 throw new OIDplusException(_L('HTTP header "%1" cannot be written as array. A newly installed plugin is probably misusing the method "%2".',$name,'httpHeaderCheck'));
  1522.                                         }
  1523.                                 }
  1524.  
  1525.                                 if (is_string($val)) {
  1526.                                         @header("$name: $val");
  1527.                                 }
  1528.                         }
  1529.  
  1530.                 } // endif (PHP_SAPI != 'cli')
  1531.  
  1532.                 // Initialize other stuff (i.e. things which require the logger!)
  1533.  
  1534.                 OIDplus::recognizeSystemUrl(); // Make sure "last_known_system_url" is set
  1535.                 OIDplus::recognizeVersion(); // Make sure "last_known_version" is set and a log entry is created
  1536.         }
  1537.  
  1538.         // --- System URL, System ID, PKI, and other functions
  1539.  
  1540.         /**
  1541.          * @return void
  1542.          */
  1543.         private static function recognizeSystemUrl() {
  1544.                 try {
  1545.                         $url = OIDplus::webpath(null,self::PATH_ABSOLUTE_CANONICAL);
  1546.                         OIDplus::config()->setValue('last_known_system_url', $url);
  1547.                 } catch (\Exception $e) {
  1548.                 }
  1549.         }
  1550.  
  1551.         /**
  1552.          * @return false|int
  1553.          */
  1554.         private static function getExecutingScriptPathDepth() {
  1555.                 if (PHP_SAPI == 'cli') {
  1556.                         global $argv;
  1557.                         $test_dir = dirname(realpath($argv[0]));
  1558.                 } else {
  1559.                         if (!isset($_SERVER["SCRIPT_FILENAME"])) return false;
  1560.                         $test_dir = dirname($_SERVER['SCRIPT_FILENAME']);
  1561.                 }
  1562.                 $test_dir = str_replace('\\', '/', $test_dir);
  1563.                 $steps_up = 0;
  1564.                 while (!file_exists($test_dir.'/oidplus.min.css.php')) { // We just assume that only the OIDplus base directory contains "oidplus.min.css.php" and not any subordinate directory!
  1565.                         $test_dir = dirname($test_dir);
  1566.                         $steps_up++;
  1567.                         if ($steps_up == 1000) return false; // to make sure there will never be an infinite loop
  1568.                 }
  1569.                 return $steps_up;
  1570.         }
  1571.  
  1572.         /**
  1573.          * @return bool
  1574.          */
  1575.         public static function isSSL(): bool {
  1576.                 return isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on');
  1577.         }
  1578.  
  1579.         /**
  1580.          * Returns the URL of the system.
  1581.          * @param int $mode If true or OIDplus::PATH_RELATIVE, the returning path is relative to the currently executed
  1582.          *                  PHP script (i.e. index.php , not the plugin PHP script!). False or OIDplus::PATH_ABSOLUTE is
  1583.          *                  results in an absolute URL. OIDplus::PATH_ABSOLUTE_CANONICAL is an absolute URL,
  1584.          *                  but a canonical path (set by base config setting CANONICAL_SYSTEM_URL) is preferred.
  1585.          * @return string|false The URL, with guaranteed trailing path delimiter for directories
  1586.          * @throws OIDplusException
  1587.          */
  1588.         private static function getSystemUrl(int $mode) {
  1589.                 if ($mode === self::PATH_RELATIVE) {
  1590.                         $steps_up = self::getExecutingScriptPathDepth();
  1591.                         if ($steps_up === false) {
  1592.                                 return false;
  1593.                         } else {
  1594.                                 return str_repeat('../', $steps_up);
  1595.                         }
  1596.                 } else {
  1597.                         if ($mode === self::PATH_ABSOLUTE_CANONICAL) {
  1598.                                 $tmp = OIDplus::baseConfig()->getValue('CANONICAL_SYSTEM_URL', '');
  1599.                                 if ($tmp) {
  1600.                                         return rtrim($tmp,'/').'/';
  1601.                                 }
  1602.                         }
  1603.  
  1604.                         if (PHP_SAPI == 'cli') {
  1605.                                 try {
  1606.                                         return OIDplus::config()->getValue('last_known_system_url', false);
  1607.                                 } catch (\Exception $e) {
  1608.                                         return false;
  1609.                                 }
  1610.                         } else {
  1611.                                 // First, try to find out how many levels we need to go up
  1612.                                 $steps_up = self::getExecutingScriptPathDepth();
  1613.  
  1614.                                 // Then go up these amount of levels, based on SCRIPT_NAME/argv[0]
  1615.                                 $res = dirname($_SERVER['SCRIPT_NAME'].'index.php'); // This fake 'index.php' ensures that SCRIPT_NAME does not end with '/', which would make dirname() fail
  1616.                                 for ($i=0; $i<$steps_up; $i++) {
  1617.                                         $res = dirname($res);
  1618.                                 }
  1619.                                 $res = str_replace('\\', '/', $res);
  1620.                                 if ($res == '/') $res = '';
  1621.  
  1622.                                 // Add protocol and hostname
  1623.                                 $is_ssl = self::isSSL();
  1624.                                 $protocol = $is_ssl ? 'https' : 'http'; // do not translate
  1625.                                 $host = $_SERVER['HTTP_HOST']; // includes port if it is not 80/443
  1626.  
  1627.                                 return $protocol.'://'.$host.$res.'/';
  1628.                         }
  1629.                 }
  1630.         }
  1631.  
  1632.         /**
  1633.          * @param string $pubKey
  1634.          * @return false|string
  1635.          */
  1636.         private static function pubKeyToRaw(string $pubKey) {
  1637.                 $m = array();
  1638.                 if (preg_match('@BEGIN PUBLIC KEY\\-+([^\\-]+)\\-+END PUBLIC KEY@imU', $pubKey, $m)) {
  1639.                         return base64_decode($m[1], false);
  1640.                 }
  1641.                 return false;
  1642.         }
  1643.  
  1644.         /**
  1645.          * @param string $pubKey
  1646.          * @return false|int
  1647.          */
  1648.         private static function getSystemIdFromPubKey(string $pubKey) {
  1649.                 $rawData = self::pubKeyToRaw($pubKey);
  1650.                 if ($rawData === false) return false;
  1651.                 return smallhash($rawData);
  1652.         }
  1653.  
  1654.         /**
  1655.          * @param string $pubKey
  1656.          * @return false|string
  1657.          */
  1658.         private static function getSystemGuidFromPubKey(string $pubKey) {
  1659.                 /*
  1660.                 $rawData = self::pubKeyToRaw($pubKey);
  1661.                 if ($rawData === false) return false;
  1662.                 $normalizedBase64 = base64_encode($rawData);
  1663.                 return gen_uuid_sha1_namebased(self::UUID_NAMEBASED_NS_Base64PubKey, $normalizedBase64);
  1664.                 */
  1665.  
  1666.                 // https://github.com/danielmarschall/oidplus/blob/master/doc/oidplus_custom_guid.md
  1667.                 $sysid = OIDplus::getSystemId(false);
  1668.                 $sysid_int = $sysid ? $sysid : 0;
  1669.                 return gen_uuid_v8(
  1670.                         dechex($sysid_int),
  1671.                         dechex(0), // Creation time of the system unknown
  1672.                         dechex(0), // Reserved
  1673.                         dechex(0), // 0=System, otherwise Object Namespace
  1674.                         sha1('') // Objectname, empty string for system UUID
  1675.                 );
  1676.         }
  1677.  
  1678.         private static $system_id_cache = null;
  1679.  
  1680.         /**
  1681.          * @param bool $oid
  1682.          * @return false|string
  1683.          * @throws OIDplusException
  1684.          */
  1685.         public static function getSystemId(bool $oid=false) {
  1686.                 if (!is_null(self::$system_id_cache)) {
  1687.                         $out = self::$system_id_cache;
  1688.                 } else {
  1689.                         $out = false;
  1690.  
  1691.                         if (self::getPkiStatus(true)) {
  1692.                                 $pubKey = OIDplus::getSystemPublicKey();
  1693.                                 $out = self::getSystemIdFromPubKey($pubKey);
  1694.                         }
  1695.                         self::$system_id_cache = $out;
  1696.                 }
  1697.                 if (!$out) return false;
  1698.                 return ($oid ? '1.3.6.1.4.1.37476.30.9.' : '').$out;
  1699.         }
  1700.  
  1701.         private static $system_guid_cache = null;
  1702.  
  1703.         /**
  1704.          * @return false|string
  1705.          * @throws OIDplusException
  1706.          */
  1707.         public static function getSystemGuid() {
  1708.                 if (!is_null(self::$system_guid_cache)) {
  1709.                         $out = self::$system_guid_cache;
  1710.                 } else {
  1711.                         $out = false;
  1712.  
  1713.                         if (self::getPkiStatus(true)) {
  1714.                                 $pubKey = OIDplus::getSystemPublicKey();
  1715.                                 $out = self::getSystemGuidFromPubKey($pubKey);
  1716.                         }
  1717.                         self::$system_guid_cache = $out;
  1718.                 }
  1719.                 if (!$out) return false;
  1720.                 return $out;
  1721.         }
  1722.  
  1723.         /**
  1724.          * @return array|string
  1725.          */
  1726.         public static function getOpenSslCnf() {
  1727.                 // The following functions need a config file, otherway they don't work
  1728.                 // - openssl_csr_new
  1729.                 // - openssl_csr_sign
  1730.                 // - openssl_pkey_export
  1731.                 // - openssl_pkey_export_to_file
  1732.                 // - openssl_pkey_new
  1733.                 $tmp = @getenv('OPENSSL_CONF');
  1734.                 if ($tmp && file_exists($tmp)) return $tmp;
  1735.  
  1736.                 // OpenSSL in XAMPP does not work OOBE, since the OPENSSL_CONF is
  1737.                 // C:/xampp/apache/bin/openssl.cnf and not C:/xampp/apache/conf/openssl.cnf
  1738.                 // Bug reports are more than 10 years old and nobody cares...
  1739.                 // Use our own config file
  1740.                 return __DIR__.'/../../vendor/phpseclib/phpseclib/phpseclib/openssl.cnf';
  1741.         }
  1742.  
  1743.         /**
  1744.          * @return string
  1745.          */
  1746.         private static function getPrivKeyPassphraseFilename(): string {
  1747.                 return OIDplus::localpath() . 'userdata/privkey_secret.php';
  1748.         }
  1749.  
  1750.         /**
  1751.          * @return void
  1752.          */
  1753.         private static function tryCreatePrivKeyPassphrase() {
  1754.                 $file = self::getPrivKeyPassphraseFilename();
  1755.  
  1756.                 $passphrase = generateRandomString(64);
  1757.                 $cont = "<?php\n";
  1758.                 $cont .= "// ATTENTION! This file was automatically generated by OIDplus to encrypt the private key\n";
  1759.                 $cont .= "// that is located in your database configuration table. DO NOT ALTER OR DELETE THIS FILE,\n";
  1760.                 $cont .= "// otherwise you will lose your OIDplus System-ID and all services connected with it!\n";
  1761.                 $cont .= "// If multiple systems access the same database, then this file must be synchronous\n";
  1762.                 $cont .= "// between all systems, otherwise you will lose your system ID, too!\n";
  1763.                 $cont .= "\$passphrase = '$passphrase';\n";
  1764.                 $cont .= "// End of file\n";
  1765.  
  1766.                 @file_put_contents($file, $cont);
  1767.         }
  1768.  
  1769.         /**
  1770.          * @return string|false
  1771.          */
  1772.         private static function getPrivKeyPassphrase() {
  1773.                 $file = self::getPrivKeyPassphraseFilename();
  1774.                 if (!file_exists($file)) return false;
  1775.                 $cont = file_get_contents($file);
  1776.                 $m = array();
  1777.                 if (!preg_match("@'(.+)'@isU", $cont, $m)) return false;
  1778.                 return $m[1];
  1779.         }
  1780.  
  1781.         /**
  1782.          * Returns the private key of the system
  1783.          * @param bool $auto_decrypt Try to decrypt the key in case it is encrypted.
  1784.          * @return string|false
  1785.          * @throws OIDplusException
  1786.          */
  1787.         public static function getSystemPrivateKey(bool $auto_decrypt=true) {
  1788.                 $privKey = OIDplus::config()->getValue('oidplus_private_key');
  1789.                 if ($privKey == '') return false;
  1790.  
  1791.                 if (is_privatekey_encrypted($privKey)) {
  1792.                         if (!$auto_decrypt) {
  1793.                                 return false;
  1794.                         }
  1795.  
  1796.                         $passphrase = self::getPrivKeyPassphrase();
  1797.                         if ($passphrase === false) {
  1798.                                 return false;
  1799.                         }
  1800.  
  1801.                         $privKey = decrypt_private_key($privKey, $passphrase);
  1802.                         if (($privKey === false) || is_privatekey_encrypted($privKey)) {
  1803.                                 // This can happen if the key file has vanished or decryption failed because of another reason
  1804.                                 return false;
  1805.                         }
  1806.                 }
  1807.  
  1808.                 return $privKey;
  1809.         }
  1810.  
  1811.         /**
  1812.          * @return string|false
  1813.          * @throws OIDplusException
  1814.          */
  1815.         public static function getSystemPublicKey() {
  1816.                 $pubKey = OIDplus::config()->getValue('oidplus_public_key');
  1817.                 if ($pubKey == '') return false;
  1818.                 return $pubKey;
  1819.         }
  1820.  
  1821.         /**
  1822.          * @param bool $try_generate
  1823.          * @return bool
  1824.          * @throws OIDplusException
  1825.          */
  1826.         public static function getPkiStatus(bool $try_generate=false): bool {
  1827.                 if (!function_exists('openssl_pkey_new')) return false;
  1828.  
  1829.                 if (basename($_SERVER['SCRIPT_NAME']) == 'test_database_plugins.php') return false; // database switching will destroy keys because of the secret file
  1830.  
  1831.                 if ($try_generate) {
  1832.                         // For debug purposes: Invalidate current key once:
  1833.                         //OIDplus::config()->setValue('oidplus_private_key', '');
  1834.  
  1835.                         $privKey = OIDplus::getSystemPrivateKey();
  1836.                         $pubKey = OIDplus::getSystemPublicKey();
  1837.                         if (!verify_private_public_key($privKey, $pubKey)) {
  1838.                                 if ($pubKey) {
  1839.                                         OIDplus::logger()->log("V2:[CRIT]A", "The private/public key-pair is broken. A new key-pair will now be generated for your system. Your System-ID will change.");
  1840.                                 }
  1841.  
  1842.                                 $pkey_config = array(
  1843.                                         "digest_alg" => "sha512",
  1844.                                         "private_key_bits" => defined('OPENSSL_SUPPLEMENT') ? 1024 : 2048, // openssl_supplement.inc.php is based on phpseclib, which is very slow. So we use 1024 bits instead of 2048 bits
  1845.                                         "private_key_type" => OPENSSL_KEYTYPE_RSA,
  1846.                                         "config" => OIDplus::getOpenSslCnf()
  1847.                                 );
  1848.  
  1849.                                 // Create the private and public key
  1850.                                 $res = openssl_pkey_new($pkey_config);
  1851.                                 if ($res === false) return false;
  1852.  
  1853.                                 // Extract the private key from $res to $privKey
  1854.                                 if (openssl_pkey_export($res, $privKey, null, $pkey_config) === false) return false;
  1855.  
  1856.                                 // Extract the public key from $res to $pubKey
  1857.                                 $tmp = openssl_pkey_get_details($res);
  1858.                                 if ($tmp === false) return false;
  1859.                                 $pubKey = $tmp["key"];
  1860.  
  1861.                                 // encrypt new keys using a passphrase stored in a secret file
  1862.                                 self::tryCreatePrivKeyPassphrase(); // *try* (re)generate this file
  1863.                                 $passphrase = self::getPrivKeyPassphrase();
  1864.                                 if ($passphrase !== false) {
  1865.                                         $privKey = encrypt_private_key($privKey, $passphrase);
  1866.                                 }
  1867.  
  1868.                                 // Calculate the system ID from the public key
  1869.                                 $system_id = self::getSystemIdFromPubKey($pubKey);
  1870.                                 if ($system_id !== false) {
  1871.                                         // Save the key pair to database
  1872.                                         OIDplus::config()->setValue('oidplus_private_key', $privKey);
  1873.                                         OIDplus::config()->setValue('oidplus_public_key', $pubKey);
  1874.  
  1875.                                         // Log the new system ID
  1876.                                         OIDplus::logger()->log("V2:[INFO]A", "A new private/public key-pair for your system had been generated. Your SystemID is now %1", $system_id);
  1877.                                 }
  1878.                         } else {
  1879.                                 $passphrase = self::getPrivKeyPassphrase();
  1880.                                 $rawPrivKey = OIDplus::config()->getValue('oidplus_private_key');
  1881.                                 if (($passphrase === false) || !is_privatekey_encrypted($rawPrivKey)) {
  1882.                                         // Upgrade to new encrypted keys
  1883.                                         self::tryCreatePrivKeyPassphrase(); // *try* generate this file
  1884.                                         $passphrase = self::getPrivKeyPassphrase();
  1885.                                         if ($passphrase !== false) {
  1886.                                                 $privKey = encrypt_private_key($privKey, $passphrase);
  1887.                                                 OIDplus::logger()->log("V2:[INFO]A", "The private/public key-pair has been upgraded to an encrypted key-pair. The key is saved in %1", self::getPrivKeyPassphraseFilename());
  1888.                                                 OIDplus::config()->setValue('oidplus_private_key', $privKey);
  1889.                                         }
  1890.                                 }
  1891.                         }
  1892.                 }
  1893.  
  1894.                 $privKey = OIDplus::getSystemPrivateKey();
  1895.                 $pubKey = OIDplus::getSystemPublicKey();
  1896.                 return verify_private_public_key($privKey, $pubKey);
  1897.         }
  1898.  
  1899.         /**
  1900.          * @return string|void
  1901.          */
  1902.         public static function getInstallType() {
  1903.                 $counter = 0;
  1904.  
  1905.                 if ($svn_dir_exists = (OIDplus::findSvnFolder() !== false)) {
  1906.                         $counter++;
  1907.                 }
  1908.                 if ($git_dir_exists = (OIDplus::findGitFolder() !== false)) {
  1909.                         $counter++;
  1910.                 }
  1911.  
  1912.                 if ($counter > 1) {
  1913.                         return 'ambigous'; // do not translate
  1914.                 }
  1915.                 else if ($svn_dir_exists) {
  1916.                         return 'svn-wc'; // do not translate
  1917.                 }
  1918.                 else if ($git_dir_exists) {
  1919.                         return 'git-wc'; // do not translate
  1920.                 }
  1921.                 else {
  1922.                         return 'manual'; // do not translate
  1923.                 }
  1924.         }
  1925.  
  1926.         /**
  1927.          * @return void
  1928.          */
  1929.         private static function recognizeVersion() {
  1930.                 try {
  1931.                         if ($ver_now = OIDplus::getVersion()) {
  1932.                                 $ver_prev = OIDplus::config()->getValue("last_known_version");
  1933.                                 if (($ver_prev) && ($ver_now != $ver_prev)) {
  1934.                                         // TODO: Problem: When the system was updated using SVN or GIT in the console, then the IP address of the next random visitor of the website is logged!
  1935.                                         //       Idea: Maybe we should extend the mask code with some kind of magic constant "[NO_IP]", so that no IP is logged for that event?
  1936.                                         OIDplus::logger()->log("V2:[INFO]A", "Detected system version change from '%1' to '%2'", $ver_prev, $ver_now);
  1937.  
  1938.                                         // Just to be sure, recanonize objects (we don't do it at every page visit due to performance reasons)
  1939.                                         self::recanonizeObjects();
  1940.                                 }
  1941.                                 OIDplus::config()->setValue("last_known_version", $ver_now);
  1942.                         }
  1943.                 } catch (\Exception $e) {
  1944.                 }
  1945.         }
  1946.  
  1947.         /**
  1948.          * @param string $infoFile Path to a changelog.json.php file (It must be in its source code form!)
  1949.          * @param bool $allow_dev_version If set to false, then versions ending with "-dev" will be ignored
  1950.          * @return false|string
  1951.          */
  1952.         public static function getVersion(string $infoFile = __DIR__.'/../../changelog.json.php', bool $allow_dev_version=true) {
  1953.                 static $cachedVersion = [];
  1954.                 if ($cachedVersion[$infoFile] ?? false) {
  1955.                         return $cachedVersion[$infoFile];
  1956.                 }
  1957.  
  1958.                 if ((stripos($infoFile,'http://')===0) || (stripos($infoFile,'https://')===0)) {
  1959.                         $cont = @url_get_contents($infoFile);
  1960.                 } else {
  1961.                         $cont = @file_get_contents($infoFile);
  1962.                 }
  1963.                 if ($cont === false) return false;
  1964.                 $json = @json_decode($cont, true);
  1965.                 if ($json === null) return false;
  1966.                 $latest_version = false;
  1967.                 foreach ($json as $v) {
  1968.                         if (isset($v['version'])) {
  1969.                                 if (!$allow_dev_version && str_ends_with($v['version'],'-dev')) continue;
  1970.                                 $latest_version = $v['version'];
  1971.                                 break; // the first item is the latest version
  1972.                         }
  1973.                 }
  1974.                 return ($cachedVersion[$infoFile] = $latest_version);
  1975.  
  1976.                 /*
  1977.  
  1978.                 $installType = OIDplus::getInstallType();
  1979.  
  1980.                 if ($installType === 'svn-wc') {
  1981.                         if (is_dir($svn_dir = OIDplus::findSvnFolder())) {
  1982.                                 $ver = get_svn_revision($svn_dir);
  1983.                                 if ($ver)
  1984.                                         return ($cachedVersion[$infoFile] = 'svn-'.$ver);
  1985.                         }
  1986.                 }
  1987.  
  1988.                 if ($installType === 'git-wc') {
  1989.                         $ver = OIDplus::getGitsvnRevision();
  1990.                         if ($ver)
  1991.                                 return ($cachedVersion[$infoFile] = 'svn-'.$ver);
  1992.                 }
  1993.  
  1994.                 if ($installType === 'manual') {
  1995.                         $cont = '';
  1996.                         if (file_exists($filename = OIDplus::localpath().'oidplus_version.txt'))
  1997.                                 $cont = file_get_contents($filename);
  1998.                         if (file_exists($filename = OIDplus::localpath().'.version.php'))
  1999.                                 $cont = file_get_contents($filename);
  2000.                         $m = array();
  2001.                         if (preg_match('@Revision (\d+)@', $cont, $m)) // do not translate
  2002.                                 return ($cachedVersion[$infoFile] = 'svn-'.$m[1]); // do not translate
  2003.                 }
  2004.  
  2005.                 return ($cachedVersion[$infoFile] = false); // version ambigous or unknown
  2006.  
  2007.                 */
  2008.         }
  2009.  
  2010.         const ENFORCE_SSL_NO   = 0;
  2011.         const ENFORCE_SSL_YES  = 1;
  2012.         const ENFORCE_SSL_AUTO = 2;
  2013.  
  2014.         /**
  2015.          * @var bool|null
  2016.          */
  2017.         private static $sslAvailableCache = null;
  2018.  
  2019.         /**
  2020.          * @return bool
  2021.          * @throws OIDplusException, OIDplusConfigInitializationException
  2022.          */
  2023.         public static function isSslAvailable(): bool {
  2024.                 if (!is_null(self::$sslAvailableCache)) return self::$sslAvailableCache;
  2025.  
  2026.                 if (PHP_SAPI == 'cli') {
  2027.                         self::$sslAvailableCache = false;
  2028.                         return false;
  2029.                 }
  2030.  
  2031.                 $timeout = 2;
  2032.                 $already_ssl = self::isSSL();
  2033.                 $ssl_port = 443;
  2034.                 $host_with_port = $_SERVER['HTTP_HOST'];
  2035.                 $host_no_port = explode(':',$host_with_port)[0];
  2036.                 $host_ssl = $host_no_port . ($ssl_port != 443 ? ':'.$ssl_port : '');
  2037.  
  2038.                 if ($already_ssl) {
  2039.                         OIDplus::cookieUtils()->setcookie('SSL_CHECK', '1', 0, true/*allowJS*/, null/*samesite*/, true/*forceInsecure*/);
  2040.                         self::$sslAvailableCache = true;
  2041.                         return true;
  2042.                 } else {
  2043.                         if (isset($_COOKIE['SSL_CHECK']) && ($_COOKIE['SSL_CHECK'] == '1')) {
  2044.                                 // The cookie "SSL_CHECK" is set once a website was loaded with HTTPS.
  2045.                                 // It forces subsequent HTTP calls to redirect to HTTPS (like HSTS).
  2046.                                 // The reason is the following problem:
  2047.                                 // If you open the page with HTTPS first, then the CSRF token cookies will get the "secure" flag
  2048.                                 // If you open the page then with HTTP, the HTTP cannot access the secure CSRF cookies,
  2049.                                 // Chrome will then block "Set-Cookie" since the HTTP cookie would overwrite the HTTPS cookie.
  2050.                                 // So we MUST redirect, even if the Mode is ENFORCE_SSL_NO.
  2051.                                 // Note: SSL_CHECK is NOT a replacement for HSTS! You should use HSTS,
  2052.                                 //       because on there your browser ensures that HTTPS is called, before the server
  2053.                                 //       is even contacted (and therefore, no HTTP connection can be hacked).
  2054.                                 $mode = OIDplus::ENFORCE_SSL_YES;
  2055.                         } else {
  2056.                                 $mode = OIDplus::baseConfig()->getValue('ENFORCE_SSL', OIDplus::ENFORCE_SSL_AUTO);
  2057.                         }
  2058.  
  2059.                         if ($mode == OIDplus::ENFORCE_SSL_NO) {
  2060.                                 // No SSL available
  2061.                                 self::$sslAvailableCache = false;
  2062.                                 return false;
  2063.                         } else if ($mode == OIDplus::ENFORCE_SSL_YES) {
  2064.                                 // Force SSL
  2065.                                 $location = 'https://' . $host_ssl . $_SERVER['REQUEST_URI'];
  2066.                                 header('Location:'.$location);
  2067.                                 die(_L('Redirecting to HTTPS...'));
  2068.                         } else if ($mode == OIDplus::ENFORCE_SSL_AUTO) {
  2069.                                 // Automatic SSL detection
  2070.                                 if (isset($_COOKIE['SSL_CHECK'])) {
  2071.                                         // We already had the HTTPS detection done before.
  2072.                                         if ($_COOKIE['SSL_CHECK'] == '1') {
  2073.                                                 // HTTPS was detected before, but we are HTTP. Redirect now
  2074.                                                 $location = 'https://' . $host_ssl . $_SERVER['REQUEST_URI'];
  2075.                                                 header('Location:'.$location);
  2076.                                                 die(_L('Redirecting to HTTPS...'));
  2077.                                         } else {
  2078.                                                 // No HTTPS available. Do nothing.
  2079.                                                 self::$sslAvailableCache = false;
  2080.                                                 return false;
  2081.                                         }
  2082.                                 } else {
  2083.                                         // This is our first check (or the browser didn't accept the SSL_CHECK cookie)
  2084.                                         $errno = -1;
  2085.                                         $errstr = '';
  2086.                                         if (@fsockopen($host_no_port, $ssl_port, $errno, $errstr, $timeout)) {
  2087.                                                 // HTTPS detected. Redirect now, and remember that we had detected HTTPS
  2088.                                                 OIDplus::cookieUtils()->setcookie('SSL_CHECK', '1', 0, true/*allowJS*/, null/*samesite*/, true/*forceInsecure*/);
  2089.                                                 $location = 'https://' . $host_ssl . $_SERVER['REQUEST_URI'];
  2090.                                                 header('Location:'.$location);
  2091.                                                 die(_L('Redirecting to HTTPS...'));
  2092.                                         } else {
  2093.                                                 // No HTTPS detected. Do nothing, and next time, don't try to detect HTTPS again.
  2094.                                                 OIDplus::cookieUtils()->setcookie('SSL_CHECK', '0', 0, true/*allowJS*/, null/*samesite*/, true/*forceInsecure*/);
  2095.                                                 self::$sslAvailableCache = false;
  2096.                                                 return false;
  2097.                                         }
  2098.                                 }
  2099.                         } else {
  2100.                                 assert(false);
  2101.                                 return false;
  2102.                         }
  2103.                 }
  2104.         }
  2105.  
  2106.         /**
  2107.          * Gets a local path pointing to a resource
  2108.          * @param string|null $target Target resource (file or directory must exist), or null to get the OIDplus base directory
  2109.          * @param bool $relative If true, the returning path is relative to the currently executed PHP file (not the CLI working directory)
  2110.          * @return string|false The local path, with guaranteed trailing path delimiter for directories
  2111.          */
  2112.         public static function localpath(string $target=null, bool $relative=false) {
  2113.                 if (is_null($target)) {
  2114.                         $target = __DIR__.'/../../';
  2115.                 }
  2116.  
  2117.                 if ($relative) {
  2118.                         // First, try to find out how many levels we need to go up
  2119.                         $steps_up = self::getExecutingScriptPathDepth();
  2120.                         if ($steps_up === false) return false;
  2121.  
  2122.                         // Virtually go back from the executing PHP script to the OIDplus base path
  2123.                         $res = str_repeat('../',$steps_up);
  2124.  
  2125.                         // Then go to the desired location
  2126.                         $basedir = realpath(__DIR__.'/../../');
  2127.                         $target = realpath($target);
  2128.                         if ($target === false) return false;
  2129.                         $res .= substr($target, strlen($basedir)+1);
  2130.                         $res = rtrim($res,'/'); // avoid '..//' for localpath(null,true)
  2131.                 } else {
  2132.                         $res = realpath($target);
  2133.                 }
  2134.  
  2135.                 if (is_dir($target)) $res .= '/';
  2136.  
  2137.                 return str_replace('/', DIRECTORY_SEPARATOR, $res);
  2138.         }
  2139.  
  2140.         /**
  2141.          * Gets a URL pointing to a resource
  2142.          * @param string|null $target Target resource (file or directory must exist), or null to get the OIDplus base directory
  2143.          * @param int|bool $mode If true or OIDplus::PATH_RELATIVE, the returning path is relative to the currently executed
  2144.          *                          PHP script (i.e. index.php , not the plugin PHP script!). False or OIDplus::PATH_ABSOLUTE is
  2145.          *                          results in an absolute URL. OIDplus::PATH_ABSOLUTE_CANONICAL is an absolute URL,
  2146.          *                          but a canonical path (set by base config setting CANONICAL_SYSTEM_URL) is preferred.
  2147.          * @return string|false The URL, with guaranteed trailing path delimiter for directories
  2148.          * @throws OIDplusException
  2149.          */
  2150.         public static function webpath(string $target=null, $mode=self::PATH_ABSOLUTE_CANONICAL) {
  2151.                 // backwards compatibility
  2152.                 if ($mode === true) $mode = self::PATH_RELATIVE;
  2153.                 if ($mode === false) $mode = self::PATH_ABSOLUTE;
  2154.  
  2155.                 if ($mode == OIDplus::PATH_RELATIVE_TO_ROOT) {
  2156.                         $tmp = OIDplus::webpath($target,OIDplus::PATH_ABSOLUTE);
  2157.                         if ($tmp === false) return false;
  2158.                         $tmp = parse_url($tmp);
  2159.                         if ($tmp === false) return false;
  2160.                         if (!isset($tmp['path'])) return false;
  2161.                         return $tmp['path'];
  2162.                 }
  2163.  
  2164.                 if ($mode == OIDplus::PATH_RELATIVE_TO_ROOT_CANONICAL) {
  2165.                         $tmp = OIDplus::webpath($target,OIDplus::PATH_ABSOLUTE_CANONICAL);
  2166.                         if ($tmp === false) return false;
  2167.                         $tmp = parse_url($tmp);
  2168.                         if ($tmp === false) return false;
  2169.                         if (!isset($tmp['path'])) return false;
  2170.                         return $tmp['path'];
  2171.                 }
  2172.  
  2173.                 $res = self::getSystemUrl($mode); // Note: already contains a trailing path delimiter
  2174.                 if ($res === false) return false;
  2175.  
  2176.                 if (!is_null($target)) {
  2177.                         $basedir = realpath(__DIR__.'/../../');
  2178.                         $target = realpath($target);
  2179.                         if ($target === false) return false;
  2180.                         $tmp = substr($target, strlen($basedir)+1);
  2181.                         $res .= str_replace(DIRECTORY_SEPARATOR,'/',$tmp); // replace OS specific path delimiters introduced by realpath()
  2182.                         if (is_dir($target)) $res .= '/';
  2183.                 }
  2184.  
  2185.                 return $res;
  2186.         }
  2187.  
  2188.         /**
  2189.          * Note: canonicalURL() is different than webpath(),
  2190.          * because it does additional things like re-ordering of arguments
  2191.          * @param string|null $goto
  2192.          * @return false|string
  2193.          * @throws OIDplusException
  2194.          */
  2195.         public static function canonicalURL(string $goto=null) {
  2196.                 // First part: OIDplus system URL (or canonical system URL)
  2197.                 $sysurl = OIDplus::getSystemUrl(self::PATH_ABSOLUTE_CANONICAL);
  2198.  
  2199.                 // Second part: Directory
  2200.                 $basedir = realpath(__DIR__.'/../../');
  2201.                 $target = realpath('.');
  2202.                 if ($target === false) return false;
  2203.                 $tmp = substr($target, strlen($basedir)+1);
  2204.                 $res = str_replace(DIRECTORY_SEPARATOR,'/',$tmp); // replace OS specific path delimiters introduced by realpath()
  2205.                 if (is_dir($target) && ($res != '')) $res .= '/';
  2206.  
  2207.                 // Third part: File name
  2208.                 $tmp = $_SERVER['SCRIPT_NAME'];
  2209.                 $tmp = preg_replace('@index\\.php$@ismU', '', $tmp);
  2210.                 $tmp = explode('/',$tmp);
  2211.                 $tmp = end($tmp);
  2212.  
  2213.                 // Fourth part: Query string (ordered)
  2214.                 $url = [];
  2215.                 parse_str($_SERVER['QUERY_STRING'], $url);
  2216.                 if ($goto !== null) $url['goto'] = $goto;
  2217.                 ksort($url);
  2218.                 $tmp2 = http_build_query($url);
  2219.                 if ($tmp2 != '') $tmp2 = '?'.$tmp2;
  2220.  
  2221.                 return $sysurl.$res.$tmp.$tmp2;
  2222.         }
  2223.  
  2224.         private static $shutdown_functions = array();
  2225.  
  2226.         /**
  2227.          * @param callable $func
  2228.          * @return void
  2229.          */
  2230.         public static function register_shutdown_function(callable $func) {
  2231.                 self::$shutdown_functions[] = $func;
  2232.         }
  2233.  
  2234.         /**
  2235.          * @return void
  2236.          */
  2237.         public static function invoke_shutdown() {
  2238.                 foreach (self::$shutdown_functions as $func) {
  2239.                         $func();
  2240.                 }
  2241.         }
  2242.  
  2243.         /**
  2244.          * @return string[]
  2245.          * @throws OIDplusException
  2246.          */
  2247.         public static function getAvailableLangs(): array {
  2248.                 $langs = array();
  2249.                 foreach (OIDplus::getAllPluginManifests('language') as $pluginManifest) {
  2250.                         $code = $pluginManifest->getLanguageCode();
  2251.                         $langs[] = $code;
  2252.                 }
  2253.                 return $langs;
  2254.         }
  2255.  
  2256.         /**
  2257.          * @return string
  2258.          * @throws OIDplusConfigInitializationException
  2259.          * @throws OIDplusException
  2260.          */
  2261.         public static function getDefaultLang(): string {
  2262.                 static $thrownOnce = false; // avoid endless loop inside OIDplusConfigInitializationException
  2263.  
  2264.                 $lang = self::baseConfig()->getValue('DEFAULT_LANGUAGE', 'enus');
  2265.  
  2266.                 if (!in_array($lang,self::getAvailableLangs())) {
  2267.                         if (!$thrownOnce) {
  2268.                                 $thrownOnce = true;
  2269.                                 throw new OIDplusConfigInitializationException(_L('DEFAULT_LANGUAGE points to an invalid language plugin. (Consider setting to "enus" = "English USA".)'));
  2270.                         } else {
  2271.                                 return 'enus';
  2272.                         }
  2273.                 }
  2274.  
  2275.                 return $lang;
  2276.         }
  2277.  
  2278.         /**
  2279.          * @return false|string
  2280.          * @throws OIDplusConfigInitializationException
  2281.          * @throws OIDplusException
  2282.          */
  2283.         public static function getCurrentLang() {
  2284.  
  2285.                 if (isset($_SERVER['REQUEST_URI'])) {
  2286.                         $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
  2287.                         if (str_starts_with($rel_url, 'rest/')) { // <== TODO: Find a way how to move this into the plugin, since REST does not belong to the core. (Maybe some kind of "stateless mode" that is enabled by the REST plugin)
  2288.                                 return self::getDefaultLang();
  2289.                         }
  2290.                 }
  2291.  
  2292.                 if (isset($_GET['lang'])) {
  2293.                         $lang = $_GET['lang'];
  2294.                 } else if (isset($_POST['lang'])) {
  2295.                         $lang = $_POST['lang'];
  2296.                 } else if (isset($_COOKIE['LANGUAGE'])) {
  2297.                         $lang = $_COOKIE['LANGUAGE'];
  2298.                 } else {
  2299.                         $lang = self::getDefaultLang();
  2300.                 }
  2301.                 return substr(preg_replace('@[^a-z]@imU', '', $lang),0,4); // sanitize
  2302.         }
  2303.  
  2304.         /**
  2305.          * @return void
  2306.          * @throws OIDplusException
  2307.          */
  2308.         public static function handleLangArgument() {
  2309.                 if (isset($_GET['lang'])) {
  2310.                         // The "?lang=" argument is only for NoScript-Browsers/SearchEngines
  2311.                         // In case someone who has JavaScript clicks a ?lang= link, they should get
  2312.                         // the page in that language, but the cookie must be set, otherwise
  2313.                         // the menu and other stuff would be in their cookie-based-language and not the
  2314.                         // argument-based-language.
  2315.                         OIDplus::cookieUtils()->setcookie('LANGUAGE', $_GET['lang'], 0, true/*HttpOnly off, because JavaScript also needs translation*/);
  2316.                 } else if (isset($_POST['lang'])) {
  2317.                         OIDplus::cookieUtils()->setcookie('LANGUAGE', $_POST['lang'], 0, true/*HttpOnly off, because JavaScript also needs translation*/);
  2318.                 }
  2319.         }
  2320.  
  2321.         private static $translationArray = array();
  2322.  
  2323.         /**
  2324.          * @param string $translation_file
  2325.          * @return array
  2326.          */
  2327.         protected static function getTranslationFileContents(string $translation_file): array {
  2328.                 // First, try the cache
  2329.                 $cache_file = __DIR__ . '/../../userdata/cache/translation_'.md5($translation_file).'.ser';
  2330.                 if (file_exists($cache_file) && (filemtime($cache_file) == filemtime($translation_file))) {
  2331.                         $cac = @unserialize(file_get_contents($cache_file));
  2332.                         if ($cac) return $cac;
  2333.                 }
  2334.  
  2335.                 // If not successful, then load the XML file
  2336.                 $xml = @simplexml_load_string(file_get_contents($translation_file));
  2337.                 if (!$xml) return array(); // if there is an UTF-8 or parsing error, don't output any errors, otherwise the JavaScript is corrupt and the page won't render correctly
  2338.                 $cac = array();
  2339.                 foreach ($xml->message as $msg) {
  2340.                         $src = trim($msg->source->__toString());
  2341.                         $dst = trim($msg->target->__toString());
  2342.                         $cac[$src] = $dst;
  2343.                 }
  2344.                 @file_put_contents($cache_file,serialize($cac));
  2345.                 @touch($cache_file,filemtime($translation_file));
  2346.                 return $cac;
  2347.         }
  2348.  
  2349.         /**
  2350.          * @param string $requested_lang
  2351.          * @return array
  2352.          * @throws OIDplusException
  2353.          */
  2354.         public static function getTranslationArray(string $requested_lang='*'): array {
  2355.                 foreach (OIDplus::getAllPluginManifests('language') as $pluginManifest) {
  2356.                         $lang = $pluginManifest->getLanguageCode();
  2357.                         if (strpos($lang,'/') !== false) continue; // just to be sure
  2358.                         if (strpos($lang,'\\') !== false) continue; // just to be sure
  2359.                         if (strpos($lang,'..') !== false) continue; // just to be sure
  2360.  
  2361.                         if (($requested_lang != '*') && ($lang != $requested_lang)) continue;
  2362.  
  2363.                         if (!isset(self::$translationArray[$lang])) {
  2364.                                 self::$translationArray[$lang] = array();
  2365.  
  2366.                                 $wildcard = $pluginManifest->getLanguageMessages();
  2367.                                 if (strpos($wildcard,'/') !== false) continue; // just to be sure
  2368.                                 if (strpos($wildcard,'\\') !== false) continue; // just to be sure
  2369.                                 if (strpos($wildcard,'..') !== false) continue; // just to be sure
  2370.  
  2371.                                 $translation_files = glob(__DIR__.'/../../plugins/'.'*'.'/language/'.$lang.'/'.$wildcard);
  2372.                                 sort($translation_files);
  2373.                                 foreach ($translation_files as $translation_file) {
  2374.                                         if (!file_exists($translation_file)) continue;
  2375.                                         $cac = self::getTranslationFileContents($translation_file);
  2376.                                         foreach ($cac as $src => $dst) {
  2377.                                                 self::$translationArray[$lang][$src] = $dst;
  2378.                                         }
  2379.                                 }
  2380.                         }
  2381.                 }
  2382.                 return self::$translationArray;
  2383.         }
  2384.  
  2385.         /**
  2386.          * @return mixed
  2387.          */
  2388.         public static function getEditionInfo() {
  2389.                 return @parse_ini_file(__DIR__.'/../edition.ini', true)['Edition'];
  2390.         }
  2391.  
  2392.         /**
  2393.          * @return false|string The git path, with guaranteed trailing path delimiter for directories
  2394.          */
  2395.         public static function findGitFolder() {
  2396.                 $dir = rtrim(OIDplus::localpath(), DIRECTORY_SEPARATOR);
  2397.  
  2398.                 // Git command line saves git information in folder ".git"
  2399.                 if (is_dir($res = $dir.'/.git')) {
  2400.                         return str_replace('/', DIRECTORY_SEPARATOR, $res.'/');
  2401.                 }
  2402.  
  2403.                 // Plesk git saves git information in folder "../../../git/oidplus/" (or similar)
  2404.                 $i = 0;
  2405.                 do {
  2406.                         if (is_dir($dir.'/git')) {
  2407.                                 $confs = @glob($dir.'/git/'.'*'.'/config');
  2408.                                 if ($confs) foreach ($confs as $conf) {
  2409.                                         $cont = file_get_contents($conf);
  2410.                                         if (isset(OIDplus::getEditionInfo()['gitrepo']) && (OIDplus::getEditionInfo()['gitrepo'] != '') && (strpos($cont, OIDplus::getEditionInfo()['gitrepo']) !== false)) {
  2411.                                                 $res = dirname($conf);
  2412.                                                 return str_replace('/', DIRECTORY_SEPARATOR, $res.'/');
  2413.                                         }
  2414.                                 }
  2415.                         }
  2416.                         $i++;
  2417.                 } while (($i<100) && ($dir != ($new_dir = @realpath($dir.'/../'))) && ($dir = $new_dir));
  2418.  
  2419.                 return false;
  2420.         }
  2421.  
  2422.         /**
  2423.          * @return false|string The SVN path, with guaranteed trailing path delimiter for directories
  2424.          */
  2425.         public static function findSvnFolder() {
  2426.                 $dir = rtrim(OIDplus::localpath(), DIRECTORY_SEPARATOR);
  2427.  
  2428.                 if (is_dir($res = $dir.'/.svn')) {
  2429.                         return str_replace('/', DIRECTORY_SEPARATOR, $res.'/');
  2430.                 }
  2431.  
  2432.                 // in case we checked out the root instead of the "trunk"
  2433.                 if (is_dir($res = $dir.'/../.svn')) {
  2434.                         return str_replace('/', DIRECTORY_SEPARATOR, $res.'/');
  2435.                 }
  2436.  
  2437.                 return false;
  2438.         }
  2439.  
  2440.         /**
  2441.          * @return false|string
  2442.          */
  2443.         public static function getGitsvnRevision() {
  2444.                 try {
  2445.                         $git_dir = OIDplus::findGitFolder();
  2446.                         if ($git_dir === false) return false;
  2447.  
  2448.                         // git_get_latest_commit_message() tries command line and binary parsing
  2449.                         // requires vendor/danielmarschall/php_utils/git_utils.inc.php
  2450.                         $commit_msg = git_get_latest_commit_message($git_dir);
  2451.                 } catch (\Exception $e) {
  2452.                         return false;
  2453.                 }
  2454.  
  2455.                 $m = array();
  2456.                 if (preg_match('%git-svn-id: (.+)@(\\d+) %ismU', $commit_msg, $m)) {
  2457.                         return $m[2];
  2458.                 } else {
  2459.                         return false;
  2460.                 }
  2461.         }
  2462.  
  2463.         /**
  2464.          * @param string $static_node_id
  2465.          * @param bool $throw_exception
  2466.          * @return string
  2467.          */
  2468.         public static function prefilterQuery(string $static_node_id, bool $throw_exception): string {
  2469.                 $static_node_id = trim($static_node_id);
  2470.  
  2471.                 // Let namespace be case-insensitive
  2472.                 // Note: The query might not contain a namespace. It might be a single OID
  2473.                 //       or MAC address, and the plugins need to detect it any add a namespace
  2474.                 //       in the prefiltering. But if we have a namespace, we should fix the
  2475.                 //       case, so that plugins don't have a problem if they check the namespace
  2476.                 //       using str_starts_with().
  2477.                 if (substr_count($static_node_id, ':') === 1) {
  2478.                         $ary = explode(':', $static_node_id, 2);
  2479.                         $ary[0] = strtolower($ary[0]);
  2480.                         $static_node_id = implode(':', $ary);
  2481.                 }
  2482.  
  2483.                 // Ask plugins if they want to change the node id
  2484.                 foreach (OIDplus::getObjectTypePluginsEnabled() as $plugin) {
  2485.                         $static_node_id = $plugin->prefilterQuery($static_node_id, $throw_exception);
  2486.                 }
  2487.  
  2488.                 // Let namespace be case-insensitive
  2489.                 // At this point, plugins should have already added the namespace during the prefiltering,
  2490.                 // so, now we make sure that the namespace is really lowercase
  2491.                 if (substr_count($static_node_id, ':') === 1) {
  2492.                         $ary = explode(':', $static_node_id, 2);
  2493.                         $ary[0] = strtolower($ary[0]);
  2494.                         $static_node_id = implode(':', $ary);
  2495.                 }
  2496.  
  2497.                 return $static_node_id;
  2498.         }
  2499.  
  2500.         /**
  2501.          * @return bool
  2502.          */
  2503.         public static function isCronjob(): bool {
  2504.                 return explode('.',basename($_SERVER['SCRIPT_NAME']))[0] === 'cron';
  2505.         }
  2506.  
  2507.         /**
  2508.          * Since OIDplus svn-184, entries in the database need to have a canonical ID
  2509.          * If the ID is not canonical (e.g. GUIDs missing hyphens), the object cannot be opened in OIDplus
  2510.          * This script re-canonizes the object IDs if required.
  2511.          * In SVN Rev 856, the canonization for GUID, IPv4 and IPv6 have changed, requiring another
  2512.          * re-canonization
  2513.          * @return void
  2514.          * @throws OIDplusException
  2515.          */
  2516.         private static function recanonizeObjects() {
  2517.                 $res = OIDplus::db()->query("select id from ###objects");
  2518.                 while ($row = $res->fetch_array()) {
  2519.                         $ida = $row['id'];
  2520.                         $obj = OIDplusObject::parse($ida);
  2521.                         if (!$obj) continue;
  2522.                         $idb = $obj->nodeId();
  2523.                         if (($idb) && ($ida != $idb)) {
  2524.                                 if (OIDplus::db()->transaction_supported()) OIDplus::db()->transaction_begin();
  2525.                                 try {
  2526.                                         OIDplus::db()->query("update ###objects set id = ? where id = ?", array($idb, $ida));
  2527.                                         OIDplus::db()->query("update ###asn1id set oid = ? where oid = ?", array($idb, $ida));
  2528.                                         OIDplus::db()->query("update ###iri set oid = ? where oid = ?", array($idb, $ida));
  2529.                                         OIDplus::db()->query("update ###log_object set object = ? where object = ?", array($idb, $ida));
  2530.                                         OIDplus::logger()->log("V2:[INFO]A", "Object name '%1' has been changed to '%2' during re-canonization", $ida, $idb);
  2531.                                         if (OIDplus::db()->transaction_supported()) OIDplus::db()->transaction_commit();
  2532.                                 } catch (\Exception $e) {
  2533.                                         if (OIDplus::db()->transaction_supported()) OIDplus::db()->transaction_rollback();
  2534.                                         throw $e;
  2535.                                 }
  2536.                                 OIDplusObject::resetObjectInformationCache();
  2537.                         }
  2538.                 }
  2539.         }
  2540.  
  2541.         /**
  2542.          * Tries to determine the IP address of the website visitor
  2543.          * @return string|false
  2544.          */
  2545.         public static function getClientIpAddress() {
  2546.                 $direct_connection = $_SERVER['REMOTE_ADDR'] ?? false;
  2547.                 if ($direct_connection === false) return false;
  2548.  
  2549.                 $trusted_proxies = OIDplus::baseConfig()->getValue('XFF_TRUSTED_PROXIES', []);
  2550.                 if (in_array($direct_connection, $trusted_proxies)) {
  2551.                         return $_SERVER['HTTP_X_FORWARDED_FOR'] ?: $direct_connection;
  2552.                 } else {
  2553.                         return $direct_connection;
  2554.                 }
  2555.         }
  2556.  
  2557. }
  2558.