diff --git a/phpBB/includes/ucp/ucp_attachments.php b/phpBB/includes/ucp/ucp_attachments.php index 666170574b..41cfbfe4ce 100644 --- a/phpBB/includes/ucp/ucp_attachments.php +++ b/phpBB/includes/ucp/ucp_attachments.php @@ -47,7 +47,7 @@ class ucp_attachments if ($delete && count($delete_ids)) { // Validate $delete_ids... - $sql = 'SELECT a.attach_id, p.post_edit_locked, t.topic_status, f.forum_id, f.forum_status + $sql = 'SELECT a.attach_id, a.in_message, p.post_edit_locked, p.post_time, t.topic_status, f.forum_id, f.forum_status, pt.folder_id FROM ' . ATTACHMENTS_TABLE . ' a LEFT JOIN ' . POSTS_TABLE . ' p ON (a.post_msg_id = p.post_id AND a.in_message = 0) @@ -55,6 +55,10 @@ class ucp_attachments ON (t.topic_id = p.topic_id AND a.in_message = 0) LEFT JOIN ' . FORUMS_TABLE . ' f ON (f.forum_id = t.forum_id AND a.in_message = 0) + LEFT JOIN ' . PRIVMSGS_TABLE . ' pr + ON (a.post_msg_id = pr.msg_id AND a.in_message = 1) + LEFT JOIN ' . PRIVMSGS_TO_TABLE . ' pt + ON (a.post_msg_id = pt.msg_id AND a.poster_id = pt.author_id AND a.poster_id = pt.user_id AND a.in_message = 1) WHERE a.poster_id = ' . $user->data['user_id'] . ' AND a.is_orphan = 0 AND ' . $db->sql_in_set('a.attach_id', $delete_ids); @@ -63,7 +67,7 @@ class ucp_attachments $delete_ids = array(); while ($row = $db->sql_fetchrow($result)) { - if (!$auth->acl_get('m_edit', $row['forum_id']) && ($row['forum_status'] == ITEM_LOCKED || $row['topic_status'] == ITEM_LOCKED || $row['post_edit_locked'])) + if (!$this->can_delete_file($row)) { continue; } @@ -141,12 +145,13 @@ class ucp_attachments $pagination = $phpbb_container->get('pagination'); $start = $pagination->validate_start($start, $config['topics_per_page'], $num_attachments); - $sql = 'SELECT a.*, t.topic_title, pr.message_subject as message_title, p.post_edit_locked, t.topic_status, f.forum_id, f.forum_status + $sql = 'SELECT a.*, t.topic_title, pr.message_subject as message_title, pr.message_time as message_time, pt.folder_id, p.post_edit_locked, t.topic_status, f.forum_id, f.forum_status FROM ' . ATTACHMENTS_TABLE . ' a LEFT JOIN ' . POSTS_TABLE . ' p ON (a.post_msg_id = p.post_id AND a.in_message = 0) LEFT JOIN ' . TOPICS_TABLE . ' t ON (a.topic_id = t.topic_id AND a.in_message = 0) LEFT JOIN ' . FORUMS_TABLE . ' f ON (f.forum_id = t.forum_id AND a.in_message = 0) LEFT JOIN ' . PRIVMSGS_TABLE . ' pr ON (a.post_msg_id = pr.msg_id AND a.in_message = 1) + LEFT JOIN ' . PRIVMSGS_TO_TABLE . ' pt ON (a.post_msg_id = pt.msg_id AND a.poster_id = pt.author_id AND a.poster_id = pt.user_id AND a.in_message = 1) WHERE a.poster_id = ' . $user->data['user_id'] . " AND a.is_orphan = 0 ORDER BY $order_by"; @@ -183,7 +188,7 @@ class ucp_attachments 'TOPIC_ID' => $row['topic_id'], 'S_IN_MESSAGE' => $row['in_message'], - 'S_LOCKED' => !$row['in_message'] && !$auth->acl_get('m_edit', $row['forum_id']) && ($row['forum_status'] == ITEM_LOCKED || $row['topic_status'] == ITEM_LOCKED || $row['post_edit_locked']), + 'S_LOCKED' => !$this->can_delete_file($row), 'U_VIEW_ATTACHMENT' => $controller_helper->route( 'phpbb_storage_attachment', @@ -228,4 +233,29 @@ class ucp_attachments $this->tpl_name = 'ucp_attachments'; $this->page_title = 'UCP_ATTACHMENTS'; } + + /** + * Check if the user can delete the file + * + * @param array $row + * + * @return bool True if user can delete the file, false if not + */ + private function can_delete_file(array $row): bool + { + global $auth, $config; + + if ($row['in_message']) + { + return ($row['message_time'] > time() - ($config['pm_edit_time'] * 60) || !$config['pm_edit_time']) && $row['folder_id'] == PRIVMSGS_OUTBOX && $auth->acl_get('u_pm_edit'); + } + else + { + $can_edit_time = !$config['edit_time'] || $row['post_time'] > time() - ($config['edit_time'] * 60); + $can_delete_time = !$config['delete_time'] || $row['post_time'] > time() - ($config['delete_time'] * 60); + $item_locked = !$auth->acl_get('m_edit', $row['forum_id']) && ($row['forum_status'] == ITEM_LOCKED || $row['topic_status'] == ITEM_LOCKED || $row['post_edit_locked']); + + return !$item_locked && $can_edit_time && $can_delete_time; + } + } } diff --git a/phpBB/includes/ucp/ucp_pm_viewmessage.php b/phpBB/includes/ucp/ucp_pm_viewmessage.php index b2f0bc5344..060a9941b3 100644 --- a/phpBB/includes/ucp/ucp_pm_viewmessage.php +++ b/phpBB/includes/ucp/ucp_pm_viewmessage.php @@ -213,6 +213,8 @@ function view_message($id, $mode, $folder_id, $msg_id, $folder, $message_row) $u_jabber = append_sid("{$phpbb_root_path}memberlist.$phpEx", 'mode=contact&action=jabber&u=' . $author_id); } + $can_edit_pm = ($message_row['message_time'] > time() - ($config['pm_edit_time'] * 60) || !$config['pm_edit_time']) && $folder_id == PRIVMSGS_OUTBOX && $auth->acl_get('u_pm_edit'); + $msg_data = array( 'MESSAGE_AUTHOR_FULL' => get_username_string('full', $author_id, $user_info['username'], $user_info['user_colour'], $user_info['username']), 'MESSAGE_AUTHOR_COLOUR' => get_username_string('colour', $author_id, $user_info['username'], $user_info['user_colour'], $user_info['username']), @@ -252,7 +254,7 @@ function view_message($id, $mode, $folder_id, $msg_id, $folder, $message_row) 'U_EMAIL' => $user_info['email'], 'U_REPORT' => ($config['allow_pm_report']) ? $phpbb_container->get('controller.helper')->route('phpbb_report_pm_controller', array('id' => $message_row['msg_id'])) : '', 'U_QUOTE' => ($auth->acl_get('u_sendpm') && $author_id != ANONYMOUS) ? "$url&mode=compose&action=quote&f=$folder_id&p=" . $message_row['msg_id'] : '', - 'U_EDIT' => (($message_row['message_time'] > time() - ($config['pm_edit_time'] * 60) || !$config['pm_edit_time']) && $folder_id == PRIVMSGS_OUTBOX && $auth->acl_get('u_pm_edit')) ? "$url&mode=compose&action=edit&f=$folder_id&p=" . $message_row['msg_id'] : '', + 'U_EDIT' => $can_edit_pm ? "$url&mode=compose&action=edit&f=$folder_id&p=" . $message_row['msg_id'] : '', 'U_POST_REPLY_PM' => ($auth->acl_get('u_sendpm') && $author_id != ANONYMOUS) ? "$url&mode=compose&action=reply&f=$folder_id&p=" . $message_row['msg_id'] : '', 'U_POST_REPLY_ALL' => ($auth->acl_get('u_sendpm') && $author_id != ANONYMOUS) ? "$url&mode=compose&action=reply&f=$folder_id&reply_to_all=1&p=" . $message_row['msg_id'] : '', 'U_PREVIOUS_PM' => "$url&f=$folder_id&p=" . $message_row['msg_id'] . "&view=previous", diff --git a/tests/functional/ucp_attachments_test.php b/tests/functional/ucp_attachments_test.php new file mode 100644 index 0000000000..d8aac88b49 --- /dev/null +++ b/tests/functional/ucp_attachments_test.php @@ -0,0 +1,374 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +/** + * @group functional + */ +class phpbb_functional_ucp_attachments_test extends phpbb_functional_test_case +{ + private $path; + + protected function setUp(): void + { + parent::setUp(); + $this->path = __DIR__ . '/fixtures/files/'; + $this->add_lang('posting'); + + if (!$this->user_exists('ucp-file-test')) + { + $this->create_user('ucp-file-test'); + } + } + + protected function tearDown(): void + { + $iterator = new DirectoryIterator(__DIR__ . '/../../phpBB/files/'); + foreach ($iterator as $fileinfo) + { + if ($fileinfo->isDot() + || $fileinfo->isDir() + || $fileinfo->getFilename() === 'index.htm' + || $fileinfo->getFilename() === '.htaccess' + ) + { + continue; + } + + unlink($fileinfo->getPathname()); + } + } + + private function upload_file($filename, $mimetype) + { + $crawler = self::$client->request( + 'GET', + 'posting.php?mode=reply&f=2&t=1&sid=' . $this->sid + ); + + $file_form_data = array_merge(['add_file' => $this->lang('ADD_FILE')], $this->get_hidden_fields($crawler, 'posting.php?mode=reply&f=2&t=1&sid=' . $this->sid)); + + $file = array( + 'tmp_name' => $this->path . $filename, + 'name' => $filename, + 'type' => $mimetype, + 'size' => filesize($this->path . $filename), + 'error' => UPLOAD_ERR_OK, + ); + + $crawler = self::$client->request( + 'POST', + 'posting.php?mode=reply&t=1&sid=' . $this->sid, + $file_form_data, + array('fileupload' => $file) + ); + + return $crawler; + } + + private function upload_file_pm($filename, $mimetype) + { + $crawler = self::$client->request( + 'GET', + 'ucp.php?i=pm&mode=compose&sid=' . $this->sid + ); + + $file_form_data = array_merge(['add_file' => $this->lang('ADD_FILE')], $this->get_hidden_fields($crawler, 'ucp.php?i=pm&mode=compose&sid=' . $this->sid)); + + $file = array( + 'tmp_name' => $this->path . $filename, + 'name' => $filename, + 'type' => $mimetype, + 'size' => filesize($this->path . $filename), + 'error' => UPLOAD_ERR_OK, + ); + + $crawler = self::$client->request( + 'POST', + 'ucp.php?i=pm&mode=compose&sid=' . $this->sid, + $file_form_data, + array('fileupload' => $file) + ); + + return $crawler; + } + + public function test_ucp_list_attachments() + { + $this->login('ucp-file-test'); + $this->add_lang(['common', 'posting']); + $crawler = $this->upload_file('valid.jpg', 'image/jpeg'); + + // Ensure there was no error message rendered + $this->assertStringNotContainsString('

' . $this->lang('INFORMATION') . '

', $this->get_content()); + + // Also the file name should be in the first row of the files table + $this->assertEquals('valid.jpg', $crawler->filter('span.file-name > a')->text()); + + $attach_link = $crawler->filter('span.file-name > a')->attr('href'); + $attach_id = $this->get_parameter_from_link($attach_link, 'id'); + + // Submit post + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'message' => 'This is a test', + ]); + $crawler = self::submit($form); + + $this->assertStringContainsString('This is a test', $crawler->text()); + $this->assertEquals('valid.jpg', $crawler->filter('img.postimage')->attr('alt')); + + // Navigate to ucp attachments for user + $crawler = self::request('GET', 'ucp.php?i=ucp_attachments&mode=attachments&sid=' . $this->sid); + $this->assertEquals(1, $crawler->filter('.attachment-filename')->count()); + + $attachment_filename = $crawler->filter('.attachment-filename'); + $this->assertEquals('valid.jpg', $attachment_filename->attr('title')); + $this->assertStringContainsString('download/file.php?id=' . $attach_id, $attachment_filename->attr('href')); + $this->assertEquals('', $crawler->filter('[name="attachment[' . $attach_id . ']"]')->attr('disabled')); + } + + public function test_ucp_delete_expired_attachment() + { + $this->login('ucp-file-test'); + $this->add_lang(['common', 'posting']); + + $this->set_flood_interval(0); + + $crawler = $this->upload_file('valid.jpg', 'image/jpeg'); + + // Ensure there was no error message rendered + $this->assertStringNotContainsString('

' . $this->lang('INFORMATION') . '

', $this->get_content()); + + // Also the file name should be in the first row of the files table + $this->assertEquals('valid.jpg', $crawler->filter('span.file-name > a')->text()); + + $attach_link = $crawler->filter('span.file-name > a')->attr('href'); + $attach_id = $this->get_parameter_from_link($attach_link, 'id'); + + // Submit post + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'message' => 'This is a test', + ]); + $crawler = self::submit($form); + $post_url = $crawler->getUri(); + $post_id = $this->get_parameter_from_link($post_url, 'p'); + + $this->assertStringContainsString('This is a test', $crawler->text()); + $this->assertEquals('valid.jpg', $crawler->filter('img.postimage')->attr('alt')); + $this->set_flood_interval(15); + + // Navigate to ucp attachments for user + $crawler = self::request('GET', 'ucp.php?i=ucp_attachments&mode=attachments&sid=' . $this->sid); + $crawler->filter('.attachment-filename')->each(function ($node, $i) use ($attach_id, &$attachment_node) + { + if (strpos($node->attr('href'), 'file.php?id=' . $attach_id) !== false) + { + $attachment_node = $node; + } + }); + $this->assertNotNull($attachment_node); + + $this->assertEquals('valid.jpg', $attachment_node->attr('title')); + $this->assertStringContainsString('download/file.php?id=' . $attach_id, $attachment_node->attr('href')); + + $this->logout(); + + // Switch to admin user + $this->login(); + $this->admin_login(); + $this->add_lang(['acp/board', 'acp/common']); + + $crawler = self::request('GET', 'adm/index.php?i=acp_board&mode=post&sid=' . $this->sid); + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'config[edit_time]' => 1, + 'config[delete_time]' => 1, + ]); + + self::submit($form); + + // Update post time to at least one minute before current time + $sql = 'UPDATE ' . POSTS_TABLE . ' + SET post_time = post_time - ' . 60 . ' + WHERE post_id = ' . (int) $post_id; + $this->db->sql_query($sql); + + // Log out and back in as test user + $this->logout(); + $this->login('ucp-file-test'); + + // Navigate to ucp attachments for user, deleting should be disabled + $crawler = self::request('GET', 'ucp.php?i=ucp_attachments&mode=attachments&sid=' . $this->sid); + $crawler->filter('.attachment-filename')->each(function ($node, $i) use ($attach_id, &$attachment_node) + { + if (strpos($node->attr('href'), 'file.php?id=' . $attach_id) !== false) + { + $attachment_node = $node; + } + }); + + $this->assertEquals('valid.jpg', $attachment_node->attr('title')); + $this->assertStringContainsString('download/file.php?id=' . $attach_id, $attachment_node->attr('href')); + $this->assertEquals('disabled', $crawler->filter('[name="attachment[' . $attach_id . ']"]')->attr('disabled')); + + // It should not be possible to delete the attachment + $crawler = self::request('POST', 'ucp.php?i=ucp_attachments&mode=attachments&sid=' . $this->sid, [ + 'delete' => true, + 'attachment[' . $attach_id . ']' => $attach_id, + ]); + + $this->assertNotContainsLang('DELETE_ATTACHMENT_CONFIRM', $crawler->text()); + + $crawler->filter('.attachment-filename')->each(function ($node, $i) use ($attach_id, &$attachment_node) + { + if (strpos($node->attr('href'), 'file.php?id=' . $attach_id) !== false) + { + $attachment_node = $node; + } + }); + $this->assertEquals('valid.jpg', $attachment_node->attr('title')); + $this->assertStringContainsString('download/file.php?id=' . $attach_id, $attachment_node->attr('href')); + $this->assertEquals('disabled', $crawler->filter('[name="attachment[' . $attach_id . ']"]')->attr('disabled')); + + $this->logout(); + + // Switch to admin user + $this->login(); + $this->admin_login(); + $this->add_lang(['acp/board', 'acp/common']); + + $crawler = self::request('GET', 'adm/index.php?i=acp_board&mode=post&sid=' . $this->sid); + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'config[edit_time]' => 0, + 'config[delete_time]' => 0, + ]); + + self::submit($form); + + // Update post time to original one + $sql = 'UPDATE ' . POSTS_TABLE . ' + SET post_time = post_time + ' . 60 . ' + WHERE post_id = ' . (int) $post_id; + $this->db->sql_query($sql); + } + + public function test_pm_attachment() + { + $this->set_flood_interval(0); + // Switch to admin user + $this->login(); + $this->admin_login(); + $this->add_lang(['common', 'acp/attachments', 'acp/board', 'acp/common']); + + $crawler = self::request('GET', 'adm/index.php?i=acp_attachments&mode=attach&sid=' . $this->sid); + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'config[allow_pm_attach]' => 1, + ]); + self::submit($form); + + $crawler = self::request('GET', 'adm/index.php?i=acp_board&mode=message&sid=' . $this->sid); + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'config[pm_edit_time]' => 60, + ]); + self::submit($form); + + $crawler = self::request('GET', 'adm/index.php?i=acp_attachments&mode=ext_groups&action=edit&g=1&sid=' . $this->sid); + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'allow_in_pm' => 1, + ]); + self::submit($form); + + $this->logout(); + $this->login('ucp-file-test'); + $this->add_lang(['ucp']); + $crawler = $this->upload_file_pm('valid.jpg', 'image/jpeg'); + + $attach_link = $crawler->filter('span.file-name > a')->attr('href'); + $attach_id = $this->get_parameter_from_link($attach_link, 'id'); + + $form = $crawler->selectButton($this->lang('ADD'))->form([ + 'username_list' => 'admin' + ]); + $crawler = self::submit($form); + + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'subject' => 'Test PM', + 'message' => 'This is a test', + ]); + $crawler = self::submit($form); + + $this->assertContainsLang('MESSAGE_STORED', $crawler->text()); + $refresh_data = explode(';', $crawler->filterXpath("//meta[@http-equiv='refresh']")->extract('content')[0]); + $pm_url = trim($refresh_data[1]); + + $pm_id = $this->get_parameter_from_link($pm_url, 'p'); + + // Navigate to ucp attachments for user + $crawler = self::request('GET', 'ucp.php?i=ucp_attachments&mode=attachments&sid=' . $this->sid); + $crawler->filter('.attachment-filename')->each(function ($node, $i) use ($attach_id, &$attachment_node) + { + if (strpos($node->attr('href'), 'file.php?id=' . $attach_id) !== false) + { + $attachment_node = $node; + } + }); + $this->assertNotNull($attachment_node); + + $this->assertEquals('valid.jpg', $attachment_node->attr('title')); + $this->assertStringContainsString('download/file.php?id=' . $attach_id, $attachment_node->attr('href')); + $this->assertEquals('', $crawler->filter('[name="attachment[' . $attach_id . ']"]')->attr('disabled')); + + // Update message time to 60 minutes later + $sql = 'UPDATE ' . PRIVMSGS_TABLE . ' + SET message_time = message_time - ' . 60 * 60 . ' + WHERE msg_id = ' . (int) $pm_id; + $this->db->sql_query($sql); + + $crawler = self::request('GET', 'ucp.php?i=ucp_attachments&mode=attachments&sid=' . $this->sid); + $crawler->filter('.attachment-filename')->each(function ($node, $i) use ($attach_id, &$attachment_node) + { + if (strpos($node->attr('href'), 'file.php?id=' . $attach_id) !== false) + { + $attachment_node = $node; + } + }); + $this->assertNotNull($attachment_node); + + $this->assertEquals('valid.jpg', $attachment_node->attr('title')); + $this->assertStringContainsString('download/file.php?id=' . $attach_id, $attachment_node->attr('href')); + $this->assertEquals('disabled', $crawler->filter('[name="attachment[' . $attach_id . ']"]')->attr('disabled')); + + $this->set_flood_interval(15); + + // Switch to admin user and disable extra settings again + $this->logout(); + $this->login(); + $this->admin_login(); + $this->add_lang(['common', 'acp/attachments', 'acp/board', 'acp/common']); + + $crawler = self::request('GET', 'adm/index.php?i=acp_attachments&mode=attach&sid=' . $this->sid); + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'config[allow_pm_attach]' => 0, + ]); + self::submit($form); + + $crawler = self::request('GET', 'adm/index.php?i=acp_board&mode=message&sid=' . $this->sid); + $form = $crawler->selectButton($this->lang('SUBMIT'))->form([ + 'config[pm_edit_time]' => 0, + ]); + self::submit($form); + + $crawler = self::request('GET', 'adm/index.php?i=acp_attachments&mode=ext_groups&action=edit&g=1&sid=' . $this->sid); + $form = $crawler->selectButton($this->lang('SUBMIT'))->form(); + $form['allow_in_pm']->untick(); + self::submit($form); + } +}