Subversion Repositories oidplus

Rev

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

Rev Author Line No. Line
2 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1073 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
2 daniel-mar 6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
 
1050 daniel-mar 20
namespace ViaThinkSoft\OIDplus;
511 daniel-mar 21
 
1086 daniel-mar 22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
25
 
730 daniel-mar 26
class OIDplus extends OIDplusBaseClass {
1116 daniel-mar 27
        /**
28
         * @var OIDplusPagePlugin[]
29
         */
281 daniel-mar 30
        private static /*OIDplusPagePlugin[]*/ $pagePlugins = array();
1116 daniel-mar 31
        /**
32
         * @var OIDplusAuthPlugin[]
33
         */
281 daniel-mar 34
        private static /*OIDplusAuthPlugin[]*/ $authPlugins = array();
1116 daniel-mar 35
        /**
36
         * @var OIDplusLoggerPlugin[]
37
         */
289 daniel-mar 38
        private static /*OIDplusLoggerPlugin[]*/ $loggerPlugins = array();
1116 daniel-mar 39
        /**
40
         * @var OIDplusObjectTypePlugin[]
41
         */
227 daniel-mar 42
        private static /*OIDplusObjectTypePlugin[]*/ $objectTypePlugins = array();
1116 daniel-mar 43
        /**
44
         * @var string[]|OIDplusObject[] Classnames of OIDplusObject classes
45
         */
227 daniel-mar 46
        private static /*string[]*/ $enabledObjectTypes = array();
1116 daniel-mar 47
        /**
48
         * @var string[]|OIDplusObject[] Classnames of OIDplusObject classes
49
         */
227 daniel-mar 50
        private static /*string[]*/ $disabledObjectTypes = array();
1116 daniel-mar 51
        /**
52
         * @var OIDplusDatabasePlugin[]
53
         */
227 daniel-mar 54
        private static /*OIDplusDatabasePlugin[]*/ $dbPlugins = array();
1116 daniel-mar 55
        /**
56
         * @var OIDplusCaptchaPlugin[]
57
         */
702 daniel-mar 58
        private static /*OIDplusCaptchaPlugin[]*/ $captchaPlugins = array();
1116 daniel-mar 59
        /**
60
         * @var OIDplusSqlSlangPlugin[]
61
         */
274 daniel-mar 62
        private static /*OIDplusSqlSlangPlugin[]*/ $sqlSlangPlugins = array();
1116 daniel-mar 63
        /**
64
         * @var OIDplusLanguagePlugin[]
65
         */
355 daniel-mar 66
        private static /*OIDplusLanguagePlugin[]*/ $languagePlugins = array();
1116 daniel-mar 67
        /**
68
         * @var OIDplusDesignPlugin[]
69
         */
449 daniel-mar 70
        private static /*OIDplusDesignPlugin[]*/ $designPlugins = array();
2 daniel-mar 71
 
1116 daniel-mar 72
        /**
73
         * @var bool
74
         */
280 daniel-mar 75
        protected static $html = true;
236 daniel-mar 76
 
1116 daniel-mar 77
        /**
78
         * e.g. "../"
79
         */
80
        /*public*/ const PATH_RELATIVE = 1;
801 daniel-mar 81
 
1116 daniel-mar 82
        /**
83
         * e.g. "http://www.example.com/oidplus/"
84
         */
85
        /*public*/ const PATH_ABSOLUTE = 2;
86
 
87
        /**
88
         * e.g. "http://www.example.org/oidplus/" (if baseconfig CANONICAL_SYSTEM_URL is set)
89
         */
90
        /*public*/ const PATH_ABSOLUTE_CANONICAL = 3;
91
 
92
        /**
93
         * e.g. "/oidplus/"
94
         */
95
        /*public*/ const PATH_RELATIVE_TO_ROOT = 4;
96
 
97
        /**
98
         * e.g. "/oidplus/" (if baseconfig CANONICAL_SYSTEM_URL is set)
99
         */
100
        /*public*/ const PATH_RELATIVE_TO_ROOT_CANONICAL = 5;
101
 
102
        /**
103
         * These plugin types can contain HTML code and therefore may
104
         * emit (non-setup) CSS/JS code via their manifest.
105
         * Note that design plugins may only output CSS, not JS.
106
         */
778 daniel-mar 107
        /*public*/ const INTERACTIVE_PLUGIN_TYPES = array(
1199 daniel-mar 108
                'publicPages',
109
                'raPages',
110
                'adminPages',
111
                'objectTypes',
112
                'captcha'
113
        );
778 daniel-mar 114
 
1336 daniel-mar 115
        //const UUID_NAMEBASED_NS_Base64PubKey = 'fd16965c-8bab-11ed-8744-3c4a92df8582';
1073 daniel-mar 116
 
1116 daniel-mar 117
        /**
118
         * Private constructor (Singleton)
119
         */
2 daniel-mar 120
        private function __construct() {
121
        }
295 daniel-mar 122
 
1116 daniel-mar 123
        /**
124
         * @return bool
125
         * @throws OIDplusException
126
         */
1068 daniel-mar 127
        private static function insideSetup(): bool {
128
                if (PHP_SAPI == 'cli') return false;
129
                if (!isset($_SERVER['REQUEST_URI'])) return false;
1055 daniel-mar 130
                return (strpos($_SERVER['REQUEST_URI'], OIDplus::webpath(null,OIDplus::PATH_RELATIVE_TO_ROOT).'setup/') === 0);
131
        }
132
 
1050 daniel-mar 133
        // --- Static classes
274 daniel-mar 134
 
263 daniel-mar 135
        private static $baseConfig = null;
1050 daniel-mar 136
        private static $oldConfigFormatLoaded = false;
274 daniel-mar 137
 
1116 daniel-mar 138
        /**
139
         * @return OIDplusBaseConfig
140
         * @throws OIDplusException, OIDplusConfigInitializationException
141
         */
142
        public static function baseConfig(): OIDplusBaseConfig {
263 daniel-mar 143
                if ($first_init = is_null(self::$baseConfig)) {
144
                        self::$baseConfig = new OIDplusBaseConfig();
261 daniel-mar 145
                }
146
 
1169 daniel-mar 147
                if ($first_init) {
148
                        if (self::insideSetup()) return self::$baseConfig;
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++) {
1342 daniel-mar 844
                                        // No conflict between ViaThinkSoft OIDs .1 till .185
1333 daniel-mar 845
                                        $block4 = dechex(hexdec(substr(sha1('1.3.6.1.4.1.37476.2.5.2.4.8.'.$i), -4)) & 0x3FFF | 0x8000);
846
                                        $coll[] = $block4;
847
                                }
1342 daniel-mar 848
                                for ($i=0; $i<=0xF; $i++) {
849
                                        // 0x8000 - 0x800F are used by the system
850
                                        $coll[] = dechex(0x8000 + $i);
851
                                }
1333 daniel-mar 852
                                $block4 = dechex(hexdec(substr(sha1($plugin->getManifest()->getOid()), -4)) & 0x3FFF | 0x8000);
853
                                if (in_array($block4, $coll)) {
854
                                        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'));
855
                                }
856
                        }
857
                }
858
 
227 daniel-mar 859
                $ot = $plugin::getObjectTypeClassName();
860
                self::registerObjectType($ot);
861
        }
862
 
1112 daniel-mar 863
        /**
1116 daniel-mar 864
         * @param string|OIDplusObject $ot Object type class name (OIDplusObject)
865
         * @return void
866
         * @throws OIDplusException
867
         */
224 daniel-mar 868
        private static function registerObjectType($ot) {
66 daniel-mar 869
                $ns = $ot::ns();
860 daniel-mar 870
                if (empty($ns)) throw new OIDplusException(_L('ObjectType plugin %1 is erroneous: Namespace must not be empty',$ot));
66 daniel-mar 871
 
860 daniel-mar 872
                // Currently, we must enforce that namespaces in objectType plugins are lowercase, because prefilterQuery() makes all namespaces lowercase and the DBMS should be case-sensitive
873
                if ($ns != strtolower($ns)) throw new OIDplusException(_L('ObjectType plugin %1 is erroneous: Namespace %2 must be lower-case',$ot,$ns));
66 daniel-mar 874
 
860 daniel-mar 875
                $root = $ot::root();
876
                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.':'));
877
 
66 daniel-mar 878
                $ns_found = false;
227 daniel-mar 879
                foreach (array_merge(OIDplus::getEnabledObjectTypes(), OIDplus::getDisabledObjectTypes()) as $test_ot) {
66 daniel-mar 880
                        if ($test_ot::ns() == $ns) {
881
                                $ns_found = true;
882
                                break;
883
                        }
884
                }
885
                if ($ns_found) {
360 daniel-mar 886
                        throw new OIDplusException(_L('Attention: Two objectType plugins use the same namespace "%1"!',$ns));
66 daniel-mar 887
                }
888
 
889
                $init = OIDplus::config()->getValue("objecttypes_initialized");
890
                $init_ary = empty($init) ? array() : explode(';', $init);
70 daniel-mar 891
                $init_ary = array_map('trim', $init_ary);
66 daniel-mar 892
 
893
                $enabled = OIDplus::config()->getValue("objecttypes_enabled");
894
                $enabled_ary = empty($enabled) ? array() : explode(';', $enabled);
70 daniel-mar 895
                $enabled_ary = array_map('trim', $enabled_ary);
66 daniel-mar 896
 
79 daniel-mar 897
                if (in_array($ns, $enabled_ary)) {
447 daniel-mar 898
                        // If it is in the list of enabled object types, it is enabled (obviously)
79 daniel-mar 899
                        $do_enable = true;
900
                } else {
447 daniel-mar 901
                        if (!OIDplus::config()->getValue('oobe_objects_done')) {
902
                                // If the OOBE wizard is NOT done, then just enable the "oid" object type by default
79 daniel-mar 903
                                $do_enable = $ns == 'oid';
904
                        } else {
447 daniel-mar 905
                                // If the OOBE wizard was done (once), then
906
                                // we will enable all object types which were never initialized
907
                                // (i.e. a plugin folder was freshly added)
79 daniel-mar 908
                                $do_enable = !in_array($ns, $init_ary);
909
                        }
910
                }
911
 
912
                if ($do_enable) {
227 daniel-mar 913
                        self::$enabledObjectTypes[] = $ot;
914
                        usort(self::$enabledObjectTypes, function($a, $b) {
66 daniel-mar 915
                                $enabled = OIDplus::config()->getValue("objecttypes_enabled");
916
                                $enabled_ary = explode(';', $enabled);
917
 
918
                                $idx_a = array_search($a::ns(), $enabled_ary);
919
                                $idx_b = array_search($b::ns(), $enabled_ary);
920
 
1112 daniel-mar 921
                                if ($idx_a == $idx_b) return 0;
922
                                return ($idx_a > $idx_b) ? +1 : -1;
66 daniel-mar 923
                        });
74 daniel-mar 924
                } else {
925
                        self::$disabledObjectTypes[] = $ot;
66 daniel-mar 926
                }
927
 
928
                if (!in_array($ns, $init_ary)) {
929
                        // Was never initialized before, so we add it to the list of enabled object types once
930
 
79 daniel-mar 931
                        if ($do_enable) {
932
                                $enabled_ary[] = $ns;
672 daniel-mar 933
                                // Important: Don't validate the input, because the other object types might not be initialized yet! So use setValueNoCallback() instead setValue().
934
                                OIDplus::config()->setValueNoCallback("objecttypes_enabled", implode(';', $enabled_ary));
79 daniel-mar 935
                        }
66 daniel-mar 936
 
937
                        $init_ary[] = $ns;
938
                        OIDplus::config()->setValue("objecttypes_initialized", implode(';', $init_ary));
939
                }
61 daniel-mar 940
        }
941
 
1116 daniel-mar 942
        /**
943
         * @return OIDplusObjectTypePlugin[]
944
         */
945
        public static function getObjectTypePlugins(): array {
227 daniel-mar 946
                return self::$objectTypePlugins;
61 daniel-mar 947
        }
948
 
