PluginProbe
wpForo Forum / 3.1.6
wpForo Forum v3.1.6
3.1.6 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 All 138 releases
wpforo / classes / Topics.php

Topics.php in wpForo Forum 3.1.6, at classes/Topics.php

2,219 lines 68.1 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 // Exit if accessed directly
6 if( ! defined( 'ABSPATH' ) ) exit;
7
8 class Topics {
9 static $cache = [ 'topics' => [], 'tags' => [], 'item' => [], 'topic' => [], 'tag' => [], 'forum_slug' => [] ];
10
11 function __construct() {
12 $this->init_hooks();
13 }
14
15 private function init_hooks() {
16 add_action( 'wpforo_after_add_post', [ $this, 'after_add_post' ], 10, 2 );
17 add_action( 'wpforo_after_delete_post', [ $this, 'after_delete_post' ] );
18 add_action( 'wpforo_post_status_update', [ $this, 'after_post_status_update' ] );
19 add_action( 'wpforo_after_delete_user', [ $this, 'after_delete_user' ], 11, 2 );
20 }
21
22 public function get_cache( $var ) {
23 if( isset( self::$cache[ $var ] ) ) return self::$cache[ $var ];
24
25 return [];
26 }
27
28 public function reset() {
29 self::$cache = [ 'topics' => [], 'tags' => [], 'item' => [], 'topic' => [], 'tag' => [], 'forum_slug' => [] ];
30 }
31
32 /**
33 * Get appropriate error message for flood protection
34 *
35 * @param string $reason The flood reason code
36 * @return string Error message
37 */
38 private function get_flood_error_message( $reason ) {
39 switch( $reason ) {
40 case 'temp_ban':
41 $remaining = WPF()->perm->get_flood_ban_remaining();
42 $minutes = ceil( $remaining / 60 );
43 return sprintf(
44 __( 'You have been temporarily blocked for posting too quickly. Please wait %d minute(s) before trying again.', 'wpforo' ),
45 $minutes
46 );
47 case 'per_minute':
48 return __( 'You are posting too quickly. Please wait a moment before creating another topic.', 'wpforo' );
49 case 'per_hour':
50 return __( 'You have reached the hourly posting limit. Please wait before creating more topics.', 'wpforo' );
51 case 'ip_per_hour':
52 return __( 'Too many posts from your location. Please wait before trying again.', 'wpforo' );
53 case 'interval':
54 default:
55 return __( 'You are posting too quickly. Slow down.', 'wpforo' );
56 }
57 }
58
59 public function edit( $args = [] ) {
60 if( empty( $args ) && empty( $_REQUEST['thread'] ) ) return false;
61 if( ! isset( $args['topicid'] ) && isset( $_GET['id'] ) ) $args['topicid'] = intval( $_GET['id'] );
62 if( empty( $args ) && ! empty( $_REQUEST['thread'] ) ) $args = $_REQUEST['thread'];
63 if( isset( $args['name'] ) ) {
64 $args['name'] = strip_tags( (string) $args['name'] );
65 }
66 if( isset( $args['email'] ) ) {
67 $args['email'] = sanitize_email( $args['email'] );
68 }
69
70
71 if( ! $topic = $this->get_topic( $args['topicid'] ) ) {
72 WPF()->notice->add( 'Topic not found.', 'error' );
73
74 return false;
75 }
76
77 if( ! $forum = WPF()->forum->get_forum( $topic['forumid'] ) ) {
78 WPF()->notice->add( 'Forum not found.', 'error' );
79
80 return false;
81 }
82
83 do_action( 'wpforo_start_edit_topic', $args, $forum );
84
85 if( ! is_user_logged_in() ) {
86 if( ! isset( $topic['email'] ) || ! $topic['email'] ) {
87 WPF()->notice->add( 'Permission denied', 'error' );
88
89 return false;
90 } elseif( ! wpforo_guest_owns_post( $topic['first_postid'] ) ) {
91 WPF()->notice->add( 'You are not allowed to edit this post', 'error' );
92
93 return false;
94 }
95 }
96
97 $args['status'] = $topic['status'];
98 $args['userid'] = $topic['userid'];
99
100 $args = apply_filters( 'wpforo_edit_topic_data_filter', $args, $forum );
101 if( empty( $args ) ) return false;
102
103 if( $min = wpforo_setting( 'posting', 'topic_body_min_length' ) ) {
104 if( wpfkey( $args, 'body' ) && (int) $min > wpforo_length( $args['body'] ) ) {
105 WPF()->notice->add( 'The content is too short', 'error' );
106
107 return false;
108 }
109 }
110
111 // Security: extract only expected keys to prevent variable injection (e.g., $topic/$forum override)
112 $topicid = isset( $args['topicid'] ) ? $args['topicid'] : null;
113 $title = isset( $args['title'] ) ? $args['title'] : null;
114 $type = isset( $args['type'] ) ? $args['type'] : null;
115 $status = isset( $args['status'] ) ? $args['status'] : null;
116 $private = isset( $args['private'] ) ? $args['private'] : null;
117 $name = isset( $args['name'] ) ? $args['name'] : null;
118 $email = isset( $args['email'] ) ? $args['email'] : null;
119 $body = isset( $args['body'] ) ? $args['body'] : null;
120 $tags = isset( $args['tags'] ) ? $args['tags'] : null;
121
122 if( isset( $topicid ) ) $topicid = intval( $topicid );
123 if( isset( $title ) ) $title = sanitize_text_field( trim( (string) $title ) );
124 if( isset( $type ) ) $type = intval( $type );
125 if( isset( $status ) ) $status = intval( $status );
126 if( isset( $private ) ) $private = intval( $private );
127 if( isset( $name ) ) $name = strip_tags( trim( (string) $name ) );
128 if( isset( $email ) ) $email = strip_tags( trim( (string) $email ) );
129 if( isset( $body ) ) $body = wpforo_kses( trim( (string) $body ) );
130 if( isset( $tags ) ) {
131 if( isset( $topic['forumid'] ) && wpforo_is_module_enabled( 'tags' ) && WPF()->perm->forum_can(
132 'tag',
133 $topic['forumid']
134 ) ) {
135 $tags = $this->sanitize_tags( $tags, false, true );
136 } else {
137 $tags = '';
138 }
139 }
140
141
142 if( ! isset( $topicid ) ) {
143 WPF()->notice->add( 'Topic edit error', 'error' );
144
145 return false;
146 }
147
148 if( isset( $body ) ) $body = preg_replace( '#</pre>[\r\n\t\s\0]*<pre>#isu', "\r\n", (string) $body );
149
150 $diff = time() - strtotime( $topic['created'] . ' GMT' );
151 if( ! ( WPF()->perm->forum_can( 'et', $topic['forumid'] ) || ( WPF()->current_userid == $topic['userid'] && WPF()->perm->forum_can( 'eot', $topic['forumid'] ) ) ) ) {
152 WPF()->notice->add( 'You have no permission to edit this topic', 'error' );
153
154 return false;
155 }
156
157 if( ! WPF()->perm->forum_can( 'et', $topic['forumid'] ) && wpforo_setting(
158 'posting',
159 'edit_own_topic_durr'
160 ) !== 0 && $diff > wpforo_setting(
161 'posting',
162 'edit_own_topic_durr'
163 ) ) {
164 WPF()->notice->add( 'The time to edit this topic is expired', 'error' );
165
166 return false;
167 }
168
169 $title = ( isset( $title ) ? stripslashes( $title ) : stripslashes( (string) $topic['title'] ) );
170 $type = ( isset( $type ) ? $type : intval( $topic['type'] ) );
171 $status = ( isset( $status ) ? $status : intval( $topic['status'] ) );
172 $private = ( isset( $private ) ? $private : intval( $topic['private'] ) );
173 // Force private for ticket_forum type forums (all topics must be private)
174 if( wpfval( $forum, 'type' ) === 'ticket_forum' ) {
175 $private = 1;
176 }
177 $has_attach = ( isset( $body ) ? ( strpos(
178 (string) $body,
179 '[attach]'
180 ) !== false ? 1 : 0 ) : $topic['has_attach'] );
181 $name = ( isset( $name ) ? stripslashes( $name ) : stripslashes( (string) $topic['name'] ) );
182 $email = ( isset( $email ) ? stripslashes( $email ) : stripslashes( (string) $topic['email'] ) );
183 $tags = ( isset( $tags ) ? $tags : '' );
184
185 $t_update = WPF()->db->update(
186 WPF()->tables->topics,
187 [
188 'title' => $title,
189 'type' => $type,
190 'status' => $status,
191 'private' => $private,
192 'has_attach' => $has_attach,
193 'name' => $name,
194 'email' => $email,
195 'tags' => $tags,
196 ],
197 [ 'topicid' => $topicid ],
198 [ '%s', '%d', '%d', '%d', '%d', '%s', '%s', '%s' ], [ '%d' ]
199 );
200
201 if( isset( $topic['first_postid'] ) ) {
202 if( ! $post = WPF()->post->get_post( $topic['first_postid'] ) ) {
203 WPF()->notice->add( 'Topic first post data not found.', 'error' );
204
205 return false;
206 }
207 } else {
208 WPF()->notice->add( 'Topic first post not found.', 'error' );
209
210 return false;
211 }
212
213 $body = ( ! $min || ( isset( $body ) && $body ) ) ? stripslashes( (string) $body ) : stripslashes(
214 (string) $post['body']
215 );
216
217 $p_update = WPF()->db->update(
218 WPF()->tables->posts,
219 [
220 'title' => $title,
221 'body' => $body,
222 'modified' => current_time(
223 'mysql',
224 1
225 ),
226 'status' => $status,
227 'private' => $private,
228 'name' => $name,
229 'email' => $email,
230 ],
231 [ 'postid' => intval( $topic['first_postid'] ) ],
232 [ '%s', '%s', '%s', '%d', '%d', '%s', '%s' ], [ '%d' ]
233 );
234
235 if( $t_update !== false && $p_update !== false ) {
236
237 if( isset( $tags ) ) $this->edit_tags( $tags, $topic );
238
239 $a = [
240 'userid' => $topic['userid'],
241 'forumid' => $topic['forumid'],
242 'topicid' => $topicid,
243 'postid' => $topic['first_postid'],
244 'first_postid' => $topic['first_postid'],
245 'title' => $title,
246 'body' => $body,
247 'status' => $status,
248 'private' => $private,
249 'name' => $name,
250 'email' => $email,
251 'type' => $type,
252 'has_attach' => $has_attach,
253 'tags' => $tags,
254 ];
255 do_action( 'wpforo_after_edit_topic', $a, $args, $forum );
256
257 wpforo_clean_cache( 'topic-first-post', $topicid, $topic );
258 WPF()->notice->add( 'Topic successfully updated', 'success' );
259
260 return $topicid;
261 }
262
263 WPF()->notice->add( 'Topic edit error', 'error' );
264
265 return false;
266 }
267
268 public function get_topic( $args = [], $protect = true ) {
269 return wpforo_ram_get( [ $this, '_get_topic' ], $args, $protect );
270 }
271
272 public function add( $args = [] ) {
273
274 if( empty( $args ) && empty( $_REQUEST['thread'] ) ) return false;
275 if( empty( $args ) && ! empty( $_REQUEST['thread'] ) ) $args = $_REQUEST['thread'];
276 if( $min = wpforo_setting( 'posting', 'topic_body_min_length' ) ) {
277 if( wpfkey( $args, 'body' ) && (int) $min > wpforo_length( $args['body'] ) ) {
278 WPF()->notice->add( 'The content is too short. It must be at least %1$d characters long.', 'error', $min );
279
280 return false;
281 }
282 }
283 $args['name'] = ( isset( $args['name'] ) ? strip_tags( (string) $args['name'] ) : '' );
284 $args['email'] = ( isset( $args['email'] ) ? sanitize_email( $args['email'] ) : '' );
285
286 if( ! isset( $args['forumid'] ) || ! $args['forumid'] = intval( $args['forumid'] ) ) {
287 WPF()->notice->add( 'Add Topic error: No forum selected', 'error' );
288
289 return false;
290 }
291
292 if( ! $forum = WPF()->forum->get_forum( $args['forumid'] ) ) {
293 WPF()->notice->add( 'Add Topic error: No forum selected', 'error' );
294
295 return false;
296 }
297
298 if( ! WPF()->perm->forum_can( 'ct', $args['forumid'] ) ) {
299 WPF()->notice->add( 'You don\'t have permission to create topic into this forum', 'error' );
300
301 return false;
302 }
303
304 // Skip flood check for AI-generated content (created by AI Tasks)
305 if ( empty( $args['is_ai_generated'] ) ) {
306 $flood_reason = '';
307 $flood_result = WPF()->perm->can_post_now( $flood_reason );
308 if( $flood_result === false ) {
309 $message = $this->get_flood_error_message( $flood_reason );
310 WPF()->notice->add( $message, 'error' );
311 return false;
312 }
313 // If flood action is 'unapprove', force the topic to be unapproved
314 if( $flood_result === 'unapprove' ) {
315 $args['status'] = 1;
316 $args['_flood_reason'] = $flood_reason; // Pass flood reason for moderation logging
317 }
318 }
319
320 if( ! isset( $args['title'] ) || ! $args['title'] = wpforo_text(
321 $args['title'],
322 250,
323 false,
324 true,
325 false,
326 false,
327 false
328 ) ) {
329 WPF()->notice->add( 'Please insert required fields!', 'error' );
330
331 return false;
332 }
333
334 if( ! is_user_logged_in() ) {
335 if( $args['name'] && $args['email'] ) {
336 WPF()->member->set_guest_cookies( $args );
337 } else {
338 $uqid = uniqid();
339 if( ! trim( (string) $args['name'] ) ) $args['name'] = wpforo_phrase( 'Anonymous', false );
340 if( ! trim( (string) $args['email'] ) ) $args['email'] = "anonymous_$uqid@example.com";
341 }
342 }
343
344 do_action( 'wpforo_start_add_topic', $args, $forum );
345
346 $root_exists = wpforo_root_exist();
347 $args['body'] = preg_replace( '#</pre>[\r\n\t\s\0]*<pre>#isu', "\r\n", (string) $args['body'] );
348 $args['slug'] = ( isset( $args['slug'] ) && $args['slug'] ) ? sanitize_title(
349 $args['slug']
350 ) : ( ( isset( $args['title'] ) ) ? sanitize_title( $args['title'] ) : md5( time() ) );
351 if( ! trim( (string) $args['slug'] ) ) $args['slug'] = md5( time() );
352 $args['slug'] = $this->unique_slug( $args['slug'] );
353 $args['created'] = ( isset( $args['created'] ) ? sanitize_text_field( $args['created'] ) : current_time(
354 'mysql',
355 1
356 ) );
357 $args['userid'] = ( isset( $args['userid'] ) && wpforo_current_user_is( 'moderator' ) ? intval( $args['userid'] ) : WPF()->current_userid );
358 $args['name'] = ( isset( $args['name'] ) ? $args['name'] : '' );
359 $args['email'] = ( isset( $args['email'] ) ? $args['email'] : '' );
360 $args['tags'] = ( isset( $args['tags'] ) ? $args['tags'] : '' );
361
362 $args = apply_filters( 'wpforo_add_topic_data_filter', $args, $forum );
363
364 if( empty( $args ) ) return false;
365
366 // Security: extract only expected keys to prevent variable injection
367 $title = isset( $args['title'] ) ? $args['title'] : null;
368 $slug = isset( $args['slug'] ) ? $args['slug'] : null;
369 $created = isset( $args['created'] ) ? $args['created'] : null;
370 $userid = isset( $args['userid'] ) ? $args['userid'] : null;
371 $name = isset( $args['name'] ) ? $args['name'] : null;
372 $email = isset( $args['email'] ) ? $args['email'] : null;
373 $body = isset( $args['body'] ) ? $args['body'] : null;
374 $tags = isset( $args['tags'] ) ? $args['tags'] : null;
375 $type = isset( $args['type'] ) ? $args['type'] : null;
376 $status = isset( $args['status'] ) ? $args['status'] : null;
377 $private = isset( $args['private'] ) ? $args['private'] : null;
378 $meta_key = isset( $args['meta_key'] ) ? $args['meta_key'] : null;
379 $meta_desc = isset( $args['meta_desc'] ) ? $args['meta_desc'] : null;
380 $views = isset( $args['views'] ) ? $args['views'] : null;
381 $has_attach = isset( $args['has_attach'] ) ? $args['has_attach'] : null;
382
383 if( isset( $title ) ) $title = sanitize_text_field( trim( (string) $title ) );
384 if( isset( $created ) ) $created = sanitize_text_field( $created );
385 if( isset( $userid ) ) $userid = intval( $userid );
386 $type = ( isset( $type ) && $type ? 1 : 0 );
387 $status = ( isset( $status ) && $status ? 1 : 0 );
388 $private = ( isset( $private ) && $private ? 1 : 0 );
389 // Force private for ticket_forum type forums (all topics must be private)
390 if( wpfval( $forum, 'type' ) === 'ticket_forum' ) {
391 $private = 1;
392 }
393 if( isset( $meta_key ) ) $meta_key = sanitize_text_field( $meta_key );
394 if( isset( $meta_desc ) ) $meta_desc = sanitize_text_field( $meta_desc );
395 if( isset( $name ) ) $name = strip_tags( trim( (string) $name ) );
396 if( isset( $email ) ) $email = strip_tags( trim( (string) $email ) );
397 if( isset( $body ) ) $body = wpforo_kses( trim( (string) $body ) );
398 if( isset( $tags ) ) {
399 if( wpforo_is_module_enabled( 'tags' ) && WPF()->perm->forum_can( 'tag', $forum['forumid'] ) ) {
400 $tags = $this->sanitize_tags( $tags, false, true );
401 } else {
402 $tags = '';
403 }
404 }
405 $views = ( isset( $views ) ? intval( $views ) : 0 );
406 $meta_key = ( isset( $meta_key ) ? $meta_key : '' );
407 $meta_desc = ( isset( $meta_desc ) ? $meta_desc : '' );
408 $has_attach = ( isset( $has_attach ) && $has_attach ) ? 1 : ( ( strpos(
409 (string) $body,
410 '[attach]'
411 ) !== false ) ? 1 : 0 );
412 $layout = WPF()->forum->get_layout( $forum );
413 $posts = ( $layout == 3 ) ? 0 : 1;
414 do_action( 'wpforo_before_add_topic', $args );
415
416 if( WPF()->db->insert(
417 WPF()->tables->topics,
418 [
419 'title' => stripslashes(
420 $title
421 ),
422 'slug' => $slug,
423 'forumid' => $forum['forumid'],
424 'userid' => $userid,
425 'type' => $type,
426 'status' => $status,
427 'private' => $private,
428 'created' => $created,
429 'modified' => $created,
430 'last_post' => 0,
431 'views' => $views,
432 'posts' => $posts,
433 'meta_key' => $meta_key,
434 'meta_desc' => $meta_desc,
435 'has_attach' => $has_attach,
436 'name' => $name,
437 'email' => $email,
438 'tags' => $tags,
439 ],
440 [
441 '%s',
442 '%s',
443 '%d',
444 '%d',
445 '%d',
446 '%d',
447 '%d',
448 '%s',
449 '%s',
450 '%d',
451 '%d',
452 '%d',
453 '%s',
454 '%s',
455 '%d',
456 '%s',
457 '%s',
458 '%s',
459 ]
460 ) ) {
461 $topicid = WPF()->db->insert_id;
462 $fields = [
463 'forumid' => $forum['forumid'],
464 'topicid' => $topicid,
465 'userid' => $userid,
466 'title' => stripslashes( $title ),
467 'body' => stripslashes( (string) $body ),
468 'created' => $created,
469 'modified' => $created,
470 'is_first_post' => 1,
471 'status' => $status,
472 'private' => $private,
473 'name' => $name,
474 'email' => $email,
475 'root' => - 1,
476 ];
477
478 $values = [ '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%d' ];
479
480 if( ! $root_exists ) {
481 unset( $fields['root'] );
482 unset( $fields[13] );
483 }
484
485 if( WPF()->db->insert(
486 WPF()->tables->posts,
487 $fields,
488 $values
489 ) ) {
490 $first_postid = WPF()->db->insert_id;
491 if( false !== WPF()->db->update(
492 WPF()->tables->topics,
493 [ 'first_postid' => $first_postid, 'last_post' => $first_postid ],
494 [ 'topicid' => $topicid ],
495 [ '%d', '%d' ], [ '%d' ]
496 ) ) {
497 $args['topicid'] = $topicid;
498 $args['first_postid'] = $first_postid;
499 $args['type'] = $type;
500 $args['status'] = $status;
501 $args['private'] = $private;
502 $args['url'] = $args['topicurl'] = $this->get_url( $topicid );
503 if( $tags && ! $status && ! $private ) $this->add_tags( $tags );
504
505 $topic = apply_filters( 'wpforo_after_add_topic_filter', $args, $forum );
506 do_action( 'wpforo_after_add_topic', $topic, $forum );
507
508 // Set guest ownership cookie for secure edit verification.
509 // Uses the local $userid/$email actually written to the post row.
510 if( ! $userid && $email ) {
511 wpforo_add_guest_ownership( $first_postid );
512 }
513
514 wpforo_clean_cache( 'topic', $topicid, $topic );
515 if( $status ) {
516 WPF()->notice->add( 'Your topic successfully added and awaiting moderation', 'success' );
517 } else {
518 WPF()->notice->add( 'Your topic successfully added', 'success' );
519 }
520
521 return $topicid;
522 }
523 }
524
525 }
526
527 WPF()->notice->add( 'Topic add error', 'error' );
528
529 return false;
530 }
531
532 #################################################################################
533
534 private function unique_slug( $slug ) {
535 $new_slug = wpforo_text( $slug, 250, false );
536 $i = 2;
537 while( ! WPF()->can_use_this_slug( $new_slug ) || WPF()->db->get_var(
538 "SELECT `topicid` FROM " . WPF()->tables->topics . " WHERE `slug` = '" . esc_sql( $new_slug ) . "'"
539 ) ) {
540 $new_slug = wpforo_text( $slug, 250, false ) . '-' . $i;
541 $i ++;
542 }
543
544 return $new_slug;
545 }
546
547 #################################################################################
548
549 public function sanitize_tags( $tags, $array = false, $limit = false ) {
550 if( $tags ) {
551 if( ! is_array( $tags ) ) {
552 $tag_separator = ( strpos( $tags, '،' ) !== false ) ? '،' : ',';
553 $tag_separator = apply_filters( 'wpforo_tag_separator', $tag_separator );
554 $tags = wp_unslash( $tags );
555 //Replace other tag separators to regular comma to store in the database
556 $tags = str_replace( $tag_separator, ',', $tags );
557 $tags = explode( $tag_separator, $tags );
558 }
559 if( wpforo_setting( 'tags', 'lowercase' ) ) {
560 if( function_exists( 'mb_strtolower' ) ) {
561 $tags = array_map( 'mb_strtolower', $tags );
562 } else {
563 $tags = array_map( 'strtolower', $tags );
564 }
565 }
566 $length = wpforo_setting( 'tags', 'length' );
567 $mb_substr = function_exists( 'mb_substr' );
568 foreach( $tags as $key => $tag ) {
569 if( $mb_substr ) {
570 $tags[ $key ] = mb_substr( (string) $tag, 0, $length );
571 } else {
572 $tags[ $key ] = substr( (string) $tag, 0, $length );
573 }
574 }
575 $tags = array_map( 'trim', $tags );
576 $tags = array_map( 'sanitize_text_field', $tags );
577 $tags = array_filter( $tags );
578 $tags = array_unique( $tags );
579 if( $limit ) {
580 $tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
581 }
582 if( $array ) {
583 return $tags;
584 } else {
585 // We keep using "," in the database even if the separator is different
586 return implode( ',', $tags );
587 }
588 }
589
590 if( $array ) {
591 return [];
592 } else {
593 return '';
594 }
595 }
596
597 /**
598 * @param $topic
599 * @param $forum
600 * @param $_cache
601 *
602 * @return string
603 */
604 public function get_url( $topic, $forum = [], $_cache = true ) {
605 return (string) wpforo_ram_get( [ $this, '_get_url' ], $topic, $forum, $_cache );
606 }
607
608 public function add_tags( $tags ) {
609 if( $tags ) {
610 $tags = $this->sanitize_tags( $tags, true );
611 if( ! empty( $tags ) ) {
612 $tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
613 foreach( $tags as $tag ) {
614 $count = (int) WPF()->db->get_var(
615 "SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
616 );
617 if( $count ) {
618 WPF()->db->update(
619 WPF()->tables->tags,
620 [ 'count' => $count + 1 ],
621 [ 'tag' => $tag ],
622 [ '%d' ], [ '%s' ]
623 );
624 } else {
625 $this->add_tag( $tag, 1 );
626 }
627 wpforo_clean_cache( 'tag', $tag );
628 }
629 wpforo_clean_cache( 'tag' );
630 }
631 }
632 }
633
634 public function add_tag( $tag, $count = 0 ) {
635 $tagid = 0;
636 if( $tag ) {
637 $tag = $this->sanitize_tag( $tag );
638 if( ! WPF()->db->get_var(
639 "SELECT `tagid` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
640 ) ) {
641 $tagid = WPF()->db->insert(
642 WPF()->tables->tags,
643 [ 'tag' => $tag, 'prefix' => 0, 'count' => intval( $count ) ], [ '%s', '%d', '%d' ]
644 );
645 }
646 }
647
648 return $tagid;
649 }
650
651 public function sanitize_tag( $tag ) {
652 $tag = trim( (string) $tag );
653 $tag = wp_unslash( $tag );
654 $tag = sanitize_text_field( $tag );
655 $length = wpforo_setting( 'tags', 'length' );
656 $tag = ( function_exists( 'mb_substr' ) ) ? mb_substr( (string) $tag, 0, $length ) : substr(
657 (string) $tag,
658 0,
659 $length
660 );
661 $lowcase = wpforo_setting( 'tags', 'lowercase' );
662 if( $lowcase ) {
663 $tag = ( function_exists( 'mb_strtolower' ) ) ? mb_strtolower( (string) $tag ) : strtolower(
664 (string) $tag
665 );
666 }
667
668 return $tag;
669 }
670
671 public function edit_tags( $tags, $topic = [] ) {
672 $old_tags = ( wpfval( $topic, 'tags' ) ) ? $this->sanitize_tags( $topic['tags'], true ) : false;
673 if( $tags ) {
674 $tags = $this->sanitize_tags( $tags, true );
675 $tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
676 if( ! empty( $tags ) ) {
677 if( wpfval( $topic, 'topicid' ) ) {
678 if( ! empty( $old_tags ) ) {
679 foreach( $old_tags as $old_tag ) {
680 if( ! in_array( $old_tag, $tags ) ) {
681 $this->remove_tags( $old_tag );
682 }
683 }
684 }
685 }
686 if( ! wpfval( $topic, 'status' ) && ! wpfval( $topic, 'private' ) ) {
687 foreach( $tags as $tag ) {
688 $count = (int) WPF()->db->get_var(
689 "SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
690 );
691 if( ! $count ) {
692 WPF()->db->insert(
693 WPF()->tables->tags,
694 [ 'tag' => $tag, 'prefix' => 0, 'count' => 1 ], [ '%s', '%d', '%d' ]
695 );
696 } elseif( empty( $old_tags ) || ! in_array( $tag, $old_tags ) ) {
697 WPF()->db->update(
698 WPF()->tables->tags,
699 [ 'count' => ( $count + 1 ) ],
700 [ 'tag' => $tag ],
701 [ '%d' ], [ '%s' ]
702 );
703 }
704 wpforo_clean_cache( 'tag', $tag );
705 }
706 }
707 }
708 } else {
709 if( ! empty( $old_tags ) && wpfval( $topic, 'topicid' ) ) {
710 WPF()->db->update(
711 WPF()->tables->topics,
712 [ 'tags' => '' ],
713 [ 'topicid' => $topic['topicid'] ],
714 [ '%s' ], [ '%d' ]
715 );
716 $this->remove_tags( $old_tags );
717 }
718 }
719 wpforo_clean_cache( 'tag' );
720 }
721
722 public function remove_tags( $tags ) {
723 if( $tags ) {
724 $tags = $this->sanitize_tags( $tags, true );
725 foreach( $tags as $tag ) {
726 $count = (int) WPF()->db->get_var(
727 "SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
728 );
729 if( $count === 1 ) {
730 WPF()->db->query(
731 "DELETE FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
732 );
733 } else {
734 WPF()->db->update(
735 WPF()->tables->tags,
736 [ 'count' => ( $count - 1 ) ],
737 [ 'tag' => $tag ],
738 [ '%d' ], [ '%s' ]
739 );
740 }
741 wpforo_clean_cache( 'tag', $tag );
742 }
743 }
744 wpforo_clean_cache( 'tag' );
745 }
746
747 /**
748 * array get_topic(array or id(num))
749 *
750 * Returns array from defined and default arguments.
751 *
752 * @param mixed $args defined arguments array for returning
753 * @param bool $protect
754 *
755 * @return array
756 * @since 1.0.0
757 *
758 */
759
760 public function _get_topic( $args = [], $protect = true ) {
761 if( ! $args ) return [];
762
763 if( is_array( $args ) ) {
764 $default = [
765 'topicid' => null,
766 'slug' => '',
767 ];
768 } elseif( wpforo_is_id( $args ) ) {
769 $default = [
770 'topicid' => $args,
771 'slug' => '',
772 ];
773 } else {
774 $default = [
775 'topicid' => null,
776 'slug' => $args,
777 ];
778 }
779
780 $args = wpforo_parse_args( $args, $default );
781
782 $sql = "SELECT * FROM `" . WPF()->tables->topics . "`";
783 $wheres = [];
784 if( $topicid = wpforo_bigintval( $args['topicid'] ) ) $wheres[] = "`topicid` = " . $topicid;
785 if( $args['slug'] ) $wheres[] = "`slug` = '" . esc_sql( $args['slug'] ) . "'";
786 if( ! empty( $wheres ) ) $sql .= " WHERE " . implode( " AND ", $wheres );
787
788 $topic = (array) WPF()->db->get_row( $sql, ARRAY_A );
789
790 if( $protect ) {
791 if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
792 'vf',
793 $topic['forumid']
794 ) ) {
795 return [];
796 }
797 if( isset( $topic['private'] ) && $topic['private'] && ! wpforo_is_owner(
798 $topic['userid'],
799 $topic['email']
800 ) ) {
801 if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
802 'vp',
803 $topic['forumid']
804 ) ) {
805 return [];
806 }
807 }
808 if( isset( $topic['status'] ) && $topic['status'] && ! wpforo_is_owner(
809 $topic['userid'],
810 $topic['email']
811 ) ) {
812 if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
813 'au',
814 $topic['forumid']
815 ) ) {
816 WPF()->current_object['status'] = 'unapproved';
817
818 return [];
819 }
820 }
821 }
822
823 if( $topic ) {
824 $topic['url'] = $this->get_url( $topic, [], false );
825 $topic['full_url'] = $this->get_full_url( $topic, [], false );
826 $topic['short_url'] = $this->get_short_url( $topic );
827 }
828
829 return apply_filters( 'wpforo_get_topic', $topic );
830 }
831
832 public function get_full_url( $topic, $forum = [], $_cache = true ): string {
833 if( ! is_array( $topic ) ) $topic = $this->get_topic( $topic, false );
834 if( $topic ) {
835 $cache = WPF()->cache->on( 'url' );
836 // $_cache is used to stop caching on merge and move actions,
837 // otherwise the merging and splitting go to redirection loop
838 if( $_cache && $cache && wpfval( $topic, 'topicid' ) ) {
839 $topic_url = (string) WPF()->cache->get_item( $topic['topicid'], 'url', 'topic' );
840 if( $topic_url ) return $topic_url;
841 }
842 if( is_array( $forum ) && ! empty( $forum ) ) {
843 $forum_slug = $forum['slug'];
844 } else {
845 $forum_slug = '';
846 if( wpfval( $topic, 'forumid' ) ) {
847 $forum_slug = $this->get_forumslug( $topic['forumid'] );
848 } elseif( wpfval( $topic, 'topicid' ) ) {
849 $forum_slug = $this->get_forumslug_byid( $topic['topicid'] );
850 }
851 }
852 if( wpfval( $topic, 'slug' ) ) {
853 $topic_url = wpforo_home_url( $forum_slug . '/' . $topic['slug'] );
854 if( $cache ) {
855 WPF()->cache->create(
856 'item',
857 [ 'topic_' . intval( $topic['topicid'] ) => $topic_url ],
858 'url'
859 );
860 }
861
862 return $topic_url;
863 }
864 }
865
866 return wpforo_home_url();
867 }
868
869 function get_forumslug( $forumid ) {
870 $slug = WPF()->db->get_var(
871 "SELECT `slug` FROM " . WPF()->tables->forums . " WHERE `forumid` = " . intval( $forumid )
872 );
873 if( $slug ) return $slug;
874
875 return 0;
876 }
877
878 function get_forumslug_byid( $topicid ) {
879 $slug = WPF()->db->get_var(
880 "SELECT `slug` FROM " . WPF()->tables->forums . " WHERE `forumid` =(SELECT forumid FROM `" . WPF()->tables->topics . "` WHERE `topicid` =" . intval( $topicid ) . ")"
881 );
882 if( $slug ) return $slug;
883
884 return 0;
885 }
886
887 public function get_short_url( $arg ) {
888 if( wpforo_is_id( $arg ) ) {
889 $topicid = $arg;
890 } elseif( wpfkey( $arg, 'topicid' ) ) {
891 $topicid = $arg['topicid'];
892 } else {
893 $topicid = 0;
894 }
895
896 if( $topicid = wpforo_bigintval( $topicid ) ) {
897 return wpforo_home_url(
898 '/' . wpforo_settings_get_slug( 'topicid' ) . '/' . $topicid . '/'
899 );
900 }
901
902 return wpforo_home_url();
903 }
904
905 /**
906 * Search in your chosen column and return array with needles
907 *
908 * @param string $needle
909 *
910 * @param array $fields can be ( slug, title, body )
911 *
912 * @return array with matches
913 * @since 1.0.0
914 *
915 */
916 function search( $needle = '', $fields = [ 'title', 'body' ] ) {
917 if( $needle !== '' ) {
918 $needle = stripslashes( (string) $needle );
919 $fields = (array) $fields;
920
921 $topicids = [];
922 foreach( $fields as $field ) {
923 $matches = [];
924 if( $field === 'body' ) {
925 // Search the needle as a whole phrase in body to find closer result
926 $matches = WPF()->db->get_col(
927 "SELECT `topicid` FROM " . WPF()->tables->posts . " WHERE `" . esc_sql(
928 $field
929 ) . "` LIKE '%" . esc_sql( sanitize_text_field( $needle ) ) . "%'"
930 );
931 } else {
932 // Search all words of the needle independently in title and other fields
933 // If the search phrase is "es lorem du ipsum dolor", then SQL becomes
934 // SELECT * FROM `wp_wpforo_topics` WHERE `title` REGEXP 'lorem|ipsum|dolor'
935 $words = array_filter(
936 explode( ' ', sanitize_text_field( $needle ) ),
937 function( $word ) { return mb_strlen( (string) $word ) > 2; }
938 );
939 if( $words ) {
940 if( apply_filters( 'wpforo_suggested_topics_search_fields_method_regexp', false ) ) {
941 $words = array_map(
942 function( $word ) {
943 return str_replace( [ '"', "'" ],
944 [ '\"', "\'" ],
945 preg_quote( preg_quote( $word ) ) );
946 },
947 $words
948 );
949 $search_mode_sql = '`' . esc_sql( $field ) . '` REGEXP "' . implode( '|', $words ) . '"';
950 } else {
951 $wheres = array_map( function( $word ) use ( $field ) {
952 return sprintf(
953 '`%1$s` LIKE \'%%%2$s%%\'',
954 esc_sql( $field ),
955 esc_sql( $word )
956 );
957 }, $words );
958 $search_mode_sql = implode( ' OR ', $wheres );
959 }
960
961 $sql = "SELECT `topicid` FROM " . WPF()->tables->topics . " WHERE " . $search_mode_sql;
962 $matches = WPF()->db->get_col( $sql );
963 }
964
965 }
966 $topicids = array_merge( $topicids, $matches );
967 }
968
969 return array_unique( array_map( 'wpforo_bigintval', $topicids ) );
970 }
971
972 return [];
973 }
974
975 function get_sum_answer( $forumids ) {
976 $sum = WPF()->db->get_var(
977 "SELECT SUM(`answers`) FROM `" . WPF()->tables->topics . "` WHERE `forumid` IN(" . implode(
978 ', ',
979 array_map( 'intval', $forumids )
980 ) . ")"
981 );
982 if( $sum ) return $sum;
983
984 return 0;
985 }
986
987 function is_sticky( $topicid ) {
988 if( $topicid ) {
989 if( WPF()->cache->on() ) {
990 $is_sticky = wpforo_topic( $topicid, 'type' );
991 } else {
992 $sql = "SELECT `type` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
993 $is_sticky = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
994 }
995
996 return (bool) $is_sticky;
997 }
998
999 return false;
1000 }
1001
1002 function is_private( $topicid ) {
1003 if( $topicid ) {
1004 if( WPF()->cache->on() ) {
1005 $private = wpforo_topic( $topicid, 'private' );
1006 } else {
1007 $sql = "SELECT `private` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
1008 $private = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
1009 }
1010
1011 return (bool) $private;
1012 }
1013
1014 return false;
1015 }
1016
1017 function is_unapproved( $topicid ) {
1018 if( WPF()->cache->on() ) {
1019 $status = wpforo_topic( $topicid, 'status' );
1020 } else {
1021 $status = WPF()->db->get_var(
1022 "SELECT `status` FROM " . WPF()->tables->topics . " WHERE `topicid` = " . intval( $topicid )
1023 );
1024 }
1025 if( $status == 1 ) return true;
1026
1027 return false;
1028 }
1029
1030 function is_closed( $topicid ) {
1031 if( $topicid ) {
1032 if( WPF()->cache->on() ) {
1033 $closed = wpforo_topic( $topicid, 'closed' );
1034 } else {
1035 $sql = "SELECT `closed` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
1036 $closed = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
1037 }
1038
1039 return (bool) $closed;
1040 }
1041
1042 return false;
1043 }
1044
1045 function is_solved( $topicid ) {
1046 if( $topicid ) {
1047 $sql = "SELECT `solved` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
1048 $solved = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
1049
1050 return (bool) $solved;
1051 }
1052
1053 return false;
1054 }
1055
1056 public function close( $topicid ) {
1057 $topicid = wpforo_bigintval( $topicid );
1058 $sql = "UPDATE " . WPF()->tables->topics . " SET closed = 1 WHERE topicid = %d";
1059 WPF()->db->query( WPF()->db->prepare( $sql, $topicid ) );
1060 wpforo_clean_cache( 'topic-first-post', $topicid );
1061 }
1062
1063 public function open( $topicid ) {
1064 $topicid = wpforo_bigintval( $topicid );
1065 $sql = "UPDATE " . WPF()->tables->topics . " SET closed = 0 WHERE topicid = %d";
1066 WPF()->db->query( WPF()->db->prepare( $sql, $topicid ) );
1067 wpforo_clean_cache( 'topic-first-post', $topicid );
1068 }
1069
1070 /**
1071 * Move topic to another forum
1072 *
1073 * @param int $topicid
1074 * @param int $forumid
1075 *
1076 * @return int|false $topicid on success, otherwise false
1077 * @since 1.0.0
1078 *
1079 */
1080 function move( $topicid, $forumid ) {
1081 $topic = $this->get_topic( $topicid );
1082 if( WPF()->db->query(
1083 "UPDATE `" . WPF()->tables->topics . "` SET `forumid` = " . intval(
1084 $forumid
1085 ) . " WHERE `topicid` = " . intval( $topicid )
1086 ) ) {
1087 WPF()->db->query(
1088 "UPDATE `" . WPF()->tables->posts . "` SET `forumid` = " . intval(
1089 $forumid
1090 ) . " WHERE `topicid` = " . intval( $topicid )
1091 );
1092
1093 do_action( 'wpforo_after_move_topic', $topic, $forumid );
1094
1095 wpforo_clean_cache( 'topic', $topicid, $topic );
1096 WPF()->notice->add( 'Done!', 'success' );
1097
1098 return $topicid;
1099 }
1100
1101 WPF()->notice->add( 'Topic Move Error', 'error' );
1102
1103 return false;
1104 }
1105
1106 public function split( $args, $to_target_title = 0 ) {
1107 if( ! $args ) return false;
1108
1109 $args['name'] = ( isset( $args['name'] ) ? strip_tags( (string) $args['name'] ) : '' );
1110 $args['email'] = ( isset( $args['email'] ) ? sanitize_email( $args['email'] ) : '' );
1111
1112 if( ! isset( $args['forumid'] ) || ! $args['forumid'] = intval( $args['forumid'] ) ) {
1113 WPF()->notice->add( 'Please select a target forum', 'error' );
1114
1115 return false;
1116 }
1117
1118 if( ! isset( $args['title'] ) || ! $args['title'] = trim( strip_tags( (string) $args['title'] ) ) ) {
1119 WPF()->notice->add( 'Please insert required fields', 'error' );
1120
1121 return false;
1122 }
1123
1124 if( empty( $args['postids'] ) ) {
1125 WPF()->notice->add( 'Please select at least one post to split', 'error' );
1126
1127 return false;
1128 }
1129
1130 $args['postids'] = array_values( $args['postids'] );
1131
1132 if( $fpost = WPF()->post->get_post( $args['postids'][0] ) ) {
1133 $args['title'] = wpforo_text( $args['title'], 250, false );
1134 $args['slug'] = ( isset( $args['slug'] ) && $args['slug'] ) ? sanitize_title(
1135 $args['slug']
1136 ) : ( ( isset( $args['title'] ) ) ? sanitize_title( $args['title'] ) : md5( time() ) );
1137 if( ! trim( $args['slug'] ) ) $args['slug'] = md5( time() );
1138 $args['slug'] = $this->unique_slug( $args['slug'] );
1139
1140
1141 $args['body'] = $fpost['body'];
1142 $args['created'] = $fpost['created'];
1143 $args['userid'] = $fpost['userid'];
1144 $args['name'] = $fpost['name'];
1145 $args['email'] = $fpost['email'];
1146
1147 // Security: extract only expected keys to prevent variable injection
1148 $forumid = isset( $args['forumid'] ) ? $args['forumid'] : null;
1149 $title = isset( $args['title'] ) ? $args['title'] : null;
1150 $slug = isset( $args['slug'] ) ? $args['slug'] : null;
1151 $created = isset( $args['created'] ) ? $args['created'] : null;
1152 $userid = isset( $args['userid'] ) ? $args['userid'] : null;
1153 $body = isset( $args['body'] ) ? $args['body'] : null;
1154 $name = isset( $args['name'] ) ? $args['name'] : null;
1155 $email = isset( $args['email'] ) ? $args['email'] : null;
1156 $type = isset( $args['type'] ) ? $args['type'] : null;
1157 $status = isset( $args['status'] ) ? $args['status'] : null;
1158 $private = isset( $args['private'] ) ? $args['private'] : null;
1159 $has_attach = isset( $args['has_attach'] ) ? $args['has_attach'] : null;
1160
1161 if( isset( $forumid ) ) $forumid = intval( $forumid );
1162 if( isset( $title ) ) $title = sanitize_text_field( trim( (string) $title ) );
1163 if( isset( $slug ) ) $slug = sanitize_title( $slug );
1164 if( isset( $created ) ) $created = sanitize_text_field( $created );
1165 if( isset( $userid ) ) $userid = intval( $userid );
1166 $type = ( isset( $type ) && $type ? 1 : 0 );
1167 $status = ( isset( $status ) && $status ? 1 : 0 );
1168 $private = ( isset( $private ) && $private ? 1 : 0 );
1169 if( isset( $name ) ) $name = strip_tags( trim( (string) $name ) );
1170 if( isset( $email ) ) $email = strip_tags( trim( (string) $email ) );
1171 $has_attach = ( isset( $has_attach ) && $has_attach ) ? 1 : ( ( strpos(
1172 (string) $body,
1173 '[attach]'
1174 ) !== false ) ? 1 : 0 );
1175
1176 if( WPF()->db->insert(
1177 WPF()->tables->topics,
1178 [
1179 'title' => stripslashes(
1180 $title
1181 ),
1182 'slug' => $slug,
1183 'forumid' => $forumid,
1184 'userid' => $userid,
1185 'type' => $type,
1186 'status' => $status,
1187 'private' => $private,
1188 'created' => $created,
1189 'modified' => $created,
1190 'last_post' => 0,
1191 'views' => 0,
1192 'posts' => 1,
1193 'has_attach' => $has_attach,
1194 'name' => $name,
1195 'email' => $email,
1196 ],
1197 [
1198 '%s',
1199 '%s',
1200 '%d',
1201 '%d',
1202 '%d',
1203 '%d',
1204 '%d',
1205 '%s',
1206 '%s',
1207 '%d',
1208 '%d',
1209 '%d',
1210 '%d',
1211 '%s',
1212 '%s',
1213 ]
1214 ) ) {
1215 $args['topicid'] = $topicid = WPF()->db->insert_id;
1216
1217 if( $this->merge( $args, WPF()->current_object['topic'], $args['postids'], $to_target_title ) ) {
1218 WPF()->notice->clear();
1219 WPF()->notice->add( 'Done!', 'success' );
1220
1221 return $topicid;
1222 }
1223 }
1224 }
1225
1226 WPF()->notice->add( 'Topic splitting error', 'error' );
1227
1228 return false;
1229 }
1230
1231 /**
1232 * merge topic with target topic
1233 *
1234 * @param array $target target topic array
1235 * @param array $current current topic array
1236 * @param array $postids current topic postids array
1237 * @param int $to_target_title Update post titles with target topic title
1238 * @param int $append Update post dates and append to end
1239 *
1240 * @return bool true|false true on success, otherwise false
1241 */
1242 public function merge( $target, $current = [], $postids = [], $to_target_title = 0, $append = 0 ) {
1243
1244 if( ! $current ) $current = WPF()->current_object['topic'];
1245
1246 $sql = "UPDATE `" . WPF()->tables->posts . "` SET `topicid` = %d, `forumid` = %d, `private` = %d,`is_first_post` = 0";
1247 $sql = WPF()->db->prepare( $sql, $target['topicid'], $target['forumid'], (int) wpfval( $target, 'private' ) );
1248
1249 if( $append ) {
1250 $sql .= ", `modified` = %s, `created` = %s";
1251 $sql = WPF()->db->prepare( $sql, current_time( 'mysql', 1 ), current_time( 'mysql', 1 ) );
1252 }
1253
1254 if( $to_target_title ) {
1255 $layout = WPF()->forum->get_layout( $target['forumid'] );
1256 $phrase = ( $layout == 3 ? wpforo_phrase( 'Answer to', false ) : wpforo_phrase( 'RE', false ) );
1257 $title = $phrase . ': ' . $target['title'];
1258 $sql .= ", `title` = %s";
1259 $sql = WPF()->db->prepare( $sql, $title );
1260 }
1261
1262 $sql .= " WHERE `topicid` = %d";
1263 $sql = WPF()->db->prepare( $sql, $current['topicid'] );
1264
1265 if( $postids ) {
1266 $postids = (array) $postids;
1267 $postids = array_map( 'wpforo_bigintval', $postids );
1268
1269 $sql .= " AND `postid` IN(" . implode( ',', $postids ) . ")";
1270 }
1271
1272 do_action( 'wpforo_before_merge_topic', $target, $current, $postids, $to_target_title, $append );
1273
1274 $db_resp = WPF()->db->query( $sql );
1275
1276 if( $db_resp !== false ) {
1277 $sql = "SELECT COUNT(*) FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d";
1278 $sql = WPF()->db->prepare( $sql, $current['topicid'] );
1279 if( ! WPF()->db->get_var( $sql ) ) {
1280 $this->delete( $current['topicid'], true, false );
1281 } else {
1282 $this->rebuild_first_last( $current );
1283 $this->rebuild_stats( $current );
1284 $this->rebuild_threads( $current );
1285 wpforo_clean_cache( 'topic', $current['topicid'], $current );
1286 }
1287
1288 $this->rebuild_first_last( $target );
1289 $this->rebuild_stats( $target );
1290 $this->rebuild_threads( $target );
1291
1292 do_action( 'wpforo_after_merge_topic', $target, $current, $postids, $to_target_title, $append );
1293
1294 WPF()->notice->clear();
1295 WPF()->notice->add( 'Done!', 'success' );
1296
1297 wpforo_clean_cache();
1298
1299 return true;
1300 }
1301
1302 WPF()->notice->add( 'Data merging error', 'error' );
1303
1304 return false;
1305 }
1306
1307 /**
1308 * Delete topic from DB
1309 *
1310 * Returns true if successfully deleted or false.
1311 *
1312 * @param int $topicid
1313 * @param bool $delete_cache
1314 *
1315 * @return bool
1316 * @since 1.0.0
1317 *
1318 */
1319 function delete( $topicid = 0, $delete_cache = true, $check_permissions = true ) {
1320 $topicid = intval( $topicid );
1321 if( ! $topicid && isset( $_REQUEST['id'] ) ) $topicid = intval( $_REQUEST['id'] );
1322
1323 if( ! $topic = $this->get_topic( $topicid ) ) return true;
1324
1325 do_action( 'wpforo_before_delete_topic', $topic );
1326
1327 if( $check_permissions ) {
1328 $diff = time() - strtotime( $topic['created'] . ' GMT' );
1329 if( ! ( WPF()->perm->forum_can( 'dt', $topic['forumid'] ) || ( WPF()->current_userid == $topic['userid'] && WPF()->perm->forum_can(
1330 'dot',
1331 $topic['forumid']
1332 ) ) ) ) {
1333 WPF()->notice->add( 'You don\'t have permission to delete topic from this forum.', 'error' );
1334
1335 return false;
1336 }
1337
1338 if( ! WPF()->perm->forum_can( 'dt', $topic['forumid'] ) && wpforo_setting(
1339 'posting',
1340 'delete_own_topic_durr'
1341 ) !== 0 && $diff > wpforo_setting(
1342 'posting',
1343 'delete_own_topic_durr'
1344 ) ) {
1345 WPF()->notice->add( 'The time to delete this topic is expired.', 'error' );
1346
1347 return false;
1348 }
1349 }
1350
1351 // START delete topic posts include first post
1352 if( $postids = WPF()->db->get_col(
1353 WPF()->db->prepare(
1354 "SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d ORDER BY `is_first_post`",
1355 $topicid
1356 )
1357 ) ) {
1358 $exclude = [];
1359 foreach( $postids as $postid ) {
1360 if( $postid == $topic['first_postid'] ) {
1361 return WPF()->post->delete( $postid, false, false, $exclude, false );
1362 } else {
1363 WPF()->post->delete( $postid, false, false, $exclude, false );
1364 }
1365 }
1366 }
1367 // END delete topic posts include first post
1368
1369 if( WPF()->db->delete( WPF()->tables->topics, [ 'topicid' => $topicid ], [ '%d' ] ) ) {
1370 if( wpfval( $topic, 'tags' ) ) $this->remove_tags( $topic['tags'] );
1371
1372 do_action( 'wpforo_after_delete_topic', $topic );
1373
1374 if( $delete_cache ) wpforo_clean_cache( 'topic', $topicid, $topic );
1375 WPF()->notice->add( 'This topic successfully deleted', 'success' );
1376
1377 return true;
1378 }
1379
1380 WPF()->notice->add( 'Topics delete error', 'error' );
1381
1382 return false;
1383 }
1384
1385 public function rebuild_first_last( $topic ) {
1386 if( ! $topic ) return false;
1387 if( wpforo_is_id( $topic ) ) $topic = $this->get_topic( $topic );
1388 if( ! is_array( $topic ) || ! $topic ) return false;
1389
1390 $sql = "SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d ORDER BY `is_first_post` DESC, `created` ASC, `postid` ASC LIMIT 1";
1391 if( $first_postid = WPF()->db->get_var( WPF()->db->prepare( $sql, $topic['topicid'] ) ) ) {
1392 $sql = "UPDATE `" . WPF()->tables->posts . "` SET `is_first_post` = 1 WHERE `postid` = %d";
1393 WPF()->db->query( WPF()->db->prepare( $sql, $first_postid ) );
1394
1395 do_action( 'wpforo_after_is_first_post_update', $first_postid, 1 );
1396 } else {
1397 $first_postid = 0;
1398 }
1399
1400 $sql = "SELECT `postid`, `created`
1401 FROM `" . WPF()->tables->posts . "`
1402 WHERE `topicid` = %d
1403 ORDER BY `is_first_post` ASC, `created` DESC, `postid` DESC LIMIT 1";
1404 if( ! $last_post = WPF()->db->get_row( WPF()->db->prepare( $sql, $topic['topicid'] ), ARRAY_A ) ) {
1405 $last_post = [ 'postid' => 0, 'created' => $topic['modified'] ];
1406 }
1407
1408 if( $r = WPF()->db->update(
1409 WPF()->tables->topics,
1410 [
1411 'first_postid' => $first_postid,
1412 'last_post' => $last_post['postid'],
1413 'modified' => $last_post['created'],
1414 ],
1415 [ 'topicid' => $topic['topicid'] ],
1416 [ '%d', '%d', '%s' ], [ '%d' ]
1417 ) ) {
1418 wpforo_clean_cache( 'topic-first-post' );
1419 }
1420
1421 return $r !== false;
1422 }
1423
1424 public function rebuild_stats( $topic ) {
1425 if( ! $topic ) return false;
1426 if( wpforo_is_id( $topic ) ) $topic = $this->get_topic( $topic );
1427 if( ! is_array( $topic ) || ! $topic ) return false;
1428
1429 $posts = WPF()->post->get_count( [ 'topicid' => $topic['topicid'], 'status' => 0 ] );
1430
1431 $data = [ 'posts' => $posts, 'answers' => 0 ];
1432 $data_format = [ '%d', '%d' ];
1433
1434 $layout = WPF()->forum->get_layout( $topic['forumid'] );
1435 if( $layout === 3 ) {
1436 $data['answers'] = WPF()->post->get_count(
1437 [ 'topicid' => $topic['topicid'], 'status' => 0, 'parentid' => 0, 'is_first_post' => 0 ]
1438 );
1439 $data['posts'] = $posts - 1;
1440 }
1441
1442 if( $r = WPF()->db->update(
1443 WPF()->tables->topics,
1444 $data,
1445 [ 'topicid' => $topic['topicid'] ],
1446 $data_format,
1447 [ '%d' ]
1448 ) ) {
1449 wpforo_clean_cache( 'topic-first-post', $topic['topicid'], $topic );
1450 }
1451
1452 return $r !== false;
1453 }
1454
1455 function get_count( $args = [] ) {
1456 $sql = "SELECT SQL_NO_CACHE COUNT(*) FROM `" . WPF()->tables->topics . "`";
1457 if( ! empty( $args ) ) {
1458 $wheres = [];
1459 foreach( $args as $key => $value ) $wheres[] = "`$key` = '" . esc_sql( $value ) . "'";
1460 if( $wheres ) $sql .= " WHERE " . implode( ' AND ', $wheres );
1461 }
1462
1463 return WPF()->db->get_var( $sql );
1464 }
1465
1466 function rebuild_threads( $topicid, $root = null ) {
1467 if( ! is_null( $root ) && $root <= 0 ) return;
1468 if( is_array( $topicid ) && wpfval( $topicid, 'topicid' ) ) $topicid = $topicid['topicid'];
1469 if( $topicid ) {
1470 $posts = WPF()->db->get_results(
1471 "SELECT * FROM `" . WPF()->tables->posts . "` WHERE `topicid` = " . intval( $topicid ),
1472 ARRAY_A
1473 );
1474 if( ! empty( $posts ) ) {
1475 $threads = [];
1476 foreach( $posts as $post ) {
1477 $threads[ $post['postid'] ] = [ 'postid' => $post['postid'], 'parentid' => $post['parentid'] ];
1478 }
1479 unset( $posts );
1480 foreach( $threads as $item ) {
1481 if( ! is_null( $root ) && $root != $item['postid'] ) continue;
1482 if( ! $item['parentid'] ) {
1483 $thread = WPF()->post->build_thread_data( $item['postid'], $threads );
1484 if( wpfval( $thread, 'children' ) ) {
1485 $children = implode( ',', $thread['children'] );
1486 WPF()->db->query(
1487 "UPDATE `" . WPF()->tables->posts . "` SET `root` = 0 WHERE `postid` = " . intval(
1488 $item['postid']
1489 )
1490 );
1491 WPF()->db->query(
1492 "UPDATE `" . WPF()->tables->posts . "` SET `root` = " . intval(
1493 $item['postid']
1494 ) . " WHERE `postid` IN(" . esc_sql( $children ) . ")"
1495 );
1496 }
1497 } elseif( ! wpfkey( $threads, $item['parentid'] ) ) {
1498 WPF()->db->query(
1499 "UPDATE `" . WPF()->tables->posts . "` SET `root` = 0, `parentid` = 0 WHERE `postid` = " . intval(
1500 $item['postid']
1501 )
1502 );
1503 }
1504 }
1505 }
1506 }
1507 }
1508
1509 /**
1510 * @param $topic
1511 * @param $forum
1512 * @param $_cache
1513 *
1514 * @return string
1515 * @deprecated since 2.0.10 instead this method use _get_url()
1516 *
1517 */
1518 public function _get_topic_url( $topic, $forum = [], $_cache = true ) {
1519 return $this->_get_url( $topic, $forum, $_cache );
1520 }
1521
1522 /**
1523 * @param $topic
1524 * @param $forum
1525 * @param $_cache
1526 *
1527 * @return string
1528 */
1529 public function _get_url( $topic, $forum = [], $_cache = true ) {
1530 if( wpforo_setting( 'board', 'url_structure' ) === 'short' ) return $this->get_short_url( $topic );
1531
1532 return $this->get_full_url( $topic, $forum, $_cache );
1533 }
1534
1535 /**
1536 * @param $topic
1537 * @param $forum
1538 * @param $_cache
1539 *
1540 * @return string
1541 * @deprecated since 2.0.10 instead this method use get_url()
1542 *
1543 */
1544 public function get_topic_url( $topic, $forum = [], $_cache = true ) {
1545 return $this->get_url( $topic, $forum, $_cache );
1546 }
1547
1548 public function set_status( $topicid, $status ) {
1549 $topicid = wpforo_bigintval( $topicid );
1550 $status = intval( $status );
1551 if( ! $topic = $this->get_topic( $topicid, false ) ) return false;
1552
1553 if( $r = WPF()->db->update(
1554 WPF()->tables->topics,
1555 [ 'status' => $status ],
1556 [ 'topicid' => $topicid ],
1557 [ '%d' ], [ '%d' ]
1558 ) ) {
1559 if( $status ) {
1560 do_action( 'wpforo_topic_unapprove', $topic );
1561 if( wpfval( $topic, 'tags' ) ) $this->remove_tags( $topic['tags'] );
1562 } else {
1563 do_action( 'wpforo_topic_approve', $topic );
1564 if( wpfval( $topic, 'tags' ) ) $this->add_tags( $topic['tags'] );
1565 }
1566
1567 do_action( 'wpforo_topic_status_update', $topic, $status );
1568 wpforo_clean_cache( 'topic-first-post', $topicid );
1569 }
1570
1571 if( $r !== false ) {
1572 WPF()->notice->add( 'Done!', 'success' );
1573
1574 return true;
1575 }
1576
1577 WPF()->notice->add( 'Status changing error', 'error' );
1578
1579 return false;
1580 }
1581
1582 public function wprivate( $topicid, $private ) {
1583 WPF()->db->update(
1584 WPF()->tables->topics,
1585 [ 'private' => $private ],
1586 [ 'topicid' => $topicid ],
1587 [ '%d' ], [ '%d' ]
1588 );
1589
1590 WPF()->db->update(
1591 WPF()->tables->posts,
1592 [ 'private' => $private ],
1593 [ 'topicid' => $topicid ],
1594 [ '%d' ], [ '%d' ]
1595 );
1596
1597 do_action( 'wpforo_topic_private_update', $topicid, $private );
1598 wpforo_clean_cache();
1599
1600 return true;
1601 }
1602
1603 public function delete_attachments( $topicid ) {
1604 $args = [ 'topicid' => $topicid ];
1605 $posts = WPF()->post->get_posts( $args );
1606 if( ! empty( $posts ) ) {
1607 foreach( $posts as $post ) {
1608 WPF()->post->delete_attachments( $post['postid'] );
1609 }
1610 }
1611 }
1612
1613 function rebuild_forum_threads( $forumid = 0 ) {
1614 if( ! $forumid ) {
1615 $args = [ 'layout' => 4 ];
1616 $forums = WPF()->forum->get_forums( $args );
1617 if( ! empty( $forums ) ) {
1618 foreach( $forums as $forum ) {
1619 $args = [ 'forumid' => $forum['forumid'] ];
1620 $topics = $this->get_topics( $args );
1621 if( ! empty( $topics ) ) {
1622 foreach( $topics as $topic ) {
1623 $this->rebuild_threads( $topic['topicid'] );
1624 }
1625 }
1626 }
1627 }
1628 } else {
1629 $args = [ 'forumid' => $forumid ];
1630 $topics = $this->get_topics( $args );
1631 if( ! empty( $topics ) ) {
1632 foreach( $topics as $topic ) {
1633 $this->rebuild_threads( $topic['topicid'] );
1634 }
1635 }
1636 }
1637 }
1638
1639 /**
1640 * array get_topic(array or id(num))
1641 * Returns merged arguments array from defined and default arguments.
1642 *
1643 * @return array where count is topic count and other numeric arrays with topic
1644 * @since 1.0.0
1645 *
1646 */
1647 function get_topics( $args = [], &$items_count = 0, $count = true ) {
1648 $cache = WPF()->cache->on( 'topic' );
1649
1650 $default = [
1651 'include' => [], // array( 2, 10, 25 )
1652 'exclude' => [], // array( 2, 10, 25 )
1653 'forumids' => [],
1654 'forumid' => null,
1655 'userid' => null, // user id in DB
1656 'type' => null, //0, 1, etc . . .
1657 'solved' => null,
1658 'closed' => null,
1659 'status' => null, //0, 1, etc . . .
1660 'private' => null, //0, 1, etc . . .''
1661 'pollid' => null,
1662 'orderby' => 'type, topicid', // type, topicid, modified, created
1663 'order' => 'DESC', // ASC DESC
1664 'offset' => null, // this use when you give row_count
1665 'row_count' => null, // 4 or 1 ...
1666 'permgroup' => null, //Checks permissions based on attribute value not on current user usergroup
1667 'read' => null, //true / false
1668 'prefix' => null, //23 / 23,24,50
1669 'where' => null,
1670 'access_filter' => true, //false to skip forum permission filtering (admin backend ops)
1671 ];
1672
1673 $args = wpforo_parse_args( $args, $default );
1674
1675 // Security: extract only expected keys instead of extract()
1676 $include = $args['include'];
1677 $exclude = $args['exclude'];
1678 $forumids = $args['forumids'];
1679 $forumid = $args['forumid'];
1680 $userid = $args['userid'];
1681 $type = $args['type'];
1682 $solved = $args['solved'];
1683 $closed = $args['closed'];
1684 $status = $args['status'];
1685 $private = $args['private'];
1686 $pollid = $args['pollid'];
1687 $orderby = $args['orderby'];
1688 $order = $args['order'];
1689 $offset = $args['offset'];
1690 $row_count = $args['row_count'];
1691 $permgroup = $args['permgroup'];
1692 $read = $args['read'];
1693 $prefix = $args['prefix'];
1694 $where = $args['where'];
1695 $access_filter = $args['access_filter'];
1696
1697 if( $row_count === 0 ) return [];
1698
1699 $include = wpforo_parse_args( $include );
1700 $exclude = wpforo_parse_args( $exclude );
1701 $forumids = wpforo_parse_args( $forumids );
1702
1703 $guest = [];
1704 $wheres = [];
1705
1706 if( ! is_null( $prefix ) ) {
1707 $prefixes = explode( ',', $prefix );
1708 if( ! empty( $prefixes ) ) {
1709 foreach( $prefixes as $prefixid ) {
1710 $wheres[] = " FIND_IN_SET('" . intval( $prefixid ) . "', `prefix`) ";
1711 }
1712 }
1713 }
1714
1715 if( ! is_null( $read ) ) {
1716 $last_read_postid = WPF()->log->get_all_read( 'post' );
1717 if( $read ) {
1718 if( $last_read_postid ) {
1719 $wheres[] = "`last_post` <= " . intval( $last_read_postid );
1720 }
1721 $include_read = WPF()->log->get_read();
1722 $include = array_merge( $include, $include_read );
1723 } else {
1724 if( $last_read_postid ) {
1725 $wheres[] = "`last_post` > " . intval( $last_read_postid );
1726 }
1727 $exclude_read = WPF()->log->get_read();
1728 $exclude = array_merge( $exclude, $exclude_read );
1729 }
1730 }
1731
1732 if( ! empty( $include ) ) $wheres[] = "`topicid` IN(" . implode( ', ', array_map( 'intval', $include ) ) . ")";
1733 if( ! empty( $exclude ) ) {
1734 $wheres[] = "`topicid` NOT IN(" . implode(
1735 ', ',
1736 array_map( 'intval', $exclude )
1737 ) . ")";
1738 }
1739 if( ! empty( $forumids ) ) {
1740 $wheres[] = "`forumid` IN(" . implode(
1741 ', ',
1742 array_map( 'intval', $forumids )
1743 ) . ")";
1744 }
1745 if( ! is_null( $forumid ) ) $wheres[] = "`forumid` = " . intval( $forumid );
1746 if( ! is_null( $userid ) ) $wheres[] = "`userid` = " . wpforo_bigintval( $userid );
1747 if( ! is_null( $solved ) ) $wheres[] = "`solved` = " . intval( $solved );
1748 if( ! is_null( $closed ) ) $wheres[] = "`closed` = " . intval( $closed );
1749 if( ! is_null( $type ) ) $wheres[] = "`type` = " . intval( $type );
1750 if( ! is_null( $where ) ) $wheres[] = $where;
1751
1752 if( ! is_user_logged_in() ) $guest = WPF()->member->get_guest_cookies();
1753
1754 if( empty( $forumids ) ) {
1755 if( isset( $forumid ) && ! WPF()->perm->forum_can( 'vf', $forumid, $permgroup ) ) {
1756 return [];
1757 }
1758 }
1759
1760 if( isset( $forumid ) && $forumid ) {
1761 if( WPF()->perm->forum_can( 'vp', $forumid, $permgroup ) ) {
1762 if( ! is_null( $private ) ) $wheres[] = " `private` = " . intval( $private );
1763 } elseif( isset( WPF()->current_userid ) && WPF()->current_userid ) {
1764 $wheres[] = " ( `private` = 0 OR (`private` = 1 AND `userid` = " . WPF()->current_userid . ") )";
1765 } elseif( wpfval( $guest, 'email' ) ) {
1766 $wheres[] = " ( `private` = 0 OR (`private` = 1 AND `email` = '" . sanitize_email(
1767 $guest['email']
1768 ) . "') )";
1769 } else {
1770 $wheres[] = " `private` = 0";
1771 }
1772 } else {
1773 if( ! is_null( $private ) ) $wheres[] = " `private` = " . intval( $private );
1774 }
1775
1776 if( isset( $forumid ) && $forumid ) {
1777 if( WPF()->perm->forum_can( 'au', $forumid, $permgroup ) ) {
1778 if( ! is_null( $status ) ) $wheres[] = " `status` = " . intval( $status );
1779 } elseif( isset( WPF()->current_userid ) && WPF()->current_userid ) {
1780 $wheres[] = " ( `status` = 0 OR (`status` = 1 AND `userid` = " . WPF()->current_userid . ") )";
1781 } elseif( wpfval( $guest, 'email' ) ) {
1782 $wheres[] = " ( `status` = 0 OR (`status` = 1 AND `email` = '" . sanitize_email(
1783 $guest['email']
1784 ) . "') )";
1785 } else {
1786 $wheres[] = " `status` = 0";
1787 }
1788 } else {
1789 if( ! is_null( $status ) ) $wheres[] = " `status` = " . intval( $status );
1790 }
1791
1792 if( function_exists( 'WPF_POLL' ) ) {
1793 if( ! is_null( $pollid ) ) $wheres[] = " `pollid` <> 0";
1794 }
1795
1796 $wheres = apply_filters( 'wpforo_get_topics_sql_wheres', $wheres );
1797
1798 $sql = "SELECT * FROM `" . WPF()->tables->topics . "`";
1799 if( ! empty( $wheres ) ) {
1800 $sql .= " WHERE " . implode( " AND ", $wheres );
1801 }
1802
1803 if( $count ) {
1804 $item_count_sql = preg_replace( '#SELECT.+?FROM#isu', 'SELECT count(*) FROM', $sql );
1805 if( $item_count_sql ) $items_count = WPF()->db->get_var( $item_count_sql );
1806 }
1807
1808 if( $order === 'RAND' ) {
1809 $sql .= " ORDER BY RAND()";
1810 } else {
1811 $sql .= " ORDER BY " . str_replace( ',', ' ' . esc_sql( $order ) . ',', esc_sql( $orderby ) ) . " " . esc_sql(
1812 $order
1813 );
1814 }
1815
1816 if( ! is_null( $row_count ) ) {
1817 if( ! is_null( $offset ) ) {
1818 $sql .= esc_sql( " LIMIT $offset,$row_count" );
1819 } else {
1820 $sql .= esc_sql( " LIMIT $row_count" );
1821 }
1822 }
1823
1824 if( $cache ) {
1825 $object_key = md5( $sql . WPF()->current_user_groupid );
1826 $object_cache = WPF()->cache->get( $object_key );
1827 if( ! empty( $object_cache ) ) {
1828 if( ! empty( $object_cache['items'] ) ) {
1829 $do_access_filter = $access_filter && apply_filters( 'wpforo_topic_access_filter_cache', true );
1830 if( $do_access_filter ) {
1831 $object_cache['items'] = $this->access_filter(
1832 $object_cache['items'],
1833 [
1834 'groupids' => ( array_filter(
1835 array_map( 'intval', (array) $permgroup )
1836 ) ?: null ),
1837 ]
1838 );
1839 }
1840 return apply_filters( 'wpforo_get_topics', $object_cache['items'] );
1841 }
1842 }
1843 }
1844
1845 $topics = WPF()->db->get_results( $sql, ARRAY_A );
1846
1847 if( $cache && isset( $object_key ) && ! empty( $topics ) ) {
1848 self::$cache['topics'][ $object_key ]['items'] = $topics;
1849 self::$cache['topics'][ $object_key ]['items_count'] = $items_count;
1850 }
1851
1852 if( $access_filter && ( ! empty( $forumids ) || ! $forumid ) ) {
1853 $topics = $this->access_filter(
1854 $topics,
1855 [
1856 'groupids' => ( array_filter(
1857 array_map( 'intval', (array) $permgroup )
1858 ) ?: null ),
1859 ]
1860 );
1861 }
1862
1863 return apply_filters( 'wpforo_get_topics', $topics );
1864 }
1865
1866 function access_filter( $topics, $user = null ) {
1867 if( ! empty( $topics ) ) {
1868 foreach( $topics as $key => $topic ) {
1869 if( ! $this->view_access( $topic, $user ) ) unset( $topics[ $key ] );
1870 }
1871 }
1872
1873 return $topics;
1874 }
1875
1876 function view_access( $topic, $user = null ) {
1877 $groupids = wpfval( $user, 'groupids' );
1878 if( ! WPF()->perm->forum_can( 'vf', $topic['forumid'], $groupids ) ) {
1879 return apply_filters(
1880 'wpforo_topic_view_access',
1881 false,
1882 $topic,
1883 $user
1884 );
1885 }
1886 if( ! WPF()->perm->forum_can( 'vt', $topic['forumid'], $groupids ) ) {
1887 return apply_filters(
1888 'wpforo_topic_view_access',
1889 false,
1890 $topic,
1891 $user
1892 );
1893 }
1894 if( ! wpforo_is_users_same( wpforo_member( $topic ), $user ) && ( ( (int) wpfval( $topic, 'private' ) && ! WPF()->perm->forum_can( 'vp', $topic['forumid'], $groupids ) ) || ( (int) wpfval(
1895 $topic,
1896 'status'
1897 ) && ! WPF()->perm->forum_can( 'au', $topic['forumid'], $groupids ) ) ) ) {
1898 return apply_filters( 'wpforo_topic_view_access', false, $topic, $user );
1899 }
1900
1901 return apply_filters( 'wpforo_topic_view_access', true, $topic, $user );
1902 }
1903
1904 public function members( $topicid, $limit = 0 ) {
1905 if( ! $topicid ) return [];
1906 $members = [];
1907 $args = [
1908 'topicid' => $topicid,
1909 'orderby' => 'created',
1910 'order' => 'ASC',
1911 'private' => 0,
1912 'status' => 0,
1913 'cache_type' => 'args',
1914 ];
1915 $posts = WPF()->post->get_posts( $args );
1916 foreach( $posts as $post ) {
1917 if( wpfval( $post, 'userid' ) ) {
1918 $members[ $post['userid'] ] = wpforo_member( $post['userid'] );
1919 if( $limit && count( $members ) >= $limit ) break;
1920 }
1921 }
1922
1923 return array_filter( $members );
1924 }
1925
1926 public function can_answer( $topicid ) {
1927 if( ! $topicid ) return false;
1928 $topic = wpforo_topic( $topicid );
1929 if( wpfval( $topic, 'topicid' ) ) {
1930 if( wpfval( $topic, 'userid' ) ) {
1931 if( ! WPF()->perm->forum_can( 'aot', $topic['forumid'] ) && WPF()->current_userid == $topic['userid'] ) {
1932 return false;
1933 }
1934 } else {
1935 $guest = WPF()->member->get_guest_cookies();
1936 if( wpfval( $topic, 'email' ) && wpfval( $guest, 'email' ) ) {
1937 if( ! WPF()->perm->forum_can( 'aot', $topic['forumid'] ) && $topic['email'] == $guest['email'] ) {
1938 return false;
1939 }
1940 }
1941 }
1942 }
1943
1944 return true;
1945 }
1946
1947 /**
1948 * @param $topicid
1949 *
1950 * @return bool
1951 */
1952 public function has_is_answer_post( $topicid ) {
1953 if( ! $topicid ) return false;
1954 $sql = "SELECT EXISTS ( SELECT * FROM `" . WPF()->tables->posts . "` WHERE `is_answer` = 1 AND `topicid` = %d) AS is_exists";
1955 $sql = WPF()->db->prepare( $sql, $topicid );
1956
1957 return (bool) WPF()->db->get_var( $sql );
1958 }
1959
1960 public function get_postids( $topicid ) {
1961 $sql = "SELECT `postid`
1962 FROM `" . WPF()->tables->posts . "`
1963 WHERE `topicid` = %d";
1964 $sql = WPF()->db->prepare( $sql, $topicid );
1965
1966 return array_map( 'wpforo_bigintval', WPF()->db->get_col( $sql ) );
1967 }
1968
1969 public function edit_tag( $tag, $old_tag = '', $disconnect = false ) {
1970
1971 $tag_id = wpfval( $tag, 'tagid' );
1972 $tag_name = wpfval( $tag, 'tag' );
1973 $tag_name = $this->sanitize_tag( $tag_name );
1974
1975 if( false !== WPF()->db->update(
1976 WPF()->tables->tags,
1977 [ 'tag' => $tag_name ],
1978 [ 'tagid' => intval( $tag_id ) ],
1979 [ '%s' ], [ '%d' ]
1980 ) ) {
1981 if( $old_tag ) {
1982 if( $disconnect ) {
1983 $this->remove_tag_from_topics( $old_tag );
1984 WPF()->db->update( WPF()->tables->tags, [ 'count' => 0 ], [ 'tag' => $tag_name ], [ '%d' ], [ '%s' ] );
1985 } else {
1986 if( $old_tag != $tag_name ) {
1987 $this->update_tag_in_topics( $tag_name, $old_tag );
1988 }
1989 $count = WPF()->db->get_var(
1990 "SELECT COUNT(*) FROM `" . WPF()->tables->topics . "` WHERE FIND_IN_SET('" . esc_sql(
1991 $tag_name
1992 ) . "', tags)"
1993 );
1994 WPF()->db->update(
1995 WPF()->tables->tags,
1996 [ 'count' => intval( $count ) ],
1997 [ 'tag' => $tag_name ],
1998 [ '%d' ], [ '%s' ]
1999 );
2000 }
2001 }
2002 wpforo_clean_cache( 'tag', $tag_name );
2003
2004 return true;
2005 }
2006
2007 return false;
2008 }
2009
2010 public function remove_tag_from_topics( $tag ) {
2011 if( $tag ) {
2012 WPF()->db->query(
2013 "UPDATE `" . WPF()->tables->topics . "`
2014 SET tags = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', tags, ','), '," . esc_sql(
2015 $tag
2016 ) . ",', ','))
2017 WHERE FIND_IN_SET('" . esc_sql( $tag ) . "', tags)"
2018 );
2019 }
2020 }
2021
2022 public function update_tag_in_topics( $new_tag, $old_tag ) {
2023 if( $new_tag && $old_tag ) {
2024
2025 WPF()->db->query(
2026 "UPDATE `" . WPF()->tables->topics . "`
2027 SET tags = '" . esc_sql( $new_tag ) . "'
2028 WHERE tags LIKE '" . esc_sql( $old_tag ) . "'"
2029 );
2030
2031 WPF()->db->query(
2032 "UPDATE `" . WPF()->tables->topics . "`
2033 SET tags = REPLACE(tags, '," . esc_sql( $old_tag ) . ",', '," . esc_sql(
2034 $new_tag
2035 ) . ",')
2036 WHERE tags LIKE '%," . esc_sql( $old_tag ) . ",%'"
2037 );
2038
2039 $topics = WPF()->db->get_results(
2040 "SELECT `topicid`, `tags`
2041 FROM `" . WPF()->tables->topics . "`
2042 WHERE FIND_IN_SET('" . esc_sql( $old_tag ) . "', tags)",
2043 ARRAY_A
2044 );
2045 if( ! empty( $topics ) ) {
2046 foreach( $topics as $topic ) {
2047 $tags = explode( ',', $topic['tags'] );
2048 if( ! empty( $tags ) ) {
2049 foreach( $tags as $key => $tag ) {
2050 if( $tag === $old_tag ) $tags[ $key ] = $new_tag;
2051 }
2052 $tags = implode( ',', $tags );
2053 WPF()->db->update(
2054 WPF()->tables->topics,
2055 [ 'tags' => $tags ],
2056 [ 'topicid' => $topic['topicid'] ],
2057 [ '%s' ], [ '%d' ]
2058 );
2059 }
2060 }
2061 }
2062 }
2063 }
2064
2065 public function remove_tag( $tag ) {
2066 if( $tag ) {
2067 $this->remove_tag_from_topics( $tag );
2068 $sql_delete_tag = "DELETE FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'";
2069 WPF()->db->query( $sql_delete_tag );
2070 wpforo_clean_cache( 'tag', $tag );
2071 }
2072 }
2073
2074 public function get_tag( $args = [] ) {
2075 if( $args ) {
2076 $default = [
2077 'tagid' => null,
2078 'tag' => null,
2079 ];
2080 if( wpforo_is_id( $args ) ) {
2081 $args = [ 'tagid' => $args ];
2082 } elseif( is_string( $args ) ) {
2083 $args = [ 'tag' => $args ];
2084 }
2085
2086 $args = wpforo_parse_args( $args, $default );
2087 $sql = "SELECT * FROM `" . WPF()->tables->tags . "`";
2088 $wheres = [];
2089 if( $args['tagid'] ) $wheres[] = "`tagid` = " . intval( $args['tagid'] );
2090 if( $args['tag'] ) $wheres[] = "`tag` = '" . esc_sql( $args['tag'] ) . "'";
2091 if( $wheres ) {
2092 $sql .= " WHERE " . implode( " AND ", $wheres );
2093
2094 if( WPF()->ram_cache->exists( $sql ) ) {
2095 $tag = WPF()->ram_cache->get( $sql );
2096 } else {
2097 $tag = WPF()->db->get_row( $sql, ARRAY_A );
2098 WPF()->ram_cache->set( $sql, $tag );
2099 }
2100
2101 return $tag;
2102 }
2103 }
2104
2105 return [];
2106 }
2107
2108 public function get_tags( $args = [], &$items_count = 0 ) {
2109 $cache = WPF()->cache->on( 'tag' );
2110 $default = [
2111 'tag' => '',
2112 'prefix' => 0,
2113 'count' => null,
2114 'orderby' => 'count',
2115 'order' => 'DESC',
2116 'offset' => null,
2117 'row_count' => null,
2118 ];
2119
2120 $args = wpforo_parse_args( $args, $default );
2121 $sql = "SELECT * FROM `" . WPF()->tables->tags . "`";
2122 $wheres = [];
2123 $wheres[] = " `prefix` = " . intval( $args['prefix'] );
2124 if( ! empty( $wheres ) ) $sql .= " WHERE " . implode( " AND ", $wheres );
2125 $item_count_sql = preg_replace( '#SELECT.+?FROM#isu', 'SELECT count(*) FROM', $sql );
2126 if( $item_count_sql ) $items_count = WPF()->db->get_var( $item_count_sql );
2127
2128 $sql .= " ORDER BY `" . esc_sql( $args['orderby'] ) . "` " . esc_sql( $args['order'] );
2129
2130 if( $args['row_count'] != null ) {
2131 if( $args['offset'] != null ) {
2132 $sql .= " LIMIT " . intval( $args['offset'] ) . ',' . intval( $args['row_count'] );
2133 } else {
2134 $sql .= " LIMIT " . intval( $args['row_count'] );
2135 }
2136 }
2137
2138 if( $cache ) {
2139 $object_key = md5( $sql . WPF()->current_user_groupid );
2140 $object_cache = WPF()->cache->get( $object_key, 'loop', 'tag' );
2141 if( ! empty( $object_cache ) ) {
2142 $items_count = $object_cache['items_count'];
2143
2144 return $object_cache['items'];
2145 }
2146 }
2147
2148 $tags = WPF()->db->get_results( $sql, ARRAY_A );
2149 $tags = apply_filters( 'wpforo_get_tags', $tags );
2150
2151 if( $cache && isset( $object_key ) && ! empty( $tags ) ) {
2152 self::$cache['tags'][ $object_key ]['items'] = $tags;
2153 self::$cache['tags'][ $object_key ]['items_count'] = $items_count;
2154 WPF()->cache->create( 'loop', self::$cache, 'tag' );
2155 }
2156
2157 return $tags;
2158 }
2159
2160 public function after_add_post( $post, $topic ) {
2161 if( ! intval( $post['status'] ) ) {
2162 $this->rebuild_first_last( $topic );
2163 $this->rebuild_stats( $topic );
2164 }
2165 }
2166
2167 public function after_delete_post( $post ) {
2168 if( ! intval( $post['status'] ) ) {
2169 $this->rebuild_first_last( $post['topicid'] );
2170 $this->rebuild_stats( $post['topicid'] );
2171 }
2172 }
2173
2174 public function after_post_status_update( $post ) {
2175 if( $topic = $this->get_topic( $post['topicid'] ) ) {
2176 $this->rebuild_first_last( $topic );
2177 $this->rebuild_stats( $topic );
2178 }
2179 }
2180
2181 public function after_delete_user( $userid, $reassign ) {
2182 if( $boardids = WPF()->board->get_active_boardids() ) {
2183 if( is_null( $reassign ) ) {
2184
2185 foreach( $boardids as $boardid ) {
2186 WPF()->change_board( $boardid );
2187 if( $topicids = WPF()->db->get_col(
2188 WPF()->db->prepare(
2189 "SELECT `topicid` FROM `" . WPF()->tables->topics . "` WHERE userid = %d",
2190 $userid
2191 )
2192 ) ) {
2193 foreach( $topicids as $topicid ) $this->delete( $topicid, false, false );
2194 }
2195 }
2196
2197 } else {
2198 if( $reassign = wpforo_bigintval( $reassign ) ) {
2199 $data = [ 'userid' => $reassign ];
2200 $format = [ '%d' ];
2201 } else {
2202 $data = [
2203 'userid' => 0,
2204 'name' => wpforo_phrase( 'Anonymous', false ) . " " . $userid,
2205 'email' => "anonymous_$userid@example.com",
2206 ];
2207 $format = [ '%d', '%s', '%s' ];
2208 }
2209
2210 foreach( $boardids as $boardid ) {
2211 WPF()->change_board( $boardid );
2212 WPF()->db->update( WPF()->tables->topics, $data, [ 'userid' => $userid ], $format, [ '%d' ] );
2213 }
2214
2215 }
2216 }
2217 }
2218 }
2219