Subversion Repositories oidplus

Rev

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

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