diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/acp/main_info.php b/acp/main_info.php index 0ed343b..4527889 100755 --- a/acp/main_info.php +++ b/acp/main_info.php @@ -4,18 +4,18 @@ namespace pedodev\tagging\acp; class main_info { - public function module() - { - return [ - 'filename' => '\pedodev\tagging\acp\main_module', - 'title' => 'ACP_TAGGING_TITLE', - 'modes' => [ - 'settings' => [ - 'title' => 'ACP_TAGGING_SETTINGS', - 'auth' => 'ext_pedodev/tagging && acl_a_board', - 'cat' => ['ACP_TAGGING_TITLE'], - ], - ], - ]; - } + public function module() + { + return [ + 'filename' => '\pedodev\tagging\acp\main_module', + 'title' => 'ACP_TAGGING_TITLE', + 'modes' => [ + 'settings' => [ + 'title' => 'ACP_TAGGING_SETTINGS', + 'auth' => 'ext_pedodev/tagging && acl_a_board', + 'cat' => ['ACP_TAGGING_TITLE'], + ], + ], + ]; + } } diff --git a/acp/main_module.php b/acp/main_module.php index 5016144..b2d4089 100755 --- a/acp/main_module.php +++ b/acp/main_module.php @@ -6,13 +6,14 @@ use phpbb\request\request_interface; class main_module { - public $u_action, $tpl_name, $page_title; - private $config, $request, $template, $user, $language, $tag_helper; - private array $tag_list; - private string $tag_list_filepath; + public $u_action, $tpl_name, $page_title; + private $config, $request, $template, $user, $language, $tag_helper; + private array $tag_list; + private string $tag_list_filepath; + private bool $tag_search_available; - public function main(string $id, string $mode): void - { + public function main(string $id, string $mode): void + { global $phpbb_container; $this->config = $phpbb_container->get('config'); @@ -23,8 +24,9 @@ class main_module $this->tag_helper = $phpbb_container->get('pedodev.tagging.tag_helper'); $this->page_title = $this->language->lang('ACP_TAGGING_TITLE'); - $this->tpl_name = 'acp_tagging_body'; - $this->tag_list = $this->tag_helper->get_tag_list(); + $this->tpl_name = 'acp_tagging_body'; + $this->tag_list = $this->tag_helper->get_tag_list(); + $this->tag_search_available = ($this->config['search_type'] == '\phpbb\search\fulltext_native' or $this->config['search_type'] == '\phpbb\search\fulltext_mysql'); $action = $this->request->variable('action', '', $super_global = request_interface::GET); @@ -43,12 +45,21 @@ class main_module $this->load_main_page(); break; } - } - - private function load_edit_page() - { - $form_key = 'pedodev_tagging_edit_tag'; + } + private function get_tag_data(): array + { + return array( + 'title' => $this->request->variable('tagging_tagtitle', ''), + 'color' => ltrim($this->request->variable('tagging_tagcolor', ''), '#'), + 'active' => $this->request->variable('tagging_tagactive', false), + 'searchable' => $this->request->variable('tagging_tagsearchable', false), + ); + } + + private function load_edit_page(): void + { + $form_key = 'pedodev_tagging_edit_tag'; add_form_key($form_key); $tag_id = $this->request->variable('tag', -1, request_interface::GET); @@ -59,43 +70,70 @@ class main_module } if ($this->request->is_set_post('submit')) - { - $this->check_form_key($form_key); + { + $this->check_form_key($form_key); - $tag_data = array( - 'title' => $this->request->variable('tagging_tagtitle', ''), - 'color' => $this->request->variable('tagging_tagcolor', ''), - 'active' => $this->request->variable('tagging_tagactive', false), - 'searchable' => $this->request->variable('tagging_tagsearchable', false), - ); - - if (empty($tag_data['title']) || empty($tag_data['color'])) - { - trigger_error($this->language->lang('ACP_TAGGING_TAG_EMPTY') . adm_back_link($this->u_action)); - } + $tag_data = $this->get_tag_data(); - $this->tag_list[$tag_id] = $tag_data; + if (!$this->tag_helper->validate_tag($tag_data)) + { + trigger_error($this->language->lang('ACP_TAGGING_TAG_INVALID') . adm_back_link($this->u_action)); + } + + $this->tag_list[$tag_id] = $tag_data; $this->tag_helper->update_tag_list($this->tag_list); - trigger_error($this->language->lang('ACP_TAGGING_TAG_EDITED', $tag_data['title']) . adm_back_link($this->u_action)); - } + trigger_error($this->language->lang('ACP_TAGGING_TAG_EDITED', $tag_data['title']) . adm_back_link($this->u_action)); + } - $tag = $this->tag_list[$tag_id]; + $tag = $this->tag_list[$tag_id]; $this->template->assign_vars([ - 'EDIT' => 1, + 'EDIT' => 1, 'TAGGING_PAGE_TITLE' => $this->language->lang('ACP_TAGGING_SETTINGS_EDIT'), - 'EDIT_TAG_NAME' => $tag['title'], - 'TAG_COLOR' => $tag['color'], - 'TAG_ACTIVE' => $tag['active'], - 'TAG_SEARCHABLE' => $tag['searchable'], + 'EDIT_TAG_NAME' => $tag['title'], + 'TAG_COLOR' => $tag['color'], + 'TAG_ACTIVE' => $tag['active'], + 'TAG_SEARCHABLE' => $tag['searchable'], ]); - } + } - private function load_delete_page() - { - $form_key = 'pedodev_tagging_delete_tag'; + private function load_add_page(): void + { + $form_key = 'pedodev_tagging_add_tag'; + add_form_key($form_key); + + if ($this->request->is_set_post('submit')) + { + $this->check_form_key($form_key); + + $tag_data = $this->get_tag_data(); + if (!$this->tag_helper->validate_tag($tag_data)) + { + trigger_error($this->language->lang('ACP_TAGGING_TAG_INVALID') . adm_back_link($this->u_action)); + } + + $this->tag_list[] = $tag_data; + $this->tag_helper->update_tag_list($this->tag_list); + + trigger_error($this->language->lang('ACP_TAGGING_TAG_ADDED', $tag_data['title']) . adm_back_link($this->u_action)); + } + + $random_color = substr(md5(rand()), 0, 6); + + $this->template->assign_vars([ + 'EDIT' => 1, + 'TAGGING_PAGE_TITLE' => $this->language->lang('ACP_TAGGING_SETTINGS_ADD'), + 'TAG_COLOR' => $random_color, + 'TAG_ACTIVE' => true, + 'TAG_SEARCHABLE' => true, + ]); + } + + private function load_delete_page(): void + { + $form_key = 'pedodev_tagging_delete_tag'; add_form_key($form_key); $tag_id = $this->request->variable('tag', -1, request_interface::GET); @@ -108,14 +146,14 @@ class main_module $tag = $this->tag_list[$tag_id]; if ($this->request->is_set_post('submit')) - { - $this->check_form_key($form_key); + { + $this->check_form_key($form_key); - unset($this->tag_list[$tag_id]); + unset($this->tag_list[$tag_id]); $this->tag_helper->update_tag_list($this->tag_list); - trigger_error($this->language->lang('ACP_TAGGING_TAG_DELETED', $tag['title']) . adm_back_link($this->u_action)); - } + trigger_error($this->language->lang('ACP_TAGGING_TAG_DELETED', $tag['title']) . adm_back_link($this->u_action)); + } $this->template->assign_vars([ 'DELETE' => 1, @@ -123,53 +161,13 @@ class main_module 'DELETE_CONFIRMATION' => $this->language->lang('ACP_TAGGING_DELETE_CONFIRMATION', $tag['title']), ]); } - - private function load_add_page() - { - $form_key = 'pedodev_tagging_add_tag'; - add_form_key($form_key); - - if ($this->request->is_set_post('submit')) - { - $this->check_form_key($form_key); - - $new_tag = $this->request->variable('tagging_tagtitle', ''); - - if (empty($new_tag)) - { - trigger_error($this->language->lang('ACP_TAGGING_TAG_EMPTY') . adm_back_link($this->u_action)); - } - - $this->tag_list[] = array( - 'title' => $new_tag, - 'color' => $this->request->variable('tagging_tagcolor', ''), - 'active' => $this->request->variable('tagging_tagactive', false), - 'searchable' => $this->request->variable('tagging_tagsearchable', false), - ); - - $this->tag_helper->update_tag_list($this->tag_list); - - trigger_error($this->language->lang('ACP_TAGGING_TAG_ADDED', $new_tag) . adm_back_link($this->u_action)); - } + private function load_main_page(): void + { + $form_key = 'pedodev_tagging_main'; + add_form_key($form_key); - $random_color = '#' . substr(md5(rand()), 0, 6); - - $this->template->assign_vars([ - 'EDIT' => 1, - 'TAGGING_PAGE_TITLE' => $this->language->lang('ACP_TAGGING_SETTINGS_ADD',), - 'TAG_COLOR' => $random_color, - 'TAG_ACTIVE' => true, - 'TAG_SEARCHABLE' => true, - ]); - } - - private function load_main_page() - { - $form_key = 'pedodev_tagging_main'; - add_form_key($form_key); - - $id = 0; + $id = 0; if ($this->request->is_set_post('submit')) { @@ -177,7 +175,7 @@ class main_module $this->config->set('pedodev_tagging_tagthreads', $this->request->variable('tagging_tagthreads', false)); $this->config->set('pedodev_tagging_tagposts', $this->request->variable('tagging_tagposts', false)); $this->config->set('pedodev_tagging_maxtags', $this->request->variable('tagging_maxtags', 0)); - $this->config->set('pedodev_tagging_tagsearch', $this->request->variable('tagging_tagsearch', false)); + $this->config->set('pedodev_tagging_tagsearch', ($this->request->variable('tagging_tagsearch', false) and $this->tag_search_available)); $this->config->set('pedodev_tagging_viewtopic', $this->request->variable('tagging_viewtopic', false)); $this->config->set('pedodev_tagging_viewforum', $this->request->variable('tagging_viewforum', false)); $this->config->set('pedodev_tagging_results', $this->request->variable('tagging_results', false)); @@ -187,37 +185,38 @@ class main_module foreach ($this->tag_list as $id => $tag) { + $id = (int)$id; + $this->template->assign_block_vars('tag_list', [ - 'TITLE' => $tag['title'], - 'COLOR' => $tag['color'], - 'ACTIVE' => $tag['active'], + 'TITLE' => $tag['title'], + 'COLOR' => $tag['color'], + 'ACTIVE' => $tag['active'], 'SEARCHABLE' => $tag['searchable'], - 'EDIT_LINK' => $this->u_action . "&tag={$id}&action=edit", + 'EDIT_LINK' => $this->u_action . "&tag={$id}&action=edit", 'DELETE_LINK' => $this->u_action . "&tag={$id}&action=delete", ]); } - - $id++; - + $this->template->assign_vars([ 'TAGGING_PAGE_TITLE' => $this->language->lang('ACP_TAGGING_SETTINGS'), - 'U_ACTION' => $this->u_action, - 'TAGGING_ADD_TAG' => $this->u_action . "&tag={$id}&action=add", - 'TAG_THREADS' => $this->config['pedodev_tagging_tagthreads'], - 'TAG_POSTS' => $this->config['pedodev_tagging_tagposts'], - 'MAX_TAGS' => (int)$this->config['pedodev_tagging_maxtags'], - 'TAG_SEARCH' => $this->config['pedodev_tagging_tagsearch'], - 'TAG_VIEWTOPIC' => $this->config['pedodev_tagging_viewtopic'], - 'TAG_VIEWFORUM' => $this->config['pedodev_tagging_viewforum'], - 'TAG_RESULTS' => $this->config['pedodev_tagging_results'], - ]); - } + 'U_ACTION' => $this->u_action, + 'TAGGING_ADD_TAG' => $this->u_action . "&action=add", + 'TAG_THREADS' => $this->config['pedodev_tagging_tagthreads'], + 'TAG_POSTS' => $this->config['pedodev_tagging_tagposts'], + 'MAX_TAGS' => (int)$this->config['pedodev_tagging_maxtags'], + 'TAG_SEARCH' => $this->config['pedodev_tagging_tagsearch'], + 'TAG_VIEWTOPIC' => $this->config['pedodev_tagging_viewtopic'], + 'TAG_VIEWFORUM' => $this->config['pedodev_tagging_viewforum'], + 'TAG_RESULTS' => $this->config['pedodev_tagging_results'], + 'TAG_SEARCH_UNAVAILABLE' => !$this->tag_search_available, + ]); + } - private function check_form_key($form_key) - { + private function check_form_key($form_key): void + { if (!check_form_key($form_key)) { trigger_error('FORM_INVALID'); } - } + } } diff --git a/adm/style/acp_tagging_edit.html b/adm/style/acp_tagging_edit.html index 69b2795..8929f32 100755 --- a/adm/style/acp_tagging_edit.html +++ b/adm/style/acp_tagging_edit.html @@ -15,7 +15,7 @@
{{ lang('ACP_TAGGING_TAGCOLOR_EXPLANATION') }}
- +
@@ -47,4 +47,4 @@ {{ S_FORM_TOKEN }} - \ No newline at end of file + diff --git a/adm/style/acp_tagging_main.html b/adm/style/acp_tagging_main.html index 83a65be..553b576 100755 --- a/adm/style/acp_tagging_main.html +++ b/adm/style/acp_tagging_main.html @@ -22,7 +22,7 @@ {% for TAG in loops.tag_list %} {{ TAG.TITLE }} - + {% if TAG.ACTIVE %}{{ lang('YES') }}{% else %}{{ lang('NO') }}{% endif %} {% if TAG.SEARCHABLE %}{{ lang('YES') }}{% else %}{{ lang('NO') }}{% endif %} {{ lang('ACP_TAGGING_EDIT') }} @@ -73,15 +73,19 @@
{{ lang('ACP_TAGGING_SEARCH') }} + {% if TAG_SEARCH_UNAVAILABLE %} + {{ lang('ACP_TAGGING_TAGSEARCH_UNAVAILABLE') }} + {% else %}