1116 daniel-mar 949
        /**
950
         * @return OIDplusObjectTypePlugin[]
951
         */
952
        public static function getObjectTypePluginsEnabled(): array {
227 daniel-mar 953
                $res = array();
954
                foreach (self::$objectTypePlugins as $plugin) {
955
                        $ot = $plugin::getObjectTypeClassName();
956
                        if (in_array($ot, self::$enabledObjectTypes)) $res[] = $plugin;
957
                }
958
                return $res;
74 daniel-mar 959
        }
960
 
1116 daniel-mar 961
        /**
962
         * @return OIDplusObjectTypePlugin[]
963
         */
964
        public static function getObjectTypePluginsDisabled(): array {
227 daniel-mar 965
                $res = array();
966
                foreach (self::$objectTypePlugins as $plugin) {
967
                        $ot = $plugin::getObjectTypeClassName();
968
                        if (in_array($ot, self::$disabledObjectTypes)) $res[] = $plugin;
74 daniel-mar 969
                }
227 daniel-mar 970
                return $res;
74 daniel-mar 971
        }
972
 
1116 daniel-mar 973
        /**
974
         * @return string[]|OIDplusObject[] Classname of a OIDplusObject class
975
         */
976
        public static function getEnabledObjectTypes(): array {
227 daniel-mar 977
                return self::$enabledObjectTypes;
978
        }
74 daniel-mar 979
 
1116 daniel-mar 980
        /**
981
         * @return string[]|OIDplusObject[] Classname of a OIDplusObject class
982
         */
983
        public static function getDisabledObjectTypes(): array {
227 daniel-mar 984
                return self::$disabledObjectTypes;
985
        }
74 daniel-mar 986
 
1050 daniel-mar 987
        // --- Plugin handling functions
277 daniel-mar 988
 
1116 daniel-mar 989
        /**
990
         * @return OIDplusPlugin[]
991
         */
992
        public static function getAllPlugins(): array {
320 daniel-mar 993
                $res = array();
994
                $res = array_merge($res, self::$pagePlugins);
995
                $res = array_merge($res, self::$authPlugins);
996
                $res = array_merge($res, self::$loggerPlugins);
997
                $res = array_merge($res, self::$objectTypePlugins);
998
                $res = array_merge($res, self::$dbPlugins);
702 daniel-mar 999
                $res = array_merge($res, self::$captchaPlugins);
320 daniel-mar 1000
                $res = array_merge($res, self::$sqlSlangPlugins);
355 daniel-mar 1001
                $res = array_merge($res, self::$languagePlugins);
1116 daniel-mar 1002
                return array_merge($res, self::$designPlugins);
320 daniel-mar 1003
        }
1004
 
1116 daniel-mar 1005
        /**
1006
         * @param string $oid
1007
         * @return OIDplusPlugin|null
1008
         */
1009
        public static function getPluginByOid(string $oid)/*: ?OIDplusPlugin*/ {
320 daniel-mar 1010
                $plugins = self::getAllPlugins();
321 daniel-mar 1011
                foreach ($plugins as $plugin) {
1012
                        if (oid_dotnotation_equal($plugin->getManifest()->getOid(), $oid)) {
1013
                                return $plugin;
320 daniel-mar 1014
                        }
1015
                }
1016
                return null;
1017
        }
1018
 
1116 daniel-mar 1019
        /**
1020
         * @param string $classname
1021
         * @return OIDplusPlugin|null
1022
         */
1023
        public static function getPluginByClassName(string $classname)/*: ?OIDplusPlugin*/ {
380 daniel-mar 1024
                $plugins = self::getAllPlugins();
1025
                foreach ($plugins as $plugin) {
1026
                        if (get_class($plugin) === $classname) {
1027
                                return $plugin;
1028
                        }
1029
                }
1030
                return null;
277 daniel-mar 1031
        }
1032
 
594 daniel-mar 1033
        /**
1122 daniel-mar 1034
         * Checks if the plugin is disabled
1035
         * @return bool true if plugin is enabled, false if plugin is disabled
1036
         * @throws OIDplusException if the class name or config file (disabled setting) does not contain a namespace
1037
         */
1050 daniel-mar 1038
        private static function pluginCheckDisabled($class_name): bool {
1039
                $path = explode('\\', $class_name);
1040
 
1041
                if (count($path) == 1) {
1042
                        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.'));
1043
                }
1044
 
1045
                $class_end = end($path);
1046
                if (OIDplus::baseConfig()->getValue('DISABLE_PLUGIN_'.$class_end, false)) {
1047
                        throw new OIDplusConfigInitializationException(_L('Your base configuration file is outdated. Please change "%1" to "%2".','DISABLE_PLUGIN_'.$class_end,'DISABLE_PLUGIN_'.$class_name));
1048
                }
1049
 
1050
                if (OIDplus::baseConfig()->getValue('DISABLE_PLUGIN_'.$class_name, false)) {
1051
                        return false;
1052
                }
1053
 
1054
                return true;
1055
        }
1056
 
1057
        /**
1116 daniel-mar 1058
         * @param string $pluginFolderMasks
1059
         * @param bool $flat
1060
         * @return OIDplusPluginManifest[]|array<string,array<string,OIDplusPluginManifest>>
1061
         * @throws OIDplusException
1062
         */
1063
        public static function getAllPluginManifests(string $pluginFolderMasks='*', bool $flat=true): array {
277 daniel-mar 1064
                $out = array();
279 daniel-mar 1065
                // Note: glob() will sort by default, so we do not need a page priority attribute.
1066
                //       So you just need to use a numeric plugin directory prefix (padded).
693 daniel-mar 1067
                $ary = array();
1068
                foreach (explode(',',$pluginFolderMasks) as $pluginFolderMask) {
1069
                        $ary = array_merge($ary,glob(OIDplus::localpath().'plugins/'.'*'.'/'.$pluginFolderMask.'/'.'*'.'/manifest.xml'));
1070
                }
646 daniel-mar 1071
 
1072
                // Sort the plugins by their type and name, as if they would be in a single vendor-folder!
1073
                uasort($ary, function($a,$b) {
1074
                        if ($a == $b) return 0;
1075
 
1122 daniel-mar 1076
                        $a = str_replace('\\', '/', $a);
646 daniel-mar 1077
                        $ary = explode('/',$a);
1078
                        $bry = explode('/',$b);
1079
 
1080
                        // First sort by type (publicPage, auth, database, language, ...)
1081
                        $a_type = $ary[count($ary)-1-2];
1082
                        $b_type = $bry[count($bry)-1-2];
1083
                        if ($a_type < $b_type) return -1;
1084
                        if ($a_type > $b_type) return 1;
1085
 
1086
                        // Then sort by name (090_login, 100_whois, etc.)
1087
                        $a_name = $ary[count($ary)-1-1];
1088
                        $b_name = $bry[count($bry)-1-1];
1089
                        if ($a_name < $b_name) return -1;
1090
                        if ($a_name > $b_name) return 1;
1091
 
1092
                        // If it is still equal, then finally sort by vendorname
1093
                        $a_vendor = $ary[count($ary)-1-3];
1094
                        $b_vendor = $bry[count($bry)-1-3];
1095
                        if ($a_vendor < $b_vendor) return -1;
1096
                        if ($a_vendor > $b_vendor) return 1;
1097
                        return 0;
1098
                });
1099
 
277 daniel-mar 1100
                foreach ($ary as $ini) {
1101
                        if (!file_exists($ini)) continue;
1102
 
307 daniel-mar 1103
                        $manifest = new OIDplusPluginManifest();
1104
                        $manifest->loadManifest($ini);
277 daniel-mar 1105
 
473 daniel-mar 1106
                        $class_name = $manifest->getPhpMainClass();
1050 daniel-mar 1107
                        if ($class_name) if (!self::pluginCheckDisabled($class_name)) continue;
473 daniel-mar 1108
 
307 daniel-mar 1109
                        if ($flat) {
1110
                                $out[] = $manifest;
1111
                        } else {
1122 daniel-mar 1112
                                $vendor_folder = basename(dirname($ini, 3));
1116 daniel-mar 1113
                                $plugintype_folder = basename(dirname($ini, 2));
307 daniel-mar 1114
                                $pluginname_folder = basename(dirname($ini));
1115
 
1116
                                if (!isset($out[$plugintype_folder])) $out[$plugintype_folder] = array();
1122 daniel-mar 1117
                                if (!isset($out[$plugintype_folder][$vendor_folder])) $out[$plugintype_folder][$vendor_folder] = array();
1118
                                $out[$plugintype_folder][$vendor_folder][$pluginname_folder] = $manifest;
307 daniel-mar 1119
                        }
277 daniel-mar 1120
                }
1121
                return $out;
1122
        }
1123
 
594 daniel-mar 1124
        /**
1130 daniel-mar 1125
         * @param string|array $pluginDirName
1126
         * @param string $expectedPluginClass
1127
         * @param callable|null $registerCallback
1116 daniel-mar 1128
         * @return string[]
1129
         * @throws OIDplusConfigInitializationException
1130
         * @throws OIDplusException
1131
         * @throws \ReflectionException
1132
         */
1130 daniel-mar 1133
        public static function registerAllPlugins($pluginDirName, string $expectedPluginClass, callable $registerCallback=null): array {
277 daniel-mar 1134
                $out = array();
525 daniel-mar 1135
                if (is_array($pluginDirName)) {
1136
                        $ary = array();
1137
                        foreach ($pluginDirName as $pluginDirName_) {
1138
                                $ary = array_merge($ary, self::getAllPluginManifests($pluginDirName_, false));
1139
                        }
1140
                } else {
1141
                        $ary = self::getAllPluginManifests($pluginDirName, false);
1142
                }
320 daniel-mar 1143
                $known_plugin_oids = array();
1123 daniel-mar 1144
                $known_main_classes_no_namespace = array();
277 daniel-mar 1145
                foreach ($ary as $plugintype_folder => $bry) {
1122 daniel-mar 1146
                        foreach ($bry as $vendor_folder => $cry) {
1147
                                foreach ($cry as $pluginname_folder => $manifest) {
1148
                                        $class_name = $manifest->getPhpMainClass();
438 daniel-mar 1149
 
1122 daniel-mar 1150
                                        // Before we load the plugin, we want to make some checks to confirm
1151
                                        // that the plugin is working correctly.
438 daniel-mar 1152
 
1122 daniel-mar 1153
                                        if (!$class_name) {
1154
                                                throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Manifest does not declare a PHP main class'));
1155
                                        }
1156
                                        if (!self::pluginCheckDisabled($class_name)) {
1157
                                                continue; // Plugin is disabled
1158
                                        }
1072 daniel-mar 1159
 
1123 daniel-mar 1160
                                        // The auto-loader of OIDplus currently does not accept PHP namespaces.
1161
                                        // Reason: The autoloader detects the classes inside plugins/*/*/*/*.class.php, but it cannot know
1162
                                        //         which namespace these files have, because their folder names do not reveal the namespace.
1163
                                        //         So it just ignores the namespace and loads all classes with the same name.
1164
                                        // TODO: Think about a solution; There was a discussion here https://github.com/frdl/frdl-oidplus-plugin-type-pen/issues/1
1165
                                        $tmp = explode('\\',$class_name);
1166
                                        $class_name_no_namespace = end($tmp);
1167
                                        if (in_array($class_name_no_namespace, $known_main_classes_no_namespace)) {
1127 daniel-mar 1168
                                                // Removed check for now, since everything should work correctly
1169
                                                // 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 1170
                                        }
1171
                                        $known_main_classes_no_namespace[] = $class_name_no_namespace;
1172
 
1122 daniel-mar 1173
                                        // Do some basic checks on the plugin PHP main class
1174
                                        if (!class_exists($class_name)) {
1175
                                                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));
1176
                                        }
1177
                                        if (!is_subclass_of($class_name, $expectedPluginClass)) {
1178
                                                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));
1179
                                        }
1180
                                        if (($class_name != $manifest->getTypeClass()) && (!is_subclass_of($class_name, $manifest->getTypeClass()))) {
1181
                                                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()));
