diff --git a/phpBB/adm/style/acp_forums.html b/phpBB/adm/style/acp_forums.html
index e8b20007dc..0bb5e10f57 100644
--- a/phpBB/adm/style/acp_forums.html
+++ b/phpBB/adm/style/acp_forums.html
@@ -278,6 +278,19 @@
+
+
{L_FORUM_PRUNE_SHADOW_EXPLAIN}
+ -
+
+
+
+
{L_AUTO_PRUNE_FREQ_EXPLAIN}
+ - {L_DAYS}
+
+
+
{L_AUTO_PRUNE_DAYS_EXPLAIN}
+ - {L_DAYS}
+
diff --git a/phpBB/config/cron_tasks.yml b/phpBB/config/cron_tasks.yml
index fd3aea85dc..4fa5d1440e 100644
--- a/phpBB/config/cron_tasks.yml
+++ b/phpBB/config/cron_tasks.yml
@@ -23,6 +23,19 @@ services:
tags:
- { name: cron.task }
+ cron.task.core.prune_shadow_topics:
+ class: phpbb\cron\task\core\prune_shadow_topics
+ arguments:
+ - %core.root_path%
+ - %core.php_ext%
+ - @config
+ - @dbal.conn
+ - @log
+ calls:
+ - [set_name, [cron.task.core.prune_shadow_topics]]
+ tags:
+ - { name: cron.task }
+
cron.task.core.prune_notifications:
class: phpbb\cron\task\core\prune_notifications
arguments:
diff --git a/phpBB/includes/acp/acp_forums.php b/phpBB/includes/acp/acp_forums.php
index a1af8c489d..c47d9bc185 100644
--- a/phpBB/includes/acp/acp_forums.php
+++ b/phpBB/includes/acp/acp_forums.php
@@ -138,12 +138,15 @@ class acp_forums
'enable_prune' => request_var('enable_prune', false),
'enable_post_review' => request_var('enable_post_review', true),
'enable_quick_reply' => request_var('enable_quick_reply', false),
+ 'enable_shadow_prune' => request_var('enable_shadow_prune', false),
'prune_days' => request_var('prune_days', 7),
'prune_viewed' => request_var('prune_viewed', 7),
'prune_freq' => request_var('prune_freq', 1),
'prune_old_polls' => request_var('prune_old_polls', false),
'prune_announce' => request_var('prune_announce', false),
'prune_sticky' => request_var('prune_sticky', false),
+ 'prune_shadow_days' => request_var('prune_shadow_days', 7),
+ 'prune_shadow_freq' => request_var('prune_shadow_freq', 1),
'forum_password' => request_var('forum_password', '', true),
'forum_password_confirm'=> request_var('forum_password_confirm', '', true),
'forum_password_unset' => request_var('forum_password_unset', false),
@@ -457,6 +460,9 @@ class acp_forums
'prune_days' => 7,
'prune_viewed' => 7,
'prune_freq' => 1,
+ 'enable_shadow_prune' => false,
+ 'prune_shadow_days' => 7,
+ 'prune_shadow_freq' => 1,
'forum_flags' => FORUM_FLAG_POST_REVIEW + FORUM_FLAG_ACTIVE_TOPICS,
'forum_options' => 0,
'forum_password' => '',
@@ -636,6 +642,8 @@ class acp_forums
'PRUNE_FREQ' => $forum_data['prune_freq'],
'PRUNE_DAYS' => $forum_data['prune_days'],
'PRUNE_VIEWED' => $forum_data['prune_viewed'],
+ 'PRUNE_SHADOW_FREQ' => $forum_data['prune_shadow_freq'],
+ 'PRUNE_SHADOW_DAYS' => $forum_data['prune_shadow_days'],
'TOPICS_PER_PAGE' => $forum_data['forum_topics_per_page'],
'FORUM_RULES_LINK' => $forum_data['forum_rules_link'],
'FORUM_RULES' => $forum_data['forum_rules'],
@@ -668,6 +676,7 @@ class acp_forums
'S_DISPLAY_SUBFORUM_LIST' => ($forum_data['display_subforum_list']) ? true : false,
'S_DISPLAY_ON_INDEX' => ($forum_data['display_on_index']) ? true : false,
'S_PRUNE_ENABLE' => ($forum_data['enable_prune']) ? true : false,
+ 'S_PRUNE_SHADOW_ENABLE' => ($forum_data['enable_shadow_prune']) ? true : false,
'S_FORUM_LINK_TRACK' => ($forum_data['forum_flags'] & FORUM_FLAG_LINK_TRACK) ? true : false,
'S_PRUNE_OLD_POLLS' => ($forum_data['forum_flags'] & FORUM_FLAG_PRUNE_POLL) ? true : false,
'S_PRUNE_ANNOUNCE' => ($forum_data['forum_flags'] & FORUM_FLAG_PRUNE_ANNOUNCE) ? true : false,
diff --git a/phpBB/includes/functions_admin.php b/phpBB/includes/functions_admin.php
index 81a381b326..2bf8e6dcf0 100644
--- a/phpBB/includes/functions_admin.php
+++ b/phpBB/includes/functions_admin.php
@@ -2326,6 +2326,11 @@ function prune($forum_id, $prune_mode, $prune_date, $prune_flags = 0, $auto_sync
$sql_and .= " AND topic_last_view_time < $prune_date";
}
+ if ($prune_mode == 'shadow')
+ {
+ $sql_and .= ' AND topic_status = ' . ITEM_MOVED . " AND topic_last_post_time < $prune_date";
+ }
+
$sql = 'SELECT topic_id
FROM ' . TOPICS_TABLE . '
WHERE ' . $db->sql_in_set('forum_id', $forum_id) . "
diff --git a/phpBB/install/schemas/schema.json b/phpBB/install/schemas/schema.json
index 15d2fd6e84..176691f1a6 100644
--- a/phpBB/install/schemas/schema.json
+++ b/phpBB/install/schemas/schema.json
@@ -800,6 +800,22 @@
"UINT:20",
0
],
+ "enable_shadow_prune": [
+ "BOOL",
+ 0
+ ],
+ "prune_shadow_days": [
+ "UINT",
+ 7
+ ],
+ "prune_shadow_freq": [
+ "UINT",
+ 1
+ ],
+ "prune_shadow_next": [
+ "INT:11",
+ 0
+ ],
"forum_posts_approved": [
"UINT",
0
diff --git a/phpBB/language/en/acp/common.php b/phpBB/language/en/acp/common.php
index cf32c7c225..2dc58d8361 100644
--- a/phpBB/language/en/acp/common.php
+++ b/phpBB/language/en/acp/common.php
@@ -676,6 +676,7 @@ $lang = array_merge($lang, array(
'LOG_PRUNE' => 'Pruned forums
» %s',
'LOG_AUTO_PRUNE' => 'Auto-pruned forums
» %s',
+ 'LOG_PRUNE_SHADOW' => 'Auto-pruned shadow topics
» %s',
'LOG_PRUNE_USER_DEAC' => 'Users deactivated
» %s',
'LOG_PRUNE_USER_DEL_DEL' => 'Users pruned and posts deleted
» %s',
'LOG_PRUNE_USER_DEL_ANON' => 'Users pruned and posts retained
» %s',
diff --git a/phpBB/language/en/acp/forums.php b/phpBB/language/en/acp/forums.php
index 756cb7ae0f..d64380b6b6 100644
--- a/phpBB/language/en/acp/forums.php
+++ b/phpBB/language/en/acp/forums.php
@@ -101,6 +101,8 @@ $lang = array_merge($lang, array(
'FORUM_PASSWORD_OLD' => 'The forum password is using an old hashing method and should be changed.',
'FORUM_PASSWORD_MISMATCH' => 'The passwords you entered did not match.',
'FORUM_PRUNE_SETTINGS' => 'Forum prune settings',
+ 'FORUM_PRUNE_SHADOW' => 'Enable auto-pruning of shadow topics',
+ 'FORUM_PRUNE_SHADOW_EXPLAIN' => 'Prunes the forum of shadow topics, set the frequency/age parameters below.',
'FORUM_RESYNCED' => 'Forum “%s” successfully resynced',
'FORUM_RULES_EXPLAIN' => 'Forum rules are displayed at any page within the given forum.',
'FORUM_RULES_LINK' => 'Link to forum rules',
diff --git a/phpBB/phpbb/cron/task/core/prune_shadow_topics.php b/phpBB/phpbb/cron/task/core/prune_shadow_topics.php
new file mode 100644
index 0000000000..b30e665a87
--- /dev/null
+++ b/phpBB/phpbb/cron/task/core/prune_shadow_topics.php
@@ -0,0 +1,191 @@
+phpbb_root_path = $phpbb_root_path;
+ $this->php_ext = $php_ext;
+ $this->config = $config;
+ $this->db = $db;
+ $this->log = $log;
+ }
+
+ /**
+ * Manually set forum data.
+ *
+ * @param array $forum_data Information about a forum to be pruned.
+ */
+ public function set_forum_data($forum_data)
+ {
+ $this->forum_data = $forum_data;
+ }
+
+ /**
+ * Runs this cron task.
+ *
+ * @return null
+ */
+ public function run()
+ {
+ if (!function_exists('auto_prune'))
+ {
+ include($this->phpbb_root_path . 'includes/functions_admin.' . $this->php_ext);
+ }
+
+ if ($this->forum_data['prune_shadow_days'])
+ {
+ $this->auto_prune_shadow_topics($this->forum_data['forum_id'], 'shadow', $this->forum_data['forum_flags'], $this->forum_data['prune_shadow_days'], $this->forum_data['prune_shadow_freq']);
+ }
+ }
+
+ /**
+ * Returns whether this cron task can run, given current board configuration.
+ *
+ * This cron task will not run when system cron is utilised, as in
+ * such cases prune_all_forums task would run instead.
+ *
+ * Additionally, this task must be given the forum data, either via
+ * the constructor or parse_parameters method.
+ *
+ * @return bool
+ */
+ public function is_runnable()
+ {
+ return !$this->config['use_system_cron'] && $this->forum_data;
+ }
+
+ /**
+ * Returns whether this cron task should run now, because enough time
+ * has passed since it was last run.
+ *
+ * Forum pruning interval is specified in the forum data.
+ *
+ * @return bool
+ */
+ public function should_run()
+ {
+ return $this->forum_data['enable_shadow_prune'] && $this->forum_data['prune_shadow_next'] < time();
+ }
+
+ /**
+ * Returns parameters of this cron task as an array.
+ * The array has one key, f, whose value is id of the forum to be pruned.
+ *
+ * @return array
+ */
+ public function get_parameters()
+ {
+ return array('f' => $this->forum_data['forum_id']);
+ }
+
+ /**
+ * Parses parameters found in $request, which is an instance of
+ * \phpbb\request\request_interface.
+ *
+ * It is expected to have a key f whose value is id of the forum to be pruned.
+ *
+ * @param \phpbb\request\request_interface $request Request object.
+ *
+ * @return null
+ */
+ public function parse_parameters(\phpbb\request\request_interface $request)
+ {
+ $this->forum_data = null;
+ if ($request->is_set('f'))
+ {
+ $forum_id = $request->variable('f', 0);
+
+ $sql = 'SELECT forum_id, prune_shadow_next, enable_shadow_prune, prune_shadow_days, forum_flags, prune_shadow_freq
+ FROM ' . FORUMS_TABLE . "
+ WHERE forum_id = $forum_id";
+ $result = $this->db->sql_query($sql);
+ $row = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ if ($row)
+ {
+ $this->forum_data = $row;
+ }
+ }
+ }
+
+ /**
+ * Automatically prune shadow topics
+ * Based on fuunction auto_prune()
+ * @param int $forum_id Forum ID of forum that should be pruned
+ * @param string $prune_mode Prune mode
+ * @param int $prune_flags Prune flags
+ * @param int $prune_freq Prune frequency
+ * @return null
+ */
+ protected function auto_prune_shadow_topics($forum_id, $prune_mode, $prune_flags, $prune_days, $prune_freq)
+ {
+ $sql = 'SELECT forum_name
+ FROM ' . FORUMS_TABLE . "
+ WHERE forum_id = $forum_id";
+ $result = $this->db->sql_query($sql, 3600);
+ $row = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ if ($row)
+ {
+ $prune_date = time() - ($prune_days * 86400);
+ $next_prune = time() + ($prune_freq * 86400);
+
+ prune($forum_id, $prune_mode, $prune_date, $prune_flags, true);
+
+ $sql = 'UPDATE ' . FORUMS_TABLE . "
+ SET prune_shadow_next = $next_prune
+ WHERE forum_id = $forum_id";
+ $this->db->sql_query($sql);
+
+ $this->log->add('admin', 'LOG_PRUNE_SHADOW', $row['forum_name']);
+ }
+
+ return;
+ }
+}
diff --git a/phpBB/phpbb/db/migration/data/v310/prune_shadow_topics.php b/phpBB/phpbb/db/migration/data/v310/prune_shadow_topics.php
new file mode 100644
index 0000000000..83f5f903e8
--- /dev/null
+++ b/phpBB/phpbb/db/migration/data/v310/prune_shadow_topics.php
@@ -0,0 +1,46 @@
+ array(
+ $this->table_prefix . 'forums' => array(
+ 'enable_shadow_prune' => array('BOOL', 0),
+ 'prune_shadow_days' => array('UINT', 7),
+ 'prune_shadow_freq' => array('UINT', 1),
+ 'prune_shadow_next' => array('INT:11', 0),
+ ),
+ ),
+ );
+ }
+
+ public function revert_schema()
+ {
+ return array(
+ 'drop_columns' => array(
+ $this->table_prefix . 'forums' => array(
+ 'enable_shadow_prune',
+ 'prune_shadow_days',
+ 'prune_shadow_freq',
+ 'prune_shadow_next',
+ ),
+ ),
+ );
+ }
+}
diff --git a/phpBB/viewforum.php b/phpBB/viewforum.php
index 7f194bbcef..4da0267284 100644
--- a/phpBB/viewforum.php
+++ b/phpBB/viewforum.php
@@ -224,6 +224,18 @@ if (!$config['use_system_cron'])
$url = $task->get_url();
$template->assign_var('RUN_CRON_TASK', '
');
}
+ else
+ {
+ // See if we should prune the shadow topics instead
+ $task = $cron->find_task('cron.task.core.prune_shadow_topics');
+ $task->set_forum_data($forum_data);
+
+ if ($task->is_ready())
+ {
+ $url = $task->get_url();
+ $template->assign_var('RUN_CRON_TASK', '
');
+ }
+ }
}
// Forum rules and subscription info
diff --git a/tests/functional/prune_shadow_topic_test.php b/tests/functional/prune_shadow_topic_test.php
new file mode 100644
index 0000000000..901cedb389
--- /dev/null
+++ b/tests/functional/prune_shadow_topic_test.php
@@ -0,0 +1,207 @@
+login();
+ $this->admin_login();
+
+ $crawler = self::request('GET', "adm/index.php?i=acp_forums&mode=manage&sid={$this->sid}");
+ $form = $crawler->selectButton('addforum')->form(array(
+ 'forum_name' => 'Prune Shadow',
+ ));
+ $crawler = self::submit($form);
+ $form = $crawler->selectButton('update')->form(array(
+ 'forum_perm_from' => 2,
+ 'enable_shadow_prune' => true,
+ 'prune_shadow_freq' => 1,
+ 'prune_shadow_days' => 1,
+ ));
+ $crawler = self::submit($form);
+ }
+
+ public function test_create_post()
+ {
+ $this->login();
+ $this->load_ids(array(
+ 'forums' => array(
+ 'Prune Shadow',
+ ),
+ ));
+
+ $this->assert_forum_details($this->data['forums']['Prune Shadow'], array(
+ 'forum_posts_approved' => 0,
+ 'forum_posts_unapproved' => 0,
+ 'forum_posts_softdeleted' => 0,
+ 'forum_topics_approved' => 0,
+ 'forum_topics_unapproved' => 0,
+ 'forum_topics_softdeleted' => 0,
+ 'forum_last_post_id' => 0,
+ ), 'initial comparison');
+
+ // Test creating topic
+ $this->post = $this->create_topic($this->data['forums']['Prune Shadow'], 'Prune Shadow #1', 'This is a test topic posted by the testing framework.');
+ $crawler = self::request('GET', "viewtopic.php?t={$this->post['topic_id']}&sid={$this->sid}");
+
+ $this->assertContains('Prune Shadow #1', $crawler->filter('html')->text());
+ $this->data['topics']['Prune Shadow #1'] = (int) $post['topic_id'];
+ $this->data['posts']['Prune Shadow #1'] = (int) $this->get_parameter_from_link($crawler->filter('.post')->selectLink($this->lang('POST', '', ''))->link()->getUri(), 'p');
+
+ $this->assert_forum_details($this->data['forums']['Prune Shadow'], array(
+ 'forum_posts_approved' => 1,
+ 'forum_posts_unapproved' => 0,
+ 'forum_posts_softdeleted' => 0,
+ 'forum_topics_approved' => 1,
+ 'forum_topics_unapproved' => 0,
+ 'forum_topics_softdeleted' => 0,
+ 'forum_last_post_id' => $this->data['posts']['Prune Shadow #1'],
+ ), 'after creating topic #1');
+
+ // Test creating a reply
+ $post2 = $this->create_post($this->data['forums']['Prune Shadow'], $this->post['topic_id'], 'Re: Prune Shadow #1-#2', 'This is a test post posted by the testing framework.');
+ $crawler = self::request('GET', "viewtopic.php?t={$post2['topic_id']}&sid={$this->sid}");
+
+ $this->assertContains('Re: Prune Shadow #1-#2', $crawler->filter('html')->text());
+ $this->data['posts']['Re: Prune Shadow #1-#2'] = (int) $this->get_parameter_from_link($crawler->filter('.post')->eq(1)->selectLink($this->lang('POST', '', ''))->link()->getUri(), 'p');
+
+ $this->assert_forum_details($this->data['forums']['Prune Shadow'], array(
+ 'forum_posts_approved' => 2,
+ 'forum_posts_unapproved' => 0,
+ 'forum_posts_softdeleted' => 0,
+ 'forum_topics_approved' => 1,
+ 'forum_topics_unapproved' => 0,
+ 'forum_topics_softdeleted' => 0,
+ 'forum_last_post_id' => $this->data['posts']['Re: Prune Shadow #1-#2'],
+ ), 'after replying');
+ }
+
+ public function test_move_topic()
+ {
+ $this->login();
+ $this->load_ids(array(
+ 'forums' => array(
+ 'Prune Shadow',
+ ),
+ 'topics' => array(
+ 'Prune Shadow #1',
+ ),
+ ));
+
+ $crawler = self::request('GET', "mcp.php?f={$this->data['forums']['Prune Shadow']}&i=main&action=move&mode=forum_view&start=0&topic_id_list[]={$this->data['topics']['Prune Shadow #1']}&sid={$this->sid}");
+ $form = $crawler->selectButton('confirm')->form(array(
+ 'to_forum_id' => 2,
+ 'move_leave_shadow' => true,
+ ));
+ $crawler = self::submit($form);
+
+ $this->assert_forum_details($this->data['forums']['Prune Shadow'], array(
+ 'forum_posts_approved' => 0,
+ 'forum_posts_unapproved' => 0,
+ 'forum_posts_softdeleted' => 0,
+ 'forum_topics_approved' => 1,
+ 'forum_topics_unapproved' => 0,
+ 'forum_topics_softdeleted' => 0,
+ ), 'after moving');
+
+ $this->db = $this->get_db();
+ // Date topic 3 days back
+ $sql = 'UPDATE phpbb_topics
+ SET topic_last_post_time = ' . (time() - 60*60*24*3) . '
+ WHERE topic_id = ' . ($this->data['topics']['Prune Shadow #1'] + 1);
+ $result = $this->db->sql_query($sql);
+
+ $crawler = self::request('GET', "viewforum.php?f={$this->data['forums']['Prune Shadow']}&sid={$this->sid}");
+ $cron_link = $crawler->filter('img')->last()->attr('src');
+ $crawler = self::request('GET', $cron_link . "&sid={$this->sid}", array(), false);
+
+ $this->assert_forum_details($this->data['forums']['Prune Shadow'], array(
+ 'forum_posts_approved' => 0,
+ 'forum_posts_unapproved' => 0,
+ 'forum_posts_softdeleted' => 0,
+ 'forum_topics_approved' => 0,
+ 'forum_topics_unapproved' => 0,
+ 'forum_topics_softdeleted' => 0,
+ ), 'after the cron job');
+ }
+
+ public function assert_forum_details($forum_id, $details, $additional_error_message = '')
+ {
+ $this->db = $this->get_db();
+
+ $sql = 'SELECT ' . implode(', ', array_keys($details)) . '
+ FROM phpbb_forums
+ WHERE forum_id = ' . (int) $forum_id;
+ $result = $this->db->sql_query($sql);
+ $data = $this->db->sql_fetchrow($result);
+ $this->db->sql_freeresult($result);
+
+ $this->assertEquals($details, $data, "Forum {$forum_id} does not match expected {$additional_error_message}");
+ }
+
+ public function load_ids($data)
+ {
+ $this->db = $this->get_db();
+
+ if (!empty($data['forums']))
+ {
+ $sql = 'SELECT *
+ FROM phpbb_forums
+ WHERE ' . $this->db->sql_in_set('forum_name', $data['forums']);
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (in_array($row['forum_name'], $data['forums']))
+ {
+ $this->data['forums'][$row['forum_name']] = (int) $row['forum_id'];
+ }
+ }
+ $this->db->sql_freeresult($result);
+ }
+
+ if (!empty($data['topics']))
+ {
+ $sql = 'SELECT *
+ FROM phpbb_topics
+ WHERE ' . $this->db->sql_in_set('topic_title', $data['topics']);
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (in_array($row['topic_title'], $data['topics']))
+ {
+ $this->data['topics'][$row['topic_title']] = (int) $row['topic_id'];
+ }
+ }
+ $this->db->sql_freeresult($result);
+ }
+
+ if (!empty($data['posts']))
+ {
+ $sql = 'SELECT *
+ FROM phpbb_posts
+ WHERE ' . $this->db->sql_in_set('post_subject', $data['posts']);
+ $result = $this->db->sql_query($sql);
+ while ($row = $this->db->sql_fetchrow($result))
+ {
+ if (in_array($row['post_subject'], $data['posts']))
+ {
+ $this->data['posts'][$row['post_subject']] = (int) $row['post_id'];
+ }
+ }
+ $this->db->sql_freeresult($result);
+ }
+ }
+}