{{ lang('ACP_TAGGING_TAGSEARCH_EXPLANATION') }}
- - + +
+ {% endif %}
@@ -125,4 +129,4 @@ {{ S_FORM_TOKEN }}
- \ No newline at end of file + diff --git a/config/services.yml b/config/services.yml index f076634..8355f04 100755 --- a/config/services.yml +++ b/config/services.yml @@ -3,11 +3,15 @@ services: class: pedodev\tagging\core\tag_helper arguments: - '@dbal.conn' + - '@config' + - '%core.root_path%' + - '%core.table_prefix%' pedodev.tagging.search_helper: class: pedodev\tagging\core\search_helper arguments: - '@dbal.conn' + - '@config' - '@pedodev.tagging.tag_helper' pedodev.tagging.request_helper: @@ -35,14 +39,15 @@ services: - '@pedodev.tagging.search_helper' - '@pedodev.tagging.request_helper' - pedodev.tagging.search_tag_listener: - class: pedodev\tagging\event\search_tag_listener + pedodev.tagging.search_listener: + class: pedodev\tagging\event\search_listener tags: - { name: event.listener } arguments: - '@config' - '@template' - '@language' + - '@auth' - '@pedodev.tagging.tag_helper' - '@pedodev.tagging.search_helper' - '@pedodev.tagging.request_helper' diff --git a/core/request_helper.php b/core/request_helper.php index 49ff745..bf2ddfb 100755 --- a/core/request_helper.php +++ b/core/request_helper.php @@ -16,7 +16,7 @@ class request_helper private function tag_is_selected(int $tag_id): bool { - return $this->request->variable('tag_' . $tag_id, '', request_interface::GET); + return (bool)$this->request->variable('tag_' . $tag_id, '', request_interface::GET); } public function get_selected_tags(): array @@ -32,9 +32,12 @@ class request_helper { return $this->request->variable('tag_filter', '', request_interface::GET); } - - public function is_keyword_search(): bool + + public function keywords_or_author(): bool { - return (bool)$this->request->variable('keywords', '', request_interface::GET); + $keywords = $this->request->variable('keywords', '', request_interface::GET); + $author = $this->request->variable('author', '', request_interface::GET); + + return !(empty($keywords) and empty($author)); } } diff --git a/core/search_helper.php b/core/search_helper.php index 309d0fc..a63b5ef 100755 --- a/core/search_helper.php +++ b/core/search_helper.php @@ -3,27 +3,36 @@ namespace pedodev\tagging\core; use phpbb\db\driver\factory; +use phpbb\config\config; use pedodev\tagging\core\tag_helper; class search_helper { public function __construct( - private factory $db, + private factory $db, + private config $config, + private tag_helper $tag_helper, ) {} - public function search_post_tags(array $post_list): array + public function search_post_tags(array $post_list, string $mode): array { - if (empty($post_list)) + if (empty($post_list)) return array(); + + if ($mode == 'posts') { - return array(); + $sql = 'SELECT t.post_id, t.tag_id + FROM ' . $this->tag_helper->tag_table . ' t + WHERE t.' . $this->db->sql_in_set('post_id', $post_list); + } + else + { + $sql = 'SELECT t.topic_id as post_id, c.tag_id + FROM ' . $this->tag_helper->tag_table . ' c, ' . TOPICS_TABLE . ' t + WHERE t.topic_first_post_id = c.post_id AND t.' . $this->db->sql_in_set('topic_id', $post_list); } - $sql = 'SELECT t.post_id, t.tag_id - FROM ' . $this->tag_helper->get_tag_table() . ' t - WHERE t.' . $this->db->sql_in_set('post_id', $post_list); - $result = $this->db->sql_query($sql); $post_tags = array(); @@ -33,67 +42,29 @@ class search_helper $post_tags[$row['post_id']][] = $row['tag_id']; } + $this->db->sql_freeresult($result); + return $post_tags; } - - public function search_topic_tags(array $topic_list): array - { - if (empty($topic_list)) - { - return array(); - } - - $sql = 'SELECT t.topic_id, c.tag_id - FROM ' . $this->tag_helper->get_tag_table() . ' c, ' . TOPICS_TABLE . ' t - WHERE t.topic_first_post_id = c.post_id AND t.' . $this->db->sql_in_set('topic_id', $topic_list); - - $result = $this->db->sql_query($sql); - - $topic_tags = array(); - - while ($row = $this->db->sql_fetchrow($result)) - { - $topic_tags[$row['topic_id']][] = $row['tag_id']; - } - return $topic_tags; - } - - public function search_posts_by_tag(array $tag_list, string $filter_mode, string $mode): array - { - if (empty($tag_list)) - { - return array(); - } + public function generate_tag_subquery(array $search_tags, string $tag_filter): ?string + { + // Explicitly cast search tags to integers for security + $search_tags = array_map('intval', $search_tags); + $tag_count = count($search_tags); - if ($mode === 'posts') - { - $sql = 'SELECT post_id, count(*) - FROM ' . $this->tag_helper->get_tag_table() . ' - WHERE ' . $this->db->sql_in_set('tag_id', $tag_list) . ' - GROUP BY post_id'; - } - else if ($mode === 'topics') - { - $sql = 'SELECT t.topic_id, count(*) - FROM ' . $this->tag_helper->get_tag_table() . ' c, ' . TOPICS_TABLE . ' t - WHERE t.topic_first_post_id = c.post_id AND c.' . $this->db->sql_in_set('tag_id', $tag_list) . ' - GROUP BY t.topic_id'; - } + if ($tag_count == 0) return null; - $result = $this->db->sql_query($sql); + $tag_string = implode(',', $search_tags); + $subquery_where = $this->db->sql_in_set('ct.tag_id', $search_tags); - $count = count($tag_list); - $post_list = array(); + $tag_subquery = "SELECT ct.post_id from {$this->tag_helper->tag_table} ct WHERE {$subquery_where}"; - while ($row = $this->db->sql_fetchrow($result)) - { - if ($filter_mode === 'union' || ($filter_mode === 'intersect' && $row['count(*)'] == $count)) - { - $post_list[] = (isset($row['post_id'])) ? (int)$row['post_id'] : (int)$row['topic_id']; - } - } + if ($tag_filter == "intersect" && $tag_count > 1) { + $tag_subquery .= ' GROUP BY ct.post_id HAVING COUNT(ct.post_id) = ' . (int)$tag_count; + } + + return $tag_subquery; + } +} - return $post_list; - } -} \ No newline at end of file diff --git a/core/tag_helper.php b/core/tag_helper.php index a7f1eb5..3fcbf54 100755 --- a/core/tag_helper.php +++ b/core/tag_helper.php @@ -3,6 +3,7 @@ namespace pedodev\tagging\core; use phpbb\db\driver\factory; +use phpbb\config\config; class tag_helper { @@ -10,20 +11,29 @@ class tag_helper private array $active_tag_list; private array $searchable_tag_list; - private string $tag_table; + public readonly string $tag_table; private string $tag_list_filepath; public function __construct( - private factory $db, + private factory $db, + private config $config, + string $phpbb_root_path, + string $table_prefix, ) { - global $table_prefix; - - $this->tag_list_filepath = __DIR__ . '/../taglist.json'; + $this->tag_list_filepath = "{$phpbb_root_path}files/{$this->config['pedodev_tagging_taglist_prefix']}_taglist.json"; $this->tag_table = $this->db->sql_escape($table_prefix . 'content_tags'); $this->load_tag_list(); } - + + public function validate_tag(array $tag): bool + { + return (bool)preg_match("/^[\w ]{1,20}$/", $tag['title']) + and (bool)preg_match('/^[0-9a-f]{6}$/', $tag['color']) + and is_bool($tag['active']) + and is_bool($tag['searchable']); + } + private function load_tag_list(): void { $tag_list_json = ''; @@ -33,22 +43,36 @@ class tag_helper $tag_list_json = file_get_contents($this->tag_list_filepath); } - $tag_list = json_decode($tag_list_json, $associative = true); - $this->tag_list = isset($tag_list) ? $tag_list : array(); - - $this->active_tag_list = array_filter($this->tag_list, function($v) { - return (bool)$v['active']; - }); + $tag_list = json_decode($tag_list_json, $associative = true); - $this->searchable_tag_list = array_filter($this->tag_list, function($v) { - return (bool)$v['searchable']; - }); + // Remove invalid tags + $tag_list = array_filter($tag_list, [$this, 'validate_tag']); + + // Remove tags with non-numeric IDs + $tag_list = array_filter($tag_list, function ($id) { + return is_int($id); + }, ARRAY_FILTER_USE_KEY); + + $this->tag_list = $tag_list ?? array(); + + $this->active_tag_list = array_filter($this->tag_list, function($tag) { + return (bool)$tag['active']; + }); + + $this->searchable_tag_list = array_filter($this->tag_list, function($tag) { + return (bool)$tag['searchable']; + }); } - public function update_tag_list(array $tag_list): bool + public function update_tag_list(array $tag_list): void { + $tag_list = array_filter($tag_list, [$this, 'validate_tag']); $tag_list_json = json_encode($tag_list); - return (bool)file_put_contents($this->tag_list_filepath, $tag_list_json); + + if (!chmod($this->tag_list_filepath, 0600) or !file_put_contents($this->tag_list_filepath, $tag_list_json) or !chmod($this->tag_list_filepath, 0400)) + { + trigger_error('Unable to update taglist. Check your file and ownership permissions', E_USER_ERROR); + } } public function get_tag_list(): array @@ -65,20 +89,15 @@ class tag_helper { return $this->searchable_tag_list; } - - public function get_tag_table(): string - { - return $this->tag_table; - } - + public function get_colored_title(int $tag_id): string { - if (!array_key_exists($tag_id, $this->tag_list)) - { - return ''; - } + if (!array_key_exists($tag_id, $this->tag_list)) return ''; $tag = $this->tag_list[$tag_id]; - return '' . $tag['title'] . ''; + $tag['title'] = htmlspecialchars($tag['title']); + $tag['color'] = htmlspecialchars($tag['color']); + + return "{$tag['title']}"; } } diff --git a/event/permissions.php b/event/permissions.php index 1f7f7e2..8e22500 100755 --- a/event/permissions.php +++ b/event/permissions.php @@ -1,9 +1,5 @@ update_subarray('permissions', 'u_pedodev_tagging_cantagposts', ['lang' => 'ACL_U_PEDODEV_TAGGING_CANTAGPOSTS', 'cat' => 'post']); + $event->update_subarray('permissions', 'u_pedodev_tagging_cantagsearch', ['lang' => 'ACL_U_PEDODEV_TAGGING_CANTAGSEARCH', 'cat' => 'misc']); } } diff --git a/event/posting_listener.php b/event/posting_listener.php index 1f4b4a6..f52b6ab 100755 --- a/event/posting_listener.php +++ b/event/posting_listener.php @@ -91,10 +91,7 @@ class posting_listener implements EventSubscriberInterface $allowed = $this->config['pedodev_tagging_tagposts']; } - if (!$allowed || !$this->auth->acl_get('u_pedodev_tagging_cantagposts')) - { - return; - } + if (!$allowed || !$this->auth->acl_get('u_pedodev_tagging_cantagposts')) return; $post_id = (int)$event['post_id']; @@ -104,7 +101,7 @@ class posting_listener implements EventSubscriberInterface } else if ($post_id != 0) { - $post_tags = $this->search_helper->search_post_tags(array($post_id)); + $post_tags = $this->search_helper->search_post_tags(array($post_id), 'posts'); $post_tags = empty($post_tags) ? array() : current($post_tags); } else @@ -153,10 +150,7 @@ class posting_listener implements EventSubscriberInterface private function insert_post_tags(int $post_id): void { - if ($post_id <= 0) - { - return; - } + if ($post_id <= 0) return; $sql_ary = array(); $active_tag_list = $this->tag_helper->get_active_tag_list(); @@ -172,17 +166,14 @@ class posting_listener implements EventSubscriberInterface } } - $this->db->sql_multi_insert($this->tag_helper->get_tag_table(), $sql_ary); + $this->db->sql_multi_insert($this->tag_helper->tag_table, $sql_ary); } private function delete_post_tags(int $post_id): void { - if ($post_id <= 0) - { - return; - } + if ($post_id <= 0) return; - $sql = 'DELETE FROM ' . $this->tag_helper->get_tag_table() . ' + $sql = 'DELETE FROM ' . $this->tag_helper->tag_table . ' WHERE post_id = ' . (int)$post_id; $this->db->sql_query($sql); diff --git a/event/search_listener.php b/event/search_listener.php new file mode 100755 index 0000000..1f255de --- /dev/null +++ b/event/search_listener.php @@ -0,0 +1,223 @@ + 'assign_presearch_tags', + 'core.search_backend_search_after' => [['tag_search', 1],['fetch_results_tags', 0]], + 'core.search_modify_url_parameters' => 'add_tags_url', + 'core.search_modify_tpl_ary' => 'assign_results_tags', + 'core.search_modify_submit_parameters' => 'load_language', + + 'core.search_native_by_keyword_modify_search_key' => 'pre_cache_assign_tags', + 'core.search_native_keywords_count_query_before' => 'native_keyword_search_with_tags', + 'core.search_native_by_author_modify_search_key' => 'pre_cache_assign_tags', + 'core.search_native_author_count_query_before' => [['authorless_tag_search', 1], ['author_search_with_tags', 0]], + + 'core.search_mysql_by_keyword_modify_search_key' => 'pre_cache_assign_tags', + 'core.search_mysql_keywords_main_query_before' => 'mysql_keyword_search_with_tags', + 'core.search_mysql_by_author_modify_search_key' => 'pre_cache_assign_tags', + 'core.search_mysql_author_query_before' => [['authorless_tag_search', 1], ['author_search_with_tags', 0]], + ]; + } + + private function can_tag_search(): bool + { + return ($this->config['pedodev_tagging_tagsearch'] and $this->auth->acl_get('u_pedodev_tagging_cantagsearch')); + } + + public function load_language(object $event): void + { + $this->language->add_lang('tagging_search', 'pedodev/tagging'); + } + + public function assign_presearch_tags(object $event): void + { + if (!$this->can_tag_search()) return; + + $active_tag_list = $this->tag_helper->get_searchable_tag_list(); + + $this->template->assign_var('TAGGING_ALLOWED', 1); + + foreach ($active_tag_list as $id => $tag) + { + $this->template->assign_block_vars('tags', [ + 'ID' => $id, + 'TITLE' => $tag['title'], + 'COLOR' => $tag['color'], + ]); + } + } + + public function fetch_results_tags(object $event): void + { + if (!$this->config['pedodev_tagging_results']) return; + + $this->post_tags = $this->search_helper->search_post_tags($event['id_ary'], $event['show_results']); + } + + public function add_tags_url(object $event): void + { + if (!$this->can_tag_search()) return; + + $tag_ids = $this->request_helper->get_selected_tags(); + if (empty($tag_ids)) return; + + $u_search = $event['u_search']; + + $u_search .= '&submit=Search'; + $u_search .= '&tag_' . implode('=1&tag_', $tag_ids) . '=1'; + $u_search .= '&tag_filter=' . $this->request_helper->get_tag_filter(); + + $event['u_search'] = $u_search; + } + + public function assign_results_tags(object $event): void + { + if (!$this->config['pedodev_tagging_results']) return; + + $post_row = $event['tpl_ary']; + $post_id = $post_row['POST_ID'] ? (int)$post_row['POST_ID'] : (int)$post_row['TOPIC_ID']; + + if (array_key_exists($post_id, $this->post_tags)) + { + $post_tags = $this->post_tags[$post_id]; + $tags = implode(' ', array_map([$this->tag_helper, 'get_colored_title'], $post_tags)); + $post_row['POST_TAGS'] = $tags; + } + + $event['tpl_ary'] = $post_row; + } + + private function get_tag_string(): string + { + $search_tags = $this->request_helper->get_selected_tags(); + $tag_filter = $this->request_helper->get_tag_filter(); + + $search_tags_string = implode(',', $search_tags); + $tag_string = "tags({$search_tags_string})"; + + $tag_string .= (count($search_tags) > 1) ? $tag_filter : ''; + + return $tag_string; + + } + + public function pre_cache_assign_tags(object $event): void + { + if (!$this->can_tag_search()) return; + + $search_key_array = $event['search_key_array']; + $search_key_array[] = $this->get_tag_string(); + $event['search_key_array'] = $search_key_array; + } + + // thread search not working? + public function native_keyword_search_with_tags(object $event): void + { + if (!$this->can_tag_search()) return; + + $search_tags = $this->request_helper->get_selected_tags(); + $tag_filter = $this->request_helper->get_tag_filter(); + + $tag_subquery = $this->search_helper->generate_tag_subquery($search_tags, $tag_filter); + + if (is_null($tag_subquery)) return; + + $sql_where = $event['sql_where']; + $sql_where[] = "p.post_id IN ({$tag_subquery})"; + $event['sql_where'] = $sql_where; + } + + public function author_search_with_tags(object $event): void + { + if (!$this->can_tag_search()) return; + + $search_tags = $this->request_helper->get_selected_tags(); + $tag_filter = $this->request_helper->get_tag_filter(); + + $tag_subquery = $this->search_helper->generate_tag_subquery($search_tags, $tag_filter); + + if (is_null($tag_subquery)) return; + + $sql_author = $event['sql_author']; + $sql_author .= " AND p.post_id IN ({$tag_subquery})"; + $event['sql_author'] = $sql_author; + } + + public function mysql_keyword_search_with_tags(object $event): void + { + if (!$this->can_tag_search()) return; + + $search_tags = $this->request_helper->get_selected_tags(); + $tag_filter = $this->request_helper->get_tag_filter(); + + $tag_subquery = $this->search_helper->generate_tag_subquery($search_tags, $tag_filter); + + if (is_null($tag_subquery)) return; + + $sql_match_where = $event['sql_match_where']; + $sql_match_where = " AND p.post_id IN ({$tag_subquery})"; + $event['sql_match_where'] = $sql_match_where; + } + + public function tag_search(object $event): void + { + global $search; + + if (!$this->can_tag_search() or $this->request_helper->keywords_or_author()) return; + + $search_tags = $this->request_helper->get_selected_tags(); + if (empty($search_tags)) return; + + $id_ary = array(); + $start = $event['start']; + + $total_results = $search->author_search($event['show_results'], true, $event['sort_by_sql'], $event['sort_key'], + $event['sort_dir'], $event['sort_days'], $event['ex_fid_ary'], $event['m_approve_posts_fid_sql'], + $event['topic_id'], array(0), '', $id_ary, $start, $event['per_page'] + ); + + $event['id_ary'] = $id_ary; + $event['total_match_count'] = $total_results; + $event['start'] = $start; + } + + public function authorless_tag_search(object $event): void + { + if (!$this->can_tag_search() or $event['sql_author'] != 'p.poster_id = 0') return; + + $event['sql_author'] = '1 = 1'; + $event['firstpost_only'] = false; + $event['sql_firstpost'] = ''; + } +} diff --git a/event/search_tag_listener.php b/event/search_tag_listener.php deleted file mode 100755 index c2f3a77..0000000 --- a/event/search_tag_listener.php +++ /dev/null @@ -1,166 +0,0 @@ - 'assign_presearch_tags', -// 'core.search_backend_search_after' => [['fetch_results_tags', 0], ['search_by_tag', 1]], - 'core.search_modify_url_parameters' => 'add_tags_url', -// 'core.search_modify_tpl_ary' => 'assign_results_tags', - 'core.search_modify_submit_parameters' => 'load_language', - 'core.search_native_keywords_count_query_before' => 'test_func', - ]; - } - - public function load_language(object $event): void - { - $this->language->add_lang('tagging_search', 'pedodev/tagging'); - } - - public function assign_presearch_tags(object $event): void - { - if (!$this->config['pedodev_tagging_tagsearch']) - { - return; - } - - $active_tag_list = $this->tag_helper->get_searchable_tag_list(); - - $this->template->assign_var('TAGGING_ALLOWED', 1); - - foreach ($active_tag_list as $id => $tag) - { - $this->template->assign_block_vars('tags', [ - 'ID' => $id, - 'TITLE' => $tag['title'], - 'COLOR' => $tag['color'], - ]); - } - } - - public function fetch_results_tags(object $event): void - { - if (!$this->config['pedodev_tagging_results']) - { - return; - } - - $this->mode = $event['show_results']; - - if ($this->mode === 'posts') - { - $this->post_tags = $this->search_helper->search_post_tags($event['id_ary']); - } - else if ($this->mode === 'topics') - { - $this->post_tags = $this->search_helper->search_topic_tags($event['id_ary']); - } - } - - public function add_tags_url(object $event): void - { - $tag_ids = $this->request_helper->get_selected_tags(); - - if (empty($tag_ids)) - { - return; - } - - $u_search = $event['u_search']; - $u_search .= '&tag_' . implode('=1&tag_', $tag_ids) . '=1'; - $u_search .= '&tag_filter=' . $this->request_helper->get_tag_filter(); - $event['u_search'] = $u_search; - } - - public function assign_results_tags(object $event): void - { - if (!$this->config['pedodev_tagging_results']) - { - return; - } - - $post_row = $event['tpl_ary']; - $post_id = $post_row['POST_ID'] ? (int)$post_row['POST_ID'] : (int)$post_row['TOPIC_ID']; - - if (array_key_exists($post_id, $this->post_tags)) - { - $post_tags = $this->post_tags[$post_id]; - $tags = implode(' ', array_map([$this->tag_helper, 'get_colored_title'], $post_tags)); - $post_row['POST_TAGS'] = $tags; - } - - $event['tpl_ary'] = $post_row; - } - - public function search_by_tag(object $event): void - { - if (!$this->config['pedodev_tagging_tagsearch']) - { - return; - } - - $id_ary = $event['id_ary']; - - if (empty($id_ary) && $this->request_helper->is_keyword_search()) - { - var_dump("Empty search"); - return; - } - - $tag_ids = $this->request_helper->get_selected_tags(); - - if (empty($tag_ids)) - { - return; - } - - $mode = $event['show_results']; - $tag_filter = $this->request_helper->get_tag_filter(); - - $post_ary = $this->search_helper->search_posts_by_tag($tag_ids, $tag_filter, $mode); - - if (empty($id_ary)) - { - $id_ary = $post_ary; - } - else - { - $id_ary = array_intersect($id_ary, $post_ary); - } - - $event['id_ary'] = $id_ary; - $event['total_match_count'] = count($id_ary); - } - - public function test_func(object $event): void - { - // var_dump($event); - } -} diff --git a/event/viewforum_listener.php b/event/viewforum_listener.php index abb541a..633c33b 100755 --- a/event/viewforum_listener.php +++ b/event/viewforum_listener.php @@ -15,6 +15,7 @@ class viewforum_listener implements EventSubscriberInterface public function __construct( private config $config, + private tag_helper $tag_helper, private search_helper $search_helper, ) {} @@ -29,20 +30,14 @@ class viewforum_listener implements EventSubscriberInterface public function fetch_topic_tags(object $event): void { - if (!$this->config['pedodev_tagging_viewforum']) - { - return; - } + if (!$this->config['pedodev_tagging_viewforum']) return; - $this->topic_tags = $this->search_helper->search_topic_tags($event['topic_list']); + $this->topic_tags = $this->search_helper->search_post_tags($event['topic_list'], 'topics'); } public function assign_topic_tags(object $event): void { - if (!$this->config['pedodev_tagging_viewforum']) - { - return; - } + if (!$this->config['pedodev_tagging_viewforum']) return; $topic_row = $event['topic_row']; $topic_id = (int)$topic_row['TOPIC_ID']; diff --git a/event/viewtopic_listener.php b/event/viewtopic_listener.php index 2a2cef0..520a71f 100755 --- a/event/viewtopic_listener.php +++ b/event/viewtopic_listener.php @@ -29,20 +29,14 @@ class viewtopic_listener implements EventSubscriberInterface public function fetch_post_tags(object $event): void { - if (!$this->config['pedodev_tagging_viewtopic']) - { - return; - } + if (!$this->config['pedodev_tagging_viewtopic']) return; - $this->post_tags = $this->search_helper->search_post_tags($event['post_list']); + $this->post_tags = $this->search_helper->search_post_tags($event['post_list'], 'posts'); } public function assign_post_tags(object $event): void { - if (!$this->config['pedodev_tagging_viewtopic']) - { - return; - } + if (!$this->config['pedodev_tagging_viewtopic']) return; $post_row = $event['post_row']; $post_id = (int)$post_row['POST_ID']; diff --git a/language/en/info_acp_tagging.php b/language/en/info_acp_tagging.php index b342cbc..459f5d3 100755 --- a/language/en/info_acp_tagging.php +++ b/language/en/info_acp_tagging.php @@ -28,6 +28,7 @@ $lang = array_merge($lang, array( 'ACP_TAGGING_TAG_ADDED' => 'New tag \'%s\' added successfully', 'ACP_TAGGING_TAG_EDITED' => 'Tag \'%s\' successfully edited', 'ACP_TAGGING_TAG_DELETED' => 'Tag \'%s\' has been deleted', + 'ACP_TAGGING_TAG_INVALID' => 'Invalid tag input', 'ACP_TAGGING_INVALID_TAG' => 'Invalid tag id', 'ACP_TAGGING_TAG_EMPTY' => 'Tag cannot be empty', 'ACP_TAGGING_DELETE_CONFIRMATION' => 'Confirm Deletion of Tag \'%s\'', @@ -39,6 +40,7 @@ $lang = array_merge($lang, array( 'ACP_TAGGING_TAGPOSTS' => 'Allow Tagging Posts', 'ACP_TAGGING_TAGLIST' => 'Tag List', 'ACP_TAGGING_TAGSEARCH' => 'Allow Searching by Tag', + 'ACP_TAGGING_TAGSEARCH_UNAVAILABLE' => 'Warning: tag search requires the Native Fulltext or MySQL Fulltext search engine', 'ACP_TAGGING_VIEWFORUM' => 'Show Tags in Forum View', 'ACP_TAGGING_VIEWTOPIC' => 'Show Tags in Topic View', 'ACP_TAGGING_RESULTS' => 'Show Tags in Search Results', diff --git a/language/en/permissions_tagging.php b/language/en/permissions_tagging.php index 6868bfe..dd91ad4 100755 --- a/language/en/permissions_tagging.php +++ b/language/en/permissions_tagging.php @@ -12,4 +12,5 @@ if (empty($lang) || !is_array($lang)) $lang = array_merge($lang, array( 'ACL_U_PEDODEV_TAGGING_CANTAGPOSTS' => 'Can tag posts', + 'ACL_U_PEDODEV_TAGGING_CANTAGSEARCH' => 'Can search by tag', )); diff --git a/migrations/dev_1.php b/migrations/dev_1.php deleted file mode 100755 index a67a678..0000000 --- a/migrations/dev_1.php +++ /dev/null @@ -1,40 +0,0 @@ -config['pedodev_tagging']); - } - - static public function depends_on() - { - return ['\phpbb\db\migration\data\v330\v330']; - } - - public function update_data() - { - return [ - - ['config.add', ['pedodev_tagging', 1]], - - ['module.add', [ - 'acp', - 'ACP_CAT_DOT_MODS', - 'ACP_TAGGING_TITLE' - ]], - - ['module.add', [ - 'acp', - 'ACP_TAGGING_TITLE', - [ - 'module_basename' => '\pedodev\tagging\acp\main_module', - 'modes' => ['settings'], - ], - ]], - - ]; - } -} diff --git a/migrations/dev_2.php b/migrations/dev_2.php deleted file mode 100755 index 62bb58c..0000000 --- a/migrations/dev_2.php +++ /dev/null @@ -1,23 +0,0 @@ - [ - $this->table_prefix . 'content_tags' => [ - 'COLUMNS' => [ - 'id' => ['ULINT', NULL, 'auto_increment'], - 'post_id' => ['ULINT', 0, 'NON-NULL'], - 'tag_id' => ['USINT', 0, 'NON-NULL'], - ], - 'PRIMARY_KEY' => 'id', - ], - ], - - ]; - } - - public function revert_schema() - { - return [ - 'drop_tables' => [ - $this->table_prefix . 'content_tags', - ], - ]; - } -} diff --git a/migrations/dev_5.php b/migrations/dev_5.php deleted file mode 100755 index 96c151f..0000000 --- a/migrations/dev_5.php +++ /dev/null @@ -1,23 +0,0 @@ -config['pedodev_tagging']); + } + + static public function depends_on() + { + return ['\phpbb\db\migration\data\v330\v330']; + } + + public function update_schema() + { + return [ + 'add_tables' => [ + $this->table_prefix . 'content_tags' => [ + 'COLUMNS' => [ + 'id' => ['ULINT', NULL, 'auto_increment'], + 'post_id' => ['ULINT', 0, 'NON-NULL'], + 'tag_id' => ['USINT', 0, 'NON-NULL'], + ], + 'PRIMARY_KEY' => 'id', + ], + ], + ]; + } + + public function revert_schema() + { + return [ + 'drop_tables' => [ + $this->table_prefix . 'content_tags', + ], + ]; + } + + public function update_data() + { + $random_string = bin2hex(random_bytes(8)); + $taglist_prefix = isset($this->config['pedodev_tagging_taglist_prefix']) ? $this->config['pedodev_tagging_taglist_prefix'] : $random_string; + $taglist_filepath = "{$this->phpbb_root_path}files/{$taglist_prefix}_taglist.json"; + + if (!touch($taglist_filepath) or !chmod($taglist_filepath, 0400)) + { + trigger_error('Unable to access taglist.json file. Check your file and ownership permissions', E_USER_ERROR); + } + + return [ + + ['config.add', ['pedodev_tagging', 1]], + + ['module.add', [ + 'acp', + 'ACP_CAT_DOT_MODS', + 'ACP_TAGGING_TITLE' + ]], + + ['module.add', [ + 'acp', + 'ACP_TAGGING_TITLE', + [ + 'module_basename' => '\pedodev\tagging\acp\main_module', + 'modes' => ['settings'], + ], + ]], + + ['config.add', ['pedodev_tagging_tagthreads', true]], + ['config.add', ['pedodev_tagging_tagposts', true]], + ['config.add', ['pedodev_tagging_maxtags', 5]], + ['config.add', ['pedodev_tagging_viewtopic', true]], + ['config.add', ['pedodev_tagging_viewforum', true]], + ['config.add', ['pedodev_tagging_results', true]], + + ['if', [ + ($this->config['search_type'] == '\phpbb\search\fulltext_native' or $this->config['search_type'] == '\phpbb\search\fulltext_mysql'), + ['config.add', ['pedodev_tagging_tagsearch', true]], + ]], + + ['permission.add', ['u_pedodev_tagging_cantagposts']], + ['permission.permission_set', ['ROLE_USER_LIMITED', 'u_pedodev_tagging_cantagposts']], + ['permission.permission_set', ['ROLE_USER_STANDARD', 'u_pedodev_tagging_cantagposts']], + ['permission.permission_set', ['ROLE_USER_FULL', 'u_pedodev_tagging_cantagposts']], + + ['permission.add', ['u_pedodev_tagging_cantagsearch']], + ['permission.permission_set', ['ROLE_USER_LIMITED', 'u_pedodev_tagging_cantagsearch']], + ['permission.permission_set', ['ROLE_USER_STANDARD', 'u_pedodev_tagging_cantagsearch']], + ['permission.permission_set', ['ROLE_USER_FULL', 'u_pedodev_tagging_cantagsearch']], + + + ['if', [ + !isset($this->config['pedodev_tagging_taglist_prefix']), + ['config.add', ['pedodev_tagging_taglist_prefix', $random_string]], + ]], + ]; + } + + public function revert_data() + { + $taglist_filepath = "{$this->phpbb_root_path}files/{$this->config['pedodev_tagging_taglist_prefix']}_taglist.json"; + + if (file_exists($taglist_filepath) and !unlink($taglist_filepath)) + { + trigger_error('Unable to delete taglist.json file. Check your file and ownership permissions', E_USER_ERROR); + } + + return []; + } +} diff --git a/release.txt b/release.txt new file mode 100755 index 0000000..3180d8d --- /dev/null +++ b/release.txt @@ -0,0 +1,30 @@ +Introducing this revolutionary new extension, designed and written specially for the pedo community. Often requested but never seen until now, this extension allows users to tag their threads with a variety of pre-selected tags chosen by the admin. A thread's tags will be visible when browsing the forums, giving users an easy way to find threads they are interested in. Most importantly, tags are fully integrated with the search system, allowing you to search for any combination of tags, keywords and authors. + +This is a beta release in preparation for an upcoming official release. You can help by downloading the plugin, testing it out and reviewing the code, then reporting on any bugs or vulnerabilites and suggesting new features. Please do not use this beta version on a live site. + +[b]Tagging threads[/b] +When creating a thread, users will have the option to select a variety of tags. These tags are defined in the Admin CP, where admins can easily add/edit/remove tags as they wish. Users can select multiple tags up to a maximum limit configured in the Admin CP. Tags can be added/removed by editing a thread, allowing moderators to fix incorrectly tagged threads. Tags are color coded for easy recognition. + +Individual posts within a thread can also be tagged. This is useful for megathreads where each post can contain a different video. The ability to tag posts can be enabled/disabled by the admin. + +[b]Search[/b] +Searching by tags is perhaps the best feature of this extension. Tags can be added to any existing keyword/author search, or you can search with only tags. When multiple tags are selected, the tag filter can be applied in two ways: 'intersect' will search for posts containing ALL matching tags, while 'union' will search for posts containing ANY matching tags. + +Tag search is fully integrated with the cache to prevent excessive server load. + +[b]Configuration[/b] +Comes with a configuration page in the Admin CP. This page is easy to use, allowing any admin to easily add, edit or remove tags. You can also set the color for each individual tag. + +[b]Permissions[/b] +Full integration with the phpBB permissions system. The following permissions can be applied to individual users or to groups: +[list] +[*] - Whether the user can tag threads/posts +[*] - Whether the user can search by tag +[/list] + +[b]Download[/b] +Download from any of the links below: + +[code] +[/code] +Installation instructions are included within the archive. See the README.txt file. diff --git a/styles/all/template/event/posting_editor_subject_after.html b/styles/all/template/event/posting_editor_subject_after.html index e9753e8..c5331a4 100755 --- a/styles/all/template/event/posting_editor_subject_after.html +++ b/styles/all/template/event/posting_editor_subject_after.html @@ -7,7 +7,7 @@
{% if loops.tags|length %} {% for TAG in loops.tags %} -
{% if loops.tags|length %} {% for TAG in loops.tags %} -