1182
                                        }
1183
                                        if (($manifest->getTypeClass() != $expectedPluginClass) && (!is_subclass_of($manifest->getTypeClass(), $expectedPluginClass))) {
1184
                                                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));
1185
                                        }
308 daniel-mar 1186
 
1122 daniel-mar 1187
                                        // Do some basic checks on the plugin OID
1188
                                        $plugin_oid = $manifest->getOid();
1189
                                        if (!$plugin_oid) {
1190
                                                throw new OIDplusException(_L('Plugin "%1" is erroneous', $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder) . ': ' . _L('Does not have an OID'));
1191
                                        }
1192
                                        if (!oid_valid_dotnotation($plugin_oid, false, false, 2)) {
1193
                                                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));
1194
                                        }
1195
                                        if (isset($known_plugin_oids[$plugin_oid])) {
1196
                                                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]));
1197
                                        }
646 daniel-mar 1198
 
1122 daniel-mar 1199
                                        // Additional check: Are third-party plugins using ViaThinkSoft plugin folders, OIDs or class namespaces?
1200
                                        $full_plugin_dir = dirname($manifest->getManifestFile());
1201
                                        $full_plugin_dir = substr($full_plugin_dir, strlen(OIDplus::localpath()));
1202
                                        $dir_is_viathinksoft = str_starts_with($full_plugin_dir, 'plugins/viathinksoft/') || str_starts_with($full_plugin_dir, 'plugins\\viathinksoft\\');
1203
                                        $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) }
1204
                                        $class_is_viathinksoft = str_starts_with($class_name, 'ViaThinkSoft\\');
1205
                                        if ($oid_is_viathinksoft != $class_is_viathinksoft) {
1206
                                                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.'));
1207
                                        }
1208
                                        $plugin_is_viathinksoft = $oid_is_viathinksoft && $class_is_viathinksoft;
1209
                                        if ($dir_is_viathinksoft != $plugin_is_viathinksoft) {
1210
                                                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/'));
1211
                                        }
1072 daniel-mar 1212
 
1122 daniel-mar 1213
                                        // Additional check: does the plugin define JS/CSS although it is not an interactive plugin type?
1214
                                        $has_js = $manifest->getJSFiles();
1215
                                        $has_css = $manifest->getCSSFiles();
1216
                                        $is_interactive = in_array(basename($plugintype_folder), OIDplus::INTERACTIVE_PLUGIN_TYPES);
1217
                                        $is_design = basename($plugintype_folder) === 'design';
1218
                                        if (!$is_interactive && $has_js) {
1219
                                                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'));
1220
                                        }
1221
                                        if (!$is_interactive && !$is_design && $has_css) {
1222
                                                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'));
1223
                                        }
320 daniel-mar 1224
 
1122 daniel-mar 1225
                                        // Additional check: Check "Setup CSS" and "Setup JS" (Allowed for plugin types: database, captcha)
1226
                                        $has_js_setup = $manifest->getJSFilesSetup();
1227
                                        $has_css_setup = $manifest->getCSSFilesSetup();
1228
                                        $is_database = basename($plugintype_folder) === 'database';
1229
                                        $is_captcha = basename($plugintype_folder) === 'captcha';
1230
                                        if (!$is_database && !$is_captcha && $has_js_setup) {
1231
                                                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'));
1232
                                        }
1233
                                        if (!$is_database && !$is_captcha && $has_css_setup) {
1234
                                                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'));
1235
                                        }
632 daniel-mar 1236
 
1122 daniel-mar 1237
                                        // Additional check: Are all CSS/JS files there?
1238
                                        $tmp = $manifest->getManifestLinkedFiles();
1239
                                        foreach ($tmp as $file) {
1240
                                                if (!file_exists($file)) {
1241
                                                        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));
1242
                                                }
1072 daniel-mar 1243
                                        }
1244
 
1122 daniel-mar 1245
                                        // For the next check, we need an instance of the object
1246
                                        $obj = new $class_name();
456 daniel-mar 1247
 
1122 daniel-mar 1248
                                        // Now we can continue
1249
                                        $known_plugin_oids[$plugin_oid] = $vendor_folder . '/' . $plugintype_folder . '/' . $pluginname_folder;
1250
                                        $out[] = $class_name;
1251
                                        if (!is_null($registerCallback)) {
1252
                                                call_user_func($registerCallback, $obj);
444 daniel-mar 1253
 
1122 daniel-mar 1254
                                                // Alternative approaches:
1255
                                                //$registerCallback[0]::{$registerCallback[1]}($obj);
1256
                                                // or:
1257
                                                //forward_static_call($registerCallback, $obj);
1258
                                        }
279 daniel-mar 1259
                                }
277 daniel-mar 1260
                        }
1261
                }
1262
                return $out;
1263
        }
1264
 
1050 daniel-mar 1265
        // --- Initialization of OIDplus
206 daniel-mar 1266
 
1116 daniel-mar 1267
        /**
1268
         * @param bool $html
1269
         * @param bool $keepBaseConfig
1270
         * @return void
1130 daniel-mar 1271
         * @throws OIDplusConfigInitializationException|OIDplusException|\ReflectionException
1116 daniel-mar 1272
         */
1273
        public static function init(bool $html=true, bool $keepBaseConfig=true) {
236 daniel-mar 1274
                self::$html = $html;
1275
 
263 daniel-mar 1276
                // Reset internal state, so we can re-init verything if required
274 daniel-mar 1277
 
263 daniel-mar 1278
                self::$config = null;
374 daniel-mar 1279
                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 1280
                self::$gui = null;
1281
                self::$authUtils = null;
1282
                self::$mailUtils = null;
1283
                self::$menuUtils = null;
1284
                self::$logger = null;
295 daniel-mar 1285
                self::$dbMainSession = null;
1286
                self::$dbIsolatedSession = null;
263 daniel-mar 1287
                self::$pagePlugins = array();
1288
                self::$authPlugins = array();
289 daniel-mar 1289
                self::$loggerPlugins = array();
263 daniel-mar 1290
                self::$objectTypePlugins = array();
1291
                self::$enabledObjectTypes = array();
1292
                self::$disabledObjectTypes = array();
1293
                self::$dbPlugins = array();
702 daniel-mar 1294
                self::$captchaPlugins = array();
274 daniel-mar 1295
                self::$sqlSlangPlugins = array();
355 daniel-mar 1296
                self::$languagePlugins = array();
449 daniel-mar 1297
                self::$designPlugins = array();
263 daniel-mar 1298
                self::$system_id_cache = null;
1299
                self::$sslAvailableCache = null;
468 daniel-mar 1300
                self::$translationArray = array();
74 daniel-mar 1301
 
263 daniel-mar 1302
                // Continue...
1303
 
294 daniel-mar 1304
                OIDplus::baseConfig(); // this loads the base configuration located in userdata/baseconfig/config.inc.php (once!)
1122 daniel-mar 1305
                // You can do changes to the configuration afterwards using OIDplus::baseConfig()->...
263 daniel-mar 1306
 
150 daniel-mar 1307
                // Register database types (highest priority)
1308
 
274 daniel-mar 1309
                // SQL slangs
1310
 
1050 daniel-mar 1311
                self::registerAllPlugins('sqlSlang', OIDplusSqlSlangPlugin::class, array(OIDplus::class,'registerSqlSlangPlugin'));
274 daniel-mar 1312
                foreach (OIDplus::getSqlSlangPlugins() as $plugin) {
1313
                        $plugin->init($html);
1314
                }
1315
 
1316
                // Database providers
1317
 
1050 daniel-mar 1318
                self::registerAllPlugins('database', OIDplusDatabasePlugin::class, array(OIDplus::class,'registerDatabasePlugin'));
237 daniel-mar 1319
                foreach (OIDplus::getDatabasePlugins() as $plugin) {
1320
                        $plugin->init($html);
1321
                }
1322
 
42 daniel-mar 1323
                // Do redirect stuff etc.
74 daniel-mar 1324
 
230 daniel-mar 1325
                self::isSslAvailable(); // This function does automatic redirects
61 daniel-mar 1326
 
263 daniel-mar 1327
                // Construct the configuration manager
66 daniel-mar 1328
 
263 daniel-mar 1329
                OIDplus::config(); // During the construction, various system settings are prepared if required
66 daniel-mar 1330
 
74 daniel-mar 1331
                // Initialize public / private keys
1332
 
227 daniel-mar 1333
                OIDplus::getPkiStatus(true);
74 daniel-mar 1334
 
237 daniel-mar 1335
                // Register non-DB plugins
74 daniel-mar 1336
 
1050 daniel-mar 1337
                self::registerAllPlugins(array('publicPages', 'raPages', 'adminPages'), OIDplusPagePlugin::class, array(OIDplus::class,'registerPagePlugin'));
1338
                self::registerAllPlugins('auth', OIDplusAuthPlugin::class, array(OIDplus::class,'registerAuthPlugin'));
1339
                self::registerAllPlugins('logger', OIDplusLoggerPlugin::class, array(OIDplus::class,'registerLoggerPlugin'));
1185 daniel-mar 1340
                self::logger()->reLogMissing(); // Some previous plugins might have tried to log. Repeat that now.
1050 daniel-mar 1341
                self::registerAllPlugins('objectTypes', OIDplusObjectTypePlugin::class, array(OIDplus::class,'registerObjectTypePlugin'));
1342
                self::registerAllPlugins('language', OIDplusLanguagePlugin::class, array(OIDplus::class,'registerLanguagePlugin'));
1343
                self::registerAllPlugins('design', OIDplusDesignPlugin::class, array(OIDplus::class,'registerDesignPlugin'));
1344
                self::registerAllPlugins('captcha', OIDplusCaptchaPlugin::class, array(OIDplus::class,'registerCaptchaPlugin'));
150 daniel-mar 1345
 
237 daniel-mar 1346
                // Initialize non-DB plugins
224 daniel-mar 1347
 
281 daniel-mar 1348
                foreach (OIDplus::getPagePlugins() as $plugin) {
74 daniel-mar 1349
                        $plugin->init($html);
1350
                }
230 daniel-mar 1351
                foreach (OIDplus::getAuthPlugins() as $plugin) {
1352
                        $plugin->init($html);
1353
                }
289 daniel-mar 1354
                foreach (OIDplus::getLoggerPlugins() as $plugin) {
1355
                        $plugin->init($html);
1356
                }
230 daniel-mar 1357
                foreach (OIDplus::getObjectTypePlugins() as $plugin) {
1358
                        $plugin->init($html);
1359
                }
355 daniel-mar 1360
                foreach (OIDplus::getLanguagePlugins() as $plugin) {
1361
                        $plugin->init($html);
1362
                }
449 daniel-mar 1363
                foreach (OIDplus::getDesignPlugins() as $plugin) {
1364
                        $plugin->init($html);
1365
                }
778 daniel-mar 1366
                foreach (OIDplus::getCaptchaPlugins() as $plugin) {
1367
                        $plugin->init($html);
1368
                }
412 daniel-mar 1369
 
778 daniel-mar 1370
                if (PHP_SAPI != 'cli') {
1371
 
1372
                        // Prepare some security related response headers (default values)
1373
 
1374
                        $content_language =
1375
                                strtolower(substr(OIDplus::getCurrentLang(),0,2)) . '-' .
1376
                                strtoupper(substr(OIDplus::getCurrentLang(),2,2)); // e.g. 'en-US'
1377
 
1378
                        $http_headers = array(
1379
                                "X-Content-Type-Options" => "nosniff",
1380
                                "X-XSS-Protection" => "1; mode=block",
1381
                                "X-Frame-Options" => "SAMEORIGIN",
1382
                                "Referrer-Policy" => array(
1383
                                        "no-referrer-when-downgrade"
1384
                                ),
1385
                                "Cache-Control" => array(
1386
                                        "no-cache",
1387
                                        "no-store",
1388
                                        "must-revalidate"
1389
                                ),
1390
                                "Pragma" => "no-cache",
1391
                                "Content-Language" => $content_language,
1392
                                "Expires" => "0",
1393
                                "Content-Security-Policy" => array(
1001 daniel-mar 1394
                                        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
1395
 
1396
                                        // --- Fetch directives ---
1397
                                        "child-src" => array(
1398
                                                "'self'",
1399
                                                "blob:"
1400
                                        ),
1401
                                        "connect-src" => array(
1402
                                                "'self'",
1403
                                                "blob:"
1404
                                        ),
778 daniel-mar 1405
                                        "default-src" => array(
1406
                                                "'self'",
1407
                                                "blob:",
1408
                                                "https://cdnjs.cloudflare.com/"
1409
                                        ),
1001 daniel-mar 1410
                                        "font-src" => array(
778 daniel-mar 1411
                                                "'self'",
1001 daniel-mar 1412
                                                "blob:"
778 daniel-mar 1413
                                        ),
1001 daniel-mar 1414
                                        "frame-src" => array(
1415
                                                "'self'",
1416
                                                "blob:"
1417
                                        ),
778 daniel-mar 1418
                                        "img-src" => array(
1001 daniel-mar 1419
                                                "blob:",
778 daniel-mar 1420
                                                "data:",
1421
                                                "http:",
1422
                                                "https:"
1423
                                        ),
1001 daniel-mar 1424
                                        "manifest-src" => array(
1425
                                                "'self'",
1426
                                                "blob:"
1427
                                        ),
1428
                                        "media-src" => array(
1429
                                                "'self'",
1430
                                                "blob:"
1431
                                        ),
1432
                                        "object-src" => array(
1433
                                                "'none'"
1434
                                        ),
778 daniel-mar 1435
                                        "script-src" => array(
1436
                                                "'self'",
1437
                                                "'unsafe-inline'",
1438
                                                "'unsafe-eval'",
1439
                                                "blob:",
1440
                                                "https://cdnjs.cloudflare.com/",
1441
                                                "https://polyfill.io/"
1442
                                        ),
1001 daniel-mar 1443
                                        // script-src-elem not used
1444
                                        // script-src-attr not used
1445
                                        "style-src" => array(
1446
                                                "'self'",
1447
                                                "'unsafe-inline'",
1448
                                                "https://cdnjs.cloudflare.com/"
1449
                                        ),
1450
                                        // style-src-elem not used
1451
                                        // style-src-attr not used
1452
                                        "worker-src" => array(
1453
                                                "'self'",
1454
                                                "blob:"
1455
                                        ),
1456
 
1457
                                        // --- Navigation directives ---
778 daniel-mar 1458
                                        "frame-ancestors" => array(
1112 daniel-mar 1459
                                                "'none'"
778 daniel-mar 1460
                                        ),
1461
                                )
1462
                        );
1463
 
780 daniel-mar 1464
                        // Give plugins the opportunity to manipulate/extend the headers
778 daniel-mar 1465
 
1466
                        foreach (OIDplus::getSqlSlangPlugins() as $plugin) {
1467
                                $plugin->httpHeaderCheck($http_headers);
1468
                        }
1015 daniel-mar 1469
                        //foreach (OIDplus::getDatabasePlugins() as $plugin) {
1470
                        if ($plugin = OIDplus::getActiveDatabasePlugin()) {
778 daniel-mar 1471
                                $plugin->httpHeaderCheck($http_headers);
1472
                        }
1473
                        foreach (OIDplus::getPagePlugins() as $plugin) {
1474
                                $plugin->httpHeaderCheck($http_headers);
1475
                        }
1476
                        foreach (OIDplus::getAuthPlugins() as $plugin) {
1477
                                $plugin->httpHeaderCheck($http_headers);
1478
                        }
1479
                        foreach (OIDplus::getLoggerPlugins() as $plugin) {
1480
                                $plugin->httpHeaderCheck($http_headers);
1481
                        }
1482
                        foreach (OIDplus::getObjectTypePlugins() as $plugin) {
1483
                                $plugin->httpHeaderCheck($http_headers);
1484
                        }
1485
                        foreach (OIDplus::getLanguagePlugins() as $plugin) {
1486
                                $plugin->httpHeaderCheck($http_headers);
1487
                        }
1488
                        foreach (OIDplus::getDesignPlugins() as $plugin) {
1489
                                $plugin->httpHeaderCheck($http_headers);
1490
                        }
1015 daniel-mar 1491
                        //foreach (OIDplus::getCaptchaPlugins() as $plugin) {
1492
                        if ($plugin = OIDplus::getActiveCaptchaPlugin()) {
778 daniel-mar 1493
                                $plugin->httpHeaderCheck($http_headers);
1494
                        }
1495
 
1496
                        // Prepare to send the headers to the client
1497
                        // The headers are sent automatically when the first output comes or the script ends
1498
 
1499
                        foreach ($http_headers as $name => $val) {
1500
 
1501
                                // Plugins can remove standard OIDplus headers by setting the value to null.
1116 daniel-mar 1502
                                if (is_null($val)) continue;
778 daniel-mar 1503
 
1504
                                // Some headers can be written as arrays to make it easier for plugin authors
1505
                                // to manipulate/extend the contents.
1506
                                if (is_array($val)) {
1507
                                        if ((strtolower($name) == 'cache-control') ||
1122 daniel-mar 1508
                                                (strtolower($name) == 'referrer-policy'))
778 daniel-mar 1509
                                        {
1510
                                                if (count($val) == 0) continue;
1511
                                                $val = implode(', ', $val);
1512
                                        } else if (strtolower($name) == 'content-security-policy') {
1513
                                                if (count($val) == 0) continue;
780 daniel-mar 1514
                                                foreach ($val as $tmp1 => &$tmp2) {
1515
                                                        $tmp2 = array_unique($tmp2);
1516
                                                        $tmp2 = $tmp1.' '.implode(' ', $tmp2);
1517
                                                }
778 daniel-mar 1518
                                                $val = implode('; ', $val);
1519
                                        } else {
779 daniel-mar 1520
                                                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 1521
                                        }
1522
                                }
1523
 
1524
                                if (is_string($val)) {
1160 daniel-mar 1525
                                        @header("$name: $val");
778 daniel-mar 1526
                                }
1527
                        }
1528
 
1529
                } // endif (PHP_SAPI != 'cli')
1530
 
412 daniel-mar 1531
                // Initialize other stuff (i.e. things which require the logger!)
1532
 
1533
                OIDplus::recognizeSystemUrl(); // Make sure "last_known_system_url" is set
1534
                OIDplus::recognizeVersion(); // Make sure "last_known_version" is set and a log entry is created
2 daniel-mar 1535
        }
