Subversion Repositories oidplus

Rev

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