PluginProbe
wpForo Forum / 3.0.7
wpForo Forum v3.0.7
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 / Topics.php

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

2,213 lines 67.9 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_current_guest( $topic['email'] ) ) {
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 wpforo_clean_cache( 'topic', $topicid, $topic );
509 if( $status ) {
510 WPF()->notice->add( 'Your topic successfully added and awaiting moderation', 'success' );
511 } else {
512 WPF()->notice->add( 'Your topic successfully added', 'success' );
513 }
514
515 return $topicid;
516 }
517 }
518
519 }
520
521 WPF()->notice->add( 'Topic add error', 'error' );
522
523 return false;
524 }
525
526 #################################################################################
527
528 private function unique_slug( $slug ) {
529 $new_slug = wpforo_text( $slug, 250, false );
530 $i = 2;
531 while( ! WPF()->can_use_this_slug( $new_slug ) || WPF()->db->get_var(
532 "SELECT `topicid` FROM " . WPF()->tables->topics . " WHERE `slug` = '" . esc_sql( $new_slug ) . "'"
533 ) ) {
534 $new_slug = wpforo_text( $slug, 250, false ) . '-' . $i;
535 $i ++;
536 }
537
538 return $new_slug;
539 }
540
541 #################################################################################
542
543 public function sanitize_tags( $tags, $array = false, $limit = false ) {
544 if( $tags ) {
545 if( ! is_array( $tags ) ) {
546 $tag_separator = ( strpos( $tags, '،' ) !== false ) ? '،' : ',';
547 $tag_separator = apply_filters( 'wpforo_tag_separator', $tag_separator );
548 $tags = wp_unslash( $tags );
549 //Replace other tag separators to regular comma to store in the database
550 $tags = str_replace( $tag_separator, ',', $tags );
551 $tags = explode( $tag_separator, $tags );
552 }
553 if( wpforo_setting( 'tags', 'lowercase' ) ) {
554 if( function_exists( 'mb_strtolower' ) ) {
555 $tags = array_map( 'mb_strtolower', $tags );
556 } else {
557 $tags = array_map( 'strtolower', $tags );
558 }
559 }
560 $length = wpforo_setting( 'tags', 'length' );
561 $mb_substr = function_exists( 'mb_substr' );
562 foreach( $tags as $key => $tag ) {
563 if( $mb_substr ) {
564 $tags[ $key ] = mb_substr( (string) $tag, 0, $length );
565 } else {
566 $tags[ $key ] = substr( (string) $tag, 0, $length );
567 }
568 }
569 $tags = array_map( 'trim', $tags );
570 $tags = array_map( 'sanitize_text_field', $tags );
571 $tags = array_filter( $tags );
572 $tags = array_unique( $tags );
573 if( $limit ) {
574 $tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
575 }
576 if( $array ) {
577 return $tags;
578 } else {
579 // We keep using "," in the database even if the separator is different
580 return implode( ',', $tags );
581 }
582 }
583
584 if( $array ) {
585 return [];
586 } else {
587 return '';
588 }
589 }
590
591 /**
592 * @param $topic
593 * @param $forum
594 * @param $_cache
595 *
596 * @return string
597 */
598 public function get_url( $topic, $forum = [], $_cache = true ) {
599 return (string) wpforo_ram_get( [ $this, '_get_url' ], $topic, $forum, $_cache );
600 }
601
602 public function add_tags( $tags ) {
603 if( $tags ) {
604 $tags = $this->sanitize_tags( $tags, true );
605 if( ! empty( $tags ) ) {
606 $tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
607 foreach( $tags as $tag ) {
608 $count = (int) WPF()->db->get_var(
609 "SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
610 );
611 if( $count ) {
612 WPF()->db->update(
613 WPF()->tables->tags,
614 [ 'count' => $count + 1 ],
615 [ 'tag' => $tag ],
616 [ '%d' ], [ '%s' ]
617 );
618 } else {
619 $this->add_tag( $tag, 1 );
620 }
621 wpforo_clean_cache( 'tag', $tag );
622 }
623 wpforo_clean_cache( 'tag' );
624 }
625 }
626 }
627
628 public function add_tag( $tag, $count = 0 ) {
629 $tagid = 0;
630 if( $tag ) {
631 $tag = $this->sanitize_tag( $tag );
632 if( ! WPF()->db->get_var(
633 "SELECT `tagid` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
634 ) ) {
635 $tagid = WPF()->db->insert(
636 WPF()->tables->tags,
637 [ 'tag' => $tag, 'prefix' => 0, 'count' => intval( $count ) ], [ '%s', '%d', '%d' ]
638 );
639 }
640 }
641
642 return $tagid;
643 }
644
645 public function sanitize_tag( $tag ) {
646 $tag = trim( (string) $tag );
647 $tag = wp_unslash( $tag );
648 $tag = sanitize_text_field( $tag );
649 $length = wpforo_setting( 'tags', 'length' );
650 $tag = ( function_exists( 'mb_substr' ) ) ? mb_substr( (string) $tag, 0, $length ) : substr(
651 (string) $tag,
652 0,
653 $length
654 );
655 $lowcase = wpforo_setting( 'tags', 'lowercase' );
656 if( $lowcase ) {
657 $tag = ( function_exists( 'mb_strtolower' ) ) ? mb_strtolower( (string) $tag ) : strtolower(
658 (string) $tag
659 );
660 }
661
662 return $tag;
663 }
664
665 public function edit_tags( $tags, $topic = [] ) {
666 $old_tags = ( wpfval( $topic, 'tags' ) ) ? $this->sanitize_tags( $topic['tags'], true ) : false;
667 if( $tags ) {
668 $tags = $this->sanitize_tags( $tags, true );
669 $tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
670 if( ! empty( $tags ) ) {
671 if( wpfval( $topic, 'topicid' ) ) {
672 if( ! empty( $old_tags ) ) {
673 foreach( $old_tags as $old_tag ) {
674 if( ! in_array( $old_tag, $tags ) ) {
675 $this->remove_tags( $old_tag );
676 }
677 }
678 }
679 }
680 if( ! wpfval( $topic, 'status' ) && ! wpfval( $topic, 'private' ) ) {
681 foreach( $tags as $tag ) {
682 $count = (int) WPF()->db->get_var(
683 "SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
684 );
685 if( ! $count ) {
686 WPF()->db->insert(
687 WPF()->tables->tags,
688 [ 'tag' => $tag, 'prefix' => 0, 'count' => 1 ], [ '%s', '%d', '%d' ]
689 );
690 } elseif( empty( $old_tags ) || ! in_array( $tag, $old_tags ) ) {
691 WPF()->db->update(
692 WPF()->tables->tags,
693 [ 'count' => ( $count + 1 ) ],
694 [ 'tag' => $tag ],
695 [ '%d' ], [ '%s' ]
696 );
697 }
698 wpforo_clean_cache( 'tag', $tag );
699 }
700 }
701 }
702 } else {
703 if( ! empty( $old_tags ) && wpfval( $topic, 'topicid' ) ) {
704 WPF()->db->update(
705 WPF()->tables->topics,
706 [ 'tags' => '' ],
707 [ 'topicid' => $topic['topicid'] ],
708 [ '%s' ], [ '%d' ]
709 );
710 $this->remove_tags( $old_tags );
711 }
712 }
713 wpforo_clean_cache( 'tag' );
714 }
715
716 public function remove_tags( $tags ) {
717 if( $tags ) {
718 $tags = $this->sanitize_tags( $tags, true );
719 foreach( $tags as $tag ) {
720 $count = (int) WPF()->db->get_var(
721 "SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
722 );
723 if( $count === 1 ) {
724 WPF()->db->query(
725 "DELETE FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
726 );
727 } else {
728 WPF()->db->update(
729 WPF()->tables->tags,
730 [ 'count' => ( $count - 1 ) ],
731 [ 'tag' => $tag ],
732 [ '%d' ], [ '%s' ]
733 );
734 }
735 wpforo_clean_cache( 'tag', $tag );
736 }
737 }
738 wpforo_clean_cache( 'tag' );
739 }
740
741 /**
742 * array get_topic(array or id(num))
743 *
744 * Returns array from defined and default arguments.
745 *
746 * @param mixed $args defined arguments array for returning
747 * @param bool $protect
748 *
749 * @return array
750 * @since 1.0.0
751 *
752 */
753
754 public function _get_topic( $args = [], $protect = true ) {
755 if( ! $args ) return [];
756
757 if( is_array( $args ) ) {
758 $default = [
759 'topicid' => null,
760 'slug' => '',
761 ];
762 } elseif( wpforo_is_id( $args ) ) {
763 $default = [
764 'topicid' => $args,
765 'slug' => '',
766 ];
767 } else {
768 $default = [
769 'topicid' => null,
770 'slug' => $args,
771 ];
772 }
773
774 $args = wpforo_parse_args( $args, $default );
775
776 $sql = "SELECT * FROM `" . WPF()->tables->topics . "`";
777 $wheres = [];
778 if( $topicid = wpforo_bigintval( $args['topicid'] ) ) $wheres[] = "`topicid` = " . $topicid;
779 if( $args['slug'] ) $wheres[] = "`slug` = '" . esc_sql( $args['slug'] ) . "'";
780 if( ! empty( $wheres ) ) $sql .= " WHERE " . implode( " AND ", $wheres );
781
782 $topic = (array) WPF()->db->get_row( $sql, ARRAY_A );
783
784 if( $protect ) {
785 if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
786 'vf',
787 $topic['forumid']
788 ) ) {
789 return [];
790 }
791 if( isset( $topic['private'] ) && $topic['private'] && ! wpforo_is_owner(
792 $topic['userid'],
793 $topic['email']
794 ) ) {
795 if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
796 'vp',
797 $topic['forumid']
798 ) ) {
799 return [];
800 }
801 }
802 if( isset( $topic['status'] ) && $topic['status'] && ! wpforo_is_owner(
803 $topic['userid'],
804 $topic['email']
805 ) ) {
806 if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
807 'au',
808 $topic['forumid']
809 ) ) {
810 WPF()->current_object['status'] = 'unapproved';
811
812 return [];
813 }
814 }
815 }
816
817 if( $topic ) {
818 $topic['url'] = $this->get_url( $topic, [], false );
819 $topic['full_url'] = $this->get_full_url( $topic, [], false );
820 $topic['short_url'] = $this->get_short_url( $topic );
821 }
822
823 return apply_filters( 'wpforo_get_topic', $topic );
824 }
825
826 public function get_full_url( $topic, $forum = [], $_cache = true ): string {
827 if( ! is_array( $topic ) ) $topic = $this->get_topic( $topic, false );
828 if( $topic ) {
829 $cache = WPF()->cache->on( 'url' );
830 // $_cache is used to stop caching on merge and move actions,
831 // otherwise the merging and splitting go to redirection loop
832 if( $_cache && $cache && wpfval( $topic, 'topicid' ) ) {
833 $topic_url = (string) WPF()->cache->get_item( $topic['topicid'], 'url', 'topic' );
834 if( $topic_url ) return $topic_url;
835 }
836 if( is_array( $forum ) && ! empty( $forum ) ) {
837 $forum_slug = $forum['slug'];
838 } else {
839 $forum_slug = '';
840 if( wpfval( $topic, 'forumid' ) ) {
841 $forum_slug = $this->get_forumslug( $topic['forumid'] );
842 } elseif( wpfval( $topic, 'topicid' ) ) {
843 $forum_slug = $this->get_forumslug_byid( $topic['topicid'] );
844 }
845 }
846 if( wpfval( $topic, 'slug' ) ) {
847 $topic_url = wpforo_home_url( $forum_slug . '/' . $topic['slug'] );
848 if( $cache ) {
849 WPF()->cache->create(
850 'item',
851 [ 'topic_' . intval( $topic['topicid'] ) => $topic_url ],
852 'url'
853 );
854 }
855
856 return $topic_url;
857 }
858 }
859
860 return wpforo_home_url();
861 }
862
863 function get_forumslug( $forumid ) {
864 $slug = WPF()->db->get_var(
865 "SELECT `slug` FROM " . WPF()->tables->forums . " WHERE `forumid` = " . intval( $forumid )
866 );
867 if( $slug ) return $slug;
868
869 return 0;
870 }
871
872 function get_forumslug_byid( $topicid ) {
873 $slug = WPF()->db->get_var(
874 "SELECT `slug` FROM " . WPF()->tables->forums . " WHERE `forumid` =(SELECT forumid FROM `" . WPF()->tables->topics . "` WHERE `topicid` =" . intval( $topicid ) . ")"
875 );
876 if( $slug ) return $slug;
877
878 return 0;
879 }
880
881 public function get_short_url( $arg ) {
882 if( wpforo_is_id( $arg ) ) {
883 $topicid = $arg;
884 } elseif( wpfkey( $arg, 'topicid' ) ) {
885 $topicid = $arg['topicid'];
886 } else {
887 $topicid = 0;
888 }
889
890 if( $topicid = wpforo_bigintval( $topicid ) ) {
891 return wpforo_home_url(
892 '/' . wpforo_settings_get_slug( 'topicid' ) . '/' . $topicid . '/'
893 );
894 }
895
896 return wpforo_home_url();
897 }
898
899 /**
900 * Search in your chosen column and return array with needles
901 *
902 * @param string $needle
903 *
904 * @param array $fields can be ( slug, title, body )
905 *
906 * @return array with matches
907 * @since 1.0.0
908 *
909 */
910 function search( $needle = '', $fields = [ 'title', 'body' ] ) {
911 if( $needle !== '' ) {
912 $needle = stripslashes( (string) $needle );
913 $fields = (array) $fields;
914
915 $topicids = [];
916 foreach( $fields as $field ) {
917 $matches = [];
918 if( $field === 'body' ) {
919 // Search the needle as a whole phrase in body to find closer result
920 $matches = WPF()->db->get_col(
921 "SELECT `topicid` FROM " . WPF()->tables->posts . " WHERE `" . esc_sql(
922 $field
923 ) . "` LIKE '%" . esc_sql( sanitize_text_field( $needle ) ) . "%'"
924 );
925 } else {
926 // Search all words of the needle independently in title and other fields
927 // If the search phrase is "es lorem du ipsum dolor", then SQL becomes
928 // SELECT * FROM `wp_wpforo_topics` WHERE `title` REGEXP 'lorem|ipsum|dolor'
929 $words = array_filter(
930 explode( ' ', sanitize_text_field( $needle ) ),
931 function( $word ) { return mb_strlen( (string) $word ) > 2; }
932 );
933 if( $words ) {
934 if( apply_filters( 'wpforo_suggested_topics_search_fields_method_regexp', false ) ) {
935 $words = array_map(
936 function( $word ) {
937 return str_replace( [ '"', "'" ],
938 [ '\"', "\'" ],
939 preg_quote( preg_quote( $word ) ) );
940 },
941 $words
942 );
943 $search_mode_sql = '`' . esc_sql( $field ) . '` REGEXP "' . implode( '|', $words ) . '"';
944 } else {
945 $wheres = array_map( function( $word ) use ( $field ) {
946 return sprintf(
947 '`%1$s` LIKE \'%%%2$s%%\'',
948 esc_sql( $field ),
949 esc_sql( $word )
950 );
951 }, $words );
952 $search_mode_sql = implode( ' OR ', $wheres );
953 }
954
955 $sql = "SELECT `topicid` FROM " . WPF()->tables->topics . " WHERE " . $search_mode_sql;
956 $matches = WPF()->db->get_col( $sql );
957 }
958
959 }
960 $topicids = array_merge( $topicids, $matches );
961 }
962
963 return array_unique( array_map( 'wpforo_bigintval', $topicids ) );
964 }
965
966 return [];
967 }
968
969 function get_sum_answer( $forumids ) {
970 $sum = WPF()->db->get_var(
971 "SELECT SUM(`answers`) FROM `" . WPF()->tables->topics . "` WHERE `forumid` IN(" . implode(
972 ', ',
973 array_map( 'intval', $forumids )
974 ) . ")"
975 );
976 if( $sum ) return $sum;
977
978 return 0;
979 }
980
981 function is_sticky( $topicid ) {
982 if( $topicid ) {
983 if( WPF()->cache->on() ) {
984 $is_sticky = wpforo_topic( $topicid, 'type' );
985 } else {
986 $sql = "SELECT `type` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
987 $is_sticky = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
988 }
989
990 return (bool) $is_sticky;
991 }
992
993 return false;
994 }
995
996 function is_private( $topicid ) {
997 if( $topicid ) {
998 if( WPF()->cache->on() ) {
999 $private = wpforo_topic( $topicid, 'private' );
1000 } else {
1001 $sql = "SELECT `private` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
1002 $private = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
1003 }
1004
1005 return (bool) $private;
1006 }
1007
1008 return false;
1009 }
1010
1011 function is_unapproved( $topicid ) {
1012 if( WPF()->cache->on() ) {
1013 $status = wpforo_topic( $topicid, 'status' );
1014 } else {
1015 $status = WPF()->db->get_var(
1016 "SELECT `status` FROM " . WPF()->tables->topics . " WHERE `topicid` = " . intval( $topicid )
1017 );
1018 }
1019 if( $status == 1 ) return true;
1020
1021 return false;
1022 }
1023
1024 function is_closed( $topicid ) {
1025 if( $topicid ) {
1026 if( WPF()->cache->on() ) {
1027 $closed = wpforo_topic( $topicid, 'closed' );
1028 } else {
1029 $sql = "SELECT `closed` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
1030 $closed = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
1031 }
1032
1033 return (bool) $closed;
1034 }
1035
1036 return false;
1037 }
1038
1039 function is_solved( $topicid ) {
1040 if( $topicid ) {
1041 $sql = "SELECT `solved` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
1042 $solved = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
1043
1044 return (bool) $solved;
1045 }
1046
1047 return false;
1048 }
1049
1050 public function close( $topicid ) {
1051 $topicid = wpforo_bigintval( $topicid );
1052 $sql = "UPDATE " . WPF()->tables->topics . " SET closed = 1 WHERE topicid = %d";
1053 WPF()->db->query( WPF()->db->prepare( $sql, $topicid ) );
1054 wpforo_clean_cache( 'topic-first-post', $topicid );
1055 }
1056
1057 public function open( $topicid ) {
1058 $topicid = wpforo_bigintval( $topicid );
1059 $sql = "UPDATE " . WPF()->tables->topics . " SET closed = 0 WHERE topicid = %d";
1060 WPF()->db->query( WPF()->db->prepare( $sql, $topicid ) );
1061 wpforo_clean_cache( 'topic-first-post', $topicid );
1062 }
1063
1064 /**
1065 * Move topic to another forum
1066 *
1067 * @param int $topicid
1068 * @param int $forumid
1069 *
1070 * @return int|false $topicid on success, otherwise false
1071 * @since 1.0.0
1072 *
1073 */
1074 function move( $topicid, $forumid ) {
1075 $topic = $this->get_topic( $topicid );
1076 if( WPF()->db->query(
1077 "UPDATE `" . WPF()->tables->topics . "` SET `forumid` = " . intval(
1078 $forumid
1079 ) . " WHERE `topicid` = " . intval( $topicid )
1080 ) ) {
1081 WPF()->db->query(
1082 "UPDATE `" . WPF()->tables->posts . "` SET `forumid` = " . intval(
1083 $forumid
1084 ) . " WHERE `topicid` = " . intval( $topicid )
1085 );
1086
1087 do_action( 'wpforo_after_move_topic', $topic, $forumid );
1088
1089 wpforo_clean_cache( 'topic', $topicid, $topic );
1090 WPF()->notice->add( 'Done!', 'success' );
1091
1092 return $topicid;
1093 }
1094
1095 WPF()->notice->add( 'Topic Move Error', 'error' );
1096
1097 return false;
1098 }
1099
1100 public function split( $args, $to_target_title = 0 ) {
1101 if( ! $args ) return false;
1102
1103 $args['name'] = ( isset( $args['name'] ) ? strip_tags( (string) $args['name'] ) : '' );
1104 $args['email'] = ( isset( $args['email'] ) ? sanitize_email( $args['email'] ) : '' );
1105
1106 if( ! isset( $args['forumid'] ) || ! $args['forumid'] = intval( $args['forumid'] ) ) {
1107 WPF()->notice->add( 'Please select a target forum', 'error' );
1108
1109 return false;
1110 }
1111
1112 if( ! isset( $args['title'] ) || ! $args['title'] = trim( strip_tags( (string) $args['title'] ) ) ) {
1113 WPF()->notice->add( 'Please insert required fields', 'error' );
1114
1115 return false;
1116 }
1117
1118 if( empty( $args['postids'] ) ) {
1119 WPF()->notice->add( 'Please select at least one post to split', 'error' );
1120
1121 return false;
1122 }
1123
1124 $args['postids'] = array_values( $args['postids'] );
1125
1126 if( $fpost = WPF()->post->get_post( $args['postids'][0] ) ) {
1127 $args['title'] = wpforo_text( $args['title'], 250, false );
1128 $args['slug'] = ( isset( $args['slug'] ) && $args['slug'] ) ? sanitize_title(
1129 $args['slug']
1130 ) : ( ( isset( $args['title'] ) ) ? sanitize_title( $args['title'] ) : md5( time() ) );
1131 if( ! trim( $args['slug'] ) ) $args['slug'] = md5( time() );
1132 $args['slug'] = $this->unique_slug( $args['slug'] );
1133
1134
1135 $args['body'] = $fpost['body'];
1136 $args['created'] = $fpost['created'];
1137 $args['userid'] = $fpost['userid'];
1138 $args['name'] = $fpost['name'];
1139 $args['email'] = $fpost['email'];
1140
1141 // Security: extract only expected keys to prevent variable injection
1142 $forumid = isset( $args['forumid'] ) ? $args['forumid'] : null;
1143 $title = isset( $args['title'] ) ? $args['title'] : null;
1144 $slug = isset( $args['slug'] ) ? $args['slug'] : null;
1145 $created = isset( $args['created'] ) ? $args['created'] : null;
1146 $userid = isset( $args['userid'] ) ? $args['userid'] : null;
1147 $body = isset( $args['body'] ) ? $args['body'] : null;
1148 $name = isset( $args['name'] ) ? $args['name'] : null;
1149 $email = isset( $args['email'] ) ? $args['email'] : null;
1150 $type = isset( $args['type'] ) ? $args['type'] : null;
1151 $status = isset( $args['status'] ) ? $args['status'] : null;
1152 $private = isset( $args['private'] ) ? $args['private'] : null;
1153 $has_attach = isset( $args['has_attach'] ) ? $args['has_attach'] : null;
1154
1155 if( isset( $forumid ) ) $forumid = intval( $forumid );
1156 if( isset( $title ) ) $title = sanitize_text_field( trim( (string) $title ) );
1157 if( isset( $slug ) ) $slug = sanitize_title( $slug );
1158 if( isset( $created ) ) $created = sanitize_text_field( $created );
1159 if( isset( $userid ) ) $userid = intval( $userid );
1160 $type = ( isset( $type ) && $type ? 1 : 0 );
1161 $status = ( isset( $status ) && $status ? 1 : 0 );
1162 $private = ( isset( $private ) && $private ? 1 : 0 );
1163 if( isset( $name ) ) $name = strip_tags( trim( (string) $name ) );
1164 if( isset( $email ) ) $email = strip_tags( trim( (string) $email ) );
1165 $has_attach = ( isset( $has_attach ) && $has_attach ) ? 1 : ( ( strpos(
1166 (string) $body,
1167 '[attach]'
1168 ) !== false ) ? 1 : 0 );
1169
1170 if( WPF()->db->insert(
1171 WPF()->tables->topics,
1172 [
1173 'title' => stripslashes(
1174 $title
1175 ),
1176 'slug' => $slug,
1177 'forumid' => $forumid,
1178 'userid' => $userid,
1179 'type' => $type,
1180 'status' => $status,
1181 'private' => $private,
1182 'created' => $created,
1183 'modified' => $created,
1184 'last_post' => 0,
1185 'views' => 0,
1186 'posts' => 1,
1187 'has_attach' => $has_attach,
1188 'name' => $name,
1189 'email' => $email,
1190 ],
1191 [
1192 '%s',
1193 '%s',
1194 '%d',
1195 '%d',
1196 '%d',
1197 '%d',
1198 '%d',
1199 '%s',
1200 '%s',
1201 '%d',
1202 '%d',
1203 '%d',
1204 '%d',
1205 '%s',
1206 '%s',
1207 ]
1208 ) ) {
1209 $args['topicid'] = $topicid = WPF()->db->insert_id;
1210
1211 if( $this->merge( $args, WPF()->current_object['topic'], $args['postids'], $to_target_title ) ) {
1212 WPF()->notice->clear();
1213 WPF()->notice->add( 'Done!', 'success' );
1214
1215 return $topicid;
1216 }
1217 }
1218 }
1219
1220 WPF()->notice->add( 'Topic splitting error', 'error' );
1221
1222 return false;
1223 }
1224
1225 /**
1226 * merge topic with target topic
1227 *
1228 * @param array $target target topic array
1229 * @param array $current current topic array
1230 * @param array $postids current topic postids array
1231 * @param int $to_target_title Update post titles with target topic title
1232 * @param int $append Update post dates and append to end
1233 *
1234 * @return bool true|false true on success, otherwise false
1235 */
1236 public function merge( $target, $current = [], $postids = [], $to_target_title = 0, $append = 0 ) {
1237
1238 if( ! $current ) $current = WPF()->current_object['topic'];
1239
1240 $sql = "UPDATE `" . WPF()->tables->posts . "` SET `topicid` = %d, `forumid` = %d, `private` = %d,`is_first_post` = 0";
1241 $sql = WPF()->db->prepare( $sql, $target['topicid'], $target['forumid'], (int) wpfval( $target, 'private' ) );
1242
1243 if( $append ) {
1244 $sql .= ", `modified` = %s, `created` = %s";
1245 $sql = WPF()->db->prepare( $sql, current_time( 'mysql', 1 ), current_time( 'mysql', 1 ) );
1246 }
1247
1248 if( $to_target_title ) {
1249 $layout = WPF()->forum->get_layout( $target['forumid'] );
1250 $phrase = ( $layout == 3 ? wpforo_phrase( 'Answer to', false ) : wpforo_phrase( 'RE', false ) );
1251 $title = $phrase . ': ' . $target['title'];
1252 $sql .= ", `title` = %s";
1253 $sql = WPF()->db->prepare( $sql, $title );
1254 }
1255
1256 $sql .= " WHERE `topicid` = %d";
1257 $sql = WPF()->db->prepare( $sql, $current['topicid'] );
1258
1259 if( $postids ) {
1260 $postids = (array) $postids;
1261 $postids = array_map( 'wpforo_bigintval', $postids );
1262
1263 $sql .= " AND `postid` IN(" . implode( ',', $postids ) . ")";
1264 }
1265
1266 do_action( 'wpforo_before_merge_topic', $target, $current, $postids, $to_target_title, $append );
1267
1268 $db_resp = WPF()->db->query( $sql );
1269
1270 if( $db_resp !== false ) {
1271 $sql = "SELECT COUNT(*) FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d";
1272 $sql = WPF()->db->prepare( $sql, $current['topicid'] );
1273 if( ! WPF()->db->get_var( $sql ) ) {
1274 $this->delete( $current['topicid'], true, false );
1275 } else {
1276 $this->rebuild_first_last( $current );
1277 $this->rebuild_stats( $current );
1278 $this->rebuild_threads( $current );
1279 wpforo_clean_cache( 'topic', $current['topicid'], $current );
1280 }
1281
1282 $this->rebuild_first_last( $target );
1283 $this->rebuild_stats( $target );
1284 $this->rebuild_threads( $target );
1285
1286 do_action( 'wpforo_after_merge_topic', $target, $current, $postids, $to_target_title, $append );
1287
1288 WPF()->notice->clear();
1289 WPF()->notice->add( 'Done!', 'success' );
1290
1291 wpforo_clean_cache();
1292
1293 return true;
1294 }
1295
1296 WPF()->notice->add( 'Data merging error', 'error' );
1297
1298 return false;
1299 }
1300
1301 /**
1302 * Delete topic from DB
1303 *
1304 * Returns true if successfully deleted or false.
1305 *
1306 * @param int $topicid
1307 * @param bool $delete_cache
1308 *
1309 * @return bool
1310 * @since 1.0.0
1311 *
1312 */
1313 function delete( $topicid = 0, $delete_cache = true, $check_permissions = true ) {
1314 $topicid = intval( $topicid );
1315 if( ! $topicid && isset( $_REQUEST['id'] ) ) $topicid = intval( $_REQUEST['id'] );
1316
1317 if( ! $topic = $this->get_topic( $topicid ) ) return true;
1318
1319 do_action( 'wpforo_before_delete_topic', $topic );
1320
1321 if( $check_permissions ) {
1322 $diff = time() - strtotime( $topic['created'] . ' GMT' );
1323 if( ! ( WPF()->perm->forum_can( 'dt', $topic['forumid'] ) || ( WPF()->current_userid == $topic['userid'] && WPF()->perm->forum_can(
1324 'dot',
1325 $topic['forumid']
1326 ) ) ) ) {
1327 WPF()->notice->add( 'You don\'t have permission to delete topic from this forum.', 'error' );
1328
1329 return false;
1330 }
1331
1332 if( ! WPF()->perm->forum_can( 'dt', $topic['forumid'] ) && wpforo_setting(
1333 'posting',
1334 'delete_own_topic_durr'
1335 ) !== 0 && $diff > wpforo_setting(
1336 'posting',
1337 'delete_own_topic_durr'
1338 ) ) {
1339 WPF()->notice->add( 'The time to delete this topic is expired.', 'error' );
1340
1341 return false;
1342 }
1343 }
1344
1345 // START delete topic posts include first post
1346 if( $postids = WPF()->db->get_col(
1347 WPF()->db->prepare(
1348 "SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d ORDER BY `is_first_post`",
1349 $topicid
1350 )
1351 ) ) {
1352 $exclude = [];
1353 foreach( $postids as $postid ) {
1354 if( $postid == $topic['first_postid'] ) {
1355 return WPF()->post->delete( $postid, false, false, $exclude, false );
1356 } else {
1357 WPF()->post->delete( $postid, false, false, $exclude, false );
1358 }
1359 }
1360 }
1361 // END delete topic posts include first post
1362
1363 if( WPF()->db->delete( WPF()->tables->topics, [ 'topicid' => $topicid ], [ '%d' ] ) ) {
1364 if( wpfval( $topic, 'tags' ) ) $this->remove_tags( $topic['tags'] );
1365
1366 do_action( 'wpforo_after_delete_topic', $topic );
1367
1368 if( $delete_cache ) wpforo_clean_cache( 'topic', $topicid, $topic );
1369 WPF()->notice->add( 'This topic successfully deleted', 'success' );
1370
1371 return true;
1372 }
1373
1374 WPF()->notice->add( 'Topics delete error', 'error' );
1375
1376 return false;
1377 }
1378
1379 public function rebuild_first_last( $topic ) {
1380 if( ! $topic ) return false;
1381 if( wpforo_is_id( $topic ) ) $topic = $this->get_topic( $topic );
1382 if( ! is_array( $topic ) || ! $topic ) return false;
1383
1384 $sql = "SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d ORDER BY `is_first_post` DESC, `created` ASC, `postid` ASC LIMIT 1";
1385 if( $first_postid = WPF()->db->get_var( WPF()->db->prepare( $sql, $topic['topicid'] ) ) ) {
1386 $sql = "UPDATE `" . WPF()->tables->posts . "` SET `is_first_post` = 1 WHERE `postid` = %d";
1387 WPF()->db->query( WPF()->db->prepare( $sql, $first_postid ) );
1388
1389 do_action( 'wpforo_after_is_first_post_update', $first_postid, 1 );
1390 } else {
1391 $first_postid = 0;
1392 }
1393
1394 $sql = "SELECT `postid`, `created`
1395 FROM `" . WPF()->tables->posts . "`
1396 WHERE `topicid` = %d
1397 ORDER BY `is_first_post` ASC, `created` DESC, `postid` DESC LIMIT 1";
1398 if( ! $last_post = WPF()->db->get_row( WPF()->db->prepare( $sql, $topic['topicid'] ), ARRAY_A ) ) {
1399 $last_post = [ 'postid' => 0, 'created' => $topic['modified'] ];
1400 }
1401
1402 if( $r = WPF()->db->update(
1403 WPF()->tables->topics,
1404 [
1405 'first_postid' => $first_postid,
1406 'last_post' => $last_post['postid'],
1407 'modified' => $last_post['created'],
1408 ],
1409 [ 'topicid' => $topic['topicid'] ],
1410 [ '%d', '%d', '%s' ], [ '%d' ]
1411 ) ) {
1412 wpforo_clean_cache( 'topic-first-post' );
1413 }
1414
1415 return $r !== false;
1416 }
1417
1418 public function rebuild_stats( $topic ) {
1419 if( ! $topic ) return false;
1420 if( wpforo_is_id( $topic ) ) $topic = $this->get_topic( $topic );
1421 if( ! is_array( $topic ) || ! $topic ) return false;
1422
1423 $posts = WPF()->post->get_count( [ 'topicid' => $topic['topicid'], 'status' => 0 ] );
1424
1425 $data = [ 'posts' => $posts, 'answers' => 0 ];
1426 $data_format = [ '%d', '%d' ];
1427
1428 $layout = WPF()->forum->get_layout( $topic['forumid'] );
1429 if( $layout === 3 ) {
1430 $data['answers'] = WPF()->post->get_count(
1431 [ 'topicid' => $topic['topicid'], 'status' => 0, 'parentid' => 0, 'is_first_post' => 0 ]
1432 );
1433 $data['posts'] = $posts - 1;
1434 }
1435
1436 if( $r = WPF()->db->update(
1437 WPF()->tables->topics,
1438 $data,
1439 [ 'topicid' => $topic['topicid'] ],
1440 $data_format,
1441 [ '%d' ]
1442 ) ) {
1443 wpforo_clean_cache( 'topic-first-post', $topic['topicid'], $topic );
1444 }
1445
1446 return $r !== false;
1447 }
1448
1449 function get_count( $args = [] ) {
1450 $sql = "SELECT SQL_NO_CACHE COUNT(*) FROM `" . WPF()->tables->topics . "`";
1451 if( ! empty( $args ) ) {
1452 $wheres = [];
1453 foreach( $args as $key => $value ) $wheres[] = "`$key` = '" . esc_sql( $value ) . "'";
1454 if( $wheres ) $sql .= " WHERE " . implode( ' AND ', $wheres );
1455 }
1456
1457 return WPF()->db->get_var( $sql );
1458 }
1459
1460 function rebuild_threads( $topicid, $root = null ) {
1461 if( ! is_null( $root ) && $root <= 0 ) return;
1462 if( is_array( $topicid ) && wpfval( $topicid, 'topicid' ) ) $topicid = $topicid['topicid'];
1463 if( $topicid ) {
1464 $posts = WPF()->db->get_results(
1465 "SELECT * FROM `" . WPF()->tables->posts . "` WHERE `topicid` = " . intval( $topicid ),
1466 ARRAY_A
1467 );
1468 if( ! empty( $posts ) ) {
1469 $threads = [];
1470 foreach( $posts as $post ) {
1471 $threads[ $post['postid'] ] = [ 'postid' => $post['postid'], 'parentid' => $post['parentid'] ];
1472 }
1473 unset( $posts );
1474 foreach( $threads as $item ) {
1475 if( ! is_null( $root ) && $root != $item['postid'] ) continue;
1476 if( ! $item['parentid'] ) {
1477 $thread = WPF()->post->build_thread_data( $item['postid'], $threads );
1478 if( wpfval( $thread, 'children' ) ) {
1479 $children = implode( ',', $thread['children'] );
1480 WPF()->db->query(
1481 "UPDATE `" . WPF()->tables->posts . "` SET `root` = 0 WHERE `postid` = " . intval(
1482 $item['postid']
1483 )
1484 );
1485 WPF()->db->query(
1486 "UPDATE `" . WPF()->tables->posts . "` SET `root` = " . intval(
1487 $item['postid']
1488 ) . " WHERE `postid` IN(" . esc_sql( $children ) . ")"
1489 );
1490 }
1491 } elseif( ! wpfkey( $threads, $item['parentid'] ) ) {
1492 WPF()->db->query(
1493 "UPDATE `" . WPF()->tables->posts . "` SET `root` = 0, `parentid` = 0 WHERE `postid` = " . intval(
1494 $item['postid']
1495 )
1496 );
1497 }
1498 }
1499 }
1500 }
1501 }
1502
1503 /**
1504 * @param $topic
1505 * @param $forum
1506 * @param $_cache
1507 *
1508 * @return string
1509 * @deprecated since 2.0.10 instead this method use _get_url()
1510 *
1511 */
1512 public function _get_topic_url( $topic, $forum = [], $_cache = true ) {
1513 return $this->_get_url( $topic, $forum, $_cache );
1514 }
1515
1516 /**
1517 * @param $topic
1518 * @param $forum
1519 * @param $_cache
1520 *
1521 * @return string
1522 */
1523 public function _get_url( $topic, $forum = [], $_cache = true ) {
1524 if( wpforo_setting( 'board', 'url_structure' ) === 'short' ) return $this->get_short_url( $topic );
1525
1526 return $this->get_full_url( $topic, $forum, $_cache );
1527 }
1528
1529 /**
1530 * @param $topic
1531 * @param $forum
1532 * @param $_cache
1533 *
1534 * @return string
1535 * @deprecated since 2.0.10 instead this method use get_url()
1536 *
1537 */
1538 public function get_topic_url( $topic, $forum = [], $_cache = true ) {
1539 return $this->get_url( $topic, $forum, $_cache );
1540 }
1541
1542 public function set_status( $topicid, $status ) {
1543 $topicid = wpforo_bigintval( $topicid );
1544 $status = intval( $status );
1545 if( ! $topic = $this->get_topic( $topicid, false ) ) return false;
1546
1547 if( $r = WPF()->db->update(
1548 WPF()->tables->topics,
1549 [ 'status' => $status ],
1550 [ 'topicid' => $topicid ],
1551 [ '%d' ], [ '%d' ]
1552 ) ) {
1553 if( $status ) {
1554 do_action( 'wpforo_topic_unapprove', $topic );
1555 if( wpfval( $topic, 'tags' ) ) $this->remove_tags( $topic['tags'] );
1556 } else {
1557 do_action( 'wpforo_topic_approve', $topic );
1558 if( wpfval( $topic, 'tags' ) ) $this->add_tags( $topic['tags'] );
1559 }
1560
1561 do_action( 'wpforo_topic_status_update', $topic, $status );
1562 wpforo_clean_cache( 'topic-first-post', $topicid );
1563 }
1564
1565 if( $r !== false ) {
1566 WPF()->notice->add( 'Done!', 'success' );
1567
1568 return true;
1569 }
1570
1571 WPF()->notice->add( 'Status changing error', 'error' );
1572
1573 return false;
1574 }
1575
1576 public function wprivate( $topicid, $private ) {
1577 WPF()->db->update(
1578 WPF()->tables->topics,
1579 [ 'private' => $private ],
1580 [ 'topicid' => $topicid ],
1581 [ '%d' ], [ '%d' ]
1582 );
1583
1584 WPF()->db->update(
1585 WPF()->tables->posts,
1586 [ 'private' => $private ],
1587 [ 'topicid' => $topicid ],
1588 [ '%d' ], [ '%d' ]
1589 );
1590
1591 do_action( 'wpforo_topic_private_update', $topicid, $private );
1592 wpforo_clean_cache();
1593
1594 return true;
1595 }
1596
1597 public function delete_attachments( $topicid ) {
1598 $args = [ 'topicid' => $topicid ];
1599 $posts = WPF()->post->get_posts( $args );
1600 if( ! empty( $posts ) ) {
1601 foreach( $posts as $post ) {
1602 WPF()->post->delete_attachments( $post['postid'] );
1603 }
1604 }
1605 }
1606
1607 function rebuild_forum_threads( $forumid = 0 ) {
1608 if( ! $forumid ) {
1609 $args = [ 'layout' => 4 ];
1610 $forums = WPF()->forum->get_forums( $args );
1611 if( ! empty( $forums ) ) {
1612 foreach( $forums as $forum ) {
1613 $args = [ 'forumid' => $forum['forumid'] ];
1614 $topics = $this->get_topics( $args );
1615 if( ! empty( $topics ) ) {
1616 foreach( $topics as $topic ) {
1617 $this->rebuild_threads( $topic['topicid'] );
1618 }
1619 }
1620 }
1621 }
1622 } else {
1623 $args = [ 'forumid' => $forumid ];
1624 $topics = $this->get_topics( $args );
1625 if( ! empty( $topics ) ) {
1626 foreach( $topics as $topic ) {
1627 $this->rebuild_threads( $topic['topicid'] );
1628 }
1629 }
1630 }
1631 }
1632
1633 /**
1634 * array get_topic(array or id(num))
1635 * Returns merged arguments array from defined and default arguments.
1636 *
1637 * @return array where count is topic count and other numeric arrays with topic
1638 * @since 1.0.0
1639 *
1640 */
1641 function get_topics( $args = [], &$items_count = 0, $count = true ) {
1642 $cache = WPF()->cache->on( 'topic' );
1643
1644 $default = [
1645 'include' => [], // array( 2, 10, 25 )
1646 'exclude' => [], // array( 2, 10, 25 )
1647 'forumids' => [],
1648 'forumid' => null,
1649 'userid' => null, // user id in DB
1650 'type' => null, //0, 1, etc . . .
1651 'solved' => null,
1652 'closed' => null,
1653 'status' => null, //0, 1, etc . . .
1654 'private' => null, //0, 1, etc . . .''
1655 'pollid' => null,
1656 'orderby' => 'type, topicid', // type, topicid, modified, created
1657 'order' => 'DESC', // ASC DESC
1658 'offset' => null, // this use when you give row_count
1659 'row_count' => null, // 4 or 1 ...
1660 'permgroup' => null, //Checks permissions based on attribute value not on current user usergroup
1661 'read' => null, //true / false
1662 'prefix' => null, //23 / 23,24,50
1663 'where' => null,
1664 'access_filter' => true, //false to skip forum permission filtering (admin backend ops)
1665 ];
1666
1667 $args = wpforo_parse_args( $args, $default );
1668
1669 // Security: extract only expected keys instead of extract()
1670 $include = $args['include'];
1671 $exclude = $args['exclude'];
1672 $forumids = $args['forumids'];
1673 $forumid = $args['forumid'];
1674 $userid = $args['userid'];
1675 $type = $args['type'];
1676 $solved = $args['solved'];
1677 $closed = $args['closed'];
1678 $status = $args['status'];
1679 $private = $args['private'];
1680 $pollid = $args['pollid'];
1681 $orderby = $args['orderby'];
1682 $order = $args['order'];
1683 $offset = $args['offset'];
1684 $row_count = $args['row_count'];
1685 $permgroup = $args['permgroup'];
1686 $read = $args['read'];
1687 $prefix = $args['prefix'];
1688 $where = $args['where'];
1689 $access_filter = $args['access_filter'];
1690
1691 if( $row_count === 0 ) return [];
1692
1693 $include = wpforo_parse_args( $include );
1694 $exclude = wpforo_parse_args( $exclude );
1695 $forumids = wpforo_parse_args( $forumids );
1696
1697 $guest = [];
1698 $wheres = [];
1699
1700 if( ! is_null( $prefix ) ) {
1701 $prefixes = explode( ',', $prefix );
1702 if( ! empty( $prefixes ) ) {
1703 foreach( $prefixes as $prefixid ) {
1704 $wheres[] = " FIND_IN_SET('" . intval( $prefixid ) . "', `prefix`) ";
1705 }
1706 }
1707 }
1708
1709 if( ! is_null( $read ) ) {
1710 $last_read_postid = WPF()->log->get_all_read( 'post' );
1711 if( $read ) {
1712 if( $last_read_postid ) {
1713 $wheres[] = "`last_post` <= " . intval( $last_read_postid );
1714 }
1715 $include_read = WPF()->log->get_read();
1716 $include = array_merge( $include, $include_read );
1717 } else {
1718 if( $last_read_postid ) {
1719 $wheres[] = "`last_post` > " . intval( $last_read_postid );
1720 }
1721 $exclude_read = WPF()->log->get_read();
1722 $exclude = array_merge( $exclude, $exclude_read );
1723 }
1724 }
1725
1726 if( ! empty( $include ) ) $wheres[] = "`topicid` IN(" . implode( ', ', array_map( 'intval', $include ) ) . ")";
1727 if( ! empty( $exclude ) ) {
1728 $wheres[] = "`topicid` NOT IN(" . implode(
1729 ', ',
1730 array_map( 'intval', $exclude )
1731 ) . ")";
1732 }
1733 if( ! empty( $forumids ) ) {
1734 $wheres[] = "`forumid` IN(" . implode(
1735 ', ',
1736 array_map( 'intval', $forumids )
1737 ) . ")";
1738 }
1739 if( ! is_null( $forumid ) ) $wheres[] = "`forumid` = " . intval( $forumid );
1740 if( ! is_null( $userid ) ) $wheres[] = "`userid` = " . wpforo_bigintval( $userid );
1741 if( ! is_null( $solved ) ) $wheres[] = "`solved` = " . intval( $solved );
1742 if( ! is_null( $closed ) ) $wheres[] = "`closed` = " . intval( $closed );
1743 if( ! is_null( $type ) ) $wheres[] = "`type` = " . intval( $type );
1744 if( ! is_null( $where ) ) $wheres[] = $where;
1745
1746 if( ! is_user_logged_in() ) $guest = WPF()->member->get_guest_cookies();
1747
1748 if( empty( $forumids ) ) {
1749 if( isset( $forumid ) && ! WPF()->perm->forum_can( 'vf', $forumid, $permgroup ) ) {
1750 return [];
1751 }
1752 }
1753
1754 if( isset( $forumid ) && $forumid ) {
1755 if( WPF()->perm->forum_can( 'vp', $forumid, $permgroup ) ) {
1756 if( ! is_null( $private ) ) $wheres[] = " `private` = " . intval( $private );
1757 } elseif( isset( WPF()->current_userid ) && WPF()->current_userid ) {
1758 $wheres[] = " ( `private` = 0 OR (`private` = 1 AND `userid` = " . WPF()->current_userid . ") )";
1759 } elseif( wpfval( $guest, 'email' ) ) {
1760 $wheres[] = " ( `private` = 0 OR (`private` = 1 AND `email` = '" . sanitize_email(
1761 $guest['email']
1762 ) . "') )";
1763 } else {
1764 $wheres[] = " `private` = 0";
1765 }
1766 } else {
1767 if( ! is_null( $private ) ) $wheres[] = " `private` = " . intval( $private );
1768 }
1769
1770 if( isset( $forumid ) && $forumid ) {
1771 if( WPF()->perm->forum_can( 'au', $forumid, $permgroup ) ) {
1772 if( ! is_null( $status ) ) $wheres[] = " `status` = " . intval( $status );
1773 } elseif( isset( WPF()->current_userid ) && WPF()->current_userid ) {
1774 $wheres[] = " ( `status` = 0 OR (`status` = 1 AND `userid` = " . WPF()->current_userid . ") )";
1775 } elseif( wpfval( $guest, 'email' ) ) {
1776 $wheres[] = " ( `status` = 0 OR (`status` = 1 AND `email` = '" . sanitize_email(
1777 $guest['email']
1778 ) . "') )";
1779 } else {
1780 $wheres[] = " `status` = 0";
1781 }
1782 } else {
1783 if( ! is_null( $status ) ) $wheres[] = " `status` = " . intval( $status );
1784 }
1785
1786 if( function_exists( 'WPF_POLL' ) ) {
1787 if( ! is_null( $pollid ) ) $wheres[] = " `pollid` <> 0";
1788 }
1789
1790 $wheres = apply_filters( 'wpforo_get_topics_sql_wheres', $wheres );
1791
1792 $sql = "SELECT * FROM `" . WPF()->tables->topics . "`";
1793 if( ! empty( $wheres ) ) {
1794 $sql .= " WHERE " . implode( " AND ", $wheres );
1795 }
1796
1797 if( $count ) {
1798 $item_count_sql = preg_replace( '#SELECT.+?FROM#isu', 'SELECT count(*) FROM', $sql );
1799 if( $item_count_sql ) $items_count = WPF()->db->get_var( $item_count_sql );
1800 }
1801
1802 if( $order === 'RAND' ) {
1803 $sql .= " ORDER BY RAND()";
1804 } else {
1805 $sql .= " ORDER BY " . str_replace( ',', ' ' . esc_sql( $order ) . ',', esc_sql( $orderby ) ) . " " . esc_sql(
1806 $order
1807 );
1808 }
1809
1810 if( ! is_null( $row_count ) ) {
1811 if( ! is_null( $offset ) ) {
1812 $sql .= esc_sql( " LIMIT $offset,$row_count" );
1813 } else {
1814 $sql .= esc_sql( " LIMIT $row_count" );
1815 }
1816 }
1817
1818 if( $cache ) {
1819 $object_key = md5( $sql . WPF()->current_user_groupid );
1820 $object_cache = WPF()->cache->get( $object_key );
1821 if( ! empty( $object_cache ) ) {
1822 if( ! empty( $object_cache['items'] ) ) {
1823 $do_access_filter = $access_filter && apply_filters( 'wpforo_topic_access_filter_cache', true );
1824 if( $do_access_filter ) {
1825 $object_cache['items'] = $this->access_filter(
1826 $object_cache['items'],
1827 [
1828 'groupids' => ( array_filter(
1829 array_map( 'intval', (array) $permgroup )
1830 ) ?: null ),
1831 ]
1832 );
1833 }
1834 return apply_filters( 'wpforo_get_topics', $object_cache['items'] );
1835 }
1836 }
1837 }
1838
1839 $topics = WPF()->db->get_results( $sql, ARRAY_A );
1840
1841 if( $cache && isset( $object_key ) && ! empty( $topics ) ) {
1842 self::$cache['topics'][ $object_key ]['items'] = $topics;
1843 self::$cache['topics'][ $object_key ]['items_count'] = $items_count;
1844 }
1845
1846 if( $access_filter && ( ! empty( $forumids ) || ! $forumid ) ) {
1847 $topics = $this->access_filter(
1848 $topics,
1849 [
1850 'groupids' => ( array_filter(
1851 array_map( 'intval', (array) $permgroup )
1852 ) ?: null ),
1853 ]
1854 );
1855 }
1856
1857 return apply_filters( 'wpforo_get_topics', $topics );
1858 }
1859
1860 function access_filter( $topics, $user = null ) {
1861 if( ! empty( $topics ) ) {
1862 foreach( $topics as $key => $topic ) {
1863 if( ! $this->view_access( $topic, $user ) ) unset( $topics[ $key ] );
1864 }
1865 }
1866
1867 return $topics;
1868 }
1869
1870 function view_access( $topic, $user = null ) {
1871 $groupids = wpfval( $user, 'groupids' );
1872 if( ! WPF()->perm->forum_can( 'vf', $topic['forumid'], $groupids ) ) {
1873 return apply_filters(
1874 'wpforo_topic_view_access',
1875 false,
1876 $topic,
1877 $user
1878 );
1879 }
1880 if( ! WPF()->perm->forum_can( 'vt', $topic['forumid'], $groupids ) ) {
1881 return apply_filters(
1882 'wpforo_topic_view_access',
1883 false,
1884 $topic,
1885 $user
1886 );
1887 }
1888 if( ! wpforo_is_users_same( wpforo_member( $topic ), $user ) && ( ( (int) wpfval( $topic, 'private' ) && ! WPF()->perm->forum_can( 'vp', $topic['forumid'], $groupids ) ) || ( (int) wpfval(
1889 $topic,
1890 'status'
1891 ) && ! WPF()->perm->forum_can( 'au', $topic['forumid'], $groupids ) ) ) ) {
1892 return apply_filters( 'wpforo_topic_view_access', false, $topic, $user );
1893 }
1894
1895 return apply_filters( 'wpforo_topic_view_access', true, $topic, $user );
1896 }
1897
1898 public function members( $topicid, $limit = 0 ) {
1899 if( ! $topicid ) return [];
1900 $members = [];
1901 $args = [
1902 'topicid' => $topicid,
1903 'orderby' => 'created',
1904 'order' => 'ASC',
1905 'private' => 0,
1906 'status' => 0,
1907 'cache_type' => 'args',
1908 ];
1909 $posts = WPF()->post->get_posts( $args );
1910 foreach( $posts as $post ) {
1911 if( wpfval( $post, 'userid' ) ) {
1912 $members[ $post['userid'] ] = wpforo_member( $post['userid'] );
1913 if( $limit && count( $members ) >= $limit ) break;
1914 }
1915 }
1916
1917 return array_filter( $members );
1918 }
1919
1920 public function can_answer( $topicid ) {
1921 if( ! $topicid ) return false;
1922 $topic = wpforo_topic( $topicid );
1923 if( wpfval( $topic, 'topicid' ) ) {
1924 if( wpfval( $topic, 'userid' ) ) {
1925 if( ! WPF()->perm->forum_can( 'aot', $topic['forumid'] ) && WPF()->current_userid == $topic['userid'] ) {
1926 return false;
1927 }
1928 } else {
1929 $guest = WPF()->member->get_guest_cookies();
1930 if( wpfval( $topic, 'email' ) && wpfval( $guest, 'email' ) ) {
1931 if( ! WPF()->perm->forum_can( 'aot', $topic['forumid'] ) && $topic['email'] == $guest['email'] ) {
1932 return false;
1933 }
1934 }
1935 }
1936 }
1937
1938 return true;
1939 }
1940
1941 /**
1942 * @param $topicid
1943 *
1944 * @return bool
1945 */
1946 public function has_is_answer_post( $topicid ) {
1947 if( ! $topicid ) return false;
1948 $sql = "SELECT EXISTS ( SELECT * FROM `" . WPF()->tables->posts . "` WHERE `is_answer` = 1 AND `topicid` = %d) AS is_exists";
1949 $sql = WPF()->db->prepare( $sql, $topicid );
1950
1951 return (bool) WPF()->db->get_var( $sql );
1952 }
1953
1954 public function get_postids( $topicid ) {
1955 $sql = "SELECT `postid`
1956 FROM `" . WPF()->tables->posts . "`
1957 WHERE `topicid` = %d";
1958 $sql = WPF()->db->prepare( $sql, $topicid );
1959
1960 return array_map( 'wpforo_bigintval', WPF()->db->get_col( $sql ) );
1961 }
1962
1963 public function edit_tag( $tag, $old_tag = '', $disconnect = false ) {
1964
1965 $tag_id = wpfval( $tag, 'tagid' );
1966 $tag_name = wpfval( $tag, 'tag' );
1967 $tag_name = $this->sanitize_tag( $tag_name );
1968
1969 if( false !== WPF()->db->update(
1970 WPF()->tables->tags,
1971 [ 'tag' => $tag_name ],
1972 [ 'tagid' => intval( $tag_id ) ],
1973 [ '%s' ], [ '%d' ]
1974 ) ) {
1975 if( $old_tag ) {
1976 if( $disconnect ) {
1977 $this->remove_tag_from_topics( $old_tag );
1978 WPF()->db->update( WPF()->tables->tags, [ 'count' => 0 ], [ 'tag' => $tag_name ], [ '%d' ], [ '%s' ] );
1979 } else {
1980 if( $old_tag != $tag_name ) {
1981 $this->update_tag_in_topics( $tag_name, $old_tag );
1982 }
1983 $count = WPF()->db->get_var(
1984 "SELECT COUNT(*) FROM `" . WPF()->tables->topics . "` WHERE FIND_IN_SET('" . esc_sql(
1985 $tag_name
1986 ) . "', tags)"
1987 );
1988 WPF()->db->update(
1989 WPF()->tables->tags,
1990 [ 'count' => intval( $count ) ],
1991 [ 'tag' => $tag_name ],
1992 [ '%d' ], [ '%s' ]
1993 );
1994 }
1995 }
1996 wpforo_clean_cache( 'tag', $tag_name );
1997
1998 return true;
1999 }
2000
2001 return false;
2002 }
2003
2004 public function remove_tag_from_topics( $tag ) {
2005 if( $tag ) {
2006 WPF()->db->query(
2007 "UPDATE `" . WPF()->tables->topics . "`
2008 SET tags = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', tags, ','), '," . esc_sql(
2009 $tag
2010 ) . ",', ','))
2011 WHERE FIND_IN_SET('" . esc_sql( $tag ) . "', tags)"
2012 );
2013 }
2014 }
2015
2016 public function update_tag_in_topics( $new_tag, $old_tag ) {
2017 if( $new_tag && $old_tag ) {
2018
2019 WPF()->db->query(
2020 "UPDATE `" . WPF()->tables->topics . "`
2021 SET tags = '" . esc_sql( $new_tag ) . "'
2022 WHERE tags LIKE '" . esc_sql( $old_tag ) . "'"
2023 );
2024
2025 WPF()->db->query(
2026 "UPDATE `" . WPF()->tables->topics . "`
2027 SET tags = REPLACE(tags, '," . esc_sql( $old_tag ) . ",', '," . esc_sql(
2028 $new_tag
2029 ) . ",')
2030 WHERE tags LIKE '%," . esc_sql( $old_tag ) . ",%'"
2031 );
2032
2033 $topics = WPF()->db->get_results(
2034 "SELECT `topicid`, `tags`
2035 FROM `" . WPF()->tables->topics . "`
2036 WHERE FIND_IN_SET('" . esc_sql( $old_tag ) . "', tags)",
2037 ARRAY_A
2038 );
2039 if( ! empty( $topics ) ) {
2040 foreach( $topics as $topic ) {
2041 $tags = explode( ',', $topic['tags'] );
2042 if( ! empty( $tags ) ) {
2043 foreach( $tags as $key => $tag ) {
2044 if( $tag === $old_tag ) $tags[ $key ] = $new_tag;
2045 }
2046 $tags = implode( ',', $tags );
2047 WPF()->db->update(
2048 WPF()->tables->topics,
2049 [ 'tags' => $tags ],
2050 [ 'topicid' => $topic['topicid'] ],
2051 [ '%s' ], [ '%d' ]
2052 );
2053 }
2054 }
2055 }
2056 }
2057 }
2058
2059 public function remove_tag( $tag ) {
2060 if( $tag ) {
2061 $this->remove_tag_from_topics( $tag );
2062 $sql_delete_tag = "DELETE FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'";
2063 WPF()->db->query( $sql_delete_tag );
2064 wpforo_clean_cache( 'tag', $tag );
2065 }
2066 }
2067
2068 public function get_tag( $args = [] ) {
2069 if( $args ) {
2070 $default = [
2071 'tagid' => null,
2072 'tag' => null,
2073 ];
2074 if( wpforo_is_id( $args ) ) {
2075 $args = [ 'tagid' => $args ];
2076 } elseif( is_string( $args ) ) {
2077 $args = [ 'tag' => $args ];
2078 }
2079
2080 $args = wpforo_parse_args( $args, $default );
2081 $sql = "SELECT * FROM `" . WPF()->tables->tags . "`";
2082 $wheres = [];
2083 if( $args['tagid'] ) $wheres[] = "`tagid` = " . intval( $args['tagid'] );
2084 if( $args['tag'] ) $wheres[] = "`tag` = '" . esc_sql( $args['tag'] ) . "'";
2085 if( $wheres ) {
2086 $sql .= " WHERE " . implode( " AND ", $wheres );
2087
2088 if( WPF()->ram_cache->exists( $sql ) ) {
2089 $tag = WPF()->ram_cache->get( $sql );
2090 } else {
2091 $tag = WPF()->db->get_row( $sql, ARRAY_A );
2092 WPF()->ram_cache->set( $sql, $tag );
2093 }
2094
2095 return $tag;
2096 }
2097 }
2098
2099 return [];
2100 }
2101
2102 public function get_tags( $args = [], &$items_count = 0 ) {
2103 $cache = WPF()->cache->on( 'tag' );
2104 $default = [
2105 'tag' => '',
2106 'prefix' => 0,
2107 'count' => null,
2108 'orderby' => 'count',
2109 'order' => 'DESC',
2110 'offset' => null,
2111 'row_count' => null,
2112 ];
2113
2114 $args = wpforo_parse_args( $args, $default );
2115 $sql = "SELECT * FROM `" . WPF()->tables->tags . "`";
2116 $wheres = [];
2117 $wheres[] = " `prefix` = " . intval( $args['prefix'] );
2118 if( ! empty( $wheres ) ) $sql .= " WHERE " . implode( " AND ", $wheres );
2119 $item_count_sql = preg_replace( '#SELECT.+?FROM#isu', 'SELECT count(*) FROM', $sql );
2120 if( $item_count_sql ) $items_count = WPF()->db->get_var( $item_count_sql );
2121
2122 $sql .= " ORDER BY `" . esc_sql( $args['orderby'] ) . "` " . esc_sql( $args['order'] );
2123
2124 if( $args['row_count'] != null ) {
2125 if( $args['offset'] != null ) {
2126 $sql .= " LIMIT " . intval( $args['offset'] ) . ',' . intval( $args['row_count'] );
2127 } else {
2128 $sql .= " LIMIT " . intval( $args['row_count'] );
2129 }
2130 }
2131
2132 if( $cache ) {
2133 $object_key = md5( $sql . WPF()->current_user_groupid );
2134 $object_cache = WPF()->cache->get( $object_key, 'loop', 'tag' );
2135 if( ! empty( $object_cache ) ) {
2136 $items_count = $object_cache['items_count'];
2137
2138 return $object_cache['items'];
2139 }
2140 }
2141
2142 $tags = WPF()->db->get_results( $sql, ARRAY_A );
2143 $tags = apply_filters( 'wpforo_get_tags', $tags );
2144
2145 if( $cache && isset( $object_key ) && ! empty( $tags ) ) {
2146 self::$cache['tags'][ $object_key ]['items'] = $tags;
2147 self::$cache['tags'][ $object_key ]['items_count'] = $items_count;
2148 WPF()->cache->create( 'loop', self::$cache, 'tag' );
2149 }
2150
2151 return $tags;
2152 }
2153
2154 public function after_add_post( $post, $topic ) {
2155 if( ! intval( $post['status'] ) ) {
2156 $this->rebuild_first_last( $topic );
2157 $this->rebuild_stats( $topic );
2158 }
2159 }
2160
2161 public function after_delete_post( $post ) {
2162 if( ! intval( $post['status'] ) ) {
2163 $this->rebuild_first_last( $post['topicid'] );
2164 $this->rebuild_stats( $post['topicid'] );
2165 }
2166 }
2167
2168 public function after_post_status_update( $post ) {
2169 if( $topic = $this->get_topic( $post['topicid'] ) ) {
2170 $this->rebuild_first_last( $topic );
2171 $this->rebuild_stats( $topic );
2172 }
2173 }
2174
2175 public function after_delete_user( $userid, $reassign ) {
2176 if( $boardids = WPF()->board->get_active_boardids() ) {
2177 if( is_null( $reassign ) ) {
2178
2179 foreach( $boardids as $boardid ) {
2180 WPF()->change_board( $boardid );
2181 if( $topicids = WPF()->db->get_col(
2182 WPF()->db->prepare(
2183 "SELECT `topicid` FROM `" . WPF()->tables->topics . "` WHERE userid = %d",
2184 $userid
2185 )
2186 ) ) {
2187 foreach( $topicids as $topicid ) $this->delete( $topicid, false, false );
2188 }
2189 }
2190
2191 } else {
2192 if( $reassign = wpforo_bigintval( $reassign ) ) {
2193 $data = [ 'userid' => $reassign ];
2194 $format = [ '%d' ];
2195 } else {
2196 $data = [
2197 'userid' => 0,
2198 'name' => wpforo_phrase( 'Anonymous', false ) . " " . $userid,
2199 'email' => "anonymous_$userid@example.com",
2200 ];
2201 $format = [ '%d', '%s', '%s' ];
2202 }
2203
2204 foreach( $boardids as $boardid ) {
2205 WPF()->change_board( $boardid );
2206 WPF()->db->update( WPF()->tables->topics, $data, [ 'userid' => $userid ], $format, [ '%d' ] );
2207 }
2208
2209 }
2210 }
2211 }
2212 }
2213