42 daniel-mar 1536
 
1050 daniel-mar 1537
        // --- System URL, System ID, PKI, and other functions
227 daniel-mar 1538
 
1116 daniel-mar 1539
        /**
1540
         * @return void
1541
         */
412 daniel-mar 1542
        private static function recognizeSystemUrl() {
1543
                try {
1072 daniel-mar 1544
                        $url = OIDplus::webpath(null,self::PATH_ABSOLUTE_CANONICAL);
412 daniel-mar 1545
                        OIDplus::config()->setValue('last_known_system_url', $url);
1050 daniel-mar 1546
                } catch (\Exception $e) {
412 daniel-mar 1547
                }
1548
        }
497 daniel-mar 1549
 
1116 daniel-mar 1550
        /**
1551
         * @return false|int
1552
         */
496 daniel-mar 1553
        private static function getExecutingScriptPathDepth() {
1554
                if (PHP_SAPI == 'cli') {
1555
                        global $argv;
1556
                        $test_dir = dirname(realpath($argv[0]));
1557
                } else {
1558
                        if (!isset($_SERVER["SCRIPT_FILENAME"])) return false;
1559
                        $test_dir = dirname($_SERVER['SCRIPT_FILENAME']);
1560
                }
1561
                $test_dir = str_replace('\\', '/', $test_dir);
1562
                $steps_up = 0;
928 daniel-mar 1563
                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 1564
                        $test_dir = dirname($test_dir);
1565
                        $steps_up++;
1566
                        if ($steps_up == 1000) return false; // to make sure there will never be an infinite loop
1567
                }
1568
                return $steps_up;
1569
        }
632 daniel-mar 1570
 
1116 daniel-mar 1571
        /**
1572
         * @return bool
1573
         */
1574
        public static function isSSL(): bool {
580 daniel-mar 1575
                return isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on');
1576
        }
412 daniel-mar 1577
 
806 daniel-mar 1578
        /**
1579
         * Returns the URL of the system.
1580
         * @param int $mode If true or OIDplus::PATH_RELATIVE, the returning path is relative to the currently executed
1581
         *                  PHP script (i.e. index.php , not the plugin PHP script!). False or OIDplus::PATH_ABSOLUTE is
1582
         *                  results in an absolute URL. OIDplus::PATH_ABSOLUTE_CANONICAL is an absolute URL,
1583
         *                  but a canonical path (set by base config setting CANONICAL_SYSTEM_URL) is preferred.
1584
         * @return string|false The URL, with guaranteed trailing path delimiter for directories
1116 daniel-mar 1585
         * @throws OIDplusException
806 daniel-mar 1586
         */
1116 daniel-mar 1587
        private static function getSystemUrl(int $mode) {
806 daniel-mar 1588
                if ($mode === self::PATH_RELATIVE) {
778 daniel-mar 1589
                        $steps_up = self::getExecutingScriptPathDepth();
1590
                        if ($steps_up === false) {
1591
                                return false;
1592
                        } else {
1593
                                return str_repeat('../', $steps_up);
1594
                        }
1595
                } else {
806 daniel-mar 1596
                        if ($mode === self::PATH_ABSOLUTE_CANONICAL) {
1597
                                $tmp = OIDplus::baseConfig()->getValue('CANONICAL_SYSTEM_URL', '');
1598
                                if ($tmp) {
1599
                                        return rtrim($tmp,'/').'/';
1600
                                }
326 daniel-mar 1601
                        }
778 daniel-mar 1602
 
495 daniel-mar 1603
                        if (PHP_SAPI == 'cli') {
494 daniel-mar 1604
                                try {
1605
                                        return OIDplus::config()->getValue('last_known_system_url', false);
1050 daniel-mar 1606
                                } catch (\Exception $e) {
494 daniel-mar 1607
                                        return false;
1608
                                }
778 daniel-mar 1609
                        } else {
1610
                                // First, try to find out how many levels we need to go up
1611
                                $steps_up = self::getExecutingScriptPathDepth();
326 daniel-mar 1612
 
778 daniel-mar 1613
                                // Then go up these amount of levels, based on SCRIPT_NAME/argv[0]
1614
                                $res = dirname($_SERVER['SCRIPT_NAME'].'index.php'); // This fake 'index.php' ensures that SCRIPT_NAME does not end with '/', which would make dirname() fail
1615
                                for ($i=0; $i<$steps_up; $i++) {
1616
                                        $res = dirname($res);
1617
                                }
1618
                                $res = str_replace('\\', '/', $res);
1619
                                if ($res == '/') $res = '';
227 daniel-mar 1620
 
778 daniel-mar 1621
                                // Add protocol and hostname
580 daniel-mar 1622
                                $is_ssl = self::isSSL();
495 daniel-mar 1623
                                $protocol = $is_ssl ? 'https' : 'http'; // do not translate
1624
                                $host = $_SERVER['HTTP_HOST']; // includes port if it is not 80/443
778 daniel-mar 1625
 
1626
                                return $protocol.'://'.$host.$res.'/';
495 daniel-mar 1627
                        }
227 daniel-mar 1628
                }
1629
        }
1630
 
1116 daniel-mar 1631
        /**
1130 daniel-mar 1632
         * @param string $pubKey
1116 daniel-mar 1633
         * @return false|string
1634
         */
1130 daniel-mar 1635
        private static function pubKeyToRaw(string $pubKey) {
827 daniel-mar 1636
                $m = array();
1140 daniel-mar 1637
                if (preg_match('@BEGIN PUBLIC KEY\\-+([^\\-]+)\\-+END PUBLIC KEY@imU', $pubKey, $m)) {
1073 daniel-mar 1638
                        return base64_decode($m[1], false);
827 daniel-mar 1639
                }
1640
                return false;
1641
        }
