Subversion Repositories oidplus

Rev

Rev 1435 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

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