Repository navigation
Expand file tree
/
Copy pathphpbb.php
More file actions
executable file
·472 lines (392 loc) · 16.5 KB
/
Copy pathphpbb.php
File metadata and controls
executable file
·472 lines (392 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
<?php
/**
*
* @package phpbb_integration
* @copyright (c) 2007 Kleeja.net
* @license ./docs/license.txt
*
*/
//no for directly open
if (!defined('IN_COMMON')) {
exit();
}
if (!function_exists('phpbb_integration_active')) {
// the version of kjauth.php that this plugin talks to, the file in the forum folder must have the same one
define('PHPBB_BRIDGE_VERSION', 2);
// seconds to wait for kjauth.php, the panel asks it on every page of an administrator
define('PHPBB_BRIDGE_CONNECT_TIMEOUT', 5);
define('PHPBB_BRIDGE_TIMEOUT', 10);
// seconds that the test waits for a forum that caches PHP files to run the kjauth.php that Kleeja just wrote,
// OPcache checks the files every 2 seconds by default
define('PHPBB_BRIDGE_CACHE_WAIT', 3);
/**
* the members sign in with their forum accounts: the integration is turned on in its page,
* and define('DISABLE_INTR', true) isn't in config.php, which gives back the Kleeja accounts when the forum is down
* @return bool
*/
function phpbb_integration_active(): bool
{
global $config;
return !defined('DISABLE_INTR') && (int) ($config['phpbb_intr_enabled'] ?? 0) === 1;
}
/**
* the settings of the plugin and their values when it is installed, the key is made by the install
* @return array
*/
function phpbb_default_settings(): array
{
return [
'phpbb_intr_enabled' => '0',
'phpbb_intr_path' => '../phpbb',
'phpbb_intr_link' => 'https://example.com/phpbb',
'phpbb_intr_api_key' => '',
// why the integration was turned off by an update, shown until it is turned on again
'phpbb_intr_notice' => '',
];
}
/**
* the saved settings as they were typed. Kleeja keeps them encoded for HTML,
* and the versions before 2.0 encoded them twice
* @return array
*/
function phpbb_settings(): array
{
global $config;
$settings = [];
foreach (phpbb_default_settings() as $name => $default) {
$settings[$name] = isset($config[$name]) ? kleeja_html_decode((string) $config[$name]) : $default;
}
return $settings;
}
/**
* a new key for kjauth.php, 64 hexadecimal letters
* @return string
*/
function phpbb_new_api_key(): string
{
return bin2hex(random_bytes(32));
}
/**
* the forum address of the settings, without its last slash, or an empty text when it isn't a web address
* @param array $settings
* @return string
*/
function phpbb_forum_address(array $settings): string
{
$link = rtrim(trim($settings['phpbb_intr_link']), '/');
return preg_match('#^https?://[^/\s?\#]+[^\s?\#]*$#iD', $link) ? $link : '';
}
/**
* the address of the settings is the one of the example, members would be sent there
* @param array $settings
* @return bool
*/
function phpbb_forum_address_is_example(array $settings): bool
{
$host = strtolower((string) parse_url(phpbb_forum_address($settings), PHP_URL_HOST));
return $host === 'example.com' || str_ends_with($host, '.example.com');
}
/**
* a page of the forum, for the links of the pages that Kleeja leaves to the forum
* @param string $page lostpw, register or profile
* @return string encoded for HTML, empty when the forum address isn't a web address
*/
function phpbb_forum_link(string $page): string
{
$forum = phpbb_forum_address(phpbb_settings());
if ($forum === '') {
return '';
}
$pages = [
'lostpw' => '/ucp.php?mode=sendpassword',
'register' => '/ucp.php?mode=register',
'profile' => '/ucp.php?i=ucp_profile&mode=reg_details',
];
return htmlspecialchars($forum . $pages[$page], ENT_QUOTES);
}
/**
* kjauth.php with the key of these settings, as it goes in the forum folder
* @param array $settings
* @return string
*/
function phpbb_bridge_code(array $settings): string
{
return str_replace(
"'{___API_KEY___}'",
var_export((string) $settings['phpbb_intr_api_key'], true),
(string) file_get_contents(__DIR__ . '/kjauth.php'),
);
}
/**
* the forum folder of the settings, from the folder of Kleeja
* @param array $settings
* @return string an empty text when no folder is set
*/
function phpbb_forum_folder(array $settings): string
{
$folder = trim(str_replace('\\', '/', trim($settings['phpbb_intr_path'])), '/');
if ($folder === '') {
return '';
}
$root = rtrim(str_replace('\\', '/', realpath(PATH) ?: dirname(__DIR__, 2)), '/');
// without the ../ of the folder, the test shows where it is, even when it isn't there
return realpath($root . '/' . $folder) ?: $root . '/' . $folder;
}
/**
* write kjauth.php in the forum folder, when the forum is on this server
* @param array $settings
* @return array 'status' is written, current, no_folder, not_phpbb or not_writable, 'file' is kjauth.php in the folder
*/
function phpbb_write_bridge(array $settings): array
{
$folder = phpbb_forum_folder($settings);
if ($folder === '') {
return ['status' => 'no_folder', 'file' => ''];
}
$file = $folder . '/kjauth.php';
if (!is_file($folder . '/common.php') || !is_file($folder . '/phpbb/user.php')) {
return ['status' => 'not_phpbb', 'file' => $folder];
}
$code = phpbb_bridge_code($settings);
if (is_file($file) && @file_get_contents($file) === $code) {
return ['status' => 'current', 'file' => $file];
}
if (@file_put_contents($file, $code, LOCK_EX) !== strlen($code)) {
return ['status' => 'not_writable', 'file' => $file];
}
// a server that caches PHP files without checking them would keep running the old one
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($file, true);
}
return ['status' => 'written', 'file' => $file];
}
/**
* ask kjauth.php of the forum, in a POST request with the key, over a verified connection for https addresses
* @param array $settings
* @param string $action test, login, user or name
* @param array $fields what the action needs
* @return array 'answer' is what kjauth.php answered, null when it isn't an answer of kjauth.php,
* 'status' the HTTP status, 'error' url, no_client or transport, 'detail' the error of the transport,
* 'redirect' where a redirect goes, 'body' the text of the answer
*/
function phpbb_bridge_request(array $settings, string $action, array $fields = []): array
{
$reply = ['answer' => null, 'status' => 0, 'error' => '', 'detail' => '', 'redirect' => '', 'body' => ''];
$forum = phpbb_forum_address($settings);
if ($forum === '') {
$reply['error'] = 'url';
return $reply;
}
$url = $forum . '/kjauth.php';
$body = http_build_query(['key' => $settings['phpbb_intr_api_key'], 'action' => $action] + $fields, '', '&');
$headers = ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'];
$agent = 'Kleeja/' . (defined('KLEEJA_VERSION') ? KLEEJA_VERSION : '4') . ' phpbb_integration';
if (function_exists('curl_init')) {
$curl = curl_init($url);
// a redirect isn't followed, it would send the password to another address, or lose it with a GET
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_USERAGENT => $agent,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => PHPBB_BRIDGE_CONNECT_TIMEOUT,
CURLOPT_TIMEOUT => PHPBB_BRIDGE_TIMEOUT,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$response = curl_exec($curl);
if ($response === false) {
$reply['error'] = 'transport';
$reply['detail'] = curl_error($curl);
return $reply;
}
$reply['status'] = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$reply['redirect'] = (string) curl_getinfo($curl, CURLINFO_REDIRECT_URL);
} elseif (filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOLEAN)) {
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => implode("\r\n", $headers) . "\r\n",
'user_agent' => $agent,
'content' => $body,
'timeout' => PHPBB_BRIDGE_TIMEOUT,
'follow_location' => 0,
// the answers of kjauth.php to a wrong key come with an error status
'ignore_errors' => true,
],
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
]);
// the warning says why it failed, the error handler of Kleeja would take it, and log it
$warning = '';
set_error_handler(function (int $number, string $message) use (&$warning): bool {
$warning = preg_replace('/^file_get_contents\([^)]*\):\s*/', '', $message);
return true;
});
$response = file_get_contents($url, false, $context);
restore_error_handler();
$lines = function_exists('http_get_last_response_headers')
? (array) http_get_last_response_headers()
: $http_response_header ?? [];
if ($response === false || !$lines) {
$reply['error'] = 'transport';
$reply['detail'] = $warning;
return $reply;
}
foreach ($lines as $line) {
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $line, $match)) {
// the last status, after the 100 Continue ones
$reply['status'] = (int) $match[1];
} elseif (preg_match('/^Location:\s*(\S+)/i', $line, $match)) {
$reply['redirect'] = $match[1];
}
}
} else {
$reply['error'] = 'no_client';
return $reply;
}
$reply['body'] = (string) $response;
$answer = json_decode($reply['body'], true);
$reply['answer'] = is_array($answer) && isset($answer['kjauth']) ? $answer : null;
return $reply;
}
/**
* what kjauth.php answered to the sign in, the account of the member, or null when it isn't one
* @param array $settings
* @param string $action login or user
* @param array $fields
* @return array|null
*/
function phpbb_bridge_member(array $settings, string $action, array $fields): ?array
{
// when the forum can't be reached, the next questions of this request don't wait for it again
static $unreachable = false;
if ($unreachable) {
return null;
}
$reply = phpbb_bridge_request($settings, $action, $fields);
$answer = $reply['answer'];
if ($answer === null) {
$unreachable = $reply['error'] !== '' || $reply['status'] !== 200;
kleeja_log(
'[phpbb_integration] kjauth.php gave no answer: ' .
($reply['error'] ?: 'HTTP ' . $reply['status']) .
' ' .
$reply['detail'],
);
return null;
}
if ((int) $answer['kjauth'] !== PHPBB_BRIDGE_VERSION) {
kleeja_log('[phpbb_integration] kjauth.php in the forum folder is version ' . (int) $answer['kjauth']);
return null;
}
$member = $answer['user'] ?? null;
if (
($answer['status'] ?? '') !== 'ok' ||
!is_array($member) ||
(int) ($member['id'] ?? 0) < 1 ||
!is_string($member['name'] ?? null) ||
!is_string($member['token'] ?? null) ||
!preg_match('/^[a-f0-9]{64}$/D', $member['token'])
) {
return null;
}
return $member;
}
/**
* sign in with a forum account, in place of the accounts of Kleeja, see usrcp::data()
* @param string $name the name as p() gives it, or the user id of the login cookie when $hashed
* @param string $pass the password as p() gives it, or the token of the login cookie when $hashed
* @param int $expire when the login cookie expires
* @param bool $hashed checking the login cookie of an administrator, Kleeja does it on every visit
* @param bool $loginadm the sign in of the control panel, it checks the password of the signed-in member
* @return bool
*/
function phpbb_auth_login(
string $name,
#[\SensitiveParameter] string $pass,
int $expire,
bool $hashed = false,
bool $loginadm = false,
): bool {
global $usrcp, $userinfo;
$settings = phpbb_settings();
// p() encoded what was typed, phpBB encodes it in its own way
$member = $hashed
? phpbb_bridge_member($settings, 'user', ['user_id' => (int) $name, 'token' => $pass])
: phpbb_bridge_member($settings, 'login', [
'username' => htmlspecialchars_decode($name, ENT_QUOTES),
'password' => htmlspecialchars_decode($pass, ENT_QUOTES),
]);
if ($member === null) {
return false;
}
$is_founder = ($member['founder'] ?? false) === true;
$user = [
'id' => (int) $member['id'],
'name' => kleeja_html_display($member['name']),
'mail' => kleeja_html_display((string) ($member['mail'] ?? '')),
// the groups of Kleeja: administrators, or members
'group_id' => $is_founder ? 1 : 3,
// the founders manage the plugins, the styles and the updates of Kleeja
'founder' => $is_founder ? 1 : 0,
// kjauth.php gives a token made from the password of the forum, never the password itself
'password' => $member['token'],
'last_visit' => time(),
];
// the control panel asks again for the password of the member who is signed in, not of another one
if ($loginadm && (int) $usrcp->id() !== $user['id']) {
return false;
}
$userinfo = $user;
if ($loginadm) {
return true;
}
if ($hashed) {
// a visit of a signed-in administrator, Kleeja reads who it is from these
$constants = [
'id' => 'USER_ID',
'group_id' => 'GROUP_ID',
'name' => 'USER_NAME',
'mail' => 'USER_MAIL',
'last_visit' => 'LAST_VISIT',
];
foreach ($constants as $key => $constant) {
if (!defined($constant)) {
define($constant, $user[$key]);
}
}
} else {
$usrcp->set_login_cookie($user, $expire);
}
return true;
}
/**
* the name of a member of the forum, for the files of a member
* @param int $user_id
* @return string|false encoded for HTML
*/
function phpbb_auth_username(int $user_id): string|false
{
static $names = [];
static $unreachable = false;
if ($user_id < 1 || $unreachable) {
return false;
}
if (!array_key_exists($user_id, $names)) {
$reply = phpbb_bridge_request(phpbb_settings(), 'name', ['user_id' => $user_id]);
$answer = $reply['answer'];
// the pages of files ask for many names, they don't wait for a forum that is down for each one
$unreachable = $answer === null;
$names[$user_id] =
$answer !== null &&
(int) $answer['kjauth'] === PHPBB_BRIDGE_VERSION &&
($answer['status'] ?? '') === 'ok' &&
is_string($answer['name'] ?? null)
? kleeja_html_display($answer['name'])
: false;
}
return $names[$user_id];
}
}