1642
 
1116 daniel-mar 1643
        /**
1130 daniel-mar 1644
         * @param string $pubKey
1116 daniel-mar 1645
         * @return false|int
1646
         */
1130 daniel-mar 1647
        private static function getSystemIdFromPubKey(string $pubKey) {
1073 daniel-mar 1648
                $rawData = self::pubKeyToRaw($pubKey);
1649
                if ($rawData === false) return false;
1650
                return smallhash($rawData);
1651
        }
1652
 
1116 daniel-mar 1653
        /**
1130 daniel-mar 1654
         * @param string $pubKey
1116 daniel-mar 1655
         * @return false|string
1656
         */
1130 daniel-mar 1657
        private static function getSystemGuidFromPubKey(string $pubKey) {
1336 daniel-mar 1658
                /*
1073 daniel-mar 1659
                $rawData = self::pubKeyToRaw($pubKey);
1660
                if ($rawData === false) return false;
1661
                $normalizedBase64 = base64_encode($rawData);
1662
                return gen_uuid_sha1_namebased(self::UUID_NAMEBASED_NS_Base64PubKey, $normalizedBase64);
1336 daniel-mar 1663
                */
1664
 
1665
                // https://github.com/danielmarschall/oidplus/blob/master/doc/oidplus_custom_guid.md
1666
                $sysid = OIDplus::getSystemId(false);
1667
                $sysid_int = $sysid ? $sysid : 0;
1668
                return gen_uuid_v8(
1669
                        dechex($sysid_int),
1670
                        dechex(0), // Creation time of the system unknown
1671
                        dechex(0), // Reserved
1672
                        dechex(0), // 0=System, otherwise Object Namespace
1673
                        sha1('') // Objectname, empty string for system UUID
1674
                );
1073 daniel-mar 1675
        }
1676
 
227 daniel-mar 1677
        private static $system_id_cache = null;
1116 daniel-mar 1678
 
1679
        /**
1680
         * @param bool $oid
1681
         * @return false|string
1682
         * @throws OIDplusException
1683
         */
1684
        public static function getSystemId(bool $oid=false) {
227 daniel-mar 1685
                if (!is_null(self::$system_id_cache)) {
1686
                        $out = self::$system_id_cache;
1687
                } else {
1688
                        $out = false;
1689
 
1690
                        if (self::getPkiStatus(true)) {
830 daniel-mar 1691
                                $pubKey = OIDplus::getSystemPublicKey();
827 daniel-mar 1692
                                $out = self::getSystemIdFromPubKey($pubKey);
227 daniel-mar 1693
                        }
1694
                        self::$system_id_cache = $out;
1695
                }
350 daniel-mar 1696
                if (!$out) return false;
291 daniel-mar 1697
                return ($oid ? '1.3.6.1.4.1.37476.30.9.' : '').$out;
227 daniel-mar 1698
        }
1699
 
1073 daniel-mar 1700
        private static $system_guid_cache = null;
1116 daniel-mar 1701
 
1702
        /**
1703
         * @return false|string
1704
         * @throws OIDplusException
1705
         */
1073 daniel-mar 1706
        public static function getSystemGuid() {
1707
                if (!is_null(self::$system_guid_cache)) {
1708
                        $out = self::$system_guid_cache;
1709
                } else {
1710
                        $out = false;
1711
 
1712
                        if (self::getPkiStatus(true)) {
1713
                                $pubKey = OIDplus::getSystemPublicKey();
1714
                                $out = self::getSystemGuidFromPubKey($pubKey);
1715
                        }
1716
                        self::$system_guid_cache = $out;
1717
                }
1718
                if (!$out) return false;
1719
                return $out;
1720
        }
1721
 
1116 daniel-mar 1722
        /**
1723
         * @return array|string
1724
         */
825 daniel-mar 1725
        public static function getOpenSslCnf() {
1726
                // The following functions need a config file, otherway they don't work
1727
                // - openssl_csr_new
1728
                // - openssl_csr_sign
1729
                // - openssl_pkey_export
1730
                // - openssl_pkey_export_to_file
1731
                // - openssl_pkey_new
1732
                $tmp = @getenv('OPENSSL_CONF');
1733
                if ($tmp && file_exists($tmp)) return $tmp;
1734
 
1735
                // OpenSSL in XAMPP does not work OOBE, since the OPENSSL_CONF is
1736
                // C:/xampp/apache/bin/openssl.cnf and not C:/xampp/apache/conf/openssl.cnf
1737
                // Bug reports are more than 10 years old and nobody cares...
1738
                // Use our own config file
1739
                return __DIR__.'/../../vendor/phpseclib/phpseclib/phpseclib/openssl.cnf';
1740
        }
1741
 
1116 daniel-mar 1742
        /**
1743
         * @return string
1744
         */
1745
        private static function getPrivKeyPassphraseFilename(): string {
830 daniel-mar 1746
                return OIDplus::localpath() . 'userdata/privkey_secret.php';
1747
        }
227 daniel-mar 1748
 
1116 daniel-mar 1749
        /**
1750
         * @return void
1751
         */
830 daniel-mar 1752
        private static function tryCreatePrivKeyPassphrase() {
1753
                $file = self::getPrivKeyPassphraseFilename();
827 daniel-mar 1754
 
830 daniel-mar 1755
                $passphrase = generateRandomString(64);
1756
                $cont = "<?php\n";
1757
                $cont .= "// ATTENTION! This file was automatically generated by OIDplus to encrypt the private key\n";
1758
                $cont .= "// that is located in your database configuration table. DO NOT ALTER OR DELETE THIS FILE,\n";
1759
                $cont .= "// otherwise you will lose your OIDplus System-ID and all services connected with it!\n";
831 daniel-mar 1760
                $cont .= "// If multiple systems access the same database, then this file must be synchronous\n";
1761
                $cont .= "// between all systems, otherwise you will lose your system ID, too!\n";
830 daniel-mar 1762
                $cont .= "\$passphrase = '$passphrase';\n";
1763
                $cont .= "// End of file\n";
1764
 
1765
                @file_put_contents($file, $cont);
1766
        }
1767
 
1116 daniel-mar 1768
        /**
1769
         * @return string|false
1770
         */
830 daniel-mar 1771
        private static function getPrivKeyPassphrase() {
1772
                $file = self::getPrivKeyPassphraseFilename();
1773
                if (!file_exists($file)) return false;
1774
                $cont = file_get_contents($file);
1775
                $m = array();
1776
                if (!preg_match("@'(.+)'@isU", $cont, $m)) return false;
1777
                return $m[1];
1778
        }
1779
 
1116 daniel-mar 1780
        /**
1781
         * @return string|false
1782
         * @throws OIDplusException
1783
         */
830 daniel-mar 1784
        public static function getSystemPrivateKey() {
227 daniel-mar 1785
                $privKey = OIDplus::config()->getValue('oidplus_private_key');
830 daniel-mar 1786
                if ($privKey == '') return false;
1787
 
1788
                $passphrase = self::getPrivKeyPassphrase();
1789
                if ($passphrase !== false) {
1790
                        $privKey = decrypt_private_key($privKey, $passphrase);
1791
                }
1792
 
1793
                if (is_privatekey_encrypted($privKey)) {
1794
                        // This can happen if the key file has vanished
1795
                        return false;
1796
                }
1797
 
1798
                return $privKey;
1799
        }
1800
 
1116 daniel-mar 1801
        /**
1802
         * @return string|false
1803
         * @throws OIDplusException
1804
         */
830 daniel-mar 1805
        public static function getSystemPublicKey() {
227 daniel-mar 1806
                $pubKey = OIDplus::config()->getValue('oidplus_public_key');
830 daniel-mar 1807
                if ($pubKey == '') return false;
1808
                return $pubKey;
1809
        }
227 daniel-mar 1810
 
1116 daniel-mar 1811
        /**
1812
         * @param bool $try_generate
1813
         * @return bool
1814
         * @throws OIDplusException
1815
         */
1816
        public static function getPkiStatus(bool $try_generate=false): bool {
830 daniel-mar 1817
                if (!function_exists('openssl_pkey_new')) return false;
227 daniel-mar 1818
 
1226 daniel-mar 1819
                if (basename($_SERVER['SCRIPT_NAME']) == 'test_database_plugins.php') return false; // database switching will destroy keys because of the secret file
1820
 
830 daniel-mar 1821
                if ($try_generate) {
1822
                        // For debug purposes: Invalidate current key once:
1823
                        //OIDplus::config()->setValue('oidplus_private_key', '');
256 daniel-mar 1824
 
830 daniel-mar 1825
                        $privKey = OIDplus::getSystemPrivateKey();
1826
                        $pubKey = OIDplus::getSystemPublicKey();
1827
                        if (!verify_private_public_key($privKey, $pubKey)) {
1828
                                if ($pubKey) {
1267 daniel-mar 1829
                                        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 1830
                                }
227 daniel-mar 1831
 
830 daniel-mar 1832
                                $pkey_config = array(
1116 daniel-mar 1833
                                        "digest_alg" => "sha512",
1834
                                        "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
1835
                                        "private_key_type" => OPENSSL_KEYTYPE_RSA,
1836
                                        "config" => OIDplus::getOpenSslCnf()
830 daniel-mar 1837
                                );
227 daniel-mar 1838
 
830 daniel-mar 1839
                                // Create the private and public key
1840
                                $res = openssl_pkey_new($pkey_config);
1841
                                if ($res === false) return false;
239 daniel-mar 1842
 
830 daniel-mar 1843
                                // Extract the private key from $res to $privKey
1844
                                if (openssl_pkey_export($res, $privKey, null, $pkey_config) === false) return false;
227 daniel-mar 1845
 
830 daniel-mar 1846
                                // Extract the public key from $res to $pubKey
1847
                                $tmp = openssl_pkey_get_details($res);
1848
                                if ($tmp === false) return false;
1849
                                $pubKey = $tmp["key"];
1850
 
1851
                                // encrypt new keys using a passphrase stored in a secret file
1852
                                self::tryCreatePrivKeyPassphrase(); // *try* (re)generate this file
1853
                                $passphrase = self::getPrivKeyPassphrase();
1854
                                if ($passphrase !== false) {
1855
                                        $privKey = encrypt_private_key($privKey, $passphrase);
1856
                                }
1857
 
1858
                                // Calculate the system ID from the public key
1859
                                $system_id = self::getSystemIdFromPubKey($pubKey);
1860
                                if ($system_id !== false) {
1861
                                        // Save the key pair to database
1862
                                        OIDplus::config()->setValue('oidplus_private_key', $privKey);
1863
                                        OIDplus::config()->setValue('oidplus_public_key', $pubKey);
1864
 
1865
                                        // Log the new system ID
1267 daniel-mar 1866
                                        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 1867
                                }
1868
                        } else {
1869
                                $passphrase = self::getPrivKeyPassphrase();
831 daniel-mar 1870
                                $rawPrivKey = OIDplus::config()->getValue('oidplus_private_key');
1871
                                if (($passphrase === false) || !is_privatekey_encrypted($rawPrivKey)) {
830 daniel-mar 1872
                                        // Upgrade to new encrypted keys
1873
                                        self::tryCreatePrivKeyPassphrase(); // *try* generate this file
1874
                                        $passphrase = self::getPrivKeyPassphrase();
1875
                                        if ($passphrase !== false) {
1876
                                                $privKey = encrypt_private_key($privKey, $passphrase);
1267 daniel-mar 1877
                                                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 1878
                                                OIDplus::config()->setValue('oidplus_private_key', $privKey);
1879
                                        }
1880
                                }
227 daniel-mar 1881
                        }
1882
                }
1883
 
830 daniel-mar 1884
                $privKey = OIDplus::getSystemPrivateKey();
1885
                $pubKey = OIDplus::getSystemPublicKey();
227 daniel-mar 1886
                return verify_private_public_key($privKey, $pubKey);
1887
        }
1888
 
1116 daniel-mar 1889
        /**
1890
         * @return string|void
1891
         */
