PluginProbe
wpForo Forum / 3.1.4
wpForo Forum v3.1.4
3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 1.4.2 All 137 releases
wpforo / classes / Permissions.php

Permissions.php in wpForo Forum 3.1.4, at classes/Permissions.php

768 lines 23.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace wpforo\classes;
4
5 use stdClass;
6 use WP_User;
7
8 // Exit if accessed directly
9 if( ! defined( 'ABSPATH' ) ) exit;
10
11 class Permissions {
12 public $default;
13 public $accesses;
14 public $cans;
15
16 function __construct() {
17 $this->init_defaults();
18 $this->init_cans();
19 $this->init();
20 add_action( 'wpforo_after_init_classes', function() {
21 if( WPF()->is_installed() ) $this->init_current_user_accesses();
22 } );
23 add_action( 'wpforo_after_change_board', function() {
24 if( WPF()->is_installed() && ! is_null( WPF()->forum ) ) $this->init_current_user_accesses();
25 } );
26 }
27
28 private function init_defaults() {
29 $this->default = new stdClass;
30 $this->default->access = [
31 'accessid' => 0,
32 'access' => '',
33 'title' => '',
34 'cans' => '',
35 ];
36 $this->default->cans = [
37 'vf' => __( 'Can view forum', 'wpforo' ),
38 'enf' => __( 'Can enter forum', 'wpforo' ),
39 'ct' => __( 'Can create topic', 'wpforo' ),
40 'vt' => __( 'Can view topic', 'wpforo' ),
41 'ent' => __( 'Can enter topic', 'wpforo' ),
42 'et' => __( 'Can edit topic', 'wpforo' ),
43 'dt' => __( 'Can delete topic', 'wpforo' ),
44 'cr' => __( 'Can post reply', 'wpforo' ),
45 'ocr' => __( 'Can reply to own topic', 'wpforo' ),
46 'vr' => __( 'Can view replies', 'wpforo' ),
47 'er' => __( 'Can edit replies', 'wpforo' ),
48 'dr' => __( 'Can delete replies', 'wpforo' ),
49 'eot' => __( 'Can edit own topic', 'wpforo' ),
50 'eor' => __( 'Can edit own reply', 'wpforo' ),
51 'dot' => __( 'Can delete own topic', 'wpforo' ),
52 'dor' => __( 'Can delete own reply', 'wpforo' ),
53 'tag' => __( 'Can add tags', 'wpforo' ),
54 'sb' => __( 'Can subscribe', 'wpforo' ),
55 'l' => __( 'Can like', 'wpforo' ),
56 'r' => __( 'Can report', 'wpforo' ),
57 's' => __( 'Can set topic sticky', 'wpforo' ),
58 'p' => __( 'Can set topic private', 'wpforo' ),
59 'op' => __( 'Can set own topic private', 'wpforo' ),
60 'vp' => __( 'Can view private topic', 'wpforo' ),
61 'au' => __( 'Can approve/unapprove content', 'wpforo' ),
62 'sv' => __( 'Can set topic solved', 'wpforo' ),
63 'osv' => __( 'Can set own topic solved', 'wpforo' ),
64 'v' => __( 'Can vote', 'wpforo' ),
65 'vop' => __( 'Can leave voice posts', 'wpforo' ),
66 'vlp' => __( 'Can listen voice posts', 'wpforo' ),
67 'a' => __( 'Can attach file', 'wpforo' ),
68 'va' => __( 'Can view attached files', 'wpforo' ),
69 'at' => __( 'Can set topic answered', 'wpforo' ),
70 'oat' => __( 'Can set own topic answered', 'wpforo' ),
71 'aot' => __( 'Can answer own question', 'wpforo' ),
72 'cot' => __( 'Can close topic', 'wpforo' ),
73 'mt' => __( 'Can move topic', 'wpforo' ),
74 // Poll-related permissions moved to wpForo Polls plugin
75 ];
76 }
77
78 private function init_cans() {
79 $this->cans = apply_filters( 'wpforo_init_cans', $this->default->cans );
80 }
81
82 private function init() {
83 if( WPF()->is_installed() ) {
84 if( $accesses = $this->get_accesses() ) {
85 foreach( $accesses as $access ) {
86 $this->accesses[ intval( $access['accessid'] ) ] = $this->accesses[ $access['access'] ] = $access;
87 }
88 }
89 }
90 }
91
92 private function init_current_user_accesses() {
93 WPF()->current_user_accesses = $this->get_forum_accesses_by_usergroup();
94 }
95
96 public function fix_access( $access ) {
97 $access = wpforo_array_args_cast_and_merge( (array) $access, $this->default->access );
98 $cans = array_map( '__return_zero', $this->cans );
99 $access['cans'] = maybe_unserialize( $access['cans'] );
100 if( is_array( $access['cans'] ) ) {
101 $access['cans'] = wpforo_array_args_cast_and_merge( $access['cans'], $cans );
102 } else {
103 $access['cans'] = $cans;
104 }
105
106 return $access;
107 }
108
109 /**
110 *
111 * @param string|int $access
112 *
113 * @return array access row by access key
114 */
115 function get_access( $access ) {
116 if( wpforo_is_id( $access ) ) {
117 $access = intval( $access );
118 } else {
119 $access = sanitize_text_field( $access );
120 }
121 if( ! empty( $this->accesses[ $access ] ) ) return $this->accesses[ $access ];
122
123 $sql = "SELECT * FROM " . WPF()->tables->accesses;
124 if( is_int( $access ) ) {
125 $sql .= " WHERE `accessid` = %d";
126 } else {
127 $sql .= " WHERE `access` = %s";
128 }
129
130 $access_row = $this->fix_access( WPF()->db->get_row( WPF()->db->prepare( $sql, $access ), ARRAY_A ) );
131 /**
132 * Filter after getting an access row to allow add-ons to adjust permissions dynamically.
133 * Example: Polls add-on sets default poll permissions per access level.
134 *
135 * @since 2.4.0
136 * @param array $access_row
137 */
138 $access_row = apply_filters( 'wpforo_after_get_access', $access_row );
139 return $access_row;
140 }
141
142
143 /**
144 * get all accesses from accesses table
145 *
146 * @return array|null
147 */
148 function get_accesses() {
149 $sql = "SELECT * FROM " . WPF()->tables->accesses . " ORDER BY `accessid`";
150
151 $rows = array_map( [ $this, 'fix_access' ], WPF()->db->get_results( $sql, ARRAY_A ) );
152 // Allow add-ons to adjust each access after retrieval (e.g., apply default poll permissions)
153 $rows = array_map( function( $row ) { return apply_filters( 'wpforo_after_get_access', $row ); }, $rows );
154 return $rows;
155 }
156
157 /**
158 * @param array $access
159 *
160 * @return int|bool inserted id or false
161 */
162 function add( $access ) {
163 if( ! ( $access['title'] = sanitize_text_field( $access['title'] ) ) ) {
164 WPF()->notice->add( 'Access title is empty', 'error' );
165
166 return false;
167 }
168
169 if( ! $access['access'] ) $access['access'] = uniqid();
170
171 /**
172 * Allow add-ons to adjust access data before adding.
173 * Example: Polls add-on injects default poll permissions.
174 *
175 * @since 2.4.0
176 * @param array $access
177 */
178 $access = apply_filters( 'wpforo_before_add_access', $access );
179
180 $i = 2;
181 $slug = $access['access'];
182 while( WPF()->db->get_var( WPF()->db->prepare( "SELECT `access` FROM " . WPF()->tables->accesses . " WHERE `access` = %s", sanitize_text_field( $slug ) ) ) ) {
183 $slug = $access['access'] . '-' . $i;
184 $i ++;
185 }
186
187 if( WPF()->db->insert(
188 WPF()->tables->accesses, [
189 'title' => $access['title'],
190 'access' => sanitize_text_field(
191 $slug
192 ),
193 'cans' => serialize(
194 $access['cans']
195 ),
196 ], [ '%s', '%s', '%s', ]
197 ) ) {
198 $access['accessid'] = WPF()->db->insert_id;
199 WPF()->notice->add( 'Access successfully added', 'success' );
200
201 return $access['accessid'];
202 }
203
204 WPF()->notice->add( 'Access add error', 'error' );
205
206 return false;
207 }
208
209 /**
210 * @param array $access
211 *
212 * @return bool|int edited id or false
213 */
214 function edit( $access ) {
215 /**
216 * Allow add-ons to adjust access data before editing.
217 * Example: Polls add-on injects/ensures poll permissions are present.
218 *
219 * @since 2.4.0
220 * @param array $access
221 */
222 $access = apply_filters( 'wpforo_before_edit_access', $access );
223 if( false !== WPF()->db->update( WPF()->tables->accesses, [
224 'title' => sanitize_text_field( $access['title'] ),
225 'cans' => serialize( $access['cans'] ),
226 ], [
227 'accessid' => $access['accessid'],
228 ], [ '%s', '%s' ], [ '%d' ] ) ) {
229 WPF()->notice->add( 'Access successfully edited', 'success' );
230
231 return $access['accessid'];
232 }
233
234 WPF()->notice->add( 'Access edit error', 'error' );
235
236 return false;
237 }
238
239 /**
240 * @param int $accessid
241 *
242 * @return bool|int deleted id or false
243 */
244 function delete( $accessid ) {
245 $accessid = intval( $accessid );
246 if( ! $accessid ) {
247 WPF()->notice->add( 'Access delete error', 'error' );
248
249 return false;
250 }
251
252 if( false !== WPF()->db->delete( WPF()->tables->accesses, [ 'accessid' => $accessid ], [ '%d' ] ) ) {
253 WPF()->notice->add( 'Access successfully deleted', 'success' );
254
255 return $accessid;
256 }
257
258 WPF()->notice->add( 'Access delete error', 'error' );
259
260 return false;
261 }
262
263 function forum_can( $do, $forumid = null, $groupids = null ) {
264 /**
265 * filter for other add-ons to manage can_attach bool value.
266 * e.g. PM add-on attachment function.
267 */
268 $filter_forum_can = apply_filters( 'wpforo_permissions_forum_can', null, $do, $forumid, $groupids );
269 if( ! is_null( $filter_forum_can ) ) return (int) (bool) $filter_forum_can;
270
271 if( ( is_null( $groupids ) && ! WPF()->current_user_groupids ) || ! $do ) return 0;
272
273 //User Forum accesses from Current Object of Current user
274 if( is_null( $groupids ) && WPF()->current_user_accesses ) {
275 $forum_id = (int) ( is_null( $forumid ) ? wpfval( WPF()->current_object, 'forum', 'forumid' ) : ( wpfkey( $forumid, 'forumid' ) ? $forumid['forumid'] : $forumid ) );
276 if( $forum_id && ( $forum_accesses = wpfval( WPF()->current_user_accesses, $forum_id ) ) ) {
277 foreach( $forum_accesses as $cans ) {
278 if( (int) wpfval( $cans, $do ) ) return 1;
279 }
280 }
281
282 return 0;
283 }
284
285 //Use Custom User Forum Accesses
286 $forum = is_null( $forumid ) ? WPF()->current_object['forum'] : ( ! wpfkey( $forumid, 'forumid' ) ? WPF()->forum->get_forum( $forumid ) : $forumid );
287 if( $forum ) {
288 $permissions = maybe_unserialize( $forum['permissions'] );
289 if( is_null( $groupids ) ) $groupids = WPF()->current_user_groupids;
290 $groupids = array_map( 'intval', (array) $groupids );
291 foreach( $groupids as $groupid ) {
292 if( $_access = wpfval( $permissions, $groupid ) ) {
293 $access = $this->get_access( $_access );
294 if( (int) wpfval( $access, 'cans', $do ) ) return 1;
295 }
296 }
297 }
298
299 return 0;
300 }
301
302 function user_can_manage_user( $user_id, $managing_user_id ) {
303 if( ! $user_id || ! $managing_user_id ) return false;
304 if( $user_id == $managing_user_id ) return true;
305
306 $user = new WP_User( $user_id );
307 $user_level = $this->user_wp_level( $user );
308 if( ! empty( $user->roles ) && is_array( $user->roles ) ) $user_role = array_shift( $user->roles );
309
310 $managing_user = new WP_User( $managing_user_id );
311 $managing_user_level = $this->user_wp_level( $managing_user );
312 if( ! empty( $managing_user->roles ) && is_array( $managing_user->roles ) ) $managing_user_role = array_shift( $managing_user->roles );
313
314 if( (int) $user_level > (int) $managing_user_level ) {
315 return true;
316 } elseif( $user_id == 1 && $user_role === 'administrator' ) {
317 return true;
318 } elseif( (int) $user_level === (int) $managing_user_level ) {
319 $member = WPF()->member->get_member( $user_id );
320 $managing_member = WPF()->member->get_member( $managing_user_id );
321 $user_wpforo_can = WPF()->usergroup->can( 'em', $member['groupids'] );
322 $managing_user_wpforo_can = WPF()->usergroup->can( 'em', $managing_member['groupids'] );
323 if( $user_wpforo_can && ! $managing_user_wpforo_can ) {
324 return true;
325 } else {
326 return false;
327 }
328 } elseif( $user_id != 1 && $managing_user_id == 1 && $managing_user_role === 'administrator' ) {
329 return false;
330 } else {
331 return false;
332 }
333 }
334
335 function user_wp_level( $user_object ) {
336 $level = 0;
337 $levels = [];
338 if( is_int( $user_object ) ) {
339 $user_object = new WP_User( $user_object );
340 }
341 if( isset( $user_object->allcaps ) && is_array( $user_object->allcaps ) && ! empty( $user_object->allcaps ) ) {
342 foreach( $user_object->allcaps as $level_key => $level_value ) {
343 if( strpos( (string) $level_key, 'level_' ) !== false && $level_value == 1 ) {
344 $levels[] = intval( str_replace( 'level_', '', $level_key ) );
345 }
346 }
347 if( ! empty( $levels ) ) {
348 $level = max( $levels );
349 }
350 }
351
352 return $level;
353 }
354
355 function can_edit_user( $userid ) {
356 if( ! $userid ) return false;
357 if( ! $this->user_can_edit_account( $userid ) ) {
358 WPF()->notice->clear();
359 WPF()->notice->add( 'Permission denied', 'error' );
360 wp_safe_redirect( wpforo_get_request_uri() );
361 exit();
362 }
363
364 return true;
365 }
366
367 public function can_link() {
368 if( ! WPF()->usergroup->can( 'em' ) ) {
369 $posts = WPF()->member->member_approved_posts( WPF()->current_userid );
370 $posts = intval( $posts );
371 if( ( $min_posts = wpforo_setting( 'antispam', 'min_number_posts_to_link' ) ) && $posts <= $min_posts ) return false;
372 }
373
374 return true;
375 }
376
377 public function can_attach( $forumid = null ) {
378 if( ! $forumid ) $forumid = null;
379
380 /**
381 * filter for other add-ons to manage can_attach bool value.
382 * e.g. PM add-on attachment function.
383 */
384 $filter_wpforo_can_attach = apply_filters( 'wpforo_can_attach', null, $forumid );
385 if( ! is_null( $filter_wpforo_can_attach ) ) return (bool) $filter_wpforo_can_attach;
386
387 if( ! $this->forum_can( 'a', $forumid ) ) return false;
388 if( ! WPF()->usergroup->can( 'em' ) ) {
389 $posts = WPF()->member->member_approved_posts( WPF()->current_userid );
390 $posts = intval( $posts );
391 if( ( $min_posts = wpforo_setting( 'antispam', 'min_number_posts_to_attach' ) ) && $posts <= $min_posts ) return false;
392 }
393
394 return true;
395 }
396
397 public function can_attach_file_type( $ext = '' ) {
398 if( ! WPF()->usergroup->can( 'em' ) && WPF()->member->current_user_is_new() && in_array( $ext, wpforo_setting( 'antispam', 'limited_file_ext' ) ) ) return false;
399
400 return true;
401 }
402
403 /**
404 * Check if user can post now based on flood protection settings
405 *
406 * @param string $flood_reason Reference to store the reason if blocked
407 * @return bool|string True if can post, false if blocked, 'unapprove' if should be unapproved
408 */
409 public function can_post_now( &$flood_reason = '' ) {
410 if( wpforo_is_admin() || ( defined( 'IS_GO2WPFORO' ) && IS_GO2WPFORO ) ) {
411 return true;
412 }
413
414 // Users with "Dashboard - Moderate Topics & Posts" permission bypass flood protection
415 if( WPF()->usergroup->can( 'aum' ) ) {
416 return true;
417 }
418
419 $userid = WPF()->current_userid;
420 $email = $userid ? '' : WPF()->current_user_email;
421 $groupid = WPF()->current_user_groupid;
422 if( WPF()->member->current_user_is_new() ) {
423 $groupid = 0;
424 }
425
426 // Check for temporary ban first
427 if( $this->is_flood_banned() ) {
428 $flood_reason = 'temp_ban';
429 return false;
430 }
431
432 // Legacy flood interval check (per user group)
433 if( $flood_interval = WPF()->usergroup->get_flood_interval( $groupid ) ) {
434 $hour_ago = gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS );
435 $args = [
436 'userid' => $userid,
437 'email' => $email,
438 'orderby' => '`created` DESC, `postid` DESC',
439 'row_count' => 1,
440 'where' => "`created` >= '$hour_ago'",
441 ];
442 $items_count = 0;
443 $lastpost = WPF()->post->get_posts( $args, $items_count, false );
444 if( $lasttime = wpfval( $lastpost, 0, 'created' ) ) {
445 $lasttime = strtotime( $lasttime . ' GMT' );
446 $nowtime = time();
447 $diff = $nowtime - $lasttime;
448 if( $diff < $flood_interval ) {
449 $flood_reason = 'interval';
450 return false;
451 }
452 }
453 }
454
455 // Advanced flood protection checks
456 if( ! wpforo_setting( 'antispam', 'flood_protection_enabled' ) ) {
457 return true;
458 }
459
460 // Check posts per minute
461 $posts_per_minute = (int) wpforo_setting( 'antispam', 'flood_posts_per_minute' );
462 if( $posts_per_minute > 0 ) {
463 $minute_ago = gmdate( 'Y-m-d H:i:s', time() - 60 );
464 $count = $this->get_user_post_count_since( $userid, $email, $minute_ago );
465 if( $count >= $posts_per_minute ) {
466 $flood_reason = 'per_minute';
467 return $this->handle_flood_action();
468 }
469 }
470
471 // Check posts per hour
472 $posts_per_hour = (int) wpforo_setting( 'antispam', 'flood_posts_per_hour' );
473 if( $posts_per_hour > 0 ) {
474 $hour_ago = gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS );
475 $count = $this->get_user_post_count_since( $userid, $email, $hour_ago );
476 if( $count >= $posts_per_hour ) {
477 $flood_reason = 'per_hour';
478 return $this->handle_flood_action();
479 }
480 }
481
482 // IP-based flood protection (tracked via transients since posts table has no IP column)
483 if( wpforo_setting( 'antispam', 'flood_ip_protection_enabled' ) ) {
484 $posts_per_ip_hour = (int) wpforo_setting( 'antispam', 'flood_posts_per_ip_hour' );
485 if( $posts_per_ip_hour > 0 ) {
486 $ip = $this->get_user_ip();
487 if( $ip ) {
488 $count = $this->get_ip_post_count_in_window( $ip, HOUR_IN_SECONDS );
489 if( $count >= $posts_per_ip_hour ) {
490 $flood_reason = 'ip_per_hour';
491 return $this->handle_flood_action();
492 }
493 }
494 }
495 }
496
497 // All checks passed - record IP post attempt for IP-based tracking
498 if( wpforo_setting( 'antispam', 'flood_ip_protection_enabled' ) ) {
499 $ip = $this->get_user_ip();
500 if( $ip ) {
501 $this->record_ip_post_attempt( $ip );
502 }
503 }
504
505 return true;
506 }
507
508 /**
509 * Get the current user's IP address
510 *
511 * @return string|null IP address or null if not available
512 */
513 private function get_user_ip() {
514 // Check for forwarded IP first (behind proxy/load balancer)
515 $headers = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' ];
516 foreach( $headers as $header ) {
517 if( ! empty( $_SERVER[ $header ] ) ) {
518 $ip = $_SERVER[ $header ];
519 // HTTP_X_FORWARDED_FOR can contain multiple IPs, get the first one
520 if( strpos( $ip, ',' ) !== false ) {
521 $ip = trim( explode( ',', $ip )[0] );
522 }
523 if( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
524 return $ip;
525 }
526 }
527 }
528 return null;
529 }
530
531 /**
532 * Get post count for a user since a specific time
533 *
534 * @param int $userid User ID
535 * @param string $email Email for guests
536 * @param string $since MySQL datetime string
537 * @return int Post count
538 */
539 private function get_user_post_count_since( $userid, $email, $since ) {
540 $posts_table = WPF()->tables->posts;
541
542 if( $userid ) {
543 $count = WPF()->db->get_var( WPF()->db->prepare(
544 "SELECT COUNT(*) FROM `{$posts_table}` WHERE `userid` = %d AND `created` >= %s",
545 $userid,
546 $since
547 ) );
548 } elseif( $email ) {
549 $count = WPF()->db->get_var( WPF()->db->prepare(
550 "SELECT COUNT(*) FROM `{$posts_table}` WHERE `email` = %s AND `created` >= %s",
551 $email,
552 $since
553 ) );
554 } else {
555 return 0;
556 }
557
558 return (int) $count;
559 }
560
561 /**
562 * Get post count for an IP address within a time window (using transients)
563 *
564 * @param string $ip IP address
565 * @param int $window_seconds Time window in seconds
566 * @return int Post count
567 */
568 private function get_ip_post_count_in_window( $ip, $window_seconds ) {
569 $key = 'wpforo_ip_posts_' . md5( $ip );
570 $timestamps = get_transient( $key );
571 if( ! is_array( $timestamps ) ) {
572 return 0;
573 }
574
575 // Filter to only include timestamps within the window
576 $cutoff = time() - $window_seconds;
577 $valid_timestamps = array_filter( $timestamps, function( $ts ) use ( $cutoff ) {
578 return $ts >= $cutoff;
579 });
580
581 return count( $valid_timestamps );
582 }
583
584 /**
585 * Record an IP post attempt for flood tracking
586 *
587 * @param string $ip IP address
588 */
589 private function record_ip_post_attempt( $ip ) {
590 $key = 'wpforo_ip_posts_' . md5( $ip );
591 $timestamps = get_transient( $key );
592 if( ! is_array( $timestamps ) ) {
593 $timestamps = [];
594 }
595
596 // Add current timestamp
597 $timestamps[] = time();
598
599 // Clean old entries (keep last hour only)
600 $cutoff = time() - HOUR_IN_SECONDS;
601 $timestamps = array_filter( $timestamps, function( $ts ) use ( $cutoff ) {
602 return $ts >= $cutoff;
603 });
604
605 // Re-index array and store with 1-hour expiration
606 $timestamps = array_values( $timestamps );
607 set_transient( $key, $timestamps, HOUR_IN_SECONDS );
608 }
609
610 /**
611 * Handle flood action based on settings
612 *
613 * @return bool|string False to block, 'unapprove' to allow but unapprove
614 */
615 private function handle_flood_action() {
616 $action = wpforo_setting( 'antispam', 'flood_action' );
617
618 if( $action === 'temp_ban' ) {
619 $this->set_flood_ban();
620 return false;
621 }
622
623 if( $action === 'unapprove' ) {
624 return 'unapprove';
625 }
626
627 // Default: block
628 return false;
629 }
630
631 /**
632 * Set a temporary flood ban for the current user/IP
633 */
634 private function set_flood_ban() {
635 $duration = (int) wpforo_setting( 'antispam', 'flood_temp_ban_duration' );
636 $ban_until = time() + ( $duration * 60 );
637
638 $userid = WPF()->current_userid;
639 $ip = $this->get_user_ip();
640
641 // Store ban in transient (keyed by user ID or IP)
642 if( $userid ) {
643 set_transient( 'wpforo_flood_ban_user_' . $userid, $ban_until, $duration * 60 );
644 }
645 if( $ip ) {
646 set_transient( 'wpforo_flood_ban_ip_' . md5( $ip ), $ban_until, $duration * 60 );
647 }
648 }
649
650 /**
651 * Check if the current user/IP is temporarily banned for flooding
652 *
653 * @return bool True if banned
654 */
655 public function is_flood_banned() {
656 $userid = WPF()->current_userid;
657 $ip = $this->get_user_ip();
658
659 // Check user ban
660 if( $userid ) {
661 $ban_until = get_transient( 'wpforo_flood_ban_user_' . $userid );
662 if( $ban_until && time() < $ban_until ) {
663 return true;
664 }
665 }
666
667 // Check IP ban
668 if( $ip ) {
669 $ban_until = get_transient( 'wpforo_flood_ban_ip_' . md5( $ip ) );
670 if( $ban_until && time() < $ban_until ) {
671 return true;
672 }
673 }
674
675 return false;
676 }
677
678 /**
679 * Get remaining flood ban time in seconds
680 *
681 * @return int Seconds remaining, 0 if not banned
682 */
683 public function get_flood_ban_remaining() {
684 $userid = WPF()->current_userid;
685 $ip = $this->get_user_ip();
686 $max_remaining = 0;
687
688 if( $userid ) {
689 $ban_until = get_transient( 'wpforo_flood_ban_user_' . $userid );
690 if( $ban_until && time() < $ban_until ) {
691 $max_remaining = max( $max_remaining, $ban_until - time() );
692 }
693 }
694
695 if( $ip ) {
696 $ban_until = get_transient( 'wpforo_flood_ban_ip_' . md5( $ip ) );
697 if( $ban_until && time() < $ban_until ) {
698 $max_remaining = max( $max_remaining, $ban_until - time() );
699 }
700 }
701
702 return $max_remaining;
703 }
704
705 public function get_forum_accesses_by_usergroup( $groupids = [] ) {
706 $forum_accesses = [];
707 if( ! $groupids ) $groupids = WPF()->current_user_groupids;
708 if( ( $groupids = array_map( 'intval', (array) $groupids ) ) && ( $forums = WPF()->forum->get_forums() ) ) {
709 foreach( $forums as $forum ) {
710 if( $permissions = maybe_unserialize( $forum['permissions'] ) ) {
711 foreach( $groupids as $groupid ) {
712 $access = wpfval( $permissions, $groupid );
713 if( $_access = $this->get_access( $access ) ) {
714 if( $cans = wpfval( $_access, 'cans' ) ) {
715 if( ! wpfkey( $forum_accesses, $forum['forumid'], $access ) ) $forum_accesses[ $forum['forumid'] ][ $access ] = $cans;
716 }
717 }
718 }
719 }
720 }
721 }
722
723 return apply_filters( 'wpforo_permissions_forum_accesses_by_usergroup', $forum_accesses, $groupids );
724 }
725
726 public function show_accesses_selectbox( $selected = [], $exclude = [] ) {
727 $accesses = $this->get_accesses();
728 foreach( $accesses as $accesse ) {
729 if( in_array( $accesse['access'], (array) $exclude ) ) continue;
730 printf(
731 '<option value="%1$s" %2$s>%3$s</option>',
732 esc_attr( $accesse['access'] ),
733 in_array( $accesse['access'], (array) $selected ) ? 'selected' : '',
734 esc_html( $accesse['title'] )
735 );
736 }
737 }
738
739 /**
740 * @param array|int $owner
741 * @param array|int $user
742 *
743 * @return bool
744 */
745 public function user_can_edit_account( $owner = [], $user = [] ) {
746 if( ! $user ) $user = WPF()->current_user;
747 if( ! $owner ) $owner = WPF()->current_object['user'];
748 if( wpforo_is_id( $owner ) ) $owner = WPF()->member->get_member( $owner );
749 if( wpforo_is_id( $user ) ) $user = WPF()->member->get_member( $user );
750 if( ! $user || ! $owner ) return false;
751 $is_users_same = wpforo_is_users_same( $user, $owner );
752
753 return wpforo_user_is( $user['userid'], 'admin' ) || ( WPF()->usergroup->can( 'em', $user['groupids'] ) && $this->user_can_manage_user(
754 $user['userid'],
755 $owner['userid']
756 ) ) || ( $is_users_same && wpforo_user_is( $user['userid'], 'moderator' ) ) || ( $is_users_same && $user['posts'] >= wpforo_setting( 'antispam', 'min_number_posts_to_edit_account' ) );
757 }
758
759 public function can_report( $forumid, $groupids = null ): bool {
760 if( ( is_null( $groupids ) && ! WPF()->current_user_groupids ) ) return false;
761 if( is_null( $groupids ) ) $groupids = WPF()->current_user_groupids;
762
763 $res = WPF()->current_userid && ! in_array( WPF()->current_user_status, [ 'banned', 'trashed' ] ) && ! WPF()->member->current_user_is_new() && $this->forum_can( 'r', $forumid, $groupids );
764
765 return apply_filters( 'wpforo_can_report', $res, $forumid, $groupids );
766 }
767 }
768