]> jfr.im git - irc/unrealircd/unrealircd-webpanel.git/blob - inc/common.php
Add simple way to deal with IRC server configuration required.
[irc/unrealircd/unrealircd-webpanel.git] / inc / common.php
1 <?php
2 if (version_compare(PHP_VERSION, '8.0.0', '<'))
3 die("This webserver is using PHP version ".PHP_VERSION." but we require at least PHP 8.0.0.<br>".
4 "If you already installed PHP8 but are still seeing this error, then it means ".
5 "apache/nginx/.. is loading an older PHP version. Eg. on Debian/Ubuntu you need ".
6 "<code>apt-get install libapache2-mod-php8.2</code> (or a similar version) and ".
7 "<code>apt-get remove libapache2-mod-php7.4</code> (or a similar version). ".
8 "You may also need to choose again the PHP module to load in apache via <code>a2enmod php8.2</code>");
9
10 define('UPATH', dirname(__DIR__));
11
12 function get_config($setting)
13 {
14 GLOBAL $config;
15
16 $item = $config;
17 foreach(explode("::", $setting) as $x)
18 {
19 if (isset($item[$x]))
20 $item = $item[$x];
21 else
22 return NULL;
23 }
24 return $item;
25 }
26
27 function get_current_page_helper($name, $p)
28 {
29 if (isset($p["script"]))
30 {
31 if (($p["script"] != '') && str_ends_with($_SERVER['SCRIPT_FILENAME'],$p["script"]))
32 return $p;
33 return null;
34 }
35 foreach ($p as $k=>$v)
36 {
37 $ret = get_current_page_helper($k, $v);
38 if ($ret !== null)
39 return $ret;
40 }
41 return null;
42 }
43
44 function get_current_page()
45 {
46 GLOBAL $pages;
47 foreach ($pages as $k=>$v)
48 {
49 $ret = get_current_page_helper($k, $v);
50 if ($ret !== null)
51 return $ret;
52 }
53 }
54
55 function page_requires_no_config()
56 {
57 if (str_ends_with($_SERVER['SCRIPT_FILENAME'],"install.php") ||
58 str_ends_with($_SERVER['SCRIPT_FILENAME'],"installation.php"))
59 {
60 return TRUE;
61 }
62 return FALSE;
63 }
64
65 function page_requires_no_login()
66 {
67 if (str_ends_with($_SERVER['SCRIPT_FILENAME'],"login/index.php") ||
68 page_requires_no_config())
69 {
70 return TRUE;
71 }
72 return FALSE;
73 }
74
75 function read_config_file()
76 {
77 GLOBAL $config;
78 GLOBAL $config_transition_unreal_server;
79
80 $config = Array();
81 if (!file_exists(UPATH."/config/config.php") && file_exists(UPATH."/config.php"))
82 {
83 require_once UPATH . "/config.php";
84 require_once UPATH . "/config/compat.php";
85 }
86 if ((@include(UPATH . "/config/config.php")) !== 1)
87 return false;
88 if (isset($config['unrealircd']))
89 $config_transition_unreal_server = true;
90 /* Upgrade needed? */
91 $plugins_modified = false;
92 foreach ($config["plugins"] as $k=>$v)
93 {
94 if ($v == "sql_auth")
95 {
96 $config["plugins"][$k] = "sql_db";
97 $plugins_modified = true;
98 } else
99 if ($v == "file_auth")
100 {
101 $config["plugins"][$k] = "file_db";
102 $plugins_modified = true;
103 }
104 }
105 if ($plugins_modified)
106 write_config_file();
107
108 return true;
109 }
110
111 function read_config_db()
112 {
113 GLOBAL $config;
114
115 if (page_requires_no_config())
116 return;
117
118 $merge = DbSettings::get();
119 /* DB settings overwrite config.php keys: */
120 $config = array_merge($config, $merge);
121 }
122
123 function config_is_file_item($name)
124 {
125 if (($name == "plugins") ||
126 ($name == "mysql") ||
127 ($name == "base_url") ||
128 ($name == "secrets"))
129 {
130 return true;
131 }
132 return false;
133 }
134
135 function write_config_file()
136 {
137 GLOBAL $config;
138
139 $file_settings = [];
140 foreach($config as $k=>$v)
141 {
142 if (config_is_file_item($k))
143 $file_settings[$k] = $v;
144 }
145
146 $cfg_filename = UPATH.'/config/config.php';
147 $tmpfile = UPATH.'/config/config.tmp.'.bin2hex(random_bytes(8)).'.php';
148 $fd = fopen($tmpfile, "w");
149 if (!$fd)
150 die("Could not write to temporary config file $tmpfile.<br>We need write permissions on the config/ directory!<br>");
151
152 $str = var_export($file_settings, true);
153 if ($str === null)
154 die("Error while running write_config_file() -- weird!");
155 if (!fwrite($fd, "<?php\n".
156 "/* This config file is written automatically by the UnrealIRCd webpanel.\n".
157 " * You are not really supposed to edit it manually.\n".
158 " */\n".
159 '$config = '.$str.";\n"))
160 {
161 die("Error writing to config file $tmpfile (on fwrite).<br>");
162 }
163 if (!fclose($fd))
164 die("Error writing to config file $tmpfile (on close).<br>");
165 /* Now atomically rename the file */
166 if (!rename($tmpfile, $cfg_filename))
167 die("Could not write (rename) to file ".$cfg_filename."<br>");
168 if (function_exists('opcache_invalidate'))
169 opcache_invalidate($cfg_filename);
170
171 /* Do not re-read config, as it would reinitialize config
172 * without having the DB settings read. (And it also
173 * serves no purpose)
174 */
175 return true;
176 }
177
178 // XXX: handle unsetting of config items :D - explicit unset function ?
179
180 function write_config($setting = null)
181 {
182 GLOBAL $config;
183
184 /* Specific request? Easy, write only this setting to the DB (never used for file) */
185 if ($setting !== null)
186 {
187 return DbSettings::set($setting, $config[$setting]);
188 }
189
190 /* Otherwise write the whole config.
191 * TODO: avoid writing settings file if unneeded,
192 * as it is more noisy than db settings.
193 */
194 $db_settings = [];
195
196 foreach($config as $k=>$v)
197 {
198 if (!config_is_file_item($k))
199 $db_settings[$k] = $v;
200 }
201
202 if (!write_config_file())
203 return false;
204
205 foreach($db_settings as $k=>$v)
206 {
207 $ret = DbSettings::set($k, $v);
208 if (!$ret)
209 return $ret;
210 }
211
212 return true;
213 }
214
215 function get_version()
216 {
217 $fd = @fopen(UPATH."/.git/FETCH_HEAD", "r");
218 if ($fd === false)
219 return "unknown";
220 $line = fgets($fd, 512);
221 fclose($fd);
222 $commit = substr($line, 0, 8);
223 return $commit; /* short git commit id */
224 }
225
226 function generate_secrets()
227 {
228 GLOBAL $config;
229
230 if (!isset($config['secrets']))
231 $config['secrets'] = Array();
232
233 if (!isset($config['secrets']['pepper']))
234 $config['secrets']['pepper'] = rtrim(base64_encode(random_bytes(16)),'=');
235
236 if (!isset($config['secrets']['key']))
237 $config['secrets']['key'] = rtrim(base64_encode(sodium_crypto_aead_xchacha20poly1305_ietf_keygen()),'=');
238 }
239
240 function get_active_rpc_server()
241 {
242 $servers = get_config("unrealircd");
243 if (empty($servers))
244 return;
245 // TODO: make user able to override this - either in user or in session
246
247 foreach ($servers as $displayname=>$e)
248 {
249 if (isset($e["default"]) && $e["default"])
250 return $displayname;
251 }
252 return null;
253 }
254
255 /* Set a new default RPC server */
256 function set_default_rpc_server($name)
257 {
258 GLOBAL $config;
259
260 /* Mark all other servers as non-default */
261 foreach ($config["unrealircd"] as $n=>$e)
262 if ($n != $name)
263 $config["unrealircd"][$n]["default"] = false;
264 $config["unrealircd"][$name]["default"] = true;
265 }
266
267 /* Ensure at least 1 server is default */
268 function set_at_least_one_default_rpc_server()
269 {
270 GLOBAL $config;
271
272 $has_default_rpc_server = false;
273 foreach ($config["unrealircd"] as $name=>$e)
274 if ($e["default"])
275 $has_default_rpc_server = true;
276 if (!$has_default_rpc_server)
277 {
278 /* Make first server in the list the default */
279 foreach ($config["unrealircd"] as $name=>$e)
280 {
281 $config["unrealircd"][$name]["default"] = true;
282 break;
283 }
284 }
285 }
286
287 function secret_encrypt(string $text)
288 {
289 GLOBAL $config;
290
291 $key = base64_decode($config['secrets']['key']);
292 $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
293 $encrypted_text = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($text, '', $nonce, $key);
294 return "secret:".rtrim(base64_encode($nonce),'=').':'.rtrim(base64_encode($encrypted_text),'='); // secret:base64(NONCE):base64(ENCRYPTEDTEXT)
295 }
296
297 function secret_decrypt(string $crypted)
298 {
299 GLOBAL $config;
300
301 $key = base64_decode($config['secrets']['key']);
302 $d = explode(":", $crypted);
303 if (count($d) != 3)
304 return null;
305 $nonce = base64_decode($d[1]);
306 $ciphertext = base64_decode($d[2]);
307
308 $ret = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($ciphertext, '', $nonce, $key);
309 if ($ret === false)
310 return null;
311 return $ret;
312 }
313
314 function upgrade_check()
315 {
316 GLOBAL $config_transition_unreal_server;
317 GLOBAL $config;
318
319 /* Moving of a config.php item to DB: */
320 if ($config_transition_unreal_server)
321 write_config();
322
323 /* Our own stuff may need upgrading.. */
324 /* - generating secrets */
325 if (!isset($config['secrets']))
326 {
327 generate_secrets();
328 write_config_file();
329 }
330 /* - encrypting rpc_password */
331 if (isset($config['unrealircd']) &&
332 isset($config['unrealircd']['rpc_password']) &&
333 !str_starts_with($config['unrealircd']['rpc_password'], "secret:"))
334 {
335 $ret = secret_encrypt($config['unrealircd']['rpc_password']);
336 if ($ret !== false)
337 {
338 $config['unrealircd']['rpc_password'] = $ret;
339 write_config('unrealircd');
340 }
341 }
342 /* $config["unrealircd"] should be an array now.. */
343 if (isset($config['unrealircd']) && isset($config['unrealircd']['rpc_password']))
344 {
345 $config["unrealircd"]["default"] = true;
346 $config['unrealircd'] = [
347 "Primary" => $config['unrealircd']];
348 write_config("unrealircd");
349 }
350
351 $version = get_version();
352 if (!isset($config['webpanel_version']))
353 $config['webpanel_version'] = '';
354 if ($version != $config['webpanel_version'])
355 {
356 $versioninfo = [
357 "old_version" => $config['webpanel_version'],
358 "new_version" => $version
359 ];
360 /* And inform the hook (eg the database backends) */
361 Hook::run(HOOKTYPE_UPGRADE, $versioninfo);
362 /* And set the new version now that the upgrade is "done" */
363 $config['webpanel_version'] = $version;
364 write_config("webpanel_version");
365 }
366 }
367
368 function panel_start_session($user = false)
369 {
370 if (!isset($_SESSION))
371 {
372 session_set_cookie_params(86400); // can't set this to session_timeout due to catch-22
373 session_start();
374 }
375
376 if ($user === false)
377 {
378 $user = unreal_get_current_user();
379 if ($user === false)
380 return false;
381 }
382
383 $timeout = 3600;
384 if (isset($user->user_meta['session_timeout']))
385 $timeout = (INT)$user->user_meta['session_timeout'];
386
387 if (!isset($_SESSION['session_timeout']))
388 $_SESSION['session_timeout'] = $timeout;
389
390 $_SESSION['last-activity'] = time();
391 return true;
392 }
393
394 /* Now read the config, and redirect to install screen if we don't have it */
395 $config_transition_unreal_server = false;
396 if (!read_config_file())
397 {
398 if (page_requires_no_config())
399 {
400 /* Allow empty conf */
401 } else
402 if (!file_exists(UPATH."/config/config.php") && !file_exists(UPATH."/config.php"))
403 {
404 header("Location: settings/install.php");
405 die();
406 }
407 }
408
409 require_once UPATH . "/Classes/class-hook.php";
410 if (!is_dir(UPATH . "/vendor"))
411 die("The vendor/ directory is missing. Most likely the admin forgot to run 'composer install'\n");
412 require_once UPATH . '/vendor/autoload.php';
413 require_once UPATH . "/Classes/class-cmodes.php";
414 require_once UPATH . "/inc/defines.php";
415 require_once UPATH . "/misc/strings.php";
416 require_once UPATH . "/misc/channel-lookup-misc.php";
417 require_once UPATH . "/misc/user-lookup-misc.php";
418 require_once UPATH . "/misc/server-lookup-misc.php";
419 require_once UPATH . "/misc/ip-whois-misc.php";
420 require_once UPATH . "/Classes/class-log.php";
421 require_once UPATH . "/Classes/class-message.php";
422 require_once UPATH . "/Classes/class-rpc.php";
423 require_once UPATH . "/Classes/class-paneluser.php";
424 require_once UPATH . "/plugins.php";
425
426 /* Do various checks and reading, except during setup step 1. */
427 if (!page_requires_no_config())
428 {
429 /* Now that plugins are loaded, read config from DB */
430 read_config_db();
431
432 /* Check if anything needs upgrading (eg on panel version change) */
433 upgrade_check();
434
435 /* And a check... */
436 if (!get_config("base_url"))
437 die("The base_url was not found in your config. Setup went wrong?");
438 }
439
440 $pages = [
441 "Overview" => ["script"=>""],
442 "Users" => ["script"=>"users/index.php"],
443 "Channels" => ["script"=>"channels/index.php"],
444 "Servers" => ["script"=>"servers/index.php"],
445 "Server Bans" => [
446 "Server Bans" => ["script" => "server-bans/index.php"],
447 "Name Bans" => ["script" => "server-bans/name-bans.php"],
448 "Ban Exceptions" => ["script" => "server-bans/ban-exceptions.php"],
449 ],
450 "Spamfilter" => ["script" => "spamfilter.php"],
451 "Tools" => [
452 "IP WHOIS" => ["script" => "tools/ip-whois.php","no_irc_server_required"=>true],
453 ],
454 "Settings" => [
455 "Plugins" => ["script" => "settings/plugins.php","no_irc_server_required"=>true],
456 "RPC Servers" => ["script" => "settings/rpc-servers.php","no_irc_server_required"=>true],
457 ],
458
459 "News" => ["script" => "news.php","no_irc_server_required"=>true],
460 ];
461
462 if (!panel_start_session())
463 {
464 if (!page_requires_no_login())
465 {
466 if (!is_auth_provided())
467 die("No authentication plugin loaded. You must load either sql_db, file_db, or a similar auth plugin!");
468 $current_page = $_SERVER['REQUEST_URI'];
469 header("Location: ".get_config("base_url")."login/?redirect=".urlencode($current_page));
470 die;
471 }
472 } else {
473 $pages["Settings"]["Accounts"] = [
474 "script" => "settings/index.php",
475 "no_irc_server_required"=>true
476 ];
477 if (current_user_can(PERMISSION_MANAGE_USERS))
478 {
479 $pages["Settings"]["Role Editor"] = [
480 "script"=>"settings/user-role-edit.php",
481 "no_irc_server_required"=>true
482 ];
483 }
484 $user = unreal_get_current_user();
485 if ($user)
486 {
487 /* Add logout page, if logged in */
488 $pages["Logout"] = [
489 "script"=>"login/?logout=true",
490 "no_irc_server_required"=>true
491 ];
492 }
493 }
494
495 Hook::run(HOOKTYPE_NAVBAR, $pages);
496
497 /* Example to add new menu item:
498 *
499 * class MyPlugin
500 * {
501 *
502 * function __construct()
503 * {
504 * Hook::func(HOOKTYPE_NAVBAR, [$this, 'add_menu'])
505 * }
506 *
507 * function add_menu(&$pages) // this should pass by reference (using the & prefix)
508 * {
509 * $page_name = "My New Page";
510 * $page_link = "link/to/page.php";
511 * $pages[$page_name] = $page_link;
512 * }
513 * }
514 */
515
516 $current_page = get_current_page();