170 daniel-mar 1892
        public static function getInstallType() {
486 daniel-mar 1893
                $counter = 0;
1894
 
661 daniel-mar 1895
                if ($new_version_file_exists = file_exists(OIDplus::localpath().'.version.php')) {
486 daniel-mar 1896
                        $counter++;
591 daniel-mar 1897
                }
661 daniel-mar 1898
                if ($old_version_file_exists = file_exists(OIDplus::localpath().'oidplus_version.txt')) {
1899
                        $counter++;
1900
                }
1901
                $version_file_exists = $old_version_file_exists | $new_version_file_exists;
1195 daniel-mar 1902
 
1903
                if ($svn_dir_exists = (OIDplus::findSvnFolder() !== false)) {
486 daniel-mar 1904
                        $counter++;
591 daniel-mar 1905
                }
698 daniel-mar 1906
                if ($git_dir_exists = (OIDplus::findGitFolder() !== false)) {
486 daniel-mar 1907
                        $counter++;
591 daniel-mar 1908
                }
486 daniel-mar 1909
 
1910
                if ($counter === 0) {
360 daniel-mar 1911
                        return 'unknown'; // do not translate
170 daniel-mar 1912
                }
591 daniel-mar 1913
                else if ($counter > 1) {
360 daniel-mar 1914
                        return 'ambigous'; // do not translate
170 daniel-mar 1915
                }
591 daniel-mar 1916
                else if ($svn_dir_exists) {
360 daniel-mar 1917
                        return 'svn-wc'; // do not translate
170 daniel-mar 1918
                }
591 daniel-mar 1919
                else if ($git_dir_exists) {
486 daniel-mar 1920
                        return 'git-wc'; // do not translate
1921
                }
591 daniel-mar 1922
                else if ($version_file_exists) {
360 daniel-mar 1923
                        return 'svn-snapshot'; // do not translate
170 daniel-mar 1924
                }
1925
        }
1926
 
1116 daniel-mar 1927
        /**
1928
         * @return void
1929
         */
412 daniel-mar 1930
        private static function recognizeVersion() {
1931
                try {
1194 daniel-mar 1932
                        if ($ver_now = OIDplus::getVersion()) {
1933
                                $ver_prev = OIDplus::config()->getValue("last_known_version");
1934
                                if (($ver_prev) && ($ver_now != $ver_prev)) {
1935
                                        // TODO: Problem: When the system was updated using SVN or GIT in the console, then the IP address of the next random visitor of the website is logged!
1936
                                        //       Idea: Maybe we should extend the mask code with some kind of magic constant "[NO_IP]", so that no IP is logged for that event?
1267 daniel-mar 1937
                                        OIDplus::logger()->log("V2:[INFO]A", "Detected system version change from '%1' to '%2'", $ver_prev, $ver_now);
856 daniel-mar 1938
 
1194 daniel-mar 1939
                                        // Just to be sure, recanonize objects (we don't do it at every page visit due to performance reasons)
1940
                                        self::recanonizeObjects();
1941
                                }
1942
                                OIDplus::config()->setValue("last_known_version", $ver_now);
412 daniel-mar 1943
                        }
1050 daniel-mar 1944
                } catch (\Exception $e) {
412 daniel-mar 1945
                }
1946
        }
1947
 
1116 daniel-mar 1948
        /**
1194 daniel-mar 1949
         * @return false|string
1116 daniel-mar 1950
         */
111 daniel-mar 1951
        public static function getVersion() {
412 daniel-mar 1952
                static $cachedVersion = null;
1953
                if (!is_null($cachedVersion)) {
1954
                        return $cachedVersion;
1955
                }
1956
 
486 daniel-mar 1957
                $installType = OIDplus::getInstallType();
1958
 
1959
                if ($installType === 'svn-wc') {
1198 daniel-mar 1960
                        if (is_dir($svn_dir = OIDplus::findSvnFolder())) {
1961
                                $ver = get_svn_revision($svn_dir);
1962
                                if ($ver)
1963
                                        return ($cachedVersion = 'svn-'.$ver);
1964
                        }
111 daniel-mar 1965
                }
162 daniel-mar 1966
 
486 daniel-mar 1967
                if ($installType === 'git-wc') {
1116 daniel-mar 1968
                        $ver = OIDplus::getGitsvnRevision();
486 daniel-mar 1969
                        if ($ver)
1970
                                return ($cachedVersion = 'svn-'.$ver);
162 daniel-mar 1971
                }
1972
 
486 daniel-mar 1973
                if ($installType === 'svn-snapshot') {
661 daniel-mar 1974
                        $cont = '';
1975
                        if (file_exists($filename = OIDplus::localpath().'oidplus_version.txt'))
1976
                                $cont = file_get_contents($filename);
1977
                        if (file_exists($filename = OIDplus::localpath().'.version.php'))
1978
                                $cont = file_get_contents($filename);
386 daniel-mar 1979
                        $m = array();
360 daniel-mar 1980
                        if (preg_match('@Revision (\d+)@', $cont, $m)) // do not translate
412 daniel-mar 1981
                                return ($cachedVersion = 'svn-'.$m[1]); // do not translate
162 daniel-mar 1982
                }
1983
 
486 daniel-mar 1984
                return ($cachedVersion = false); // version ambigous or unknown
111 daniel-mar 1985
        }
1986
 
974 daniel-mar 1987
        const ENFORCE_SSL_NO   = 0;
1988
        const ENFORCE_SSL_YES  = 1;
1989
        const ENFORCE_SSL_AUTO = 2;
1117 daniel-mar 1990
 
1991
        /**
1992
         * @var bool|null
1993
         */
230 daniel-mar 1994
        private static $sslAvailableCache = null;
1116 daniel-mar 1995
 
1996
        /**
1117 daniel-mar 1997
         * @return bool
1116 daniel-mar 1998
         * @throws OIDplusException, OIDplusConfigInitializationException
1999
         */
1117 daniel-mar 2000
        public static function isSslAvailable(): bool {
230 daniel-mar 2001
                if (!is_null(self::$sslAvailableCache)) return self::$sslAvailableCache;
256 daniel-mar 2002
 
495 daniel-mar 2003
                if (PHP_SAPI == 'cli') {
230 daniel-mar 2004
                        self::$sslAvailableCache = false;
2005
                        return false;
2006
                }
2007
 
49 daniel-mar 2008
                $timeout = 2;
580 daniel-mar 2009
                $already_ssl = self::isSSL();
80 daniel-mar 2010
                $ssl_port = 443;
1059 daniel-mar 2011
                $host_with_port = $_SERVER['HTTP_HOST'];
2012
                $host_no_port = explode(':',$host_with_port)[0];
1117 daniel-mar 2013
                $host_ssl = $host_no_port . ($ssl_port != 443 ? ':'.$ssl_port : '');
42 daniel-mar 2014
 
974 daniel-mar 2015
                if ($already_ssl) {
1059 daniel-mar 2016
                        OIDplus::cookieUtils()->setcookie('SSL_CHECK', '1', 0, true/*allowJS*/, null/*samesite*/, true/*forceInsecure*/);
974 daniel-mar 2017
                        self::$sslAvailableCache = true;
2018
                        return true;
2019
                } else {
2020
                        if (isset($_COOKIE['SSL_CHECK']) && ($_COOKIE['SSL_CHECK'] == '1')) {
2021
                                // The cookie "SSL_CHECK" is set once a website was loaded with HTTPS.
2022
                                // It forces subsequent HTTP calls to redirect to HTTPS (like HSTS).
2023
                                // The reason is the following problem:
2024
                                // If you open the page with HTTPS first, then the CSRF token cookies will get the "secure" flag
2025
                                // If you open the page then with HTTP, the HTTP cannot access the secure CSRF cookies,
2026
                                // Chrome will then block "Set-Cookie" since the HTTP cookie would overwrite the HTTPS cookie.
1059 daniel-mar 2027
                                // So we MUST redirect, even if the Mode is ENFORCE_SSL_NO.
974 daniel-mar 2028
                                // Note: SSL_CHECK is NOT a replacement for HSTS! You should use HSTS,
1059 daniel-mar 2029
                                //       because on there your browser ensures that HTTPS is called, before the server
2030
                                //       is even contacted (and therefore, no HTTP connection can be hacked).
974 daniel-mar 2031
                                $mode = OIDplus::ENFORCE_SSL_YES;
2032
                        } else {
2033
                                $mode = OIDplus::baseConfig()->getValue('ENFORCE_SSL', OIDplus::ENFORCE_SSL_AUTO);
2034
                        }
261 daniel-mar 2035
 
974 daniel-mar 2036
                        if ($mode == OIDplus::ENFORCE_SSL_NO) {
2037
                                // No SSL available
2038
                                self::$sslAvailableCache = false;
2039
                                return false;
2040
                        } else if ($mode == OIDplus::ENFORCE_SSL_YES) {
2041
                                // Force SSL
1059 daniel-mar 2042
                                $location = 'https://' . $host_ssl . $_SERVER['REQUEST_URI'];
80 daniel-mar 2043
                                header('Location:'.$location);
360 daniel-mar 2044
                                die(_L('Redirecting to HTTPS...'));
974 daniel-mar 2045
                        } else if ($mode == OIDplus::ENFORCE_SSL_AUTO) {
2046
                                // Automatic SSL detection
80 daniel-mar 2047
                                if (isset($_COOKIE['SSL_CHECK'])) {
2048
                                        // We already had the HTTPS detection done before.
974 daniel-mar 2049
                                        if ($_COOKIE['SSL_CHECK'] == '1') {
80 daniel-mar 2050
                                                // HTTPS was detected before, but we are HTTP. Redirect now
1059 daniel-mar 2051
                                                $location = 'https://' . $host_ssl . $_SERVER['REQUEST_URI'];
80 daniel-mar 2052
                                                header('Location:'.$location);
360 daniel-mar 2053
                                                die(_L('Redirecting to HTTPS...'));
80 daniel-mar 2054
                                        } else {
2055
                                                // No HTTPS available. Do nothing.
230 daniel-mar 2056
                                                self::$sslAvailableCache = false;
80 daniel-mar 2057
                                                return false;
2058
                                        }
49 daniel-mar 2059
                                } else {
80 daniel-mar 2060
                                        // This is our first check (or the browser didn't accept the SSL_CHECK cookie)
386 daniel-mar 2061
                                        $errno = -1;
2062
                                        $errstr = '';
1059 daniel-mar 2063
                                        if (@fsockopen($host_no_port, $ssl_port, $errno, $errstr, $timeout)) {
80 daniel-mar 2064
                                                // HTTPS detected. Redirect now, and remember that we had detected HTTPS
1059 daniel-mar 2065
                                                OIDplus::cookieUtils()->setcookie('SSL_CHECK', '1', 0, true/*allowJS*/, null/*samesite*/, true/*forceInsecure*/);
2066
                                                $location = 'https://' . $host_ssl . $_SERVER['REQUEST_URI'];
80 daniel-mar 2067
                                                header('Location:'.$location);
360 daniel-mar 2068
                                                die(_L('Redirecting to HTTPS...'));
80 daniel-mar 2069
                                        } else {
2070
                                                // No HTTPS detected. Do nothing, and next time, don't try to detect HTTPS again.
1059 daniel-mar 2071
                                                OIDplus::cookieUtils()->setcookie('SSL_CHECK', '0', 0, true/*allowJS*/, null/*samesite*/, true/*forceInsecure*/);
230 daniel-mar 2072
                                                self::$sslAvailableCache = false;
80 daniel-mar 2073
                                                return false;
2074
                                        }
49 daniel-mar 2075
                                }
1117 daniel-mar 2076
                        } else {
2077
                                assert(false);
2078
                                return false;
42 daniel-mar 2079
                        }
2080
                }
2081
        }
497 daniel-mar 2082
 
496 daniel-mar 2083
        /**
2084
         * Gets a local path pointing to a resource
1116 daniel-mar 2085
         * @param string|null $target Target resource (file or directory must exist), or null to get the OIDplus base directory
2086
         * @param bool $relative If true, the returning path is relative to the currently executed PHP file (not the CLI working directory)
590 daniel-mar 2087
         * @return string|false The local path, with guaranteed trailing path delimiter for directories
496 daniel-mar 2088
         */
