Subversion Repositories oidplus

Rev

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