Subversion Repositories oidplus

Rev

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