1116 daniel-mar 2089
        public static function localpath(string $target=null, bool $relative=false) {
496 daniel-mar 2090
                if (is_null($target)) {
2091
                        $target = __DIR__.'/../../';
2092
                }
241 daniel-mar 2093
 
496 daniel-mar 2094
                if ($relative) {
2095
                        // First, try to find out how many levels we need to go up
2096
                        $steps_up = self::getExecutingScriptPathDepth();
2097
                        if ($steps_up === false) return false;
497 daniel-mar 2098
 
496 daniel-mar 2099
                        // Virtually go back from the executing PHP script to the OIDplus base path
2100
                        $res = str_repeat('../',$steps_up);
497 daniel-mar 2101
 
496 daniel-mar 2102
                        // Then go to the desired location
2103
                        $basedir = realpath(__DIR__.'/../../');
2104
                        $target = realpath($target);
500 daniel-mar 2105
                        if ($target === false) return false;
496 daniel-mar 2106
                        $res .= substr($target, strlen($basedir)+1);
2107
                        $res = rtrim($res,'/'); // avoid '..//' for localpath(null,true)
2108
                } else {
2109
                        $res = realpath($target);
241 daniel-mar 2110
                }
497 daniel-mar 2111
 
801 daniel-mar 2112
                if (is_dir($target)) $res .= '/';
497 daniel-mar 2113
 
1116 daniel-mar 2114
                return str_replace('/', DIRECTORY_SEPARATOR, $res);
241 daniel-mar 2115
        }
355 daniel-mar 2116
 
496 daniel-mar 2117
        /**
2118
         * Gets a URL pointing to a resource
1116 daniel-mar 2119
         * @param string|null $target Target resource (file or directory must exist), or null to get the OIDplus base directory
2120
         * @param int|bool $mode If true or OIDplus::PATH_RELATIVE, the returning path is relative to the currently executed
806 daniel-mar 2121
         *                          PHP script (i.e. index.php , not the plugin PHP script!). False or OIDplus::PATH_ABSOLUTE is
2122
         *                          results in an absolute URL. OIDplus::PATH_ABSOLUTE_CANONICAL is an absolute URL,
2123
         *                          but a canonical path (set by base config setting CANONICAL_SYSTEM_URL) is preferred.
590 daniel-mar 2124
         * @return string|false The URL, with guaranteed trailing path delimiter for directories
1116 daniel-mar 2125
         * @throws OIDplusException
496 daniel-mar 2126
         */
1116 daniel-mar 2127
        public static function webpath(string $target=null, $mode=self::PATH_ABSOLUTE_CANONICAL) {
801 daniel-mar 2128
                // backwards compatibility
2129
                if ($mode === true) $mode = self::PATH_RELATIVE;
2130
                if ($mode === false) $mode = self::PATH_ABSOLUTE;
2131
 
811 daniel-mar 2132
                if ($mode == OIDplus::PATH_RELATIVE_TO_ROOT) {
812 daniel-mar 2133
                        $tmp = OIDplus::webpath($target,OIDplus::PATH_ABSOLUTE);
2134
                        if ($tmp === false) return false;
2135
                        $tmp = parse_url($tmp);
2136
                        if ($tmp === false) return false;
2137
                        if (!isset($tmp['path'])) return false;
2138
                        return $tmp['path'];
811 daniel-mar 2139
                }
2140
 
812 daniel-mar 2141
                if ($mode == OIDplus::PATH_RELATIVE_TO_ROOT_CANONICAL) {
2142
                        $tmp = OIDplus::webpath($target,OIDplus::PATH_ABSOLUTE_CANONICAL);
2143
                        if ($tmp === false) return false;
2144
                        $tmp = parse_url($tmp);
2145
                        if ($tmp === false) return false;
2146
                        if (!isset($tmp['path'])) return false;
2147
                        return $tmp['path'];
2148
                }
2149
 
801 daniel-mar 2150
                $res = self::getSystemUrl($mode); // Note: already contains a trailing path delimiter
778 daniel-mar 2151
                if ($res === false) return false;
497 daniel-mar 2152
 
496 daniel-mar 2153
                if (!is_null($target)) {
2154
                        $basedir = realpath(__DIR__.'/../../');
2155
                        $target = realpath($target);
500 daniel-mar 2156
                        if ($target === false) return false;
496 daniel-mar 2157
                        $tmp = substr($target, strlen($basedir)+1);
1287 daniel-mar 2158
                        $res .= str_replace(DIRECTORY_SEPARATOR,'/',$tmp); // replace OS specific path delimiters introduced by realpath()
497 daniel-mar 2159
                        if (is_dir($target)) $res .= '/';
496 daniel-mar 2160
                }
497 daniel-mar 2161
 
496 daniel-mar 2162
                return $res;
2163
        }
497 daniel-mar 2164
 
1116 daniel-mar 2165
        /**
1287 daniel-mar 2166
         * Note: canonicalURL() is different than webpath(),
2167
         * because it does additional things like re-ordering of arguments
1247 daniel-mar 2168
         * @param string|null $goto
1116 daniel-mar 2169
         * @return false|string
2170
         * @throws OIDplusException
2171
         */
1247 daniel-mar 2172
        public static function canonicalURL(string $goto=null) {
778 daniel-mar 2173
                // First part: OIDplus system URL (or canonical system URL)
801 daniel-mar 2174
                $sysurl = OIDplus::getSystemUrl(self::PATH_ABSOLUTE_CANONICAL);
778 daniel-mar 2175
 
2176
                // Second part: Directory
2177
                $basedir = realpath(__DIR__.'/../../');
2178
                $target = realpath('.');
2179
                if ($target === false) return false;
2180
                $tmp = substr($target, strlen($basedir)+1);
1287 daniel-mar 2181
                $res = str_replace(DIRECTORY_SEPARATOR,'/',$tmp); // replace OS specific path delimiters introduced by realpath()
780 daniel-mar 2182
                if (is_dir($target) && ($res != '')) $res .= '/';
778 daniel-mar 2183
 
2184
                // Third part: File name
1287 daniel-mar 2185
                $tmp = $_SERVER['SCRIPT_NAME'];
1317 daniel-mar 2186
                $tmp = preg_replace('@index\\.php$@ismU', '', $tmp);
1287 daniel-mar 2187
                $tmp = explode('/',$tmp);
778 daniel-mar 2188
                $tmp = end($tmp);
2189
 
2190
                // Fourth part: Query string (ordered)
1247 daniel-mar 2191
                $url = [];
2192
                parse_str($_SERVER['QUERY_STRING'], $url);
2193
                if ($goto !== null) $url['goto'] = $goto;
2194
                ksort($url);
2195
                $tmp2 = http_build_query($url);
778 daniel-mar 2196
                if ($tmp2 != '') $tmp2 = '?'.$tmp2;
2197
 
2198
                return $sysurl.$res.$tmp.$tmp2;
2199
        }
2200
 
639 daniel-mar 2201
        private static $shutdown_functions = array();
1116 daniel-mar 2202
 
2203
        /**
1130 daniel-mar 2204
         * @param callable $func
1116 daniel-mar 2205
         * @return void
2206
         */
1130 daniel-mar 2207
        public static function register_shutdown_function(callable $func) {
639 daniel-mar 2208
                self::$shutdown_functions[] = $func;
2209
        }
2210
 
1116 daniel-mar 2211
        /**
2212
         * @return void
2213
         */
639 daniel-mar 2214
        public static function invoke_shutdown() {
2215
                foreach (self::$shutdown_functions as $func) {
2216
                        $func();
2217
                }
2218
        }
2219
 
1116 daniel-mar 2220
        /**
2221
         * @return string[]
2222
         * @throws OIDplusException
2223
         */
2224
        public static function getAvailableLangs(): array {
360 daniel-mar 2225
                $langs = array();
2226
                foreach (OIDplus::getAllPluginManifests('language') as $pluginManifest) {
389 daniel-mar 2227
                        $code = $pluginManifest->getLanguageCode();
360 daniel-mar 2228
                        $langs[] = $code;
2229
                }
2230
                return $langs;
2231
        }
2232
 
1116 daniel-mar 2233
        /**
2234
         * @return string
2235
         * @throws OIDplusConfigInitializationException
2236
         * @throws OIDplusException
2237
         */
2238
        public static function getDefaultLang(): string {
1041 daniel-mar 2239
                static $thrownOnce = false; // avoid endless loop inside OIDplusConfigInitializationException
2240
 
1049 daniel-mar 2241
                $lang = self::baseConfig()->getValue('DEFAULT_LANGUAGE', 'enus');
1041 daniel-mar 2242
 
2243
                if (!in_array($lang,self::getAvailableLangs())) {
2244
                        if (!$thrownOnce) {
2245
                                $thrownOnce = true;
1048 daniel-mar 2246
                                throw new OIDplusConfigInitializationException(_L('DEFAULT_LANGUAGE points to an invalid language plugin. (Consider setting to "enus" = "English USA".)'));
1041 daniel-mar 2247
                        } else {
2248
                                return 'enus';
2249
                        }
2250
                }
2251
 
2252
                return $lang;
2253
        }
2254
 
1116 daniel-mar 2255
        /**
2256
         * @return false|string
2257
         * @throws OIDplusConfigInitializationException
2258
         * @throws OIDplusException
2259
         */
355 daniel-mar 2260
        public static function getCurrentLang() {
1265 daniel-mar 2261
 
2262
                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
2263
                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)
2264
                        return self::getDefaultLang();
2265
                }
2266
 
360 daniel-mar 2267
                if (isset($_GET['lang'])) {
2268
                        $lang = $_GET['lang'];
2269
                } else if (isset($_POST['lang'])) {
2270
                        $lang = $_POST['lang'];
2271
                } else if (isset($_COOKIE['LANGUAGE'])) {
2272
                        $lang = $_COOKIE['LANGUAGE'];
2273
                } else {
1041 daniel-mar 2274
                        $lang = self::getDefaultLang();
360 daniel-mar 2275
                }
1140 daniel-mar 2276
                return substr(preg_replace('@[^a-z]@imU', '', $lang),0,4); // sanitize
355 daniel-mar 2277
        }
2278
 
1116 daniel-mar 2279
        /**
2280
         * @return void
2281
         * @throws OIDplusException
2282
         */
362 daniel-mar 2283
        public static function handleLangArgument() {
2284
                if (isset($_GET['lang'])) {
2285
                        // The "?lang=" argument is only for NoScript-Browsers/SearchEngines
2286
                        // In case someone who has JavaScript clicks a ?lang= link, they should get
2287
                        // the page in that language, but the cookie must be set, otherwise
2288
                        // the menu and other stuff would be in their cookie-based-language and not the
2289
                        // argument-based-language.
557 daniel-mar 2290
                        OIDplus::cookieUtils()->setcookie('LANGUAGE', $_GET['lang'], 0, true/*HttpOnly off, because JavaScript also needs translation*/);
362 daniel-mar 2291
                } else if (isset($_POST['lang'])) {
557 daniel-mar 2292
                        OIDplus::cookieUtils()->setcookie('LANGUAGE', $_POST['lang'], 0, true/*HttpOnly off, because JavaScript also needs translation*/);
362 daniel-mar 2293
                }
2294
        }
2295
 
468 daniel-mar 2296
        private static $translationArray = array();
1116 daniel-mar 2297
 
2298
        /**
1130 daniel-mar 2299
         * @param string $translation_file
2300
         * @return array
1116 daniel-mar 2301
         */
