Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.88% covered (warning)
84.88%
275 / 324
8.33% covered (danger)
8.33%
1 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
content_visibility
84.88% covered (warning)
84.88%
275 / 324
8.33% covered (danger)
8.33%
1 / 12
150.10
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 can_soft_delete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 get_count
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 is_visible
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 get_visibility_sql
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 get_forums_visibility_sql
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
4
 get_global_visibility_sql
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
3.00
 set_post_visibility
96.00% covered (success)
96.00%
144 / 150
0.00% covered (danger)
0.00%
0 / 1
41
 set_topic_visibility
92.45% covered (success)
92.45%
49 / 53
0.00% covered (danger)
0.00%
0 / 1
14.08
 add_post_to_statistic
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 remove_post_from_statistic
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
15.64
 remove_topic_from_statistic
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
13.12
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb;
15
16/**
17* phpbb_visibility
18* Handle fetching and setting the visibility for topics and posts
19*/
20class content_visibility
21{
22    /**
23    * Database object
24    * @var \phpbb\db\driver\driver_interface
25    */
26    protected $db;
27
28    /**
29    * User object
30    * @var \phpbb\user
31    */
32    protected $user;
33
34    /**
35    * Auth object
36    * @var \phpbb\auth\auth
37    */
38    protected $auth;
39
40    /**
41    * config object
42    * @var \phpbb\config\config
43    */
44    protected $config;
45
46    /**
47    * Event dispatcher object
48    * @var \phpbb\event\dispatcher_interface
49    */
50    protected $phpbb_dispatcher;
51
52    /**
53    * phpBB root path
54    * @var string
55    */
56    protected $phpbb_root_path;
57
58    /**
59    * PHP Extension
60    * @var string
61    */
62    protected $php_ext;
63
64    /**
65    * @var string
66    */
67    protected $forums_table;
68
69    /**
70    * @var string
71    */
72    protected $posts_table;
73
74    /**
75    * @var string
76    */
77    protected $topics_table;
78
79    /**
80    * @var string
81    */
82    protected $users_table;
83
84    /**
85    * Constructor
86    *
87    * @param    \phpbb\auth\auth        $auth    Auth object
88    * @param    \phpbb\config\config    $config    Config object
89    * @param    \phpbb\event\dispatcher_interface    $phpbb_dispatcher    Event dispatcher object
90    * @param    \phpbb\db\driver\driver_interface    $db        Database object
91    * @param    \phpbb\user        $user            User object
92    * @param    string        $phpbb_root_path    Root path
93    * @param    string        $php_ext            PHP Extension
94    * @param    string        $forums_table        Forums table name
95    * @param    string        $posts_table        Posts table name
96    * @param    string        $topics_table        Topics table name
97    * @param    string        $users_table        Users table name
98    */
99    public function __construct(\phpbb\auth\auth $auth, \phpbb\config\config $config, \phpbb\event\dispatcher_interface $phpbb_dispatcher, \phpbb\db\driver\driver_interface $db, \phpbb\user $user, $phpbb_root_path, $php_ext, $forums_table, $posts_table, $topics_table, $users_table)
100    {
101        $this->auth = $auth;
102        $this->config = $config;
103        $this->phpbb_dispatcher = $phpbb_dispatcher;
104        $this->db = $db;
105        $this->user = $user;
106        $this->phpbb_root_path = $phpbb_root_path;
107        $this->php_ext = $php_ext;
108        $this->forums_table = $forums_table;
109        $this->posts_table = $posts_table;
110        $this->topics_table = $topics_table;
111        $this->users_table = $users_table;
112    }
113
114    /**
115    * Can the current logged-in user soft-delete posts?
116    *
117    * @param $forum_id        int        Forum ID whose permissions to check
118    * @param $poster_id        int        Poster ID of the post in question
119    * @param $post_locked    bool    Is the post locked?
120    * @return bool
121    */
122    public function can_soft_delete($forum_id, $poster_id, $post_locked)
123    {
124        if ($this->auth->acl_get('m_softdelete', $forum_id))
125        {
126            return true;
127        }
128        else if ($this->auth->acl_get('f_softdelete', $forum_id) && $poster_id == $this->user->data['user_id'] && !$post_locked)
129        {
130            return true;
131        }
132
133        return false;
134    }
135
136    /**
137    * Get the topics post count or the forums post/topic count based on permissions
138    *
139    * @param $mode            string    One of topic_posts, forum_posts or forum_topics
140    * @param $data            array    Array with the topic/forum data to calculate from
141    * @param $forum_id        int        The forum id is used for permission checks
142    * @return int    Number of posts/topics the user can see in the topic/forum
143    */
144    public function get_count($mode, $data, $forum_id)
145    {
146        if (!$this->auth->acl_get('m_approve', $forum_id))
147        {
148            return (int) $data[$mode . '_approved'];
149        }
150
151        return (int) $data[$mode . '_approved'] + (int) $data[$mode . '_unapproved'] + (int) $data[$mode . '_softdeleted'];
152    }
153
154
155    /**
156    * Check topic/post visibility for a given forum ID
157    *
158    * Note: Read permissions are not checked.
159    *
160    * @param $mode        string    Either "topic" or "post"
161    * @param $forum_id    int        The forum id is used for permission checks
162    * @param $data        array    Array with item information to check visibility
163    * @return bool        True if the item is visible, false if not
164    */
165    public function is_visible($mode, $forum_id, $data)
166    {
167        $visibility = $data[$mode . '_visibility'];
168        $poster_key = ($mode === 'topic') ? 'topic_poster' : 'poster_id';
169        $is_visible = ($visibility == ITEM_APPROVED) ||
170            ($this->config['display_unapproved_posts'] &&
171                ($this->user->data['user_id'] != ANONYMOUS) &&
172                ($visibility == ITEM_UNAPPROVED || $visibility == ITEM_REAPPROVE) &&
173                ($this->user->data['user_id'] == $data[$poster_key])) ||
174             $this->auth->acl_get('m_approve', $forum_id);
175
176        /**
177        * Allow changing the result of calling is_visible
178        *
179        * @event core.phpbb_content_visibility_is_visible
180        * @var    bool        is_visible            Default visibility condition, to be modified by extensions if needed.
181        * @var    string        mode                Either "topic" or "post"
182        * @var    int            forum_id            Forum id of the current item
183        * @var    array        data                Array of item information
184        * @since 3.2.2-RC1
185        */
186        $vars = array(
187            'is_visible',
188            'mode',
189            'forum_id',
190            'data',
191        );
192        extract($this->phpbb_dispatcher->trigger_event('core.phpbb_content_visibility_is_visible', compact($vars)));
193
194        return $is_visible;
195    }
196
197    /**
198    * Create topic/post visibility SQL for a given forum ID
199    *
200    * Note: Read permissions are not checked.
201    *
202    * @param $mode            string    Either "topic" or "post"
203    * @param $forum_id        int        The forum id is used for permission checks
204    * @param $table_alias    string    Table alias to prefix in SQL queries
205    * @return string    The appropriate combination SQL logic for topic/post_visibility
206    */
207    public function get_visibility_sql($mode, $forum_id, $table_alias = '')
208    {
209        $where_sql = '';
210
211        $get_visibility_sql_overwrite = false;
212
213        /**
214        * Allow changing the result of calling get_visibility_sql
215        *
216        * @event core.phpbb_content_visibility_get_visibility_sql_before
217        * @var    string        where_sql                        Extra visibility conditions. It must end with either an SQL "AND" or an "OR"
218        * @var    string        mode                            Either "topic" or "post" depending on the query this is being used in
219        * @var    array        forum_id                        The forum id in which the search is made.
220        * @var    string        table_alias                        Table alias to prefix in SQL queries
221        * @var    mixed        get_visibility_sql_overwrite    If a string, forces the function to return get_forums_visibility_sql_overwrite after executing the event
222        *                                                     If false, get_visibility_sql continues normally
223        *                                                     It must be either boolean or string
224        * @since 3.1.4-RC1
225        */
226        $vars = array(
227            'where_sql',
228            'mode',
229            'forum_id',
230            'table_alias',
231            'get_visibility_sql_overwrite',
232        );
233        extract($this->phpbb_dispatcher->trigger_event('core.phpbb_content_visibility_get_visibility_sql_before', compact($vars)));
234
235        if ($get_visibility_sql_overwrite !== false)
236        {
237            return $get_visibility_sql_overwrite;
238        }
239
240        if ($this->auth->acl_get('m_approve', $forum_id))
241        {
242            $where_sql .= '1 = 1';
243        }
244        else
245        {
246            $visibility_query = $table_alias . $mode . '_visibility = ';
247
248            $where_sql .= '(' . $visibility_query . ITEM_APPROVED . ')';
249            if ($this->config['display_unapproved_posts'] && ($this->user->data['user_id'] != ANONYMOUS))
250            {
251                $poster_key = ($mode === 'topic') ? 'topic_poster' : 'poster_id';
252                $where_sql .= ' OR ((' . $visibility_query . ITEM_UNAPPROVED . ' OR ' . $visibility_query . ITEM_REAPPROVE .')';
253                $where_sql .= ' AND ' . $table_alias . $poster_key . ' = ' . ((int) $this->user->data['user_id']) . ')';
254            }
255        }
256        return '(' . $where_sql . ')';
257    }
258
259    /**
260    * Create topic/post visibility SQL for a set of forums
261    *
262    * Note: Read permissions are not checked. Forums without read permissions
263    *        should not be in $forum_ids
264    *
265    * @param $mode            string    Either "topic" or "post"
266    * @param $forum_ids        array    Array of forum ids which the posts/topics are limited to
267    * @param $table_alias    string    Table alias to prefix in SQL queries
268    * @return string    The appropriate combination SQL logic for topic/post_visibility
269    */
270    public function get_forums_visibility_sql($mode, $forum_ids = array(), $table_alias = '')
271    {
272        $where_sql = '';
273
274        $approve_forums = array_keys($this->auth->acl_getf('m_approve', true));
275        if (!empty($forum_ids) && !empty($approve_forums))
276        {
277            $approve_forums = array_intersect($forum_ids, $approve_forums);
278            $forum_ids = array_diff($forum_ids, $approve_forums);
279        }
280
281        $get_forums_visibility_sql_overwrite = false;
282        /**
283        * Allow changing the result of calling get_forums_visibility_sql
284        *
285        * @event core.phpbb_content_visibility_get_forums_visibility_before
286        * @var    string        where_sql                            Extra visibility conditions. It must end with either an SQL "AND" or an "OR"
287        * @var    string        mode                                Either "topic" or "post" depending on the query this is being used in
288        * @var    array        forum_ids                            Array of forum ids which the posts/topics are limited to
289        * @var    string        table_alias                            Table alias to prefix in SQL queries
290        * @var    array        approve_forums                        Array of forums where the user has m_approve permissions
291        * @var    mixed        get_forums_visibility_sql_overwrite    If a string, forces the function to return get_forums_visibility_sql_overwrite after executing the event
292        *                                                         If false, get_forums_visibility_sql continues normally
293        *                                                         It must be either boolean or string
294        * @since 3.1.3-RC1
295        */
296        $vars = array(
297            'where_sql',
298            'mode',
299            'forum_ids',
300            'table_alias',
301            'approve_forums',
302            'get_forums_visibility_sql_overwrite',
303        );
304        extract($this->phpbb_dispatcher->trigger_event('core.phpbb_content_visibility_get_forums_visibility_before', compact($vars)));
305
306        if ($get_forums_visibility_sql_overwrite !== false)
307        {
308            return $get_forums_visibility_sql_overwrite;
309        }
310
311        // Moderator can view all posts/topics in the moderated forums
312        $where_sql .= '(' . $this->db->sql_in_set($table_alias . 'forum_id', $approve_forums, false, true) . ' OR ';
313        // Normal user can view approved items only
314        $where_sql .= '(' . $table_alias . $mode . '_visibility = ' . ITEM_APPROVED . '
315            AND ' . $this->db->sql_in_set($table_alias . 'forum_id', $forum_ids, false, true) . '))';
316
317        return '(' . $where_sql . ')';
318    }
319
320    /**
321    * Create topic/post visibility SQL for all forums on the board
322    *
323    * Note: Read permissions are not checked. Forums without read permissions
324    *        should be in $exclude_forum_ids
325    *
326    * @param $mode                string    Either "topic" or "post"
327    * @param $exclude_forum_ids    array    Array of forum ids which are excluded
328    * @param $table_alias        string    Table alias to prefix in SQL queries
329    * @return string    The appropriate combination SQL logic for topic/post_visibility
330    */
331    public function get_global_visibility_sql($mode, $exclude_forum_ids = array(), $table_alias = '')
332    {
333        $where_sqls = array();
334
335        $approve_forums = array_diff(array_keys($this->auth->acl_getf('m_approve', true)), $exclude_forum_ids);
336
337        $visibility_sql_overwrite = null;
338
339        /**
340        * Allow changing the result of calling get_global_visibility_sql
341        *
342        * @event core.phpbb_content_visibility_get_global_visibility_before
343        * @var    array        where_sqls                            Array of extra visibility conditions. Will be joined by imploding with "OR".
344        * @var    string        mode                                Either "topic" or "post" depending on the query this is being used in
345        * @var    array        exclude_forum_ids                    Array of forum ids the current user doesn't have access to
346        * @var    string        table_alias                            Table alias to prefix in SQL queries
347        * @var    array        approve_forums                        Array of forums where the user has m_approve permissions
348        * @var    string        visibility_sql_overwrite            If not empty, forces the function to return visibility_sql_overwrite after executing the event
349        * @since 3.1.3-RC1
350        */
351        $vars = array(
352            'where_sqls',
353            'mode',
354            'exclude_forum_ids',
355            'table_alias',
356            'approve_forums',
357            'visibility_sql_overwrite',
358        );
359        extract($this->phpbb_dispatcher->trigger_event('core.phpbb_content_visibility_get_global_visibility_before', compact($vars)));
360
361        if ($visibility_sql_overwrite)
362        {
363            return $visibility_sql_overwrite;
364        }
365
366        // Include approved items in all forums but the excluded
367        $where_sqls[] = '(' . $this->db->sql_in_set($table_alias . 'forum_id', $exclude_forum_ids, true, true) . '
368            AND ' . $table_alias . $mode . '_visibility = ' . ITEM_APPROVED . ')';
369
370        // If user has moderator permissions, add everything in the moderated forums
371        if (count($approve_forums))
372        {
373            $where_sqls[] = $this->db->sql_in_set($table_alias . 'forum_id', $approve_forums);
374        }
375
376        return '(' . implode(' OR ', $where_sqls) . ')';
377    }
378
379    /**
380    * Change visibility status of one post or all posts of a topic
381    *
382    * @param $visibility    int        Element of {ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE}
383    * @param $post_id        mixed    Post ID or array of post IDs to act on,
384    *                                if it is empty, all posts of topic_id will be modified
385    * @param $topic_id        int        Topic where $post_id is found
386    * @param $forum_id        int        Forum where $topic_id is found
387    * @param $user_id        int        User performing the action
388    * @param $time            int        Timestamp when the action is performed
389    * @param $reason        string    Reason why the visibility was changed.
390    * @param $is_starter    bool    Is this the first post of the topic changed?
391    * @param $is_latest        bool    Is this the last post of the topic changed?
392    * @param $limit_visibility    mixed    Limit updating per topic_id to a certain visibility
393    * @param $limit_delete_time    mixed    Limit updating per topic_id to a certain deletion time
394    * @return array        Changed post data, empty array if an error occurred.
395    */
396    public function set_post_visibility($visibility, $post_id, $topic_id, $forum_id, $user_id, $time, $reason, $is_starter, $is_latest, $limit_visibility = false, $limit_delete_time = false)
397    {
398        if (!in_array($visibility, array(ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE)))
399        {
400            return array();
401        }
402
403        if ($post_id)
404        {
405            if (is_array($post_id))
406            {
407                $where_sql = $this->db->sql_in_set('post_id', array_map('intval', $post_id));
408            }
409            else
410            {
411                $where_sql = 'post_id = ' . (int) $post_id;
412            }
413            $where_sql .= ' AND topic_id = ' . (int) $topic_id;
414        }
415        else
416        {
417            $where_sql = 'topic_id = ' . (int) $topic_id;
418
419            // Limit the posts to a certain visibility and deletion time
420            // This allows us to only restore posts, that were approved
421            // when the topic got soft deleted. So previous soft deleted
422            // and unapproved posts are still soft deleted/unapproved
423            if ($limit_visibility !== false)
424            {
425                $where_sql .= ' AND post_visibility = ' . (int) $limit_visibility;
426            }
427
428            if ($limit_delete_time !== false)
429            {
430                $where_sql .= ' AND post_delete_time = ' . (int) $limit_delete_time;
431            }
432        }
433
434        $sql = 'SELECT poster_id, post_id, post_postcount, post_visibility
435            FROM ' . $this->posts_table . '
436            WHERE ' . $where_sql;
437        $result = $this->db->sql_query($sql);
438
439        $post_ids = $poster_postcounts = $postcounts = $postcount_visibility = array();
440        while ($row = $this->db->sql_fetchrow($result))
441        {
442            $post_ids[] = (int) $row['post_id'];
443
444            if ($row['post_visibility'] != $visibility)
445            {
446                if ($row['post_postcount'] && !isset($poster_postcounts[(int) $row['poster_id']]))
447                {
448                    $poster_postcounts[(int) $row['poster_id']] = 1;
449                }
450                else if ($row['post_postcount'])
451                {
452                    $poster_postcounts[(int) $row['poster_id']]++;
453                }
454
455                if (!isset($postcount_visibility[$row['post_visibility']]))
456                {
457                    $postcount_visibility[$row['post_visibility']] = 1;
458                }
459                else
460                {
461                    $postcount_visibility[$row['post_visibility']]++;
462                }
463            }
464        }
465        $this->db->sql_freeresult($result);
466
467        if (empty($post_ids))
468        {
469            return array();
470        }
471
472        if (!function_exists('truncate_string'))
473        {
474            include($this->phpbb_root_path . 'includes/functions_content.' . $this->php_ext);
475        }
476
477        $data = array(
478            'post_visibility'        => (int) $visibility,
479            'post_delete_user'        => (int) $user_id,
480            'post_delete_time'        => ((int) $time) ?: time(),
481            'post_delete_reason'    => truncate_string($reason, 255, 255, false),
482        );
483        /**
484         * Perform actions right before the query to change post visibility
485         *
486         * @event core.set_post_visibility_before_sql
487         * @var            int            visibility        Element of {ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE}
488         * @var            array        post_id            Array containing all post IDs to be modified. If blank, all posts within the topic are modified.
489         * @var            int            topic_id        Topic of the post IDs to be modified.
490         * @var            int            forum_id        Forum ID that the topic_id resides in.
491         * @var            int            user_id            User ID doing this action.
492         * @var            int            time            Timestamp of this action.
493         * @var            string        reason            Reason specified by the user for this change.
494         * @var            bool        is_starter        Are we changing the topic's starter?
495         * @var            bool        is_latest        Are we changing the topic's latest post?
496         * @var            array        data            The data array for this action.
497         * @since 3.1.10-RC1
498         * @changed 3.2.2-RC1 Use time instead of non-existent timestamp
499         */
500        $vars = array(
501            'visibility',
502            'post_id',
503            'topic_id',
504            'forum_id',
505            'user_id',
506            'time',
507            'reason',
508            'is_starter',
509            'is_latest',
510            'data',
511        );
512        extract($this->phpbb_dispatcher->trigger_event('core.set_post_visibility_before_sql', compact($vars)));
513        $sql = 'UPDATE ' . $this->posts_table . '
514            SET ' . $this->db->sql_build_array('UPDATE', $data) . '
515            WHERE ' . $this->db->sql_in_set('post_id', $post_ids);
516        $this->db->sql_query($sql);
517
518        // Group the authors by post count, to reduce the number of queries
519        foreach ($poster_postcounts as $poster_id => $num_posts)
520        {
521            $postcounts[$num_posts][] = $poster_id;
522        }
523
524        $postcount_change = 0;
525
526        // Update users postcounts
527        foreach ($postcounts as $num_posts => $poster_ids)
528        {
529            if (in_array($visibility, array(ITEM_REAPPROVE, ITEM_DELETED)))
530            {
531                $postcount_change -= $num_posts;
532
533                $sql = 'UPDATE ' . $this->users_table . '
534                    SET user_posts = 0
535                    WHERE ' . $this->db->sql_in_set('user_id', $poster_ids) . '
536                        AND user_posts < ' . $num_posts;
537                $this->db->sql_query($sql);
538
539                $sql = 'UPDATE ' . $this->users_table . '
540                    SET user_posts = user_posts - ' . $num_posts . '
541                    WHERE ' . $this->db->sql_in_set('user_id', $poster_ids) . '
542                        AND user_posts >= ' . $num_posts;
543                $this->db->sql_query($sql);
544            }
545            else
546            {
547                $postcount_change += $num_posts;
548
549                $sql = 'UPDATE ' . $this->users_table . '
550                    SET user_posts = user_posts + ' . $num_posts . '
551                    WHERE ' . $this->db->sql_in_set('user_id', $poster_ids);
552                $this->db->sql_query($sql);
553            }
554        }
555
556        if ($postcount_change != 0)
557        {
558            $this->config->increment('num_posts', $postcount_change, false);
559        }
560
561        $update_topic_postcount = true;
562
563        // Sync the first/last topic information if needed
564        if (!$is_starter && $is_latest)
565        {
566            if (!function_exists('update_post_information'))
567            {
568                include($this->phpbb_root_path . 'includes/functions_posting.' . $this->php_ext);
569            }
570
571            // update_post_information can only update the last post info ...
572            if ($topic_id)
573            {
574                update_post_information('topic', $topic_id, false);
575            }
576            if ($forum_id)
577            {
578                update_post_information('forum', $forum_id, false);
579            }
580        }
581        else if ($is_starter && $topic_id)
582        {
583            if (!function_exists('sync'))
584            {
585                include($this->phpbb_root_path . 'includes/functions_admin.' . $this->php_ext);
586            }
587
588            // ... so we need to use sync, if the first post is changed.
589            // The forum is resynced recursive by sync() itself.
590            sync('topic', 'topic_id', $topic_id, true);
591
592            // sync recalculates the topic replies and forum posts by itself, so we don't do that.
593            $update_topic_postcount = false;
594        }
595
596        $topic_update_array = array();
597        // Update the topic's reply count and the forum's post count
598        if ($update_topic_postcount)
599        {
600            $field_alias = array(
601                ITEM_APPROVED    => 'posts_approved',
602                ITEM_UNAPPROVED    => 'posts_unapproved',
603                ITEM_DELETED    => 'posts_softdeleted',
604                ITEM_REAPPROVE    => 'posts_unapproved',
605            );
606            $cur_posts = array_fill_keys($field_alias, 0);
607
608            foreach ($postcount_visibility as $post_visibility => $visibility_posts)
609            {
610                $cur_posts[$field_alias[(int) $post_visibility]] += $visibility_posts;
611            }
612
613            $sql_ary = array();
614            $recipient_field = $field_alias[$visibility];
615
616            foreach ($cur_posts as $field => $count)
617            {
618                // Decrease the count for the old statuses.
619                if ($count && $field != $recipient_field)
620                {
621                    $sql_ary[$field] = " - $count";
622                }
623            }
624            // Add up the count from all statuses excluding the recipient status.
625            $count_increase = array_sum(array_diff($cur_posts, array($recipient_field)));
626
627            if ($count_increase)
628            {
629                $sql_ary[$recipient_field] = " + $count_increase";
630            }
631
632            if (count($sql_ary))
633            {
634                $forum_sql = array();
635
636                foreach ($sql_ary as $field => $value_change)
637                {
638                    $topic_update_array[] = 'topic_' . $field . ' = topic_' . $field . $value_change;
639                    $forum_sql[] = 'forum_' . $field . ' = forum_' . $field . $value_change;
640                }
641
642                $sql = 'UPDATE ' . $this->forums_table . '
643                    SET ' . implode(', ', $forum_sql) . '
644                    WHERE forum_id = ' . (int) $forum_id;
645                $this->db->sql_query($sql);
646            }
647        }
648
649        if ($post_id)
650        {
651            $sql = 'SELECT 1 AS has_attachments
652                FROM ' . POSTS_TABLE . '
653                WHERE topic_id = ' . (int) $topic_id . '
654                    AND post_attachment = 1
655                    AND post_visibility = ' . ITEM_APPROVED . '
656                    AND ' . $this->db->sql_in_set('post_id', $post_id, true);
657            $result = $this->db->sql_query_limit($sql, 1);
658
659            $has_attachment = (bool) $this->db->sql_fetchfield('has_attachments');
660            $this->db->sql_freeresult($result);
661
662            if ($has_attachment && $visibility == ITEM_APPROVED)
663            {
664                $topic_update_array[] = 'topic_attachment = 1';
665            }
666            else if (!$has_attachment && $visibility != ITEM_APPROVED)
667            {
668                $topic_update_array[] = 'topic_attachment = 0';
669            }
670        }
671
672        if (!empty($topic_update_array))
673        {
674            // Update the number for replies and posts, and update the attachments flag
675            $sql = 'UPDATE ' . $this->topics_table . '
676                SET ' . implode(', ', $topic_update_array) . '
677                WHERE topic_id = ' . (int) $topic_id;
678            $this->db->sql_query($sql);
679        }
680        /**
681         * Perform actions after all steps to changing post visibility
682         *
683         * @event core.set_post_visibility_after
684         * @var            int            visibility        Element of {ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE}
685         * @var            array        post_id            Array containing all post IDs to be modified. If blank, all posts within the topic are modified.
686         * @var            int            topic_id        Topic of the post IDs to be modified.
687         * @var            int            forum_id        Forum ID that the topic_id resides in.
688         * @var            int            user_id            User ID doing this action.
689         * @var            int            time            Timestamp of this action.
690         * @var            string        reason            Reason specified by the user for this change.
691         * @var            bool        is_starter        Are we changing the topic's starter?
692         * @var            bool        is_latest        Are we changing the topic's latest post?
693         * @var            array        data            The data array for this action.
694         * @since 3.1.10-RC1
695         * @changed 3.2.2-RC1 Use time instead of non-existent timestamp
696         */
697        $vars = array(
698            'visibility',
699            'post_id',
700            'topic_id',
701            'forum_id',
702            'user_id',
703            'time',
704            'reason',
705            'is_starter',
706            'is_latest',
707            'data',
708        );
709        extract($this->phpbb_dispatcher->trigger_event('core.set_post_visibility_after', compact($vars)));
710        return $data;
711    }
712
713    /**
714    * Set topic visibility
715    *
716    * Allows approving (which is akin to undeleting/restore) or soft deleting an entire topic.
717    * Calls set_post_visibility as needed.
718    *
719    * Note: By default, when a soft deleted topic is restored. Only posts that
720    *        were approved at the time of soft deleting, are being restored.
721    *        Same applies to soft deleting. Only approved posts will be marked
722    *        as soft deleted.
723    *        If you want to update all posts, use the force option.
724    *
725    * @param $visibility    int        Element of {ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE}
726    * @param $topic_id        mixed    Topic ID to act on
727    * @param $forum_id        int        Forum where $topic_id is found
728    * @param $user_id        int        User performing the action
729    * @param $time            int        Timestamp when the action is performed
730    * @param $reason        string    Reason why the visibilty was changed.
731    * @param $force_update_all    bool    Force to update all posts within the topic
732    * @return array        Changed topic data, empty array if an error occurred.
733    */
734    public function set_topic_visibility($visibility, $topic_id, $forum_id, $user_id, $time, $reason, $force_update_all = false)
735    {
736        if (!in_array($visibility, array(ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE)))
737        {
738            return array();
739        }
740
741        if (!$force_update_all)
742        {
743            $sql = 'SELECT topic_visibility, topic_delete_time
744                FROM ' . $this->topics_table . '
745                WHERE topic_id = ' . (int) $topic_id;
746            $result = $this->db->sql_query($sql);
747            $original_topic_data = $this->db->sql_fetchrow($result);
748            $this->db->sql_freeresult($result);
749
750            if (!$original_topic_data)
751            {
752                // The topic does not exist...
753                return array();
754            }
755        }
756
757        if (!function_exists('truncate_string'))
758        {
759            include($this->phpbb_root_path . 'includes/functions_content.' . $this->php_ext);
760        }
761
762        // Note, we do not set a reason for the posts, just for the topic
763        $data = array(
764            'topic_visibility'        => (int) $visibility,
765            'topic_delete_user'        => (int) $user_id,
766            'topic_delete_time'        => ((int) $time) ?: time(),
767            'topic_delete_reason'    => truncate_string($reason, 255, 255, false),
768        );
769        /**
770         * Perform actions right before the query to change topic visibility
771         *
772         * @event core.set_topic_visibility_before_sql
773         * @var            int            visibility            Element of {ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE}
774         * @var            int            topic_id            Topic of the post IDs to be modified.
775         * @var            int            forum_id            Forum ID that the topic_id resides in.
776         * @var            int            user_id                User ID doing this action.
777         * @var            int            time                Timestamp of this action.
778         * @var            string        reason                Reason specified by the user for this change.
779         * @var            bool        force_update_all    Force an update on all posts within the topic, regardless of their current approval state.
780         * @var            array        data                The data array for this action.
781         * @since 3.1.10-RC1
782         * @changed 3.2.2-RC1 Use time instead of non-existent timestamp
783         */
784        $vars = array(
785            'visibility',
786            'topic_id',
787            'forum_id',
788            'user_id',
789            'time',
790            'reason',
791            'force_update_all',
792            'data',
793        );
794        extract($this->phpbb_dispatcher->trigger_event('core.set_topic_visibility_before_sql', compact($vars)));
795        $sql = 'UPDATE ' . $this->topics_table . '
796            SET ' . $this->db->sql_build_array('UPDATE', $data) . '
797            WHERE topic_id = ' . (int) $topic_id;
798        $this->db->sql_query($sql);
799
800        if (!$this->db->sql_affectedrows())
801        {
802            return array();
803        }
804
805        if (!$force_update_all && $original_topic_data['topic_delete_time'] && $original_topic_data['topic_visibility'] == ITEM_DELETED && $visibility == ITEM_APPROVED)
806        {
807            // If we're restoring a topic we only restore posts, that were soft deleted through the topic soft deletion.
808            $this->set_post_visibility($visibility, false, $topic_id, $forum_id, $user_id, $time, '', true, true, $original_topic_data['topic_visibility'], $original_topic_data['topic_delete_time']);
809        }
810        else if (!$force_update_all && $original_topic_data['topic_visibility'] == ITEM_APPROVED && $visibility == ITEM_DELETED)
811        {
812            // If we're soft deleting a topic we only mark approved posts as soft deleted.
813            $this->set_post_visibility($visibility, false, $topic_id, $forum_id, $user_id, $time, '', true, true, $original_topic_data['topic_visibility']);
814        }
815        else
816        {
817            $this->set_post_visibility($visibility, false, $topic_id, $forum_id, $user_id, $time, '', true, true);
818        }
819        /**
820         * Perform actions after all steps to changing topic visibility
821         *
822         * @event core.set_topic_visibility_after
823         * @var            int            visibility            Element of {ITEM_APPROVED, ITEM_DELETED, ITEM_REAPPROVE}
824         * @var            int            topic_id            Topic of the post IDs to be modified.
825         * @var            int            forum_id            Forum ID that the topic_id resides in.
826         * @var            int            user_id                User ID doing this action.
827         * @var            int            time                Timestamp of this action.
828         * @var            string        reason                Reason specified by the user for this change.
829         * @var            bool        force_update_all    Force an update on all posts within the topic, regardless of their current approval state.
830         * @var            array        data                The data array for this action.
831         * @since 3.1.10-RC1
832         * @changed 3.2.2-RC1 Use time instead of non-existent timestamp
833         */
834        $vars = array(
835            'visibility',
836            'topic_id',
837            'forum_id',
838            'user_id',
839            'time',
840            'reason',
841            'force_update_all',
842            'data',
843        );
844        extract($this->phpbb_dispatcher->trigger_event('core.set_topic_visibility_after', compact($vars)));
845        return $data;
846    }
847
848    /**
849    * Add post to topic and forum statistics
850    *
851    * @param $data            array    Contains information from the topics table about given topic
852    * @param $sql_data        array    Populated with the SQL changes, may be empty at call time (by reference)
853    * @return void
854    */
855    public function add_post_to_statistic($data, &$sql_data)
856    {
857        $sql_data[$this->topics_table] = (($sql_data[$this->topics_table]) ? $sql_data[$this->topics_table] . ', ' : '') . 'topic_posts_approved = topic_posts_approved + 1';
858
859        $sql_data[$this->forums_table] = (($sql_data[$this->forums_table]) ? $sql_data[$this->forums_table] . ', ' : '') . 'forum_posts_approved = forum_posts_approved + 1';
860
861        if ($data['post_postcount'])
862        {
863            $sql_data[$this->users_table] = (($sql_data[$this->users_table]) ? $sql_data[$this->users_table] . ', ' : '') . 'user_posts = user_posts + 1';
864        }
865
866        $this->config->increment('num_posts', 1, false);
867    }
868
869    /**
870    * Remove post from topic and forum statistics
871    *
872    * @param $data            array    Contains information from the topics table about given topic
873    * @param $sql_data        array    Populated with the SQL changes, may be empty at call time (by reference)
874    * @return void
875    */
876    public function remove_post_from_statistic($data, &$sql_data)
877    {
878        if ($data['post_visibility'] == ITEM_APPROVED)
879        {
880            $sql_data[$this->topics_table] = ((!empty($sql_data[$this->topics_table])) ? $sql_data[$this->topics_table] . ', ' : '') . 'topic_posts_approved = topic_posts_approved - 1';
881            $sql_data[$this->forums_table] = ((!empty($sql_data[$this->forums_table])) ? $sql_data[$this->forums_table] . ', ' : '') . 'forum_posts_approved = forum_posts_approved - 1';
882
883            if ($data['post_postcount'])
884            {
885                $sql_data[$this->users_table] = ((!empty($sql_data[$this->users_table])) ? $sql_data[$this->users_table] . ', ' : '') . 'user_posts = user_posts - 1';
886            }
887
888            $this->config->increment('num_posts', -1, false);
889        }
890        else if ($data['post_visibility'] == ITEM_UNAPPROVED || $data['post_visibility'] == ITEM_REAPPROVE)
891        {
892            $sql_data[FORUMS_TABLE] = (($sql_data[FORUMS_TABLE]) ? $sql_data[FORUMS_TABLE] . ', ' : '') . 'forum_posts_unapproved = forum_posts_unapproved - 1';
893            $sql_data[TOPICS_TABLE] = (($sql_data[TOPICS_TABLE]) ? $sql_data[TOPICS_TABLE] . ', ' : '') . 'topic_posts_unapproved = topic_posts_unapproved - 1';
894        }
895        else if ($data['post_visibility'] == ITEM_DELETED)
896        {
897            $sql_data[FORUMS_TABLE] = (($sql_data[FORUMS_TABLE]) ? $sql_data[FORUMS_TABLE] . ', ' : '') . 'forum_posts_softdeleted = forum_posts_softdeleted - 1';
898            $sql_data[TOPICS_TABLE] = (($sql_data[TOPICS_TABLE]) ? $sql_data[TOPICS_TABLE] . ', ' : '') . 'topic_posts_softdeleted = topic_posts_softdeleted - 1';
899        }
900    }
901
902    /**
903    * Remove topic from forum statistics
904    *
905    * @param $data            array    Post and topic data
906    * @param $sql_data        array    Populated with the SQL changes, may be empty at call time (by reference)
907    * @return void
908    */
909    public function remove_topic_from_statistic($data, &$sql_data)
910    {
911        if ($data['topic_visibility'] == ITEM_APPROVED)
912        {
913            $sql_data[FORUMS_TABLE] .= 'forum_posts_approved = forum_posts_approved - 1, forum_topics_approved = forum_topics_approved - 1';
914
915            if ($data['post_postcount'])
916            {
917                $sql_data[$this->users_table] = ((!empty($sql_data[$this->users_table])) ? $sql_data[$this->users_table] . ', ' : '') . 'user_posts = user_posts - 1';
918            }
919        }
920        else if ($data['topic_visibility'] == ITEM_UNAPPROVED || $data['post_visibility'] == ITEM_REAPPROVE)
921        {
922            $sql_data[FORUMS_TABLE] .= 'forum_posts_unapproved = forum_posts_unapproved - 1, forum_topics_unapproved = forum_topics_unapproved - 1';
923        }
924        else if ($data['topic_visibility'] == ITEM_DELETED)
925        {
926            $sql_data[FORUMS_TABLE] .= 'forum_posts_softdeleted = forum_posts_softdeleted - 1, forum_topics_softdeleted = forum_topics_softdeleted - 1';
927        }
928
929    }
930}