Subversion Repositories oidplus

Rev

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

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