Subversion Repositories oidplus

Rev

Rev 1265 | Rev 1304 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

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