Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
223 / 223 |
|
100.00% |
11 / 11 |
CRAP | |
100.00% |
1 / 1 |
manager | |
100.00% |
223 / 223 |
|
100.00% |
11 / 11 |
63 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
ban | |
100.00% |
53 / 53 |
|
100.00% |
1 / 1 |
10 | |||
unban | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
6 | |||
check | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
13 | |||
get_bans | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
get_banned_users | |
100.00% |
49 / 49 |
|
100.00% |
1 / 1 |
14 | |||
get_ban_end | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
tidy | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
find_type | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
get_info_cache | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
4 | |||
get_ban_message | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb\ban; |
15 | |
16 | use phpbb\ban\exception\invalid_length_exception; |
17 | use phpbb\ban\exception\type_not_found_exception; |
18 | use phpbb\ban\type\type_interface; |
19 | use phpbb\cache\driver\driver_interface as cache_driver; |
20 | use phpbb\db\driver\driver_interface; |
21 | use phpbb\di\service_collection; |
22 | use phpbb\language\language; |
23 | use phpbb\log\log_interface; |
24 | use phpbb\user; |
25 | |
26 | class manager |
27 | { |
28 | const CACHE_KEY_INFO = '_ban_info'; |
29 | const CACHE_KEY_USERS = '_banned_users'; |
30 | const CACHE_TTL = 3600; |
31 | |
32 | /** @var string */ |
33 | protected $bans_table; |
34 | |
35 | /** @var cache_driver */ |
36 | protected $cache; |
37 | |
38 | /** @var driver_interface */ |
39 | protected $db; |
40 | |
41 | /** @var service_collection */ |
42 | protected $types; |
43 | |
44 | /** @var language */ |
45 | protected $language; |
46 | |
47 | /** @var log_interface */ |
48 | protected $log; |
49 | |
50 | /** @var user */ |
51 | protected $user; |
52 | |
53 | /** @var string */ |
54 | protected $users_table; |
55 | |
56 | /** |
57 | * Creates a service which manages all bans. Developers can |
58 | * create their own ban types which will be handled in this. |
59 | * |
60 | * @param service_collection $types A service collection containing all ban types |
61 | * @param cache_driver $cache A cache object |
62 | * @param driver_interface $db A phpBB DBAL object |
63 | * @param language $language Language object |
64 | * @param log_interface $log Log object |
65 | * @param user $user User object |
66 | * @param string $bans_table The bans table |
67 | * @param string $users_table The users table |
68 | */ |
69 | public function __construct(service_collection $types, cache_driver $cache, driver_interface $db, language $language, |
70 | log_interface $log, user $user, string $bans_table, string $users_table = '') |
71 | { |
72 | $this->bans_table = $bans_table; |
73 | $this->cache = $cache; |
74 | $this->db = $db; |
75 | $this->types = $types; |
76 | $this->language = $language; |
77 | $this->log = $log; |
78 | $this->user = $user; |
79 | $this->users_table = $users_table; |
80 | } |
81 | |
82 | /** |
83 | * Creates ban entries for the given $items. Returns true if successful |
84 | * and false if no entries were added to the database |
85 | * |
86 | * @param string $mode A string which identifies a ban type |
87 | * @param array $items An array of items which should be banned |
88 | * @param \DateTimeInterface $start A DateTimeInterface object which is the start of the ban |
89 | * @param \DateTimeInterface $end A DateTimeInterface object which is the end of the ban (or 0 if permanent) |
90 | * @param string $reason An (internal) reason for the ban |
91 | * @param string $display_reason An optional reason which should be displayed to the banned |
92 | * |
93 | * @return bool |
94 | */ |
95 | public function ban(string $mode, array $items, \DateTimeInterface $start, \DateTimeInterface $end, string $reason, string $display_reason = ''): bool |
96 | { |
97 | if ($start > $end && $end->getTimestamp() !== 0) |
98 | { |
99 | throw new invalid_length_exception('LENGTH_BAN_INVALID'); |
100 | } |
101 | |
102 | /** @var type_interface $ban_mode */ |
103 | $ban_mode = $this->find_type($mode); |
104 | if ($ban_mode === false) |
105 | { |
106 | throw new type_not_found_exception(); |
107 | } |
108 | |
109 | if (!empty($this->user)) |
110 | { |
111 | $ban_mode->set_user($this->user); |
112 | } |
113 | $this->tidy(); |
114 | |
115 | $ban_items = $ban_mode->prepare_for_storage($items); |
116 | |
117 | // Prevent duplicate bans |
118 | $sql = 'DELETE FROM ' . $this->bans_table . " |
119 | WHERE ban_mode = '" . $this->db->sql_escape($mode) . "' |
120 | AND " . $this->db->sql_in_set('ban_item', $ban_items, false, true); |
121 | $this->db->sql_query($sql); |
122 | |
123 | $insert_array = []; |
124 | foreach ($ban_items as $ban_item) |
125 | { |
126 | $insert_array[] = [ |
127 | 'ban_mode' => $mode, |
128 | 'ban_item' => $ban_item, |
129 | 'ban_userid' => $mode === 'user' ? $ban_item : 0, |
130 | 'ban_start' => $start->getTimestamp(), |
131 | 'ban_end' => $end->getTimestamp(), |
132 | 'ban_reason' => $reason, |
133 | 'ban_reason_display' => $display_reason, |
134 | ]; |
135 | } |
136 | |
137 | if (empty($insert_array)) |
138 | { |
139 | return false; |
140 | } |
141 | |
142 | $this->db->sql_multi_insert($this->bans_table, $insert_array); |
143 | |
144 | $ban_data = [ |
145 | 'items' => $ban_items, |
146 | 'start' => $start, |
147 | 'end' => $end, |
148 | 'reason' => $reason, |
149 | 'display_reason' => $display_reason, |
150 | ]; |
151 | |
152 | // Add to admin log, moderator log and user notes |
153 | $ban_list_log = implode(', ', $items); |
154 | |
155 | $log_operation = 'LOG_BAN_' . strtoupper($mode); |
156 | $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, $log_operation, false, [$reason, $ban_list_log]); |
157 | $this->log->add('mod', $this->user->data['user_id'], $this->user->ip, $log_operation, false, [ |
158 | 'forum_id' => 0, |
159 | 'topic_id' => 0, |
160 | $reason, |
161 | $ban_list_log |
162 | ]); |
163 | |
164 | if ($banlist_ary = $ban_mode->after_ban($ban_data)) |
165 | { |
166 | foreach ($banlist_ary as $user_id) |
167 | { |
168 | $this->log->add('user', $this->user->data['user_id'], $this->user->ip, $log_operation, false, [ |
169 | 'reportee_id' => $user_id, |
170 | $reason, |
171 | $ban_list_log |
172 | ]); |
173 | } |
174 | } |
175 | |
176 | $this->cache->destroy(self::CACHE_KEY_INFO); |
177 | $this->cache->destroy(self::CACHE_KEY_USERS); |
178 | |
179 | return true; |
180 | } |
181 | |
182 | /** |
183 | * Removes ban entries from the database with the given IDs |
184 | * |
185 | * @param string $mode The ban type in which the ban IDs were created |
186 | * @param array $items An array of ban IDs which should be removed |
187 | */ |
188 | public function unban(string $mode, array $items) |
189 | { |
190 | /** @var type_interface $ban_mode */ |
191 | $ban_mode = $this->find_type($mode); |
192 | if ($ban_mode === false) |
193 | { |
194 | throw new type_not_found_exception(); |
195 | } |
196 | $this->tidy(); |
197 | |
198 | $sql_ids = array_map('intval', $items); |
199 | |
200 | if (count($sql_ids)) |
201 | { |
202 | $sql = 'SELECT ban_item |
203 | FROM ' . $this->bans_table . ' |
204 | WHERE ' . $this->db->sql_in_set('ban_id', $sql_ids); |
205 | $result = $this->db->sql_query($sql); |
206 | |
207 | $unbanned_items = []; |
208 | while ($row = $this->db->sql_fetchrow($result)) |
209 | { |
210 | $unbanned_items[] = $row['ban_item']; |
211 | } |
212 | $this->db->sql_freeresult($result); |
213 | |
214 | $sql = 'DELETE FROM ' . $this->bans_table . ' |
215 | WHERE ' . $this->db->sql_in_set('ban_id', $sql_ids); |
216 | $this->db->sql_query($sql); |
217 | |
218 | $unban_data = [ |
219 | 'items' => $unbanned_items, |
220 | ]; |
221 | $unbanned_users = $ban_mode->after_unban($unban_data); |
222 | |
223 | // Add to moderator log, admin log and user notes |
224 | $log_operation = 'LOG_UNBAN_' . strtoupper($mode); |
225 | $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, $log_operation, false, [$unbanned_users]); |
226 | $this->log->add('mod', $this->user->data['user_id'], $this->user->ip, $log_operation, false, [ |
227 | 'forum_id' => 0, |
228 | 'topic_id' => 0, |
229 | $unbanned_users |
230 | ]); |
231 | if (count($unbanned_users)) |
232 | { |
233 | foreach ($unbanned_users as $user_id) |
234 | { |
235 | $this->log->add('user', $this->user->data['user_id'], $this->user->ip, $log_operation, false, array( |
236 | 'reportee_id' => $user_id, |
237 | $unbanned_users |
238 | )); |
239 | } |
240 | } |
241 | } |
242 | |
243 | $this->cache->destroy(self::CACHE_KEY_INFO); |
244 | $this->cache->destroy(self::CACHE_KEY_USERS); |
245 | } |
246 | |
247 | /** |
248 | * Checks for the given user data whether the user is banned. |
249 | * Returns false if nothing was found and an array containing |
250 | * 'mode', 'end', 'reason' and 'item' otherwise. |
251 | * |
252 | * @param array $user_data The array containing the user data |
253 | * |
254 | * @return array|bool |
255 | */ |
256 | public function check(array $user_data = []) |
257 | { |
258 | if (empty($user_data)) |
259 | { |
260 | $user_data = $this->user->data; |
261 | } |
262 | |
263 | $ban_info = $this->get_info_cache(); |
264 | |
265 | foreach ($ban_info as $mode => $ban_rows) |
266 | { |
267 | /** @var type_interface $ban_mode */ |
268 | $ban_mode = $this->find_type($mode); |
269 | if ($ban_mode === false) |
270 | { |
271 | continue; |
272 | } |
273 | |
274 | if ($ban_mode->get_user_column() === null) |
275 | { |
276 | $ban_result = $ban_mode->check($ban_rows, $user_data); |
277 | if ($ban_result !== false) |
278 | { |
279 | return $ban_result + ['mode' => $mode]; |
280 | } |
281 | } |
282 | else |
283 | { |
284 | $user_column = $ban_mode->get_user_column(); |
285 | if (!isset($user_data[$user_column])) |
286 | { |
287 | continue; |
288 | } |
289 | |
290 | foreach ($ban_rows as $ban_row) |
291 | { |
292 | if (!$ban_row['end'] || $ban_row['end'] > time()) |
293 | { |
294 | if (stripos($ban_row['item'], '*') === false) |
295 | { |
296 | if ($ban_row['item'] == $user_data[$user_column]) |
297 | { |
298 | return $ban_row + ['mode' => $mode]; |
299 | } |
300 | } |
301 | else |
302 | { |
303 | $regex = '#^' . str_replace('\*', '.*?', preg_quote($ban_row['item'], '#')) . '$#i'; |
304 | if (preg_match($regex, $user_data[$user_column])) |
305 | { |
306 | return $ban_row + ['mode' => $mode]; |
307 | } |
308 | } |
309 | } |
310 | } |
311 | } |
312 | } |
313 | |
314 | return false; |
315 | } |
316 | |
317 | /** |
318 | * Returns all bans for a given ban type. False, if none were found |
319 | * |
320 | * @param string $mode The ban type for which the entries should be retrieved |
321 | * |
322 | * @return array|bool |
323 | */ |
324 | public function get_bans(string $mode) |
325 | { |
326 | /** @var type_interface $ban_type */ |
327 | $ban_type = $this->find_type($mode); |
328 | if ($ban_type === false) |
329 | { |
330 | throw new type_not_found_exception(); |
331 | } |
332 | $this->tidy(); |
333 | |
334 | return $ban_type->get_ban_options(); |
335 | } |
336 | |
337 | /** |
338 | * Returns an array of banned users with 'id' => 'end' values. |
339 | * The result is cached for performance reasons and is not as |
340 | * accurate as the check() method. (Wildcards aren't considered e.g.) |
341 | * |
342 | * @return array |
343 | */ |
344 | public function get_banned_users(): array |
345 | { |
346 | $banned_users = $this->cache->get(self::CACHE_KEY_USERS); |
347 | if ($banned_users === false) |
348 | { |
349 | $manual_modes = []; |
350 | $where_array = []; |
351 | |
352 | /** @var type_interface $ban_mode */ |
353 | foreach ($this->types as $ban_mode) |
354 | { |
355 | $user_column = $ban_mode->get_user_column(); |
356 | if (empty($user_column)) |
357 | { |
358 | $manual_modes[] = $ban_mode; |
359 | continue; |
360 | } |
361 | |
362 | $where_column = $user_column == 'user_id' ? 'b.ban_userid' : 'b.ban_item'; |
363 | |
364 | $where_array[] = ['AND', |
365 | [ |
366 | [$where_column, '=', 'u.' . $user_column], |
367 | ['b.ban_mode', '=', "'{$ban_mode->get_type()}'"], |
368 | ], |
369 | ]; |
370 | } |
371 | |
372 | $sql_array = [ |
373 | 'SELECT' => 'u.user_id, b.ban_end', |
374 | 'FROM' => [ |
375 | $this->bans_table => 'b', |
376 | $this->users_table => 'u', |
377 | ], |
378 | 'WHERE' => ['AND', |
379 | [ |
380 | ['OR', |
381 | $where_array, |
382 | ], |
383 | ['u.user_type', '<>', USER_FOUNDER], |
384 | ], |
385 | ], |
386 | ]; |
387 | $sql = $this->db->sql_build_query('SELECT', $sql_array); |
388 | $result = $this->db->sql_query($sql); |
389 | |
390 | $banned_users = []; |
391 | while ($row = $this->db->sql_fetchrow($result)) |
392 | { |
393 | $user_id = (int) $row['user_id']; |
394 | $end = (int) $row['ban_end']; |
395 | if (!isset($banned_users[$user_id]) || ($banned_users[$user_id] > 0 && $banned_users[$user_id] < $end)) |
396 | { |
397 | $banned_users[$user_id] = $end; |
398 | } |
399 | } |
400 | $this->db->sql_freeresult($result); |
401 | |
402 | /** @var type_interface $manual_mode */ |
403 | foreach ($manual_modes as $manual_mode) |
404 | { |
405 | $mode_banned_users = $manual_mode->get_banned_users(); |
406 | foreach ($mode_banned_users as $user_id => $end) |
407 | { |
408 | $user_id = (int) $user_id; |
409 | $end = (int) $end; |
410 | if (!isset($banned_users[$user_id]) || ($banned_users[$user_id] > 0 && $banned_users[$user_id] < $end)) |
411 | { |
412 | $banned_users[$user_id] = $end; |
413 | } |
414 | } |
415 | } |
416 | |
417 | $this->cache->put(self::CACHE_KEY_USERS, $banned_users, self::CACHE_TTL); |
418 | } |
419 | |
420 | return $banned_users; |
421 | } |
422 | |
423 | /** |
424 | * Get ban end |
425 | * |
426 | * @param \DateTimeInterface $ban_start Ban start time |
427 | * @param int $length Ban length in minutes |
428 | * @param string $end_date Ban end date as YYYY-MM-DD string |
429 | * @return \DateTimeInterface Ban end as DateTimeInterface instance |
430 | */ |
431 | public function get_ban_end(\DateTimeInterface $ban_start, int $length, string $end_date): \DateTimeInterface |
432 | { |
433 | $current_time = $ban_start->getTimestamp(); |
434 | $end_time = 0; |
435 | |
436 | if ($length) |
437 | { |
438 | if ($length != -1 || !$end_date) |
439 | { |
440 | $end_time = max($current_time, $current_time + ($length) * 60); |
441 | } |
442 | else |
443 | { |
444 | if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $end_date)) |
445 | { |
446 | $end_time = max( |
447 | $current_time, |
448 | \DateTime::createFromFormat('Y-m-d', $end_date, $this->user->timezone)->getTimestamp() |
449 | ); |
450 | } |
451 | else |
452 | { |
453 | throw new invalid_length_exception('LENGTH_BAN_INVALID'); |
454 | } |
455 | } |
456 | } |
457 | |
458 | $ban_end = new \DateTime(); |
459 | $ban_end->setTimestamp($end_time); |
460 | |
461 | return $ban_end; |
462 | } |
463 | |
464 | /** |
465 | * Cleans up the database of e.g. stale bans |
466 | */ |
467 | public function tidy() |
468 | { |
469 | // Delete stale bans |
470 | $sql = 'DELETE FROM ' . $this->bans_table . ' |
471 | WHERE ban_end > 0 |
472 | AND ban_end < ' . (int) time(); |
473 | $this->db->sql_query($sql); |
474 | |
475 | /** @var type_interface $type */ |
476 | foreach ($this->types as $type) |
477 | { |
478 | $type->tidy(); |
479 | } |
480 | } |
481 | |
482 | /** |
483 | * Finds the ban type for the given mode string. |
484 | * Returns false if none was found |
485 | * |
486 | * @param string $mode The mode string |
487 | * |
488 | * @return bool|type\type_interface |
489 | */ |
490 | protected function find_type(string $mode) |
491 | { |
492 | /** @var type_interface $type */ |
493 | foreach ($this->types as $type) |
494 | { |
495 | if ($type->get_type() === $mode) |
496 | { |
497 | return $type; |
498 | } |
499 | } |
500 | |
501 | return false; |
502 | } |
503 | |
504 | /** |
505 | * Returns the ban_info from the cache. |
506 | * If they're not in the cache, bans are retrieved from the database |
507 | * and then put into the cache. |
508 | * The array contains an array for each mode with respectively |
509 | * three values for 'item', 'end' and 'reason' only. |
510 | * |
511 | * @return array |
512 | */ |
513 | protected function get_info_cache(): array |
514 | { |
515 | $ban_info = $this->cache->get(self::CACHE_KEY_INFO); |
516 | if ($ban_info === false) |
517 | { |
518 | $sql = 'SELECT ban_mode, ban_item, ban_end, ban_reason_display |
519 | FROM ' . $this->bans_table; |
520 | $result = $this->db->sql_query($sql); |
521 | |
522 | $ban_info = []; |
523 | |
524 | while ($row = $this->db->sql_fetchrow($result)) |
525 | { |
526 | if (!isset($ban_info[$row['ban_mode']])) |
527 | { |
528 | $ban_info[$row['ban_mode']] = []; |
529 | } |
530 | |
531 | $ban_info[$row['ban_mode']][] = [ |
532 | 'item' => $row['ban_item'], |
533 | 'end' => $row['ban_end'], |
534 | 'reason' => $row['ban_reason_display'], |
535 | ]; |
536 | } |
537 | $this->db->sql_freeresult($result); |
538 | |
539 | $this->cache->put(self::CACHE_KEY_INFO, $ban_info, self::CACHE_TTL); |
540 | } |
541 | |
542 | return $ban_info; |
543 | } |
544 | |
545 | /** |
546 | * Get ban info message |
547 | * |
548 | * @param array $ban_row Ban data row from database |
549 | * @param string $ban_triggered_by Ban triggered by; allowed 'user', 'ip', 'email |
550 | * @param string $contact_link Contact link URL |
551 | * |
552 | * @return string Ban message |
553 | */ |
554 | public function get_ban_message(array $ban_row, string $ban_triggered_by, string $contact_link): string |
555 | { |
556 | if ($ban_row['end'] > 0) |
557 | { |
558 | $till_date = $this->user->format_date($ban_row['end']); |
559 | $ban_type = 'BOARD_BAN_TIME'; |
560 | } |
561 | else |
562 | { |
563 | $till_date = ''; |
564 | $ban_type = 'BOARD_BAN_PERM'; |
565 | } |
566 | |
567 | $message = $this->language->lang($ban_type, $till_date, '<a href="' . $contact_link . '">', '</a>'); |
568 | $message .= !empty($ban_row['reason']) ? '<br><br>' . $this->language->lang('BOARD_BAN_REASON', $ban_row['reason']) : ''; |
569 | $message .= '<br><br><em>' . $this->language->lang('BAN_TRIGGERED_BY_' . strtoupper($ban_triggered_by)) . '</em>'; |
570 | |
571 | return $message; |
572 | } |
573 | } |