Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
32.21% covered (danger)
32.21%
497 / 1543
3.45% covered (danger)
3.45%
1 / 29
CRAP
n/a
0 / 0
recalc_nested_sets
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
make_forum_select
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
992
size_select_options
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
group_select_options
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
132
get_forum_list
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
210
get_forum_branch
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
72
copy_forum_permissions
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 1
182
filelist
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
15.31
move_topics
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
56
move_posts
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
30
delete_topics
86.57% covered (warning)
86.57%
58 / 67
0.00% covered (danger)
0.00%
0 / 1
14.48
delete_posts
96.03% covered (success)
96.03%
121 / 126
0.00% covered (danger)
0.00%
0 / 1
24
delete_topic_shadows
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
update_posted_info
11.54% covered (danger)
11.54%
3 / 26
0.00% covered (danger)
0.00%
0 / 1
30.92
sync
53.26% covered (warning)
53.26%
286 / 537
0.00% covered (danger)
0.00%
0 / 1
2024.76
prune
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
132
auto_prune
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
phpbb_cache_moderators
9.52% covered (danger)
9.52%
8 / 84
0.00% covered (danger)
0.00%
0 / 1
286.37
view_log
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
phpbb_update_foes
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
210
view_inactive_users
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
132
view_warned_users
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
get_database_size
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
380
tidy_warnings
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
tidy_database
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
add_permission_language
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
enable_bitfield_column_flag
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
display_ban_end_options
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
display_ban_options
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14/**
15* @ignore
16*/
17if (!defined('IN_PHPBB'))
18{
19    exit;
20}
21
22/**
23* Recalculate Nested Sets
24*
25* @param int    $new_id    first left_id (should start with 1)
26* @param string    $pkey    primary key-column (containing the id for the parent_id of the children)
27* @param string    $table    constant or fullname of the table
28* @param int    $parent_id parent_id of the current set (default = 0)
29* @param array    $where    contains strings to compare closer on the where statement (additional)
30*/
31function recalc_nested_sets(&$new_id, $pkey, $table, $parent_id = 0, $where = array())
32{
33    global $db;
34
35    $sql = 'SELECT *
36        FROM ' . $table . '
37        WHERE parent_id = ' . (int) $parent_id .
38        ((!empty($where)) ? ' AND ' . implode(' AND ', $where) : '') . '
39        ORDER BY left_id ASC';
40    $result = $db->sql_query($sql);
41    while ($row = $db->sql_fetchrow($result))
42    {
43        // First we update the left_id for this module
44        if ($row['left_id'] != $new_id)
45        {
46            $db->sql_query('UPDATE ' . $table . ' SET ' . $db->sql_build_array('UPDATE', array('left_id' => $new_id)) . " WHERE $pkey = {$row[$pkey]}");
47        }
48        $new_id++;
49
50        // Then we go through any children and update their left/right id's
51        recalc_nested_sets($new_id, $pkey, $table, $row[$pkey], $where);
52
53        // Then we come back and update the right_id for this module
54        if ($row['right_id'] != $new_id)
55        {
56            $db->sql_query('UPDATE ' . $table . ' SET ' . $db->sql_build_array('UPDATE', array('right_id' => $new_id)) . " WHERE $pkey = {$row[$pkey]}");
57        }
58        $new_id++;
59    }
60    $db->sql_freeresult($result);
61}
62
63/**
64* Simple version of jumpbox, just lists authed forums
65*/
66function make_forum_select($select_id = false, $ignore_id = false, $ignore_acl = false, $ignore_nonpost = false, $ignore_emptycat = true, $only_acl_post = false, $return_array = false)
67{
68    global $db, $auth, $phpbb_dispatcher;
69
70    // This query is identical to the jumpbox one
71    $sql = 'SELECT forum_id, forum_name, parent_id, forum_type, forum_flags, forum_options, left_id, right_id
72        FROM ' . FORUMS_TABLE . '
73        ORDER BY left_id ASC';
74    $result = $db->sql_query($sql, 600);
75
76    $rowset = array();
77    while ($row = $db->sql_fetchrow($result))
78    {
79        $rowset[(int) $row['forum_id']] = $row;
80    }
81    $db->sql_freeresult($result);
82
83    $right = 0;
84    $padding_store = array('0' => '');
85    $padding = '';
86    $forum_list = ($return_array) ? array() : '';
87
88    /**
89    * Modify the forum list data
90    *
91    * @event core.make_forum_select_modify_forum_list
92    * @var    array    rowset    Array with the forums list data
93    * @since 3.1.10-RC1
94    */
95    $vars = array('rowset');
96    extract($phpbb_dispatcher->trigger_event('core.make_forum_select_modify_forum_list', compact($vars)));
97
98    // Sometimes it could happen that forums will be displayed here not be displayed within the index page
99    // This is the result of forums not displayed at index, having list permissions and a parent of a forum with no permissions.
100    // If this happens, the padding could be "broken"
101
102    foreach ($rowset as $row)
103    {
104        if ($row['left_id'] < $right)
105        {
106            $padding .= '&nbsp; &nbsp;';
107            $padding_store[$row['parent_id']] = $padding;
108        }
109        else if ($row['left_id'] > $right + 1)
110        {
111            $padding = (isset($padding_store[$row['parent_id']])) ? $padding_store[$row['parent_id']] : '';
112        }
113
114        $right = $row['right_id'];
115        $disabled = false;
116
117        if (!$ignore_acl && $auth->acl_gets(array('f_list', 'a_forum', 'a_forumadd', 'a_forumdel'), $row['forum_id']))
118        {
119            if ($only_acl_post && !$auth->acl_get('f_post', $row['forum_id']) || (!$auth->acl_get('m_approve', $row['forum_id']) && !$auth->acl_get('f_noapprove', $row['forum_id'])))
120            {
121                $disabled = true;
122            }
123        }
124        else if (!$ignore_acl)
125        {
126            continue;
127        }
128
129        if (
130            ((is_array($ignore_id) && in_array($row['forum_id'], $ignore_id)) || $row['forum_id'] == $ignore_id)
131            ||
132            // Non-postable forum with no subforums, don't display
133            ($row['forum_type'] == FORUM_CAT && ($row['left_id'] + 1 == $row['right_id']) && $ignore_emptycat)
134            ||
135            ($row['forum_type'] != FORUM_POST && $ignore_nonpost)
136            )
137        {
138            $disabled = true;
139        }
140
141        if ($return_array)
142        {
143            // Include some more information...
144            $selected = (is_array($select_id)) ? ((in_array($row['forum_id'], $select_id)) ? true : false) : (($row['forum_id'] == $select_id) ? true : false);
145            $forum_list[$row['forum_id']] = array_merge(array('padding' => $padding, 'selected' => ($selected && !$disabled), 'disabled' => $disabled), $row);
146        }
147        else
148        {
149            $selected = (is_array($select_id)) ? ((in_array($row['forum_id'], $select_id)) ? ' selected="selected"' : '') : (($row['forum_id'] == $select_id) ? ' selected="selected"' : '');
150            $forum_list .= '<option value="' . $row['forum_id'] . '"' . (($disabled) ? ' disabled="disabled" class="disabled-option"' : $selected) . '>' . $padding . $row['forum_name'] . '</option>';
151        }
152    }
153    unset($padding_store, $rowset);
154
155    return $forum_list;
156}
157
158/**
159* Generate size select options
160*/
161function size_select_options($size_compare)
162{
163    global $language;
164
165    $size_types_text = array($language->lang('BYTES'), $language->lang('KIB'), $language->lang('MIB'));
166    $size_types = array('b', 'kb', 'mb');
167
168    $size_options = [];
169
170    for ($i = 0, $size = count($size_types_text); $i < $size; $i++)
171    {
172        $size_options[] = [
173            'value'        => $size_types[$i],
174            'selected'    => $size_compare == $size_types[$i],
175            'label'        => $size_types_text[$i],
176        ];
177    }
178
179    return $size_options;
180}
181
182/**
183* Generate list of groups (option fields without select)
184*
185* @param int $group_id The default group id to mark as selected
186* @param array $exclude_ids The group ids to exclude from the list, false (default) if you whish to exclude no id
187* @param int $manage_founder If set to false (default) all groups are returned, if 0 only those groups returned not being managed by founders only, if 1 only those groups returned managed by founders only.
188*
189* @return string The list of options.
190*/
191function group_select_options($group_id, $exclude_ids = false, $manage_founder = false)
192{
193    global $db, $config, $phpbb_container;
194
195    /** @var \phpbb\group\helper $group_helper */
196    $group_helper = $phpbb_container->get('group_helper');
197
198    $exclude_sql = ($exclude_ids !== false && count($exclude_ids)) ? 'WHERE ' . $db->sql_in_set('group_id', array_map('intval', $exclude_ids), true) : '';
199    $sql_and = (!$config['coppa_enable']) ? (($exclude_sql) ? ' AND ' : ' WHERE ') . "group_name <> 'REGISTERED_COPPA'" : '';
200    $sql_founder = ($manage_founder !== false) ? (($exclude_sql || $sql_and) ? ' AND ' : ' WHERE ') . 'group_founder_manage = ' . (int) $manage_founder : '';
201
202    $sql = 'SELECT group_id, group_name, group_type
203        FROM ' . GROUPS_TABLE . "
204        $exclude_sql
205        $sql_and
206        $sql_founder
207        ORDER BY group_type DESC, group_name ASC";
208    $result = $db->sql_query($sql);
209
210    $s_group_options = '';
211    while ($row = $db->sql_fetchrow($result))
212    {
213        $selected = ($row['group_id'] == $group_id) ? ' selected="selected"' : '';
214        $s_group_options .= '<option' . (($row['group_type'] == GROUP_SPECIAL) ? ' class="sep"' : '') . ' value="' . $row['group_id'] . '"' . $selected . '>' . $group_helper->get_name($row['group_name']) . '</option>';
215    }
216    $db->sql_freeresult($result);
217
218    return $s_group_options;
219}
220
221/**
222* Obtain authed forums list
223*/
224function get_forum_list($acl_list = 'f_list', $id_only = true, $postable_only = false, $no_cache = false)
225{
226    global $db, $auth, $phpbb_dispatcher;
227    static $forum_rows;
228
229    if (!isset($forum_rows))
230    {
231        // This query is identical to the jumpbox one
232        $expire_time = ($no_cache) ? 0 : 600;
233
234        $sql = 'SELECT forum_id, forum_name, parent_id, forum_type, left_id, right_id
235            FROM ' . FORUMS_TABLE . '
236            ORDER BY left_id ASC';
237        $result = $db->sql_query($sql, $expire_time);
238
239        $forum_rows = array();
240
241        $right = $padding = 0;
242        $padding_store = array('0' => 0);
243
244        while ($row = $db->sql_fetchrow($result))
245        {
246            if ($row['left_id'] < $right)
247            {
248                $padding++;
249                $padding_store[$row['parent_id']] = $padding;
250            }
251            else if ($row['left_id'] > $right + 1)
252            {
253                // Ok, if the $padding_store for this parent is empty there is something wrong. For now we will skip over it.
254                // @todo digging deep to find out "how" this can happen.
255                $padding = (isset($padding_store[$row['parent_id']])) ? $padding_store[$row['parent_id']] : $padding;
256            }
257
258            $right = $row['right_id'];
259            $row['padding'] = $padding;
260
261            $forum_rows[] = $row;
262        }
263        $db->sql_freeresult($result);
264        unset($padding_store);
265    }
266
267    $rowset = array();
268    foreach ($forum_rows as $row)
269    {
270        if ($postable_only && $row['forum_type'] != FORUM_POST)
271        {
272            continue;
273        }
274
275        if ($acl_list == '' || ($acl_list != '' && $auth->acl_gets($acl_list, $row['forum_id'])))
276        {
277            $rowset[] = ($id_only) ? (int) $row['forum_id'] : $row;
278        }
279    }
280
281    /**
282    * Modify the forum list data
283    *
284    * @event core.get_forum_list_modify_data
285    * @var    array    rowset    Array with the forum list data
286    * @since 3.1.10-RC1
287    */
288    $vars = array('rowset');
289    extract($phpbb_dispatcher->trigger_event('core.get_forum_list_modify_data', compact($vars)));
290
291    return $rowset;
292}
293
294/**
295* Get forum branch
296*/
297function get_forum_branch($forum_id, $type = 'all', $order = 'descending', $include_forum = true)
298{
299    global $db;
300
301    switch ($type)
302    {
303        case 'parents':
304            $condition = 'f1.left_id BETWEEN f2.left_id AND f2.right_id';
305        break;
306
307        case 'children':
308            $condition = 'f2.left_id BETWEEN f1.left_id AND f1.right_id';
309        break;
310
311        default:
312            $condition = 'f2.left_id BETWEEN f1.left_id AND f1.right_id OR f1.left_id BETWEEN f2.left_id AND f2.right_id';
313        break;
314    }
315
316    $rows = array();
317
318    $sql = 'SELECT f2.*
319        FROM ' . FORUMS_TABLE . ' f1
320        LEFT JOIN ' . FORUMS_TABLE . " f2 ON ($condition)
321        WHERE f1.forum_id = $forum_id
322        ORDER BY f2.left_id " . (($order == 'descending') ? 'ASC' : 'DESC');
323    $result = $db->sql_query($sql);
324
325    while ($row = $db->sql_fetchrow($result))
326    {
327        if (!$include_forum && $row['forum_id'] == $forum_id)
328        {
329            continue;
330        }
331
332        $rows[] = $row;
333    }
334    $db->sql_freeresult($result);
335
336    return $rows;
337}
338
339/**
340* Copies permissions from one forum to others
341*
342* @param int    $src_forum_id        The source forum we want to copy permissions from
343* @param array    $dest_forum_ids        The destination forum(s) we want to copy to
344* @param bool    $clear_dest_perms    True if destination permissions should be deleted
345* @param bool    $add_log            True if log entry should be added
346*
347* @return bool                        False on error
348*/
349function copy_forum_permissions($src_forum_id, $dest_forum_ids, $clear_dest_perms = true, $add_log = true)
350{
351    global $db, $user, $phpbb_log;
352
353    // Only one forum id specified
354    if (!is_array($dest_forum_ids))
355    {
356        $dest_forum_ids = array($dest_forum_ids);
357    }
358
359    // Make sure forum ids are integers
360    $src_forum_id = (int) $src_forum_id;
361    $dest_forum_ids = array_map('intval', $dest_forum_ids);
362
363    // No source forum or no destination forums specified
364    if (empty($src_forum_id) || empty($dest_forum_ids))
365    {
366        return false;
367    }
368
369    // Check if source forum exists
370    $sql = 'SELECT forum_name
371        FROM ' . FORUMS_TABLE . '
372        WHERE forum_id = ' . $src_forum_id;
373    $result = $db->sql_query($sql);
374    $src_forum_name = $db->sql_fetchfield('forum_name');
375    $db->sql_freeresult($result);
376
377    // Source forum doesn't exist
378    if (empty($src_forum_name))
379    {
380        return false;
381    }
382
383    // Check if destination forums exists
384    $sql = 'SELECT forum_id, forum_name
385        FROM ' . FORUMS_TABLE . '
386        WHERE ' . $db->sql_in_set('forum_id', $dest_forum_ids);
387    $result = $db->sql_query($sql);
388
389    $dest_forum_ids = $dest_forum_names = array();
390    while ($row = $db->sql_fetchrow($result))
391    {
392        $dest_forum_ids[]    = (int) $row['forum_id'];
393        $dest_forum_names[]    = $row['forum_name'];
394    }
395    $db->sql_freeresult($result);
396
397    // No destination forum exists
398    if (empty($dest_forum_ids))
399    {
400        return false;
401    }
402
403    // From the mysql documentation:
404    // Prior to MySQL 4.0.14, the target table of the INSERT statement cannot appear
405    // in the FROM clause of the SELECT part of the query. This limitation is lifted in 4.0.14.
406    // Due to this we stay on the safe side if we do the insertion "the manual way"
407
408    // Rowsets we're going to insert
409    $users_sql_ary = $groups_sql_ary = array();
410
411    // Query acl users table for source forum data
412    $sql = 'SELECT user_id, auth_option_id, auth_role_id, auth_setting
413        FROM ' . ACL_USERS_TABLE . '
414        WHERE forum_id = ' . $src_forum_id;
415    $result = $db->sql_query($sql);
416
417    while ($row = $db->sql_fetchrow($result))
418    {
419        $row = array(
420            'user_id'            => (int) $row['user_id'],
421            'auth_option_id'    => (int) $row['auth_option_id'],
422            'auth_role_id'        => (int) $row['auth_role_id'],
423            'auth_setting'        => (int) $row['auth_setting'],
424        );
425
426        foreach ($dest_forum_ids as $dest_forum_id)
427        {
428            $users_sql_ary[] = $row + array('forum_id' => $dest_forum_id);
429        }
430    }
431    $db->sql_freeresult($result);
432
433    // Query acl groups table for source forum data
434    $sql = 'SELECT group_id, auth_option_id, auth_role_id, auth_setting
435        FROM ' . ACL_GROUPS_TABLE . '
436        WHERE forum_id = ' . $src_forum_id;
437    $result = $db->sql_query($sql);
438
439    while ($row = $db->sql_fetchrow($result))
440    {
441        $row = array(
442            'group_id'            => (int) $row['group_id'],
443            'auth_option_id'    => (int) $row['auth_option_id'],
444            'auth_role_id'        => (int) $row['auth_role_id'],
445            'auth_setting'        => (int) $row['auth_setting'],
446        );
447
448        foreach ($dest_forum_ids as $dest_forum_id)
449        {
450            $groups_sql_ary[] = $row + array('forum_id' => $dest_forum_id);
451        }
452    }
453    $db->sql_freeresult($result);
454
455    $db->sql_transaction('begin');
456
457    // Clear current permissions of destination forums
458    if ($clear_dest_perms)
459    {
460        $sql = 'DELETE FROM ' . ACL_USERS_TABLE . '
461            WHERE ' . $db->sql_in_set('forum_id', $dest_forum_ids);
462        $db->sql_query($sql);
463
464        $sql = 'DELETE FROM ' . ACL_GROUPS_TABLE . '
465            WHERE ' . $db->sql_in_set('forum_id', $dest_forum_ids);
466        $db->sql_query($sql);
467    }
468
469    $db->sql_multi_insert(ACL_USERS_TABLE, $users_sql_ary);
470    $db->sql_multi_insert(ACL_GROUPS_TABLE, $groups_sql_ary);
471
472    if ($add_log)
473    {
474        $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_FORUM_COPIED_PERMISSIONS', false, array($src_forum_name, implode(', ', $dest_forum_names)));
475    }
476
477    $db->sql_transaction('commit');
478
479    return true;
480}
481
482/**
483* Get physical file listing
484*/
485function filelist($rootdir, $dir = '', $type = 'gif|jpg|jpeg|png|svg|webp')
486{
487    $matches = array($dir => array());
488
489    // Remove initial / if present
490    $rootdir = (substr($rootdir, 0, 1) == '/') ? substr($rootdir, 1) : $rootdir;
491    // Add closing / if not present
492    $rootdir = ($rootdir && substr($rootdir, -1) != '/') ? $rootdir . '/' : $rootdir;
493
494    // Remove initial / if present
495    $dir = (substr($dir, 0, 1) == '/') ? substr($dir, 1) : $dir;
496    // Add closing / if not present
497    $dir = ($dir && substr($dir, -1) != '/') ? $dir . '/' : $dir;
498
499    if (!is_dir($rootdir . $dir))
500    {
501        return $matches;
502    }
503
504    $dh = @opendir($rootdir . $dir);
505
506    if (!$dh)
507    {
508        return $matches;
509    }
510
511    while (($fname = readdir($dh)) !== false)
512    {
513        if (is_file("$rootdir$dir$fname"))
514        {
515            if (filesize("$rootdir$dir$fname") && preg_match('#\.' . $type . '$#i', $fname))
516            {
517                $matches[$dir][] = $fname;
518            }
519        }
520        else if ($fname[0] != '.' && is_dir("$rootdir$dir$fname"))
521        {
522            $matches += filelist($rootdir, "$dir$fname", $type);
523        }
524    }
525    closedir($dh);
526
527    return $matches;
528}
529
530/**
531* Move topic(s)
532*/
533function move_topics($topic_ids, $forum_id, $auto_sync = true)
534{
535    global $db, $phpbb_dispatcher;
536
537    if (empty($topic_ids))
538    {
539        return;
540    }
541
542    $forum_ids = array($forum_id);
543
544    if (!is_array($topic_ids))
545    {
546        $topic_ids = array($topic_ids);
547    }
548
549    /**
550     * Perform additional actions before topics move
551     *
552     * @event core.move_topics_before
553     * @var    array    topic_ids    Array of the moved topic ids
554     * @var    string    forum_id    The forum id from where the topics are moved
555     * @since 3.2.9-RC1
556     */
557    $vars = array(
558        'topic_ids',
559        'forum_id',
560    );
561    extract($phpbb_dispatcher->trigger_event('core.move_topics_before', compact($vars)));
562
563    $sql = 'DELETE FROM ' . TOPICS_TABLE . '
564        WHERE ' . $db->sql_in_set('topic_moved_id', $topic_ids) . '
565            AND forum_id = ' . $forum_id;
566    $db->sql_query($sql);
567
568    if ($auto_sync)
569    {
570        $sql = 'SELECT DISTINCT forum_id
571            FROM ' . TOPICS_TABLE . '
572            WHERE ' . $db->sql_in_set('topic_id', $topic_ids);
573        $result = $db->sql_query($sql);
574
575        while ($row = $db->sql_fetchrow($result))
576        {
577            $forum_ids[] = $row['forum_id'];
578        }
579        $db->sql_freeresult($result);
580    }
581
582    $table_ary = array(TOPICS_TABLE, POSTS_TABLE, LOG_TABLE, DRAFTS_TABLE, TOPICS_TRACK_TABLE);
583
584    /**
585     * Perform additional actions before topics move
586     *
587     * @event core.move_topics_before_query
588     * @var    array    table_ary    Array of tables from which forum_id will be updated for all rows that hold the moved topics
589     * @var    array    topic_ids    Array of the moved topic ids
590     * @var    string    forum_id    The forum id from where the topics are moved
591     * @var    array    forum_ids    Array of the forums where the topics are moving (includes also forum_id)
592     * @var bool    auto_sync    Whether or not to perform auto sync
593     * @since 3.1.5-RC1
594     */
595    $vars = array(
596            'table_ary',
597            'topic_ids',
598            'forum_id',
599            'forum_ids',
600            'auto_sync',
601    );
602    extract($phpbb_dispatcher->trigger_event('core.move_topics_before_query', compact($vars)));
603
604    foreach ($table_ary as $table)
605    {
606        $sql = "UPDATE $table
607            SET forum_id = $forum_id
608            WHERE " . $db->sql_in_set('topic_id', $topic_ids);
609        $db->sql_query($sql);
610    }
611    unset($table_ary);
612
613    /**
614     * Perform additional actions after topics move
615     *
616     * @event core.move_topics_after
617     * @var    array    topic_ids    Array of the moved topic ids
618     * @var    string    forum_id    The forum id from where the topics were moved
619     * @var    array    forum_ids    Array of the forums where the topics were moved (includes also forum_id)
620     * @since 3.2.9-RC1
621     */
622    $vars = array(
623        'topic_ids',
624        'forum_id',
625        'forum_ids',
626    );
627    extract($phpbb_dispatcher->trigger_event('core.move_topics_after', compact($vars)));
628
629    if ($auto_sync)
630    {
631        sync('forum', 'forum_id', $forum_ids, true, true);
632        unset($forum_ids);
633    }
634}
635
636/**
637* Move post(s)
638*/
639function move_posts($post_ids, $topic_id, $auto_sync = true)
640{
641    global $db, $phpbb_dispatcher;
642
643    if (!is_array($post_ids))
644    {
645        $post_ids = array($post_ids);
646    }
647
648    $forum_ids = array();
649    $topic_ids = array($topic_id);
650
651    $sql = 'SELECT DISTINCT topic_id, forum_id
652        FROM ' . POSTS_TABLE . '
653        WHERE ' . $db->sql_in_set('post_id', $post_ids);
654    $result = $db->sql_query($sql);
655
656    while ($row = $db->sql_fetchrow($result))
657    {
658        $forum_ids[] = (int) $row['forum_id'];
659        $topic_ids[] = (int) $row['topic_id'];
660    }
661    $db->sql_freeresult($result);
662
663    $sql = 'SELECT forum_id
664        FROM ' . TOPICS_TABLE . '
665        WHERE topic_id = ' . $topic_id;
666    $result = $db->sql_query($sql);
667    $forum_row = $db->sql_fetchrow($result);
668    $db->sql_freeresult($result);
669
670    if (!$forum_row)
671    {
672        trigger_error('NO_TOPIC');
673    }
674
675    /**
676     * Perform additional actions before moving posts
677     *
678     * @event core.move_posts_before
679     * @var    array    post_ids    Array of post ids to move
680     * @var    int        topic_id    The topic id the posts are moved to
681     * @var    bool    auto_sync    Whether or not to perform auto sync
682     * @var    array    forum_ids    Array of the forum ids the posts are moved from
683     * @var    array    topic_ids    Array of the topic ids the posts are moved from
684     * @var    array    forum_row    Array with the forum id of the topic the posts are moved to
685     * @since 3.1.7-RC1
686     */
687    $vars = array(
688            'post_ids',
689            'topic_id',
690            'auto_sync',
691            'forum_ids',
692            'topic_ids',
693            'forum_row',
694    );
695    extract($phpbb_dispatcher->trigger_event('core.move_posts_before', compact($vars)));
696
697    $sql = 'UPDATE ' . POSTS_TABLE . '
698        SET forum_id = ' . (int) $forum_row['forum_id'] . ", topic_id = $topic_id
699        WHERE " . $db->sql_in_set('post_id', $post_ids);
700    $db->sql_query($sql);
701
702    $sql = 'UPDATE ' . ATTACHMENTS_TABLE . "
703        SET topic_id = $topic_id, in_message = 0
704        WHERE " . $db->sql_in_set('post_msg_id', $post_ids);
705    $db->sql_query($sql);
706
707    /**
708     * Perform additional actions after moving posts
709     *
710     * @event core.move_posts_after
711     * @var    array    post_ids    Array of the moved post ids
712     * @var    int        topic_id    The topic id the posts are moved to
713     * @var    bool    auto_sync    Whether or not to perform auto sync
714     * @var    array    forum_ids    Array of the forum ids the posts are moved from
715     * @var    array    topic_ids    Array of the topic ids the posts are moved from
716     * @var    array    forum_row    Array with the forum id of the topic the posts are moved to
717     * @since 3.1.7-RC1
718     */
719    $vars = array(
720            'post_ids',
721            'topic_id',
722            'auto_sync',
723            'forum_ids',
724            'topic_ids',
725            'forum_row',
726    );
727    extract($phpbb_dispatcher->trigger_event('core.move_posts_after', compact($vars)));
728
729    if ($auto_sync)
730    {
731        $forum_ids[] = (int) $forum_row['forum_id'];
732
733        sync('topic_reported', 'topic_id', $topic_ids);
734        sync('topic_attachment', 'topic_id', $topic_ids);
735        sync('topic', 'topic_id', $topic_ids, true);
736        sync('forum', 'forum_id', $forum_ids, true, true);
737
738        /**
739         * Perform additional actions after move post sync
740         *
741         * @event core.move_posts_sync_after
742         * @var    array    post_ids    Array of the moved post ids
743         * @var    int        topic_id    The topic id the posts are moved to
744         * @var    bool    auto_sync    Whether or not to perform auto sync
745         * @var    array    forum_ids    Array of the forum ids the posts are moved from
746         * @var    array    topic_ids    Array of the topic ids the posts are moved from
747         * @var    array    forum_row    Array with the forum id of the topic the posts are moved to
748         * @since 3.1.11-RC1
749         */
750        $vars = array(
751            'post_ids',
752            'topic_id',
753            'auto_sync',
754            'forum_ids',
755            'topic_ids',
756            'forum_row',
757        );
758        extract($phpbb_dispatcher->trigger_event('core.move_posts_sync_after', compact($vars)));
759    }
760
761    // Update posted information
762    update_posted_info($topic_ids);
763}
764
765/**
766* Remove topic(s)
767* @return array with topics and posts affected
768*/
769function delete_topics($where_type, $where_ids, $auto_sync = true, $post_count_sync = true, $call_delete_posts = true)
770{
771    global $db, $config, $phpbb_container, $phpbb_dispatcher;
772
773    $approved_topics = 0;
774    $forum_ids = $topic_ids = array();
775
776    if ($where_type === 'range')
777    {
778        $where_clause = $where_ids;
779    }
780    else
781    {
782        $where_ids = (is_array($where_ids)) ? array_unique($where_ids) : array($where_ids);
783
784        if (!count($where_ids))
785        {
786            return array('topics' => 0, 'posts' => 0);
787        }
788
789        $where_clause = $db->sql_in_set($where_type, $where_ids);
790    }
791
792    // Making sure that delete_posts does not call delete_topics again...
793    $return = array(
794        'posts' => ($call_delete_posts) ? delete_posts($where_type, $where_ids, false, true, $post_count_sync, false) : 0,
795    );
796
797    $sql = 'SELECT topic_id, forum_id, topic_visibility, topic_moved_id
798        FROM ' . TOPICS_TABLE . '
799        WHERE ' . $where_clause;
800    $result = $db->sql_query($sql);
801
802    while ($row = $db->sql_fetchrow($result))
803    {
804        $forum_ids[] = $row['forum_id'];
805        $topic_ids[] = $row['topic_id'];
806
807        if ($row['topic_visibility'] == ITEM_APPROVED && !$row['topic_moved_id'])
808        {
809            $approved_topics++;
810        }
811    }
812    $db->sql_freeresult($result);
813
814    $return['topics'] = count($topic_ids);
815
816    if (!count($topic_ids))
817    {
818        return $return;
819    }
820
821    $db->sql_transaction('begin');
822
823    $table_ary = array(BOOKMARKS_TABLE, TOPICS_TRACK_TABLE, TOPICS_POSTED_TABLE, POLL_VOTES_TABLE, POLL_OPTIONS_TABLE, TOPICS_WATCH_TABLE, TOPICS_TABLE);
824
825    /**
826     * Perform additional actions before topic(s) deletion
827     *
828     * @event core.delete_topics_before_query
829     * @var    array    table_ary    Array of tables from which all rows will be deleted that hold a topic_id occuring in topic_ids
830     * @var    array    topic_ids    Array of topic ids to delete
831     * @since 3.1.4-RC1
832     */
833    $vars = array(
834            'table_ary',
835            'topic_ids',
836    );
837    extract($phpbb_dispatcher->trigger_event('core.delete_topics_before_query', compact($vars)));
838
839    foreach ($table_ary as $table)
840    {
841        $sql = "DELETE FROM $table
842            WHERE " . $db->sql_in_set('topic_id', $topic_ids);
843        $db->sql_query($sql);
844    }
845    unset($table_ary);
846
847    /**
848     * Perform additional actions after topic(s) deletion
849     *
850     * @event core.delete_topics_after_query
851     * @var    array    topic_ids    Array of topic ids that were deleted
852     * @since 3.1.4-RC1
853     */
854    $vars = array(
855            'topic_ids',
856    );
857    extract($phpbb_dispatcher->trigger_event('core.delete_topics_after_query', compact($vars)));
858
859    $moved_topic_ids = array();
860
861    // update the other forums
862    $sql = 'SELECT topic_id, forum_id
863        FROM ' . TOPICS_TABLE . '
864        WHERE ' . $db->sql_in_set('topic_moved_id', $topic_ids);
865    $result = $db->sql_query($sql);
866
867    while ($row = $db->sql_fetchrow($result))
868    {
869        $forum_ids[] = $row['forum_id'];
870        $moved_topic_ids[] = $row['topic_id'];
871    }
872    $db->sql_freeresult($result);
873
874    if (count($moved_topic_ids))
875    {
876        $sql = 'DELETE FROM ' . TOPICS_TABLE . '
877            WHERE ' . $db->sql_in_set('topic_id', $moved_topic_ids);
878        $db->sql_query($sql);
879    }
880
881    $db->sql_transaction('commit');
882
883    if ($auto_sync)
884    {
885        sync('forum', 'forum_id', array_unique($forum_ids), true, true);
886        sync('topic_reported', $where_type, $where_ids);
887    }
888
889    if ($approved_topics)
890    {
891        $config->increment('num_topics', $approved_topics * (-1), false);
892    }
893
894    /* @var $phpbb_notifications \phpbb\notification\manager */
895    $phpbb_notifications = $phpbb_container->get('notification_manager');
896
897    $phpbb_notifications->delete_notifications_by_types([
898        'notification.type.topic',
899        'notification.type.approve_topic',
900        'notification.type.topic_in_queue',
901    ], $topic_ids);
902
903    return $return;
904}
905
906/**
907* Remove post(s)
908*/
909function delete_posts($where_type, $where_ids, $auto_sync = true, $posted_sync = true, $post_count_sync = true, $call_delete_topics = true)
910{
911    global $db, $config, $phpbb_container, $phpbb_dispatcher;
912
913    // Notifications types to delete
914    $delete_notifications_types = array(
915        'notification.type.mention',
916        'notification.type.quote',
917        'notification.type.approve_post',
918        'notification.type.post_in_queue',
919        'notification.type.report_post',
920    );
921
922    /**
923    * Perform additional actions before post(s) deletion
924    *
925    * @event core.delete_posts_before
926    * @var    string    where_type                    Variable containing posts deletion mode
927    * @var    mixed    where_ids                    Array or comma separated list of posts ids to delete
928    * @var    bool    auto_sync                    Flag indicating if topics/forums should be synchronized
929    * @var    bool    posted_sync                    Flag indicating if topics_posted table should be resynchronized
930    * @var    bool    post_count_sync                Flag indicating if posts count should be resynchronized
931    * @var    bool    call_delete_topics            Flag indicating if topics having no posts should be deleted
932    * @var    array    delete_notifications_types    Array with notifications types to delete
933    * @since 3.1.0-a4
934    */
935    $vars = array(
936        'where_type',
937        'where_ids',
938        'auto_sync',
939        'posted_sync',
940        'post_count_sync',
941        'call_delete_topics',
942        'delete_notifications_types',
943    );
944    extract($phpbb_dispatcher->trigger_event('core.delete_posts_before', compact($vars)));
945
946    if ($where_type === 'range')
947    {
948        $where_clause = $where_ids;
949    }
950    else
951    {
952        if (is_array($where_ids))
953        {
954            $where_ids = array_unique($where_ids);
955        }
956        else
957        {
958            $where_ids = array($where_ids);
959        }
960
961        if (!count($where_ids))
962        {
963            return false;
964        }
965
966        $where_ids = array_map('intval', $where_ids);
967
968/*        Possible code for splitting post deletion
969        if (count($where_ids) >= 1001)
970        {
971            // Split into chunks of 1000
972            $chunks = array_chunk($where_ids, 1000);
973
974            foreach ($chunks as $_where_ids)
975            {
976                delete_posts($where_type, $_where_ids, $auto_sync, $posted_sync, $post_count_sync, $call_delete_topics);
977            }
978
979            return;
980        }*/
981
982        $where_clause = $db->sql_in_set($where_type, $where_ids);
983    }
984
985    $approved_posts = 0;
986    $post_ids = $poster_ids = $topic_ids = $forum_ids = $post_counts = $remove_topics = array();
987
988    $sql = 'SELECT post_id, poster_id, post_visibility, post_postcount, topic_id, forum_id
989        FROM ' . POSTS_TABLE . '
990        WHERE ' . $where_clause;
991    $result = $db->sql_query($sql);
992
993    while ($row = $db->sql_fetchrow($result))
994    {
995        $post_ids[] = (int) $row['post_id'];
996        $poster_ids[] = (int) $row['poster_id'];
997        $topic_ids[] = (int) $row['topic_id'];
998        $forum_ids[] = (int) $row['forum_id'];
999
1000        if ($row['post_postcount'] && $post_count_sync && $row['post_visibility'] == ITEM_APPROVED)
1001        {
1002            $post_counts[$row['poster_id']] = (!empty($post_counts[$row['poster_id']])) ? $post_counts[$row['poster_id']] + 1 : 1;
1003        }
1004
1005        if ($row['post_visibility'] == ITEM_APPROVED)
1006        {
1007            $approved_posts++;
1008        }
1009    }
1010    $db->sql_freeresult($result);
1011
1012    if (!count($post_ids))
1013    {
1014        return false;
1015    }
1016
1017    $db->sql_transaction('begin');
1018
1019    $table_ary = array(POSTS_TABLE, REPORTS_TABLE);
1020
1021    /**
1022    * Perform additional actions during post(s) deletion before running the queries
1023    *
1024    * @event core.delete_posts_in_transaction_before
1025    * @var    array    post_ids                    Array with deleted posts' ids
1026    * @var    array    poster_ids                    Array with deleted posts' author ids
1027    * @var    array    topic_ids                    Array with deleted posts' topic ids
1028    * @var    array    forum_ids                    Array with deleted posts' forum ids
1029    * @var    string    where_type                    Variable containing posts deletion mode
1030    * @var    mixed    where_ids                    Array or comma separated list of post ids to delete
1031    * @var    array    delete_notifications_types    Array with notifications types to delete
1032    * @var    array    table_ary                    Array with table names to delete data from
1033    * @since 3.1.7-RC1
1034    */
1035    $vars = array(
1036        'post_ids',
1037        'poster_ids',
1038        'topic_ids',
1039        'forum_ids',
1040        'where_type',
1041        'where_ids',
1042        'delete_notifications_types',
1043        'table_ary',
1044    );
1045    extract($phpbb_dispatcher->trigger_event('core.delete_posts_in_transaction_before', compact($vars)));
1046
1047    foreach ($table_ary as $table)
1048    {
1049        $sql = "DELETE FROM $table
1050            WHERE " . $db->sql_in_set('post_id', $post_ids);
1051        $db->sql_query($sql);
1052    }
1053    unset($table_ary);
1054
1055    // Adjust users post counts
1056    if (count($post_counts) && $post_count_sync)
1057    {
1058        foreach ($post_counts as $poster_id => $substract)
1059        {
1060            $sql = 'UPDATE ' . USERS_TABLE . '
1061                SET user_posts = 0
1062                WHERE user_id = ' . $poster_id . '
1063                AND user_posts < ' . $substract;
1064            $db->sql_query($sql);
1065
1066            $sql = 'UPDATE ' . USERS_TABLE . '
1067                SET user_posts = user_posts - ' . $substract . '
1068                WHERE user_id = ' . $poster_id . '
1069                AND user_posts >= ' . $substract;
1070            $db->sql_query($sql);
1071        }
1072    }
1073
1074    // Remove topics now having no posts?
1075    if (count($topic_ids))
1076    {
1077        $sql = 'SELECT topic_id
1078            FROM ' . POSTS_TABLE . '
1079            WHERE ' . $db->sql_in_set('topic_id', $topic_ids) . '
1080            GROUP BY topic_id';
1081        $result = $db->sql_query($sql);
1082
1083        while ($row = $db->sql_fetchrow($result))
1084        {
1085            $remove_topics[] = $row['topic_id'];
1086        }
1087        $db->sql_freeresult($result);
1088
1089        // Actually, those not within remove_topics should be removed. ;)
1090        $remove_topics = array_diff($topic_ids, $remove_topics);
1091    }
1092
1093    // Remove the message from the search index
1094    try
1095    {
1096        $search_backend_factory = $phpbb_container->get('search.backend_factory');
1097        $search = $search_backend_factory->get_active();
1098    }
1099    catch (\phpbb\search\exception\no_search_backend_found_exception $e)
1100    {
1101        trigger_error('NO_SUCH_SEARCH_MODULE');
1102    }
1103
1104    $search->index_remove($post_ids, $poster_ids, $forum_ids);
1105
1106    /** @var \phpbb\attachment\manager $attachment_manager */
1107    $attachment_manager = $phpbb_container->get('attachment.manager');
1108    $attachment_manager->delete('post', $post_ids, false);
1109    unset($attachment_manager);
1110
1111    /**
1112    * Perform additional actions during post(s) deletion
1113    *
1114    * @event core.delete_posts_in_transaction
1115    * @var    array    post_ids                    Array with deleted posts' ids
1116    * @var    array    poster_ids                    Array with deleted posts' author ids
1117    * @var    array    topic_ids                    Array with deleted posts' topic ids
1118    * @var    array    forum_ids                    Array with deleted posts' forum ids
1119    * @var    string    where_type                    Variable containing posts deletion mode
1120    * @var    mixed    where_ids                    Array or comma separated list of posts ids to delete
1121    * @var    array    delete_notifications_types    Array with notifications types to delete
1122    * @since 3.1.0-a4
1123    */
1124    $vars = array(
1125        'post_ids',
1126        'poster_ids',
1127        'topic_ids',
1128        'forum_ids',
1129        'where_type',
1130        'where_ids',
1131        'delete_notifications_types',
1132    );
1133    extract($phpbb_dispatcher->trigger_event('core.delete_posts_in_transaction', compact($vars)));
1134
1135    $db->sql_transaction('commit');
1136
1137    /**
1138    * Perform additional actions after post(s) deletion
1139    *
1140    * @event core.delete_posts_after
1141    * @var    array    post_ids                    Array with deleted posts' ids
1142    * @var    array    poster_ids                    Array with deleted posts' author ids
1143    * @var    array    topic_ids                    Array with deleted posts' topic ids
1144    * @var    array    forum_ids                    Array with deleted posts' forum ids
1145    * @var    string    where_type                    Variable containing posts deletion mode
1146    * @var    mixed    where_ids                    Array or comma separated list of posts ids to delete
1147    * @var    array    delete_notifications_types    Array with notifications types to delete
1148    * @since 3.1.0-a4
1149    */
1150    $vars = array(
1151        'post_ids',
1152        'poster_ids',
1153        'topic_ids',
1154        'forum_ids',
1155        'where_type',
1156        'where_ids',
1157        'delete_notifications_types',
1158    );
1159    extract($phpbb_dispatcher->trigger_event('core.delete_posts_after', compact($vars)));
1160
1161    // Resync topics_posted table
1162    if ($posted_sync)
1163    {
1164        update_posted_info($topic_ids);
1165    }
1166
1167    if ($auto_sync)
1168    {
1169        sync('topic_reported', 'topic_id', $topic_ids);
1170        sync('topic', 'topic_id', $topic_ids, true);
1171        sync('forum', 'forum_id', $forum_ids, true, true);
1172    }
1173
1174    if ($approved_posts && $post_count_sync)
1175    {
1176        $config->increment('num_posts', $approved_posts * (-1), false);
1177    }
1178
1179    // We actually remove topics now to not be inconsistent (the delete_topics function calls this function too)
1180    if (count($remove_topics) && $call_delete_topics)
1181    {
1182        delete_topics('topic_id', $remove_topics, $auto_sync, $post_count_sync, false);
1183    }
1184
1185    /* @var $phpbb_notifications \phpbb\notification\manager */
1186    $phpbb_notifications = $phpbb_container->get('notification_manager');
1187
1188    $phpbb_notifications->delete_notifications_by_types($delete_notifications_types, $post_ids);
1189
1190    return count($post_ids);
1191}
1192
1193/**
1194* Deletes shadow topics pointing to a specified forum.
1195*
1196* @param int        $forum_id        The forum id
1197* @param string        $sql_more        Additional WHERE statement, e.g. t.topic_time < (time() - 1234)
1198* @param bool        $auto_sync        Will call sync() if this is true
1199*
1200* @return array        Array with affected forums
1201*/
1202function delete_topic_shadows($forum_id, $sql_more = '', $auto_sync = true)
1203{
1204    global $db;
1205
1206    if (!$forum_id)
1207    {
1208        // Nothing to do.
1209        return [];
1210    }
1211
1212    // Set of affected forums we have to resync
1213    $sync_forum_ids = array();
1214
1215    // Amount of topics we select and delete at once.
1216    $batch_size = 500;
1217
1218    do
1219    {
1220        $sql = 'SELECT t2.forum_id, t2.topic_id
1221            FROM ' . TOPICS_TABLE . ' t2, ' . TOPICS_TABLE . ' t
1222            WHERE t2.topic_moved_id = t.topic_id
1223                AND t.forum_id = ' . (int) $forum_id . '
1224                ' . (($sql_more) ? 'AND ' . $sql_more : '');
1225        $result = $db->sql_query_limit($sql, $batch_size);
1226
1227        $topic_ids = array();
1228        while ($row = $db->sql_fetchrow($result))
1229        {
1230            $topic_ids[] = (int) $row['topic_id'];
1231
1232            $sync_forum_ids[(int) $row['forum_id']] = (int) $row['forum_id'];
1233        }
1234        $db->sql_freeresult($result);
1235
1236        if (!empty($topic_ids))
1237        {
1238            $sql = 'DELETE FROM ' . TOPICS_TABLE . '
1239                WHERE ' . $db->sql_in_set('topic_id', $topic_ids);
1240            $db->sql_query($sql);
1241        }
1242    }
1243    while (count($topic_ids) == $batch_size);
1244
1245    if ($auto_sync)
1246    {
1247        sync('forum', 'forum_id', $sync_forum_ids, true, true);
1248    }
1249
1250    return $sync_forum_ids;
1251}
1252
1253/**
1254* Update/Sync posted information for topics
1255*/
1256function update_posted_info(&$topic_ids)
1257{
1258    global $db, $config;
1259
1260    if (empty($topic_ids) || !$config['load_db_track'])
1261    {
1262        return;
1263    }
1264
1265    // First of all, let us remove any posted information for these topics
1266    $sql = 'DELETE FROM ' . TOPICS_POSTED_TABLE . '
1267        WHERE ' . $db->sql_in_set('topic_id', $topic_ids);
1268    $db->sql_query($sql);
1269
1270    // Now, let us collect the user/topic combos for rebuilding the information
1271    $sql = 'SELECT poster_id, topic_id
1272        FROM ' . POSTS_TABLE . '
1273        WHERE ' . $db->sql_in_set('topic_id', $topic_ids) . '
1274            AND poster_id <> ' . ANONYMOUS . '
1275        GROUP BY poster_id, topic_id';
1276    $result = $db->sql_query($sql);
1277
1278    $posted = array();
1279    while ($row = $db->sql_fetchrow($result))
1280    {
1281        // Add as key to make them unique (grouping by) and circumvent empty keys on array_unique
1282        $posted[$row['poster_id']][] = $row['topic_id'];
1283    }
1284    $db->sql_freeresult($result);
1285
1286    // Now add the information...
1287    $sql_ary = array();
1288    foreach ($posted as $user_id => $topic_row)
1289    {
1290        foreach ($topic_row as $topic_id)
1291        {
1292            $sql_ary[] = array(
1293                'user_id'        => (int) $user_id,
1294                'topic_id'        => (int) $topic_id,
1295                'topic_posted'    => 1,
1296            );
1297        }
1298    }
1299    unset($posted);
1300
1301    $db->sql_multi_insert(TOPICS_POSTED_TABLE, $sql_ary);
1302}
1303
1304/**
1305* All-encompasing sync function
1306*
1307* Exaples:
1308* <code>
1309* sync('topic', 'topic_id', 123);            // resync topic #123
1310* sync('topic', 'forum_id', array(2, 3));    // resync topics from forum #2 and #3
1311* sync('topic');                            // resync all topics
1312* sync('topic', 'range', 'topic_id BETWEEN 1 AND 60');    // resync a range of topics/forums (only available for 'topic' and 'forum' modes)
1313* </code>
1314*
1315* Modes:
1316* - forum                Resync complete forum
1317* - topic                Resync topics
1318* - topic_moved            Removes topic shadows that would be in the same forum as the topic they link to
1319* - topic_visibility    Resyncs the topic_visibility flag according to the status of the first post
1320* - post_reported        Resyncs the post_reported flag, relying on actual reports
1321* - topic_reported        Resyncs the topic_reported flag, relying on post_reported flags
1322* - post_attachement    Same as post_reported, but with attachment flags
1323* - topic_attachement    Same as topic_reported, but with attachment flags
1324*/
1325function sync($mode, $where_type = '', $where_ids = '', $resync_parents = false, $sync_extra = false)
1326{
1327    global $db, $phpbb_dispatcher;
1328
1329    if (is_array($where_ids))
1330    {
1331        $where_ids = array_unique($where_ids);
1332        $where_ids = array_map('intval', $where_ids);
1333    }
1334    else if ($where_type != 'range')
1335    {
1336        $where_ids = ($where_ids) ? array((int) $where_ids) : array();
1337    }
1338
1339    if ($mode == 'forum' || $mode == 'topic' || $mode == 'topic_visibility' || $mode == 'topic_reported' || $mode == 'post_reported')
1340    {
1341        if (!$where_type)
1342        {
1343            $where_sql = '';
1344            $where_sql_and = 'WHERE';
1345        }
1346        else if ($where_type == 'range')
1347        {
1348            // Only check a range of topics/forums. For instance: 'topic_id BETWEEN 1 AND 60'
1349            $where_sql = 'WHERE (' . $mode[0] . ".$where_ids)";
1350            $where_sql_and = $where_sql . "\n\tAND";
1351        }
1352        else
1353        {
1354            // Do not sync the "global forum"
1355            $where_ids = array_diff($where_ids, array(0));
1356
1357            if (!count($where_ids))
1358            {
1359                // Empty array with IDs. This means that we don't have any work to do. Just return.
1360                return;
1361            }
1362
1363            // Limit the topics/forums we are syncing, use specific topic/forum IDs.
1364            // $where_type contains the field for the where clause (forum_id, topic_id)
1365            $where_sql = 'WHERE ' . $db->sql_in_set($mode[0] . '.' . $where_type, $where_ids);
1366            $where_sql_and = $where_sql . "\n\tAND";
1367        }
1368    }
1369    else
1370    {
1371        if (!count($where_ids))
1372        {
1373            return;
1374        }
1375
1376        // $where_type contains the field for the where clause (forum_id, topic_id)
1377        $where_sql = 'WHERE ' . $db->sql_in_set($mode[0] . '.' . $where_type, $where_ids);
1378        $where_sql_and = $where_sql . "\n\tAND";
1379    }
1380
1381    switch ($mode)
1382    {
1383        case 'topic_moved':
1384            $db->sql_transaction('begin');
1385            switch ($db->get_sql_layer())
1386            {
1387                case 'mysqli':
1388                    $sql = 'DELETE FROM ' . TOPICS_TABLE . '
1389                        USING ' . TOPICS_TABLE . ' t1, ' . TOPICS_TABLE . " t2
1390                        WHERE t1.topic_moved_id = t2.topic_id
1391                            AND t1.forum_id = t2.forum_id";
1392                    $db->sql_query($sql);
1393                break;
1394
1395                default:
1396                    $sql = 'SELECT t1.topic_id
1397                        FROM ' .TOPICS_TABLE . ' t1, ' . TOPICS_TABLE . " t2
1398                        WHERE t1.topic_moved_id = t2.topic_id
1399                            AND t1.forum_id = t2.forum_id";
1400                    $result = $db->sql_query($sql);
1401
1402                    $topic_id_ary = array();
1403                    while ($row = $db->sql_fetchrow($result))
1404                    {
1405                        $topic_id_ary[] = $row['topic_id'];
1406                    }
1407                    $db->sql_freeresult($result);
1408
1409                    if (!count($topic_id_ary))
1410                    {
1411                        return;
1412                    }
1413
1414                    $sql = 'DELETE FROM ' . TOPICS_TABLE . '
1415                        WHERE ' . $db->sql_in_set('topic_id', $topic_id_ary);
1416                    $db->sql_query($sql);
1417
1418                break;
1419            }
1420
1421            $db->sql_transaction('commit');
1422            break;
1423
1424        case 'topic_visibility':
1425
1426            $db->sql_transaction('begin');
1427
1428            $sql = 'SELECT t.topic_id, p.post_visibility
1429                FROM ' . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
1430                $where_sql_and p.topic_id = t.topic_id
1431                    AND p.post_visibility = " . ITEM_APPROVED;
1432            $result = $db->sql_query($sql);
1433
1434            $topics_approved = array();
1435            while ($row = $db->sql_fetchrow($result))
1436            {
1437                $topics_approved[] = (int) $row['topic_id'];
1438            }
1439            $db->sql_freeresult($result);
1440
1441            $sql = 'SELECT t.topic_id, p.post_visibility
1442                FROM ' . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
1443                $where_sql_and " . $db->sql_in_set('t.topic_id', $topics_approved, true, true) . '
1444                    AND p.topic_id = t.topic_id
1445                    AND p.post_visibility = ' . ITEM_DELETED;
1446            $result = $db->sql_query($sql);
1447
1448            $topics_softdeleted = array();
1449            while ($row = $db->sql_fetchrow($result))
1450            {
1451                $topics_softdeleted[] = (int) $row['topic_id'];
1452            }
1453            $db->sql_freeresult($result);
1454
1455            $topics_softdeleted = array_diff($topics_softdeleted, $topics_approved);
1456            $topics_not_unapproved = array_merge($topics_softdeleted, $topics_approved);
1457
1458            $update_ary = array(
1459                ITEM_UNAPPROVED    => (!empty($topics_not_unapproved)) ? $where_sql_and . ' ' . $db->sql_in_set('topic_id', $topics_not_unapproved, true) : '',
1460                ITEM_APPROVED    => (!empty($topics_approved)) ? ' WHERE ' . $db->sql_in_set('topic_id', $topics_approved) : '',
1461                ITEM_DELETED    => (!empty($topics_softdeleted)) ? ' WHERE ' . $db->sql_in_set('topic_id', $topics_softdeleted) : '',
1462            );
1463
1464            foreach ($update_ary as $visibility => $sql_where)
1465            {
1466                if ($sql_where)
1467                {
1468                    $sql = 'UPDATE ' . TOPICS_TABLE . '
1469                        SET topic_visibility = ' . $visibility . '
1470                        ' . $sql_where;
1471                    $db->sql_query($sql);
1472                }
1473            }
1474
1475            $db->sql_transaction('commit');
1476            break;
1477
1478        case 'post_reported':
1479            $post_ids = $post_reported = array();
1480
1481            $db->sql_transaction('begin');
1482
1483            $sql = 'SELECT p.post_id, p.post_reported
1484                FROM ' . POSTS_TABLE . " p
1485                $where_sql
1486                GROUP BY p.post_id, p.post_reported";
1487            $result = $db->sql_query($sql);
1488
1489            while ($row = $db->sql_fetchrow($result))
1490            {
1491                $post_ids[$row['post_id']] = $row['post_id'];
1492                if ($row['post_reported'])
1493                {
1494                    $post_reported[$row['post_id']] = 1;
1495                }
1496            }
1497            $db->sql_freeresult($result);
1498
1499            $sql = 'SELECT DISTINCT(post_id)
1500                FROM ' . REPORTS_TABLE . '
1501                WHERE ' . $db->sql_in_set('post_id', $post_ids) . '
1502                    AND report_closed = 0';
1503            $result = $db->sql_query($sql);
1504
1505            $post_ids = array();
1506            while ($row = $db->sql_fetchrow($result))
1507            {
1508                if (!isset($post_reported[$row['post_id']]))
1509                {
1510                    $post_ids[] = $row['post_id'];
1511                }
1512                else
1513                {
1514                    unset($post_reported[$row['post_id']]);
1515                }
1516            }
1517            $db->sql_freeresult($result);
1518
1519            // $post_reported should be empty by now, if it's not it contains
1520            // posts that are falsely flagged as reported
1521            foreach (array_keys($post_reported) as $post_id)
1522            {
1523                $post_ids[] = $post_id;
1524            }
1525
1526            if (count($post_ids))
1527            {
1528                $sql = 'UPDATE ' . POSTS_TABLE . '
1529                    SET post_reported = 1 - post_reported
1530                    WHERE ' . $db->sql_in_set('post_id', $post_ids);
1531                $db->sql_query($sql);
1532            }
1533
1534            $db->sql_transaction('commit');
1535            break;
1536
1537        case 'topic_reported':
1538            if ($sync_extra)
1539            {
1540                sync('post_reported', $where_type, $where_ids);
1541            }
1542
1543            $topic_ids = $topic_reported = array();
1544
1545            $db->sql_transaction('begin');
1546
1547            $sql = 'SELECT DISTINCT(t.topic_id)
1548                FROM ' . POSTS_TABLE . " t
1549                $where_sql_and t.post_reported = 1";
1550            $result = $db->sql_query($sql);
1551
1552            while ($row = $db->sql_fetchrow($result))
1553            {
1554                $topic_reported[$row['topic_id']] = 1;
1555            }
1556            $db->sql_freeresult($result);
1557
1558            $sql = 'SELECT t.topic_id, t.topic_reported
1559                FROM ' . TOPICS_TABLE . " t
1560                $where_sql";
1561            $result = $db->sql_query($sql);
1562
1563            while ($row = $db->sql_fetchrow($result))
1564            {
1565                if ($row['topic_reported'] ^ isset($topic_reported[$row['topic_id']]))
1566                {
1567                    $topic_ids[] = $row['topic_id'];
1568                }
1569            }
1570            $db->sql_freeresult($result);
1571
1572            if (count($topic_ids))
1573            {
1574                $sql = 'UPDATE ' . TOPICS_TABLE . '
1575                    SET topic_reported = 1 - topic_reported
1576                    WHERE ' . $db->sql_in_set('topic_id', $topic_ids);
1577                $db->sql_query($sql);
1578            }
1579
1580            $db->sql_transaction('commit');
1581            break;
1582
1583        case 'post_attachment':
1584            $post_ids = $post_attachment = array();
1585
1586            $db->sql_transaction('begin');
1587
1588            $sql = 'SELECT p.post_id, p.post_attachment
1589                FROM ' . POSTS_TABLE . " p
1590                $where_sql
1591                GROUP BY p.post_id, p.post_attachment";
1592            $result = $db->sql_query($sql);
1593
1594            while ($row = $db->sql_fetchrow($result))
1595            {
1596                $post_ids[$row['post_id']] = $row['post_id'];
1597                if ($row['post_attachment'])
1598                {
1599                    $post_attachment[$row['post_id']] = 1;
1600                }
1601            }
1602            $db->sql_freeresult($result);
1603
1604            $sql = 'SELECT DISTINCT(post_msg_id)
1605                FROM ' . ATTACHMENTS_TABLE . '
1606                WHERE ' . $db->sql_in_set('post_msg_id', $post_ids) . '
1607                    AND in_message = 0';
1608            $result = $db->sql_query($sql);
1609
1610            $post_ids = array();
1611            while ($row = $db->sql_fetchrow($result))
1612            {
1613                if (!isset($post_attachment[$row['post_msg_id']]))
1614                {
1615                    $post_ids[] = $row['post_msg_id'];
1616                }
1617                else
1618                {
1619                    unset($post_attachment[$row['post_msg_id']]);
1620                }
1621            }
1622            $db->sql_freeresult($result);
1623
1624            // $post_attachment should be empty by now, if it's not it contains
1625            // posts that are falsely flagged as having attachments
1626            foreach (array_keys($post_attachment) as $post_id)
1627            {
1628                $post_ids[] = $post_id;
1629            }
1630
1631            if (count($post_ids))
1632            {
1633                $sql = 'UPDATE ' . POSTS_TABLE . '
1634                    SET post_attachment = 1 - post_attachment
1635                    WHERE ' . $db->sql_in_set('post_id', $post_ids);
1636                $db->sql_query($sql);
1637            }
1638
1639            $db->sql_transaction('commit');
1640            break;
1641
1642        case 'topic_attachment':
1643            if ($sync_extra)
1644            {
1645                sync('post_attachment', $where_type, $where_ids);
1646            }
1647
1648            $topic_ids = $topic_attachment = array();
1649
1650            $db->sql_transaction('begin');
1651
1652            $sql = 'SELECT DISTINCT(t.topic_id)
1653                FROM ' . POSTS_TABLE . " t
1654                $where_sql_and t.post_attachment = 1";
1655            $result = $db->sql_query($sql);
1656
1657            while ($row = $db->sql_fetchrow($result))
1658            {
1659                $topic_attachment[$row['topic_id']] = 1;
1660            }
1661            $db->sql_freeresult($result);
1662
1663            $sql = 'SELECT t.topic_id, t.topic_attachment
1664                FROM ' . TOPICS_TABLE . " t
1665                $where_sql";
1666            $result = $db->sql_query($sql);
1667
1668            while ($row = $db->sql_fetchrow($result))
1669            {
1670                if ($row['topic_attachment'] ^ isset($topic_attachment[$row['topic_id']]))
1671                {
1672                    $topic_ids[] = $row['topic_id'];
1673                }
1674            }
1675            $db->sql_freeresult($result);
1676
1677            if (count($topic_ids))
1678            {
1679                $sql = 'UPDATE ' . TOPICS_TABLE . '
1680                    SET topic_attachment = 1 - topic_attachment
1681                    WHERE ' . $db->sql_in_set('topic_id', $topic_ids);
1682                $db->sql_query($sql);
1683            }
1684
1685            $db->sql_transaction('commit');
1686
1687            break;
1688
1689        case 'forum':
1690
1691            $db->sql_transaction('begin');
1692
1693            // 1: Get the list of all forums
1694            $sql = 'SELECT f.*
1695                FROM ' . FORUMS_TABLE . " f
1696                $where_sql";
1697            $result = $db->sql_query($sql);
1698
1699            $forum_data = $forum_ids = $post_ids = $post_info = array();
1700            while ($row = $db->sql_fetchrow($result))
1701            {
1702                if ($row['forum_type'] == FORUM_LINK)
1703                {
1704                    continue;
1705                }
1706
1707                $forum_id = (int) $row['forum_id'];
1708                $forum_ids[$forum_id] = $forum_id;
1709
1710                $forum_data[$forum_id] = $row;
1711                if ($sync_extra)
1712                {
1713                    $forum_data[$forum_id]['posts_approved'] = 0;
1714                    $forum_data[$forum_id]['posts_unapproved'] = 0;
1715                    $forum_data[$forum_id]['posts_softdeleted'] = 0;
1716                    $forum_data[$forum_id]['topics_approved'] = 0;
1717                    $forum_data[$forum_id]['topics_unapproved'] = 0;
1718                    $forum_data[$forum_id]['topics_softdeleted'] = 0;
1719                }
1720                $forum_data[$forum_id]['last_post_id'] = 0;
1721                $forum_data[$forum_id]['last_post_subject'] = '';
1722                $forum_data[$forum_id]['last_post_time'] = 0;
1723                $forum_data[$forum_id]['last_poster_id'] = 0;
1724                $forum_data[$forum_id]['last_poster_name'] = '';
1725                $forum_data[$forum_id]['last_poster_colour'] = '';
1726            }
1727            $db->sql_freeresult($result);
1728
1729            if (!count($forum_ids))
1730            {
1731                break;
1732            }
1733
1734            $forum_ids = array_values($forum_ids);
1735
1736            // 2: Get topic counts for each forum (optional)
1737            if ($sync_extra)
1738            {
1739                $sql = 'SELECT forum_id, topic_visibility, COUNT(topic_id) AS total_topics
1740                    FROM ' . TOPICS_TABLE . '
1741                    WHERE ' . $db->sql_in_set('forum_id', $forum_ids) . '
1742                    GROUP BY forum_id, topic_visibility';
1743                $result = $db->sql_query($sql);
1744
1745                while ($row = $db->sql_fetchrow($result))
1746                {
1747                    $forum_id = (int) $row['forum_id'];
1748
1749                    if ($row['topic_visibility'] == ITEM_APPROVED)
1750                    {
1751                        $forum_data[$forum_id]['topics_approved'] = $row['total_topics'];
1752                    }
1753                    else if ($row['topic_visibility'] == ITEM_UNAPPROVED || $row['topic_visibility'] == ITEM_REAPPROVE)
1754                    {
1755                        $forum_data[$forum_id]['topics_unapproved'] = $row['total_topics'];
1756                    }
1757                    else if ($row['topic_visibility'] == ITEM_DELETED)
1758                    {
1759                        $forum_data[$forum_id]['topics_softdeleted'] = $row['total_topics'];
1760                    }
1761                }
1762                $db->sql_freeresult($result);
1763            }
1764
1765            // 3: Get post count for each forum (optional)
1766            if ($sync_extra)
1767            {
1768                if (count($forum_ids) == 1)
1769                {
1770                    $sql = 'SELECT SUM(t.topic_posts_approved) AS forum_posts_approved, SUM(t.topic_posts_unapproved) AS forum_posts_unapproved, SUM(t.topic_posts_softdeleted) AS forum_posts_softdeleted
1771                        FROM ' . TOPICS_TABLE . ' t
1772                        WHERE ' . $db->sql_in_set('t.forum_id', $forum_ids) . '
1773                            AND t.topic_status <> ' . ITEM_MOVED;
1774                }
1775                else
1776                {
1777                    $sql = 'SELECT t.forum_id, SUM(t.topic_posts_approved) AS forum_posts_approved, SUM(t.topic_posts_unapproved) AS forum_posts_unapproved, SUM(t.topic_posts_softdeleted) AS forum_posts_softdeleted
1778                        FROM ' . TOPICS_TABLE . ' t
1779                        WHERE ' . $db->sql_in_set('t.forum_id', $forum_ids) . '
1780                            AND t.topic_status <> ' . ITEM_MOVED . '
1781                        GROUP BY t.forum_id';
1782                }
1783
1784                $result = $db->sql_query($sql);
1785
1786                while ($row = $db->sql_fetchrow($result))
1787                {
1788                    $forum_id = (count($forum_ids) == 1) ? (int) $forum_ids[0] : (int) $row['forum_id'];
1789
1790                    $forum_data[$forum_id]['posts_approved'] = (int) $row['forum_posts_approved'];
1791                    $forum_data[$forum_id]['posts_unapproved'] = (int) $row['forum_posts_unapproved'];
1792                    $forum_data[$forum_id]['posts_softdeleted'] = (int) $row['forum_posts_softdeleted'];
1793                }
1794                $db->sql_freeresult($result);
1795            }
1796
1797            // 4: Get last_post_id for each forum
1798            if (count($forum_ids) == 1)
1799            {
1800                $sql = 'SELECT MAX(t.topic_last_post_id) as last_post_id
1801                    FROM ' . TOPICS_TABLE . ' t
1802                    WHERE ' . $db->sql_in_set('t.forum_id', $forum_ids) . '
1803                        AND t.topic_visibility = ' . ITEM_APPROVED;
1804            }
1805            else
1806            {
1807                $sql = 'SELECT t.forum_id, MAX(t.topic_last_post_id) as last_post_id
1808                    FROM ' . TOPICS_TABLE . ' t
1809                    WHERE ' . $db->sql_in_set('t.forum_id', $forum_ids) . '
1810                        AND t.topic_visibility = ' . ITEM_APPROVED . '
1811                    GROUP BY t.forum_id';
1812            }
1813
1814            $result = $db->sql_query($sql);
1815
1816            while ($row = $db->sql_fetchrow($result))
1817            {
1818                $forum_id = (count($forum_ids) == 1) ? (int) $forum_ids[0] : (int) $row['forum_id'];
1819
1820                $forum_data[$forum_id]['last_post_id'] = (int) $row['last_post_id'];
1821
1822                $post_ids[] = $row['last_post_id'];
1823            }
1824            $db->sql_freeresult($result);
1825
1826            // 5: Retrieve last_post infos
1827            if (count($post_ids))
1828            {
1829                $sql_ary = array(
1830                    'SELECT'    => 'p.post_id, p.poster_id, p.post_subject, p.post_time, p.post_username, u.username, u.user_colour',
1831                    'FROM'        => array(
1832                        POSTS_TABLE    => 'p',
1833                        USERS_TABLE => 'u',
1834                    ),
1835                    'WHERE'        => $db->sql_in_set('p.post_id', $post_ids) . '
1836                        AND p.poster_id = u.user_id',
1837                );
1838
1839                /**
1840                * Event to modify the SQL array to get the post and user data from all forums' last posts
1841                *
1842                * @event core.sync_forum_last_post_info_sql
1843                * @var    array    sql_ary        SQL array with some post and user data from the last posts list
1844                * @since 3.3.5-RC1
1845                */
1846                $vars = ['sql_ary'];
1847                extract($phpbb_dispatcher->trigger_event('core.sync_forum_last_post_info_sql', compact($vars)));
1848                $result = $db->sql_query($db->sql_build_query('SELECT', $sql_ary));
1849
1850                while ($row = $db->sql_fetchrow($result))
1851                {
1852                    $post_info[$row['post_id']] = $row;
1853                }
1854                $db->sql_freeresult($result);
1855
1856                foreach ($forum_data as $forum_id => $data)
1857                {
1858                    if ($data['last_post_id'])
1859                    {
1860                        if (isset($post_info[$data['last_post_id']]))
1861                        {
1862                            $forum_data[$forum_id]['last_post_subject'] = $post_info[$data['last_post_id']]['post_subject'];
1863                            $forum_data[$forum_id]['last_post_time'] = $post_info[$data['last_post_id']]['post_time'];
1864                            $forum_data[$forum_id]['last_poster_id'] = $post_info[$data['last_post_id']]['poster_id'];
1865                            $forum_data[$forum_id]['last_poster_name'] = ($post_info[$data['last_post_id']]['poster_id'] != ANONYMOUS) ? $post_info[$data['last_post_id']]['username'] : $post_info[$data['last_post_id']]['post_username'];
1866                            $forum_data[$forum_id]['last_poster_colour'] = $post_info[$data['last_post_id']]['user_colour'];
1867                        }
1868                        else
1869                        {
1870                            // For some reason we did not find the post in the db
1871                            $forum_data[$forum_id]['last_post_id'] = 0;
1872                            $forum_data[$forum_id]['last_post_subject'] = '';
1873                            $forum_data[$forum_id]['last_post_time'] = 0;
1874                            $forum_data[$forum_id]['last_poster_id'] = 0;
1875                            $forum_data[$forum_id]['last_poster_name'] = '';
1876                            $forum_data[$forum_id]['last_poster_colour'] = '';
1877                        }
1878                    }
1879                }
1880            }
1881
1882            // 6: Now do that thing
1883            $fieldnames = array('last_post_id', 'last_post_subject', 'last_post_time', 'last_poster_id', 'last_poster_name', 'last_poster_colour');
1884
1885            if ($sync_extra)
1886            {
1887                array_push($fieldnames, 'posts_approved', 'posts_unapproved', 'posts_softdeleted', 'topics_approved', 'topics_unapproved', 'topics_softdeleted');
1888            }
1889
1890            /**
1891            * Event to modify the SQL array to get the post and user data from all forums' last posts
1892            *
1893            * @event core.sync_modify_forum_data
1894            * @var    array    forum_data        Array with data to update for all forum ids
1895            * @var    array    post_info        Array with some post and user data from the last posts list
1896            * @var    array    fieldnames        Array with the partial column names that are being updated
1897            * @since 3.3.5-RC1
1898            */
1899            $vars = [
1900                'forum_data',
1901                'post_info',
1902                'fieldnames',
1903            ];
1904            extract($phpbb_dispatcher->trigger_event('core.sync_modify_forum_data', compact($vars)));
1905            unset($post_info);
1906
1907            foreach ($forum_data as $forum_id => $row)
1908            {
1909                $sql_ary = array();
1910
1911                foreach ($fieldnames as $fieldname)
1912                {
1913                    if ($row['forum_' . $fieldname] != $row[$fieldname])
1914                    {
1915                        if (preg_match('#(name|colour|subject)$#', $fieldname))
1916                        {
1917                            $sql_ary['forum_' . $fieldname] = (string) $row[$fieldname];
1918                        }
1919                        else
1920                        {
1921                            $sql_ary['forum_' . $fieldname] = (int) $row[$fieldname];
1922                        }
1923                    }
1924                }
1925
1926                if (count($sql_ary))
1927                {
1928                    $sql = 'UPDATE ' . FORUMS_TABLE . '
1929                        SET ' . $db->sql_build_array('UPDATE', $sql_ary) . '
1930                        WHERE forum_id = ' . $forum_id;
1931                    $db->sql_query($sql);
1932                }
1933            }
1934
1935            $db->sql_transaction('commit');
1936            break;
1937
1938        case 'topic':
1939            $topic_data = $post_ids = $resync_forums = $delete_topics = $delete_posts = $moved_topics = array();
1940
1941            $db->sql_transaction('begin');
1942
1943            $sql = 'SELECT t.topic_id, t.forum_id, t.topic_moved_id, t.topic_visibility, ' . (($sync_extra) ? 't.topic_attachment, t.topic_reported, ' : '') . 't.topic_poster, t.topic_time, t.topic_posts_approved, t.topic_posts_unapproved, t.topic_posts_softdeleted, t.topic_first_post_id, t.topic_first_poster_name, t.topic_first_poster_colour, t.topic_last_post_id, t.topic_last_post_subject, t.topic_last_poster_id, t.topic_last_poster_name, t.topic_last_poster_colour, t.topic_last_post_time
1944                FROM ' . TOPICS_TABLE . " t
1945                $where_sql";
1946            $result = $db->sql_query($sql);
1947
1948            while ($row = $db->sql_fetchrow($result))
1949            {
1950                if ($row['topic_moved_id'])
1951                {
1952                    $moved_topics[] = $row['topic_id'];
1953                    continue;
1954                }
1955
1956                $topic_id = (int) $row['topic_id'];
1957                $topic_data[$topic_id] = $row;
1958                $topic_data[$topic_id]['visibility'] = ITEM_UNAPPROVED;
1959                $topic_data[$topic_id]['posts_approved'] = 0;
1960                $topic_data[$topic_id]['posts_unapproved'] = 0;
1961                $topic_data[$topic_id]['posts_softdeleted'] = 0;
1962                $topic_data[$topic_id]['first_post_id'] = 0;
1963                $topic_data[$topic_id]['last_post_id'] = 0;
1964                unset($topic_data[$topic_id]['topic_id']);
1965
1966                // This array holds all topic_ids
1967                $delete_topics[$topic_id] = '';
1968
1969                if ($sync_extra)
1970                {
1971                    $topic_data[$topic_id]['reported'] = 0;
1972                    $topic_data[$topic_id]['attachment'] = 0;
1973                }
1974            }
1975            $db->sql_freeresult($result);
1976
1977            // Use "t" as table alias because of the $where_sql clause
1978            // NOTE: 't.post_visibility' in the GROUP BY is causing a major slowdown.
1979            $sql = 'SELECT t.topic_id, t.post_visibility, COUNT(t.post_id) AS total_posts, MIN(t.post_id) AS first_post_id, MAX(t.post_id) AS last_post_id
1980                FROM ' . POSTS_TABLE . " t
1981                $where_sql
1982                GROUP BY t.topic_id, t.post_visibility";
1983            $result = $db->sql_query($sql);
1984
1985            while ($row = $db->sql_fetchrow($result))
1986            {
1987                $topic_id = (int) $row['topic_id'];
1988
1989                $row['first_post_id'] = (int) $row['first_post_id'];
1990                $row['last_post_id'] = (int) $row['last_post_id'];
1991
1992                if (!isset($topic_data[$topic_id]))
1993                {
1994                    // Hey, these posts come from a topic that does not exist
1995                    $delete_posts[$topic_id] = '';
1996                }
1997                else
1998                {
1999                    // Unset the corresponding entry in $delete_topics
2000                    // When we'll be done, only topics with no posts will remain
2001                    unset($delete_topics[$topic_id]);
2002
2003                    if ($row['post_visibility'] == ITEM_APPROVED)
2004                    {
2005                        $topic_data[$topic_id]['posts_approved'] = $row['total_posts'];
2006                    }
2007                    else if ($row['post_visibility'] == ITEM_UNAPPROVED || $row['post_visibility'] == ITEM_REAPPROVE)
2008                    {
2009                        $topic_data[$topic_id]['posts_unapproved'] = $row['total_posts'];
2010                    }
2011                    else if ($row['post_visibility'] == ITEM_DELETED)
2012                    {
2013                        $topic_data[$topic_id]['posts_softdeleted'] = $row['total_posts'];
2014                    }
2015
2016                    if ($row['post_visibility'] == ITEM_APPROVED)
2017                    {
2018                        $topic_data[$topic_id]['visibility'] = ITEM_APPROVED;
2019                        $topic_data[$topic_id]['first_post_id'] = $row['first_post_id'];
2020                        $topic_data[$topic_id]['last_post_id'] = $row['last_post_id'];
2021                    }
2022                    else if ($topic_data[$topic_id]['visibility'] != ITEM_APPROVED)
2023                    {
2024                        // If there is no approved post, we take the min/max of the other visibilities
2025                        // for the last and first post info, because it is only visible to moderators anyway
2026                        $topic_data[$topic_id]['first_post_id'] = (!empty($topic_data[$topic_id]['first_post_id'])) ? min($topic_data[$topic_id]['first_post_id'], $row['first_post_id']) : $row['first_post_id'];
2027                        $topic_data[$topic_id]['last_post_id'] = max($topic_data[$topic_id]['last_post_id'], $row['last_post_id']);
2028
2029                        if ($topic_data[$topic_id]['visibility'] == ITEM_UNAPPROVED || $topic_data[$topic_id]['visibility'] == ITEM_REAPPROVE)
2030                        {
2031                            // Soft delete status is stronger than unapproved.
2032                            $topic_data[$topic_id]['visibility'] = $row['post_visibility'];
2033                        }
2034                    }
2035                }
2036            }
2037            $db->sql_freeresult($result);
2038
2039            foreach ($topic_data as $topic_id => $row)
2040            {
2041                $post_ids[] = $row['first_post_id'];
2042                if ($row['first_post_id'] != $row['last_post_id'])
2043                {
2044                    $post_ids[] = $row['last_post_id'];
2045                }
2046            }
2047
2048            // Now we delete empty topics and orphan posts
2049            if (count($delete_posts))
2050            {
2051                delete_posts('topic_id', array_keys($delete_posts), false);
2052                unset($delete_posts);
2053            }
2054
2055            if (!count($topic_data))
2056            {
2057                // If we get there, topic ids were invalid or topics did not contain any posts
2058                delete_topics($where_type, $where_ids, true);
2059                return;
2060            }
2061
2062            if (count($delete_topics))
2063            {
2064                $delete_topic_ids = array();
2065                foreach (array_keys($delete_topics) as $topic_id)
2066                {
2067                    unset($topic_data[$topic_id]);
2068                    $delete_topic_ids[] = $topic_id;
2069                }
2070
2071                delete_topics('topic_id', $delete_topic_ids, false);
2072                unset($delete_topics, $delete_topic_ids);
2073            }
2074
2075            $sql_ary = array(
2076                'SELECT'    => 'p.post_id, p.topic_id, p.post_visibility, p.poster_id, p.post_subject, p.post_username, p.post_time, u.username, u.user_colour',
2077                'FROM'        => array(
2078                    POSTS_TABLE    => 'p',
2079                    USERS_TABLE => 'u',
2080                ),
2081                'WHERE'        => $db->sql_in_set('p.post_id', $post_ids) . '
2082                    AND u.user_id = p.poster_id',
2083            );
2084
2085            $custom_fieldnames = [];
2086            /**
2087            * Event to modify the SQL array to get the post and user data from all topics' last posts
2088            *
2089            * @event core.sync_topic_last_post_info_sql
2090            * @var    array    sql_ary                    SQL array with some post and user data from the last posts list
2091            * @var    array    custom_fieldnames        Empty array for custom fieldnames to update the topics_table with
2092            * @since 3.3.5-RC1
2093            */
2094            $vars = [
2095                'sql_ary',
2096                'custom_fieldnames',
2097            ];
2098            extract($phpbb_dispatcher->trigger_event('core.sync_topic_last_post_info_sql', compact($vars)));
2099            $result = $db->sql_query($db->sql_build_query('SELECT', $sql_ary));
2100
2101            while ($row = $db->sql_fetchrow($result))
2102            {
2103                $topic_id = intval($row['topic_id']);
2104
2105                if ($row['post_id'] == $topic_data[$topic_id]['first_post_id'])
2106                {
2107                    $topic_data[$topic_id]['time'] = $row['post_time'];
2108                    $topic_data[$topic_id]['poster'] = $row['poster_id'];
2109                    $topic_data[$topic_id]['first_poster_name'] = ($row['poster_id'] == ANONYMOUS) ? $row['post_username'] : $row['username'];
2110                    $topic_data[$topic_id]['first_poster_colour'] = $row['user_colour'];
2111                }
2112
2113                if ($row['post_id'] == $topic_data[$topic_id]['last_post_id'])
2114                {
2115                    $topic_data[$topic_id]['last_poster_id'] = $row['poster_id'];
2116                    $topic_data[$topic_id]['last_post_subject'] = $row['post_subject'];
2117                    $topic_data[$topic_id]['last_post_time'] = $row['post_time'];
2118                    $topic_data[$topic_id]['last_poster_name'] = ($row['poster_id'] == ANONYMOUS) ? $row['post_username'] : $row['username'];
2119                    $topic_data[$topic_id]['last_poster_colour'] = $row['user_colour'];
2120                }
2121
2122                /**
2123                * Event to modify the topic_data when syncing topics
2124                *
2125                * @event core.sync_modify_topic_data
2126                * @var    array    topic_data        Array with the topics' data we are syncing
2127                * @var    array    row                Array with some of the current user and post data
2128                * @var    int        topic_id        The current topic_id of $row
2129                * @since 3.3.5-RC1
2130                */
2131                $vars = [
2132                    'topic_data',
2133                    'row',
2134                    'topic_id',
2135                ];
2136                extract($phpbb_dispatcher->trigger_event('core.sync_modify_topic_data', compact($vars)));
2137            }
2138            $db->sql_freeresult($result);
2139
2140            // Make sure shadow topics do link to existing topics
2141            if (count($moved_topics))
2142            {
2143                $delete_topics = array();
2144
2145                $sql = 'SELECT t1.topic_id, t1.topic_moved_id
2146                    FROM ' . TOPICS_TABLE . ' t1
2147                    LEFT JOIN ' . TOPICS_TABLE . ' t2 ON (t2.topic_id = t1.topic_moved_id)
2148                    WHERE ' . $db->sql_in_set('t1.topic_id', $moved_topics) . '
2149                        AND t2.topic_id IS NULL';
2150                $result = $db->sql_query($sql);
2151
2152                while ($row = $db->sql_fetchrow($result))
2153                {
2154                    $delete_topics[] = $row['topic_id'];
2155                }
2156                $db->sql_freeresult($result);
2157
2158                if (count($delete_topics))
2159                {
2160                    delete_topics('topic_id', $delete_topics, false);
2161                }
2162                unset($delete_topics);
2163
2164                // Make sure shadow topics having no last post data being updated (this only rarely happens...)
2165                $sql = 'SELECT topic_id, topic_moved_id, topic_last_post_id, topic_first_post_id
2166                    FROM ' . TOPICS_TABLE . '
2167                    WHERE ' . $db->sql_in_set('topic_id', $moved_topics) . '
2168                        AND topic_last_post_time = 0';
2169                $result = $db->sql_query($sql);
2170
2171                $shadow_topic_data = $post_ids = array();
2172                while ($row = $db->sql_fetchrow($result))
2173                {
2174                    $shadow_topic_data[$row['topic_moved_id']] = $row;
2175                    $post_ids[] = $row['topic_last_post_id'];
2176                    $post_ids[] = $row['topic_first_post_id'];
2177                }
2178                $db->sql_freeresult($result);
2179
2180                $sync_shadow_topics = array();
2181                if (count($post_ids))
2182                {
2183                    $sql = 'SELECT p.post_id, p.topic_id, p.post_visibility, p.poster_id, p.post_subject, p.post_username, p.post_time, u.username, u.user_colour
2184                        FROM ' . POSTS_TABLE . ' p, ' . USERS_TABLE . ' u
2185                        WHERE ' . $db->sql_in_set('p.post_id', $post_ids) . '
2186                            AND u.user_id = p.poster_id';
2187                    $result = $db->sql_query($sql);
2188
2189                    while ($row = $db->sql_fetchrow($result))
2190                    {
2191                        $topic_id = (int) $row['topic_id'];
2192
2193                        // Ok, there should be a shadow topic. If there isn't, then there's something wrong with the db.
2194                        // However, there's not much we can do about it.
2195                        if (!empty($shadow_topic_data[$topic_id]))
2196                        {
2197                            if ($row['post_id'] == $shadow_topic_data[$topic_id]['topic_first_post_id'])
2198                            {
2199                                $orig_topic_id = $shadow_topic_data[$topic_id]['topic_id'];
2200
2201                                if (!isset($sync_shadow_topics[$orig_topic_id]))
2202                                {
2203                                    $sync_shadow_topics[$orig_topic_id] = array();
2204                                }
2205
2206                                $sync_shadow_topics[$orig_topic_id]['topic_time'] = $row['post_time'];
2207                                $sync_shadow_topics[$orig_topic_id]['topic_poster'] = $row['poster_id'];
2208                                $sync_shadow_topics[$orig_topic_id]['topic_first_poster_name'] = ($row['poster_id'] == ANONYMOUS) ? $row['post_username'] : $row['username'];
2209                                $sync_shadow_topics[$orig_topic_id]['topic_first_poster_colour'] = $row['user_colour'];
2210                            }
2211
2212                            if ($row['post_id'] == $shadow_topic_data[$topic_id]['topic_last_post_id'])
2213                            {
2214                                $orig_topic_id = $shadow_topic_data[$topic_id]['topic_id'];
2215
2216                                if (!isset($sync_shadow_topics[$orig_topic_id]))
2217                                {
2218                                    $sync_shadow_topics[$orig_topic_id] = array();
2219                                }
2220
2221                                $sync_shadow_topics[$orig_topic_id]['topic_last_poster_id'] = $row['poster_id'];
2222                                $sync_shadow_topics[$orig_topic_id]['topic_last_post_subject'] = $row['post_subject'];
2223                                $sync_shadow_topics[$orig_topic_id]['topic_last_post_time'] = $row['post_time'];
2224                                $sync_shadow_topics[$orig_topic_id]['topic_last_poster_name'] = ($row['poster_id'] == ANONYMOUS) ? $row['post_username'] : $row['username'];
2225                                $sync_shadow_topics[$orig_topic_id]['topic_last_poster_colour'] = $row['user_colour'];
2226                            }
2227                        }
2228                    }
2229                    $db->sql_freeresult($result);
2230
2231                    $shadow_topic_data = array();
2232
2233                    // Update the information we collected
2234                    if (count($sync_shadow_topics))
2235                    {
2236                        foreach ($sync_shadow_topics as $sync_topic_id => $sql_ary)
2237                        {
2238                            $sql = 'UPDATE ' . TOPICS_TABLE . '
2239                                SET ' . $db->sql_build_array('UPDATE', $sql_ary) . '
2240                                WHERE topic_id = ' . $sync_topic_id;
2241                            $db->sql_query($sql);
2242                        }
2243                    }
2244                }
2245
2246                unset($sync_shadow_topics, $shadow_topic_data);
2247            }
2248
2249            // These are fields that will be synchronised
2250            $fieldnames = array('time', 'visibility', 'posts_approved', 'posts_unapproved', 'posts_softdeleted', 'poster', 'first_post_id', 'first_poster_name', 'first_poster_colour', 'last_post_id', 'last_post_subject', 'last_post_time', 'last_poster_id', 'last_poster_name', 'last_poster_colour');
2251
2252            // Add custom fieldnames
2253            $fieldnames = array_merge($fieldnames, $custom_fieldnames);
2254            unset($custom_fieldnames);
2255
2256            if ($sync_extra)
2257            {
2258                // This routine assumes that post_reported values are correct
2259                // if they are not, use sync('post_reported') first
2260                $sql = 'SELECT t.topic_id, p.post_id
2261                    FROM ' . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
2262                    $where_sql_and p.topic_id = t.topic_id
2263                        AND p.post_reported = 1
2264                    GROUP BY t.topic_id, p.post_id";
2265                $result = $db->sql_query($sql);
2266
2267                $fieldnames[] = 'reported';
2268                while ($row = $db->sql_fetchrow($result))
2269                {
2270                    $topic_data[intval($row['topic_id'])]['reported'] = 1;
2271                }
2272                $db->sql_freeresult($result);
2273
2274                // This routine assumes that post_attachment values are correct
2275                // if they are not, use sync('post_attachment') first
2276                $sql = 'SELECT t.topic_id, p.post_id
2277                    FROM ' . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
2278                    $where_sql_and p.topic_id = t.topic_id
2279                        AND p.post_attachment = 1
2280                    GROUP BY t.topic_id, p.post_id";
2281                $result = $db->sql_query($sql);
2282
2283                $fieldnames[] = 'attachment';
2284                while ($row = $db->sql_fetchrow($result))
2285                {
2286                    $topic_data[intval($row['topic_id'])]['attachment'] = 1;
2287                }
2288                $db->sql_freeresult($result);
2289            }
2290
2291            foreach ($topic_data as $topic_id => $row)
2292            {
2293                $sql_ary = array();
2294
2295                foreach ($fieldnames as $fieldname)
2296                {
2297                    if (isset($row[$fieldname]) && isset($row['topic_' . $fieldname]) && $row['topic_' . $fieldname] != $row[$fieldname])
2298                    {
2299                        $sql_ary['topic_' . $fieldname] = $row[$fieldname];
2300                    }
2301                }
2302
2303                if (count($sql_ary))
2304                {
2305                    $sql = 'UPDATE ' . TOPICS_TABLE . '
2306                        SET ' . $db->sql_build_array('UPDATE', $sql_ary) . '
2307                        WHERE topic_id = ' . $topic_id;
2308                    $db->sql_query($sql);
2309
2310                    $resync_forums[$row['forum_id']] = $row['forum_id'];
2311                }
2312            }
2313            unset($topic_data);
2314
2315            $db->sql_transaction('commit');
2316
2317            // if some topics have been resync'ed then resync parent forums
2318            // except when we're only syncing a range, we don't want to sync forums during
2319            // batch processing.
2320            if ($resync_parents && count($resync_forums) && $where_type != 'range')
2321            {
2322                sync('forum', 'forum_id', array_values($resync_forums), true, true);
2323            }
2324            break;
2325    }
2326
2327    return;
2328}
2329
2330/**
2331* Prune function
2332* @return array with topics and posts affected
2333*/
2334function prune($forum_id, $prune_mode, $prune_date, $prune_flags = 0, $auto_sync = true, $prune_limit = 0)
2335{
2336    global $db, $phpbb_dispatcher;
2337
2338    if (!is_array($forum_id))
2339    {
2340        $forum_id = array($forum_id);
2341    }
2342
2343    if (!count($forum_id))
2344    {
2345        return ['topics' => 0, 'posts' => 0];
2346    }
2347
2348    $sql_and = '';
2349
2350    if (!($prune_flags & FORUM_FLAG_PRUNE_ANNOUNCE))
2351    {
2352        $sql_and .= ' AND topic_type <> ' . POST_ANNOUNCE;
2353        $sql_and .= ' AND topic_type <> ' . POST_GLOBAL;
2354    }
2355
2356    if (!($prune_flags & FORUM_FLAG_PRUNE_STICKY))
2357    {
2358        $sql_and .= ' AND topic_type <> ' . POST_STICKY;
2359    }
2360
2361    if ($prune_mode == 'posted')
2362    {
2363        $sql_and .= " AND topic_last_post_time < $prune_date";
2364    }
2365
2366    if ($prune_mode == 'viewed')
2367    {
2368        $sql_and .= " AND topic_last_view_time < $prune_date";
2369    }
2370
2371    if ($prune_mode == 'shadow')
2372    {
2373        $sql_and .= ' AND topic_status = ' . ITEM_MOVED . " AND topic_last_post_time < $prune_date";
2374    }
2375
2376    /**
2377    * Use this event to modify the SQL that selects topics to be pruned
2378    *
2379    * @event core.prune_sql
2380    * @var string    forum_id        The forum id
2381    * @var string    prune_mode        The prune mode
2382    * @var string    prune_date        The prune date
2383    * @var int        prune_flags        The prune flags
2384    * @var bool        auto_sync        Whether or not to perform auto sync
2385    * @var string    sql_and            SQL text appended to where clause
2386    * @var int        prune_limit        The prune limit
2387    * @since 3.1.3-RC1
2388    * @changed 3.1.10-RC1            Added prune_limit
2389    */
2390    $vars = array(
2391        'forum_id',
2392        'prune_mode',
2393        'prune_date',
2394        'prune_flags',
2395        'auto_sync',
2396        'sql_and',
2397        'prune_limit',
2398    );
2399    extract($phpbb_dispatcher->trigger_event('core.prune_sql', compact($vars)));
2400
2401    $sql = 'SELECT topic_id
2402        FROM ' . TOPICS_TABLE . '
2403        WHERE ' . $db->sql_in_set('forum_id', $forum_id) . "
2404            AND poll_start = 0
2405            $sql_and";
2406    $result = $db->sql_query_limit($sql, $prune_limit);
2407
2408    $topic_list = array();
2409    while ($row = $db->sql_fetchrow($result))
2410    {
2411        $topic_list[] = $row['topic_id'];
2412    }
2413    $db->sql_freeresult($result);
2414
2415    if ($prune_flags & FORUM_FLAG_PRUNE_POLL)
2416    {
2417        $sql = 'SELECT topic_id
2418            FROM ' . TOPICS_TABLE . '
2419            WHERE ' . $db->sql_in_set('forum_id', $forum_id) . "
2420                AND poll_start > 0
2421                AND poll_last_vote < $prune_date
2422                $sql_and";
2423        $result = $db->sql_query_limit($sql, $prune_limit);
2424
2425        while ($row = $db->sql_fetchrow($result))
2426        {
2427            $topic_list[] = $row['topic_id'];
2428        }
2429        $db->sql_freeresult($result);
2430
2431        $topic_list = array_unique($topic_list);
2432    }
2433
2434    /**
2435     * Perform additional actions before topic deletion via pruning
2436     *
2437     * @event core.prune_delete_before
2438     * @var int[]    topic_list        The IDs of the topics to be deleted
2439     * @since 3.2.2-RC1
2440     */
2441    $vars = array('topic_list');
2442    extract($phpbb_dispatcher->trigger_event('core.prune_delete_before', compact($vars)));
2443
2444    return delete_topics('topic_id', $topic_list, $auto_sync, false);
2445}
2446
2447/**
2448* Function auto_prune(), this function now relies on passed vars
2449*/
2450function auto_prune($forum_id, $prune_mode, $prune_flags, $prune_days, $prune_freq, $log_prune = true)
2451{
2452    global $db, $user, $phpbb_log;
2453
2454    $sql = 'SELECT forum_name
2455        FROM ' . FORUMS_TABLE . "
2456        WHERE forum_id = $forum_id";
2457    $result = $db->sql_query($sql, 3600);
2458    $row = $db->sql_fetchrow($result);
2459    $db->sql_freeresult($result);
2460
2461    if ($row)
2462    {
2463        $prune_date = time() - ($prune_days * 86400);
2464        $next_prune = time() + ($prune_freq * 86400);
2465
2466        $result = prune($forum_id, $prune_mode, $prune_date, $prune_flags, true, 300);
2467
2468        if ($result['topics'] == 0 && $result['posts'] == 0)
2469        {
2470            $column = $prune_mode === 'shadow' ? 'prune_shadow_next' : 'prune_next';
2471
2472            $sql = 'UPDATE ' . FORUMS_TABLE . "
2473                SET $column = $next_prune
2474                WHERE forum_id = $forum_id";
2475            $db->sql_query($sql);
2476        }
2477
2478        if ($log_prune)
2479        {
2480            $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_AUTO_PRUNE', false, [$row['forum_name']]);
2481        }
2482    }
2483
2484    return;
2485}
2486
2487/**
2488* Cache moderators. Called whenever permissions are changed
2489* via admin_permissions. Changes of usernames and group names
2490* must be carried through for the moderators table.
2491*
2492* @param \phpbb\db\driver\driver_interface $db Database connection
2493* @param \phpbb\db\tools\tools_interface $db_tools Database tools
2494* @param \phpbb\cache\driver\driver_interface $cache Cache driver
2495* @param \phpbb\auth\auth $auth Authentication object
2496* @return void
2497*/
2498function phpbb_cache_moderators($db, $db_tools, $cache, $auth)
2499{
2500    // Remove cached sql results
2501    $cache->destroy('sql', MODERATOR_CACHE_TABLE);
2502
2503    $db_tools->sql_truncate_table(MODERATOR_CACHE_TABLE);
2504
2505    // We add moderators who have forum moderator permissions without an explicit ACL_NEVER setting
2506    $sql_ary = array();
2507
2508    // Grab all users having moderative options...
2509    $hold_ary = $auth->acl_user_raw_data(false, 'm_%', false);
2510
2511    // Add users?
2512    if (!empty($hold_ary))
2513    {
2514        // At least one moderative option warrants a display
2515        $ug_id_ary = array_keys($hold_ary);
2516
2517        // Remove users who have group memberships with DENY moderator permissions
2518        $sql_ary_deny = array(
2519            'SELECT'    => 'a.forum_id, ug.user_id, g.group_id',
2520
2521            'FROM'        => array(
2522                ACL_OPTIONS_TABLE    => 'o',
2523                USER_GROUP_TABLE    => 'ug',
2524                GROUPS_TABLE        => 'g',
2525                ACL_GROUPS_TABLE    => 'a',
2526            ),
2527
2528            'LEFT_JOIN'    => array(
2529                array(
2530                    'FROM'    => array(ACL_ROLES_DATA_TABLE => 'r'),
2531                    'ON'    => 'a.auth_role_id = r.role_id',
2532                ),
2533            ),
2534
2535            'WHERE'        => '(o.auth_option_id = a.auth_option_id OR o.auth_option_id = r.auth_option_id)
2536                AND ((a.auth_setting = ' . ACL_NEVER . ' AND r.auth_setting IS NULL)
2537                    OR r.auth_setting = ' . ACL_NEVER . ')
2538                AND a.group_id = ug.group_id
2539                AND g.group_id = ug.group_id
2540                AND NOT (ug.group_leader = 1 AND g.group_skip_auth = 1)
2541                AND ' . $db->sql_in_set('ug.user_id', $ug_id_ary) . "
2542                AND ug.user_pending = 0
2543                AND o.auth_option " . $db->sql_like_expression('m_' . $db->get_any_char()),
2544        );
2545        $sql = $db->sql_build_query('SELECT', $sql_ary_deny);
2546        $result = $db->sql_query($sql);
2547
2548        while ($row = $db->sql_fetchrow($result))
2549        {
2550            if (isset($hold_ary[$row['user_id']][$row['forum_id']]))
2551            {
2552                unset($hold_ary[$row['user_id']][$row['forum_id']]);
2553            }
2554        }
2555        $db->sql_freeresult($result);
2556
2557        if (count($hold_ary))
2558        {
2559            // Get usernames...
2560            $sql = 'SELECT user_id, username
2561                FROM ' . USERS_TABLE . '
2562                WHERE ' . $db->sql_in_set('user_id', array_keys($hold_ary));
2563            $result = $db->sql_query($sql);
2564
2565            $usernames_ary = array();
2566            while ($row = $db->sql_fetchrow($result))
2567            {
2568                $usernames_ary[$row['user_id']] = $row['username'];
2569            }
2570            $db->sql_freeresult($result);
2571
2572            foreach ($hold_ary as $user_id => $forum_id_ary)
2573            {
2574                // Do not continue if user does not exist
2575                if (!isset($usernames_ary[$user_id]))
2576                {
2577                    continue;
2578                }
2579
2580                foreach ($forum_id_ary as $forum_id => $auth_ary)
2581                {
2582                    $sql_ary[] = array(
2583                        'forum_id'        => (int) $forum_id,
2584                        'user_id'        => (int) $user_id,
2585                        'username'        => (string) $usernames_ary[$user_id],
2586                        'group_id'        => 0,
2587                        'group_name'    => ''
2588                    );
2589                }
2590            }
2591        }
2592    }
2593
2594    // Now to the groups...
2595    $hold_ary = $auth->acl_group_raw_data(false, 'm_%', false);
2596
2597    if (!empty($hold_ary))
2598    {
2599        $ug_id_ary = array_keys($hold_ary);
2600
2601        // Make sure not hidden or special groups are involved...
2602        $sql = 'SELECT group_name, group_id, group_type
2603            FROM ' . GROUPS_TABLE . '
2604            WHERE ' . $db->sql_in_set('group_id', $ug_id_ary);
2605        $result = $db->sql_query($sql);
2606
2607        $groupnames_ary = array();
2608        while ($row = $db->sql_fetchrow($result))
2609        {
2610            if ($row['group_type'] == GROUP_HIDDEN || $row['group_type'] == GROUP_SPECIAL)
2611            {
2612                unset($hold_ary[$row['group_id']]);
2613            }
2614
2615            $groupnames_ary[$row['group_id']] = $row['group_name'];
2616        }
2617        $db->sql_freeresult($result);
2618
2619        foreach ($hold_ary as $group_id => $forum_id_ary)
2620        {
2621            // If there is no group, we do not assign it...
2622            if (!isset($groupnames_ary[$group_id]))
2623            {
2624                continue;
2625            }
2626
2627            foreach ($forum_id_ary as $forum_id => $auth_ary)
2628            {
2629                $flag = false;
2630                foreach ($auth_ary as $auth_option => $setting)
2631                {
2632                    // Make sure at least one ACL_YES option is set...
2633                    if ($setting == ACL_YES)
2634                    {
2635                        $flag = true;
2636                        break;
2637                    }
2638                }
2639
2640                if (!$flag)
2641                {
2642                    continue;
2643                }
2644
2645                $sql_ary[] = array(
2646                    'forum_id'        => (int) $forum_id,
2647                    'user_id'        => 0,
2648                    'username'        => '',
2649                    'group_id'        => (int) $group_id,
2650                    'group_name'    => (string) $groupnames_ary[$group_id]
2651                );
2652            }
2653        }
2654    }
2655
2656    $db->sql_multi_insert(MODERATOR_CACHE_TABLE, $sql_ary);
2657}
2658
2659/**
2660* View log
2661*
2662* @param    string    $mode            The mode defines which log_type is used and from which log the entry is retrieved
2663* @param    array    &$log            The result array with the logs
2664* @param    mixed    &$log_count        If $log_count is set to false, we will skip counting all entries in the database.
2665*                                    Otherwise an integer with the number of total matching entries is returned.
2666* @param    int        $limit            Limit the number of entries that are returned
2667* @param    int        $offset            Offset when fetching the log entries, f.e. when paginating
2668* @param    mixed    $forum_id        Restrict the log entries to the given forum_id (can also be an array of forum_ids)
2669* @param    int        $topic_id        Restrict the log entries to the given topic_id
2670* @param    int        $user_id        Restrict the log entries to the given user_id
2671* @param    int        $limit_days        Only get log entries newer than the given timestamp
2672* @param    string    $sort_by        SQL order option, e.g. 'l.log_time DESC'
2673* @param    string    $keywords        Will only return log entries that have the keywords in log_operation or log_data
2674*
2675* @return    int                Returns the offset of the last valid page, if the specified offset was invalid (too high)
2676*/
2677function view_log($mode, &$log, &$log_count, $limit = 0, $offset = 0, $forum_id = 0, $topic_id = 0, $user_id = 0, $limit_days = 0, $sort_by = 'l.log_time DESC', $keywords = '')
2678{
2679    global $phpbb_log;
2680
2681    $count_logs = ($log_count !== false);
2682
2683    $log = $phpbb_log->get_logs($mode, $count_logs, $limit, $offset, $forum_id, $topic_id, $user_id, $limit_days, $sort_by, $keywords);
2684    $log_count = $phpbb_log->get_log_count();
2685
2686    return $phpbb_log->get_valid_offset();
2687}
2688
2689/**
2690* Removes moderators and administrators from foe lists.
2691*
2692* @param \phpbb\db\driver\driver_interface $db Database connection
2693* @param \phpbb\auth\auth $auth Authentication object
2694* @param array|bool $group_id If an array, remove all members of this group from foe lists, or false to ignore
2695* @param array|bool $user_id If an array, remove this user from foe lists, or false to ignore
2696* @return void
2697*/
2698function phpbb_update_foes($db, $auth, $group_id = false, $user_id = false)
2699{
2700    // update foes for some user
2701    if (is_array($user_id) && count($user_id))
2702    {
2703        $sql = 'DELETE FROM ' . ZEBRA_TABLE . '
2704            WHERE ' . $db->sql_in_set('zebra_id', $user_id) . '
2705                AND foe = 1';
2706        $db->sql_query($sql);
2707        return;
2708    }
2709
2710    // update foes for some group
2711    if (is_array($group_id) && count($group_id))
2712    {
2713        // Grab group settings...
2714        $sql_ary = array(
2715            'SELECT'    => 'a.group_id',
2716
2717            'FROM'        => array(
2718                ACL_OPTIONS_TABLE    => 'ao',
2719                ACL_GROUPS_TABLE    => 'a',
2720            ),
2721
2722            'LEFT_JOIN'    => array(
2723                array(
2724                    'FROM'    => array(ACL_ROLES_DATA_TABLE => 'r'),
2725                    'ON'    => 'a.auth_role_id = r.role_id',
2726                ),
2727            ),
2728
2729            'WHERE'        => '(ao.auth_option_id = a.auth_option_id OR ao.auth_option_id = r.auth_option_id)
2730                AND ' . $db->sql_in_set('a.group_id', $group_id) . "
2731                AND ao.auth_option IN ('a_', 'm_')",
2732
2733            'GROUP_BY'    => 'a.group_id',
2734        );
2735        $sql = $db->sql_build_query('SELECT', $sql_ary);
2736        $result = $db->sql_query($sql);
2737
2738        $groups = array();
2739        while ($row = $db->sql_fetchrow($result))
2740        {
2741            $groups[] = (int) $row['group_id'];
2742        }
2743        $db->sql_freeresult($result);
2744
2745        if (!count($groups))
2746        {
2747            return;
2748        }
2749
2750        switch ($db->get_sql_layer())
2751        {
2752            case 'mysqli':
2753                $sql = 'DELETE z.*
2754                    FROM ' . ZEBRA_TABLE . ' z, ' . USER_GROUP_TABLE . ' ug
2755                    WHERE z.zebra_id = ug.user_id
2756                        AND z.foe = 1
2757                        AND ' . $db->sql_in_set('ug.group_id', $groups);
2758                $db->sql_query($sql);
2759            break;
2760
2761            default:
2762                $sql = 'SELECT user_id
2763                    FROM ' . USER_GROUP_TABLE . '
2764                    WHERE ' . $db->sql_in_set('group_id', $groups);
2765                $result = $db->sql_query($sql);
2766
2767                $users = array();
2768                while ($row = $db->sql_fetchrow($result))
2769                {
2770                    $users[] = (int) $row['user_id'];
2771                }
2772                $db->sql_freeresult($result);
2773
2774                if (count($users))
2775                {
2776                    $sql = 'DELETE FROM ' . ZEBRA_TABLE . '
2777                        WHERE ' . $db->sql_in_set('zebra_id', $users) . '
2778                            AND foe = 1';
2779                    $db->sql_query($sql);
2780                }
2781            break;
2782        }
2783
2784        return;
2785    }
2786
2787    // update foes for everyone
2788    $perms = array();
2789    foreach ($auth->acl_get_list(false, array('a_', 'm_'), false) as $forum_id => $forum_ary)
2790    {
2791        foreach ($forum_ary as $auth_option => $user_ary)
2792        {
2793            $perms = array_merge($perms, $user_ary);
2794        }
2795    }
2796
2797    if (count($perms))
2798    {
2799        $sql = 'DELETE FROM ' . ZEBRA_TABLE . '
2800            WHERE ' . $db->sql_in_set('zebra_id', array_unique($perms)) . '
2801                AND foe = 1';
2802        $db->sql_query($sql);
2803    }
2804    unset($perms);
2805}
2806
2807/**
2808* Lists inactive users
2809*/
2810function view_inactive_users(&$users, &$user_count, $limit = 0, $offset = 0, $limit_days = 0, $sort_by = 'user_inactive_time DESC')
2811{
2812    global $db, $user;
2813
2814    $sql = 'SELECT COUNT(user_id) AS user_count
2815        FROM ' . USERS_TABLE . '
2816        WHERE user_type = ' . USER_INACTIVE .
2817        (($limit_days) ? " AND user_inactive_time >= $limit_days" : '');
2818    $result = $db->sql_query($sql);
2819    $user_count = (int) $db->sql_fetchfield('user_count');
2820    $db->sql_freeresult($result);
2821
2822    if ($user_count == 0)
2823    {
2824        // Save the queries, because there are no users to display
2825        return 0;
2826    }
2827
2828    if ($offset >= $user_count)
2829    {
2830        $offset = ($offset - $limit < 0) ? 0 : $offset - $limit;
2831    }
2832
2833    $sql = 'SELECT *
2834        FROM ' . USERS_TABLE . '
2835        WHERE user_type = ' . USER_INACTIVE .
2836        (($limit_days) ? " AND user_inactive_time >= $limit_days" : '') . "
2837        ORDER BY $sort_by";
2838    $result = $db->sql_query_limit($sql, $limit, $offset);
2839
2840    while ($row = $db->sql_fetchrow($result))
2841    {
2842        $row['inactive_reason'] = $user->lang['INACTIVE_REASON_UNKNOWN'];
2843        switch ($row['user_inactive_reason'])
2844        {
2845            case INACTIVE_REGISTER:
2846                $row['inactive_reason'] = $user->lang['INACTIVE_REASON_REGISTER'];
2847            break;
2848
2849            case INACTIVE_PROFILE:
2850                $row['inactive_reason'] = $user->lang['INACTIVE_REASON_PROFILE'];
2851            break;
2852
2853            case INACTIVE_MANUAL:
2854                $row['inactive_reason'] = $user->lang['INACTIVE_REASON_MANUAL'];
2855            break;
2856
2857            case INACTIVE_REMIND:
2858                $row['inactive_reason'] = $user->lang['INACTIVE_REASON_REMIND'];
2859            break;
2860        }
2861
2862        $users[] = $row;
2863    }
2864    $db->sql_freeresult($result);
2865
2866    return $offset;
2867}
2868
2869/**
2870* Lists warned users
2871*/
2872function view_warned_users(&$users, &$user_count, $limit = 0, $offset = 0, $limit_days = 0, $sort_by = 'user_warnings DESC')
2873{
2874    global $db;
2875
2876    $sql = 'SELECT user_id, username, user_colour, user_warnings, user_last_warning
2877        FROM ' . USERS_TABLE . '
2878        WHERE user_warnings > 0
2879        ' . (($limit_days) ? "AND user_last_warning >= $limit_days" : '') . "
2880        ORDER BY $sort_by";
2881    $result = $db->sql_query_limit($sql, $limit, $offset);
2882    $users = $db->sql_fetchrowset($result);
2883    $db->sql_freeresult($result);
2884
2885    $sql = 'SELECT count(user_id) AS user_count
2886        FROM ' . USERS_TABLE . '
2887        WHERE user_warnings > 0
2888        ' . (($limit_days) ? "AND user_last_warning >= $limit_days" : '');
2889    $result = $db->sql_query($sql);
2890    $user_count = (int) $db->sql_fetchfield('user_count');
2891    $db->sql_freeresult($result);
2892
2893    return;
2894}
2895
2896/**
2897* Get database size
2898*/
2899function get_database_size()
2900{
2901    global $db, $user;
2902
2903    $database_size = false;
2904
2905    switch ($db->get_sql_layer())
2906    {
2907        case 'mysqli':
2908            $mysql_engine    = ['MyISAM', 'InnoDB', 'Aria'];
2909            $db_name        = $db->get_db_name();
2910            $database_size    = 0;
2911
2912            $sql = 'SHOW TABLE STATUS
2913                FROM ' . $db->sql_quote($db_name);
2914            $result = $db->sql_query($sql, 7200);
2915
2916            while ($row = $db->sql_fetchrow($result))
2917            {
2918                if (isset($row['Engine']) && in_array($row['Engine'], $mysql_engine))
2919                {
2920                    $database_size += $row['Data_length'] + $row['Index_length'];
2921                }
2922            }
2923
2924            $db->sql_freeresult($result);
2925
2926            $database_size = $database_size ? $database_size : false;
2927
2928        break;
2929
2930        case 'sqlite3':
2931            global $dbhost;
2932
2933            if (file_exists($dbhost))
2934            {
2935                $database_size = filesize($dbhost);
2936            }
2937
2938        break;
2939
2940        case 'mssql_odbc':
2941        case 'mssqlnative':
2942            $sql = 'SELECT @@VERSION AS mssql_version';
2943            $result = $db->sql_query($sql);
2944            $row = $db->sql_fetchrow($result);
2945            $db->sql_freeresult($result);
2946
2947            $sql = 'SELECT ((SUM(size) * 8.0) * 1024.0) as dbsize
2948                FROM sysfiles';
2949
2950            if ($row)
2951            {
2952                // Azure stats are stored elsewhere
2953                if (strpos($row['mssql_version'], 'SQL Azure') !== false)
2954                {
2955                    $sql = 'SELECT ((SUM(reserved_page_count) * 8.0) * 1024.0) as dbsize
2956                    FROM sys.dm_db_partition_stats';
2957                }
2958            }
2959
2960            $result = $db->sql_query($sql, 7200);
2961            $database_size = ($row = $db->sql_fetchrow($result)) ? $row['dbsize'] : false;
2962            $db->sql_freeresult($result);
2963        break;
2964
2965        case 'postgres':
2966            $database = $db->get_db_name();
2967
2968            if (strpos($database, '.') !== false)
2969            {
2970                $database = explode('.', $database)[0];
2971            }
2972
2973            $sql = "SELECT pg_database_size('" . $database . "') AS dbsize";
2974            $result = $db->sql_query($sql, 7200);
2975            $row = $db->sql_fetchrow($result);
2976            $database_size = !empty($row['dbsize']) ? $row['dbsize'] : false;
2977            $db->sql_freeresult($result);
2978        break;
2979
2980        case 'oracle':
2981            $sql = 'SELECT SUM(bytes) as dbsize
2982                FROM user_segments';
2983            $result = $db->sql_query($sql, 7200);
2984            $database_size = ($row = $db->sql_fetchrow($result)) ? $row['dbsize'] : false;
2985            $db->sql_freeresult($result);
2986        break;
2987    }
2988
2989    $database_size = ($database_size !== false) ? get_formatted_filesize($database_size) : $user->lang['NOT_AVAILABLE'];
2990
2991    return $database_size;
2992}
2993
2994/*
2995* Tidy Warnings
2996* Remove all warnings which have now expired from the database
2997* The duration of a warning can be defined by the administrator
2998* This only removes the warning and reduces the associated count,
2999* it does not remove the user note recording the contents of the warning
3000*/
3001function tidy_warnings()
3002{
3003    global $db, $config;
3004
3005    $expire_date = time() - ($config['warnings_expire_days'] * 86400);
3006    $warning_list = $user_list = array();
3007
3008    $sql = 'SELECT * FROM ' . WARNINGS_TABLE . "
3009        WHERE warning_time < $expire_date";
3010    $result = $db->sql_query($sql);
3011
3012    while ($row = $db->sql_fetchrow($result))
3013    {
3014        $warning_list[] = $row['warning_id'];
3015        $user_list[$row['user_id']] = isset($user_list[$row['user_id']]) ? ++$user_list[$row['user_id']] : 1;
3016    }
3017    $db->sql_freeresult($result);
3018
3019    if (count($warning_list))
3020    {
3021        $db->sql_transaction('begin');
3022
3023        $sql = 'DELETE FROM ' . WARNINGS_TABLE . '
3024            WHERE ' . $db->sql_in_set('warning_id', $warning_list);
3025        $db->sql_query($sql);
3026
3027        foreach ($user_list as $user_id => $value)
3028        {
3029            $sql = 'UPDATE ' . USERS_TABLE . " SET user_warnings = user_warnings - $value
3030                WHERE user_id = $user_id";
3031            $db->sql_query($sql);
3032        }
3033
3034        $db->sql_transaction('commit');
3035    }
3036
3037    $config->set('warnings_last_gc', time(), false);
3038}
3039
3040/**
3041* Tidy database, doing some maintanance tasks
3042*/
3043function tidy_database()
3044{
3045    global $config, $db;
3046
3047    // Here we check permission consistency
3048
3049    // Sometimes, it can happen permission tables having forums listed which do not exist
3050    $sql = 'SELECT forum_id
3051        FROM ' . FORUMS_TABLE;
3052    $result = $db->sql_query($sql);
3053
3054    $forum_ids = array(0);
3055    while ($row = $db->sql_fetchrow($result))
3056    {
3057        $forum_ids[] = $row['forum_id'];
3058    }
3059    $db->sql_freeresult($result);
3060
3061    $db->sql_transaction('begin');
3062
3063    // Delete those rows from the acl tables not having listed the forums above
3064    $sql = 'DELETE FROM ' . ACL_GROUPS_TABLE . '
3065        WHERE ' . $db->sql_in_set('forum_id', $forum_ids, true);
3066    $db->sql_query($sql);
3067
3068    $sql = 'DELETE FROM ' . ACL_USERS_TABLE . '
3069        WHERE ' . $db->sql_in_set('forum_id', $forum_ids, true);
3070    $db->sql_query($sql);
3071
3072    $db->sql_transaction('commit');
3073
3074    $config->set('database_last_gc', time(), false);
3075}
3076
3077/**
3078* Add permission language - this will make sure custom files will be included
3079*/
3080function add_permission_language()
3081{
3082    global $user, $phpEx, $phpbb_extension_manager;
3083
3084    // add permission language files from extensions
3085    $finder = $phpbb_extension_manager->get_finder();
3086
3087    $lang_files = $finder
3088        ->prefix('permissions_')
3089        ->suffix(".$phpEx")
3090        ->core_path('language/')
3091        ->extension_directory('/language')
3092        ->find();
3093
3094    foreach ($lang_files as $lang_file => $ext_name)
3095    {
3096        if ($ext_name === '/')
3097        {
3098            $user->add_lang($lang_file);
3099        }
3100        else
3101        {
3102            $user->add_lang_ext($ext_name, $lang_file);
3103        }
3104    }
3105}
3106
3107/**
3108 * Enables a particular flag in a bitfield column of a given table.
3109 *
3110 * @param string    $table_name        The table to update
3111 * @param string    $column_name    The column containing a bitfield to update
3112 * @param int        $flag            The binary flag which is OR-ed with the current column value
3113 * @param string    $sql_more        This string is attached to the sql query generated to update the table.
3114 *
3115 * @return void
3116 */
3117function enable_bitfield_column_flag($table_name, $column_name, $flag, $sql_more = '')
3118{
3119    global $db;
3120
3121    $sql = 'UPDATE ' . $table_name . '
3122        SET ' . $column_name . ' = ' . $db->sql_bit_or($column_name, $flag) . '
3123        ' . $sql_more;
3124    $db->sql_query($sql);
3125}
3126
3127function display_ban_end_options()
3128{
3129    global $user, $template;
3130
3131    // Ban length options
3132    $ban_end_text = array(0 => $user->lang['PERMANENT'], 30 => $user->lang['30_MINS'], 60 => $user->lang['1_HOUR'], 360 => $user->lang['6_HOURS'], 1440 => $user->lang['1_DAY'], 10080 => $user->lang['7_DAYS'], 20160 => $user->lang['2_WEEKS'], 40320 => $user->lang['1_MONTH'], -1 => $user->lang['UNTIL'] . ' -&gt; ');
3133
3134    $ban_end_options = '';
3135    foreach ($ban_end_text as $length => $text)
3136    {
3137        $ban_end_options .= '<option value="' . $length . '">' . $text . '</option>';
3138    }
3139
3140    $template->assign_vars(array(
3141        'S_BAN_END_OPTIONS'    => $ban_end_options
3142    ));
3143}
3144
3145/**
3146* Display ban options
3147*/
3148function display_ban_options($mode)
3149{
3150    global $language, $user, $template, $phpbb_container;
3151
3152    /** @var \phpbb\ban\manager $ban_manager */
3153    $ban_manager = $phpbb_container->get('ban.manager');
3154    $ban_rows = $ban_manager->get_bans($mode);
3155
3156    $banned_options = [];
3157
3158    foreach ($ban_rows as $ban_row)
3159    {
3160        $banned_options[] = [
3161            'value'        => $ban_row['ban_id'],
3162            'label'        => $ban_row['label'] ?? $ban_row['ban_item'],
3163        ];
3164
3165        $time_length = ($ban_row['ban_end']) ? ($ban_row['ban_end'] - $ban_row['ban_start']) / 60 : 0;
3166
3167        if ($time_length == 0)
3168        {
3169            // Banned permanently
3170            $ban_length = $language->lang('PERMANENT');
3171        }
3172        else if (isset($ban_end_text[$time_length]))
3173        {
3174            // Banned for a given duration
3175            $ban_length = $language->lang('BANNED_UNTIL_DURATION', $ban_end_text[$time_length], $user->format_date($ban_row['ban_end'], false, true));
3176        }
3177        else
3178        {
3179            // Banned until given date
3180            $ban_length = $language->lang('BANNED_UNTIL_DATE', $user->format_date($ban_row['ban_end'], false, true));
3181        }
3182
3183        $template->assign_block_vars('bans', array(
3184            'BAN_ID'        => (int) $ban_row['ban_id'],
3185            'LENGTH'        => $ban_length,
3186            'A_LENGTH'        => addslashes($ban_length),
3187            'REASON'        => $ban_row['ban_reason'],
3188            'A_REASON'        => addslashes($ban_row['ban_reason']),
3189            'GIVE_REASON'    => $ban_row['ban_reason_display'],
3190            'A_GIVE_REASON'    => addslashes($ban_row['ban_reason_display']),
3191        ));
3192    }
3193
3194    if (count($banned_options))
3195    {
3196        $banned_select = [
3197            'tag'        => 'select',
3198            'name'        => 'unban[]',
3199            'id'        => 'unban',
3200            'class'        => 'w-50',
3201            'multiple'    => true,
3202            'size'        => 10,
3203            'data'        => [
3204                'onchange'    => 'display_details',
3205            ],
3206            'options'    => [[
3207                'label'        => $language->lang('OPTIONS_BANNED'),
3208                'options'    => $banned_options,
3209            ]],
3210        ];
3211
3212        $template->assign_vars(['BANNED_SELECT' => $banned_select]);
3213    }
3214}