1130 daniel-mar 2302
        protected static function getTranslationFileContents(string $translation_file): array {
469 daniel-mar 2303
                // First, try the cache
481 daniel-mar 2304
                $cache_file = __DIR__ . '/../../userdata/cache/translation_'.md5($translation_file).'.ser';
469 daniel-mar 2305
                if (file_exists($cache_file) && (filemtime($cache_file) == filemtime($translation_file))) {
2306
                        $cac = @unserialize(file_get_contents($cache_file));
2307
                        if ($cac) return $cac;
2308
                }
481 daniel-mar 2309
 
469 daniel-mar 2310
                // If not successful, then load the XML file
2311
                $xml = @simplexml_load_string(file_get_contents($translation_file));
2312
                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
2313
                $cac = array();
2314
                foreach ($xml->message as $msg) {
2315
                        $src = trim($msg->source->__toString());
2316
                        $dst = trim($msg->target->__toString());
2317
                        $cac[$src] = $dst;
2318
                }
2319
                @file_put_contents($cache_file,serialize($cac));
2320
                @touch($cache_file,filemtime($translation_file));
2321
                return $cac;
2322
        }
1116 daniel-mar 2323
 
2324
        /**
2325
         * @param string $requested_lang
2326
         * @return array
2327
         * @throws OIDplusException
2328
         */
2329
        public static function getTranslationArray(string $requested_lang='*'): array {
362 daniel-mar 2330
                foreach (OIDplus::getAllPluginManifests('language') as $pluginManifest) {
389 daniel-mar 2331
                        $lang = $pluginManifest->getLanguageCode();
362 daniel-mar 2332
                        if (strpos($lang,'/') !== false) continue; // just to be sure
2333
                        if (strpos($lang,'\\') !== false) continue; // just to be sure
2334
                        if (strpos($lang,'..') !== false) continue; // just to be sure
401 daniel-mar 2335
 
468 daniel-mar 2336
                        if (($requested_lang != '*') && ($lang != $requested_lang)) continue;
401 daniel-mar 2337
 
468 daniel-mar 2338
                        if (!isset(self::$translationArray[$lang])) {
2339
                                self::$translationArray[$lang] = array();
2340
 
2341
                                $wildcard = $pluginManifest->getLanguageMessages();
2342
                                if (strpos($wildcard,'/') !== false) continue; // just to be sure
2343
                                if (strpos($wildcard,'\\') !== false) continue; // just to be sure
2344
                                if (strpos($wildcard,'..') !== false) continue; // just to be sure
2345
 
635 daniel-mar 2346
                                $translation_files = glob(__DIR__.'/../../plugins/'.'*'.'/language/'.$lang.'/'.$wildcard);
468 daniel-mar 2347
                                sort($translation_files);
2348
                                foreach ($translation_files as $translation_file) {
2349
                                        if (!file_exists($translation_file)) continue;
469 daniel-mar 2350
                                        $cac = self::getTranslationFileContents($translation_file);
2351
                                        foreach ($cac as $src => $dst) {
2352
                                                self::$translationArray[$lang][$src] = $dst;
468 daniel-mar 2353
                                        }
401 daniel-mar 2354
                                }
362 daniel-mar 2355
                        }
2356
                }
468 daniel-mar 2357
                return self::$translationArray;
362 daniel-mar 2358
        }
2359
 
1116 daniel-mar 2360
        /**
2361
         * @return mixed
2362
         */
699 daniel-mar 2363
        public static function getEditionInfo() {
2364
                return @parse_ini_file(__DIR__.'/../edition.ini', true)['Edition'];
2365
        }
2366
 
1116 daniel-mar 2367
        /**
1195 daniel-mar 2368
         * @return false|string The git path, with guaranteed trailing path delimiter for directories
1116 daniel-mar 2369
         */
698 daniel-mar 2370
        public static function findGitFolder() {
1195 daniel-mar 2371
                $dir = rtrim(OIDplus::localpath(), DIRECTORY_SEPARATOR);
2372
 
698 daniel-mar 2373
                // Git command line saves git information in folder ".git"
1195 daniel-mar 2374
                if (is_dir($res = $dir.'/.git')) {
2375
                        return str_replace('/', DIRECTORY_SEPARATOR, $res.'/');
2376
                }
2377
 
698 daniel-mar 2378
                // Plesk git saves git information in folder "../../../git/oidplus/" (or similar)
2379
                $i = 0;
2380
                do {
2381
                        if (is_dir($dir.'/git')) {
719 daniel-mar 2382
                                $confs = @glob($dir.'/git/'.'*'.'/config');
2383
                                if ($confs) foreach ($confs as $conf) {
698 daniel-mar 2384
                                        $cont = file_get_contents($conf);
699 daniel-mar 2385
                                        if (isset(OIDplus::getEditionInfo()['gitrepo']) && (OIDplus::getEditionInfo()['gitrepo'] != '') && (strpos($cont, OIDplus::getEditionInfo()['gitrepo']) !== false)) {
1195 daniel-mar 2386
                                                $res = dirname($conf);
2387
                                                return str_replace('/', DIRECTORY_SEPARATOR, $res.'/');
698 daniel-mar 2388
                                        }
2389
                                }
2390
                        }
2391
                        $i++;
719 daniel-mar 2392
                } while (($i<100) && ($dir != ($new_dir = @realpath($dir.'/../'))) && ($dir = $new_dir));
1195 daniel-mar 2393
 
698 daniel-mar 2394
                return false;
2395
        }
2396
 
1116 daniel-mar 2397
        /**
1195 daniel-mar 2398
         * @return false|string The SVN path, with guaranteed trailing path delimiter for directories
2399
         */
2400
        public static function findSvnFolder() {
2401
                $dir = rtrim(OIDplus::localpath(), DIRECTORY_SEPARATOR);
2402
 
1198 daniel-mar 2403
                if (is_dir($res = $dir.'/.svn')) {
1195 daniel-mar 2404
                        return str_replace('/', DIRECTORY_SEPARATOR, $res.'/');
2405
                }
2406
 
2407
                // in case we checked out the root instead of the "trunk"
1198 daniel-mar 2408
                if (is_dir($res = $dir.'/../.svn')) {
1195 daniel-mar 2409
                        return str_replace('/', DIRECTORY_SEPARATOR, $res.'/');
2410
                }
2411
 
2412
                return false;
2413
        }
2414
 
2415
        /**
1116 daniel-mar 2416
         * @return false|string
2417
         */
2418
        public static function getGitsvnRevision() {
698 daniel-mar 2419
                try {
2420
                        $git_dir = OIDplus::findGitFolder();
2421
                        if ($git_dir === false) return false;
1191 daniel-mar 2422
 
2423
                        // git_get_latest_commit_message() tries command line and binary parsing
2424
                        // requires vendor/danielmarschall/php_utils/git_utils.inc.php
698 daniel-mar 2425
                        $commit_msg = git_get_latest_commit_message($git_dir);
1050 daniel-mar 2426
                } catch (\Exception $e) {
698 daniel-mar 2427
                        return false;
2428
                }
2429
 
2430
                $m = array();
2431
                if (preg_match('%git-svn-id: (.+)@(\\d+) %ismU', $commit_msg, $m)) {
2432
                        return $m[2];
2433
                } else {
2434
                        return false;
2435
                }
2436
        }
2437
 
1116 daniel-mar 2438
        /**
2439
         * @param string $static_node_id
2440
         * @param bool $throw_exception
2441
         * @return string
2442
         */
2443
        public static function prefilterQuery(string $static_node_id, bool $throw_exception): string {
1246 daniel-mar 2444
                $static_node_id = trim($static_node_id);
2445
 
775 daniel-mar 2446
                // Let namespace be case-insensitive
1261 daniel-mar 2447
                // Note: The query might not contain a namespace. It might be a single OID
2448
                //       or MAC address, and the plugins need to detect it any add a namespace
2449
                //       in the prefiltering. But if we have a namespace, we should fix the
2450
                //       case, so that plugins don't have a problem if they check the namespace
2451
                //       using str_starts_with().
2452
                if (substr_count($static_node_id, ':') === 1) {
2453
                        $ary = explode(':', $static_node_id, 2);
2454
                        $ary[0] = strtolower($ary[0]);
2455
                        $static_node_id = implode(':', $ary);
2456
                }
775 daniel-mar 2457
 
889 daniel-mar 2458
                // Ask plugins if they want to change the node id
2459
                foreach (OIDplus::getObjectTypePluginsEnabled() as $plugin) {
2460
                        $static_node_id = $plugin->prefilterQuery($static_node_id, $throw_exception);
775 daniel-mar 2461
                }
2462
 
1261 daniel-mar 2463
                // Let namespace be case-insensitive
2464
                // At this point, plugins should have already added the namespace during the prefiltering,
2465
                // so, now we make sure that the namespace is really lowercase
2466
                if (substr_count($static_node_id, ':') === 1) {
2467
                        $ary = explode(':', $static_node_id, 2);
2468
                        $ary[0] = strtolower($ary[0]);
2469
                        $static_node_id = implode(':', $ary);
2470
                }
2471
 
775 daniel-mar 2472
                return $static_node_id;
2473
        }
849 daniel-mar 2474
 
1116 daniel-mar 2475
        /**
2476
         * @return bool
2477
         */
2478
        public static function isCronjob(): bool {
849 daniel-mar 2479
                return explode('.',basename($_SERVER['SCRIPT_NAME']))[0] === 'cron';
2480
        }
856 daniel-mar 2481
 
1116 daniel-mar 2482
        /**
2483
         * Since OIDplus svn-184, entries in the database need to have a canonical ID
2484
         * If the ID is not canonical (e.g. GUIDs missing hyphens), the object cannot be opened in OIDplus
2485
         * This script re-canonizes the object IDs if required.
2486
         * In SVN Rev 856, the canonization for GUID, IPv4 and IPv6 have changed, requiring another
2487
         * re-canonization
2488
         * @return void
2489
         * @throws OIDplusException
2490
         */
857 daniel-mar 2491
        private static function recanonizeObjects() {
856 daniel-mar 2492
                $res = OIDplus::db()->query("select id from ###objects");
2493
                while ($row = $res->fetch_array()) {
2494
                        $ida = $row['id'];
857 daniel-mar 2495
                        $obj = OIDplusObject::parse($ida);
2496
                        if (!$obj) continue;
2497
                        $idb = $obj->nodeId();
856 daniel-mar 2498
                        if (($idb) && ($ida != $idb)) {
1231 daniel-mar 2499
                                if (OIDplus::db()->transaction_supported()) OIDplus::db()->transaction_begin();
2500
                                try {
2501
                                        OIDplus::db()->query("update ###objects set id = ? where id = ?", array($idb, $ida));
2502
                                        OIDplus::db()->query("update ###asn1id set oid = ? where oid = ?", array($idb, $ida));
2503
                                        OIDplus::db()->query("update ###iri set oid = ? where oid = ?", array($idb, $ida));
2504
                                        OIDplus::db()->query("update ###log_object set object = ? where object = ?", array($idb, $ida));
1267 daniel-mar 2505
                                        OIDplus::logger()->log("V2:[INFO]A", "Object name '%1' has been changed to '%2' during re-canonization", $ida, $idb);
1231 daniel-mar 2506
                                        if (OIDplus::db()->transaction_supported()) OIDplus::db()->transaction_commit();
2507
                                } catch (\Exception $e) {
2508
                                        if (OIDplus::db()->transaction_supported()) OIDplus::db()->transaction_rollback();
2509
                                        throw $e;
2510
                                }
978 daniel-mar 2511
                                OIDplusObject::resetObjectInformationCache();
856 daniel-mar 2512
                        }
2513
                }
2514
        }
2515
 
1345 daniel-mar 2516
        /**
2517
         * Tries to determine the IP address of the website visitor
2518
         * @return string|false
2519
         */
2520
        public static function getClientIpAddress() {
2521
                $direct_connection = $_SERVER['REMOTE_ADDR'] ?? false;
2522
                if ($direct_connection === false) return false;
2523
 
2524
                $trusted_proxies = OIDplus::baseConfig()->getValue('XFF_TRUSTED_PROXIES', []);
2525
                if (in_array($direct_connection, $trusted_proxies)) {
2526
                        return $_SERVER['HTTP_X_FORWARDED_FOR'] ?: $direct_connection;
2527
                } else {
2528
                        return $direct_connection;
2529
                }
2530
        }
2531
 
374 daniel-mar 2532
}