From f757e65559e94ee332d6d0677471df01d6a9394c Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 17 May 2018 04:50:37 +0300 Subject: [PATCH 001/113] [ticket/13713] Start working on User Mentions PHPBB3-13713 --- phpBB/config/default/container/services.yml | 1 + phpBB/config/default/routing/routing.yml | 5 ++ .../default/container/services_mention.yml | 25 +++++++++ phpbb/phpbb/mention/controller/mention.php | 52 ++++++++++++++++++ phpbb/phpbb/mention/source/topic.php | 53 +++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 phpbb/config/default/container/services_mention.yml create mode 100644 phpbb/phpbb/mention/controller/mention.php create mode 100644 phpbb/phpbb/mention/source/topic.php diff --git a/phpBB/config/default/container/services.yml b/phpBB/config/default/container/services.yml index b8fc7fa755..1d48ead588 100644 --- a/phpBB/config/default/container/services.yml +++ b/phpBB/config/default/container/services.yml @@ -15,6 +15,7 @@ imports: - { resource: services_help.yml } - { resource: services_http.yml } - { resource: services_language.yml } + - { resource: services_mention.yml } - { resource: services_migrator.yml } - { resource: services_mimetype_guesser.yml } - { resource: services_module.yml } diff --git a/phpBB/config/default/routing/routing.yml b/phpBB/config/default/routing/routing.yml index a5e9265dc3..9ed725fc06 100644 --- a/phpBB/config/default/routing/routing.yml +++ b/phpBB/config/default/routing/routing.yml @@ -24,6 +24,11 @@ phpbb_help_routing: resource: help.yml prefix: /help +phpbb_mention_controller: + path: /mention + methods: [GET, POST] + defaults: { _controller: phpbb.mention.controller:handle } + phpbb_report_routing: resource: report.yml diff --git a/phpbb/config/default/container/services_mention.yml b/phpbb/config/default/container/services_mention.yml new file mode 100644 index 0000000000..c7fc969182 --- /dev/null +++ b/phpbb/config/default/container/services_mention.yml @@ -0,0 +1,25 @@ +services: +# ----- Controller ----- + phpbb.mention.controller: + class: phpbb\mention\controller\mention + arguments: + - '@request' + - '%core.root_path%' + - '%core.php_ext%' + +# ----- Sources for mention ----- + phpbb.mention.source_collection: + class: phpbb\di\service_collection + arguments: + - '@service_container' + tags: + - { name: service_collection, tag: mention.source } + + phpbb.mention.source.topic: + class: phpbb\mention\source\topic + arguments: + - '@dbal.conn' + - '@request' + tags: + - { name: mention.source } + diff --git a/phpbb/phpbb/mention/controller/mention.php b/phpbb/phpbb/mention/controller/mention.php new file mode 100644 index 0000000000..b9e51eed28 --- /dev/null +++ b/phpbb/phpbb/mention/controller/mention.php @@ -0,0 +1,52 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\controller; + +use Symfony\Component\HttpFoundation\JsonResponse; + +class mention +{ + /** @var \phpbb\request\request_interface */ + protected $request; + + /** @var string */ + protected $phpbb_root_path; + + /** @var string */ + protected $php_ext; + + /** + * Constructor + * + */ + public function __construct(\phpbb\request\request_interface $request, $phpbb_root_path, $phpEx) + { + $this->request = $request; + $this->phpbb_root_path = $phpbb_root_path; + $this->php_ext = $phpEx; + } + + public function handle() + { + if (!$this->request->is_ajax()) + { + redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); + } + + $topic_id = $this->request->variable('topic_id', 0); + // TODO + + return new JsonResponse(); + } +} diff --git a/phpbb/phpbb/mention/source/topic.php b/phpbb/phpbb/mention/source/topic.php new file mode 100644 index 0000000000..739be108cd --- /dev/null +++ b/phpbb/phpbb/mention/source/topic.php @@ -0,0 +1,53 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\mention\method; + +class topic +{ + /** @var \phpbb\db\driver\driver_interface */ + protected $db; + + /** @var \phpbb\request\request_interface */ + protected $request; + + /** + * Constructor + */ + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\request\request_interface $request) + { + $this->db = $db; + $this->request = $request; + } + + public function get($keyword, $topic_id) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.username, u.user_id', + 'FROM' => [ + USERS_TABLE => 'u', + ], + 'LEFT_JOIN' => [ + 'FROM' => [POSTS_TABLE => 'p'], + 'ON' => 'u.user_id = p.poster_id' + ], + 'WHERE' => 'p.topic_id = ' . $topic_id . ' AND u.user_id <> ' . ANONYMOUS . ' + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + 'ORDER_BY' => 'p.post_time DESC' + ]); + $res = $this->db->sql_query_limit($query, 5); + + return $this->db->sql_fetchrowset($res); + } +} From b6e2d1f48c852e85bfbb96179206105702b45017 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 18 May 2018 16:15:06 +0300 Subject: [PATCH 002/113] [ticket/13713] Abstract class for usernames PHPBB3-13713 --- .../default/container/services_mention.yml | 11 +++- phpbb/phpbb/mention/controller/mention.php | 16 ++++- phpbb/phpbb/mention/source/friend.php | 56 +++++++++++++++++ phpbb/phpbb/mention/source/topic.php | 61 +++++++------------ phpbb/phpbb/mention/source/user.php | 44 +++++++++++++ 5 files changed, 145 insertions(+), 43 deletions(-) create mode 100644 phpbb/phpbb/mention/source/friend.php create mode 100644 phpbb/phpbb/mention/source/user.php diff --git a/phpbb/config/default/container/services_mention.yml b/phpbb/config/default/container/services_mention.yml index c7fc969182..e1e8fa9a35 100644 --- a/phpbb/config/default/container/services_mention.yml +++ b/phpbb/config/default/container/services_mention.yml @@ -3,6 +3,7 @@ services: phpbb.mention.controller: class: phpbb\mention\controller\mention arguments: + - '@phpbb.mention.source_collection' - '@request' - '%core.root_path%' - '%core.php_ext%' @@ -15,11 +16,17 @@ services: tags: - { name: service_collection, tag: mention.source } + phpbb.mention.source.friend: + class: phpbb\mention\source\friend + arguments: + - '@dbal.conn' + - '@user' + tags: + - { name: mention.source } + phpbb.mention.source.topic: class: phpbb\mention\source\topic arguments: - '@dbal.conn' - - '@request' tags: - { name: mention.source } - diff --git a/phpbb/phpbb/mention/controller/mention.php b/phpbb/phpbb/mention/controller/mention.php index b9e51eed28..14ec892269 100644 --- a/phpbb/phpbb/mention/controller/mention.php +++ b/phpbb/phpbb/mention/controller/mention.php @@ -17,6 +17,9 @@ use Symfony\Component\HttpFoundation\JsonResponse; class mention { + /** @var \phpbb\di\service_collection */ + protected $mention_sources; + /** @var \phpbb\request\request_interface */ protected $request; @@ -30,8 +33,9 @@ class mention * Constructor * */ - public function __construct(\phpbb\request\request_interface $request, $phpbb_root_path, $phpEx) + public function __construct($mention_sources, \phpbb\request\request_interface $request, $phpbb_root_path, $phpEx) { + $this->mention_sources = $mention_sources; $this->request = $request; $this->phpbb_root_path = $phpbb_root_path; $this->php_ext = $phpEx; @@ -44,9 +48,15 @@ class mention redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); } + $keyword = $this->request->variable('keyword', '', true); $topic_id = $this->request->variable('topic_id', 0); - // TODO + $names = []; - return new JsonResponse(); + foreach ($this->mention_sources as $source) + { + $names = array_merge($names, $source->get($keyword, $topic_id)); + } + + return new JsonResponse($names); } } diff --git a/phpbb/phpbb/mention/source/friend.php b/phpbb/phpbb/mention/source/friend.php new file mode 100644 index 0000000000..c0cbc261ce --- /dev/null +++ b/phpbb/phpbb/mention/source/friend.php @@ -0,0 +1,56 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +class friend extends user +{ + /** @var \phpbb\db\driver\driver_interface */ + protected $db; + + /** @var \phpbb\user */ + protected $user; + + /** + * Constructor + */ + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user $user) + { + $this->db = $db; + $this->user = $user; + + parent::__construct($db); + } + + protected function query($keyword, $topic_id) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.username, u.user_id', + 'FROM' => [ + USERS_TABLE => 'u', + ], + 'LEFT_JOIN' => [ + [ + 'FROM' => [ZEBRA_TABLE => 'z'], + 'ON' => 'u.user_id = z.zebra_id' + ] + ], + 'WHERE' => 'z.friend = 1 AND z.user_id = ' . (int) $this->user->data['user_id'] . ' + AND u.user_id <> ' . ANONYMOUS . ' + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + 'ORDER_BY' => 'u.user_lastvisit DESC' + ]); + return $query; + } +} diff --git a/phpbb/phpbb/mention/source/topic.php b/phpbb/phpbb/mention/source/topic.php index 739be108cd..553abb07b6 100644 --- a/phpbb/phpbb/mention/source/topic.php +++ b/phpbb/phpbb/mention/source/topic.php @@ -1,53 +1,38 @@ -* @license GNU General Public License, version 2 (GPL-2.0) -* -* For full copyright and license information, please see -* the docs/CREDITS.txt file. -* -*/ + * + * This file is part of the phpBB Forum Software package. + * + * @copyright (c) phpBB Limited + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ -namespace phpbb\mention\method; +namespace phpbb\mention\source; -class topic +class topic extends user { - /** @var \phpbb\db\driver\driver_interface */ - protected $db; - - /** @var \phpbb\request\request_interface */ - protected $request; - - /** - * Constructor - */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\request\request_interface $request) - { - $this->db = $db; - $this->request = $request; - } - - public function get($keyword, $topic_id) + protected function query($keyword, $topic_id) { $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username, u.user_id', - 'FROM' => [ + 'SELECT' => 'u.username, u.user_id', + 'FROM' => [ USERS_TABLE => 'u', ], 'LEFT_JOIN' => [ - 'FROM' => [POSTS_TABLE => 'p'], - 'ON' => 'u.user_id = p.poster_id' + [ + 'FROM' => [POSTS_TABLE => 'p'], + 'ON' => 'u.user_id = p.poster_id' + ] ], - 'WHERE' => 'p.topic_id = ' . $topic_id . ' AND u.user_id <> ' . ANONYMOUS . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + 'WHERE' => 'p.topic_id = ' . $topic_id . ' AND u.user_id <> ' . ANONYMOUS . ' + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), - 'ORDER_BY' => 'p.post_time DESC' + 'ORDER_BY' => 'p.post_time DESC' ]); - $res = $this->db->sql_query_limit($query, 5); - - return $this->db->sql_fetchrowset($res); + return $query; } } diff --git a/phpbb/phpbb/mention/source/user.php b/phpbb/phpbb/mention/source/user.php new file mode 100644 index 0000000000..216fafa36d --- /dev/null +++ b/phpbb/phpbb/mention/source/user.php @@ -0,0 +1,44 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +abstract class user +{ + /** @var \phpbb\db\driver\driver_interface */ + protected $db; + + /** + * Constructor + */ + public function __construct(\phpbb\db\driver\driver_interface $db) + { + $this->db = $db; + } + + abstract protected function query($keyword, $topic_id); + + public function get($keyword, $topic_id) + { + $keyword = utf8_clean_string($keyword); + $res = $this->db->sql_query_limit($this->query($keyword, $topic_id), 5); + + $names = []; + while ($row = $this->db->sql_fetchrow($res)) + { + $names['u' . $row['user_id']] = $row['username']; + } + + return $names; + } +} From f6c9f4fb131bed4d0c495c6ae86301b1a633e0fe Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 18 May 2018 16:33:55 +0300 Subject: [PATCH 003/113] [ticket/13713] Create source interface PHPBB3-13713 --- phpbb/phpbb/mention/source/friend.php | 3 +++ .../phpbb/mention/source/source_interface.php | 27 +++++++++++++++++++ phpbb/phpbb/mention/source/topic.php | 3 +++ phpbb/phpbb/mention/source/user.php | 12 ++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 phpbb/phpbb/mention/source/source_interface.php diff --git a/phpbb/phpbb/mention/source/friend.php b/phpbb/phpbb/mention/source/friend.php index c0cbc261ce..8f78159b6c 100644 --- a/phpbb/phpbb/mention/source/friend.php +++ b/phpbb/phpbb/mention/source/friend.php @@ -32,6 +32,9 @@ class friend extends user parent::__construct($db); } + /** + * {@inheritdoc} + */ protected function query($keyword, $topic_id) { $query = $this->db->sql_build_query('SELECT', [ diff --git a/phpbb/phpbb/mention/source/source_interface.php b/phpbb/phpbb/mention/source/source_interface.php new file mode 100644 index 0000000000..ace5cc9149 --- /dev/null +++ b/phpbb/phpbb/mention/source/source_interface.php @@ -0,0 +1,27 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +interface source_interface +{ + /** + * Searches database for names to mention + * and returns and array of found items + * + * @param string $keyword Search string + * @param int $topic_id Current topic ID + * @return array Array of names + */ + public function get($keyword, $topic_id); +} diff --git a/phpbb/phpbb/mention/source/topic.php b/phpbb/phpbb/mention/source/topic.php index 553abb07b6..1d72df711c 100644 --- a/phpbb/phpbb/mention/source/topic.php +++ b/phpbb/phpbb/mention/source/topic.php @@ -15,6 +15,9 @@ namespace phpbb\mention\source; class topic extends user { + /** + * {@inheritdoc} + */ protected function query($keyword, $topic_id) { $query = $this->db->sql_build_query('SELECT', [ diff --git a/phpbb/phpbb/mention/source/user.php b/phpbb/phpbb/mention/source/user.php index 216fafa36d..be0d39f3af 100644 --- a/phpbb/phpbb/mention/source/user.php +++ b/phpbb/phpbb/mention/source/user.php @@ -13,7 +13,7 @@ namespace phpbb\mention\source; -abstract class user +abstract class user implements source_interface { /** @var \phpbb\db\driver\driver_interface */ protected $db; @@ -26,8 +26,18 @@ abstract class user $this->db = $db; } + /** + * Builds a query based on user input + * + * @param string $keyword Search string + * @param int $topic_id Current topic ID + * @return string Query ready for execution + */ abstract protected function query($keyword, $topic_id); + /** + * {@inheritdoc} + */ public function get($keyword, $topic_id) { $keyword = utf8_clean_string($keyword); From 218e6bfcad9b512f7561ce6445e67f82d1056166 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 25 May 2018 13:40:36 +0300 Subject: [PATCH 004/113] [ticket/13713] Add group search PHPBB3-13713 --- .../default/container/services_mention.yml | 9 ++ phpbb/phpbb/mention/controller/mention.php | 8 +- phpbb/phpbb/mention/source/friend.php | 4 - phpbb/phpbb/mention/source/group.php | 103 ++++++++++++++++++ phpbb/phpbb/mention/source/user.php | 4 +- phpbb/phpbb/mention/source/usergroup.php | 51 +++++++++ 6 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 phpbb/phpbb/mention/source/group.php create mode 100644 phpbb/phpbb/mention/source/usergroup.php diff --git a/phpbb/config/default/container/services_mention.yml b/phpbb/config/default/container/services_mention.yml index e1e8fa9a35..bae6b45bfe 100644 --- a/phpbb/config/default/container/services_mention.yml +++ b/phpbb/config/default/container/services_mention.yml @@ -30,3 +30,12 @@ services: - '@dbal.conn' tags: - { name: mention.source } + + phpbb.mention.source.usergroup: + class: phpbb\mention\source\usergroup + arguments: + - '@dbal.conn' + - '@group_helper' + - '@user' + tags: + - { name: mention.source } diff --git a/phpbb/phpbb/mention/controller/mention.php b/phpbb/phpbb/mention/controller/mention.php index 14ec892269..8d731ae210 100644 --- a/phpbb/phpbb/mention/controller/mention.php +++ b/phpbb/phpbb/mention/controller/mention.php @@ -43,10 +43,10 @@ class mention public function handle() { - if (!$this->request->is_ajax()) - { - redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); - } +// if (!$this->request->is_ajax()) +// { +// redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); +// } $keyword = $this->request->variable('keyword', '', true); $topic_id = $this->request->variable('topic_id', 0); diff --git a/phpbb/phpbb/mention/source/friend.php b/phpbb/phpbb/mention/source/friend.php index 8f78159b6c..bb3ba9ecb7 100644 --- a/phpbb/phpbb/mention/source/friend.php +++ b/phpbb/phpbb/mention/source/friend.php @@ -15,9 +15,6 @@ namespace phpbb\mention\source; class friend extends user { - /** @var \phpbb\db\driver\driver_interface */ - protected $db; - /** @var \phpbb\user */ protected $user; @@ -26,7 +23,6 @@ class friend extends user */ public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user $user) { - $this->db = $db; $this->user = $user; parent::__construct($db); diff --git a/phpbb/phpbb/mention/source/group.php b/phpbb/phpbb/mention/source/group.php new file mode 100644 index 0000000000..b503ac714c --- /dev/null +++ b/phpbb/phpbb/mention/source/group.php @@ -0,0 +1,103 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +abstract class group implements source_interface +{ + /** @var \phpbb\db\driver\driver_interface */ + protected $db; + + /** @var \phpbb\group\helper */ + protected $helper; + + /** + * Constructor + */ + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper) + { + $this->db = $db; + $this->helper = $helper; + } + + /** + * Returns data for all board groups + * + * @return array Array of groups' data + */ + protected function get_groups() + { + static $groups = null; + + if (is_null($groups)) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'g.*', + 'FROM' => [ + GROUPS_TABLE => 'g', + ], + ]); + $res = $this->db->sql_query($query); + + $groups = []; + while ($row = $this->db->sql_fetchrow($res)) + { + $group_name = $this->helper->get_name($row['group_name']); + $groups['names'][$row['group_id']] = $group_name; + $groups[$row['group_id']] = $row; + $groups[$row['group_id']]['group_name'] = $group_name; + } + } + return $groups; + } + + /** + * Builds a query for getting group IDs based on user input + * + * @param string $keyword Search string + * @param int $topic_id Current topic ID + * @return string Query ready for execution + */ + abstract protected function query($keyword, $topic_id); + + /** + * {@inheritdoc} + */ + public function get($keyword, $topic_id) + { + // Grab all group IDs + $res = $this->db->sql_query($this->query($keyword, $topic_id)); + + $group_ids = []; + while ($row = $this->db->sql_fetchrow($res)) + { + $group_ids[] = $row['group_id']; + } + + // Grab group data + $groups = $this->get_groups(); + + $matches = preg_grep('/^' . $keyword . '.*/i', $groups['names']); + $group_ids = array_intersect($group_ids, array_flip($matches)); + + $names = []; + foreach ($group_ids as $group_id) + { + $names['g' . $group_id] = [ + 'name' => $groups[$group_id]['group_name'], + ]; + } + + return $names; + } +} diff --git a/phpbb/phpbb/mention/source/user.php b/phpbb/phpbb/mention/source/user.php index be0d39f3af..55f94e4866 100644 --- a/phpbb/phpbb/mention/source/user.php +++ b/phpbb/phpbb/mention/source/user.php @@ -46,7 +46,9 @@ abstract class user implements source_interface $names = []; while ($row = $this->db->sql_fetchrow($res)) { - $names['u' . $row['user_id']] = $row['username']; + $names['u' . $row['user_id']] = [ + 'name' => $row['username'], + ]; } return $names; diff --git a/phpbb/phpbb/mention/source/usergroup.php b/phpbb/phpbb/mention/source/usergroup.php new file mode 100644 index 0000000000..1a0e20eff8 --- /dev/null +++ b/phpbb/phpbb/mention/source/usergroup.php @@ -0,0 +1,51 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +class usergroup extends group +{ + /** @var \phpbb\user */ + protected $user; + + /** + * Constructor + */ + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper, \phpbb\user $user) + { + $this->user = $user; + + parent::__construct($db, $helper); + } + + /** + * {@inheritdoc} + */ + protected function query($keyword, $topic_id) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'g.group_id', + 'FROM' => [ + GROUPS_TABLE => 'g', + ], + 'LEFT_JOIN' => [ + [ + 'FROM' => [USER_GROUP_TABLE => 'ug'], + 'ON' => 'g.group_id = ug.group_id' + ] + ], + 'WHERE' => 'ug.user_id = ' . (int) $this->user->data['user_id'], + ]); + return $query; + } +} From 86b5fbed38d8b5732c60633a023fc2f458e835c5 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 25 May 2018 16:31:32 +0300 Subject: [PATCH 005/113] [ticket/13713] Very basic dropdown implementation PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 26 ++++++++++++++++--- phpBB/posting.php | 1 + .../prosilver/template/posting_buttons.html | 5 ++++ phpbb/assets/css/jquery.atwho.min.css | 1 + phpbb/assets/javascript/jquery.atwho.min.js | 1 + phpbb/assets/javascript/jquery.caret.min.js | 2 ++ phpbb/phpbb/mention/controller/mention.php | 16 ++++++++---- 7 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 phpbb/assets/css/jquery.atwho.min.css create mode 100644 phpbb/assets/javascript/jquery.atwho.min.js create mode 100644 phpbb/assets/javascript/jquery.caret.min.js diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 94063c2766..a2998a3f38 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -384,11 +384,20 @@ function getCaretPosition(txtarea) { return caretPos; } -/** -* Allow to use tab character when typing code -* Keep indentation of last line of code when typing code -*/ (function($) { + function handle_mentions(txtarea) { + $(txtarea).atwho({ + at: "@", + callbacks: { + remoteFilter: function(query, callback) { + $.getJSON(mention_url, {keyword: query, topic_id: mention_topic_id}, function (data) { + callback(data) + }); + } + } + }); + } + $(document).ready(function() { var doc, textarea; @@ -405,11 +414,20 @@ function getCaretPosition(txtarea) { textarea = doc.forms[form_name].elements[text_name]; + /** + * Allow to use tab character when typing code + * Keep indentation of last line of code when typing code + */ phpbb.applyCodeEditor(textarea); + if ($('#attach-panel').length) { phpbb.showDragNDrop(textarea); } + if (mention_url) { + handle_mentions(textarea); + } + $('textarea').on('keydown', function (e) { if (e.which === 13 && (e.metaKey || e.ctrlKey)) { $(this).closest('form').find(':submit').click(); diff --git a/phpBB/posting.php b/phpBB/posting.php index 696c3346cb..7c29f205f5 100644 --- a/phpBB/posting.php +++ b/phpBB/posting.php @@ -1905,6 +1905,7 @@ $page_data = array( 'U_VIEW_TOPIC' => ($mode != 'post') ? append_sid("{$phpbb_root_path}viewtopic.$phpEx", "f=$forum_id&t=$topic_id") : '', 'U_PROGRESS_BAR' => append_sid("{$phpbb_root_path}posting.$phpEx", "f=$forum_id&mode=popup"), 'UA_PROGRESS_BAR' => addslashes(append_sid("{$phpbb_root_path}posting.$phpEx", "f=$forum_id&mode=popup")), + 'UA_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), 'S_PRIVMSGS' => false, 'S_CLOSE_PROGRESS_WINDOW' => (isset($_POST['add_file'])) ? true : false, diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 27a7481ad8..3ce5539d54 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -4,6 +4,8 @@ var text_name = 'signature''message'; var load_draft = false; var upload = false; + var mention_url = '{UA_MENTION_URL}'; + var mention_topic_id = '{S_TOPIC_ID}'; // Define the bbCode tags var bbcode = new Array(); @@ -25,6 +27,9 @@ } } + + + diff --git a/phpbb/assets/css/jquery.atwho.min.css b/phpbb/assets/css/jquery.atwho.min.css new file mode 100644 index 0000000000..f770dc73b3 --- /dev/null +++ b/phpbb/assets/css/jquery.atwho.min.css @@ -0,0 +1 @@ +.atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;z-index:11110!important}.atwho-view .atwho-header{padding:5px;margin:5px;cursor:pointer;border-bottom:solid 1px #eaeff1;color:#6f8092;font-size:11px;font-weight:700}.atwho-view .atwho-header .small{color:#6f8092;float:right;padding-top:2px;margin-right:-5px;font-size:12px;font-weight:400}.atwho-view .atwho-header:hover{cursor:default}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto;max-height:200px;overflow-y:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400} \ No newline at end of file diff --git a/phpbb/assets/javascript/jquery.atwho.min.js b/phpbb/assets/javascript/jquery.atwho.min.js new file mode 100644 index 0000000000..857bb93126 --- /dev/null +++ b/phpbb/assets/javascript/jquery.atwho.min.js @@ -0,0 +1 @@ +!function(t,e){"function"==typeof define&&define.amd?define(["jquery"],function(t){return e(t)}):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(this,function(t){var e,i;i={ESC:27,TAB:9,ENTER:13,CTRL:17,A:65,P:80,N:78,LEFT:37,UP:38,RIGHT:39,DOWN:40,BACKSPACE:8,SPACE:32},e={beforeSave:function(t){return r.arrayToDefaultHash(t)},matcher:function(t,e,i,n){var r,o,s,a,h;return t=t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),i&&(t="(?:^|\\s)"+t),r=decodeURI("%C3%80"),o=decodeURI("%C3%BF"),h=n?" ":"",a=new RegExp(t+"([A-Za-z"+r+"-"+o+"0-9_"+h+"'.+-]*)$|"+t+"([^\\x00-\\xff]*)$","gi"),s=a.exec(e),s?s[2]||s[1]:null},filter:function(t,e,i){var n,r,o,s;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],~new String(o[i]).toLowerCase().indexOf(t.toLowerCase())&&n.push(o);return n},remoteFilter:null,sorter:function(t,e,i){var n,r,o,s;if(!t)return e;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],o.atwho_order=new String(o[i]).toLowerCase().indexOf(t.toLowerCase()),o.atwho_order>-1&&n.push(o);return n.sort(function(t,e){return t.atwho_order-e.atwho_order})},tplEval:function(t,e){var i,n,r;r=t;try{return"string"!=typeof t&&(r=t(e)),r.replace(/\$\{([^\}]*)\}/g,function(t,i,n){return e[i]})}catch(n){return i=n,""}},highlighter:function(t,e){var i;return e?(i=new RegExp(">\\s*([^<]*?)("+e.replace("+","\\+")+")([^<]*)\\s*<","ig"),t.replace(i,function(t,e,i,n){return"> "+e+""+i+""+n+" <"})):t},beforeInsert:function(t,e,i){return t},beforeReposition:function(t){return t},afterMatchFailed:function(t,e){}};var n;n=function(){function e(e){this.currentFlag=null,this.controllers={},this.aliasMaps={},this.$inputor=t(e),this.setupRootElement(),this.listen()}return e.prototype.createContainer=function(e){var i;return null!=(i=this.$el)&&i.remove(),t(e.body).append(this.$el=t("
"))},e.prototype.setupRootElement=function(e,i){var n,r;if(null==i&&(i=!1),e)this.window=e.contentWindow,this.document=e.contentDocument||this.window.document,this.iframe=e;else{this.document=this.$inputor[0].ownerDocument,this.window=this.document.defaultView||this.document.parentWindow;try{this.iframe=this.window.frameElement}catch(r){if(n=r,this.iframe=null,t.fn.atwho.debug)throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n"+n)}}return this.createContainer((this.iframeAsRoot=i)?this.document:document)},e.prototype.controller=function(t){var e,i,n,r;if(this.aliasMaps[t])i=this.controllers[this.aliasMaps[t]];else{r=this.controllers;for(n in r)if(e=r[n],n===t){i=e;break}}return i?i:this.controllers[this.currentFlag]},e.prototype.setContextFor=function(t){return this.currentFlag=t,this},e.prototype.reg=function(t,e){var i,n;return n=(i=this.controllers)[t]||(i[t]=this.$inputor.is("[contentEditable]")?new l(this,t):new s(this,t)),e.alias&&(this.aliasMaps[e.alias]=t),n.init(e),this},e.prototype.listen=function(){return this.$inputor.on("compositionstart",function(t){return function(e){var i;return null!=(i=t.controller())&&i.view.hide(),t.isComposing=!0,null}}(this)).on("compositionend",function(t){return function(e){return t.isComposing=!1,setTimeout(function(e){return t.dispatch(e)}),null}}(this)).on("keyup.atwhoInner",function(t){return function(e){return t.onKeyup(e)}}(this)).on("keydown.atwhoInner",function(t){return function(e){return t.onKeydown(e)}}(this)).on("blur.atwhoInner",function(t){return function(e){var i;return(i=t.controller())?(i.expectedQueryCBId=null,i.view.hide(e,i.getOpt("displayTimeout"))):void 0}}(this)).on("click.atwhoInner",function(t){return function(e){return t.dispatch(e)}}(this)).on("scroll.atwhoInner",function(t){return function(){var e;return e=t.$inputor.scrollTop(),function(i){var n,r;return n=i.target.scrollTop,e!==n&&null!=(r=t.controller())&&r.view.hide(i),e=n,!0}}}(this)())},e.prototype.shutdown=function(){var t,e,i;i=this.controllers;for(t in i)e=i[t],e.destroy(),delete this.controllers[t];return this.$inputor.off(".atwhoInner"),this.$el.remove()},e.prototype.dispatch=function(t){var e,i,n,r;if(void 0!==t){n=this.controllers,r=[];for(e in n)i=n[e],r.push(i.lookUp(t));return r}},e.prototype.onKeyup=function(e){var n;switch(e.keyCode){case i.ESC:e.preventDefault(),null!=(n=this.controller())&&n.view.hide();break;case i.DOWN:case i.UP:case i.CTRL:case i.ENTER:t.noop();break;case i.P:case i.N:e.ctrlKey||this.dispatch(e);break;default:this.dispatch(e)}},e.prototype.onKeydown=function(e){var n,r;if(r=null!=(n=this.controller())?n.view:void 0,r&&r.visible())switch(e.keyCode){case i.ESC:e.preventDefault(),r.hide(e);break;case i.UP:e.preventDefault(),r.prev();break;case i.DOWN:e.preventDefault(),r.next();break;case i.P:if(!e.ctrlKey)return;e.preventDefault(),r.prev();break;case i.N:if(!e.ctrlKey)return;e.preventDefault(),r.next();break;case i.TAB:case i.ENTER:case i.SPACE:if(!r.visible())return;if(!this.controller().getOpt("spaceSelectsMatch")&&e.keyCode===i.SPACE)return;if(!this.controller().getOpt("tabSelectsMatch")&&e.keyCode===i.TAB)return;r.highlighted()?(e.preventDefault(),r.choose(e)):r.hide(e);break;default:t.noop()}},e}();var r,o=[].slice;r=function(){function i(e,i){this.app=e,this.at=i,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.expectedQueryCBId=null,this.setting=null,this.query=null,this.pos=0,this.range=null,0===(this.$el=t("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=t("
")),this.model=new u(this),this.view=new c(this)}return i.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},i.prototype.init=function(e){return this.setting=t.extend({},this.setting||t.fn.atwho["default"],e),this.view.init(),this.model.reload(this.setting.data)},i.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},i.prototype.callDefault=function(){var i,n,r,s;s=arguments[0],i=2<=arguments.length?o.call(arguments,1):[];try{return e[s].apply(this,i)}catch(r){return n=r,t.error(n+" Or maybe At.js doesn't have function "+s)}},i.prototype.trigger=function(t,e){var i,n;return null==e&&(e=[]),e.push(this),i=this.getOpt("alias"),n=i?t+"-"+i+".atwho":t+".atwho",this.$inputor.trigger(n,e)},i.prototype.callbacks=function(t){return this.getOpt("callbacks")[t]||e[t]},i.prototype.getOpt=function(t,e){var i,n;try{return this.setting[t]}catch(n){return i=n,null}},i.prototype.insertContentFor=function(e){var i,n;return n=this.getOpt("insertTpl"),i=t.extend({},e.data("item-data"),{"atwho-at":this.at}),this.callbacks("tplEval").call(this,n,i,"onInsert")},i.prototype.renderView=function(t){var e;return e=this.getOpt("searchKey"),t=this.callbacks("sorter").call(this,this.query.text,t.slice(0,1001),e),this.view.render(t.slice(0,this.getOpt("limit")))},i.arrayToDefaultHash=function(e){var i,n,r,o;if(!t.isArray(e))return e;for(o=[],i=0,r=e.length;r>i;i++)n=e[i],t.isPlainObject(n)?o.push(n):o.push({name:n});return o},i.prototype.lookUp=function(t){var e,i;if((!t||"click"!==t.type||this.getOpt("lookUpOnClick"))&&(!this.getOpt("suspendOnComposing")||!this.app.isComposing))return(e=this.catchQuery(t))?(this.app.setContextFor(this.at),(i=this.getOpt("delay"))?this._delayLookUp(e,i):this._lookUp(e),e):(this.expectedQueryCBId=null,e)},i.prototype._delayLookUp=function(t,e){var i,n;return i=Date.now?Date.now():(new Date).getTime(),this.previousCallTime||(this.previousCallTime=i),n=e-(i-this.previousCallTime),n>0&&e>n?(this.previousCallTime=i,this._stopDelayedCall(),this.delayedCallTimeout=setTimeout(function(e){return function(){return e.previousCallTime=0,e.delayedCallTimeout=null,e._lookUp(t)}}(this),e)):(this._stopDelayedCall(),this.previousCallTime!==i&&(this.previousCallTime=0),this._lookUp(t))},i.prototype._stopDelayedCall=function(){return this.delayedCallTimeout?(clearTimeout(this.delayedCallTimeout),this.delayedCallTimeout=null):void 0},i.prototype._generateQueryCBId=function(){return{}},i.prototype._lookUp=function(e){var i;return i=function(t,e){return t===this.expectedQueryCBId?e&&e.length>0?this.renderView(this.constructor.arrayToDefaultHash(e)):this.view.hide():void 0},this.expectedQueryCBId=this._generateQueryCBId(),this.model.query(e.text,t.proxy(i,this,this.expectedQueryCBId))},i}();var s,a=function(t,e){function i(){this.constructor=t}for(var n in e)h.call(e,n)&&(t[n]=e[n]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},h={}.hasOwnProperty;s=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return a(i,e),i.prototype.catchQuery=function(){var t,e,i,n,r,o,s;return e=this.$inputor.val(),t=this.$inputor.caret("pos",{iframe:this.app.iframe}),s=e.slice(0,t),r=this.callbacks("matcher").call(this,this.at,s,this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),n="string"==typeof r,n&&r.length0?t.getRangeAt(0):void 0},n.prototype._setRange=function(e,i,n){return null==n&&(n=this._getRange()),n&&i?(i=t(i)[0],"after"===e?(n.setEndAfter(i),n.setStartAfter(i)):(n.setEndBefore(i),n.setStartBefore(i)),n.collapse(!1),this._clearRange(n)):void 0},n.prototype._clearRange=function(t){var e;return null==t&&(t=this._getRange()),e=this.app.window.getSelection(),null==this.ctrl_a_pressed?(e.removeAllRanges(),e.addRange(t)):void 0},n.prototype._movingEvent=function(t){var e;return"click"===t.type||(e=t.which)===i.RIGHT||e===i.LEFT||e===i.UP||e===i.DOWN},n.prototype._unwrap=function(e){var i;return e=t(e).unwrap().get(0),(i=e.nextSibling)&&i.nodeValue&&(e.nodeValue+=i.nodeValue,t(i).remove()),e},n.prototype.catchQuery=function(e){var n,r,o,s,a,h,l,u,c,p,f,d;if((d=this._getRange())&&d.collapsed){if(e.which===i.ENTER)return(r=t(d.startContainer).closest(".atwho-query")).contents().unwrap(),r.is(":empty")&&r.remove(),(r=t(".atwho-query",this.app.document)).text(r.text()).contents().last().unwrap(),void this._clearRange();if(/firefox/i.test(navigator.userAgent)){if(t(d.startContainer).is(this.$inputor))return void this._clearRange();e.which===i.BACKSPACE&&d.startContainer.nodeType===document.ELEMENT_NODE&&(c=d.startOffset-1)>=0?(o=d.cloneRange(),o.setStart(d.startContainer,c),t(o.cloneContents()).contents().last().is(".atwho-inserted")&&(a=t(d.startContainer).contents().get(c),this._setRange("after",t(a).contents().last()))):e.which===i.LEFT&&d.startContainer.nodeType===document.TEXT_NODE&&(n=t(d.startContainer.previousSibling),n.is(".atwho-inserted")&&0===d.startOffset&&this._setRange("after",n.contents().last()))}if(t(d.startContainer).closest(".atwho-inserted").addClass("atwho-query").siblings().removeClass("atwho-query"),(r=t(".atwho-query",this.app.document)).length>0&&r.is(":empty")&&0===r.text().length&&r.remove(),this._movingEvent(e)||r.removeClass("atwho-inserted"),r.length>0)switch(e.which){case i.LEFT:return this._setRange("before",r.get(0),d),void r.removeClass("atwho-query");case i.RIGHT:return this._setRange("after",r.get(0).nextSibling,d),void r.removeClass("atwho-query")}if(r.length>0&&(f=r.attr("data-atwho-at-query"))&&(r.empty().html(f).attr("data-atwho-at-query",null),this._setRange("after",r.get(0),d)),o=d.cloneRange(),o.setStart(d.startContainer,0),u=this.callbacks("matcher").call(this,this.at,o.toString(),this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),h="string"==typeof u,0===r.length&&h&&(s=d.startOffset-this.at.length-u.length)>=0&&(d.setStart(d.startContainer,s),r=t("",this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass("atwho-query"),d.surroundContents(r.get(0)),l=r.contents().last().get(0),l&&(/firefox/i.test(navigator.userAgent)?(d.setStart(l,l.length),d.setEnd(l,l.length),this._clearRange(d)):this._setRange("after",l,d))),!(h&&u.length=0&&(this._movingEvent(e)&&r.hasClass("atwho-inserted")?r.removeClass("atwho-query"):!1!==this.callbacks("afterMatchFailed").call(this,this.at,r)&&this._setRange("after",this._unwrap(r.text(r.text()).contents().first()))),null)}},n.prototype.rect=function(){var e,i,n;return n=this.query.el.offset(),n&&this.query.el[0].getClientRects().length?(this.app.iframe&&!this.app.iframeAsRoot&&(i=(e=t(this.app.iframe)).offset(),n.left+=i.left-this.$inputor.scrollLeft(),n.top+=i.top-this.$inputor.scrollTop()),n.bottom=n.top+this.query.el.height(),n):void 0},n.prototype.insert=function(t,e){var i,n,r,o,s;return this.$inputor.is(":focus")||this.$inputor.focus(),n=this.getOpt("functionOverrides"),n.insert?n.insert.call(this,t,e):(o=""===(o=this.getOpt("suffix"))?o:o||" ",i=e.data("item-data"),this.query.el.removeClass("atwho-query").addClass("atwho-inserted").html(t).attr("data-atwho-at-query",""+i["atwho-at"]+this.query.text).attr("contenteditable","false"),(r=this._getRange())&&(this.query.el.length&&r.setEndAfter(this.query.el[0]),r.collapse(!1),r.insertNode(s=this.app.document.createTextNode(""+o)),this._setRange("after",s,r)),this.$inputor.is(":focus")||this.$inputor.focus(),this.$inputor.change())},n}(r);var u;u=function(){function e(t){this.context=t,this.at=this.context.at,this.storage=this.context.$inputor}return e.prototype.destroy=function(){return this.storage.data(this.at,null)},e.prototype.saved=function(){return this.fetch()>0},e.prototype.query=function(t,e){var i,n,r;return n=this.fetch(),r=this.context.getOpt("searchKey"),n=this.context.callbacks("filter").call(this.context,t,n,r)||[],i=this.context.callbacks("remoteFilter"),n.length>0||!i&&0===n.length?e(n):i.call(this.context,t,e)},e.prototype.fetch=function(){return this.storage.data(this.at)||[]},e.prototype.save=function(t){return this.storage.data(this.at,this.context.callbacks("beforeSave").call(this.context,t||[]))},e.prototype.load=function(t){return!this.saved()&&t?this._load(t):void 0},e.prototype.reload=function(t){return this._load(t)},e.prototype._load=function(e){return"string"==typeof e?t.ajax(e,{dataType:"json"}).done(function(t){return function(e){return t.save(e)}}(this)):this.save(e)},e}();var c;c=function(){function e(e){this.context=e,this.$el=t("
    "),this.$elUl=this.$el.children(),this.timeoutID=null,this.context.$el.append(this.$el),this.bindEvent()}return e.prototype.init=function(){var t,e;return e=this.context.getOpt("alias")||this.context.at.charCodeAt(0),t=this.context.getOpt("headerTpl"),t&&1===this.$el.children().length&&this.$el.prepend(t),this.$el.attr({id:"at-view-"+e})},e.prototype.destroy=function(){return this.$el.remove()},e.prototype.bindEvent=function(){var e,i,n;return e=this.$el.find("ul"),i=0,n=0,e.on("mousemove.atwho-view","li",function(r){return function(r){var o;if((i!==r.clientX||n!==r.clientY)&&(i=r.clientX,n=r.clientY,o=t(r.currentTarget),!o.hasClass("cur")))return e.find(".cur").removeClass("cur"),o.addClass("cur")}}(this)).on("click.atwho-view","li",function(i){return function(n){return e.find(".cur").removeClass("cur"),t(n.currentTarget).addClass("cur"),i.choose(n),n.preventDefault()}}(this))},e.prototype.visible=function(){return t.expr.filters.visible(this.$el[0])},e.prototype.highlighted=function(){return this.$el.find(".cur").length>0},e.prototype.choose=function(t){var e,i;return(e=this.$el.find(".cur")).length&&(i=this.context.insertContentFor(e),this.context._stopDelayedCall(),this.context.insert(this.context.callbacks("beforeInsert").call(this.context,i,e,t),e),this.context.trigger("inserted",[e,t]),this.hide(t)),this.context.getOpt("hideWithoutSuffix")?this.stopShowing=!0:void 0},e.prototype.reposition=function(e){var i,n,r,o;return i=this.context.app.iframeAsRoot?this.context.app.window:window,e.bottom+this.$el.height()-t(i).scrollTop()>t(i).height()&&(e.bottom=e.top-this.$el.height()),e.left>(r=t(i).width()-this.$el.width()-5)&&(e.left=r),n={left:e.left,top:e.bottom},null!=(o=this.context.callbacks("beforeReposition"))&&o.call(this.context,n),this.$el.offset(n),this.context.trigger("reposition",[n])},e.prototype.next=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),e=t.next(),e.length||(e=this.$el.find("li:first")),e.addClass("cur"),i=e[0],n=i.offsetTop+i.offsetHeight+(i.nextSibling?i.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,n-this.$el.height()))},e.prototype.prev=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),i=t.prev(),i.length||(i=this.$el.find("li:last")),i.addClass("cur"),n=i[0],e=n.offsetTop+n.offsetHeight+(n.nextSibling?n.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,e-this.$el.height()))},e.prototype.scrollTop=function(t){var e;return e=this.context.getOpt("scrollDuration"),e?this.$elUl.animate({scrollTop:t},e):this.$elUl.scrollTop(t)},e.prototype.show=function(){var t;return this.stopShowing?void(this.stopShowing=!1):(this.visible()||(this.$el.show(),this.$el.scrollTop(0),this.context.trigger("shown")),(t=this.context.rect())?this.reposition(t):void 0)},e.prototype.hide=function(t,e){var i;if(this.visible())return isNaN(e)?(this.$el.hide(),this.context.trigger("hidden",[t])):(i=function(t){return function(){return t.hide()}}(this),clearTimeout(this.timeoutID),this.timeoutID=setTimeout(i,e))},e.prototype.render=function(e){var i,n,r,o,s,a,h;if(!(t.isArray(e)&&e.length>0))return void this.hide();for(this.$el.find("ul").empty(),n=this.$el.find("ul"),h=this.context.getOpt("displayTpl"),r=0,s=e.length;s>r;r++)o=e[r],o=t.extend({},o,{"atwho-at":this.context.at}),a=this.context.callbacks("tplEval").call(this.context,h,o,"onDisplay"),i=t(this.context.callbacks("highlighter").call(this.context,a,this.context.query.text)),i.data("item-data",o),n.append(i);return this.show(),this.context.getOpt("highlightFirst")?n.find("li:first").addClass("cur"):void 0},e}();var p;p={load:function(t,e){var i;return(i=this.controller(t))?i.model.load(e):void 0},isSelecting:function(){var t;return!!(null!=(t=this.controller())?t.view.visible():void 0)},hide:function(){var t;return null!=(t=this.controller())?t.view.hide():void 0},reposition:function(){var t;return(t=this.controller())?t.view.reposition(t.rect()):void 0},setIframe:function(t,e){return this.setupRootElement(t,e),null},run:function(){return this.dispatch()},destroy:function(){return this.shutdown(),this.$inputor.data("atwho",null)}},t.fn.atwho=function(e){var i,r;return i=arguments,r=null,this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function(){var o,s;return(s=(o=t(this)).data("atwho"))||o.data("atwho",s=new n(this)),"object"!=typeof e&&e?p[e]&&s?r=p[e].apply(s,Array.prototype.slice.call(i,1)):t.error("Method "+e+" does not exist on jQuery.atwho"):s.reg(e.at,e)}),null!=r?r:this},t.fn.atwho["default"]={at:void 0,alias:void 0,data:null,displayTpl:"
  • ${name}
  • ",insertTpl:"${atwho-at}${name}",headerTpl:null,callbacks:e,functionOverrides:{},searchKey:"name",suffix:void 0,hideWithoutSuffix:!1,startWithSpace:!0,acceptSpaceBar:!1,highlightFirst:!0,limit:5,maxLen:20,minLen:0,displayTimeout:300,delay:null,spaceSelectsMatch:!1,tabSelectsMatch:!0,editableAtwhoQueryAttrs:{},scrollDuration:150,suspendOnComposing:!0,lookUpOnClick:!0},t.fn.atwho.debug=!1}); \ No newline at end of file diff --git a/phpbb/assets/javascript/jquery.caret.min.js b/phpbb/assets/javascript/jquery.caret.min.js new file mode 100644 index 0000000000..a25584e2ae --- /dev/null +++ b/phpbb/assets/javascript/jquery.caret.min.js @@ -0,0 +1,2 @@ +/*! jquery.caret 2016-02-27 */ +!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(c){return a.returnExportsGlobal=b(c)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){"use strict";var b,c,d,e,f,g,h,i,j,k,l;k="caret",b=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.setPos=function(a){var b,c,d,e;return(e=j.getSelection())&&(d=0,c=!1,(b=function(a,f){var g,i,j,k,l,m;for(l=f.childNodes,m=[],j=0,k=l.length;k>j&&(g=l[j],!c);j++)if(3===g.nodeType){if(d+g.length>=a){c=!0,i=h.createRange(),i.setStart(g,a-d),e.removeAllRanges(),e.addRange(i);break}m.push(d+=g.length)}else m.push(b(a,g));return m})(a,this.domInputor)),this.domInputor},b.prototype.getIEPosition=function(){return this.getPosition()},b.prototype.getPosition=function(){var a,b;return b=this.getOffset(),a=this.$inputor.offset(),b.left-=a.left,b.top-=a.top,b},b.prototype.getOldIEPos=function(){var a,b;return b=h.selection.createRange(),a=h.body.createTextRange(),a.moveToElementText(this.domInputor),a.setEndPoint("EndToEnd",b),a.text.length},b.prototype.getPos=function(){var a,b,c;return(c=this.range())?(a=c.cloneRange(),a.selectNodeContents(this.domInputor),a.setEnd(c.endContainer,c.endOffset),b=a.toString().length,a.detach(),b):h.selection?this.getOldIEPos():void 0},b.prototype.getOldIEOffset=function(){var a,b;return a=h.selection.createRange().duplicate(),a.moveStart("character",-1),b=a.getBoundingClientRect(),{height:b.bottom-b.top,left:b.left,top:b.top}},b.prototype.getOffset=function(){var b,c,d,e,f;return j.getSelection&&(d=this.range())?(d.endOffset-1>0&&d.endContainer!==this.domInputor&&(b=d.cloneRange(),b.setStart(d.endContainer,d.endOffset-1),b.setEnd(d.endContainer,d.endOffset),e=b.getBoundingClientRect(),c={height:e.height,left:e.left+e.width,top:e.top},b.detach()),c&&0!==(null!=c?c.height:void 0)||(b=d.cloneRange(),f=a(h.createTextNode("|")),b.insertNode(f[0]),b.selectNode(f[0]),e=b.getBoundingClientRect(),c={height:e.height,left:e.left,top:e.top},f.remove(),b.detach())):h.selection&&(c=this.getOldIEOffset()),c&&(c.top+=a(j).scrollTop(),c.left+=a(j).scrollLeft()),c},b.prototype.range=function(){var a;if(j.getSelection)return a=j.getSelection(),a.rangeCount>0?a.getRangeAt(0):null},b}(),c=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.getIEPos=function(){var a,b,c,d,e,f,g;return b=this.domInputor,f=h.selection.createRange(),e=0,f&&f.parentElement()===b&&(d=b.value.replace(/\r\n/g,"\n"),c=d.length,g=b.createTextRange(),g.moveToBookmark(f.getBookmark()),a=b.createTextRange(),a.collapse(!1),e=g.compareEndPoints("StartToEnd",a)>-1?c:-g.moveStart("character",-c)),e},b.prototype.getPos=function(){return h.selection?this.getIEPos():this.domInputor.selectionStart},b.prototype.setPos=function(a){var b,c;return b=this.domInputor,h.selection?(c=b.createTextRange(),c.move("character",a),c.select()):b.setSelectionRange&&b.setSelectionRange(a,a),b},b.prototype.getIEOffset=function(a){var b,c,d,e;return c=this.domInputor.createTextRange(),a||(a=this.getPos()),c.move("character",a),d=c.boundingLeft,e=c.boundingTop,b=c.boundingHeight,{left:d,top:e,height:b}},b.prototype.getOffset=function(b){var c,d,e;return c=this.$inputor,h.selection?(d=this.getIEOffset(b),d.top+=a(j).scrollTop()+c.scrollTop(),d.left+=a(j).scrollLeft()+c.scrollLeft(),d):(d=c.offset(),e=this.getPosition(b),d={left:d.left+e.left-c.scrollLeft(),top:d.top+e.top-c.scrollTop(),height:e.height})},b.prototype.getPosition=function(a){var b,c,e,f,g,h,i;return b=this.$inputor,f=function(a){return a=a.replace(/<|>|`|"|&/g,"?").replace(/\r\n|\r|\n/g,"
    "),/firefox/i.test(navigator.userAgent)&&(a=a.replace(/\s/g," ")),a},void 0===a&&(a=this.getPos()),i=b.val().slice(0,a),e=b.val().slice(a),g=""+f(i)+"",g+="|",g+=""+f(e)+"",h=new d(b),c=h.create(g).rect()},b.prototype.getIEPosition=function(a){var b,c,d,e,f;return d=this.getIEOffset(a),c=this.$inputor.offset(),e=d.left-c.left,f=d.top-c.top,b=d.height,{left:e,top:f,height:b}},b}(),d=function(){function b(a){this.$inputor=a}return b.prototype.css_attr=["borderBottomWidth","borderLeftWidth","borderRightWidth","borderTopStyle","borderRightStyle","borderBottomStyle","borderLeftStyle","borderTopWidth","boxSizing","fontFamily","fontSize","fontWeight","height","letterSpacing","lineHeight","marginBottom","marginLeft","marginRight","marginTop","outlineWidth","overflow","overflowX","overflowY","paddingBottom","paddingLeft","paddingRight","paddingTop","textAlign","textOverflow","textTransform","whiteSpace","wordBreak","wordWrap"],b.prototype.mirrorCss=function(){var b,c=this;return b={position:"absolute",left:-9999,top:0,zIndex:-2e4},"TEXTAREA"===this.$inputor.prop("tagName")&&this.css_attr.push("width"),a.each(this.css_attr,function(a,d){return b[d]=c.$inputor.css(d)}),b},b.prototype.create=function(b){return this.$mirror=a("
    "),this.$mirror.css(this.mirrorCss()),this.$mirror.html(b),this.$inputor.after(this.$mirror),this},b.prototype.rect=function(){var a,b,c;return a=this.$mirror.find("#caret"),b=a.position(),c={left:b.left,top:b.top,height:a.height()},this.$mirror.remove(),c},b}(),e={contentEditable:function(a){return!(!a[0].contentEditable||"true"!==a[0].contentEditable)}},g={pos:function(a){return a||0===a?this.setPos(a):this.getPos()},position:function(a){return h.selection?this.getIEPosition(a):this.getPosition(a)},offset:function(a){var b;return b=this.getOffset(a)}},h=null,j=null,i=null,l=function(a){var b;return(b=null!=a?a.iframe:void 0)?(i=b,j=b.contentWindow,h=b.contentDocument||j.document):(i=void 0,j=window,h=document)},f=function(a){var b;h=a[0].ownerDocument,j=h.defaultView||h.parentWindow;try{return i=j.frameElement}catch(c){b=c}},a.fn.caret=function(d,f,h){var i;return g[d]?(a.isPlainObject(f)?(l(f),f=void 0):l(h),i=e.contentEditable(this)?new b(this):new c(this),g[d].apply(i,[f])):a.error("Method "+d+" does not exist on jQuery.caret")},a.fn.caret.EditableCaret=b,a.fn.caret.InputCaret=c,a.fn.caret.Utils=e,a.fn.caret.apis=g}); \ No newline at end of file diff --git a/phpbb/phpbb/mention/controller/mention.php b/phpbb/phpbb/mention/controller/mention.php index 8d731ae210..c8bc801e0f 100644 --- a/phpbb/phpbb/mention/controller/mention.php +++ b/phpbb/phpbb/mention/controller/mention.php @@ -43,10 +43,10 @@ class mention public function handle() { -// if (!$this->request->is_ajax()) -// { -// redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); -// } + if (!$this->request->is_ajax()) + { + redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); + } $keyword = $this->request->variable('keyword', '', true); $topic_id = $this->request->variable('topic_id', 0); @@ -57,6 +57,12 @@ class mention $names = array_merge($names, $source->get($keyword, $topic_id)); } - return new JsonResponse($names); + $clean_names = []; + foreach ($names as $name) + { + $clean_names[] = $name['name']; + } + + return new JsonResponse($clean_names); } } From 41b1b32e290aaadf773512b5aaa863496d555ff7 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 30 May 2018 20:52:35 +0300 Subject: [PATCH 006/113] [ticket/13713] Implement mention BBCode PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 1 + phpbb/phpbb/mention/controller/mention.php | 8 +-- phpbb/phpbb/mention/source/group.php | 4 +- phpbb/phpbb/mention/source/user.php | 4 +- .../textformatter/s9e/mention_helper.php | 69 +++++++++++++++++++ 5 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 phpbb/phpbb/textformatter/s9e/mention_helper.php diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index a2998a3f38..bdcd2b0b7a 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -388,6 +388,7 @@ function getCaretPosition(txtarea) { function handle_mentions(txtarea) { $(txtarea).atwho({ at: "@", + insertTpl: "[mention ${param}=${id}]${name}[/mention]", callbacks: { remoteFilter: function(query, callback) { $.getJSON(mention_url, {keyword: query, topic_id: mention_topic_id}, function (data) { diff --git a/phpbb/phpbb/mention/controller/mention.php b/phpbb/phpbb/mention/controller/mention.php index c8bc801e0f..106ba5744f 100644 --- a/phpbb/phpbb/mention/controller/mention.php +++ b/phpbb/phpbb/mention/controller/mention.php @@ -57,12 +57,6 @@ class mention $names = array_merge($names, $source->get($keyword, $topic_id)); } - $clean_names = []; - foreach ($names as $name) - { - $clean_names[] = $name['name']; - } - - return new JsonResponse($clean_names); + return new JsonResponse(array_values($names)); } } diff --git a/phpbb/phpbb/mention/source/group.php b/phpbb/phpbb/mention/source/group.php index b503ac714c..61225c6b6b 100644 --- a/phpbb/phpbb/mention/source/group.php +++ b/phpbb/phpbb/mention/source/group.php @@ -94,7 +94,9 @@ abstract class group implements source_interface foreach ($group_ids as $group_id) { $names['g' . $group_id] = [ - 'name' => $groups[$group_id]['group_name'], + 'name' => $groups[$group_id]['group_name'], + 'param' => 'group_id', + 'id' => $group_id, ]; } diff --git a/phpbb/phpbb/mention/source/user.php b/phpbb/phpbb/mention/source/user.php index 55f94e4866..6910a0b401 100644 --- a/phpbb/phpbb/mention/source/user.php +++ b/phpbb/phpbb/mention/source/user.php @@ -47,7 +47,9 @@ abstract class user implements source_interface while ($row = $this->db->sql_fetchrow($res)) { $names['u' . $row['user_id']] = [ - 'name' => $row['username'], + 'name' => $row['username'], + 'param' => 'user_id', + 'id' => $row['user_id'], ]; } diff --git a/phpbb/phpbb/textformatter/s9e/mention_helper.php b/phpbb/phpbb/textformatter/s9e/mention_helper.php new file mode 100644 index 0000000000..4f19a9dc5f --- /dev/null +++ b/phpbb/phpbb/textformatter/s9e/mention_helper.php @@ -0,0 +1,69 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +class mention_helper +{ + /** + * @var string Base URL for a user profile link, uses {USER_ID} as placeholder + */ + protected $user_profile_url; + + /** + * @var string Base URL for a group profile link, uses {GROUP_ID} as placeholder + */ + protected $group_profile_url; + + /** + * Constructor + * + * @param string $root_path + * @param string $php_ext + */ + public function __construct($root_path, $php_ext) + { + $this->user_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={USER_ID}', false); + $this->group_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=group&g={GROUP_ID}', false); + } + + /** + * Inject dynamic metadata into MENTION tags in given XML + * + * @param string $xml Original XML + * @return string Modified XML + */ + public function inject_metadata($xml) + { + $user_profile_url = $this->user_profile_url; + $group_profile_url = $this->group_profile_url; + + return \s9e\TextFormatter\Utils::replaceAttributes( + $xml, + 'MENTION', + function ($attributes) use ($user_profile_url, $group_profile_url) + { + if (isset($attributes['user_id'])) + { + $attributes['profile_url'] = str_replace('{USER_ID}', $attributes['user_id'], $user_profile_url); + } + else if (isset($attributes['group_id'])) + { + $attributes['profile_url'] = str_replace('{GROUP_ID}', $attributes['group_id'], $group_profile_url); + } + + return $attributes; + } + ); + } +} From 6c42563b4d567107c754f6b66034d4ae067852fa Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 30 May 2018 21:58:28 +0300 Subject: [PATCH 007/113] [ticket/13713] Add mention BBCode PHPBB3-13713 --- .../container/services_text_formatter.yml | 7 +++++++ phpBB/includes/constants.php | 1 + phpBB/phpbb/textformatter/data_access.php | 1 + phpBB/phpbb/textformatter/s9e/factory.php | 6 ++++++ phpBB/phpbb/textformatter/s9e/renderer.php | 20 +++++++++++++++++++ phpBB/styles/prosilver/template/bbcode.html | 16 +++++++++++++++ 6 files changed, 51 insertions(+) diff --git a/phpBB/config/default/container/services_text_formatter.yml b/phpBB/config/default/container/services_text_formatter.yml index 4e4abf6564..df24d9a080 100644 --- a/phpBB/config/default/container/services_text_formatter.yml +++ b/phpBB/config/default/container/services_text_formatter.yml @@ -52,6 +52,12 @@ services: text_formatter.s9e.link_helper: class: phpbb\textformatter\s9e\link_helper + text_formatter.s9e.mention_helper: + class: phpbb\textformatter\s9e\mention_helper + arguments: + - '%core.root_path%' + - '%core.php_ext%' + text_formatter.s9e.parser: class: phpbb\textformatter\s9e\parser arguments: @@ -76,6 +82,7 @@ services: - '@text_formatter.s9e.factory' - '@dispatcher' calls: + - [configure_mention_helper, ['@text_formatter.s9e.mention_helper']] - [configure_quote_helper, ['@text_formatter.s9e.quote_helper']] - [configure_smilies_path, ['@config', '@path_helper']] - [configure_user, ['@user', '@config', '@auth']] diff --git a/phpBB/includes/constants.php b/phpBB/includes/constants.php index be6916442b..1a7d52a38b 100644 --- a/phpBB/includes/constants.php +++ b/phpBB/includes/constants.php @@ -183,6 +183,7 @@ define('BBCODE_ID_LIST', 9); define('BBCODE_ID_EMAIL', 10); define('BBCODE_ID_FLASH', 11); define('BBCODE_ID_ATTACH', 12); +define('BBCODE_ID_MENTION', 13); // BBCode hard limit define('BBCODE_LIMIT', 1511); diff --git a/phpBB/phpbb/textformatter/data_access.php b/phpBB/phpbb/textformatter/data_access.php index 27ce778904..bbb6ba0f0d 100644 --- a/phpBB/phpbb/textformatter/data_access.php +++ b/phpBB/phpbb/textformatter/data_access.php @@ -138,6 +138,7 @@ class data_access 'email' => 10, 'flash' => 11, 'attachment' => 12, + 'mention' => 13, ); $styles = array(); diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php index 721549cf72..6497f5fd47 100644 --- a/phpBB/phpbb/textformatter/s9e/factory.php +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -84,6 +84,12 @@ class factory implements \phpbb\textformatter\cache_interface 'img' => '[IMG src={IMAGEURL;useContent}]', 'list' => '[LIST type={HASHMAP=1:decimal,a:lower-alpha,A:upper-alpha,i:lower-roman,I:upper-roman;optional;postFilter=#simpletext} #createChild=LI]{TEXT}[/LIST]', 'li' => '[* $tagName=LI]{TEXT}[/*]', + 'mention' => + "[MENTION + group_id={UINT;optional} + profile_url={URL;optional;postFilter=#false} + user_id={UINT;optional} + ]{TEXT}[/MENTION]", 'quote' => "[QUOTE author={TEXT1;optional} diff --git a/phpBB/phpbb/textformatter/s9e/renderer.php b/phpBB/phpbb/textformatter/s9e/renderer.php index 6fcd2b0a98..cc909ea90c 100644 --- a/phpBB/phpbb/textformatter/s9e/renderer.php +++ b/phpBB/phpbb/textformatter/s9e/renderer.php @@ -28,6 +28,11 @@ class renderer implements \phpbb\textformatter\renderer_interface */ protected $dispatcher; + /** + * @var mention_helper + */ + protected $mention_helper; + /** * @var quote_helper */ @@ -117,6 +122,16 @@ class renderer implements \phpbb\textformatter\renderer_interface extract($dispatcher->trigger_event('core.text_formatter_s9e_renderer_setup', compact($vars))); } + /** + * Configure the mention_helper object used to display extended information in mentions + * + * @param mention_helper $mention_helper + */ + public function configure_mention_helper(mention_helper $mention_helper) + { + $this->mention_helper = $mention_helper; + } + /** * Configure the quote_helper object used to display extended information in quotes * @@ -229,6 +244,11 @@ class renderer implements \phpbb\textformatter\renderer_interface */ public function render($xml) { + if (isset($this->mention_helper)) + { + $xml = $this->mention_helper->inject_metadata($xml); + } + if (isset($this->quote_helper)) { $xml = $this->quote_helper->inject_metadata($xml); diff --git a/phpBB/styles/prosilver/template/bbcode.html b/phpBB/styles/prosilver/template/bbcode.html index 2780d869d2..71e8d2b0d2 100644 --- a/phpBB/styles/prosilver/template/bbcode.html +++ b/phpBB/styles/prosilver/template/bbcode.html @@ -8,6 +8,22 @@
  • + + + + + + + + + + + + + + + +
    {USERNAME} {L_WROTE}{L_COLON}
    From eec7703d3b155cebeceb8d222bf8a4bb17056843 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 30 May 2018 22:06:26 +0300 Subject: [PATCH 008/113] [ticket/13713] Fix case for phpBB directory PHPBB3-13713 --- {phpbb => phpBB}/assets/css/jquery.atwho.min.css | 0 {phpbb => phpBB}/assets/javascript/jquery.atwho.min.js | 0 {phpbb => phpBB}/assets/javascript/jquery.caret.min.js | 0 {phpbb => phpBB}/config/default/container/services_mention.yml | 0 {phpbb => phpBB}/phpbb/mention/controller/mention.php | 0 {phpbb => phpBB}/phpbb/mention/source/friend.php | 0 {phpbb => phpBB}/phpbb/mention/source/group.php | 0 {phpbb => phpBB}/phpbb/mention/source/source_interface.php | 0 {phpbb => phpBB}/phpbb/mention/source/topic.php | 0 {phpbb => phpBB}/phpbb/mention/source/user.php | 0 {phpbb => phpBB}/phpbb/mention/source/usergroup.php | 0 {phpbb => phpBB}/phpbb/textformatter/s9e/mention_helper.php | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename {phpbb => phpBB}/assets/css/jquery.atwho.min.css (100%) rename {phpbb => phpBB}/assets/javascript/jquery.atwho.min.js (100%) rename {phpbb => phpBB}/assets/javascript/jquery.caret.min.js (100%) rename {phpbb => phpBB}/config/default/container/services_mention.yml (100%) rename {phpbb => phpBB}/phpbb/mention/controller/mention.php (100%) rename {phpbb => phpBB}/phpbb/mention/source/friend.php (100%) rename {phpbb => phpBB}/phpbb/mention/source/group.php (100%) rename {phpbb => phpBB}/phpbb/mention/source/source_interface.php (100%) rename {phpbb => phpBB}/phpbb/mention/source/topic.php (100%) rename {phpbb => phpBB}/phpbb/mention/source/user.php (100%) rename {phpbb => phpBB}/phpbb/mention/source/usergroup.php (100%) rename {phpbb => phpBB}/phpbb/textformatter/s9e/mention_helper.php (100%) diff --git a/phpbb/assets/css/jquery.atwho.min.css b/phpBB/assets/css/jquery.atwho.min.css similarity index 100% rename from phpbb/assets/css/jquery.atwho.min.css rename to phpBB/assets/css/jquery.atwho.min.css diff --git a/phpbb/assets/javascript/jquery.atwho.min.js b/phpBB/assets/javascript/jquery.atwho.min.js similarity index 100% rename from phpbb/assets/javascript/jquery.atwho.min.js rename to phpBB/assets/javascript/jquery.atwho.min.js diff --git a/phpbb/assets/javascript/jquery.caret.min.js b/phpBB/assets/javascript/jquery.caret.min.js similarity index 100% rename from phpbb/assets/javascript/jquery.caret.min.js rename to phpBB/assets/javascript/jquery.caret.min.js diff --git a/phpbb/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml similarity index 100% rename from phpbb/config/default/container/services_mention.yml rename to phpBB/config/default/container/services_mention.yml diff --git a/phpbb/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php similarity index 100% rename from phpbb/phpbb/mention/controller/mention.php rename to phpBB/phpbb/mention/controller/mention.php diff --git a/phpbb/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php similarity index 100% rename from phpbb/phpbb/mention/source/friend.php rename to phpBB/phpbb/mention/source/friend.php diff --git a/phpbb/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php similarity index 100% rename from phpbb/phpbb/mention/source/group.php rename to phpBB/phpbb/mention/source/group.php diff --git a/phpbb/phpbb/mention/source/source_interface.php b/phpBB/phpbb/mention/source/source_interface.php similarity index 100% rename from phpbb/phpbb/mention/source/source_interface.php rename to phpBB/phpbb/mention/source/source_interface.php diff --git a/phpbb/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php similarity index 100% rename from phpbb/phpbb/mention/source/topic.php rename to phpBB/phpbb/mention/source/topic.php diff --git a/phpbb/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php similarity index 100% rename from phpbb/phpbb/mention/source/user.php rename to phpBB/phpbb/mention/source/user.php diff --git a/phpbb/phpbb/mention/source/usergroup.php b/phpBB/phpbb/mention/source/usergroup.php similarity index 100% rename from phpbb/phpbb/mention/source/usergroup.php rename to phpBB/phpbb/mention/source/usergroup.php diff --git a/phpbb/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php similarity index 100% rename from phpbb/phpbb/textformatter/s9e/mention_helper.php rename to phpBB/phpbb/textformatter/s9e/mention_helper.php From a176be4c1dbebadf616bdcae2392880c419990ca Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 1 Jun 2018 17:59:31 +0300 Subject: [PATCH 009/113] [ticket/13713] Implement avatars PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 5 ++++ .../default/container/services_mention.yml | 2 ++ phpBB/phpbb/mention/controller/mention.php | 8 +++--- phpBB/phpbb/mention/source/friend.php | 4 +-- phpBB/phpbb/mention/source/group.php | 10 ++++--- phpBB/phpbb/mention/source/user.php | 16 ++++++++--- phpBB/styles/prosilver/theme/bidi.css | 11 ++++++++ phpBB/styles/prosilver/theme/colours.css | 12 +++++++++ phpBB/styles/prosilver/theme/forms.css | 27 +++++++++++++++++++ 9 files changed, 82 insertions(+), 13 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index bdcd2b0b7a..0ba7f81398 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -388,6 +388,11 @@ function getCaretPosition(txtarea) { function handle_mentions(txtarea) { $(txtarea).atwho({ at: "@", + displayTpl: function(data) { + var avatar = (data.avatar.src) ? "" : + ""; + return "
  • " + avatar + "" + data.name + "
  • "; + }, insertTpl: "[mention ${param}=${id}]${name}[/mention]", callbacks: { remoteFilter: function(query, callback) { diff --git a/phpBB/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml index bae6b45bfe..7a582bb6b6 100644 --- a/phpBB/config/default/container/services_mention.yml +++ b/phpBB/config/default/container/services_mention.yml @@ -20,6 +20,7 @@ services: class: phpbb\mention\source\friend arguments: - '@dbal.conn' + - '@user_loader' - '@user' tags: - { name: mention.source } @@ -28,6 +29,7 @@ services: class: phpbb\mention\source\topic arguments: - '@dbal.conn' + - '@user_loader' tags: - { name: mention.source } diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index 106ba5744f..b4a42799b9 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -43,10 +43,10 @@ class mention public function handle() { - if (!$this->request->is_ajax()) - { - redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); - } +// if (!$this->request->is_ajax()) +// { +// redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); +// } $keyword = $this->request->variable('keyword', '', true); $topic_id = $this->request->variable('topic_id', 0); diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index bb3ba9ecb7..bf29daae61 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -21,11 +21,11 @@ class friend extends user /** * Constructor */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user $user) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader, \phpbb\user $user) { $this->user = $user; - parent::__construct($db); + parent::__construct($db, $user_loader); } /** diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index 61225c6b6b..0d004814e3 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -94,9 +94,13 @@ abstract class group implements source_interface foreach ($group_ids as $group_id) { $names['g' . $group_id] = [ - 'name' => $groups[$group_id]['group_name'], - 'param' => 'group_id', - 'id' => $group_id, + 'name' => $groups[$group_id]['group_name'], + 'param' => 'group_id', + 'id' => $group_id, + 'avatar' => [ + 'type' => 'group', + 'src' => phpbb_get_group_avatar($groups[$group_id]), + ], ]; } diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index 6910a0b401..72f84d659c 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -18,12 +18,16 @@ abstract class user implements source_interface /** @var \phpbb\db\driver\driver_interface */ protected $db; + /** @var \phpbb\user_loader */ + protected $user_loader; + /** * Constructor */ - public function __construct(\phpbb\db\driver\driver_interface $db) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader) { $this->db = $db; + $this->user_loader = $user_loader; } /** @@ -47,9 +51,13 @@ abstract class user implements source_interface while ($row = $this->db->sql_fetchrow($res)) { $names['u' . $row['user_id']] = [ - 'name' => $row['username'], - 'param' => 'user_id', - 'id' => $row['user_id'], + 'name' => $row['username'], + 'param' => 'user_id', + 'id' => $row['user_id'], + 'avatar' => [ + 'type' => 'user', + 'src' => $this->user_loader->get_avatar($row['user_id'], true), + ], ]; } diff --git a/phpBB/styles/prosilver/theme/bidi.css b/phpBB/styles/prosilver/theme/bidi.css index bcf271b0c8..6d6a2b5cb1 100644 --- a/phpBB/styles/prosilver/theme/bidi.css +++ b/phpBB/styles/prosilver/theme/bidi.css @@ -930,6 +930,17 @@ float: left; } +/* Mention dropdown */ +.atwho-container .atwho-view ul li { + padding-right: 45px; + padding-left: 15px; +} + +.mention-avatar { + right: 8px; + left: auto; +} + /* Search box ---------------------------------------- */ diff --git a/phpBB/styles/prosilver/theme/colours.css b/phpBB/styles/prosilver/theme/colours.css index 60d8b03c36..12e432cbbb 100644 --- a/phpBB/styles/prosilver/theme/colours.css +++ b/phpBB/styles/prosilver/theme/colours.css @@ -983,6 +983,18 @@ fieldset.fields2 dl:hover dt label { outline-color: rgba(19, 164, 236, 0.5); } +.atwho-container .atwho-view ul li:hover, +.atwho-container .atwho-view ul li.cur { + background-color: #0077b3; + color: #ffffff; +} + +.mention-avatar { + background-color: #0077b3; + border-color: #ffffff; + color: #ffffff; +} + /* input field styles */ .inputbox { background-color: #ffffff; diff --git a/phpBB/styles/prosilver/theme/forms.css b/phpBB/styles/prosilver/theme/forms.css index e05cf090ca..85b58dbc1d 100644 --- a/phpBB/styles/prosilver/theme/forms.css +++ b/phpBB/styles/prosilver/theme/forms.css @@ -284,6 +284,33 @@ fieldset.submit-buttons input { margin: 3px; } +/* Mention dropdown */ +.atwho-container .atwho-view { + font-size: 12px; + min-width: 300px; +} + +.atwho-container .atwho-view ul li { + position: relative; + padding: 15px 5px 15px 45px; +} + +.mention-avatar { + font-size: 14px; + line-height: 30px; + text-align: center; + vertical-align: middle; + border: 1px solid transparent; + border-radius: 100%; + position: absolute; + top: 50%; + left: 8px; + display: inline-block; + width: 30px; + height: 30px; + margin-top: -15px; +} + /* Input field styles ---------------------------------------- */ .inputbox { From f775c1e79d9e414d532590d30aeeaaf12178c1e3 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 1 Jun 2018 18:46:39 +0300 Subject: [PATCH 010/113] [ticket/13713] Implement ranks PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 5 +++-- .../default/container/services_mention.yml | 6 ++++++ phpBB/phpbb/mention/source/friend.php | 4 ++-- phpBB/phpbb/mention/source/group.php | 17 ++++++++++++++++- phpBB/phpbb/mention/source/user.php | 17 ++++++++++++++++- phpBB/phpbb/mention/source/usergroup.php | 4 ++-- phpBB/styles/prosilver/theme/bidi.css | 2 +- phpBB/styles/prosilver/theme/forms.css | 12 +++++++++--- 8 files changed, 55 insertions(+), 12 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 0ba7f81398..63d28e918a 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -390,8 +390,9 @@ function getCaretPosition(txtarea) { at: "@", displayTpl: function(data) { var avatar = (data.avatar.src) ? "" : - ""; - return "
  • " + avatar + "" + data.name + "
  • "; + "", + rank = (data.rank) ? "" + data.rank + "" : ''; + return "
  • " + avatar + "" + data.name + "" + rank + "
  • "; }, insertTpl: "[mention ${param}=${id}]${name}[/mention]", callbacks: { diff --git a/phpBB/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml index 7a582bb6b6..286c16a8da 100644 --- a/phpBB/config/default/container/services_mention.yml +++ b/phpBB/config/default/container/services_mention.yml @@ -22,6 +22,8 @@ services: - '@dbal.conn' - '@user_loader' - '@user' + - '%core.root_path%' + - '%core.php_ext%' tags: - { name: mention.source } @@ -30,6 +32,8 @@ services: arguments: - '@dbal.conn' - '@user_loader' + - '%core.root_path%' + - '%core.php_ext%' tags: - { name: mention.source } @@ -39,5 +43,7 @@ services: - '@dbal.conn' - '@group_helper' - '@user' + - '%core.root_path%' + - '%core.php_ext%' tags: - { name: mention.source } diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index bf29daae61..d63efa4c72 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -21,11 +21,11 @@ class friend extends user /** * Constructor */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader, \phpbb\user $user) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader, \phpbb\user $user, $phpbb_root_path, $phpEx) { $this->user = $user; - parent::__construct($db, $user_loader); + parent::__construct($db, $user_loader, $phpbb_root_path, $phpEx); } /** diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index 0d004814e3..c27c7d6d33 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -21,13 +21,26 @@ abstract class group implements source_interface /** @var \phpbb\group\helper */ protected $helper; + /** @var string */ + protected $phpbb_root_path; + + /** @var string */ + protected $php_ext; + /** * Constructor */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper, $phpbb_root_path, $phpEx) { $this->db = $db; $this->helper = $helper; + $this->phpbb_root_path = $phpbb_root_path; + $this->php_ext = $phpEx; + + if (!function_exists('phpbb_get_user_rank')) + { + include($this->phpbb_root_path . 'includes/functions_display.' . $this->php_ext); + } } /** @@ -93,6 +106,7 @@ abstract class group implements source_interface $names = []; foreach ($group_ids as $group_id) { + $group_rank = phpbb_get_user_rank($groups[$group_id], false); $names['g' . $group_id] = [ 'name' => $groups[$group_id]['group_name'], 'param' => 'group_id', @@ -101,6 +115,7 @@ abstract class group implements source_interface 'type' => 'group', 'src' => phpbb_get_group_avatar($groups[$group_id]), ], + 'rank' => $group_rank['title'], ]; } diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index 72f84d659c..69fe6b8bb8 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -21,13 +21,26 @@ abstract class user implements source_interface /** @var \phpbb\user_loader */ protected $user_loader; + /** @var string */ + protected $phpbb_root_path; + + /** @var string */ + protected $php_ext; + /** * Constructor */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader, $phpbb_root_path, $phpEx) { $this->db = $db; $this->user_loader = $user_loader; + $this->phpbb_root_path = $phpbb_root_path; + $this->php_ext = $phpEx; + + if (!function_exists('phpbb_get_user_rank')) + { + include($this->phpbb_root_path . 'includes/functions_display.' . $this->php_ext); + } } /** @@ -50,6 +63,7 @@ abstract class user implements source_interface $names = []; while ($row = $this->db->sql_fetchrow($res)) { + $user_rank = $this->user_loader->get_rank($row['user_id'], true); $names['u' . $row['user_id']] = [ 'name' => $row['username'], 'param' => 'user_id', @@ -58,6 +72,7 @@ abstract class user implements source_interface 'type' => 'user', 'src' => $this->user_loader->get_avatar($row['user_id'], true), ], + 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', ]; } diff --git a/phpBB/phpbb/mention/source/usergroup.php b/phpBB/phpbb/mention/source/usergroup.php index 1a0e20eff8..6d965961be 100644 --- a/phpBB/phpbb/mention/source/usergroup.php +++ b/phpBB/phpbb/mention/source/usergroup.php @@ -21,11 +21,11 @@ class usergroup extends group /** * Constructor */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper, \phpbb\user $user) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper, \phpbb\user $user, $phpbb_root_path, $phpEx) { $this->user = $user; - parent::__construct($db, $helper); + parent::__construct($db, $helper, $phpbb_root_path, $phpEx); } /** diff --git a/phpBB/styles/prosilver/theme/bidi.css b/phpBB/styles/prosilver/theme/bidi.css index 6d6a2b5cb1..127ac48c71 100644 --- a/phpBB/styles/prosilver/theme/bidi.css +++ b/phpBB/styles/prosilver/theme/bidi.css @@ -937,7 +937,7 @@ } .mention-avatar { - right: 8px; + right: 7px; left: auto; } diff --git a/phpBB/styles/prosilver/theme/forms.css b/phpBB/styles/prosilver/theme/forms.css index 85b58dbc1d..6a5048ad8d 100644 --- a/phpBB/styles/prosilver/theme/forms.css +++ b/phpBB/styles/prosilver/theme/forms.css @@ -287,7 +287,7 @@ fieldset.submit-buttons input { /* Mention dropdown */ .atwho-container .atwho-view { font-size: 12px; - min-width: 300px; + min-width: 260px; } .atwho-container .atwho-view ul li { @@ -304,11 +304,17 @@ fieldset.submit-buttons input { border-radius: 100%; position: absolute; top: 50%; - left: 8px; + left: 7px; display: inline-block; width: 30px; height: 30px; - margin-top: -15px; + margin-top: -16px; +} + +.mention-rank { + font-size: 10px; + display: block; + margin-top: 2px; } /* Input field styles From 4b31a29c2cfd809b26874cec5b0954ee3ccb88ac Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 6 Jun 2018 03:51:52 +0300 Subject: [PATCH 011/113] [ticket/13713] Implement colour handling PHPBB3-13713 --- .../container/services_text_formatter.yml | 1 + .../textformatter/s9e/mention_helper.php | 83 ++++++++++++++++++- phpBB/styles/prosilver/template/bbcode.html | 8 +- phpBB/styles/prosilver/theme/content.css | 5 ++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/phpBB/config/default/container/services_text_formatter.yml b/phpBB/config/default/container/services_text_formatter.yml index df24d9a080..119e0b2ba4 100644 --- a/phpBB/config/default/container/services_text_formatter.yml +++ b/phpBB/config/default/container/services_text_formatter.yml @@ -55,6 +55,7 @@ services: text_formatter.s9e.mention_helper: class: phpbb\textformatter\s9e\mention_helper arguments: + - '@dbal.conn' - '%core.root_path%' - '%core.php_ext%' diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index 4f19a9dc5f..96bc896bad 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -13,8 +13,15 @@ namespace phpbb\textformatter\s9e; +use s9e\TextFormatter\Utils; + class mention_helper { + /** + * @var \phpbb\db\driver\driver_interface + */ + protected $db; + /** * @var string Base URL for a user profile link, uses {USER_ID} as placeholder */ @@ -25,18 +32,74 @@ class mention_helper */ protected $group_profile_url; + /** + * @var array Array of users' and groups' colors for each cached ID + */ + protected $cached_colors = []; + /** * Constructor * + * @param \phpbb\db\driver\driver_interface $db * @param string $root_path * @param string $php_ext */ - public function __construct($root_path, $php_ext) + public function __construct($db, $root_path, $php_ext) { + $this->db = $db; $this->user_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={USER_ID}', false); $this->group_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=group&g={GROUP_ID}', false); } + /** + * Caches colors for specified user IDs and group IDs + * + * @param array $user_ids + * @param array $group_ids + */ + protected function get_colors($user_ids, $group_ids) + { + $this->cached_colors = []; + $this->cached_colors['users'] = []; + $this->cached_colors['groups'] = []; + + if (!empty($user_ids)) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.user_colour, u.user_id', + 'FROM' => [ + USERS_TABLE => 'u', + ], + 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND ' . $this->db->sql_in_set('u.user_id', $user_ids), + ]); + $res = $this->db->sql_query($query); + + while ($row = $this->db->sql_fetchrow($res)) + { + $this->cached_colors['users'][$row['user_id']] = $row['user_colour']; + } + } + + if (!empty($group_ids)) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'g.group_colour, g.group_id', + 'FROM' => [ + GROUPS_TABLE => 'g', + ], + 'WHERE' => $this->db->sql_in_set('g.group_id', $group_ids), + ]); + $res = $this->db->sql_query($query); + + while ($row = $this->db->sql_fetchrow($res)) + { + $this->cached_colors['groups'][$row['group_id']] = $row['group_colour']; + } + } + } + /** * Inject dynamic metadata into MENTION tags in given XML * @@ -48,7 +111,13 @@ class mention_helper $user_profile_url = $this->user_profile_url; $group_profile_url = $this->group_profile_url; - return \s9e\TextFormatter\Utils::replaceAttributes( + // TODO: think about optimization for caching colors. + $this->get_colors( + Utils::getAttributeValues($xml, 'MENTION', 'user_id'), + Utils::getAttributeValues($xml, 'MENTION', 'group_id') + ); + + return Utils::replaceAttributes( $xml, 'MENTION', function ($attributes) use ($user_profile_url, $group_profile_url) @@ -56,10 +125,20 @@ class mention_helper if (isset($attributes['user_id'])) { $attributes['profile_url'] = str_replace('{USER_ID}', $attributes['user_id'], $user_profile_url); + + if (isset($this->cached_colors['users'][$attributes['user_id']])) + { + $attributes['color'] = $this->cached_colors['users'][$attributes['user_id']]; + } } else if (isset($attributes['group_id'])) { $attributes['profile_url'] = str_replace('{GROUP_ID}', $attributes['group_id'], $group_profile_url); + + if (isset($this->cached_colors['groups'][$attributes['group_id']])) + { + $attributes['color'] = $this->cached_colors['groups'][$attributes['group_id']]; + } } return $attributes; diff --git a/phpBB/styles/prosilver/template/bbcode.html b/phpBB/styles/prosilver/template/bbcode.html index 71e8d2b0d2..8ee7351486 100644 --- a/phpBB/styles/prosilver/template/bbcode.html +++ b/phpBB/styles/prosilver/template/bbcode.html @@ -9,15 +9,19 @@ +@ - + + + color: #; + - + diff --git a/phpBB/styles/prosilver/theme/content.css b/phpBB/styles/prosilver/theme/content.css index 338016ac2c..701fa5fd94 100644 --- a/phpBB/styles/prosilver/theme/content.css +++ b/phpBB/styles/prosilver/theme/content.css @@ -576,6 +576,11 @@ blockquote .codebox { padding: 5px 3px; } +/* Mention block */ +.mention { + font-weight: bold; +} + /* Attachments ---------------------------------------- */ .attachbox { From ac8adcb9a653a8815bea9bc8509cb44a3bae78e8 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 6 Jun 2018 03:58:33 +0300 Subject: [PATCH 012/113] [ticket/13713] Properly handle empty colour strings PHPBB3-13713 --- phpBB/phpbb/textformatter/s9e/mention_helper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index 96bc896bad..0348809cbc 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -126,7 +126,7 @@ class mention_helper { $attributes['profile_url'] = str_replace('{USER_ID}', $attributes['user_id'], $user_profile_url); - if (isset($this->cached_colors['users'][$attributes['user_id']])) + if (!empty($this->cached_colors['users'][$attributes['user_id']])) { $attributes['color'] = $this->cached_colors['users'][$attributes['user_id']]; } @@ -135,7 +135,7 @@ class mention_helper { $attributes['profile_url'] = str_replace('{GROUP_ID}', $attributes['group_id'], $group_profile_url); - if (isset($this->cached_colors['groups'][$attributes['group_id']])) + if (!empty($this->cached_colors['groups'][$attributes['group_id']])) { $attributes['color'] = $this->cached_colors['groups'][$attributes['group_id']]; } From 8817e234747375fc01276f01fbc3c949ef24d52b Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 6 Jun 2018 12:39:51 +0300 Subject: [PATCH 013/113] [ticket/13713] Make mentions globally available PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 5 +++ phpBB/adm/style/admin.css | 46 ++++++++++++++++++++++++ phpBB/includes/functions.php | 2 ++ phpBB/includes/functions_acp.php | 5 +++ phpBB/posting.php | 1 - 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index dfe09ae12e..be15ab40d3 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -1,5 +1,7 @@ + + + diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 2a5eacfcd7..620a658167 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1670,6 +1670,52 @@ fieldset.submit-buttons legend { } } +/* Mentions and mention dropdown +---------------------------------------- */ +.mention { + font-weight: bold; +} + +.atwho-container .atwho-view { + font-size: 12px; + min-width: 260px; +} + +.atwho-container .atwho-view ul li { + position: relative; + padding: 15px 5px 15px 45px; +} + +.atwho-container .atwho-view ul li:hover, +.atwho-container .atwho-view ul li.cur { + background-color: #0077b3; + color: #ffffff; +} + +.mention-avatar { + font-size: 14px; + line-height: 30px; + text-align: center; + vertical-align: middle; + background-color: #0077b3; + border: 1px solid #ffffff; + border-radius: 100%; + color: #ffffff; + position: absolute; + top: 50%; + left: 7px; + display: inline-block; + width: 30px; + height: 30px; + margin-top: -16px; +} + +.mention-rank { + font-size: 10px; + display: block; + margin-top: 2px; +} + /* Input field styles ---------------------------------------- */ input.radio, diff --git a/phpBB/includes/functions.php b/phpBB/includes/functions.php index 337242bcd7..adc09b9382 100644 --- a/phpBB/includes/functions.php +++ b/phpBB/includes/functions.php @@ -3943,6 +3943,8 @@ function page_header($page_title = '', $display_online_list = false, $item_id = 'U_RESTORE_PERMISSIONS' => ($user->data['user_perm_from'] && $auth->acl_get('a_switchperm')) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=restore_perm') : '', 'U_FEED' => $controller_helper->route('phpbb_feed_index'), + 'UA_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), + 'S_USER_LOGGED_IN' => ($user->data['user_id'] != ANONYMOUS) ? true : false, 'S_AUTOLOGIN_ENABLED' => ($config['allow_autologin']) ? true : false, 'S_BOARD_DISABLED' => ($config['board_disable']) ? true : false, diff --git a/phpBB/includes/functions_acp.php b/phpBB/includes/functions_acp.php index ff2a8badb8..ce6712ffd0 100644 --- a/phpBB/includes/functions_acp.php +++ b/phpBB/includes/functions_acp.php @@ -66,6 +66,9 @@ function adm_page_header($page_title) } } + /** @var \phpbb\controller\helper $controller_helper */ + $controller_helper = $phpbb_container->get('controller.helper'); + $phpbb_version_parts = explode('.', PHPBB_VERSION, 3); $phpbb_major = $phpbb_version_parts[0] . '.' . $phpbb_version_parts[1]; @@ -86,6 +89,8 @@ function adm_page_header($page_title) 'U_ADM_INDEX' => append_sid("{$phpbb_admin_path}index.$phpEx"), 'U_INDEX' => append_sid("{$phpbb_root_path}index.$phpEx"), + 'UA_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), + 'T_IMAGES_PATH' => "{$phpbb_root_path}images/", 'T_SMILIES_PATH' => "{$phpbb_root_path}{$config['smilies_path']}/", 'T_AVATAR_GALLERY_PATH' => "{$phpbb_root_path}{$config['avatar_gallery_path']}/", diff --git a/phpBB/posting.php b/phpBB/posting.php index 7c29f205f5..696c3346cb 100644 --- a/phpBB/posting.php +++ b/phpBB/posting.php @@ -1905,7 +1905,6 @@ $page_data = array( 'U_VIEW_TOPIC' => ($mode != 'post') ? append_sid("{$phpbb_root_path}viewtopic.$phpEx", "f=$forum_id&t=$topic_id") : '', 'U_PROGRESS_BAR' => append_sid("{$phpbb_root_path}posting.$phpEx", "f=$forum_id&mode=popup"), 'UA_PROGRESS_BAR' => addslashes(append_sid("{$phpbb_root_path}posting.$phpEx", "f=$forum_id&mode=popup")), - 'UA_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), 'S_PRIVMSGS' => false, 'S_CLOSE_PROGRESS_WINDOW' => (isset($_POST['add_file'])) ? true : false, From 99fe5910882be927f443efa9550f05c6f0f8ee15 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 6 Jun 2018 19:32:15 +0300 Subject: [PATCH 014/113] [ticket/13713] Change $res to $result PHPBB3-13713 --- phpBB/phpbb/mention/source/group.php | 8 ++++---- phpBB/phpbb/mention/source/user.php | 4 ++-- phpBB/phpbb/textformatter/s9e/mention_helper.php | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index c27c7d6d33..cf725789c1 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -60,10 +60,10 @@ abstract class group implements source_interface GROUPS_TABLE => 'g', ], ]); - $res = $this->db->sql_query($query); + $result = $this->db->sql_query($query); $groups = []; - while ($row = $this->db->sql_fetchrow($res)) + while ($row = $this->db->sql_fetchrow($result)) { $group_name = $this->helper->get_name($row['group_name']); $groups['names'][$row['group_id']] = $group_name; @@ -89,10 +89,10 @@ abstract class group implements source_interface public function get($keyword, $topic_id) { // Grab all group IDs - $res = $this->db->sql_query($this->query($keyword, $topic_id)); + $result = $this->db->sql_query($this->query($keyword, $topic_id)); $group_ids = []; - while ($row = $this->db->sql_fetchrow($res)) + while ($row = $this->db->sql_fetchrow($result)) { $group_ids[] = $row['group_id']; } diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index 69fe6b8bb8..d691634859 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -58,10 +58,10 @@ abstract class user implements source_interface public function get($keyword, $topic_id) { $keyword = utf8_clean_string($keyword); - $res = $this->db->sql_query_limit($this->query($keyword, $topic_id), 5); + $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), 5); $names = []; - while ($row = $this->db->sql_fetchrow($res)) + while ($row = $this->db->sql_fetchrow($result)) { $user_rank = $this->user_loader->get_rank($row['user_id'], true); $names['u' . $row['user_id']] = [ diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index 0348809cbc..91530d5c5c 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -74,9 +74,9 @@ class mention_helper AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND ' . $this->db->sql_in_set('u.user_id', $user_ids), ]); - $res = $this->db->sql_query($query); + $result = $this->db->sql_query($query); - while ($row = $this->db->sql_fetchrow($res)) + while ($row = $this->db->sql_fetchrow($result)) { $this->cached_colors['users'][$row['user_id']] = $row['user_colour']; } @@ -91,9 +91,9 @@ class mention_helper ], 'WHERE' => $this->db->sql_in_set('g.group_id', $group_ids), ]); - $res = $this->db->sql_query($query); + $result = $this->db->sql_query($query); - while ($row = $this->db->sql_fetchrow($res)) + while ($row = $this->db->sql_fetchrow($result)) { $this->cached_colors['groups'][$row['group_id']] = $row['group_colour']; } From 2fe0a2ffd91218709dddfaf645f3ffeb8bca0a42 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 6 Jun 2018 19:44:20 +0300 Subject: [PATCH 015/113] [ticket/13713] Move mention template to default templates PHPBB3-13713 --- phpBB/phpbb/textformatter/s9e/factory.php | 31 ++++++++++++++++----- phpBB/styles/prosilver/template/bbcode.html | 20 ------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php index 6497f5fd47..1edb170634 100644 --- a/phpBB/phpbb/textformatter/s9e/factory.php +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -114,13 +114,13 @@ class factory implements \phpbb\textformatter\cache_interface * @var array Default templates, taken from bbcode::bbcode_tpl() */ protected $default_templates = array( - 'b' => '', - 'i' => '', - 'u' => '', - 'img' => '{L_IMAGE}', - 'size' => 'font-size: %; line-height: normal', - 'color' => '', - 'email' => ' + 'b' => '', + 'i' => '', + 'u' => '', + 'img' => '{L_IMAGE}', + 'size' => 'font-size: %; line-height: normal', + 'color' => '', + 'email' => ' mailto: @@ -132,6 +132,23 @@ class factory implements \phpbb\textformatter\cache_interface ', + 'mention' => '@ + + + + + + color: #; + + + + + + + + + + ', ); /** diff --git a/phpBB/styles/prosilver/template/bbcode.html b/phpBB/styles/prosilver/template/bbcode.html index 8ee7351486..2780d869d2 100644 --- a/phpBB/styles/prosilver/template/bbcode.html +++ b/phpBB/styles/prosilver/template/bbcode.html @@ -8,26 +8,6 @@
  • - -@ - - - - - - color: #; - - - - - - - - - - - -
    {USERNAME} {L_WROTE}{L_COLON}
    From 9eef103e7578c93490841696fcd428fc8f89e273 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 6 Jun 2018 20:02:04 +0300 Subject: [PATCH 016/113] [ticket/13713] Free SQL results PHPBB3-13713 --- phpBB/phpbb/mention/source/group.php | 4 ++++ phpBB/phpbb/mention/source/user.php | 2 ++ phpBB/phpbb/textformatter/s9e/mention_helper.php | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index cf725789c1..92619ee772 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -70,6 +70,8 @@ abstract class group implements source_interface $groups[$row['group_id']] = $row; $groups[$row['group_id']]['group_name'] = $group_name; } + + $this->db->sql_freeresult($result); } return $groups; } @@ -97,6 +99,8 @@ abstract class group implements source_interface $group_ids[] = $row['group_id']; } + $this->db->sql_freeresult($result); + // Grab group data $groups = $this->get_groups(); diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index d691634859..466bda36db 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -76,6 +76,8 @@ abstract class user implements source_interface ]; } + $this->db->sql_freeresult($result); + return $names; } } diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index 91530d5c5c..b383dc46f7 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -80,6 +80,8 @@ class mention_helper { $this->cached_colors['users'][$row['user_id']] = $row['user_colour']; } + + $this->db->sql_freeresult($result); } if (!empty($group_ids)) @@ -97,6 +99,8 @@ class mention_helper { $this->cached_colors['groups'][$row['group_id']] = $row['group_colour']; } + + $this->db->sql_freeresult($result); } } From c70ac7eb623de0a18f3ef63dec2432e0cbcde10c Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 8 Jun 2018 13:18:44 +0300 Subject: [PATCH 017/113] [ticket/13713] Introduce notifications for mentions PHPBB3-13713 --- .../container/services_notification.yml | 9 + phpBB/includes/functions.php | 3 + phpBB/includes/functions_admin.php | 1 + phpBB/includes/functions_posting.php | 3 + phpBB/includes/mcp/mcp_queue.php | 9 +- phpBB/language/en/common.php | 3 + phpBB/language/en/email/mention.txt | 20 ++ phpBB/language/en/ucp.php | 1 + phpBB/phpbb/notification/type/mention.php | 172 ++++++++++++++++++ .../textformatter/s9e/mention_helper.php | 26 +++ 10 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 phpBB/language/en/email/mention.txt create mode 100644 phpBB/phpbb/notification/type/mention.php diff --git a/phpBB/config/default/container/services_notification.yml b/phpBB/config/default/container/services_notification.yml index c18e358114..65552b1e13 100644 --- a/phpBB/config/default/container/services_notification.yml +++ b/phpBB/config/default/container/services_notification.yml @@ -95,6 +95,15 @@ services: tags: - { name: notification.type } + notification.type.mention: + class: phpbb\notification\type\mention + shared: false + parent: notification.type.post + calls: + - [set_helper, ['@text_formatter.s9e.mention_helper']] + tags: + - { name: notification.type } + notification.type.pm: class: phpbb\notification\type\pm shared: false diff --git a/phpBB/includes/functions.php b/phpBB/includes/functions.php index adc09b9382..c7c6375b06 100644 --- a/phpBB/includes/functions.php +++ b/phpBB/includes/functions.php @@ -575,6 +575,7 @@ function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $ // Mark all topic notifications read for this user $phpbb_notifications->mark_notifications(array( 'notification.type.topic', + 'notification.type.mention', 'notification.type.quote', 'notification.type.bookmark', 'notification.type.post', @@ -660,6 +661,7 @@ function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $ $db->sql_freeresult($result); $phpbb_notifications->mark_notifications_by_parent(array( + 'notification.type.mention', 'notification.type.quote', 'notification.type.bookmark', 'notification.type.post', @@ -771,6 +773,7 @@ function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $ ), $topic_id, $user->data['user_id'], $post_time); $phpbb_notifications->mark_notifications_by_parent(array( + 'notification.type.mention', 'notification.type.quote', 'notification.type.bookmark', 'notification.type.post', diff --git a/phpBB/includes/functions_admin.php b/phpBB/includes/functions_admin.php index dc76a2eb80..f0813708c3 100644 --- a/phpBB/includes/functions_admin.php +++ b/phpBB/includes/functions_admin.php @@ -908,6 +908,7 @@ function delete_posts($where_type, $where_ids, $auto_sync = true, $posted_sync = // Notifications types to delete $delete_notifications_types = array( + 'notification.type.mention', 'notification.type.quote', 'notification.type.approve_post', 'notification.type.post_in_queue', diff --git a/phpBB/includes/functions_posting.php b/phpBB/includes/functions_posting.php index 817a98f836..14b50a4a31 100644 --- a/phpBB/includes/functions_posting.php +++ b/phpBB/includes/functions_posting.php @@ -2405,6 +2405,7 @@ function submit_post($mode, $subject, $username, $topic_type, &$poll_ary, &$data { case 'post': $phpbb_notifications->add_notifications(array( + 'notification.type.mention', 'notification.type.quote', 'notification.type.topic', ), $notification_data); @@ -2413,6 +2414,7 @@ function submit_post($mode, $subject, $username, $topic_type, &$poll_ary, &$data case 'reply': case 'quote': $phpbb_notifications->add_notifications(array( + 'notification.type.mention', 'notification.type.quote', 'notification.type.bookmark', 'notification.type.post', @@ -2432,6 +2434,7 @@ function submit_post($mode, $subject, $username, $topic_type, &$poll_ary, &$data } $phpbb_notifications->update_notifications(array( + 'notification.type.mention', 'notification.type.bookmark', 'notification.type.topic', 'notification.type.post', diff --git a/phpBB/includes/mcp/mcp_queue.php b/phpBB/includes/mcp/mcp_queue.php index fe54a01de7..eebb8e4fc4 100644 --- a/phpBB/includes/mcp/mcp_queue.php +++ b/phpBB/includes/mcp/mcp_queue.php @@ -810,10 +810,14 @@ class mcp_queue ), $post_data); } } - $phpbb_notifications->add_notifications(array('notification.type.quote'), $post_data); + $phpbb_notifications->add_notifications(array( + 'notification.type.mention', + 'notification.type.quote', + ), $post_data); $phpbb_notifications->delete_notifications('notification.type.post_in_queue', $post_id); $phpbb_notifications->mark_notifications(array( + 'notification.type.mention', 'notification.type.quote', 'notification.type.bookmark', 'notification.type.post', @@ -1045,12 +1049,13 @@ class mcp_queue if ($topic_data['topic_visibility'] == ITEM_UNAPPROVED) { $phpbb_notifications->add_notifications(array( + 'notification.type.mention', 'notification.type.quote', 'notification.type.topic', ), $topic_data); } - $phpbb_notifications->mark_notifications('quote', $topic_data['post_id'], $user->data['user_id']); + $phpbb_notifications->mark_notifications(array('mention', 'quote'), $topic_data['post_id'], $user->data['user_id']); $phpbb_notifications->mark_notifications('topic', $topic_id, $user->data['user_id']); if ($notify_poster) diff --git a/phpBB/language/en/common.php b/phpBB/language/en/common.php index 24fd293326..f04a0e891b 100644 --- a/phpBB/language/en/common.php +++ b/phpBB/language/en/common.php @@ -475,6 +475,9 @@ $lang = array_merge($lang, array( 'NOTIFICATION_FORUM' => 'Forum: %1$s', 'NOTIFICATION_GROUP_REQUEST' => 'Group request from %1$s to join the group %2$s.', 'NOTIFICATION_GROUP_REQUEST_APPROVED' => 'Group request approved to join the group %1$s.', + 'NOTIFICATION_MENTION' => array( + 1 => 'Mentioned by %1$s in:', + ), 'NOTIFICATION_METHOD_INVALID' => 'The method "%s" does not refer to a valid notification method.', 'NOTIFICATION_PM' => 'Private Message from %1$s:', 'NOTIFICATION_POST' => array( diff --git a/phpBB/language/en/email/mention.txt b/phpBB/language/en/email/mention.txt new file mode 100644 index 0000000000..51c161453e --- /dev/null +++ b/phpBB/language/en/email/mention.txt @@ -0,0 +1,20 @@ +Subject: Topic reply notification - "{TOPIC_TITLE}" + +Hello {USERNAME}, + +You are receiving this notification because "{AUTHOR_NAME}" mentioned you in the topic "{TOPIC_TITLE}" at "{SITENAME}". You can use the following link to view the reply made. + +If you want to view the post where you have been mentioned, click the following link: +{U_VIEW_POST} + +If you want to view the topic, click the following link: +{U_TOPIC} + +If you want to view the forum, click the following link: +{U_FORUM} + +If you no longer wish to receive updates about replies mentioning you, please update your notification settings here: + +{U_NOTIFICATION_SETTINGS} + +{EMAIL_SIG} diff --git a/phpBB/language/en/ucp.php b/phpBB/language/en/ucp.php index 446f357f5d..8c4e59904c 100644 --- a/phpBB/language/en/ucp.php +++ b/phpBB/language/en/ucp.php @@ -332,6 +332,7 @@ $lang = array_merge($lang, array( 'NOTIFICATION_TYPE_GROUP_REQUEST' => 'Someone requests to join a group you lead', 'NOTIFICATION_TYPE_FORUM' => 'Someone replies to a topic in a forum to which you are subscribed', 'NOTIFICATION_TYPE_IN_MODERATION_QUEUE' => 'A post or topic needs approval', + 'NOTIFICATION_TYPE_MENTION' => 'Someone mentiones you in a post', 'NOTIFICATION_TYPE_MODERATION_QUEUE' => 'Your topics/posts are approved or disapproved by a moderator', 'NOTIFICATION_TYPE_PM' => 'Someone sends you a private message', 'NOTIFICATION_TYPE_POST' => 'Someone replies to a topic to which you are subscribed', diff --git a/phpBB/phpbb/notification/type/mention.php b/phpBB/phpbb/notification/type/mention.php new file mode 100644 index 0000000000..54c180ad2c --- /dev/null +++ b/phpBB/phpbb/notification/type/mention.php @@ -0,0 +1,172 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\notification\type; + +/** +* Post mentioning notifications class +* This class handles notifying users when they have been mentioned in a post +*/ + +class mention extends \phpbb\notification\type\post +{ + /** + * @var \phpbb\textformatter\s9e\mention_helper + */ + protected $helper; + + /** + * Get notification type name + * + * @return string + */ + public function get_type() + { + return 'notification.type.mention'; + } + + /** + * Language key used to output the text + * + * @var string + */ + protected $language_key = 'NOTIFICATION_MENTION'; + + /** + * Notification option data (for outputting to the user) + * + * @var bool|array False if the service should use it's default data + * Array of data (including keys 'id', 'lang', and 'group') + */ + static public $notification_option = array( + 'lang' => 'NOTIFICATION_TYPE_MENTION', + 'group' => 'NOTIFICATION_GROUP_POSTING', + ); + + /** + * Is available + */ + public function is_available() + { + return true; + } + + /** + * Find the users who want to receive notifications + * + * @param array $post Data from submit_post + * @param array $options Options for finding users for notification + * + * @return array + */ + public function find_users_for_notification($post, $options = array()) + { + $options = array_merge(array( + 'ignore_users' => array(), + ), $options); + + $user_ids = $this->helper->get_mentioned_users($post['post_text']); + + $user_ids = array_unique($user_ids); + + $user_ids = array_diff($user_ids, [(int) $post['poster_id']]); + + if (empty($user_ids)) + { + return array(); + } + + return $this->get_authorised_recipients($user_ids, $post['forum_id'], $options, true); + } + + /** + * Update a notification + * TODO: decide what to do with this stuff + * + * @param array $post Data specific for this type that will be updated + * @return true + */ + public function update_notifications($post) + { + $old_notifications = $this->notification_manager->get_notified_users($this->get_type(), array( + 'item_id' => static::get_item_id($post), + )); + + // Find the new users to notify + $notifications = $this->find_users_for_notification($post); + + // Find the notifications we must delete + $remove_notifications = array_diff(array_keys($old_notifications), array_keys($notifications)); + + // Find the notifications we must add + $add_notifications = array(); + foreach (array_diff(array_keys($notifications), array_keys($old_notifications)) as $user_id) + { + $add_notifications[$user_id] = $notifications[$user_id]; + } + + // Add the necessary notifications + $this->notification_manager->add_notifications_for_users($this->get_type(), $post, $add_notifications); + + // Remove the necessary notifications + if (!empty($remove_notifications)) + { + $this->notification_manager->delete_notifications($this->get_type(), static::get_item_id($post), false, $remove_notifications); + } + + // return true to continue with the update code in the notifications service (this will update the rest of the notifications) + return true; + } + + /** + * {inheritDoc} + */ + public function get_redirect_url() + { + return $this->get_url(); + } + + /** + * Get email template + * + * @return string|bool + */ + public function get_email_template() + { + return 'mention'; + } + + /** + * Get email template variables + * + * @return array + */ + public function get_email_template_variables() + { + $user_data = $this->user_loader->get_user($this->get_data('poster_id')); + + return array_merge(parent::get_email_template_variables(), array( + 'AUTHOR_NAME' => htmlspecialchars_decode($user_data['username']), + )); + } + + /** + * Set the helper service used to retrieve mentioned used + * + * @param \phpbb\textformatter\s9e\mention_helper $helper + */ + public function set_helper(\phpbb\textformatter\s9e\mention_helper $helper) + { + $this->helper = $helper; + } +} diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index b383dc46f7..66092dbbc5 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -149,4 +149,30 @@ class mention_helper } ); } + + /** + * Get a list of mentioned users + * TODO: decide what to do with groups + * + * @param string $xml Parsed text + * @return int[] List of user IDs + */ + public function get_mentioned_users($xml) + { + $user_ids = array(); + if (strpos($xml, 'loadXML($xml); + $xpath = new \DOMXPath($dom); + foreach ($xpath->query('//MENTION/@user_id') as $user_id) + { + $user_ids[] = (int) $user_id->textContent; + } + + return $user_ids; + } } From 52c2e11fdd32f6c05c2f998a3b6641fb26df4eae Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 9 Jun 2018 18:56:26 +0300 Subject: [PATCH 018/113] [ticket/13713] Add two new sources: member and team PHPBB3-13713 --- .../default/container/services_mention.yml | 20 ++++++++++ phpBB/phpbb/mention/source/member.php | 35 +++++++++++++++++ phpBB/phpbb/mention/source/team.php | 38 +++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 phpBB/phpbb/mention/source/member.php create mode 100644 phpBB/phpbb/mention/source/team.php diff --git a/phpBB/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml index 286c16a8da..8b466b3b01 100644 --- a/phpBB/config/default/container/services_mention.yml +++ b/phpBB/config/default/container/services_mention.yml @@ -27,6 +27,26 @@ services: tags: - { name: mention.source } + phpbb.mention.source.member: + class: phpbb\mention\source\member + arguments: + - '@dbal.conn' + - '@user_loader' + - '%core.root_path%' + - '%core.php_ext%' + tags: + - { name: mention.source } + + phpbb.mention.source.team: + class: phpbb\mention\source\team + arguments: + - '@dbal.conn' + - '@user_loader' + - '%core.root_path%' + - '%core.php_ext%' + tags: + - { name: mention.source } + phpbb.mention.source.topic: class: phpbb\mention\source\topic arguments: diff --git a/phpBB/phpbb/mention/source/member.php b/phpBB/phpbb/mention/source/member.php new file mode 100644 index 0000000000..bc1c5960ee --- /dev/null +++ b/phpBB/phpbb/mention/source/member.php @@ -0,0 +1,35 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +class member extends user +{ + /** + * {@inheritdoc} + */ + protected function query($keyword, $topic_id) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.username, u.user_id', + 'FROM' => [ + USERS_TABLE => 'u', + ], + 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + 'ORDER_BY' => 'u.user_lastvisit DESC' + ]); + return $query; + } +} diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php new file mode 100644 index 0000000000..dce5630944 --- /dev/null +++ b/phpBB/phpbb/mention/source/team.php @@ -0,0 +1,38 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +class team extends user +{ + /** + * {@inheritdoc} + */ + protected function query($keyword, $topic_id) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.username, u.user_id', + 'FROM' => [ + USERS_TABLE => 'u', + USER_GROUP_TABLE => 'ug', + TEAMPAGE_TABLE => 't', + ], + 'WHERE' => 'ug.group_id = t.group_id AND ug.user_id = u.user_id AND ug.user_pending = 0 + AND u.user_id <> ' . ANONYMOUS . ' + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + 'ORDER_BY' => 'u.user_lastvisit DESC' + ]); + return $query; + } +} From 52fac451a3008978a30078492736f6fa1e1b89c5 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 9 Jun 2018 19:52:21 +0300 Subject: [PATCH 019/113] [ticket/13713] Create abstract parent service for user source PHPBB3-13713 --- .../default/container/services_mention.yml | 29 +++++++------------ phpBB/phpbb/mention/source/friend.php | 8 ++--- .../textformatter/s9e/mention_helper.php | 20 ++++++------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/phpBB/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml index 8b466b3b01..06c94d54b3 100644 --- a/phpBB/config/default/container/services_mention.yml +++ b/phpBB/config/default/container/services_mention.yml @@ -18,44 +18,37 @@ services: phpbb.mention.source.friend: class: phpbb\mention\source\friend - arguments: - - '@dbal.conn' - - '@user_loader' - - '@user' - - '%core.root_path%' - - '%core.php_ext%' + parent: phpbb.mention.source.user + calls: + - [set_user, ['@user']] tags: - { name: mention.source } phpbb.mention.source.member: class: phpbb\mention\source\member - arguments: - - '@dbal.conn' - - '@user_loader' - - '%core.root_path%' - - '%core.php_ext%' + parent: phpbb.mention.source.user tags: - { name: mention.source } phpbb.mention.source.team: class: phpbb\mention\source\team - arguments: - - '@dbal.conn' - - '@user_loader' - - '%core.root_path%' - - '%core.php_ext%' + parent: phpbb.mention.source.user tags: - { name: mention.source } phpbb.mention.source.topic: class: phpbb\mention\source\topic + parent: phpbb.mention.source.user + tags: + - { name: mention.source } + + phpbb.mention.source.user: + abstract: true arguments: - '@dbal.conn' - '@user_loader' - '%core.root_path%' - '%core.php_ext%' - tags: - - { name: mention.source } phpbb.mention.source.usergroup: class: phpbb\mention\source\usergroup diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index d63efa4c72..b3c6a1898b 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -19,13 +19,13 @@ class friend extends user protected $user; /** - * Constructor + * Set the user service used to retrieve current user ID + * + * @param \phpbb\user $user */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader, \phpbb\user $user, $phpbb_root_path, $phpEx) + public function set_user(\phpbb\user $user) { $this->user = $user; - - parent::__construct($db, $user_loader, $phpbb_root_path, $phpEx); } /** diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index 66092dbbc5..c693151f1f 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -35,7 +35,7 @@ class mention_helper /** * @var array Array of users' and groups' colors for each cached ID */ - protected $cached_colors = []; + protected $cached_colours = []; /** * Constructor @@ -59,9 +59,9 @@ class mention_helper */ protected function get_colors($user_ids, $group_ids) { - $this->cached_colors = []; - $this->cached_colors['users'] = []; - $this->cached_colors['groups'] = []; + $this->cached_colours = []; + $this->cached_colours['users'] = []; + $this->cached_colours['groups'] = []; if (!empty($user_ids)) { @@ -78,7 +78,7 @@ class mention_helper while ($row = $this->db->sql_fetchrow($result)) { - $this->cached_colors['users'][$row['user_id']] = $row['user_colour']; + $this->cached_colours['users'][$row['user_id']] = $row['user_colour']; } $this->db->sql_freeresult($result); @@ -97,7 +97,7 @@ class mention_helper while ($row = $this->db->sql_fetchrow($result)) { - $this->cached_colors['groups'][$row['group_id']] = $row['group_colour']; + $this->cached_colours['groups'][$row['group_id']] = $row['group_colour']; } $this->db->sql_freeresult($result); @@ -130,18 +130,18 @@ class mention_helper { $attributes['profile_url'] = str_replace('{USER_ID}', $attributes['user_id'], $user_profile_url); - if (!empty($this->cached_colors['users'][$attributes['user_id']])) + if (!empty($this->cached_colours['users'][$attributes['user_id']])) { - $attributes['color'] = $this->cached_colors['users'][$attributes['user_id']]; + $attributes['color'] = $this->cached_colours['users'][$attributes['user_id']]; } } else if (isset($attributes['group_id'])) { $attributes['profile_url'] = str_replace('{GROUP_ID}', $attributes['group_id'], $group_profile_url); - if (!empty($this->cached_colors['groups'][$attributes['group_id']])) + if (!empty($this->cached_colours['groups'][$attributes['group_id']])) { - $attributes['color'] = $this->cached_colors['groups'][$attributes['group_id']]; + $attributes['color'] = $this->cached_colours['groups'][$attributes['group_id']]; } } From a21c115bf3ea9340cb61d834e869b51eebd63568 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 9 Jun 2018 20:39:52 +0300 Subject: [PATCH 020/113] [ticket/13713] Create properly named base services for sources PHPBB3-13713 --- .../default/container/services_mention.yml | 79 ++++++---- phpBB/phpbb/mention/source/base_group.php | 148 ++++++++++++++++++ phpBB/phpbb/mention/source/base_user.php | 83 ++++++++++ phpBB/phpbb/mention/source/friend.php | 2 +- phpBB/phpbb/mention/source/group.php | 115 ++------------ phpBB/phpbb/mention/source/member.php | 35 ----- phpBB/phpbb/mention/source/team.php | 2 +- phpBB/phpbb/mention/source/topic.php | 2 +- phpBB/phpbb/mention/source/user.php | 74 ++------- phpBB/phpbb/mention/source/usergroup.php | 15 +- 10 files changed, 302 insertions(+), 253 deletions(-) create mode 100644 phpBB/phpbb/mention/source/base_group.php create mode 100644 phpBB/phpbb/mention/source/base_user.php delete mode 100644 phpBB/phpbb/mention/source/member.php diff --git a/phpBB/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml index 06c94d54b3..310bfce6a4 100644 --- a/phpBB/config/default/container/services_mention.yml +++ b/phpBB/config/default/container/services_mention.yml @@ -16,33 +16,17 @@ services: tags: - { name: service_collection, tag: mention.source } - phpbb.mention.source.friend: - class: phpbb\mention\source\friend - parent: phpbb.mention.source.user - calls: - - [set_user, ['@user']] - tags: - - { name: mention.source } + phpbb.mention.source.base_group: + abstract: true + arguments: + - '@dbal.conn' + - '@group_helper' + - '@user' + - '@auth' + - '%core.root_path%' + - '%core.php_ext%' - phpbb.mention.source.member: - class: phpbb\mention\source\member - parent: phpbb.mention.source.user - tags: - - { name: mention.source } - - phpbb.mention.source.team: - class: phpbb\mention\source\team - parent: phpbb.mention.source.user - tags: - - { name: mention.source } - - phpbb.mention.source.topic: - class: phpbb\mention\source\topic - parent: phpbb.mention.source.user - tags: - - { name: mention.source } - - phpbb.mention.source.user: + phpbb.mention.source.base_user: abstract: true arguments: - '@dbal.conn' @@ -50,13 +34,40 @@ services: - '%core.root_path%' - '%core.php_ext%' - phpbb.mention.source.usergroup: - class: phpbb\mention\source\usergroup - arguments: - - '@dbal.conn' - - '@group_helper' - - '@user' - - '%core.root_path%' - - '%core.php_ext%' + phpbb.mention.source.friend: + class: phpbb\mention\source\friend + parent: phpbb.mention.source.base_user + calls: + - [set_user, ['@user']] + tags: + - { name: mention.source } + + phpbb.mention.source.group: + class: phpbb\mention\source\group + parent: phpbb.mention.source.base_group + tags: + - { name: mention.source } + + phpbb.mention.source.team: + class: phpbb\mention\source\team + parent: phpbb.mention.source.base_user + tags: + - { name: mention.source } + + phpbb.mention.source.topic: + class: phpbb\mention\source\topic + parent: phpbb.mention.source.base_user + tags: + - { name: mention.source } + + phpbb.mention.source.user: + class: phpbb\mention\source\user + parent: phpbb.mention.source.base_user + tags: + - { name: mention.source } + + phpbb.mention.source.usergroup: + class: phpbb\mention\source\usergroup + parent: phpbb.mention.source.base_group tags: - { name: mention.source } diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php new file mode 100644 index 0000000000..47784835d7 --- /dev/null +++ b/phpBB/phpbb/mention/source/base_group.php @@ -0,0 +1,148 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +abstract class base_group implements source_interface +{ + /** @var \phpbb\db\driver\driver_interface */ + protected $db; + + /** @var \phpbb\group\helper */ + protected $helper; + + /** @var \phpbb\user */ + protected $user; + + /** @var \phpbb\auth\auth */ + protected $auth; + + /** @var string */ + protected $phpbb_root_path; + + /** @var string */ + protected $php_ext; + + /** + * Constructor + */ + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper, \phpbb\user $user, \phpbb\auth\auth $auth, $phpbb_root_path, $phpEx) + { + $this->db = $db; + $this->helper = $helper; + $this->user = $user; + $this->auth = $auth; + $this->phpbb_root_path = $phpbb_root_path; + $this->php_ext = $phpEx; + + if (!function_exists('phpbb_get_user_rank')) + { + include($this->phpbb_root_path . 'includes/functions_display.' . $this->php_ext); + } + } + + /** + * Returns data for all board groups + * + * @return array Array of groups' data + */ + protected function get_groups() + { + static $groups = null; + + if (is_null($groups)) + { + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'g.*, ug.user_id as ug_user_id', + 'FROM' => [ + GROUPS_TABLE => 'g', + ], + 'LEFT_JOIN' => array( + array( + 'FROM' => array(USER_GROUP_TABLE => 'ug'), + 'ON' => 'ug.group_id = g.group_id AND ug.user_pending = 0 AND ug.user_id = ' . (int) $this->user->data['user_id'], + ), + ), + ]); + $result = $this->db->sql_query($query); + + $groups = []; + while ($row = $this->db->sql_fetchrow($result)) + { + if ($row['group_type'] == GROUP_SPECIAL && !in_array($row['group_name'], ['ADMINISTRATORS', 'GLOBAL_MODERATORS']) || $row['group_type'] == GROUP_HIDDEN && !$this->auth->acl_gets('a_group', 'a_groupadd', 'a_groupdel') && $row['ug_user_id'] != $this->user->data['user_id']) + { + // Skip the group that we should not be able to mention. + continue; + } + + $group_name = $this->helper->get_name($row['group_name']); + $groups['names'][$row['group_id']] = $group_name; + $groups[$row['group_id']] = $row; + $groups[$row['group_id']]['group_name'] = $group_name; + } + + $this->db->sql_freeresult($result); + } + return $groups; + } + + /** + * Builds a query for getting group IDs based on user input + * + * @param string $keyword Search string + * @param int $topic_id Current topic ID + * @return string Query ready for execution + */ + abstract protected function query($keyword, $topic_id); + + /** + * {@inheritdoc} + */ + public function get($keyword, $topic_id) + { + // Grab all group IDs + $result = $this->db->sql_query($this->query($keyword, $topic_id)); + + $group_ids = []; + while ($row = $this->db->sql_fetchrow($result)) + { + $group_ids[] = $row['group_id']; + } + + $this->db->sql_freeresult($result); + + // Grab group data + $groups = $this->get_groups(); + + $matches = preg_grep('/^' . $keyword . '.*/i', $groups['names']); + $group_ids = array_intersect($group_ids, array_flip($matches)); + + $names = []; + foreach ($group_ids as $group_id) + { + $group_rank = phpbb_get_user_rank($groups[$group_id], false); + $names['g' . $group_id] = [ + 'name' => $groups[$group_id]['group_name'], + 'param' => 'group_id', + 'id' => $group_id, + 'avatar' => [ + 'type' => 'group', + 'src' => phpbb_get_group_avatar($groups[$group_id]), + ], + 'rank' => $group_rank['title'], + ]; + } + + return $names; + } +} diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php new file mode 100644 index 0000000000..d23d3db02e --- /dev/null +++ b/phpBB/phpbb/mention/source/base_user.php @@ -0,0 +1,83 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\mention\source; + +abstract class base_user implements source_interface +{ + /** @var \phpbb\db\driver\driver_interface */ + protected $db; + + /** @var \phpbb\user_loader */ + protected $user_loader; + + /** @var string */ + protected $phpbb_root_path; + + /** @var string */ + protected $php_ext; + + /** + * Constructor + */ + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader, $phpbb_root_path, $phpEx) + { + $this->db = $db; + $this->user_loader = $user_loader; + $this->phpbb_root_path = $phpbb_root_path; + $this->php_ext = $phpEx; + + if (!function_exists('phpbb_get_user_rank')) + { + include($this->phpbb_root_path . 'includes/functions_display.' . $this->php_ext); + } + } + + /** + * Builds a query based on user input + * + * @param string $keyword Search string + * @param int $topic_id Current topic ID + * @return string Query ready for execution + */ + abstract protected function query($keyword, $topic_id); + + /** + * {@inheritdoc} + */ + public function get($keyword, $topic_id) + { + $keyword = utf8_clean_string($keyword); + $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), 5); + + $names = []; + while ($row = $this->db->sql_fetchrow($result)) + { + $user_rank = $this->user_loader->get_rank($row['user_id'], true); + $names['u' . $row['user_id']] = [ + 'name' => $row['username'], + 'param' => 'user_id', + 'id' => $row['user_id'], + 'avatar' => [ + 'type' => 'user', + 'src' => $this->user_loader->get_avatar($row['user_id'], true), + ], + 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', + ]; + } + + $this->db->sql_freeresult($result); + + return $names; + } +} diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index b3c6a1898b..3c946dcd73 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -13,7 +13,7 @@ namespace phpbb\mention\source; -class friend extends user +class friend extends base_user { /** @var \phpbb\user */ protected $user; diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index 92619ee772..d063d64324 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -13,116 +13,19 @@ namespace phpbb\mention\source; -abstract class group implements source_interface +class group extends base_group { - /** @var \phpbb\db\driver\driver_interface */ - protected $db; - - /** @var \phpbb\group\helper */ - protected $helper; - - /** @var string */ - protected $phpbb_root_path; - - /** @var string */ - protected $php_ext; - - /** - * Constructor - */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper, $phpbb_root_path, $phpEx) - { - $this->db = $db; - $this->helper = $helper; - $this->phpbb_root_path = $phpbb_root_path; - $this->php_ext = $phpEx; - - if (!function_exists('phpbb_get_user_rank')) - { - include($this->phpbb_root_path . 'includes/functions_display.' . $this->php_ext); - } - } - - /** - * Returns data for all board groups - * - * @return array Array of groups' data - */ - protected function get_groups() - { - static $groups = null; - - if (is_null($groups)) - { - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'g.*', - 'FROM' => [ - GROUPS_TABLE => 'g', - ], - ]); - $result = $this->db->sql_query($query); - - $groups = []; - while ($row = $this->db->sql_fetchrow($result)) - { - $group_name = $this->helper->get_name($row['group_name']); - $groups['names'][$row['group_id']] = $group_name; - $groups[$row['group_id']] = $row; - $groups[$row['group_id']]['group_name'] = $group_name; - } - - $this->db->sql_freeresult($result); - } - return $groups; - } - - /** - * Builds a query for getting group IDs based on user input - * - * @param string $keyword Search string - * @param int $topic_id Current topic ID - * @return string Query ready for execution - */ - abstract protected function query($keyword, $topic_id); - /** * {@inheritdoc} */ - public function get($keyword, $topic_id) + protected function query($keyword, $topic_id) { - // Grab all group IDs - $result = $this->db->sql_query($this->query($keyword, $topic_id)); - - $group_ids = []; - while ($row = $this->db->sql_fetchrow($result)) - { - $group_ids[] = $row['group_id']; - } - - $this->db->sql_freeresult($result); - - // Grab group data - $groups = $this->get_groups(); - - $matches = preg_grep('/^' . $keyword . '.*/i', $groups['names']); - $group_ids = array_intersect($group_ids, array_flip($matches)); - - $names = []; - foreach ($group_ids as $group_id) - { - $group_rank = phpbb_get_user_rank($groups[$group_id], false); - $names['g' . $group_id] = [ - 'name' => $groups[$group_id]['group_name'], - 'param' => 'group_id', - 'id' => $group_id, - 'avatar' => [ - 'type' => 'group', - 'src' => phpbb_get_group_avatar($groups[$group_id]), - ], - 'rank' => $group_rank['title'], - ]; - } - - return $names; + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'g.group_id', + 'FROM' => [ + GROUPS_TABLE => 'g', + ], + ]); + return $query; } } diff --git a/phpBB/phpbb/mention/source/member.php b/phpBB/phpbb/mention/source/member.php deleted file mode 100644 index bc1c5960ee..0000000000 --- a/phpBB/phpbb/mention/source/member.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @license GNU General Public License, version 2 (GPL-2.0) - * - * For full copyright and license information, please see - * the docs/CREDITS.txt file. - * - */ - -namespace phpbb\mention\source; - -class member extends user -{ - /** - * {@inheritdoc} - */ - protected function query($keyword, $topic_id) - { - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username, u.user_id', - 'FROM' => [ - USERS_TABLE => 'u', - ], - 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' - AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), - 'ORDER_BY' => 'u.user_lastvisit DESC' - ]); - return $query; - } -} diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index dce5630944..187f0fb691 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -13,7 +13,7 @@ namespace phpbb\mention\source; -class team extends user +class team extends base_user { /** * {@inheritdoc} diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index 1d72df711c..f8a5123c56 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -13,7 +13,7 @@ namespace phpbb\mention\source; -class topic extends user +class topic extends base_user { /** * {@inheritdoc} diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index 466bda36db..10f065df0d 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -13,71 +13,23 @@ namespace phpbb\mention\source; -abstract class user implements source_interface +class user extends base_user { - /** @var \phpbb\db\driver\driver_interface */ - protected $db; - - /** @var \phpbb\user_loader */ - protected $user_loader; - - /** @var string */ - protected $phpbb_root_path; - - /** @var string */ - protected $php_ext; - - /** - * Constructor - */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader, $phpbb_root_path, $phpEx) - { - $this->db = $db; - $this->user_loader = $user_loader; - $this->phpbb_root_path = $phpbb_root_path; - $this->php_ext = $phpEx; - - if (!function_exists('phpbb_get_user_rank')) - { - include($this->phpbb_root_path . 'includes/functions_display.' . $this->php_ext); - } - } - - /** - * Builds a query based on user input - * - * @param string $keyword Search string - * @param int $topic_id Current topic ID - * @return string Query ready for execution - */ - abstract protected function query($keyword, $topic_id); - /** * {@inheritdoc} */ - public function get($keyword, $topic_id) + protected function query($keyword, $topic_id) { - $keyword = utf8_clean_string($keyword); - $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), 5); - - $names = []; - while ($row = $this->db->sql_fetchrow($result)) - { - $user_rank = $this->user_loader->get_rank($row['user_id'], true); - $names['u' . $row['user_id']] = [ - 'name' => $row['username'], - 'param' => 'user_id', - 'id' => $row['user_id'], - 'avatar' => [ - 'type' => 'user', - 'src' => $this->user_loader->get_avatar($row['user_id'], true), - ], - 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', - ]; - } - - $this->db->sql_freeresult($result); - - return $names; + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.username, u.user_id', + 'FROM' => [ + USERS_TABLE => 'u', + ], + 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + 'ORDER_BY' => 'u.user_lastvisit DESC' + ]); + return $query; } } diff --git a/phpBB/phpbb/mention/source/usergroup.php b/phpBB/phpbb/mention/source/usergroup.php index 6d965961be..c3b95ffb49 100644 --- a/phpBB/phpbb/mention/source/usergroup.php +++ b/phpBB/phpbb/mention/source/usergroup.php @@ -13,21 +13,8 @@ namespace phpbb\mention\source; -class usergroup extends group +class usergroup extends base_group { - /** @var \phpbb\user */ - protected $user; - - /** - * Constructor - */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper, \phpbb\user $user, $phpbb_root_path, $phpEx) - { - $this->user = $user; - - parent::__construct($db, $helper, $phpbb_root_path, $phpEx); - } - /** * {@inheritdoc} */ From 012d009fbe2f20c993d56d12003162013cd9b165 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 14 Jun 2018 00:51:06 +0300 Subject: [PATCH 021/113] [ticket/13713] Remove mention ID constant PHPBB3-13713 --- phpBB/includes/constants.php | 1 - phpBB/phpbb/textformatter/data_access.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/phpBB/includes/constants.php b/phpBB/includes/constants.php index 1a7d52a38b..be6916442b 100644 --- a/phpBB/includes/constants.php +++ b/phpBB/includes/constants.php @@ -183,7 +183,6 @@ define('BBCODE_ID_LIST', 9); define('BBCODE_ID_EMAIL', 10); define('BBCODE_ID_FLASH', 11); define('BBCODE_ID_ATTACH', 12); -define('BBCODE_ID_MENTION', 13); // BBCode hard limit define('BBCODE_LIMIT', 1511); diff --git a/phpBB/phpbb/textformatter/data_access.php b/phpBB/phpbb/textformatter/data_access.php index bbb6ba0f0d..808c1ea04e 100644 --- a/phpBB/phpbb/textformatter/data_access.php +++ b/phpBB/phpbb/textformatter/data_access.php @@ -138,7 +138,7 @@ class data_access 'email' => 10, 'flash' => 11, 'attachment' => 12, - 'mention' => 13, + 'mention' => 13, // TODO: change ID/remove? ); $styles = array(); From 31e4fb472942a71c8aed19be73342470e34f0a26 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 15 Jun 2018 02:03:57 +0300 Subject: [PATCH 022/113] [ticket/13713] Introduce ACP settings for mentions PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 5 ++- phpBB/assets/javascript/editor.js | 9 +++- phpBB/includes/acp/acp_board.php | 6 ++- phpBB/includes/functions.php | 4 +- phpBB/includes/functions_acp.php | 6 ++- phpBB/language/en/acp/board.php | 3 ++ phpBB/language/en/acp/permissions_phpbb.php | 2 + .../data/v330/add_mention_settings.php | 42 +++++++++++++++++++ phpBB/phpbb/notification/type/mention.php | 2 +- phpBB/phpbb/permissions.php | 2 + .../textformatter/renderer_interface.php | 8 ++++ phpBB/phpbb/textformatter/s9e/factory.php | 4 +- phpBB/phpbb/textformatter/s9e/renderer.php | 15 +++++++ .../prosilver/template/posting_buttons.html | 6 ++- 14 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 phpBB/phpbb/db/migration/data/v330/add_mention_settings.php diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index be15ab40d3..8a6a4462c7 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -1,7 +1,5 @@ + + + diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 63d28e918a..61d852d254 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -386,6 +386,10 @@ function getCaretPosition(txtarea) { (function($) { function handle_mentions(txtarea) { + var $mentionParams = $('#mention_params'), + mentionURL = $mentionParams.data('mentionUrl'), + mentionNamesLimit = $mentionParams.data('mentionNamesLimit'), + mentionTopicId = $mentionParams.data('topicId'); $(txtarea).atwho({ at: "@", displayTpl: function(data) { @@ -395,9 +399,10 @@ function getCaretPosition(txtarea) { return "
  • " + avatar + "" + data.name + "" + rank + "
  • "; }, insertTpl: "[mention ${param}=${id}]${name}[/mention]", + limit: mentionNamesLimit, callbacks: { remoteFilter: function(query, callback) { - $.getJSON(mention_url, {keyword: query, topic_id: mention_topic_id}, function (data) { + $.getJSON(mentionURL, {keyword: query, topic_id: mentionTopicId}, function (data) { callback(data) }); } @@ -431,7 +436,7 @@ function getCaretPosition(txtarea) { phpbb.showDragNDrop(textarea); } - if (mention_url) { + if ($('#mention_params').length) { handle_mentions(textarea); } diff --git a/phpBB/includes/acp/acp_board.php b/phpBB/includes/acp/acp_board.php index f03a3e1ee4..62daba0373 100644 --- a/phpBB/includes/acp/acp_board.php +++ b/phpBB/includes/acp/acp_board.php @@ -220,7 +220,11 @@ class acp_board 'max_post_img_width' => array('lang' => 'MAX_POST_IMG_WIDTH', 'validate' => 'int:0:9999', 'type' => 'number:0:9999', 'explain' => true, 'append' => ' ' . $user->lang['PIXEL']), 'max_post_img_height' => array('lang' => 'MAX_POST_IMG_HEIGHT', 'validate' => 'int:0:9999', 'type' => 'number:0:9999', 'explain' => true, 'append' => ' ' . $user->lang['PIXEL']), - 'legend3' => 'ACP_SUBMIT_CHANGES', + 'legend3' => 'MENTIONS', + 'allow_mentions' => array('lang' => 'ALLOW_MENTIONS', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => false), + 'mention_names_limit' => array('lang' => 'MENTION_NAMES_LIMIT', 'validate' => 'int:1:9999', 'type' => 'number:1:9999', 'explain' => false), + + 'legend4' => 'ACP_SUBMIT_CHANGES', ) ); break; diff --git a/phpBB/includes/functions.php b/phpBB/includes/functions.php index c7c6375b06..bc9bc3dbd9 100644 --- a/phpBB/includes/functions.php +++ b/phpBB/includes/functions.php @@ -3946,7 +3946,9 @@ function page_header($page_title = '', $display_online_list = false, $item_id = 'U_RESTORE_PERMISSIONS' => ($user->data['user_perm_from'] && $auth->acl_get('a_switchperm')) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=restore_perm') : '', 'U_FEED' => $controller_helper->route('phpbb_feed_index'), - 'UA_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), + 'S_ALLOW_MENTIONS' => ($config['allow_mentions'] && $auth->acl_get('u_mention') && (empty($forum_id) || $auth->acl_get('f_mention', $forum_id))) ? true : false, + 'S_MENTION_NAMES_LIMIT' => $config['mention_names_limit'], + 'U_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), 'S_USER_LOGGED_IN' => ($user->data['user_id'] != ANONYMOUS) ? true : false, 'S_AUTOLOGIN_ENABLED' => ($config['allow_autologin']) ? true : false, diff --git a/phpBB/includes/functions_acp.php b/phpBB/includes/functions_acp.php index ce6712ffd0..a2ffe0b1b2 100644 --- a/phpBB/includes/functions_acp.php +++ b/phpBB/includes/functions_acp.php @@ -24,7 +24,7 @@ if (!defined('IN_PHPBB')) */ function adm_page_header($page_title) { - global $config, $user, $template; + global $config, $user, $template, $auth; global $phpbb_root_path, $phpbb_admin_path, $phpEx, $SID, $_SID; global $phpbb_dispatcher, $phpbb_container; @@ -89,7 +89,9 @@ function adm_page_header($page_title) 'U_ADM_INDEX' => append_sid("{$phpbb_admin_path}index.$phpEx"), 'U_INDEX' => append_sid("{$phpbb_root_path}index.$phpEx"), - 'UA_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), + 'S_ALLOW_MENTIONS' => ($config['allow_mentions'] && $auth->acl_get('u_mention')) ? true : false, + 'S_MENTION_NAMES_LIMIT' => $config['mention_names_limit'], + 'U_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), 'T_IMAGES_PATH' => "{$phpbb_root_path}images/", 'T_SMILIES_PATH' => "{$phpbb_root_path}{$config['smilies_path']}/", diff --git a/phpBB/language/en/acp/board.php b/phpBB/language/en/acp/board.php index d02c8b0141..d4aba08f3c 100644 --- a/phpBB/language/en/acp/board.php +++ b/phpBB/language/en/acp/board.php @@ -157,6 +157,7 @@ $lang = array_merge($lang, array( // Post Settings $lang = array_merge($lang, array( 'ACP_POST_SETTINGS_EXPLAIN' => 'Here you can set all default settings for posting.', + 'ALLOW_MENTIONS' => 'Allow mentions of users and groups boardwide', 'ALLOW_POST_LINKS' => 'Allow links in posts/private messages', 'ALLOW_POST_LINKS_EXPLAIN' => 'If disallowed the [URL] BBCode tag and automatic/magic URLs are disabled.', 'ALLOWED_SCHEMES_LINKS' => 'Allowed schemes in links', @@ -187,6 +188,8 @@ $lang = array_merge($lang, array( 'MAX_POST_IMG_WIDTH_EXPLAIN' => 'Maximum width of a flash file in postings. Set to 0 for unlimited size.', 'MAX_POST_URLS' => 'Maximum links per post', 'MAX_POST_URLS_EXPLAIN' => 'Maximum number of URLs in a post. Set to 0 for unlimited links.', + 'MENTIONS' => 'Mentions', + 'MENTION_NAMES_LIMIT' => 'Maximum number of names in dropdown list', 'MIN_CHAR_LIMIT' => 'Minimum characters per post/message', 'MIN_CHAR_LIMIT_EXPLAIN' => 'The minimum number of characters the user need to enter within a post/private message. The minimum for this setting is 1.', 'POSTING' => 'Posting', diff --git a/phpBB/language/en/acp/permissions_phpbb.php b/phpBB/language/en/acp/permissions_phpbb.php index cdf4820475..475ac5aadd 100644 --- a/phpBB/language/en/acp/permissions_phpbb.php +++ b/phpBB/language/en/acp/permissions_phpbb.php @@ -76,6 +76,7 @@ $lang = array_merge($lang, array( 'ACL_U_ATTACH' => 'Can attach files', 'ACL_U_DOWNLOAD' => 'Can download files', + 'ACL_U_MENTION' => 'Can mention users and groups', 'ACL_U_SAVEDRAFTS' => 'Can save drafts', 'ACL_U_CHGCENSORS' => 'Can disable word censors', 'ACL_U_SIG' => 'Can use signature', @@ -123,6 +124,7 @@ $lang = array_merge($lang, array( 'ACL_F_STICKY' => 'Can post stickies', 'ACL_F_ANNOUNCE' => 'Can post announcements', 'ACL_F_ANNOUNCE_GLOBAL' => 'Can post global announcements', + 'ACL_F_MENTION' => 'Can mention users and groups', 'ACL_F_REPLY' => 'Can reply to topics', 'ACL_F_EDIT' => 'Can edit own posts', 'ACL_F_DELETE' => 'Can permanently delete own posts', diff --git a/phpBB/phpbb/db/migration/data/v330/add_mention_settings.php b/phpBB/phpbb/db/migration/data/v330/add_mention_settings.php new file mode 100644 index 0000000000..1f38d919b2 --- /dev/null +++ b/phpBB/phpbb/db/migration/data/v330/add_mention_settings.php @@ -0,0 +1,42 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\db\migration\data\v330; + +class add_mention_settings extends \phpbb\db\migration\migration +{ + public function update_data() + { + return array( + array('config.add', array('allow_mentions', true)), + array('config.add', array('mention_names_limit', 10)), + + // Set up user permissions + array('permission.add', array('u_mention', true)), + array('permission.permission_set', array('ROLE_USER_FULL', 'u_mention')), + array('permission.permission_set', array('ROLE_USER_STANDARD', 'u_mention')), + array('permission.permission_set', array('ROLE_USER_LIMITED', 'u_mention')), + array('permission.permission_set', array('ROLE_USER_NOPM', 'u_mention')), + array('permission.permission_set', array('ROLE_USER_NOAVATAR', 'u_mention')), + + // Set up forum permissions + array('permission.add', array('f_mention', false)), + array('permission.permission_set', array('ROLE_FORUM_FULL', 'f_mention')), + array('permission.permission_set', array('ROLE_FORUM_STANDARD', 'f_mention')), + array('permission.permission_set', array('ROLE_FORUM_LIMITED', 'f_mention')), + array('permission.permission_set', array('ROLE_FORUM_ONQUEUE', 'f_mention')), + array('permission.permission_set', array('ROLE_FORUM_POLLS', 'f_mention')), + array('permission.permission_set', array('ROLE_FORUM_LIMITED_POLLS', 'f_mention')), + ); + } +} diff --git a/phpBB/phpbb/notification/type/mention.php b/phpBB/phpbb/notification/type/mention.php index 54c180ad2c..1161814dbe 100644 --- a/phpBB/phpbb/notification/type/mention.php +++ b/phpBB/phpbb/notification/type/mention.php @@ -58,7 +58,7 @@ class mention extends \phpbb\notification\type\post */ public function is_available() { - return true; + return $this->config['allow_mentions'] && $this->auth->acl_get('u_mention'); } /** diff --git a/phpBB/phpbb/permissions.php b/phpBB/phpbb/permissions.php index bf3b33856e..857ae2a1ec 100644 --- a/phpBB/phpbb/permissions.php +++ b/phpBB/phpbb/permissions.php @@ -231,6 +231,7 @@ class permissions 'u_attach' => array('lang' => 'ACL_U_ATTACH', 'cat' => 'post'), 'u_download' => array('lang' => 'ACL_U_DOWNLOAD', 'cat' => 'post'), + 'u_mention' => array('lang' => 'ACL_U_MENTION', 'cat' => 'post'), 'u_savedrafts' => array('lang' => 'ACL_U_SAVEDRAFTS', 'cat' => 'post'), 'u_chgcensors' => array('lang' => 'ACL_U_CHGCENSORS', 'cat' => 'post'), 'u_sig' => array('lang' => 'ACL_U_SIG', 'cat' => 'post'), @@ -276,6 +277,7 @@ class permissions 'f_sticky' => array('lang' => 'ACL_F_STICKY', 'cat' => 'post'), 'f_announce' => array('lang' => 'ACL_F_ANNOUNCE', 'cat' => 'post'), 'f_announce_global' => array('lang' => 'ACL_F_ANNOUNCE_GLOBAL', 'cat' => 'post'), + 'f_mention' => array('lang' => 'ACL_F_MENTION', 'cat' => 'post'), 'f_reply' => array('lang' => 'ACL_F_REPLY', 'cat' => 'post'), 'f_edit' => array('lang' => 'ACL_F_EDIT', 'cat' => 'post'), 'f_delete' => array('lang' => 'ACL_F_DELETE', 'cat' => 'post'), diff --git a/phpBB/phpbb/textformatter/renderer_interface.php b/phpBB/phpbb/textformatter/renderer_interface.php index 609b0bb642..106dbdc25f 100644 --- a/phpBB/phpbb/textformatter/renderer_interface.php +++ b/phpBB/phpbb/textformatter/renderer_interface.php @@ -89,4 +89,12 @@ interface renderer_interface * @return null */ public function set_viewsmilies($value); + + /** + * Set the "usemention" option + * + * @param bool $value Option's value + * @return null + */ + public function set_usemention($value); } diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php index 1edb170634..16bd63cf73 100644 --- a/phpBB/phpbb/textformatter/s9e/factory.php +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -310,8 +310,8 @@ class factory implements \phpbb\textformatter\cache_interface $configurator->tags['QUOTE']->nestingLimit = PHP_INT_MAX; } - // Modify the template to disable images/flash depending on user's settings - foreach (array('FLASH', 'IMG') as $name) + // Modify the template to disable images/flash/mentions depending on user's settings + foreach (array('FLASH', 'IMG', 'MENTION') as $name) { $tag = $configurator->tags[$name]; $tag->template = '' . $tag->template . ''; diff --git a/phpBB/phpbb/textformatter/s9e/renderer.php b/phpBB/phpbb/textformatter/s9e/renderer.php index cc909ea90c..64875d96fc 100644 --- a/phpBB/phpbb/textformatter/s9e/renderer.php +++ b/phpBB/phpbb/textformatter/s9e/renderer.php @@ -63,6 +63,11 @@ class renderer implements \phpbb\textformatter\renderer_interface */ protected $viewsmilies = false; + /** + * @var bool Whether the user is allowed to use mentions + */ + protected $usemention = false; + /** * Constructor * @@ -177,6 +182,7 @@ class renderer implements \phpbb\textformatter\renderer_interface $this->set_viewflash($user->optionget('viewflash')); $this->set_viewimg($user->optionget('viewimg')); $this->set_viewsmilies($user->optionget('viewsmilies')); + $this->set_usemention($config['allow_mentions'] && $auth->acl_get('u_mention')); // Set the stylesheet parameters foreach (array_keys($this->renderer->getParameters()) as $param_name) @@ -330,4 +336,13 @@ class renderer implements \phpbb\textformatter\renderer_interface $this->viewsmilies = $value; $this->renderer->setParameter('S_VIEWSMILIES', $value); } + + /** + * {@inheritdoc} + */ + public function set_usemention($value) + { + $this->usemention = $value; + $this->renderer->setParameter('S_VIEWMENTION', $value); + } } diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 3ce5539d54..df4c038323 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -4,8 +4,6 @@ var text_name = 'signature''message'; var load_draft = false; var upload = false; - var mention_url = '{UA_MENTION_URL}'; - var mention_topic_id = '{S_TOPIC_ID}'; // Define the bbCode tags var bbcode = new Array(); @@ -27,6 +25,10 @@ } } + + + + From 50400cb785f646c97cd02d331c5b03d09bd88898 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 15 Jun 2018 02:38:48 +0300 Subject: [PATCH 023/113] [ticket/13713] Fix styles for mention dropdown PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 1 - phpBB/adm/style/admin.css | 42 ++++++++++++++++--- phpBB/assets/css/jquery.atwho.min.css | 1 - phpBB/assets/javascript/editor.js | 2 +- .../prosilver/template/posting_buttons.html | 1 - phpBB/styles/prosilver/theme/bidi.css | 4 +- phpBB/styles/prosilver/theme/colours.css | 15 ++++++- phpBB/styles/prosilver/theme/forms.css | 25 +++++++++-- 8 files changed, 75 insertions(+), 16 deletions(-) delete mode 100644 phpBB/assets/css/jquery.atwho.min.css diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index 8a6a4462c7..74ebc88d78 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -11,7 +11,6 @@ - diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 620a658167..ac7991aa69 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1676,18 +1676,45 @@ fieldset.submit-buttons legend { font-weight: bold; } -.atwho-container .atwho-view { - font-size: 12px; +.atwho-view { + background: #ffffff; + border: 1px solid #dddddd; + border-radius: 3px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + color: #000000; + position: absolute; + z-index: 11110 !important; + top: 0; + left: 0; + display: none; min-width: 260px; + margin-top: 18px; } -.atwho-container .atwho-view ul li { +.atwho-view-ul { + overflow-y: auto; + max-height: 200px; + margin: auto; + padding: 0; + list-style: none; +} + +.mention-name { + font-size: 12px; + border-bottom: 1px solid #dddddd; position: relative; + display: block; padding: 15px 5px 15px 45px; + cursor: pointer; } -.atwho-container .atwho-view ul li:hover, -.atwho-container .atwho-view ul li.cur { +.rtl .mention-name { + padding-right: 45px; + padding-left: 15px; +} + +.mention-name:hover, +.mention-name.cur { background-color: #0077b3; color: #ffffff; } @@ -1710,6 +1737,11 @@ fieldset.submit-buttons legend { margin-top: -16px; } +.rtl .mention-avatar { + right: 7px; + left: auto; +} + .mention-rank { font-size: 10px; display: block; diff --git a/phpBB/assets/css/jquery.atwho.min.css b/phpBB/assets/css/jquery.atwho.min.css deleted file mode 100644 index f770dc73b3..0000000000 --- a/phpBB/assets/css/jquery.atwho.min.css +++ /dev/null @@ -1 +0,0 @@ -.atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;z-index:11110!important}.atwho-view .atwho-header{padding:5px;margin:5px;cursor:pointer;border-bottom:solid 1px #eaeff1;color:#6f8092;font-size:11px;font-weight:700}.atwho-view .atwho-header .small{color:#6f8092;float:right;padding-top:2px;margin-right:-5px;font-size:12px;font-weight:400}.atwho-view .atwho-header:hover{cursor:default}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto;max-height:200px;overflow-y:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400} \ No newline at end of file diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 61d852d254..f96ccbbd01 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -396,7 +396,7 @@ function getCaretPosition(txtarea) { var avatar = (data.avatar.src) ? "" : "", rank = (data.rank) ? "" + data.rank + "" : ''; - return "
  • " + avatar + "" + data.name + "" + rank + "
  • "; + return "
  • " + avatar + "" + data.name + "" + rank + "
  • "; }, insertTpl: "[mention ${param}=${id}]${name}[/mention]", limit: mentionNamesLimit, diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index df4c038323..73b3159349 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -29,7 +29,6 @@ - diff --git a/phpBB/styles/prosilver/theme/bidi.css b/phpBB/styles/prosilver/theme/bidi.css index 127ac48c71..493d05bea8 100644 --- a/phpBB/styles/prosilver/theme/bidi.css +++ b/phpBB/styles/prosilver/theme/bidi.css @@ -931,12 +931,12 @@ } /* Mention dropdown */ -.atwho-container .atwho-view ul li { +.rtl .mention-name { padding-right: 45px; padding-left: 15px; } -.mention-avatar { +.rtl .mention-avatar { right: 7px; left: auto; } diff --git a/phpBB/styles/prosilver/theme/colours.css b/phpBB/styles/prosilver/theme/colours.css index 12e432cbbb..01b69d331b 100644 --- a/phpBB/styles/prosilver/theme/colours.css +++ b/phpBB/styles/prosilver/theme/colours.css @@ -983,8 +983,19 @@ fieldset.fields2 dl:hover dt label { outline-color: rgba(19, 164, 236, 0.5); } -.atwho-container .atwho-view ul li:hover, -.atwho-container .atwho-view ul li.cur { +.atwho-view { + background: #ffffff; + border-color: #dddddd; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + color: #000000; +} + +.mention-name { + border-bottom-color: #dddddd; +} + +.mention-name:hover, +.mention-name.cur { background-color: #0077b3; color: #ffffff; } diff --git a/phpBB/styles/prosilver/theme/forms.css b/phpBB/styles/prosilver/theme/forms.css index 6a5048ad8d..787122d10b 100644 --- a/phpBB/styles/prosilver/theme/forms.css +++ b/phpBB/styles/prosilver/theme/forms.css @@ -285,14 +285,33 @@ fieldset.submit-buttons input { } /* Mention dropdown */ -.atwho-container .atwho-view { - font-size: 12px; +.atwho-view { + border: 1px solid transparent; + border-radius: 3px; + position: absolute; + z-index: 11110 !important; + top: 0; + left: 0; + display: none; min-width: 260px; + margin-top: 18px; } -.atwho-container .atwho-view ul li { +.atwho-view-ul { + overflow-y: auto; + max-height: 200px; + margin: auto; + padding: 0; + list-style: none; +} + +.mention-name { + font-size: 12px; + border-bottom: 1px solid transparent; position: relative; + display: block; padding: 15px 5px 15px 45px; + cursor: pointer; } .mention-avatar { From b66d298dde41539ce36eb30b90701ef52bb66ff3 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 16 Jun 2018 20:52:09 +0300 Subject: [PATCH 024/113] [ticket/13713] Use format buttons div as mentions data container PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 5 +---- phpBB/assets/javascript/editor.js | 8 ++++---- phpBB/styles/prosilver/template/posting_buttons.html | 5 +---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index 74ebc88d78..5398726da7 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -8,15 +8,12 @@ // ]]> - - - -
    +
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-topic-id="{S_TOPIC_ID}"> diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index f96ccbbd01..10dbc18935 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -386,10 +386,10 @@ function getCaretPosition(txtarea) { (function($) { function handle_mentions(txtarea) { - var $mentionParams = $('#mention_params'), - mentionURL = $mentionParams.data('mentionUrl'), - mentionNamesLimit = $mentionParams.data('mentionNamesLimit'), - mentionTopicId = $mentionParams.data('topicId'); + var $mentionDataContainer = $('#format-buttons'), + mentionURL = $mentionDataContainer.data('mentionUrl'), + mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'), + mentionTopicId = $mentionDataContainer.data('topicId'); $(txtarea).atwho({ at: "@", displayTpl: function(data) { diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 73b3159349..5765a29dca 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -26,9 +26,6 @@ } - - - @@ -42,7 +39,7 @@
    -
    +
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-topic-id="{S_TOPIC_ID}"> From ad97751d68cd86a3cdafdbde0f585ca5c7daab3e Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 21 Jun 2018 01:13:29 +0300 Subject: [PATCH 025/113] [ticket/13713] Fix styles for mentions PHPBB3-13713 --- phpBB/adm/style/admin.css | 123 ++++++++++++-------- phpBB/assets/javascript/editor.js | 10 +- phpBB/styles/prosilver/theme/bidi.css | 29 +++-- phpBB/styles/prosilver/theme/colours.css | 58 +++++---- phpBB/styles/prosilver/theme/content.css | 5 - phpBB/styles/prosilver/theme/forms.css | 52 --------- phpBB/styles/prosilver/theme/mentions.css | 87 ++++++++++++++ phpBB/styles/prosilver/theme/stylesheet.css | 1 + 8 files changed, 219 insertions(+), 146 deletions(-) create mode 100644 phpBB/styles/prosilver/theme/mentions.css diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index ac7991aa69..a71d55a499 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1676,76 +1676,99 @@ fieldset.submit-buttons legend { font-weight: bold; } -.atwho-view { - background: #ffffff; - border: 1px solid #dddddd; - border-radius: 3px; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); - color: #000000; +.atwho-view { /* mention-container */ + text-align: left; + background-color: #ffffff; + border-radius: 2px; + box-shadow: + 0 3px 1px -2px rgba(0, 0, 0, 0.2), + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12); position: absolute; - z-index: 11110 !important; - top: 0; - left: 0; - display: none; - min-width: 260px; - margin-top: 18px; + z-index: 999; + top: 20px; + overflow: auto; + transition: all 0.2s ease; } -.atwho-view-ul { - overflow-y: auto; - max-height: 200px; - margin: auto; +.rtl .atwho-view { /* mention-container */ + text-align: right; +} + +.atwho-view-ul { /* mention-list */ + margin: 0; padding: 0; - list-style: none; + list-style-type: none; } -.mention-name { - font-size: 12px; +.mention-media { + color: #757575; + display: inline-flex; + flex-shrink: 0; + justify-content: center; + align-items: center; + margin-right: 10px; + margin-left: 0; +} + +.rtl .mention-media { + margin-right: 0; + margin-left: 10px; +} + +.mention-media-avatar { + font-size: 32px; + line-height: 36px; + text-align: center; + vertical-align: center; + width: 36px; + height: 36px; +} + +.mention-item { + font-size: 14px; + font-weight: 400; + line-height: 1.5; + letter-spacing: 0.04em; border-bottom: 1px solid #dddddd; + color: #212121; position: relative; - display: block; - padding: 15px 5px 15px 45px; + display: flex; + overflow: hidden; + justify-content: flex-start; + align-items: center; + max-width: 300px; + padding: 10px; cursor: pointer; } -.rtl .mention-name { - padding-right: 45px; - padding-left: 15px; +.mention-item:hover, +.mention-item.cur { + text-decoration: none; + background-color: #eeeeee; + color: #2d80d2; } -.mention-name:hover, -.mention-name.cur { - background-color: #0077b3; - color: #ffffff; +.mention-item:hover .mention-media-avatar, +.mention-item.cur .mention-media-avatar { + color: #2d80d2; } -.mention-avatar { - font-size: 14px; - line-height: 30px; - text-align: center; - vertical-align: middle; - background-color: #0077b3; - border: 1px solid #ffffff; - border-radius: 100%; - color: #ffffff; - position: absolute; - top: 50%; - left: 7px; - display: inline-block; - width: 30px; - height: 30px; - margin-top: -16px; +.mention-name, +.mention-rank { + display: block; } -.rtl .mention-avatar { - right: 7px; - left: auto; +.mention-name { + line-height: 1.25; } .mention-rank { - font-size: 10px; - display: block; - margin-top: 2px; + font-size: 12px; + font-weight: 400; + line-height: 1.2871; + letter-spacing: 0.04em; + color: #757575; } /* Input field styles diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 10dbc18935..c80d47ddf8 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -386,17 +386,17 @@ function getCaretPosition(txtarea) { (function($) { function handle_mentions(txtarea) { - var $mentionDataContainer = $('#format-buttons'), + var $mentionDataContainer = $('[data-mention-url]'), mentionURL = $mentionDataContainer.data('mentionUrl'), mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'), mentionTopicId = $mentionDataContainer.data('topicId'); $(txtarea).atwho({ at: "@", displayTpl: function(data) { - var avatar = (data.avatar.src) ? "" : - "", + var avatar = (data.avatar.src) ? "" : + "", rank = (data.rank) ? "" + data.rank + "" : ''; - return "
  • " + avatar + "" + data.name + "" + rank + "
  • "; + return "
  • " + avatar + "" + data.name + rank + "
  • "; }, insertTpl: "[mention ${param}=${id}]${name}[/mention]", limit: mentionNamesLimit, @@ -436,7 +436,7 @@ function getCaretPosition(txtarea) { phpbb.showDragNDrop(textarea); } - if ($('#mention_params').length) { + if ($('[data-mention-url]').length) { handle_mentions(textarea); } diff --git a/phpBB/styles/prosilver/theme/bidi.css b/phpBB/styles/prosilver/theme/bidi.css index 493d05bea8..5a3286da56 100644 --- a/phpBB/styles/prosilver/theme/bidi.css +++ b/phpBB/styles/prosilver/theme/bidi.css @@ -365,6 +365,24 @@ float: left; } +/** +* mentions.css +*/ + +/* Mention block +---------------------------------------- */ + +/* Mention dropdown +---------------------------------------- */ +.rtl .atwho-view { /* mention-container */ + text-align: right; +} + +.rtl .mention-media { + margin-right: 0; + margin-left: 10px; +} + /** * content.css */ @@ -930,17 +948,6 @@ float: left; } -/* Mention dropdown */ -.rtl .mention-name { - padding-right: 45px; - padding-left: 15px; -} - -.rtl .mention-avatar { - right: 7px; - left: auto; -} - /* Search box ---------------------------------------- */ diff --git a/phpBB/styles/prosilver/theme/colours.css b/phpBB/styles/prosilver/theme/colours.css index 01b69d331b..96cac04ee1 100644 --- a/phpBB/styles/prosilver/theme/colours.css +++ b/phpBB/styles/prosilver/theme/colours.css @@ -369,6 +369,41 @@ p.post-notice { background-image: none; } +/* colours and backgrounds for mentions.css */ + +/* mention dropdown */ +.atwho-view { /* mention-container */ + background-color: #ffffff; + box-shadow: + 0 3px 1px -2px rgba(0, 0, 0, 0.2), + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12); +} + +.mention-media { + color: #757575; +} + +.mention-item { + border-bottom-color: #dddddd; + color: #212121; +} + +.mention-item:hover, +.mention-item.cur { + background-color: #eeeeee; + color: #2d80d2; +} + +.mention-item:hover .mention-media-avatar, +.mention-item.cur .mention-media-avatar { + color: #2d80d2; +} + +.mention-rank { + color: #757575; +} + /* colours and backgrounds for content.css */ ul.forums { background-color: #edf4f7; @@ -983,29 +1018,6 @@ fieldset.fields2 dl:hover dt label { outline-color: rgba(19, 164, 236, 0.5); } -.atwho-view { - background: #ffffff; - border-color: #dddddd; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); - color: #000000; -} - -.mention-name { - border-bottom-color: #dddddd; -} - -.mention-name:hover, -.mention-name.cur { - background-color: #0077b3; - color: #ffffff; -} - -.mention-avatar { - background-color: #0077b3; - border-color: #ffffff; - color: #ffffff; -} - /* input field styles */ .inputbox { background-color: #ffffff; diff --git a/phpBB/styles/prosilver/theme/content.css b/phpBB/styles/prosilver/theme/content.css index 701fa5fd94..338016ac2c 100644 --- a/phpBB/styles/prosilver/theme/content.css +++ b/phpBB/styles/prosilver/theme/content.css @@ -576,11 +576,6 @@ blockquote .codebox { padding: 5px 3px; } -/* Mention block */ -.mention { - font-weight: bold; -} - /* Attachments ---------------------------------------- */ .attachbox { diff --git a/phpBB/styles/prosilver/theme/forms.css b/phpBB/styles/prosilver/theme/forms.css index 787122d10b..e05cf090ca 100644 --- a/phpBB/styles/prosilver/theme/forms.css +++ b/phpBB/styles/prosilver/theme/forms.css @@ -284,58 +284,6 @@ fieldset.submit-buttons input { margin: 3px; } -/* Mention dropdown */ -.atwho-view { - border: 1px solid transparent; - border-radius: 3px; - position: absolute; - z-index: 11110 !important; - top: 0; - left: 0; - display: none; - min-width: 260px; - margin-top: 18px; -} - -.atwho-view-ul { - overflow-y: auto; - max-height: 200px; - margin: auto; - padding: 0; - list-style: none; -} - -.mention-name { - font-size: 12px; - border-bottom: 1px solid transparent; - position: relative; - display: block; - padding: 15px 5px 15px 45px; - cursor: pointer; -} - -.mention-avatar { - font-size: 14px; - line-height: 30px; - text-align: center; - vertical-align: middle; - border: 1px solid transparent; - border-radius: 100%; - position: absolute; - top: 50%; - left: 7px; - display: inline-block; - width: 30px; - height: 30px; - margin-top: -16px; -} - -.mention-rank { - font-size: 10px; - display: block; - margin-top: 2px; -} - /* Input field styles ---------------------------------------- */ .inputbox { diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css new file mode 100644 index 0000000000..01146e1128 --- /dev/null +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -0,0 +1,87 @@ +/* -------------------------------------------------------------- /* + $Mentions +/* -------------------------------------------------------------- */ + +/* stylelint-disable selector-max-compound-selectors */ +/* stylelint-disable selector-no-qualifying-type */ + +/* Mention block +---------------------------------------- */ +.mention { + font-weight: bold; +} + +/* Mention dropdown +---------------------------------------- */ +.atwho-view { /* mention-container */ + text-align: left; + border-radius: 2px; + position: absolute; + z-index: 999; + top: 20px; + overflow: auto; + transition: all 0.2s ease; +} + +.atwho-view-ul { /* mention-list */ + margin: 0; + padding: 0; + list-style-type: none; +} + +.mention-media { + display: inline-flex; + flex-shrink: 0; + justify-content: center; + align-items: center; + margin-right: 10px; + margin-left: 0; +} + +.mention-media-avatar { + font-size: 32px; + line-height: 36px; + text-align: center; + vertical-align: center; + width: 36px; + height: 36px; +} + +.mention-item { + font-size: 14px; + font-weight: 400; + line-height: 1.5; + letter-spacing: 0.04em; + border-bottom: 1px solid transparent; + position: relative; + display: flex; + overflow: hidden; + justify-content: flex-start; + align-items: center; + max-width: 300px; + padding: 10px; + cursor: pointer; +} + +.mention-item:hover { + text-decoration: none; +} + +.mention-name, +.mention-rank { + display: block; +} + +.mention-name { + line-height: 1.25; +} + +.mention-rank { + font-size: 12px; + font-weight: 400; + line-height: 1.2871; + letter-spacing: 0.04em; +} + +/* stylelint-enable selector-max-compound-selectors */ +/* stylelint-enable selector-no-qualifying-type */ diff --git a/phpBB/styles/prosilver/theme/stylesheet.css b/phpBB/styles/prosilver/theme/stylesheet.css index 94bea37750..8ae9d1ea42 100644 --- a/phpBB/styles/prosilver/theme/stylesheet.css +++ b/phpBB/styles/prosilver/theme/stylesheet.css @@ -14,6 +14,7 @@ @import url("common.css?hash=658f990b"); @import url("buttons.css?hash=eb16911f"); @import url("links.css?hash=5fec3654"); +@import url("mentions.css?hash=a67fa183"); @import url("content.css?hash=f7bdea58"); @import url("buttons.css?hash=eb16911f"); @import url("cp.css?hash=73c6f37d"); From 659928f042afd1da2bd8344d1cb351ec1e4714f9 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 21 Jun 2018 03:18:52 +0300 Subject: [PATCH 026/113] [ticket/13713] Use SVGs and the styling by @hanakin PHPBB3-13713 --- phpBB/adm/style/admin.css | 22 +++++++++++----------- phpBB/assets/javascript/editor.js | 7 +++++-- phpBB/styles/prosilver/theme/bidi.css | 2 +- phpBB/styles/prosilver/theme/mentions.css | 20 ++++++++++---------- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index a71d55a499..50d55cad17 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1707,26 +1707,26 @@ fieldset.submit-buttons legend { flex-shrink: 0; justify-content: center; align-items: center; - margin-right: 10px; + margin-right: 16px; margin-left: 0; } .rtl .mention-media { margin-right: 0; - margin-left: 10px; + margin-left: 16px; } .mention-media-avatar { - font-size: 32px; - line-height: 36px; - text-align: center; - vertical-align: center; - width: 36px; - height: 36px; + width: 40px; + height: 40px; +} + +svg.mention-media-avatar { /* TODO: remove it after general normalization */ + fill: currentColor; } .mention-item { - font-size: 14px; + font-size: 16px; font-weight: 400; line-height: 1.5; letter-spacing: 0.04em; @@ -1738,7 +1738,7 @@ fieldset.submit-buttons legend { justify-content: flex-start; align-items: center; max-width: 300px; - padding: 10px; + padding: 16px; cursor: pointer; } @@ -1764,7 +1764,7 @@ fieldset.submit-buttons legend { } .mention-rank { - font-size: 12px; + font-size: 14px; font-weight: 400; line-height: 1.2871; letter-spacing: 0.04em; diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index c80d47ddf8..076b7fd7ff 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -385,6 +385,10 @@ function getCaretPosition(txtarea) { } (function($) { + function mention_default_avatar(type) { + return (type === 'group') ? '' : ''; + } + function handle_mentions(txtarea) { var $mentionDataContainer = $('[data-mention-url]'), mentionURL = $mentionDataContainer.data('mentionUrl'), @@ -393,8 +397,7 @@ function getCaretPosition(txtarea) { $(txtarea).atwho({ at: "@", displayTpl: function(data) { - var avatar = (data.avatar.src) ? "" : - "", + var avatar = (data.avatar.src) ? "" : mention_default_avatar(data.avatar.type), rank = (data.rank) ? "" + data.rank + "" : ''; return "
  • " + avatar + "" + data.name + rank + "
  • "; }, diff --git a/phpBB/styles/prosilver/theme/bidi.css b/phpBB/styles/prosilver/theme/bidi.css index 5a3286da56..7f9642b7cc 100644 --- a/phpBB/styles/prosilver/theme/bidi.css +++ b/phpBB/styles/prosilver/theme/bidi.css @@ -380,7 +380,7 @@ .rtl .mention-media { margin-right: 0; - margin-left: 10px; + margin-left: 16px; } /** diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 01146e1128..32c77b196c 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -34,21 +34,21 @@ flex-shrink: 0; justify-content: center; align-items: center; - margin-right: 10px; + margin-right: 16px; margin-left: 0; } .mention-media-avatar { - font-size: 32px; - line-height: 36px; - text-align: center; - vertical-align: center; - width: 36px; - height: 36px; + width: 40px; + height: 40px; +} + +svg.mention-media-avatar { /* TODO: remove it after general normalization */ + fill: currentColor; } .mention-item { - font-size: 14px; + font-size: 16px; font-weight: 400; line-height: 1.5; letter-spacing: 0.04em; @@ -59,7 +59,7 @@ justify-content: flex-start; align-items: center; max-width: 300px; - padding: 10px; + padding: 16px; cursor: pointer; } @@ -77,7 +77,7 @@ } .mention-rank { - font-size: 12px; + font-size: 14px; font-weight: 400; line-height: 1.2871; letter-spacing: 0.04em; From c6e9f001e3ea65966c612554674cc9b72f5f92e6 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 21 Jun 2018 03:20:46 +0300 Subject: [PATCH 027/113] [ticket/13713] Escape special characters in preg_replace PHPBB3-13713 --- phpBB/phpbb/mention/source/base_group.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index 47784835d7..592c6e0da2 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -124,7 +124,7 @@ abstract class base_group implements source_interface // Grab group data $groups = $this->get_groups(); - $matches = preg_grep('/^' . $keyword . '.*/i', $groups['names']); + $matches = preg_grep('/^' . preg_quote($keyword) . '.*/i', $groups['names']); $group_ids = array_intersect($group_ids, array_flip($matches)); $names = []; From fefeba2687884a8e71afe2c7f386f1e7ef6ad5bd Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 21 Jun 2018 03:28:43 +0300 Subject: [PATCH 028/113] [ticket/13713] Remove unneeded top property PHPBB3-13713 --- phpBB/adm/style/admin.css | 1 - phpBB/styles/prosilver/theme/mentions.css | 1 - 2 files changed, 2 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 50d55cad17..f9d5c8257f 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1686,7 +1686,6 @@ fieldset.submit-buttons legend { 0 1px 5px 0 rgba(0, 0, 0, 0.12); position: absolute; z-index: 999; - top: 20px; overflow: auto; transition: all 0.2s ease; } diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 32c77b196c..262e83eecf 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -18,7 +18,6 @@ border-radius: 2px; position: absolute; z-index: 999; - top: 20px; overflow: auto; transition: all 0.2s ease; } From ab91cf6ca6012f2e27297573b5c71d5489361b2a Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 21 Jun 2018 11:03:12 +0300 Subject: [PATCH 029/113] [ticket/13713] Do not allow to add mention BBCode as custom PHPBB3-13713 --- phpBB/includes/acp/acp_bbcodes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpBB/includes/acp/acp_bbcodes.php b/phpBB/includes/acp/acp_bbcodes.php index 5706367ee3..3c0371a3a7 100644 --- a/phpBB/includes/acp/acp_bbcodes.php +++ b/phpBB/includes/acp/acp_bbcodes.php @@ -195,7 +195,7 @@ class acp_bbcodes $data = $this->build_regexp($bbcode_match, $bbcode_tpl); // Make sure the user didn't pick a "bad" name for the BBCode tag. - $hard_coded = array('code', 'quote', 'quote=', 'attachment', 'attachment=', 'b', 'i', 'url', 'url=', 'img', 'size', 'size=', 'color', 'color=', 'u', 'list', 'list=', 'email', 'email=', 'flash', 'flash='); + $hard_coded = array('code', 'quote', 'quote=', 'attachment', 'attachment=', 'b', 'i', 'url', 'url=', 'img', 'size', 'size=', 'color', 'color=', 'u', 'list', 'list=', 'email', 'email=', 'flash', 'flash=', 'mention'); if (($action == 'modify' && strtolower($data['bbcode_tag']) !== strtolower($row['bbcode_tag'])) || ($action == 'create')) { From 368090b7e62060c011996e3aff9ad2dbf9ddb5a4 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 21 Jun 2018 11:04:07 +0300 Subject: [PATCH 030/113] [ticket/13713] Use config limit for fetching users PHPBB3-13713 --- phpBB/config/default/container/services_mention.yml | 1 + phpBB/phpbb/mention/source/base_user.php | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/phpBB/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml index 310bfce6a4..d94aaca088 100644 --- a/phpBB/config/default/container/services_mention.yml +++ b/phpBB/config/default/container/services_mention.yml @@ -30,6 +30,7 @@ services: abstract: true arguments: - '@dbal.conn' + - '@config' - '@user_loader' - '%core.root_path%' - '%core.php_ext%' diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index d23d3db02e..a78f995a75 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -18,6 +18,9 @@ abstract class base_user implements source_interface /** @var \phpbb\db\driver\driver_interface */ protected $db; + /** @var \phpbb\config\config */ + protected $config; + /** @var \phpbb\user_loader */ protected $user_loader; @@ -30,9 +33,10 @@ abstract class base_user implements source_interface /** * Constructor */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user_loader $user_loader, $phpbb_root_path, $phpEx) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\user_loader $user_loader, $phpbb_root_path, $phpEx) { $this->db = $db; + $this->config = $config; $this->user_loader = $user_loader; $this->phpbb_root_path = $phpbb_root_path; $this->php_ext = $phpEx; @@ -58,7 +62,7 @@ abstract class base_user implements source_interface public function get($keyword, $topic_id) { $keyword = utf8_clean_string($keyword); - $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), 5); + $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), $this->config['mention_names_limit']); $names = []; while ($row = $this->db->sql_fetchrow($result)) From 90ee0993e60e6017aae291ed57a583c29b9ae8ff Mon Sep 17 00:00:00 2001 From: lavigor Date: Sun, 24 Jun 2018 04:41:43 +0300 Subject: [PATCH 031/113] [ticket/13713] Fix current issues with tests PHPBB3-13713 --- tests/notification/base.php | 9 +++++++++ tests/notification/submit_post_base.php | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/notification/base.php b/tests/notification/base.php index 59c7956ee8..d1861245bb 100644 --- a/tests/notification/base.php +++ b/tests/notification/base.php @@ -33,6 +33,7 @@ abstract class phpbb_tests_notification_base extends phpbb_database_test_case 'notification.type.disapprove_post', 'notification.type.disapprove_topic', 'notification.type.forum', + 'notification.type.mention', 'notification.type.pm', 'notification.type.post', 'notification.type.post_in_queue', @@ -105,6 +106,14 @@ abstract class phpbb_tests_notification_base extends phpbb_database_test_case $phpbb_container->set('cache.driver', $cache_driver); $phpbb_container->set('cache', $cache); $phpbb_container->set('text_formatter.utils', new \phpbb\textformatter\s9e\utils()); + $phpbb_container->set( + 'text_formatter.s9e.mention_helper', + new \phpbb\textformatter\s9e\mention_helper( + $this->db, + $phpbb_root_path, + $phpEx + ) + ); $phpbb_container->set('dispatcher', $this->phpbb_dispatcher); $phpbb_container->setParameter('core.root_path', $phpbb_root_path); $phpbb_container->setParameter('core.php_ext', $phpEx); diff --git a/tests/notification/submit_post_base.php b/tests/notification/submit_post_base.php index 2d17b601a2..1e348f4529 100644 --- a/tests/notification/submit_post_base.php +++ b/tests/notification/submit_post_base.php @@ -132,6 +132,14 @@ abstract class phpbb_notification_submit_post_base extends phpbb_database_test_c $phpbb_container->set('cache.driver', $cache_driver); $phpbb_container->set('cache', $cache); $phpbb_container->set('text_formatter.utils', new \phpbb\textformatter\s9e\utils()); + $phpbb_container->set( + 'text_formatter.s9e.mention_helper', + new \phpbb\textformatter\s9e\mention_helper( + $this->db, + $phpbb_root_path, + $phpEx + ) + ); $phpbb_container->set('dispatcher', $phpbb_dispatcher); $phpbb_container->set('storage.attachment', $storage); $phpbb_container->setParameter('core.root_path', $phpbb_root_path); @@ -145,7 +153,7 @@ abstract class phpbb_notification_submit_post_base extends phpbb_database_test_c $phpbb_container->compile(); // Notification Types - $notification_types = array('quote', 'bookmark', 'post', 'post_in_queue', 'topic', 'topic_in_queue', 'approve_topic', 'approve_post', 'forum'); + $notification_types = array('quote', 'mention', 'bookmark', 'post', 'post_in_queue', 'topic', 'topic_in_queue', 'approve_topic', 'approve_post', 'forum'); $notification_types_array = array(); foreach ($notification_types as $type) { From fd92abda5992aa9757507909a19ade444ef4c265 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sun, 24 Jun 2018 20:33:28 +0300 Subject: [PATCH 032/113] [ticket/13713] Fix tests for PHP 5.6 PHPBB3-13713 --- phpBB/phpbb/textformatter/s9e/mention_helper.php | 8 ++++---- tests/notification/fixtures/services_notification.yml | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index c693151f1f..fded9c5cd4 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -13,7 +13,7 @@ namespace phpbb\textformatter\s9e; -use s9e\TextFormatter\Utils; +use s9e\TextFormatter\Utils as TextFormatterUtils; class mention_helper { @@ -117,11 +117,11 @@ class mention_helper // TODO: think about optimization for caching colors. $this->get_colors( - Utils::getAttributeValues($xml, 'MENTION', 'user_id'), - Utils::getAttributeValues($xml, 'MENTION', 'group_id') + TextFormatterUtils::getAttributeValues($xml, 'MENTION', 'user_id'), + TextFormatterUtils::getAttributeValues($xml, 'MENTION', 'group_id') ); - return Utils::replaceAttributes( + return TextFormatterUtils::replaceAttributes( $xml, 'MENTION', function ($attributes) use ($user_profile_url, $group_profile_url) diff --git a/tests/notification/fixtures/services_notification.yml b/tests/notification/fixtures/services_notification.yml index 470768d986..69e6374f4c 100644 --- a/tests/notification/fixtures/services_notification.yml +++ b/tests/notification/fixtures/services_notification.yml @@ -44,6 +44,9 @@ services: text_formatter.s9e.quote_helper: synthetic: true + text_formatter.s9e.mention_helper: + synthetic: true + text_formatter.parser: synthetic: true From 5006a26f6c47f8eb3b09276dcbf18a7ea84fc371 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 29 Jun 2018 03:01:20 +0300 Subject: [PATCH 033/113] [ticket/13713] Add new values to installation schema PHPBB3-13713 --- phpBB/install/schemas/schema_data.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/phpBB/install/schemas/schema_data.sql b/phpBB/install/schemas/schema_data.sql index 3a98eab482..a11b1e1a9c 100644 --- a/phpBB/install/schemas/schema_data.sql +++ b/phpBB/install/schemas/schema_data.sql @@ -21,6 +21,7 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('allow_emailreuse', INSERT INTO phpbb_config (config_name, config_value) VALUES ('allow_forum_notify', '1'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('allow_live_searches', '1'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('allow_mass_pm', '1'); +INSERT INTO phpbb_config (config_name, config_value) VALUES ('allow_mentions', '1'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('allow_name_chars', 'USERNAME_CHARS_ANY'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('allow_namechange', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('allow_nocensors', '0'); @@ -234,6 +235,7 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('max_sig_img_height INSERT INTO phpbb_config (config_name, config_value) VALUES ('max_sig_img_width', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('max_sig_smilies', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('max_sig_urls', '5'); +INSERT INTO phpbb_config (config_name, config_value) VALUES ('mention_names_limit', '10'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('mime_triggers', 'body|head|html|img|plaintext|a href|pre|script|table|title'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('min_name_chars', '3'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('min_pass_chars', '6'); @@ -380,6 +382,7 @@ INSERT INTO phpbb_acl_options (auth_option, is_local) VALUES ('f_ignoreflood', 1 INSERT INTO phpbb_acl_options (auth_option, is_local) VALUES ('f_img', 1); INSERT INTO phpbb_acl_options (auth_option, is_local) VALUES ('f_list', 1); INSERT INTO phpbb_acl_options (auth_option, is_local) VALUES ('f_list_topics', 1); +INSERT INTO phpbb_acl_options (auth_option, is_local) VALUES ('f_mention', 1); INSERT INTO phpbb_acl_options (auth_option, is_local) VALUES ('f_noapprove', 1); INSERT INTO phpbb_acl_options (auth_option, is_local) VALUES ('f_poll', 1); INSERT INTO phpbb_acl_options (auth_option, is_local) VALUES ('f_post', 1); @@ -478,6 +481,7 @@ INSERT INTO phpbb_acl_options (auth_option, is_global) VALUES ('u_hideonline', 1 INSERT INTO phpbb_acl_options (auth_option, is_global) VALUES ('u_ignoreflood', 1); INSERT INTO phpbb_acl_options (auth_option, is_global) VALUES ('u_masspm', 1); INSERT INTO phpbb_acl_options (auth_option, is_global) VALUES ('u_masspm_group', 1); +INSERT INTO phpbb_acl_options (auth_option, is_global) VALUES ('u_mention', 1); INSERT INTO phpbb_acl_options (auth_option, is_global) VALUES ('u_pm_attach', 1); INSERT INTO phpbb_acl_options (auth_option, is_global) VALUES ('u_pm_bbcode', 1); INSERT INTO phpbb_acl_options (auth_option, is_global) VALUES ('u_pm_delete', 1); @@ -590,7 +594,7 @@ INSERT INTO phpbb_acl_roles_data (role_id, auth_option_id, auth_setting) SELECT INSERT INTO phpbb_acl_roles_data (role_id, auth_option_id, auth_setting) SELECT 7, auth_option_id, 1 FROM phpbb_acl_options WHERE auth_option LIKE 'u_%' AND auth_option NOT IN ('u_attach', 'u_viewonline', 'u_chggrp', 'u_chgname', 'u_ignoreflood', 'u_pm_attach', 'u_pm_emailpm', 'u_pm_flash', 'u_savedrafts', 'u_search', 'u_sendemail', 'u_sendim', 'u_masspm', 'u_masspm_group'); # No Private Messages (u_) -INSERT INTO phpbb_acl_roles_data (role_id, auth_option_id, auth_setting) SELECT 8, auth_option_id, 1 FROM phpbb_acl_options WHERE auth_option LIKE 'u_%' AND auth_option IN ('u_', 'u_chgavatar', 'u_chgcensors', 'u_chgemail', 'u_chgpasswd', 'u_download', 'u_hideonline', 'u_sig', 'u_viewprofile'); +INSERT INTO phpbb_acl_roles_data (role_id, auth_option_id, auth_setting) SELECT 8, auth_option_id, 1 FROM phpbb_acl_options WHERE auth_option LIKE 'u_%' AND auth_option IN ('u_', 'u_chgavatar', 'u_chgcensors', 'u_chgemail', 'u_chgpasswd', 'u_download', 'u_hideonline', 'u_mention', 'u_sig', 'u_viewprofile'); INSERT INTO phpbb_acl_roles_data (role_id, auth_option_id, auth_setting) SELECT 8, auth_option_id, 0 FROM phpbb_acl_options WHERE auth_option LIKE 'u_%' AND auth_option IN ('u_readpm', 'u_sendpm', 'u_masspm', 'u_masspm_group'); # No Avatar (u_) From 9ded988c275e7baa7fffc8611ecb56a4084ab2f8 Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 3 Jul 2018 02:47:19 +0300 Subject: [PATCH 034/113] [ticket/13713] Refactor JS PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 61 +++++++++++++++++-------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 076b7fd7ff..66f9fb3444 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -385,36 +385,43 @@ function getCaretPosition(txtarea) { } (function($) { - function mention_default_avatar(type) { - return (type === 'group') ? '' : ''; - } + function Mentions() { + function defaultAvatar(type) { + return (type === 'group') ? '' : ''; + } - function handle_mentions(txtarea) { - var $mentionDataContainer = $('[data-mention-url]'), - mentionURL = $mentionDataContainer.data('mentionUrl'), - mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'), - mentionTopicId = $mentionDataContainer.data('topicId'); - $(txtarea).atwho({ - at: "@", - displayTpl: function(data) { - var avatar = (data.avatar.src) ? "" : mention_default_avatar(data.avatar.type), - rank = (data.rank) ? "" + data.rank + "" : ''; - return "
  • " + avatar + "" + data.name + rank + "
  • "; - }, - insertTpl: "[mention ${param}=${id}]${name}[/mention]", - limit: mentionNamesLimit, - callbacks: { - remoteFilter: function(query, callback) { - $.getJSON(mentionURL, {keyword: query, topic_id: mentionTopicId}, function (data) { - callback(data) - }); + this.isEnabled = function() { + return $('[data-mention-url]').length; + }; + + this.handle = function(txtarea) { + let $mentionDataContainer = $('[data-mention-url]'), + mentionURL = $mentionDataContainer.data('mentionUrl'), + mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'), + mentionTopicId = $mentionDataContainer.data('topicId'); + $(txtarea).atwho({ + at: "@", + displayTpl: function(data) { + let avatar = (data.avatar.src) ? "" : defaultAvatar(data.avatar.type), + rank = (data.rank) ? "" + data.rank + "" : ''; + return "
  • " + avatar + "" + data.name + rank + "
  • "; + }, + insertTpl: "[mention ${param}=${id}]${name}[/mention]", + limit: mentionNamesLimit, + callbacks: { + remoteFilter: function(query, callback) { + $.getJSON(mentionURL, {keyword: query, topic_id: mentionTopicId}, function (data) { + callback(data) + }); + } } - } - }); + }); + }; } + phpbb.mentions = new Mentions(); $(document).ready(function() { - var doc, textarea; + let doc, textarea; // find textarea, make sure browser supports necessary functions if (document.forms[form_name]) { @@ -439,8 +446,8 @@ function getCaretPosition(txtarea) { phpbb.showDragNDrop(textarea); } - if ($('[data-mention-url]').length) { - handle_mentions(textarea); + if (phpbb.mentions.isEnabled()) { + phpbb.mentions.handle(textarea); } $('textarea').on('keydown', function (e) { From c66f4806e8912fb6a3fa0eed917f7db15cb455ca Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 4 Jul 2018 00:47:38 +0300 Subject: [PATCH 035/113] [ticket/13713] Rework BBCode parameters PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 2 +- phpBB/phpbb/mention/source/base_group.php | 2 +- phpBB/phpbb/mention/source/base_user.php | 2 +- phpBB/phpbb/notification/type/mention.php | 2 +- phpBB/phpbb/textformatter/s9e/factory.php | 4 +- .../textformatter/s9e/mention_helper.php | 140 +++++++++--------- 6 files changed, 77 insertions(+), 75 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 66f9fb3444..3514c09ed5 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -406,7 +406,7 @@ function getCaretPosition(txtarea) { rank = (data.rank) ? "" + data.rank + "" : ''; return "
  • " + avatar + "" + data.name + rank + "
  • "; }, - insertTpl: "[mention ${param}=${id}]${name}[/mention]", + insertTpl: "[mention=${type}:${id}]${name}[/mention]", limit: mentionNamesLimit, callbacks: { remoteFilter: function(query, callback) { diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index 592c6e0da2..da1acc6689 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -133,7 +133,7 @@ abstract class base_group implements source_interface $group_rank = phpbb_get_user_rank($groups[$group_id], false); $names['g' . $group_id] = [ 'name' => $groups[$group_id]['group_name'], - 'param' => 'group_id', + 'type' => 'g', 'id' => $group_id, 'avatar' => [ 'type' => 'group', diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index a78f995a75..bc1bcc0053 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -70,7 +70,7 @@ abstract class base_user implements source_interface $user_rank = $this->user_loader->get_rank($row['user_id'], true); $names['u' . $row['user_id']] = [ 'name' => $row['username'], - 'param' => 'user_id', + 'type' => 'u', 'id' => $row['user_id'], 'avatar' => [ 'type' => 'user', diff --git a/phpBB/phpbb/notification/type/mention.php b/phpBB/phpbb/notification/type/mention.php index 1161814dbe..21d4bf6f91 100644 --- a/phpBB/phpbb/notification/type/mention.php +++ b/phpBB/phpbb/notification/type/mention.php @@ -75,7 +75,7 @@ class mention extends \phpbb\notification\type\post 'ignore_users' => array(), ), $options); - $user_ids = $this->helper->get_mentioned_users($post['post_text']); + $user_ids = $this->helper->get_mentioned_ids($post['post_text']); $user_ids = array_unique($user_ids); diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php index 16bd63cf73..66c6b5132e 100644 --- a/phpBB/phpbb/textformatter/s9e/factory.php +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -85,10 +85,8 @@ class factory implements \phpbb\textformatter\cache_interface 'list' => '[LIST type={HASHMAP=1:decimal,a:lower-alpha,A:upper-alpha,i:lower-roman,I:upper-roman;optional;postFilter=#simpletext} #createChild=LI]{TEXT}[/LIST]', 'li' => '[* $tagName=LI]{TEXT}[/*]', 'mention' => - "[MENTION - group_id={UINT;optional} + "[MENTION={PARSE=/^(?[ug]):(?\d+)$/} profile_url={URL;optional;postFilter=#false} - user_id={UINT;optional} ]{TEXT}[/MENTION]", 'quote' => "[QUOTE diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index fded9c5cd4..9e0cd65437 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -23,17 +23,17 @@ class mention_helper protected $db; /** - * @var string Base URL for a user profile link, uses {USER_ID} as placeholder + * @var string Base URL for a user profile link, uses {ID} as placeholder */ protected $user_profile_url; /** - * @var string Base URL for a group profile link, uses {GROUP_ID} as placeholder + * @var string Base URL for a group profile link, uses {ID} as placeholder */ protected $group_profile_url; /** - * @var array Array of users' and groups' colors for each cached ID + * @var array Array of users' and groups' colours for each cached ID */ protected $cached_colours = []; @@ -47,57 +47,61 @@ class mention_helper public function __construct($db, $root_path, $php_ext) { $this->db = $db; - $this->user_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={USER_ID}', false); - $this->group_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=group&g={GROUP_ID}', false); + $this->user_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={ID}', false); + $this->group_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=group&g={ID}', false); } /** - * Caches colors for specified user IDs and group IDs + * Returns SQL query data for colour SELECT request * - * @param array $user_ids - * @param array $group_ids + * @param string $type Name type ('u' for users, 'g' for groups) + * @param array $ids Array of IDs + * @return array Array of SQL SELECT query data for extracting colours for names */ - protected function get_colors($user_ids, $group_ids) + protected function get_colours_sql($type, $ids) { - $this->cached_colours = []; - $this->cached_colours['users'] = []; - $this->cached_colours['groups'] = []; - - if (!empty($user_ids)) + switch ($type) { - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.user_colour, u.user_id', - 'FROM' => [ - USERS_TABLE => 'u', - ], - 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' - AND ' . $this->db->sql_in_set('u.user_id', $user_ids), - ]); - $result = $this->db->sql_query($query); - - while ($row = $this->db->sql_fetchrow($result)) - { - $this->cached_colours['users'][$row['user_id']] = $row['user_colour']; - } - - $this->db->sql_freeresult($result); + default: + case 'u': + return [ + 'SELECT' => 'u.user_colour as colour, u.user_id as id', + 'FROM' => [ + USERS_TABLE => 'u', + ], + 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND ' . $this->db->sql_in_set('u.user_id', $ids), + ]; + case 'g': + return [ + 'SELECT' => 'g.group_colour as colour, g.group_id as id', + 'FROM' => [ + GROUPS_TABLE => 'g', + ], + 'WHERE' => $this->db->sql_in_set('g.group_id', $ids), + ]; } + } - if (!empty($group_ids)) + /** + * Caches colours for selected IDs of the specified type + * + * @param string $type Name type ('u' for users, 'g' for groups) + * @param array $ids Array of IDs + */ + protected function get_colours($type, $ids) + { + $this->cached_colours[$type] = []; + + if (!empty($ids)) { - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'g.group_colour, g.group_id', - 'FROM' => [ - GROUPS_TABLE => 'g', - ], - 'WHERE' => $this->db->sql_in_set('g.group_id', $group_ids), - ]); + $query = $this->db->sql_build_query('SELECT', $this->get_colours_sql($type, $ids)); $result = $this->db->sql_query($query); while ($row = $this->db->sql_fetchrow($result)) { - $this->cached_colours['groups'][$row['group_id']] = $row['group_colour']; + $this->cached_colours[$type][$row['id']] = $row['colour']; } $this->db->sql_freeresult($result); @@ -112,36 +116,31 @@ class mention_helper */ public function inject_metadata($xml) { - $user_profile_url = $this->user_profile_url; - $group_profile_url = $this->group_profile_url; + $profile_urls = [ + 'u' => $this->user_profile_url, + 'g' => $this->group_profile_url, + ]; // TODO: think about optimization for caching colors. - $this->get_colors( - TextFormatterUtils::getAttributeValues($xml, 'MENTION', 'user_id'), - TextFormatterUtils::getAttributeValues($xml, 'MENTION', 'group_id') - ); + $this->cached_colours = []; + $this->get_colours('u', $this->get_mentioned_ids($xml, 'u')); + $this->get_colours('g', $this->get_mentioned_ids($xml, 'g')); return TextFormatterUtils::replaceAttributes( $xml, 'MENTION', - function ($attributes) use ($user_profile_url, $group_profile_url) + function ($attributes) use ($profile_urls) { - if (isset($attributes['user_id'])) + if (isset($attributes['type']) && isset($attributes['id'])) { - $attributes['profile_url'] = str_replace('{USER_ID}', $attributes['user_id'], $user_profile_url); + $type = $attributes['type']; + $id = $attributes['id']; - if (!empty($this->cached_colours['users'][$attributes['user_id']])) - { - $attributes['color'] = $this->cached_colours['users'][$attributes['user_id']]; - } - } - else if (isset($attributes['group_id'])) - { - $attributes['profile_url'] = str_replace('{GROUP_ID}', $attributes['group_id'], $group_profile_url); + $attributes['profile_url'] = str_replace('{ID}', $id, $profile_urls[$type]); - if (!empty($this->cached_colours['groups'][$attributes['group_id']])) + if (!empty($this->cached_colours[$type][$id])) { - $attributes['color'] = $this->cached_colours['groups'][$attributes['group_id']]; + $attributes['color'] = $this->cached_colours[$type][$id]; } } @@ -151,28 +150,33 @@ class mention_helper } /** - * Get a list of mentioned users + * Get a list of mentioned names * TODO: decide what to do with groups * - * @param string $xml Parsed text - * @return int[] List of user IDs + * @param string $xml Parsed text + * @param string $type Name type ('u' for users, 'g' for groups) + * @return int[] List of IDs */ - public function get_mentioned_users($xml) + public function get_mentioned_ids($xml, $type = 'u') { - $user_ids = array(); + $ids = array(); if (strpos($xml, 'loadXML($xml); $xpath = new \DOMXPath($dom); - foreach ($xpath->query('//MENTION/@user_id') as $user_id) + /** @var \DOMElement $mention */ + foreach ($xpath->query('//MENTION') as $mention) { - $user_ids[] = (int) $user_id->textContent; + if ($mention->getAttribute('type') === $type) + { + $ids[] = (int) $mention->getAttribute('id'); + } } - return $user_ids; + return $ids; } } From 6d849f2cce2b5c932d516fb7a98b9e68d1b73fd9 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 4 Jul 2018 01:37:35 +0300 Subject: [PATCH 036/113] [ticket/13713] Fix avatars PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 6 ++++-- phpBB/phpbb/mention/source/base_group.php | 2 +- phpBB/phpbb/mention/source/base_user.php | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 3514c09ed5..d6287f5e49 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -402,7 +402,8 @@ function getCaretPosition(txtarea) { $(txtarea).atwho({ at: "@", displayTpl: function(data) { - let avatar = (data.avatar.src) ? "" : defaultAvatar(data.avatar.type), + // TODO: handle image scaling + let avatar = (data.avatar.img) ? "" + data.avatar.img + "" : defaultAvatar(data.avatar.type), rank = (data.rank) ? "" + data.rank + "" : ''; return "
  • " + avatar + "" + data.name + rank + "
  • "; }, @@ -410,7 +411,8 @@ function getCaretPosition(txtarea) { limit: mentionNamesLimit, callbacks: { remoteFilter: function(query, callback) { - $.getJSON(mentionURL, {keyword: query, topic_id: mentionTopicId}, function (data) { + let params = {keyword: query, topic_id: mentionTopicId, _referer: location.href}; + $.getJSON(mentionURL, params, function (data) { callback(data) }); } diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index da1acc6689..644119ecd9 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -137,7 +137,7 @@ abstract class base_group implements source_interface 'id' => $group_id, 'avatar' => [ 'type' => 'group', - 'src' => phpbb_get_group_avatar($groups[$group_id]), + 'img' => phpbb_get_group_avatar($groups[$group_id]), ], 'rank' => $group_rank['title'], ]; diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index bc1bcc0053..804c20c14d 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -74,7 +74,7 @@ abstract class base_user implements source_interface 'id' => $row['user_id'], 'avatar' => [ 'type' => 'user', - 'src' => $this->user_loader->get_avatar($row['user_id'], true), + 'img' => $this->user_loader->get_avatar($row['user_id'], true), ], 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', ]; From d1952440042b596a630bc75061aea32100ac3230 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 6 Jul 2018 07:30:07 +0300 Subject: [PATCH 037/113] [ticket/13713] Introduce priorities based sorting PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 68 ++++++++++++++++++++-- phpBB/phpbb/mention/controller/mention.php | 2 +- phpBB/phpbb/mention/source/base_group.php | 2 +- phpBB/phpbb/mention/source/base_user.php | 19 +++++- 4 files changed, 82 insertions(+), 9 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index d6287f5e49..aa39c15568 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -386,21 +386,24 @@ function getCaretPosition(txtarea) { (function($) { function Mentions() { + let $mentionDataContainer = $('[data-mention-url]:first'), cachedNames = null, filteredNamesCount = 0; + function defaultAvatar(type) { return (type === 'group') ? '' : ''; } this.isEnabled = function() { - return $('[data-mention-url]').length; + return $mentionDataContainer.length; }; this.handle = function(txtarea) { - let $mentionDataContainer = $('[data-mention-url]'), - mentionURL = $mentionDataContainer.data('mentionUrl'), + let mentionURL = $mentionDataContainer.data('mentionUrl'), mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'), - mentionTopicId = $mentionDataContainer.data('topicId'); + mentionTopicId = $mentionDataContainer.data('topicId'), + defaultFilter = $.fn.atwho['default'].callbacks.filter; $(txtarea).atwho({ at: "@", + acceptSpaceBar: true, displayTpl: function(data) { // TODO: handle image scaling let avatar = (data.avatar.img) ? "" + data.avatar.img + "" : defaultAvatar(data.avatar.type), @@ -410,11 +413,66 @@ function getCaretPosition(txtarea) { insertTpl: "[mention=${type}:${id}]${name}[/mention]", limit: mentionNamesLimit, callbacks: { + filter: function(query, data, searchKey) { + data = defaultFilter(query, data, searchKey); + + // Update our cached statistics used by remoteFilter + filteredNamesCount = data.length; + + return data; + }, remoteFilter: function(query, callback) { + if (cachedNames && filteredNamesCount >= mentionNamesLimit) { + callback(cachedNames); + return; + } let params = {keyword: query, topic_id: mentionTopicId, _referer: location.href}; $.getJSON(mentionURL, params, function (data) { - callback(data) + cachedNames = data; + callback(data); }); + }, + sorter: function(query, items, searchKey) { + let _unsorted, _results, _exactMatch, i, item, len, highestPriority = 0; + _unsorted = {u: {}, g: {}}; + _exactMatch = []; + for (i = 0, len = items.length; i < len; i++) { + item = items[i]; + if (item.name === query) { + _exactMatch.push(items[i]); + continue; + } + if (!_unsorted[item.type]) { + continue; + } + if (!_unsorted[item.type][item.id] || item.type === 'g') { + _unsorted[item.type][item.id] = item; + continue; + } + _unsorted[item.type][item.id].priority += parseFloat(item.priority); + highestPriority = Math.max(highestPriority, _unsorted[item.type][item.id].priority); + } + _results = []; + if (_unsorted['u']) { + $.each(_unsorted['u'], function(name, value) { + _results.push(value); + }); + } + if (_unsorted['g']) { + $.each(_unsorted['g'], function(name, value) { + // Groups should come at the same level of importance + // as users, otherwise they will be unlikely to be shown + value.priority = highestPriority; + _results.push(value); + }); + } + _results = _results.sort(function(a, b) { + return a.priority - b.priority; + }); + $.each(_exactMatch, function(name, value) { + _results.unshift(value); + }); + return _results; } } }); diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index b4a42799b9..0aa08e090b 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -54,7 +54,7 @@ class mention foreach ($this->mention_sources as $source) { - $names = array_merge($names, $source->get($keyword, $topic_id)); + $names += $source->get($keyword, $topic_id); } return new JsonResponse(array_values($names)); diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index 644119ecd9..9a8c70695c 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -131,7 +131,7 @@ abstract class base_group implements source_interface foreach ($group_ids as $group_id) { $group_rank = phpbb_get_user_rank($groups[$group_id], false); - $names['g' . $group_id] = [ + $names[] = [ 'name' => $groups[$group_id]['group_name'], 'type' => 'g', 'id' => $group_id, diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index 804c20c14d..45f5e3b917 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -15,6 +15,9 @@ namespace phpbb\mention\source; abstract class base_user implements source_interface { + /** @var int */ + const NAMES_BATCH_SIZE = 100; + /** @var \phpbb\db\driver\driver_interface */ protected $db; @@ -56,19 +59,30 @@ abstract class base_user implements source_interface */ abstract protected function query($keyword, $topic_id); + /** + * Returns the priority of the currently selected name + * + * @param array $row Array of fetched user data + * @return int Priority (defaults to 1) + */ + public function get_priority($row) + { + return 1; + } + /** * {@inheritdoc} */ public function get($keyword, $topic_id) { $keyword = utf8_clean_string($keyword); - $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), $this->config['mention_names_limit']); + $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), self::NAMES_BATCH_SIZE); $names = []; while ($row = $this->db->sql_fetchrow($result)) { $user_rank = $this->user_loader->get_rank($row['user_id'], true); - $names['u' . $row['user_id']] = [ + $names[] = [ 'name' => $row['username'], 'type' => 'u', 'id' => $row['user_id'], @@ -77,6 +91,7 @@ abstract class base_user implements source_interface 'img' => $this->user_loader->get_avatar($row['user_id'], true), ], 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', + 'priority' => $this->get_priority($row), ]; } From ffbff7ed7924164d1b21ffdb7f5c204b3db11d86 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 7 Jul 2018 23:53:20 +0300 Subject: [PATCH 038/113] [ticket/13713] Make changes pointed by @Nicofuma PHPBB3-13713 --- phpBB/language/en/email/mention.txt | 16 ++++++++-------- phpBB/phpbb/mention/controller/mention.php | 3 ++- phpBB/phpbb/mention/source/base_group.php | 17 +++++++++-------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/phpBB/language/en/email/mention.txt b/phpBB/language/en/email/mention.txt index 51c161453e..95bc4c8601 100644 --- a/phpBB/language/en/email/mention.txt +++ b/phpBB/language/en/email/mention.txt @@ -1,20 +1,20 @@ -Subject: Topic reply notification - "{TOPIC_TITLE}" +Subject: Topic reply notification - "{{ TOPIC_TITLE }}" -Hello {USERNAME}, +Hello {{ USERNAME }}, -You are receiving this notification because "{AUTHOR_NAME}" mentioned you in the topic "{TOPIC_TITLE}" at "{SITENAME}". You can use the following link to view the reply made. +You are receiving this notification because "{{ AUTHOR_NAME }}" mentioned you in the topic "{{ TOPIC_TITLE }}" at "{{ SITENAME }}". You can use the following link to view the reply made. If you want to view the post where you have been mentioned, click the following link: -{U_VIEW_POST} +{{ U_VIEW_POST }} If you want to view the topic, click the following link: -{U_TOPIC} +{{ U_TOPIC }} If you want to view the forum, click the following link: -{U_FORUM} +{{ U_FORUM }} If you no longer wish to receive updates about replies mentioning you, please update your notification settings here: -{U_NOTIFICATION_SETTINGS} +{{ U_NOTIFICATION_SETTINGS }} -{EMAIL_SIG} +{{ EMAIL_SIG }} diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index 0aa08e090b..6548a8a995 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -14,6 +14,7 @@ namespace phpbb\mention\controller; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; class mention { @@ -45,7 +46,7 @@ class mention { // if (!$this->request->is_ajax()) // { -// redirect(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); +// new RedirectResponse(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); // } $keyword = $this->request->variable('keyword', '', true); diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index 9a8c70695c..b8d6c44091 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -33,6 +33,9 @@ abstract class base_group implements source_interface /** @var string */ protected $php_ext; + /** @var array Fetched groups' data */ + protected $groups = null; + /** * Constructor */ @@ -58,9 +61,7 @@ abstract class base_group implements source_interface */ protected function get_groups() { - static $groups = null; - - if (is_null($groups)) + if (is_null($this->groups)) { $query = $this->db->sql_build_query('SELECT', [ 'SELECT' => 'g.*, ug.user_id as ug_user_id', @@ -76,7 +77,7 @@ abstract class base_group implements source_interface ]); $result = $this->db->sql_query($query); - $groups = []; + $this->groups = []; while ($row = $this->db->sql_fetchrow($result)) { if ($row['group_type'] == GROUP_SPECIAL && !in_array($row['group_name'], ['ADMINISTRATORS', 'GLOBAL_MODERATORS']) || $row['group_type'] == GROUP_HIDDEN && !$this->auth->acl_gets('a_group', 'a_groupadd', 'a_groupdel') && $row['ug_user_id'] != $this->user->data['user_id']) @@ -86,14 +87,14 @@ abstract class base_group implements source_interface } $group_name = $this->helper->get_name($row['group_name']); - $groups['names'][$row['group_id']] = $group_name; - $groups[$row['group_id']] = $row; - $groups[$row['group_id']]['group_name'] = $group_name; + $this->groups['names'][$row['group_id']] = $group_name; + $this->groups[$row['group_id']] = $row; + $this->groups[$row['group_id']]['group_name'] = $group_name; } $this->db->sql_freeresult($result); } - return $groups; + return $this->groups; } /** From 2bb50add04801d0f9cc25244415ad9fb94a6e6b6 Mon Sep 17 00:00:00 2001 From: lavigor Date: Mon, 9 Jul 2018 02:16:42 +0300 Subject: [PATCH 039/113] [ticket/13713] Refactor sorting functionality PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 2 +- phpBB/assets/javascript/editor.js | 91 +++++++++++++++---- phpBB/includes/functions.php | 1 + phpBB/includes/functions_acp.php | 1 + phpBB/phpbb/mention/controller/mention.php | 2 +- phpBB/phpbb/mention/source/base_group.php | 7 +- phpBB/phpbb/mention/source/base_user.php | 7 +- phpBB/phpbb/mention/source/friend.php | 1 - .../phpbb/mention/source/source_interface.php | 3 +- phpBB/phpbb/mention/source/team.php | 1 - phpBB/phpbb/mention/source/topic.php | 2 +- phpBB/phpbb/mention/source/user.php | 3 +- .../prosilver/template/posting_buttons.html | 2 +- 13 files changed, 86 insertions(+), 37 deletions(-) diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index 5398726da7..770501cabb 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -13,7 +13,7 @@ -
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-topic-id="{S_TOPIC_ID}"> +
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-mention-batch-size="{S_MENTION_BATCH_SIZE}" data-topic-id="{S_TOPIC_ID}"> diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index aa39c15568..f15b776889 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -386,21 +386,40 @@ function getCaretPosition(txtarea) { (function($) { function Mentions() { - let $mentionDataContainer = $('[data-mention-url]:first'), cachedNames = null, filteredNamesCount = 0; + let $mentionDataContainer = $('[data-mention-url]:first'); + let cachedNames = null; + let cachedFor = null; + let cachedSearchKey = 'name'; function defaultAvatar(type) { return (type === 'group') ? '' : ''; } + function getMatchedNames(query, items, searchKey) { + let i, len; + let _results = []; + for (i = 0, len = items.length; i < len; i++) { + let item = items[i]; + if (String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()) === 0) { + _results.push(item); + } + } + return _results; + } + + function getNumberOfMatchedCachedNames(query) { + return getMatchedNames(query, cachedNames, cachedSearchKey).length; + } + this.isEnabled = function() { return $mentionDataContainer.length; }; this.handle = function(txtarea) { - let mentionURL = $mentionDataContainer.data('mentionUrl'), - mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'), - mentionTopicId = $mentionDataContainer.data('topicId'), - defaultFilter = $.fn.atwho['default'].callbacks.filter; + let mentionURL = $mentionDataContainer.data('mentionUrl'); + let mentionBatchSize = $mentionDataContainer.data('mentionBatchSize'); + let mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'); + let mentionTopicId = $mentionDataContainer.data('topicId'); $(txtarea).atwho({ at: "@", acceptSpaceBar: true, @@ -413,51 +432,78 @@ function getCaretPosition(txtarea) { insertTpl: "[mention=${type}:${id}]${name}[/mention]", limit: mentionNamesLimit, callbacks: { - filter: function(query, data, searchKey) { - data = defaultFilter(query, data, searchKey); - - // Update our cached statistics used by remoteFilter - filteredNamesCount = data.length; - - return data; - }, remoteFilter: function(query, callback) { - if (cachedNames && filteredNamesCount >= mentionNamesLimit) { + /* + * Use cached values when we can: + * 1) There are some names in the cache + * 2) The cache contains relevant data for the query + * (it was made for the query with the same first characters) + * 3) We have enough names to display OR + * all relevant names have been fetched from the server + */ + if (cachedNames && + query.indexOf(cachedFor) === 0 && + (getNumberOfMatchedCachedNames(query) >= mentionNamesLimit || + cachedNames.length < mentionBatchSize)) { callback(cachedNames); return; } + let params = {keyword: query, topic_id: mentionTopicId, _referer: location.href}; $.getJSON(mentionURL, params, function (data) { cachedNames = data; + cachedFor = query; callback(data); }); }, sorter: function(query, items, searchKey) { - let _unsorted, _results, _exactMatch, i, item, len, highestPriority = 0; - _unsorted = {u: {}, g: {}}; - _exactMatch = []; + let i, len; + let highestPriority = 0; + let _unsorted = {u: {}, g: {}}; + let _exactMatch = []; + let _results = []; + + // Reduce the items array to the relevant ones + items = getMatchedNames(query, items, searchKey); + + // Group names by their types and calculate priorities for (i = 0, len = items.length; i < len; i++) { - item = items[i]; + let item = items[i]; + + // Exact matches should not be prioritised - they always come first if (item.name === query) { _exactMatch.push(items[i]); continue; } + + // Check for unsupported type - in general, this should never happen if (!_unsorted[item.type]) { continue; } + + // If the item hasn't been added yet - add it + // Group names do not have priorities and are also handled here if (!_unsorted[item.type][item.id] || item.type === 'g') { _unsorted[item.type][item.id] = item; continue; } + + // Priority is calculated as the sum of priorities from different sources _unsorted[item.type][item.id].priority += parseFloat(item.priority); + + // Calculate the highest priority - we'll give it to group names highestPriority = Math.max(highestPriority, _unsorted[item.type][item.id].priority); } - _results = []; + + // Push user names to the result array if (_unsorted['u']) { $.each(_unsorted['u'], function(name, value) { _results.push(value); }); } + + // Push group names to the result array and give them the highest priority + // They will be sorted together with the usernames on top of the list if (_unsorted['g']) { $.each(_unsorted['g'], function(name, value) { // Groups should come at the same level of importance @@ -466,12 +512,17 @@ function getCaretPosition(txtarea) { _results.push(value); }); } + + // Sort names by priorities - higher values come first _results = _results.sort(function(a, b) { - return a.priority - b.priority; + return b.priority - a.priority; }); + + // Exact match is the most important - should come above anything else $.each(_exactMatch, function(name, value) { _results.unshift(value); }); + return _results; } } diff --git a/phpBB/includes/functions.php b/phpBB/includes/functions.php index bc9bc3dbd9..e00c2fc25d 100644 --- a/phpBB/includes/functions.php +++ b/phpBB/includes/functions.php @@ -3947,6 +3947,7 @@ function page_header($page_title = '', $display_online_list = false, $item_id = 'U_FEED' => $controller_helper->route('phpbb_feed_index'), 'S_ALLOW_MENTIONS' => ($config['allow_mentions'] && $auth->acl_get('u_mention') && (empty($forum_id) || $auth->acl_get('f_mention', $forum_id))) ? true : false, + 'S_MENTION_BATCH_SIZE' => 100, // TODO: do not hardcode the value 'S_MENTION_NAMES_LIMIT' => $config['mention_names_limit'], 'U_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), diff --git a/phpBB/includes/functions_acp.php b/phpBB/includes/functions_acp.php index a2ffe0b1b2..e1d3159e02 100644 --- a/phpBB/includes/functions_acp.php +++ b/phpBB/includes/functions_acp.php @@ -90,6 +90,7 @@ function adm_page_header($page_title) 'U_INDEX' => append_sid("{$phpbb_root_path}index.$phpEx"), 'S_ALLOW_MENTIONS' => ($config['allow_mentions'] && $auth->acl_get('u_mention')) ? true : false, + 'S_MENTION_BATCH_SIZE' => 100, // TODO: do not hardcode the value 'S_MENTION_NAMES_LIMIT' => $config['mention_names_limit'], 'U_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index 6548a8a995..a188ea6bf8 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -55,7 +55,7 @@ class mention foreach ($this->mention_sources as $source) { - $names += $source->get($keyword, $topic_id); + $source->get($names, $keyword, $topic_id); } return new JsonResponse(array_values($names)); diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index b8d6c44091..d7037f77ae 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -109,7 +109,7 @@ abstract class base_group implements source_interface /** * {@inheritdoc} */ - public function get($keyword, $topic_id) + public function get(array &$names, $keyword, $topic_id) { // Grab all group IDs $result = $this->db->sql_query($this->query($keyword, $topic_id)); @@ -128,11 +128,10 @@ abstract class base_group implements source_interface $matches = preg_grep('/^' . preg_quote($keyword) . '.*/i', $groups['names']); $group_ids = array_intersect($group_ids, array_flip($matches)); - $names = []; foreach ($group_ids as $group_id) { $group_rank = phpbb_get_user_rank($groups[$group_id], false); - $names[] = [ + array_push($names, [ 'name' => $groups[$group_id]['group_name'], 'type' => 'g', 'id' => $group_id, @@ -141,7 +140,7 @@ abstract class base_group implements source_interface 'img' => phpbb_get_group_avatar($groups[$group_id]), ], 'rank' => $group_rank['title'], - ]; + ]); } return $names; diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index 45f5e3b917..7ac830fc6e 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -73,16 +73,15 @@ abstract class base_user implements source_interface /** * {@inheritdoc} */ - public function get($keyword, $topic_id) + public function get(array &$names, $keyword, $topic_id) { $keyword = utf8_clean_string($keyword); $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), self::NAMES_BATCH_SIZE); - $names = []; while ($row = $this->db->sql_fetchrow($result)) { $user_rank = $this->user_loader->get_rank($row['user_id'], true); - $names[] = [ + array_push($names, [ 'name' => $row['username'], 'type' => 'u', 'id' => $row['user_id'], @@ -92,7 +91,7 @@ abstract class base_user implements source_interface ], 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', 'priority' => $this->get_priority($row), - ]; + ]); } $this->db->sql_freeresult($result); diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index 3c946dcd73..60e706b458 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -45,7 +45,6 @@ class friend extends base_user ] ], 'WHERE' => 'z.friend = 1 AND z.user_id = ' . (int) $this->user->data['user_id'] . ' - AND u.user_id <> ' . ANONYMOUS . ' AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), 'ORDER_BY' => 'u.user_lastvisit DESC' diff --git a/phpBB/phpbb/mention/source/source_interface.php b/phpBB/phpbb/mention/source/source_interface.php index ace5cc9149..43b6106363 100644 --- a/phpBB/phpbb/mention/source/source_interface.php +++ b/phpBB/phpbb/mention/source/source_interface.php @@ -19,9 +19,10 @@ interface source_interface * Searches database for names to mention * and returns and array of found items * + * @param array $names Array of already fetched data with names * @param string $keyword Search string * @param int $topic_id Current topic ID * @return array Array of names */ - public function get($keyword, $topic_id); + public function get(array &$names, $keyword, $topic_id); } diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index 187f0fb691..2cfffd9f82 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -28,7 +28,6 @@ class team extends base_user TEAMPAGE_TABLE => 't', ], 'WHERE' => 'ug.group_id = t.group_id AND ug.user_id = u.user_id AND ug.user_pending = 0 - AND u.user_id <> ' . ANONYMOUS . ' AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), 'ORDER_BY' => 'u.user_lastvisit DESC' diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index f8a5123c56..688495ddc7 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -31,7 +31,7 @@ class topic extends base_user 'ON' => 'u.user_id = p.poster_id' ] ], - 'WHERE' => 'p.topic_id = ' . $topic_id . ' AND u.user_id <> ' . ANONYMOUS . ' + 'WHERE' => 'p.topic_id = ' . $topic_id . ' AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), 'ORDER_BY' => 'p.post_time DESC' diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index 10f065df0d..8a03bc41a8 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -25,8 +25,7 @@ class user extends base_user 'FROM' => [ USERS_TABLE => 'u', ], - 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), 'ORDER_BY' => 'u.user_lastvisit DESC' ]); diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 5765a29dca..449f677dd9 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -39,7 +39,7 @@
    -
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-topic-id="{S_TOPIC_ID}"> +
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-mention-batch-size="{S_MENTION_BATCH_SIZE}" data-topic-id="{S_TOPIC_ID}"> From 783449d626cf85bcb4632ba550959943bcebd4fd Mon Sep 17 00:00:00 2001 From: lavigor Date: Mon, 9 Jul 2018 02:26:19 +0300 Subject: [PATCH 040/113] [ticket/13713] Fix issue with duplicate queries PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index f15b776889..2f854080eb 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -387,6 +387,7 @@ function getCaretPosition(txtarea) { (function($) { function Mentions() { let $mentionDataContainer = $('[data-mention-url]:first'); + let queryInProgress = null; let cachedNames = null; let cachedFor = null; let cachedSearchKey = 'name'; @@ -449,10 +450,20 @@ function getCaretPosition(txtarea) { return; } + /* + * Do not make a new request until the previous one for the same query is returned + * This fixes duplicate server queries e.g. when arrow keys are pressed + */ + if (queryInProgress === query) { + return; + } + queryInProgress = query; + let params = {keyword: query, topic_id: mentionTopicId, _referer: location.href}; $.getJSON(mentionURL, params, function (data) { cachedNames = data; cachedFor = query; + queryInProgress = null; callback(data); }); }, From b5ce3343ed62d1a9f1dc89d67095698772b28790 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 11 Jul 2018 01:06:58 +0300 Subject: [PATCH 041/113] [ticket/13713] Fix variable definitions as requested by @hanakin PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 2f854080eb..9972ffdd6a 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -397,7 +397,8 @@ function getCaretPosition(txtarea) { } function getMatchedNames(query, items, searchKey) { - let i, len; + let i; + let len; let _results = []; for (i = 0, len = items.length; i < len; i++) { let item = items[i]; @@ -468,7 +469,8 @@ function getCaretPosition(txtarea) { }); }, sorter: function(query, items, searchKey) { - let i, len; + let i; + let len; let highestPriority = 0; let _unsorted = {u: {}, g: {}}; let _exactMatch = []; @@ -543,7 +545,8 @@ function getCaretPosition(txtarea) { phpbb.mentions = new Mentions(); $(document).ready(function() { - let doc, textarea; + let doc; + let textarea; // find textarea, make sure browser supports necessary functions if (document.forms[form_name]) { From 0aadd52014d598baac06a344a8f5cf01fe60b68d Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 11 Jul 2018 02:56:31 +0300 Subject: [PATCH 042/113] [ticket/13713] Cache SQL queries PHPBB3-13713 --- phpBB/phpbb/mention/controller/mention.php | 8 +++---- phpBB/phpbb/mention/source/base_group.php | 19 ++++++++------- phpBB/phpbb/mention/source/base_user.php | 23 +++++++++++++++---- phpBB/phpbb/mention/source/friend.php | 10 +++++--- .../phpbb/mention/source/source_interface.php | 3 +-- phpBB/phpbb/mention/source/team.php | 10 +++++--- phpBB/phpbb/mention/source/topic.php | 8 +++++-- phpBB/phpbb/mention/source/user.php | 7 +++--- phpBB/phpbb/mention/source/usergroup.php | 2 +- 9 files changed, 58 insertions(+), 32 deletions(-) diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index a188ea6bf8..d2b43d0914 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -44,10 +44,10 @@ class mention public function handle() { -// if (!$this->request->is_ajax()) -// { -// new RedirectResponse(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); -// } + if (!$this->request->is_ajax()) + { + new RedirectResponse(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); + } $keyword = $this->request->variable('keyword', '', true); $topic_id = $this->request->variable('topic_id', 0); diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index d7037f77ae..607fb91cc9 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -68,14 +68,15 @@ abstract class base_group implements source_interface 'FROM' => [ GROUPS_TABLE => 'g', ], - 'LEFT_JOIN' => array( - array( - 'FROM' => array(USER_GROUP_TABLE => 'ug'), + 'LEFT_JOIN' => [ + [ + 'FROM' => [USER_GROUP_TABLE => 'ug'], 'ON' => 'ug.group_id = g.group_id AND ug.user_pending = 0 AND ug.user_id = ' . (int) $this->user->data['user_id'], - ), - ), + ], + ], ]); - $result = $this->db->sql_query($query); + // Cache results for 5 minutes + $result = $this->db->sql_query($query, 600); $this->groups = []; while ($row = $this->db->sql_fetchrow($result)) @@ -111,8 +112,8 @@ abstract class base_group implements source_interface */ public function get(array &$names, $keyword, $topic_id) { - // Grab all group IDs - $result = $this->db->sql_query($this->query($keyword, $topic_id)); + // Grab all group IDs, cache for 5 minutes + $result = $this->db->sql_query($this->query($keyword, $topic_id), 300); $group_ids = []; while ($row = $this->db->sql_fetchrow($result)) @@ -142,7 +143,5 @@ abstract class base_group implements source_interface 'rank' => $group_rank['title'], ]); } - - return $names; } } diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index 7ac830fc6e..8ab1d05df1 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -76,10 +76,27 @@ abstract class base_user implements source_interface public function get(array &$names, $keyword, $topic_id) { $keyword = utf8_clean_string($keyword); - $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), self::NAMES_BATCH_SIZE); - while ($row = $this->db->sql_fetchrow($result)) + // Do not query all possible users (just a moderate amount), cache results for 5 minutes + $result = $this->db->sql_query($this->query($keyword, $topic_id), 300); + + $i = 0; + while ($i < self::NAMES_BATCH_SIZE) { + $row = $this->db->sql_fetchrow($result); + + if (!$row) + { + break; + } + + if (!empty($keyword) && strpos($row['username_clean'], $keyword) !== 0) + { + continue; + } + + $i++; + $user_rank = $this->user_loader->get_rank($row['user_id'], true); array_push($names, [ 'name' => $row['username'], @@ -95,7 +112,5 @@ abstract class base_user implements source_interface } $this->db->sql_freeresult($result); - - return $names; } } diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index 60e706b458..dfd90a813c 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -33,8 +33,13 @@ class friend extends base_user */ protected function query($keyword, $topic_id) { + /* + * For optimization purposes all friends are returned regardless of the keyword + * Names filtering is done on the frontend + * Results will be cached on a per-user basis + */ $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username, u.user_id', + 'SELECT' => 'u.username_clean, u.username, u.user_id', 'FROM' => [ USERS_TABLE => 'u', ], @@ -45,8 +50,7 @@ class friend extends base_user ] ], 'WHERE' => 'z.friend = 1 AND z.user_id = ' . (int) $this->user->data['user_id'] . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' - AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), 'ORDER_BY' => 'u.user_lastvisit DESC' ]); return $query; diff --git a/phpBB/phpbb/mention/source/source_interface.php b/phpBB/phpbb/mention/source/source_interface.php index 43b6106363..7c7da7369f 100644 --- a/phpBB/phpbb/mention/source/source_interface.php +++ b/phpBB/phpbb/mention/source/source_interface.php @@ -17,12 +17,11 @@ interface source_interface { /** * Searches database for names to mention - * and returns and array of found items + * and alters the passed array of found items * * @param array $names Array of already fetched data with names * @param string $keyword Search string * @param int $topic_id Current topic ID - * @return array Array of names */ public function get(array &$names, $keyword, $topic_id); } diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index 2cfffd9f82..89c1f4071e 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -20,16 +20,20 @@ class team extends base_user */ protected function query($keyword, $topic_id) { + /* + * For optimization purposes all team members are returned regardless of the keyword + * Names filtering is done on the frontend + * Results will be cached in a single file + */ $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username, u.user_id', + 'SELECT' => 'u.username_clean, u.username, u.user_id', 'FROM' => [ USERS_TABLE => 'u', USER_GROUP_TABLE => 'ug', TEAMPAGE_TABLE => 't', ], 'WHERE' => 'ug.group_id = t.group_id AND ug.user_id = u.user_id AND ug.user_pending = 0 - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' - AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), 'ORDER_BY' => 'u.user_lastvisit DESC' ]); return $query; diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index 688495ddc7..da1d6152d6 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -20,6 +20,11 @@ class topic extends base_user */ protected function query($keyword, $topic_id) { + /* + * For optimization purposes all users are returned regardless of the keyword + * Names filtering is done on the frontend + * Results will be cached on a per-topic basis + */ $query = $this->db->sql_build_query('SELECT', [ 'SELECT' => 'u.username, u.user_id', 'FROM' => [ @@ -32,8 +37,7 @@ class topic extends base_user ] ], 'WHERE' => 'p.topic_id = ' . $topic_id . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' - AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), 'ORDER_BY' => 'p.post_time DESC' ]); return $query; diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index 8a03bc41a8..d37ff416c0 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -20,13 +20,14 @@ class user extends base_user */ protected function query($keyword, $topic_id) { + // TODO: think about caching ALL users: 1m users results to ~40MB file $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username, u.user_id', + 'SELECT' => 'u.username_clean, u.username, u.user_id', 'FROM' => [ USERS_TABLE => 'u', ], - 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' - AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), + 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER])/* . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char())*/, 'ORDER_BY' => 'u.user_lastvisit DESC' ]); return $query; diff --git a/phpBB/phpbb/mention/source/usergroup.php b/phpBB/phpbb/mention/source/usergroup.php index c3b95ffb49..fd1184ff60 100644 --- a/phpBB/phpbb/mention/source/usergroup.php +++ b/phpBB/phpbb/mention/source/usergroup.php @@ -31,7 +31,7 @@ class usergroup extends base_group 'ON' => 'g.group_id = ug.group_id' ] ], - 'WHERE' => 'ug.user_id = ' . (int) $this->user->data['user_id'], + 'WHERE' => 'ug.user_pending = 0 AND ug.user_id = ' . (int) $this->user->data['user_id'], ]); return $query; } From 0269d53c5dc01bb6a6c59f984532a6511657859e Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 13 Jul 2018 07:24:35 +0300 Subject: [PATCH 043/113] [ticket/13713] Improve client-side caching PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 102 ++++++++++++++++++------------ 1 file changed, 62 insertions(+), 40 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 9972ffdd6a..b6382304bc 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -387,15 +387,35 @@ function getCaretPosition(txtarea) { (function($) { function Mentions() { let $mentionDataContainer = $('[data-mention-url]:first'); + let mentionURL = $mentionDataContainer.data('mentionUrl'); + let mentionBatchSize = $mentionDataContainer.data('mentionBatchSize'); + let mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'); + let mentionTopicId = $mentionDataContainer.data('topicId'); let queryInProgress = null; - let cachedNames = null; - let cachedFor = null; + let cachedNames = []; let cachedSearchKey = 'name'; function defaultAvatar(type) { return (type === 'group') ? '' : ''; } + function getCachedNames(query) { + if (!cachedNames) { + return null; + } + + let i; + + for (i = query.length; i > 0; i--) { + let startStr = query.substr(0, i); + if (cachedNames[startStr]) { + return cachedNames[startStr]; + } + } + + return cachedNames['']; + } + function getMatchedNames(query, items, searchKey) { let i; let len; @@ -413,15 +433,50 @@ function getCaretPosition(txtarea) { return getMatchedNames(query, cachedNames, cachedSearchKey).length; } + function remoteFilter(query, callback) { + /* + * Do not make a new request until the previous one for the same query is returned + * This fixes duplicate server queries e.g. when arrow keys are pressed + */ + if (queryInProgress === query) { + setTimeout(function() { + remoteFilter(query, callback); + }, 1000); + return; + } + + let cachedNamesForQuery = getCachedNames(query); + + /* + * Use cached values when we can: + * 1) There are some names in the cache relevant for the query + * (cache for the query with the same first characters cointains some data) + * 2) We have enough names to display OR + * all relevant names have been fetched from the server + */ + if (cachedNamesForQuery && + (getNumberOfMatchedCachedNames(query) >= mentionNamesLimit || + cachedNamesForQuery.length < mentionBatchSize)) { + callback(cachedNamesForQuery); + return; + } + + queryInProgress = query; + + let params = {keyword: query, topic_id: mentionTopicId, _referer: location.href}; + $.getJSON(mentionURL, params, function(data) { + cachedNames[query] = data; + callback(data); + }).always(function() { + queryInProgress = null; + }); + } + this.isEnabled = function() { return $mentionDataContainer.length; }; this.handle = function(txtarea) { - let mentionURL = $mentionDataContainer.data('mentionUrl'); - let mentionBatchSize = $mentionDataContainer.data('mentionBatchSize'); - let mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'); - let mentionTopicId = $mentionDataContainer.data('topicId'); $(txtarea).atwho({ at: "@", acceptSpaceBar: true, @@ -434,40 +489,7 @@ function getCaretPosition(txtarea) { insertTpl: "[mention=${type}:${id}]${name}[/mention]", limit: mentionNamesLimit, callbacks: { - remoteFilter: function(query, callback) { - /* - * Use cached values when we can: - * 1) There are some names in the cache - * 2) The cache contains relevant data for the query - * (it was made for the query with the same first characters) - * 3) We have enough names to display OR - * all relevant names have been fetched from the server - */ - if (cachedNames && - query.indexOf(cachedFor) === 0 && - (getNumberOfMatchedCachedNames(query) >= mentionNamesLimit || - cachedNames.length < mentionBatchSize)) { - callback(cachedNames); - return; - } - - /* - * Do not make a new request until the previous one for the same query is returned - * This fixes duplicate server queries e.g. when arrow keys are pressed - */ - if (queryInProgress === query) { - return; - } - queryInProgress = query; - - let params = {keyword: query, topic_id: mentionTopicId, _referer: location.href}; - $.getJSON(mentionURL, params, function (data) { - cachedNames = data; - cachedFor = query; - queryInProgress = null; - callback(data); - }); - }, + remoteFilter: remoteFilter, sorter: function(query, items, searchKey) { let i; let len; From e874ce9898db3f0774f47217611f3cc29f1961f8 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sun, 22 Jul 2018 01:37:00 +0300 Subject: [PATCH 044/113] [ticket/13713] Load all data for users in a single SQL query PHPBB3-13713 --- phpBB/phpbb/mention/source/base_user.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index 8ab1d05df1..c47bb313c7 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -81,6 +81,8 @@ abstract class base_user implements source_interface $result = $this->db->sql_query($this->query($keyword, $topic_id), 300); $i = 0; + $users = []; + $user_ids = []; while ($i < self::NAMES_BATCH_SIZE) { $row = $this->db->sql_fetchrow($result); @@ -96,18 +98,26 @@ abstract class base_user implements source_interface } $i++; + $users[] = $row; + $user_ids[] = $row['user_id']; + } - $user_rank = $this->user_loader->get_rank($row['user_id'], true); + // Load all user data with a single SQL query, needed for ranks and avatars + $this->user_loader->load_users($user_ids); + + foreach ($users as $user) + { + $user_rank = $this->user_loader->get_rank($user['user_id'], true); array_push($names, [ - 'name' => $row['username'], + 'name' => $user['username'], 'type' => 'u', - 'id' => $row['user_id'], + 'id' => $user['user_id'], 'avatar' => [ 'type' => 'user', - 'img' => $this->user_loader->get_avatar($row['user_id'], true), + 'img' => $this->user_loader->get_avatar($user['user_id'], true), ], 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', - 'priority' => $this->get_priority($row), + 'priority' => $this->get_priority($user), ]); } From aee1dfd837562207e8f31d21cbad9c83ff57e86e Mon Sep 17 00:00:00 2001 From: lavigor Date: Sun, 22 Jul 2018 02:06:18 +0300 Subject: [PATCH 045/113] [ticket/13713] Introduce priorities for groups PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 36 +++++++++---------- phpBB/phpbb/mention/source/base_group.php | 10 ++++++ phpBB/phpbb/mention/source/base_user.php | 6 ++-- .../phpbb/mention/source/source_interface.php | 8 +++++ 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index b6382304bc..f1931f0bdf 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -493,7 +493,7 @@ function getCaretPosition(txtarea) { sorter: function(query, items, searchKey) { let i; let len; - let highestPriority = 0; + let highestPriorities = {u: 0, g: 0}; let _unsorted = {u: {}, g: {}}; let _exactMatch = []; let _results = []; @@ -517,8 +517,7 @@ function getCaretPosition(txtarea) { } // If the item hasn't been added yet - add it - // Group names do not have priorities and are also handled here - if (!_unsorted[item.type][item.id] || item.type === 'g') { + if (!_unsorted[item.type][item.id]) { _unsorted[item.type][item.id] = item; continue; } @@ -527,26 +526,23 @@ function getCaretPosition(txtarea) { _unsorted[item.type][item.id].priority += parseFloat(item.priority); // Calculate the highest priority - we'll give it to group names - highestPriority = Math.max(highestPriority, _unsorted[item.type][item.id].priority); + highestPriorities[item.type] = Math.max(highestPriorities[item.type], _unsorted[item.type][item.id].priority); } - // Push user names to the result array - if (_unsorted['u']) { - $.each(_unsorted['u'], function(name, value) { - _results.push(value); - }); - } + // All types of names should come at the same level of importance, + // otherwise they will be unlikely to be shown + // That's why we normalize priorities and push names to a single results array + $.each(['u', 'g'], function(key, type) { + if (_unsorted[type]) { + $.each(_unsorted[type], function(name, value) { + // Normalize priority + value.priority /= highestPriorities[type]; - // Push group names to the result array and give them the highest priority - // They will be sorted together with the usernames on top of the list - if (_unsorted['g']) { - $.each(_unsorted['g'], function(name, value) { - // Groups should come at the same level of importance - // as users, otherwise they will be unlikely to be shown - value.priority = highestPriority; - _results.push(value); - }); - } + // Add item to all results + _results.push(value); + }); + } + }); // Sort names by priorities - higher values come first _results = _results.sort(function(a, b) { diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index 607fb91cc9..a31071a964 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -107,6 +107,15 @@ abstract class base_group implements source_interface */ abstract protected function query($keyword, $topic_id); + /** + * {@inheritdoc} + */ + public function get_priority($row) + { + // By default every result from the source increases the priority by a fixed value + return 1; + } + /** * {@inheritdoc} */ @@ -141,6 +150,7 @@ abstract class base_group implements source_interface 'img' => phpbb_get_group_avatar($groups[$group_id]), ], 'rank' => $group_rank['title'], + 'priority' => $this->get_priority($groups[$group_id]), ]); } } diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index c47bb313c7..cb6ae1eaca 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -60,13 +60,11 @@ abstract class base_user implements source_interface abstract protected function query($keyword, $topic_id); /** - * Returns the priority of the currently selected name - * - * @param array $row Array of fetched user data - * @return int Priority (defaults to 1) + * {@inheritdoc} */ public function get_priority($row) { + // By default every result from the source increases the priority by a fixed value return 1; } diff --git a/phpBB/phpbb/mention/source/source_interface.php b/phpBB/phpbb/mention/source/source_interface.php index 7c7da7369f..731aedc763 100644 --- a/phpBB/phpbb/mention/source/source_interface.php +++ b/phpBB/phpbb/mention/source/source_interface.php @@ -24,4 +24,12 @@ interface source_interface * @param int $topic_id Current topic ID */ public function get(array &$names, $keyword, $topic_id); + + /** + * Returns the priority of the currently selected name + * + * @param array $row Array of fetched data for the name type (e.g. user row) + * @return int Priority (defaults to 1) + */ + public function get_priority($row); } From 6f8467a2fabcde4abbc7bda5dafdba2e1a5ecf56 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sun, 22 Jul 2018 02:58:11 +0300 Subject: [PATCH 046/113] [ticket/13713] Do not show user's own name in the dropdown list PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 2 +- phpBB/assets/javascript/editor.js | 17 ++++++++++++----- phpBB/includes/functions.php | 1 + phpBB/includes/functions_acp.php | 1 + .../prosilver/template/posting_buttons.html | 2 +- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index 770501cabb..ff453d16c7 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -13,7 +13,7 @@ -
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-mention-batch-size="{S_MENTION_BATCH_SIZE}" data-topic-id="{S_TOPIC_ID}"> +
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-mention-batch-size="{S_MENTION_BATCH_SIZE}" data-topic-id="{S_TOPIC_ID}" data-user-id="{S_USER_ID}"> diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index f1931f0bdf..9fc46d7efd 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -391,6 +391,7 @@ function getCaretPosition(txtarea) { let mentionBatchSize = $mentionDataContainer.data('mentionBatchSize'); let mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'); let mentionTopicId = $mentionDataContainer.data('topicId'); + let mentionUserId = $mentionDataContainer.data('userId'); let queryInProgress = null; let cachedNames = []; let cachedSearchKey = 'name'; @@ -505,14 +506,20 @@ function getCaretPosition(txtarea) { for (i = 0, len = items.length; i < len; i++) { let item = items[i]; - // Exact matches should not be prioritised - they always come first - if (item.name === query) { - _exactMatch.push(items[i]); + // Check for unsupported type - in general, this should never happen + if (!_unsorted[item.type]) { continue; } - // Check for unsupported type - in general, this should never happen - if (!_unsorted[item.type]) { + // Current user doesn't want to mention themselves with "@" in most cases - + // do not waste list space with their own name + if (item.type === 'u' && item.id === String(mentionUserId)) { + continue; + } + + // Exact matches should not be prioritised - they always come first + if (item.name === query) { + _exactMatch.push(items[i]); continue; } diff --git a/phpBB/includes/functions.php b/phpBB/includes/functions.php index e00c2fc25d..fb65af2dc8 100644 --- a/phpBB/includes/functions.php +++ b/phpBB/includes/functions.php @@ -3972,6 +3972,7 @@ function page_header($page_title = '', $display_online_list = false, $item_id = 'S_REGISTER_ENABLED' => ($config['require_activation'] != USER_ACTIVATION_DISABLE) ? true : false, 'S_FORUM_ID' => $forum_id, 'S_TOPIC_ID' => $topic_id, + 'S_USER_ID' => $user->data['user_id'], 'S_LOGIN_ACTION' => ((!defined('ADMIN_START')) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=login') : append_sid("{$phpbb_admin_path}index.$phpEx", false, true, $user->session_id)), 'S_LOGIN_REDIRECT' => $s_login_redirect, diff --git a/phpBB/includes/functions_acp.php b/phpBB/includes/functions_acp.php index e1d3159e02..c06ae1f748 100644 --- a/phpBB/includes/functions_acp.php +++ b/phpBB/includes/functions_acp.php @@ -116,6 +116,7 @@ function adm_page_header($page_title) 'ICON_SYNC' => '', 'ICON_SYNC_DISABLED' => '', + 'S_USER_ID' => $user->data['user_id'], 'S_USER_LANG' => $user->lang['USER_LANG'], 'S_CONTENT_DIRECTION' => $user->lang['DIRECTION'], 'S_CONTENT_ENCODING' => 'UTF-8', diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 449f677dd9..697035b850 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -39,7 +39,7 @@
    -
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-mention-batch-size="{S_MENTION_BATCH_SIZE}" data-topic-id="{S_TOPIC_ID}"> +
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-mention-batch-size="{S_MENTION_BATCH_SIZE}" data-topic-id="{S_TOPIC_ID}" data-user-id="{S_USER_ID}"> From e616ec025c287d70c98cb59919f9c7e9b8e7d26f Mon Sep 17 00:00:00 2001 From: lavigor Date: Sun, 22 Jul 2018 03:58:00 +0300 Subject: [PATCH 047/113] [ticket/13713] Fix priorities PHPBB3-13713 --- phpBB/phpbb/mention/controller/mention.php | 2 +- phpBB/phpbb/mention/source/group.php | 14 ++++++++++ .../phpbb/mention/source/source_interface.php | 2 ++ phpBB/phpbb/mention/source/team.php | 5 +++- phpBB/phpbb/mention/source/topic.php | 28 +++++++++++++++++-- phpBB/phpbb/mention/source/user.php | 18 ++++++++++-- 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index d2b43d0914..770f84b015 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -46,7 +46,7 @@ class mention { if (!$this->request->is_ajax()) { - new RedirectResponse(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); + return new RedirectResponse(append_sid($this->phpbb_root_path . 'index.' . $this->php_ext)); } $keyword = $this->request->variable('keyword', '', true); diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index d063d64324..cdd3596d75 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -15,6 +15,20 @@ namespace phpbb\mention\source; class group extends base_group { + /** + * {@inheritdoc} + */ + public function get_priority($row) + { + /* + * Presence in array with all names for this type should not increase the priority + * Otherwise names will not be properly sorted because we fetch them in batches + * and the name from 'special' source can be absent from the array with all names + * and therefore it will appear lower than needed + */ + return 0; + } + /** * {@inheritdoc} */ diff --git a/phpBB/phpbb/mention/source/source_interface.php b/phpBB/phpbb/mention/source/source_interface.php index 731aedc763..075bea295d 100644 --- a/phpBB/phpbb/mention/source/source_interface.php +++ b/phpBB/phpbb/mention/source/source_interface.php @@ -27,6 +27,8 @@ interface source_interface /** * Returns the priority of the currently selected name + * Please note that simple inner priorities for a certain source + * can be set with ORDER BY SQL clause * * @param array $row Array of fetched data for the name type (e.g. user row) * @return int Priority (defaults to 1) diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index 89c1f4071e..cf373e8b03 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -21,11 +21,14 @@ class team extends base_user protected function query($keyword, $topic_id) { /* + * Select unique names of team members: each name should be selected only once + * regardless of the number of groups the certain user is a member of + * * For optimization purposes all team members are returned regardless of the keyword * Names filtering is done on the frontend * Results will be cached in a single file */ - $query = $this->db->sql_build_query('SELECT', [ + $query = $this->db->sql_build_query('SELECT_DISTINCT', [ 'SELECT' => 'u.username_clean, u.username, u.user_id', 'FROM' => [ USERS_TABLE => 'u', diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index da1d6152d6..86b01d37fc 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -15,18 +15,38 @@ namespace phpbb\mention\source; class topic extends base_user { + /** + * {@inheritdoc} + */ + public function get_priority($row) + { + /* + * Topic's open poster is probably the most mentionable user in the topic + * so we give him a significant priority + */ + if ($row['user_id'] === $row['topic_poster']) + { + return 5; + } + + return 1; + } + /** * {@inheritdoc} */ protected function query($keyword, $topic_id) { /* + * Select poster's username together with topic author's ID + * that will be later used for priotirisation + * * For optimization purposes all users are returned regardless of the keyword * Names filtering is done on the frontend * Results will be cached on a per-topic basis */ $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username, u.user_id', + 'SELECT' => 'u.username, u.user_id, t.topic_poster', 'FROM' => [ USERS_TABLE => 'u', ], @@ -34,7 +54,11 @@ class topic extends base_user [ 'FROM' => [POSTS_TABLE => 'p'], 'ON' => 'u.user_id = p.poster_id' - ] + ], + [ + 'FROM' => [TOPICS_TABLE => 't'], + 'ON' => 't.topic_id = p.topic_id' + ], ], 'WHERE' => 'p.topic_id = ' . $topic_id . ' AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index d37ff416c0..dedafd7d24 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -15,19 +15,31 @@ namespace phpbb\mention\source; class user extends base_user { + /** + * {@inheritdoc} + */ + public function get_priority($row) + { + /* + * Presence in array with all names for this type should not increase the priority + * Otherwise names will not be properly sorted because we fetch them in batches + * and the name from 'special' source can be absent from the array with all names + * and therefore it will appear lower than needed + */ + return 0; + } + /** * {@inheritdoc} */ protected function query($keyword, $topic_id) { - // TODO: think about caching ALL users: 1m users results to ~40MB file $query = $this->db->sql_build_query('SELECT', [ 'SELECT' => 'u.username_clean, u.username, u.user_id', 'FROM' => [ USERS_TABLE => 'u', ], - 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER])/* . ' - AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char())*/, + 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), 'ORDER_BY' => 'u.user_lastvisit DESC' ]); return $query; From 99d57e27430aaa22315666a0c0b6ea3fb59afdf2 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sun, 29 Jul 2018 03:55:16 +0300 Subject: [PATCH 048/113] [ticket/13713] Fix caching and priorities PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 10 +++------- phpBB/phpbb/mention/source/base_user.php | 2 ++ phpBB/phpbb/mention/source/topic.php | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 9fc46d7efd..9bd49cc8ed 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -430,10 +430,6 @@ function getCaretPosition(txtarea) { return _results; } - function getNumberOfMatchedCachedNames(query) { - return getMatchedNames(query, cachedNames, cachedSearchKey).length; - } - function remoteFilter(query, callback) { /* * Do not make a new request until the previous one for the same query is returned @@ -456,7 +452,7 @@ function getCaretPosition(txtarea) { * all relevant names have been fetched from the server */ if (cachedNamesForQuery && - (getNumberOfMatchedCachedNames(query) >= mentionNamesLimit || + (getMatchedNames(query, cachedNamesForQuery, cachedSearchKey).length >= mentionNamesLimit || cachedNamesForQuery.length < mentionBatchSize)) { callback(cachedNamesForQuery); return; @@ -494,7 +490,7 @@ function getCaretPosition(txtarea) { sorter: function(query, items, searchKey) { let i; let len; - let highestPriorities = {u: 0, g: 0}; + let highestPriorities = {u: 1, g: 1}; let _unsorted = {u: {}, g: {}}; let _exactMatch = []; let _results = []; @@ -530,7 +526,7 @@ function getCaretPosition(txtarea) { } // Priority is calculated as the sum of priorities from different sources - _unsorted[item.type][item.id].priority += parseFloat(item.priority); + _unsorted[item.type][item.id].priority += parseFloat(item.priority.toString()); // Calculate the highest priority - we'll give it to group names highestPriorities[item.type] = Math.max(highestPriorities[item.type], _unsorted[item.type][item.id].priority); diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index cb6ae1eaca..a9c0fdaf82 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -100,6 +100,8 @@ abstract class base_user implements source_interface $user_ids[] = $row['user_id']; } + $this->db->sql_freeresult($result); + // Load all user data with a single SQL query, needed for ranks and avatars $this->user_loader->load_users($user_ids); diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index 86b01d37fc..c1b78542c8 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -46,7 +46,7 @@ class topic extends base_user * Results will be cached on a per-topic basis */ $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username, u.user_id, t.topic_poster', + 'SELECT' => 'u.username_clean, u.username, u.user_id, t.topic_poster', 'FROM' => [ USERS_TABLE => 'u', ], From 0fd78c5c872c758e38d3563402c634a0c0aff3a7 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sun, 29 Jul 2018 04:13:37 +0300 Subject: [PATCH 049/113] [ticket/13713] Introduce tests for user notifications PHPBB3-13713 --- tests/notification/base.php | 1 + .../submit_post_notification.type.mention.xml | 135 ++++++++++++++++++ tests/notification/notification_test.php | 2 + tests/notification/submit_post_base.php | 3 + .../submit_post_type_mention_test.php | 121 ++++++++++++++++ 5 files changed, 262 insertions(+) create mode 100644 tests/notification/fixtures/submit_post_notification.type.mention.xml create mode 100644 tests/notification/submit_post_type_mention_test.php diff --git a/tests/notification/base.php b/tests/notification/base.php index d1861245bb..a22596f926 100644 --- a/tests/notification/base.php +++ b/tests/notification/base.php @@ -74,6 +74,7 @@ abstract class phpbb_tests_notification_base extends phpbb_database_test_case 'allow_topic_notify' => true, 'allow_forum_notify' => true, 'allow_board_notifications' => true, + 'allow_mentions' => true, )); $lang_loader = new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx); $lang = new \phpbb\language\language($lang_loader); diff --git a/tests/notification/fixtures/submit_post_notification.type.mention.xml b/tests/notification/fixtures/submit_post_notification.type.mention.xml new file mode 100644 index 0000000000..0b47241b2a --- /dev/null +++ b/tests/notification/fixtures/submit_post_notification.type.mention.xml @@ -0,0 +1,135 @@ + + + + notification_id + notification_type_id + user_id + item_id + item_parent_id + notification_read + notification_data + + 1 + 1 + 5 + 1 + 1 + 0 + + +
    + + notification_type_id + notification_type_name + notification_type_enabled + + 1 + notification.type.mention + 1 + +
    + + post_id + topic_id + forum_id + post_text + + 1 + 1 + 1 + + +
    + + topic_id + forum_id + + 1 + 1 + +
    + + user_id + username_clean + user_permissions + user_sig + + 2 + poster + + + + + 3 + test + + + + + 4 + unauthorized + + + + + 5 + notified + + + + + 6 + disabled + + + + + 7 + default + + + +
    + + item_type + item_id + user_id + method + notify + + notification.type.mention + 0 + 2 + notification.method.board + 1 + + + notification.type.mention + 0 + 3 + notification.method.board + 1 + + + notification.type.mention + 0 + 4 + notification.method.board + 1 + + + notification.type.mention + 0 + 5 + notification.method.board + 1 + + + notification.type.mention + 0 + 6 + notification.method.board + 0 + +
    +
    diff --git a/tests/notification/notification_test.php b/tests/notification/notification_test.php index 08eabaa12a..4658f4c39a 100644 --- a/tests/notification/notification_test.php +++ b/tests/notification/notification_test.php @@ -59,6 +59,7 @@ class phpbb_notification_test extends phpbb_tests_notification_base self::assertArrayHasKey('NOTIFICATION_GROUP_POSTING', $subscription_types); self::assertArrayHasKey('notification.type.bookmark', $subscription_types['NOTIFICATION_GROUP_POSTING']); + self::assertArrayHasKey('notification.type.mention', $subscription_types['NOTIFICATION_GROUP_POSTING']); self::assertArrayHasKey('notification.type.post', $subscription_types['NOTIFICATION_GROUP_POSTING']); self::assertArrayHasKey('notification.type.quote', $subscription_types['NOTIFICATION_GROUP_POSTING']); self::assertArrayHasKey('notification.type.topic', $subscription_types['NOTIFICATION_GROUP_POSTING']); @@ -73,6 +74,7 @@ class phpbb_notification_test extends phpbb_tests_notification_base { $expected_subscriptions = array( 'notification.type.forum' => array('notification.method.board'), + 'notification.type.mention' => array('notification.method.board'), 'notification.type.post' => array('notification.method.board'), 'notification.type.topic' => array('notification.method.board'), 'notification.type.quote' => array('notification.method.board'), diff --git a/tests/notification/submit_post_base.php b/tests/notification/submit_post_base.php index 1e348f4529..98733b0fcd 100644 --- a/tests/notification/submit_post_base.php +++ b/tests/notification/submit_post_base.php @@ -70,6 +70,8 @@ abstract class phpbb_notification_submit_post_base extends phpbb_database_test_c array('f_noapprove', 1, true), array('f_postcount', 1, true), array('m_edit', 1, false), + array('f_mention', 1, true), + array('u_mention', 0, true), ))); // Config @@ -77,6 +79,7 @@ abstract class phpbb_notification_submit_post_base extends phpbb_database_test_c 'num_topics' => 1, 'num_posts' => 1, 'allow_board_notifications' => true, + 'allow_mentions' => true, )); $cache_driver = new \phpbb\cache\driver\dummy(); diff --git a/tests/notification/submit_post_type_mention_test.php b/tests/notification/submit_post_type_mention_test.php new file mode 100644 index 0000000000..14cd18e3bd --- /dev/null +++ b/tests/notification/submit_post_type_mention_test.php @@ -0,0 +1,121 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +require_once dirname(__FILE__) . '/submit_post_base.php'; + +class phpbb_notification_submit_post_type_mention_test extends phpbb_notification_submit_post_base +{ + protected $item_type = 'notification.type.mention'; + + public function setUp() + { + parent::setUp(); + + global $auth; + + // Add additional permissions + $auth->expects($this->any()) + ->method('acl_get_list') + ->with($this->anything(), + $this->stringContains('_'), + $this->greaterThan(0)) + ->will($this->returnValueMap(array( + array( + array(3, 4, 5, 6, 7, 8), + 'f_read', + 1, + array( + 1 => array( + 'f_read' => array(3, 5, 6, 7), + ), + ), + ), + ))); + } + + /** + * submit_post() Notifications test + * + * submit_post() $mode = 'reply' + * Notification item_type = 'mention' + */ + public function submit_post_data() + { + // The new mock container is needed because the data providers may be executed before phpunit call setUp() + $parser = $this->get_test_case_helpers()->set_s9e_services(new phpbb_mock_container_builder())->get('text_formatter.parser'); + + return array( + /** + * Normal post + * + * User => State description + * 2 => Poster, should NOT receive a notification + * 3 => mentioned, should receive a notification + * 4 => mentioned, but unauthed to read, should NOT receive a notification + * 5 => mentioned, but already notified, should STILL receive a new notification + * 6 => mentioned, but option disabled, should NOT receive a notification + * 7 => mentioned, option set to default, should receive a notification + */ + array( + array( + 'message' => $parser->parse(implode(' ', array( + '[mention=u:2]poster[/mention] poster should not be notified', + '[mention=u:3]test[/mention] test should be notified', + '[mention=u:4]unauthorized[/mention] unauthorized to read, should not receive a notification', + '[mention=u:5]notified[/mention] already notified, should not receive a new notification', + '[mention=u:6]disabled[/mention] option disabled, should not receive a notification', + '[mention=u:7]default[/mention] option set to default, should receive a notification', + '[mention=u:8]doesn\'t exist[/mention] user does not exist, should not receive a notification', + ))), + 'bbcode_uid' => 'uid', + ), + array( + array('user_id' => 5, 'item_id' => 1, 'item_parent_id' => 1), + ), + array( + array('user_id' => 3, 'item_id' => 2, 'item_parent_id' => 1), + array('user_id' => 5, 'item_id' => 1, 'item_parent_id' => 1), + array('user_id' => 5, 'item_id' => 2, 'item_parent_id' => 1), + array('user_id' => 7, 'item_id' => 2, 'item_parent_id' => 1), + ), + ), + + /** + * Unapproved post + * + * No new notifications + */ + array( + array( + 'message' => $parser->parse(implode(' ', array( + '[mention=u:2]poster[/mention] poster should not be notified', + '[mention=u:3]test[/mention] test should be notified', + '[mention=u:4]unauthorized[/mention] unauthorized to read, should not receive a notification', + '[mention=u:5]notified[/mention] already notified, should not receive a new notification', + '[mention=u:6]disabled[/mention] option disabled, should not receive a notification', + '[mention=u:7]default[/mention] option set to default, should receive a notification', + '[mention=u:8]doesn\'t exist[/mention] user does not exist, should not receive a notification', + ))), + 'bbcode_uid' => 'uid', + 'force_approved_state' => false, + ), + array( + array('user_id' => 5, 'item_id' => 1, 'item_parent_id' => 1), + ), + array( + array('user_id' => 5, 'item_id' => 1, 'item_parent_id' => 1), + ), + ), + ); + } +} From 27b37f38819ea3f4a3cab86b06213b3978ad1460 Mon Sep 17 00:00:00 2001 From: lavigor Date: Mon, 30 Jul 2018 02:58:40 +0300 Subject: [PATCH 050/113] [ticket/13713] Introduce mention notifications for groups PHPBB3-13713 --- .../container/services_text_formatter.yml | 2 + .../textformatter/s9e/mention_helper.php | 191 ++++++++++++++---- tests/notification/base.php | 2 + .../submit_post_notification.type.mention.xml | 52 +++++ tests/notification/submit_post_base.php | 2 + .../submit_post_type_mention_test.php | 14 +- 6 files changed, 221 insertions(+), 42 deletions(-) diff --git a/phpBB/config/default/container/services_text_formatter.yml b/phpBB/config/default/container/services_text_formatter.yml index 119e0b2ba4..3d44e87948 100644 --- a/phpBB/config/default/container/services_text_formatter.yml +++ b/phpBB/config/default/container/services_text_formatter.yml @@ -56,6 +56,8 @@ services: class: phpbb\textformatter\s9e\mention_helper arguments: - '@dbal.conn' + - '@auth' + - '@user' - '%core.root_path%' - '%core.php_ext%' diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index 9e0cd65437..d4b97852f3 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -1,15 +1,15 @@ -* @license GNU General Public License, version 2 (GPL-2.0) -* -* For full copyright and license information, please see -* the docs/CREDITS.txt file. -* -*/ + * + * This file is part of the phpBB Forum Software package. + * + * @copyright (c) phpBB Limited + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ namespace phpbb\textformatter\s9e; @@ -18,35 +18,54 @@ use s9e\TextFormatter\Utils as TextFormatterUtils; class mention_helper { /** - * @var \phpbb\db\driver\driver_interface - */ + * @var \phpbb\db\driver\driver_interface + */ protected $db; /** - * @var string Base URL for a user profile link, uses {ID} as placeholder - */ + * @var \phpbb\auth\auth + */ + protected $auth; + + /** + * @var \phpbb\user + */ + protected $user; + + /** + * @var string Base URL for a user profile link, uses {ID} as placeholder + */ protected $user_profile_url; /** - * @var string Base URL for a group profile link, uses {ID} as placeholder - */ + * @var string Base URL for a group profile link, uses {ID} as placeholder + */ protected $group_profile_url; /** - * @var array Array of users' and groups' colours for each cached ID - */ + * @var array Array of users' and groups' colours for each cached ID + */ protected $cached_colours = []; /** - * Constructor - * - * @param \phpbb\db\driver\driver_interface $db - * @param string $root_path - * @param string $php_ext - */ - public function __construct($db, $root_path, $php_ext) + * @var array Array of group IDs allowed to be mentioned by current user + */ + protected $mentionable_groups = null; + + /** + * Constructor + * + * @param \phpbb\db\driver\driver_interface $db + * @param \phpbb\auth\auth $auth + * @param \phpbb\user $user + * @param string $root_path + * @param string $php_ext + */ + public function __construct($db, $auth, $user, $root_path, $php_ext) { $this->db = $db; + $this->auth = $auth; + $this->user = $user; $this->user_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={ID}', false); $this->group_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=group&g={ID}', false); } @@ -109,11 +128,11 @@ class mention_helper } /** - * Inject dynamic metadata into MENTION tags in given XML - * - * @param string $xml Original XML - * @return string Modified XML - */ + * Inject dynamic metadata into MENTION tags in given XML + * + * @param string $xml Original XML + * @return string Modified XML + */ public function inject_metadata($xml) { $profile_urls = [ @@ -129,8 +148,7 @@ class mention_helper return TextFormatterUtils::replaceAttributes( $xml, 'MENTION', - function ($attributes) use ($profile_urls) - { + function ($attributes) use ($profile_urls) { if (isset($attributes['type']) && isset($attributes['id'])) { $type = $attributes['type']; @@ -149,15 +167,91 @@ class mention_helper ); } + /** + * Get group IDs allowed to be mentioned by current user + * + * @return array + */ + protected function get_mentionable_groups() + { + if (is_array($this->mentionable_groups)) + { + return $this->mentionable_groups; + } + + $hidden_restriction = (!$this->auth->acl_gets('a_group', 'a_groupadd', 'a_groupdel')) ? ' AND (g.group_type <> ' . GROUP_HIDDEN . ' OR (ug.user_pending = 0 AND ug.user_id = ' . (int) $this->user->data['user_id'] . '))' : ''; + + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'g.group_id', + 'FROM' => [ + GROUPS_TABLE => 'g', + ], + 'LEFT_JOIN' => [[ + 'FROM' => [ + USER_GROUP_TABLE => 'ug', + ], + 'ON' => 'g.group_id = ug.group_id', + ]], + 'WHERE' => '(g.group_type <> ' . GROUP_SPECIAL . ' OR ' . $this->db->sql_in_set('g.group_name', ['ADMINISTRATORS', 'GLOBAL_MODERATORS']) . ')' . $hidden_restriction, + ]); + $result = $this->db->sql_query($query); + + $this->mentionable_groups = []; + + while ($row = $this->db->sql_fetchrow($result)) + { + $this->mentionable_groups[] = $row['group_id']; + } + + $this->db->sql_freeresult($result); + + return $this->mentionable_groups; + } + + /** + * Selects IDs of user members of a certain group + * + * @param array $user_ids Array of already selected user IDs + * @param int $group_id ID of the group to search members in + */ + protected function get_user_ids_for_group(&$user_ids, $group_id) + { + if (!in_array($group_id, $this->get_mentionable_groups())) + { + return; + } + + $query = $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'ug.user_id, ug.group_id', + 'FROM' => [ + USER_GROUP_TABLE => 'ug', + GROUPS_TABLE => 'g', + ], + 'WHERE' => 'g.group_id = ug.group_id', + ]); + // Cache results for 5 minutes + $result = $this->db->sql_query($query, 300); + + while ($row = $this->db->sql_fetchrow($result)) + { + if ($row['group_id'] == $group_id) + { + $user_ids[] = (int) $row['user_id']; + } + } + + $this->db->sql_freeresult($result); + } + /** * Get a list of mentioned names - * TODO: decide what to do with groups * * @param string $xml Parsed text - * @param string $type Name type ('u' for users, 'g' for groups) + * @param string $type Name type ('u' for users, 'g' for groups, + * 'ug' for usernames mentioned separately or as group members) * @return int[] List of IDs */ - public function get_mentioned_ids($xml, $type = 'u') + public function get_mentioned_ids($xml, $type = 'ug') { $ids = array(); if (strpos($xml, 'loadXML($xml); $xpath = new \DOMXPath($dom); - /** @var \DOMElement $mention */ - foreach ($xpath->query('//MENTION') as $mention) + + if ($type === 'ug') { - if ($mention->getAttribute('type') === $type) + /** @var \DOMElement $mention */ + foreach ($xpath->query('//MENTION') as $mention) { - $ids[] = (int) $mention->getAttribute('id'); + if ($mention->getAttribute('type') === 'u') + { + $ids[] = (int) $mention->getAttribute('id'); + } + else if ($mention->getAttribute('type') === 'g') + { + $this->get_user_ids_for_group($ids, (int) $mention->getAttribute('id')); + } + } + } + else + { + /** @var \DOMElement $mention */ + foreach ($xpath->query('//MENTION') as $mention) + { + if ($mention->getAttribute('type') === $type) + { + $ids[] = (int) $mention->getAttribute('id'); + } } } diff --git a/tests/notification/base.php b/tests/notification/base.php index a22596f926..d1ceb9aabd 100644 --- a/tests/notification/base.php +++ b/tests/notification/base.php @@ -111,6 +111,8 @@ abstract class phpbb_tests_notification_base extends phpbb_database_test_case 'text_formatter.s9e.mention_helper', new \phpbb\textformatter\s9e\mention_helper( $this->db, + $auth, + $this->user, $phpbb_root_path, $phpEx ) diff --git a/tests/notification/fixtures/submit_post_notification.type.mention.xml b/tests/notification/fixtures/submit_post_notification.type.mention.xml index 0b47241b2a..86ae1fd037 100644 --- a/tests/notification/fixtures/submit_post_notification.type.mention.xml +++ b/tests/notification/fixtures/submit_post_notification.type.mention.xml @@ -1,5 +1,23 @@ + + group_id + group_name + group_type + group_desc + + 1 + Normal group + 0 + + + + 2 + Hidden group + 2 + + +
    notification_idnotification_type_id @@ -89,6 +107,33 @@ + + 8 + member of normal group + + + + + 9 + member of hidden group + + + +
    + + user_id + group_id + user_pending + + 8 + 1 + 0 + + + 9 + 2 + 0 +
    item_type @@ -131,5 +176,12 @@ notification.method.board0 + + notification.type.mention + 0 + 8 + notification.method.board + 1 +
    diff --git a/tests/notification/submit_post_base.php b/tests/notification/submit_post_base.php index 98733b0fcd..82ea9609c3 100644 --- a/tests/notification/submit_post_base.php +++ b/tests/notification/submit_post_base.php @@ -139,6 +139,8 @@ abstract class phpbb_notification_submit_post_base extends phpbb_database_test_c 'text_formatter.s9e.mention_helper', new \phpbb\textformatter\s9e\mention_helper( $this->db, + $auth, + $this->user, $phpbb_root_path, $phpEx ) diff --git a/tests/notification/submit_post_type_mention_test.php b/tests/notification/submit_post_type_mention_test.php index 14cd18e3bd..642ef83ec3 100644 --- a/tests/notification/submit_post_type_mention_test.php +++ b/tests/notification/submit_post_type_mention_test.php @@ -31,16 +31,20 @@ class phpbb_notification_submit_post_type_mention_test extends phpbb_notificatio $this->greaterThan(0)) ->will($this->returnValueMap(array( array( - array(3, 4, 5, 6, 7, 8), + array(3, 4, 5, 6, 7, 8, 10), 'f_read', 1, array( 1 => array( - 'f_read' => array(3, 5, 6, 7), + 'f_read' => array(3, 5, 6, 7, 8), ), ), ), ))); + $auth->expects($this->any()) + ->method('acl_gets') + ->with('a_group', 'a_groupadd', 'a_groupdel') + ->will($this->returnValue(false)); } /** @@ -65,6 +69,7 @@ class phpbb_notification_submit_post_type_mention_test extends phpbb_notificatio * 5 => mentioned, but already notified, should STILL receive a new notification * 6 => mentioned, but option disabled, should NOT receive a notification * 7 => mentioned, option set to default, should receive a notification + * 8 => mentioned as a member of group 1, should receive a notification */ array( array( @@ -75,7 +80,9 @@ class phpbb_notification_submit_post_type_mention_test extends phpbb_notificatio '[mention=u:5]notified[/mention] already notified, should not receive a new notification', '[mention=u:6]disabled[/mention] option disabled, should not receive a notification', '[mention=u:7]default[/mention] option set to default, should receive a notification', - '[mention=u:8]doesn\'t exist[/mention] user does not exist, should not receive a notification', + '[mention=g:1]normal group[/mention] group members of a normal group shoud receive a notification', + '[mention=g:2]hidden group[/mention] group members of a hidden group shoud not receive a notification from a non-member', + '[mention=u:10]doesn\'t exist[/mention] user does not exist, should not receive a notification', ))), 'bbcode_uid' => 'uid', ), @@ -87,6 +94,7 @@ class phpbb_notification_submit_post_type_mention_test extends phpbb_notificatio array('user_id' => 5, 'item_id' => 1, 'item_parent_id' => 1), array('user_id' => 5, 'item_id' => 2, 'item_parent_id' => 1), array('user_id' => 7, 'item_id' => 2, 'item_parent_id' => 1), + array('user_id' => 8, 'item_id' => 2, 'item_parent_id' => 1), ), ), From 07130bf077bbae87f195ace1b9dae740e26f4d12 Mon Sep 17 00:00:00 2001 From: lavigor Date: Mon, 30 Jul 2018 03:03:45 +0300 Subject: [PATCH 051/113] [ticket/13713] Fix closure formatting PHPBB3-13713 --- phpBB/phpbb/textformatter/s9e/mention_helper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index d4b97852f3..1922ede0e5 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -148,7 +148,8 @@ class mention_helper return TextFormatterUtils::replaceAttributes( $xml, 'MENTION', - function ($attributes) use ($profile_urls) { + function ($attributes) use ($profile_urls) + { if (isset($attributes['type']) && isset($attributes['id'])) { $type = $attributes['type']; From c6789ad2948b287f33070ca0725a9fe655cfa02a Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 2 Aug 2018 22:09:09 +0300 Subject: [PATCH 052/113] [ticket/13713] Fix base classes for sources PHPBB3-13713 --- phpBB/phpbb/mention/source/base_group.php | 2 +- phpBB/phpbb/mention/source/base_user.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index a31071a964..e6281bcbac 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -149,7 +149,7 @@ abstract class base_group implements source_interface 'type' => 'group', 'img' => phpbb_get_group_avatar($groups[$group_id]), ], - 'rank' => $group_rank['title'], + 'rank' => (isset($group_rank['title'])) ? $group_rank['title'] : '', 'priority' => $this->get_priority($groups[$group_id]), ]); } diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index a9c0fdaf82..2fd6cf5f98 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -120,7 +120,5 @@ abstract class base_user implements source_interface 'priority' => $this->get_priority($user), ]); } - - $this->db->sql_freeresult($result); } } From e3cee760773e5fb5a7af7e2c9a682cd6f0e61e75 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 2 Aug 2018 22:10:30 +0300 Subject: [PATCH 053/113] [ticket/13713] Do not use phpbb prefix in configuration PHPBB3-13713 --- .../default/container/services_mention.yml | 34 +++++++++---------- phpBB/config/default/routing/routing.yml | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/phpBB/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml index d94aaca088..917da928d5 100644 --- a/phpBB/config/default/container/services_mention.yml +++ b/phpBB/config/default/container/services_mention.yml @@ -1,22 +1,22 @@ services: # ----- Controller ----- - phpbb.mention.controller: + mention.controller: class: phpbb\mention\controller\mention arguments: - - '@phpbb.mention.source_collection' + - '@mention.source_collection' - '@request' - '%core.root_path%' - '%core.php_ext%' # ----- Sources for mention ----- - phpbb.mention.source_collection: + mention.source_collection: class: phpbb\di\service_collection arguments: - '@service_container' tags: - { name: service_collection, tag: mention.source } - phpbb.mention.source.base_group: + mention.source.base_group: abstract: true arguments: - '@dbal.conn' @@ -26,7 +26,7 @@ services: - '%core.root_path%' - '%core.php_ext%' - phpbb.mention.source.base_user: + mention.source.base_user: abstract: true arguments: - '@dbal.conn' @@ -35,40 +35,40 @@ services: - '%core.root_path%' - '%core.php_ext%' - phpbb.mention.source.friend: + mention.source.friend: class: phpbb\mention\source\friend - parent: phpbb.mention.source.base_user + parent: mention.source.base_user calls: - [set_user, ['@user']] tags: - { name: mention.source } - phpbb.mention.source.group: + mention.source.group: class: phpbb\mention\source\group - parent: phpbb.mention.source.base_group + parent: mention.source.base_group tags: - { name: mention.source } - phpbb.mention.source.team: + mention.source.team: class: phpbb\mention\source\team - parent: phpbb.mention.source.base_user + parent: mention.source.base_user tags: - { name: mention.source } - phpbb.mention.source.topic: + mention.source.topic: class: phpbb\mention\source\topic - parent: phpbb.mention.source.base_user + parent: mention.source.base_user tags: - { name: mention.source } - phpbb.mention.source.user: + mention.source.user: class: phpbb\mention\source\user - parent: phpbb.mention.source.base_user + parent: mention.source.base_user tags: - { name: mention.source } - phpbb.mention.source.usergroup: + mention.source.usergroup: class: phpbb\mention\source\usergroup - parent: phpbb.mention.source.base_group + parent: mention.source.base_group tags: - { name: mention.source } diff --git a/phpBB/config/default/routing/routing.yml b/phpBB/config/default/routing/routing.yml index 9ed725fc06..441e544cbf 100644 --- a/phpBB/config/default/routing/routing.yml +++ b/phpBB/config/default/routing/routing.yml @@ -27,7 +27,7 @@ phpbb_help_routing: phpbb_mention_controller: path: /mention methods: [GET, POST] - defaults: { _controller: phpbb.mention.controller:handle } + defaults: { _controller: mention.controller:handle } phpbb_report_routing: resource: report.yml From 224d753414480f888b96cf6b6fc7c3a7925a19a9 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 2 Aug 2018 22:11:48 +0300 Subject: [PATCH 054/113] [ticket/13713] Add mention_helper for testing in helpers PHPBB3-13713 --- tests/test_framework/phpbb_test_case_helpers.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_framework/phpbb_test_case_helpers.php b/tests/test_framework/phpbb_test_case_helpers.php index 0c22b3c5a4..6efb15a4fb 100644 --- a/tests/test_framework/phpbb_test_case_helpers.php +++ b/tests/test_framework/phpbb_test_case_helpers.php @@ -579,6 +579,9 @@ class phpbb_test_case_helpers } $user->add_lang('common'); + // Get an auth interface + $auth = ($container->has('auth')) ? $container->get('auth') : new \phpbb\auth\auth; + // Create and register a quote_helper $quote_helper = new \phpbb\textformatter\s9e\quote_helper( $container->get('user'), @@ -587,6 +590,16 @@ class phpbb_test_case_helpers ); $container->set('text_formatter.s9e.quote_helper', $quote_helper); + // Create and register a mention_helper + $mention_helper = new \phpbb\textformatter\s9e\mention_helper( + $db_driver, + $auth, + $container->get('user'), + $phpbb_root_path, + $phpEx + ); + $container->set('text_formatter.s9e.mention_helper', $mention_helper); + // Create and register the text_formatter.s9e.parser service and its alias $parser = new \phpbb\textformatter\s9e\parser( $cache, @@ -607,8 +620,8 @@ class phpbb_test_case_helpers ); // Calls configured in services.yml - $auth = ($container->has('auth')) ? $container->get('auth') : new \phpbb\auth\auth; $renderer->configure_quote_helper($quote_helper); + $renderer->configure_mention_helper($mention_helper); $renderer->configure_smilies_path($config, $path_helper); $renderer->configure_user($user, $config, $auth); From 29a11cf930344aec513a59437e03e725aa851886 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 2 Aug 2018 22:20:04 +0300 Subject: [PATCH 055/113] [ticket/13713] Add test for all mention sources and controller PHPBB3-13713 --- tests/mention/controller_test.php | 659 ++++++++++++++++++++ tests/mention/fixtures/mention.xml | 198 ++++++ tests/mention/fixtures/services_mention.yml | 70 +++ 3 files changed, 927 insertions(+) create mode 100644 tests/mention/controller_test.php create mode 100644 tests/mention/fixtures/mention.xml create mode 100644 tests/mention/fixtures/services_mention.yml diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php new file mode 100644 index 0000000000..57b498c39f --- /dev/null +++ b/tests/mention/controller_test.php @@ -0,0 +1,659 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; + +require_once dirname(__FILE__) . '/../../phpBB/includes/functions_posting.php'; + +class phpbb_mention_controller_test extends phpbb_database_test_case +{ + protected $db, $container, $user, $config, $auth, $cache; + + /** + * @var \phpbb\mention\controller\mention + */ + protected $controller; + + /** + * @var PHPUnit_Framework_MockObject_MockObject + */ + protected $request; + + public function getDataSet() + { + return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/mention.xml'); + } + + public function setUp() + { + parent::setUp(); + + global $auth, $cache, $config, $db, $phpbb_container, $phpbb_dispatcher, $lang, $user, $request, $phpEx, $phpbb_root_path, $user_loader; + + // Database + $this->db = $this->new_dbal(); + $db = $this->db; + + // Auth + $auth = $this->createMock('\phpbb\auth\auth'); + $auth->expects($this->any()) + ->method('acl_gets') + ->with('a_group', 'a_groupadd', 'a_groupdel') + ->willReturn(false); + + // Config + $config = new \phpbb\config\config(array( + 'allow_mentions' => true, + 'mention_names_limit' => 3, + )); + + $cache_driver = new \phpbb\cache\driver\dummy(); + $cache = new \phpbb\cache\service( + $cache_driver, + $config, + $db, + $phpbb_root_path, + $phpEx + ); + + // Event dispatcher + $phpbb_dispatcher = new phpbb_mock_event_dispatcher(); + + // Language + $lang = new \phpbb\language\language(new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx)); + + // User + $user = $this->createMock('\phpbb\user', array(), array( + $lang, + '\phpbb\datetime' + )); + $user->ip = ''; + $user->data = array( + 'user_id' => 2, + 'username' => 'myself', + 'is_registered' => true, + 'user_colour' => '', + ); + + // Request + $this->request = $request = $this->createMock('\phpbb\request\request'); + + $request->expects($this->any()) + ->method('is_ajax') + ->willReturn(true); + + $user_loader = new \phpbb\user_loader($db, $phpbb_root_path, $phpEx, USERS_TABLE); + + // Container + $phpbb_container = new ContainerBuilder(); + + $loader = new YamlFileLoader($phpbb_container, new FileLocator(__DIR__ . '/fixtures')); + $loader->load('services_mention.yml'); + $phpbb_container->set('user_loader', $user_loader); + $phpbb_container->set('user', $user); + $phpbb_container->set('language', $lang); + $phpbb_container->set('config', $config); + $phpbb_container->set('dbal.conn', $db); + $phpbb_container->set('auth', $auth); + $phpbb_container->set('cache.driver', $cache_driver); + $phpbb_container->set('cache', $cache); + $phpbb_container->set('group_helper', new \phpbb\group\helper($lang)); + $phpbb_container->set('text_formatter.utils', new \phpbb\textformatter\s9e\utils()); + $phpbb_container->set( + 'text_formatter.s9e.mention_helper', + new \phpbb\textformatter\s9e\mention_helper( + $this->db, + $auth, + $user, + $phpbb_root_path, + $phpEx + ) + ); + $phpbb_container->setParameter('core.root_path', $phpbb_root_path); + $phpbb_container->setParameter('core.php_ext', $phpEx); + $phpbb_container->compile(); + + // Mention Sources + $mention_sources = array('friend', 'group', 'team', 'topic', 'user', 'usergroup'); + $mention_sources_array = array(); + foreach ($mention_sources as $source) + { + $class = $phpbb_container->get('mention.source.' . $source); + $mention_sources_array['mention.source.' . $source] = $class; + } + + $this->controller = new \phpbb\mention\controller\mention($mention_sources_array, $request, $phpbb_root_path, $phpEx); + } + + public function handle_data() + { + /** + * NOTE: + * 1) in production comparison with 'myself' is being done in JS + * 2) mention_names_limit does not limit the number of returned items + */ + return [ + ['', 0, [ + [ + 'name' => 'friend', + 'type' => 'u', + 'id' => 7, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], + [ + 'name' => 'Group we are a member of', + 'type' => 'g', + 'id' => 3, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'Normal group', + 'type' => 'g', + 'id' => 1, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], + [ + 'name' => 'myself', + 'type' => 'u', + 'id' => 2, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'poster', + 'type' => 'u', + 'id' => 3, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'replier', + 'type' => 'u', + 'id' => 4, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'friend', + 'type' => 'u', + 'id' => 7, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test', + 'type' => 'u', + 'id' => 8, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test2', + 'type' => 'u', + 'id' => 10, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test3', + 'type' => 'u', + 'id' => 11, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'Group we are a member of', + 'type' => 'g', + 'id' => 3, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], + ]], + ['', 1, [ + [ + 'name' => 'friend', + 'type' => 'u', + 'id' => 7, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], + [ + 'name' => 'Group we are a member of', + 'type' => 'g', + 'id' => 3, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'Normal group', + 'type' => 'g', + 'id' => 1, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], + [ + 'name' => 'replier', + 'type' => 'u', + 'id' => 4, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], + [ + 'name' => 'poster', + 'type' => 'u', + 'id' => 3, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 5, + ], + [ + 'name' => 'myself', + 'type' => 'u', + 'id' => 2, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'poster', + 'type' => 'u', + 'id' => 3, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'replier', + 'type' => 'u', + 'id' => 4, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'friend', + 'type' => 'u', + 'id' => 7, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test', + 'type' => 'u', + 'id' => 8, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test2', + 'type' => 'u', + 'id' => 10, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test3', + 'type' => 'u', + 'id' => 11, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'Group we are a member of', + 'type' => 'g', + 'id' => 3, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], + ]], + ['t', 1, [ + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test', + 'type' => 'u', + 'id' => 8, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test2', + 'type' => 'u', + 'id' => 10, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test3', + 'type' => 'u', + 'id' => 11, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + ]], + ['test', 1, [ + [ + 'name' => 'test', + 'type' => 'u', + 'id' => 8, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test2', + 'type' => 'u', + 'id' => 10, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test3', + 'type' => 'u', + 'id' => 11, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + ]], + ['test1', 1, [[ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ]]], + ]; + } + + /** + * @dataProvider handle_data + */ + public function test_handle($keyword, $topic_id, $expected_result) + { + $this->request->expects($this->at(1)) + ->method('variable') + ->with('keyword', '', true) + ->willReturn($keyword); + $this->request->expects($this->at(2)) + ->method('variable') + ->with('topic_id', 0) + ->willReturn($topic_id); + $data = json_decode($this->controller->handle()->getContent(), true); + $this->assertEquals($expected_result, $data); + } +} diff --git a/tests/mention/fixtures/mention.xml b/tests/mention/fixtures/mention.xml new file mode 100644 index 0000000000..7e76f58440 --- /dev/null +++ b/tests/mention/fixtures/mention.xml @@ -0,0 +1,198 @@ + + + + group_id + group_name + group_type + group_desc + + 1 + Normal group + 0 + + + + 2 + Hidden group + 2 + + + + 3 + Group we are a member of + 0 + + +
    + + post_id + topic_id + forum_id + poster_id + post_time + post_text + + 1 + 1 + 1 + 3 + 1 + Topic's initial post. + + + 2 + 1 + 1 + 4 + 2 + A reply. + +
    + + group_id + + 1 + +
    + + topic_id + forum_id + topic_poster + + 1 + 1 + 3 + +
    + + user_id + username + username_clean + user_type + user_lastvisit + user_permissions + user_sig + + 2 + myself + myself + 0 + 0 + + + + + 3 + poster + poster + 0 + 0 + + + + + 4 + replier + replier + 0 + 0 + + + + + 5 + team_member_normal + team_member_normal + 0 + 0 + + + + + 6 + team_member_hidden + team_member_hidden + 0 + 0 + + + + + 7 + friend + friend + 0 + 0 + + + + + 8 + test + test + 0 + 0 + + + + + 9 + test1 + test1 + 0 + 0 + + + + + 10 + test2 + test2 + 0 + 0 + + + + + 11 + test3 + test3 + 0 + 0 + + + +
    + + user_id + group_id + user_pending + + 2 + 3 + 0 + + + 5 + 1 + 0 + + + 6 + 2 + 0 + +
    + + user_id + zebra_id + friend + foe + + 2 + 7 + 1 + 0 + +
    +
    diff --git a/tests/mention/fixtures/services_mention.yml b/tests/mention/fixtures/services_mention.yml new file mode 100644 index 0000000000..f96d18b7ba --- /dev/null +++ b/tests/mention/fixtures/services_mention.yml @@ -0,0 +1,70 @@ +imports: + - { resource: ../../../phpBB/config/default/container/services_mention.yml } + +services: + user_loader: + synthetic: true + + user: + synthetic: true + + config: + synthetic: true + + dbal.conn: + synthetic: true + + language: + synthetic: true + + auth: + synthetic: true + + cache.driver: + synthetic: true + + group_helper: + synthetic: true + + path_helper: + synthetic: true + + request: + synthetic: true + + text_formatter.s9e.factory: + synthetic: true + + text_formatter.s9e.quote_helper: + synthetic: true + + text_formatter.s9e.mention_helper: + synthetic: true + + text_formatter.parser: + synthetic: true + + text_formatter.s9e.parser: + synthetic: true + + text_formatter.renderer: + synthetic: true + + text_formatter.s9e.renderer: + synthetic: true + + text_formatter.utils: + synthetic: true + + text_formatter.s9e.utils: + synthetic: true + + text_formatter.data_access: + synthetic: true +# +# test: +# class: phpbb\notification\type\test +# shared: false +# parent: notification.type.base +# tags: +# - { name: notification.type } From a8cb12e455e9582af48deac7ffbcf25e5a954e0e Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 2 Aug 2018 22:30:32 +0300 Subject: [PATCH 056/113] [ticket/13713] Tests cleanup PHPBB3-13713 --- tests/mention/controller_test.php | 3 +- tests/mention/fixtures/mention.xml | 342 ++++++++++---------- tests/mention/fixtures/services_mention.yml | 68 ---- 3 files changed, 172 insertions(+), 241 deletions(-) diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php index 57b498c39f..2b1832df64 100644 --- a/tests/mention/controller_test.php +++ b/tests/mention/controller_test.php @@ -15,8 +15,6 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; -require_once dirname(__FILE__) . '/../../phpBB/includes/functions_posting.php'; - class phpbb_mention_controller_test extends phpbb_database_test_case { protected $db, $container, $user, $config, $auth, $cache; @@ -109,6 +107,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case $phpbb_container->set('auth', $auth); $phpbb_container->set('cache.driver', $cache_driver); $phpbb_container->set('cache', $cache); + $phpbb_container->set('request', $request); $phpbb_container->set('group_helper', new \phpbb\group\helper($lang)); $phpbb_container->set('text_formatter.utils', new \phpbb\textformatter\s9e\utils()); $phpbb_container->set( diff --git a/tests/mention/fixtures/mention.xml b/tests/mention/fixtures/mention.xml index 7e76f58440..66ba0477f5 100644 --- a/tests/mention/fixtures/mention.xml +++ b/tests/mention/fixtures/mention.xml @@ -24,175 +24,175 @@ - - post_id - topic_id - forum_id - poster_id - post_time - post_text - - 1 - 1 - 1 - 3 - 1 - Topic's initial post. - - - 2 - 1 - 1 - 4 - 2 - A reply. - -
    - - group_id - - 1 - -
    - - topic_id - forum_id - topic_poster - - 1 - 1 - 3 - -
    - - user_id - username - username_clean - user_type - user_lastvisit - user_permissions - user_sig - - 2 - myself - myself - 0 - 0 - - - - - 3 - poster - poster - 0 - 0 - - - - - 4 - replier - replier - 0 - 0 - - - - - 5 - team_member_normal - team_member_normal - 0 - 0 - - - - - 6 - team_member_hidden - team_member_hidden - 0 - 0 - - - - - 7 - friend - friend - 0 - 0 - - - - - 8 - test - test - 0 - 0 - - - - - 9 - test1 - test1 - 0 - 0 - - - - - 10 - test2 - test2 - 0 - 0 - - - - - 11 - test3 - test3 - 0 - 0 - - - -
    - - user_id - group_id - user_pending - - 2 - 3 - 0 - - - 5 - 1 - 0 - - - 6 - 2 - 0 - -
    - - user_id - zebra_id - friend - foe - - 2 - 7 - 1 - 0 - -
    + + post_id + topic_id + forum_id + poster_id + post_time + post_text + + 1 + 1 + 1 + 3 + 1 + Topic's initial post. + + + 2 + 1 + 1 + 4 + 2 + A reply. + +
    + + group_id + + 1 + +
    + + topic_id + forum_id + topic_poster + + 1 + 1 + 3 + +
    + + user_id + username + username_clean + user_type + user_lastvisit + user_permissions + user_sig + + 2 + myself + myself + 0 + 0 + + + + + 3 + poster + poster + 0 + 0 + + + + + 4 + replier + replier + 0 + 0 + + + + + 5 + team_member_normal + team_member_normal + 0 + 0 + + + + + 6 + team_member_hidden + team_member_hidden + 0 + 0 + + + + + 7 + friend + friend + 0 + 0 + + + + + 8 + test + test + 0 + 0 + + + + + 9 + test1 + test1 + 0 + 0 + + + + + 10 + test2 + test2 + 0 + 0 + + + + + 11 + test3 + test3 + 0 + 0 + + + +
    + + user_id + group_id + user_pending + + 2 + 3 + 0 + + + 5 + 1 + 0 + + + 6 + 2 + 0 + +
    + + user_id + zebra_id + friend + foe + + 2 + 7 + 1 + 0 + +
    diff --git a/tests/mention/fixtures/services_mention.yml b/tests/mention/fixtures/services_mention.yml index f96d18b7ba..3cf14918a3 100644 --- a/tests/mention/fixtures/services_mention.yml +++ b/tests/mention/fixtures/services_mention.yml @@ -1,70 +1,2 @@ imports: - { resource: ../../../phpBB/config/default/container/services_mention.yml } - -services: - user_loader: - synthetic: true - - user: - synthetic: true - - config: - synthetic: true - - dbal.conn: - synthetic: true - - language: - synthetic: true - - auth: - synthetic: true - - cache.driver: - synthetic: true - - group_helper: - synthetic: true - - path_helper: - synthetic: true - - request: - synthetic: true - - text_formatter.s9e.factory: - synthetic: true - - text_formatter.s9e.quote_helper: - synthetic: true - - text_formatter.s9e.mention_helper: - synthetic: true - - text_formatter.parser: - synthetic: true - - text_formatter.s9e.parser: - synthetic: true - - text_formatter.renderer: - synthetic: true - - text_formatter.s9e.renderer: - synthetic: true - - text_formatter.utils: - synthetic: true - - text_formatter.s9e.utils: - synthetic: true - - text_formatter.data_access: - synthetic: true -# -# test: -# class: phpbb\notification\type\test -# shared: false -# parent: notification.type.base -# tags: -# - { name: notification.type } From 5651c7f3ff519e114f3569ee50c866ead8c755b6 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 3 Aug 2018 06:43:27 +0300 Subject: [PATCH 057/113] [ticket/13713] Fix ORDER_BY in queries PHPBB3-13713 --- phpBB/phpbb/mention/source/group.php | 1 + phpBB/phpbb/mention/source/team.php | 2 +- phpBB/phpbb/mention/source/usergroup.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index cdd3596d75..e74948d402 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -39,6 +39,7 @@ class group extends base_group 'FROM' => [ GROUPS_TABLE => 'g', ], + 'ORDER_BY' => 'g.group_name', ]); return $query; } diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index cf373e8b03..ab59cc7733 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -37,7 +37,7 @@ class team extends base_user ], 'WHERE' => 'ug.group_id = t.group_id AND ug.user_id = u.user_id AND ug.user_pending = 0 AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), - 'ORDER_BY' => 'u.user_lastvisit DESC' + 'ORDER_BY' => 'u.username' ]); return $query; } diff --git a/phpBB/phpbb/mention/source/usergroup.php b/phpBB/phpbb/mention/source/usergroup.php index fd1184ff60..b3b3e71ded 100644 --- a/phpBB/phpbb/mention/source/usergroup.php +++ b/phpBB/phpbb/mention/source/usergroup.php @@ -32,6 +32,7 @@ class usergroup extends base_group ] ], 'WHERE' => 'ug.user_pending = 0 AND ug.user_id = ' . (int) $this->user->data['user_id'], + 'ORDER_BY' => 'g.group_name', ]); return $query; } From 118b98841c0c28a0f462828eaba5353103f5fbad Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 3 Aug 2018 07:11:18 +0300 Subject: [PATCH 058/113] [ticket/13713] Fix tests PHPBB3-13713 --- tests/mention/controller_test.php | 114 +++++++++++++++++++---------- tests/mention/fixtures/mention.xml | 6 ++ 2 files changed, 82 insertions(+), 38 deletions(-) diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php index 2b1832df64..ffc65e7c18 100644 --- a/tests/mention/controller_test.php +++ b/tests/mention/controller_test.php @@ -1,15 +1,15 @@ -* @license GNU General Public License, version 2 (GPL-2.0) -* -* For full copyright and license information, please see -* the docs/CREDITS.txt file. -* -*/ + * + * This file is part of the phpBB Forum Software package. + * + * @copyright (c) phpBB Limited + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -47,14 +47,15 @@ class phpbb_mention_controller_test extends phpbb_database_test_case // Auth $auth = $this->createMock('\phpbb\auth\auth'); $auth->expects($this->any()) - ->method('acl_gets') - ->with('a_group', 'a_groupadd', 'a_groupdel') - ->willReturn(false); + ->method('acl_gets') + ->with('a_group', 'a_groupadd', 'a_groupdel') + ->willReturn(false) + ; // Config $config = new \phpbb\config\config(array( - 'allow_mentions' => true, - 'mention_names_limit' => 3, + 'allow_mentions' => true, + 'mention_names_limit' => 3, )); $cache_driver = new \phpbb\cache\driver\dummy(); @@ -79,25 +80,26 @@ class phpbb_mention_controller_test extends phpbb_database_test_case )); $user->ip = ''; $user->data = array( - 'user_id' => 2, - 'username' => 'myself', - 'is_registered' => true, - 'user_colour' => '', + 'user_id' => 2, + 'username' => 'myself', + 'is_registered' => true, + 'user_colour' => '', ); // Request $this->request = $request = $this->createMock('\phpbb\request\request'); $request->expects($this->any()) - ->method('is_ajax') - ->willReturn(true); + ->method('is_ajax') + ->willReturn(true) + ; $user_loader = new \phpbb\user_loader($db, $phpbb_root_path, $phpEx, USERS_TABLE); // Container $phpbb_container = new ContainerBuilder(); - $loader = new YamlFileLoader($phpbb_container, new FileLocator(__DIR__ . '/fixtures')); + $loader = new YamlFileLoader($phpbb_container, new FileLocator(__DIR__ . '/fixtures')); $loader->load('services_mention.yml'); $phpbb_container->set('user_loader', $user_loader); $phpbb_container->set('user', $user); @@ -142,6 +144,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case * NOTE: * 1) in production comparison with 'myself' is being done in JS * 2) mention_names_limit does not limit the number of returned items + * 3) team members of hidden groups can also be mentioned (because they are shown on teampage) */ return [ ['', 0, [ @@ -178,6 +181,17 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'rank' => '', 'priority' => 0, ], + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], [ 'name' => 'team_member_normal', 'type' => 'u', @@ -345,6 +359,17 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'rank' => '', 'priority' => 0, ], + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], [ 'name' => 'team_member_normal', 'type' => 'u', @@ -501,6 +526,17 @@ class phpbb_mention_controller_test extends phpbb_database_test_case ], ]], ['t', 1, [ + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, + ], [ 'name' => 'team_member_normal', 'type' => 'u', @@ -626,32 +662,34 @@ class phpbb_mention_controller_test extends phpbb_database_test_case ], ]], ['test1', 1, [[ - 'name' => 'test1', - 'type' => 'u', - 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', ], - 'rank' => '', - 'priority' => 0, + 'rank' => '', + 'priority' => 0, ]]], ]; } /** - * @dataProvider handle_data - */ + * @dataProvider handle_data + */ public function test_handle($keyword, $topic_id, $expected_result) { $this->request->expects($this->at(1)) - ->method('variable') - ->with('keyword', '', true) - ->willReturn($keyword); + ->method('variable') + ->with('keyword', '', true) + ->willReturn($keyword) + ; $this->request->expects($this->at(2)) - ->method('variable') - ->with('topic_id', 0) - ->willReturn($topic_id); + ->method('variable') + ->with('topic_id', 0) + ->willReturn($topic_id) + ; $data = json_decode($this->controller->handle()->getContent(), true); $this->assertEquals($expected_result, $data); } diff --git a/tests/mention/fixtures/mention.xml b/tests/mention/fixtures/mention.xml index 66ba0477f5..106398e610 100644 --- a/tests/mention/fixtures/mention.xml +++ b/tests/mention/fixtures/mention.xml @@ -49,9 +49,15 @@ + teampage_idgroup_id 1 + 1 + + + 2 + 2
    From 59bf57eb7390a8fc98826b907787f787f34636d5 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 3 Aug 2018 17:34:09 +0300 Subject: [PATCH 059/113] [ticket/13713] Fix language typo PHPBB3-13713 --- phpBB/language/en/ucp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpBB/language/en/ucp.php b/phpBB/language/en/ucp.php index 8c4e59904c..f42992ca0b 100644 --- a/phpBB/language/en/ucp.php +++ b/phpBB/language/en/ucp.php @@ -332,7 +332,7 @@ $lang = array_merge($lang, array( 'NOTIFICATION_TYPE_GROUP_REQUEST' => 'Someone requests to join a group you lead', 'NOTIFICATION_TYPE_FORUM' => 'Someone replies to a topic in a forum to which you are subscribed', 'NOTIFICATION_TYPE_IN_MODERATION_QUEUE' => 'A post or topic needs approval', - 'NOTIFICATION_TYPE_MENTION' => 'Someone mentiones you in a post', + 'NOTIFICATION_TYPE_MENTION' => 'Someone mentions you in a post', 'NOTIFICATION_TYPE_MODERATION_QUEUE' => 'Your topics/posts are approved or disapproved by a moderator', 'NOTIFICATION_TYPE_PM' => 'Someone sends you a private message', 'NOTIFICATION_TYPE_POST' => 'Someone replies to a topic to which you are subscribed', From d91e3bd66ac0f7148f3a543413d2b05d6b43d381 Mon Sep 17 00:00:00 2001 From: lavigor Date: Mon, 6 Aug 2018 12:54:53 +0300 Subject: [PATCH 060/113] [ticket/13713] Optimize caching colours PHPBB3-13713 --- .../textformatter/s9e/mention_helper.php | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index 1922ede0e5..f5a708fdf6 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -74,10 +74,9 @@ class mention_helper * Returns SQL query data for colour SELECT request * * @param string $type Name type ('u' for users, 'g' for groups) - * @param array $ids Array of IDs * @return array Array of SQL SELECT query data for extracting colours for names */ - protected function get_colours_sql($type, $ids) + protected function get_colours_sql($type) { switch ($type) { @@ -88,9 +87,7 @@ class mention_helper 'FROM' => [ USERS_TABLE => 'u', ], - 'WHERE' => 'u.user_id <> ' . ANONYMOUS . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' - AND ' . $this->db->sql_in_set('u.user_id', $ids), + 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), ]; case 'g': return [ @@ -98,25 +95,28 @@ class mention_helper 'FROM' => [ GROUPS_TABLE => 'g', ], - 'WHERE' => $this->db->sql_in_set('g.group_id', $ids), ]; } } /** - * Caches colours for selected IDs of the specified type - * - * @param string $type Name type ('u' for users, 'g' for groups) - * @param array $ids Array of IDs + * Caches colours of users and groups */ - protected function get_colours($type, $ids) + protected function cache_colours() { - $this->cached_colours[$type] = []; - - if (!empty($ids)) + if (count($this->cached_colours) > 0) { - $query = $this->db->sql_build_query('SELECT', $this->get_colours_sql($type, $ids)); - $result = $this->db->sql_query($query); + return; + } + + $types = ['u', 'g']; + + foreach ($types as $type) + { + $this->cached_colours[$type] = []; + + $query = $this->db->sql_build_query('SELECT', $this->get_colours_sql($type)); + $result = $this->db->sql_query($query, 300); while ($row = $this->db->sql_fetchrow($result)) { @@ -140,10 +140,7 @@ class mention_helper 'g' => $this->group_profile_url, ]; - // TODO: think about optimization for caching colors. - $this->cached_colours = []; - $this->get_colours('u', $this->get_mentioned_ids($xml, 'u')); - $this->get_colours('g', $this->get_mentioned_ids($xml, 'g')); + $this->cache_colours(); return TextFormatterUtils::replaceAttributes( $xml, From 89d65f5da05b265be1ed36fb384aa21245e247f3 Mon Sep 17 00:00:00 2001 From: lavigor Date: Mon, 6 Aug 2018 14:17:34 +0300 Subject: [PATCH 061/113] [ticket/13713] Remove unneeded code in helper PHPBB3-13713 --- phpBB/phpbb/notification/type/mention.php | 2 +- .../textformatter/s9e/mention_helper.php | 34 +++++-------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/phpBB/phpbb/notification/type/mention.php b/phpBB/phpbb/notification/type/mention.php index 21d4bf6f91..1ec8cc51ce 100644 --- a/phpBB/phpbb/notification/type/mention.php +++ b/phpBB/phpbb/notification/type/mention.php @@ -75,7 +75,7 @@ class mention extends \phpbb\notification\type\post 'ignore_users' => array(), ), $options); - $user_ids = $this->helper->get_mentioned_ids($post['post_text']); + $user_ids = $this->helper->get_mentioned_user_ids($post['post_text']); $user_ids = array_unique($user_ids); diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index f5a708fdf6..bd641b8b24 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -242,14 +242,12 @@ class mention_helper } /** - * Get a list of mentioned names + * Get a list of mentioned user IDs * * @param string $xml Parsed text - * @param string $type Name type ('u' for users, 'g' for groups, - * 'ug' for usernames mentioned separately or as group members) - * @return int[] List of IDs + * @return int[] List of user IDs */ - public function get_mentioned_ids($xml, $type = 'ug') + public function get_mentioned_user_ids($xml) { $ids = array(); if (strpos($xml, 'loadXML($xml); $xpath = new \DOMXPath($dom); - if ($type === 'ug') + /** @var \DOMElement $mention */ + foreach ($xpath->query('//MENTION') as $mention) { - /** @var \DOMElement $mention */ - foreach ($xpath->query('//MENTION') as $mention) + if ($mention->getAttribute('type') === 'u') { - if ($mention->getAttribute('type') === 'u') - { - $ids[] = (int) $mention->getAttribute('id'); - } - else if ($mention->getAttribute('type') === 'g') - { - $this->get_user_ids_for_group($ids, (int) $mention->getAttribute('id')); - } + $ids[] = (int) $mention->getAttribute('id'); } - } - else - { - /** @var \DOMElement $mention */ - foreach ($xpath->query('//MENTION') as $mention) + else if ($mention->getAttribute('type') === 'g') { - if ($mention->getAttribute('type') === $type) - { - $ids[] = (int) $mention->getAttribute('id'); - } + $this->get_user_ids_for_group($ids, (int) $mention->getAttribute('id')); } } From c5f515acc4c516d51630b00424e1327a5adac726 Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 7 Aug 2018 03:11:32 +0300 Subject: [PATCH 062/113] [ticket/13713] Add test for mention_helper PHPBB3-13713 --- .../phpbb_test_case_helpers.php | 2 +- tests/text_formatter/s9e/fixtures/mention.xml | 116 ++++++++++++++++ .../s9e/mention_helper_test.php | 126 ++++++++++++++++++ 3 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 tests/text_formatter/s9e/fixtures/mention.xml create mode 100644 tests/text_formatter/s9e/mention_helper_test.php diff --git a/tests/test_framework/phpbb_test_case_helpers.php b/tests/test_framework/phpbb_test_case_helpers.php index 6efb15a4fb..3e28a3271b 100644 --- a/tests/test_framework/phpbb_test_case_helpers.php +++ b/tests/test_framework/phpbb_test_case_helpers.php @@ -592,7 +592,7 @@ class phpbb_test_case_helpers // Create and register a mention_helper $mention_helper = new \phpbb\textformatter\s9e\mention_helper( - $db_driver, + ($container->has('dbal.conn')) ? $container->get('dbal.conn') : $db_driver, $auth, $container->get('user'), $phpbb_root_path, diff --git a/tests/text_formatter/s9e/fixtures/mention.xml b/tests/text_formatter/s9e/fixtures/mention.xml new file mode 100644 index 0000000000..9174029fd8 --- /dev/null +++ b/tests/text_formatter/s9e/fixtures/mention.xml @@ -0,0 +1,116 @@ + + +
    + group_id + group_name + group_type + group_colour + group_desc + + 1 + Normal group + 0 + + + + + 2 + Hidden group + 2 + + + + + 3 + Hidden group we are a member of + 2 + FF0000 + + +
    + + user_id + username + username_clean + user_type + user_lastvisit + user_colour + user_permissions + user_sig + + 2 + myself + myself + 0 + 0 + + + + + + 3 + test + test + 0 + 0 + 00FF00 + + + + + 4 + group_member_normal + group_member_normal + 0 + 0 + + + + + + 5 + group_member_hidden + group_member_hidden + 0 + 0 + + + + + + 6 + group_member_visible + group_member_visible + 0 + 0 + + + + +
    + + user_id + group_id + user_pending + + 2 + 3 + 0 + + + 4 + 1 + 0 + + + 5 + 2 + 0 + + + 6 + 3 + 0 + +
    + diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php new file mode 100644 index 0000000000..3cebdd950c --- /dev/null +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -0,0 +1,126 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; + +class mention_helper_test extends phpbb_database_test_case +{ + protected $db, $container, $user, $auth; + + /** + * @var \phpbb\textformatter\s9e\mention_helper + */ + protected $mention_helper; + + public function getDataSet() + { + return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/mention.xml'); + } + + public function setUp() + { + parent::setUp(); + + global $auth, $db, $phpbb_container, $phpEx, $phpbb_root_path; + + // Database + $this->db = $this->new_dbal(); + $db = $this->db; + + // Auth + $auth = $this->createMock('\phpbb\auth\auth'); + $auth->expects($this->any()) + ->method('acl_gets') + ->with('a_group', 'a_groupadd', 'a_groupdel') + ->willReturn(false) + ; + + // Language + $lang = new \phpbb\language\language(new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx)); + + // User + $user = $this->createMock('\phpbb\user', array(), array( + $lang, + '\phpbb\datetime' + )); + $user->ip = ''; + $user->data = array( + 'user_id' => 2, + 'username' => 'myself', + 'is_registered' => true, + 'user_colour' => '', + ); + + // Container + $phpbb_container = new ContainerBuilder(); + + $phpbb_container->set('dbal.conn', $db); + $phpbb_container->set('auth', $auth); + $phpbb_container->set('user', $user); + + $container = $this->get_test_case_helpers()->set_s9e_services($phpbb_container); + $this->mention_helper = $container->get('text_formatter.s9e.mention_helper'); + + $phpbb_container->compile(); + } + + public function inject_metadata_data() + { + return [ + [ + '[mention=u:3]test[/mention]', + 'mode=viewprofile&u=3', + 'color="00FF00"', + ], + [ + '[mention=g:3]test[/mention]', + 'mode=group&g=3', + 'color="FF0000"', + ], + ]; + } + + /** + * @dataProvider inject_metadata_data + */ + public function test_inject_metadata($incoming_xml, $expected_profile_substring, $expected_colour) + { + $result = $this->mention_helper->inject_metadata($incoming_xml); + $this->assertContains($expected_profile_substring, $result); + $this->assertContains($expected_colour, $result); + } + + public function get_mentioned_user_ids_data() + { + return [ + [ + '[mention=u:3]test[/mention][mention=u:4]test[/mention][mention=u:5]test[/mention]', + [3, 4, 5], + ], + [ + '[mention=g:1]test[/mention][mention=g:2]test[/mention][mention=g:3]test[/mention]', + [4, 2, 6], + ], + ]; + } + + /** + * @dataProvider get_mentioned_user_ids_data + */ + public function test_get_mentioned_user_ids($incoming_xml, $expected_result) + { + $this->assertSame($expected_result, $this->mention_helper->get_mentioned_user_ids($incoming_xml)); + } +} From 86caf3f57766b068596811664d4f0bc182541caf Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 7 Aug 2018 03:12:31 +0300 Subject: [PATCH 063/113] [ticket/13713] Test cleanup PHPBB3-13713 --- tests/text_formatter/s9e/mention_helper_test.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index 3cebdd950c..1df9b8f2f3 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -11,9 +11,7 @@ * */ -use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; class mention_helper_test extends phpbb_database_test_case { From 0c47c72ccea0527b694418efb82df12b95b104e7 Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 7 Aug 2018 15:40:24 +0300 Subject: [PATCH 064/113] [ticket/13713] Fix container compilation in mention_helper test PHPBB3-13713 --- tests/text_formatter/s9e/mention_helper_test.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index 1df9b8f2f3..44c9c5a00f 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -68,10 +68,11 @@ class mention_helper_test extends phpbb_database_test_case $phpbb_container->set('auth', $auth); $phpbb_container->set('user', $user); - $container = $this->get_test_case_helpers()->set_s9e_services($phpbb_container); - $this->mention_helper = $container->get('text_formatter.s9e.mention_helper'); + $this->get_test_case_helpers()->set_s9e_services($phpbb_container); $phpbb_container->compile(); + + $this->mention_helper = $phpbb_container->get('text_formatter.s9e.mention_helper'); } public function inject_metadata_data() From 35a81fce52c0dd23e0435206c33e75bcca31fdb7 Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 7 Aug 2018 16:19:58 +0300 Subject: [PATCH 065/113] [ticket/13713] Use mock for ContainerBuilder in helper test PHPBB3-13713 --- tests/text_formatter/s9e/mention_helper_test.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index 44c9c5a00f..5fd3762524 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -62,7 +62,7 @@ class mention_helper_test extends phpbb_database_test_case ); // Container - $phpbb_container = new ContainerBuilder(); + $phpbb_container = new phpbb_mock_container_builder(); $phpbb_container->set('dbal.conn', $db); $phpbb_container->set('auth', $auth); @@ -70,8 +70,6 @@ class mention_helper_test extends phpbb_database_test_case $this->get_test_case_helpers()->set_s9e_services($phpbb_container); - $phpbb_container->compile(); - $this->mention_helper = $phpbb_container->get('text_formatter.s9e.mention_helper'); } From 8f21a7365d403256fab3fa6c9393c6f43bedd277 Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 7 Aug 2018 17:32:37 +0300 Subject: [PATCH 066/113] [ticket/13713] Rework batch size handling PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 2 +- phpBB/assets/javascript/editor.js | 16 +- .../default/container/services_mention.yml | 1 + phpBB/includes/acp/acp_board.php | 1 + phpBB/includes/functions.php | 1 - phpBB/includes/functions_acp.php | 1 - phpBB/install/schemas/schema_data.sql | 1 + phpBB/language/en/acp/board.php | 2 + .../data/v330/add_mention_settings.php | 1 + phpBB/phpbb/mention/controller/mention.php | 8 +- phpBB/phpbb/mention/source/base_group.php | 17 +- phpBB/phpbb/mention/source/base_user.php | 27 +- .../phpbb/mention/source/source_interface.php | 1 + .../prosilver/template/posting_buttons.html | 2 +- tests/mention/controller_test.php | 905 +++++++++--------- 15 files changed, 501 insertions(+), 485 deletions(-) diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index ff453d16c7..08c81de0c0 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -13,7 +13,7 @@ -
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-mention-batch-size="{S_MENTION_BATCH_SIZE}" data-topic-id="{S_TOPIC_ID}" data-user-id="{S_USER_ID}"> +
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-topic-id="{S_TOPIC_ID}" data-user-id="{S_USER_ID}"> diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 9bd49cc8ed..9a05b0a458 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -388,19 +388,19 @@ function getCaretPosition(txtarea) { function Mentions() { let $mentionDataContainer = $('[data-mention-url]:first'); let mentionURL = $mentionDataContainer.data('mentionUrl'); - let mentionBatchSize = $mentionDataContainer.data('mentionBatchSize'); let mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'); let mentionTopicId = $mentionDataContainer.data('topicId'); let mentionUserId = $mentionDataContainer.data('userId'); let queryInProgress = null; let cachedNames = []; + let cachedAll = []; let cachedSearchKey = 'name'; function defaultAvatar(type) { return (type === 'group') ? '' : ''; } - function getCachedNames(query) { + function getCachedKeyword(query) { if (!cachedNames) { return null; } @@ -410,11 +410,11 @@ function getCaretPosition(txtarea) { for (i = query.length; i > 0; i--) { let startStr = query.substr(0, i); if (cachedNames[startStr]) { - return cachedNames[startStr]; + return startStr; } } - return cachedNames['']; + return ''; } function getMatchedNames(query, items, searchKey) { @@ -442,7 +442,8 @@ function getCaretPosition(txtarea) { return; } - let cachedNamesForQuery = getCachedNames(query); + let cachedKeyword = getCachedKeyword(query), + cachedNamesForQuery = (cachedKeyword) ? cachedNames[cachedKeyword] : null; /* * Use cached values when we can: @@ -453,7 +454,7 @@ function getCaretPosition(txtarea) { */ if (cachedNamesForQuery && (getMatchedNames(query, cachedNamesForQuery, cachedSearchKey).length >= mentionNamesLimit || - cachedNamesForQuery.length < mentionBatchSize)) { + cachedAll[cachedKeyword])) { callback(cachedNamesForQuery); return; } @@ -462,7 +463,8 @@ function getCaretPosition(txtarea) { let params = {keyword: query, topic_id: mentionTopicId, _referer: location.href}; $.getJSON(mentionURL, params, function(data) { - cachedNames[query] = data; + cachedNames[query] = data.names; + cachedAll[query] = data.all; callback(data); }).always(function() { queryInProgress = null; diff --git a/phpBB/config/default/container/services_mention.yml b/phpBB/config/default/container/services_mention.yml index 917da928d5..e2861b984c 100644 --- a/phpBB/config/default/container/services_mention.yml +++ b/phpBB/config/default/container/services_mention.yml @@ -20,6 +20,7 @@ services: abstract: true arguments: - '@dbal.conn' + - '@config' - '@group_helper' - '@user' - '@auth' diff --git a/phpBB/includes/acp/acp_board.php b/phpBB/includes/acp/acp_board.php index 62daba0373..8e549b4b34 100644 --- a/phpBB/includes/acp/acp_board.php +++ b/phpBB/includes/acp/acp_board.php @@ -223,6 +223,7 @@ class acp_board 'legend3' => 'MENTIONS', 'allow_mentions' => array('lang' => 'ALLOW_MENTIONS', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => false), 'mention_names_limit' => array('lang' => 'MENTION_NAMES_LIMIT', 'validate' => 'int:1:9999', 'type' => 'number:1:9999', 'explain' => false), + 'mention_batch_size' => array('lang' => 'MENTION_BATCH_SIZE', 'validate' => 'int:1:9999', 'type' => 'number:1:9999', 'explain' => true), 'legend4' => 'ACP_SUBMIT_CHANGES', ) diff --git a/phpBB/includes/functions.php b/phpBB/includes/functions.php index fb65af2dc8..be8eecfe2a 100644 --- a/phpBB/includes/functions.php +++ b/phpBB/includes/functions.php @@ -3947,7 +3947,6 @@ function page_header($page_title = '', $display_online_list = false, $item_id = 'U_FEED' => $controller_helper->route('phpbb_feed_index'), 'S_ALLOW_MENTIONS' => ($config['allow_mentions'] && $auth->acl_get('u_mention') && (empty($forum_id) || $auth->acl_get('f_mention', $forum_id))) ? true : false, - 'S_MENTION_BATCH_SIZE' => 100, // TODO: do not hardcode the value 'S_MENTION_NAMES_LIMIT' => $config['mention_names_limit'], 'U_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), diff --git a/phpBB/includes/functions_acp.php b/phpBB/includes/functions_acp.php index c06ae1f748..ba1584ab82 100644 --- a/phpBB/includes/functions_acp.php +++ b/phpBB/includes/functions_acp.php @@ -90,7 +90,6 @@ function adm_page_header($page_title) 'U_INDEX' => append_sid("{$phpbb_root_path}index.$phpEx"), 'S_ALLOW_MENTIONS' => ($config['allow_mentions'] && $auth->acl_get('u_mention')) ? true : false, - 'S_MENTION_BATCH_SIZE' => 100, // TODO: do not hardcode the value 'S_MENTION_NAMES_LIMIT' => $config['mention_names_limit'], 'U_MENTION_URL' => $controller_helper->route('phpbb_mention_controller'), diff --git a/phpBB/install/schemas/schema_data.sql b/phpBB/install/schemas/schema_data.sql index a11b1e1a9c..e8294f0a8f 100644 --- a/phpBB/install/schemas/schema_data.sql +++ b/phpBB/install/schemas/schema_data.sql @@ -235,6 +235,7 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('max_sig_img_height INSERT INTO phpbb_config (config_name, config_value) VALUES ('max_sig_img_width', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('max_sig_smilies', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('max_sig_urls', '5'); +INSERT INTO phpbb_config (config_name, config_value) VALUES ('mention_batch_size', '50'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('mention_names_limit', '10'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('mime_triggers', 'body|head|html|img|plaintext|a href|pre|script|table|title'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('min_name_chars', '3'); diff --git a/phpBB/language/en/acp/board.php b/phpBB/language/en/acp/board.php index d4aba08f3c..8d85f898a0 100644 --- a/phpBB/language/en/acp/board.php +++ b/phpBB/language/en/acp/board.php @@ -189,6 +189,8 @@ $lang = array_merge($lang, array( 'MAX_POST_URLS' => 'Maximum links per post', 'MAX_POST_URLS_EXPLAIN' => 'Maximum number of URLs in a post. Set to 0 for unlimited links.', 'MENTIONS' => 'Mentions', + 'MENTION_BATCH_SIZE' => 'Maximum number of names fetched from each source of names for a single request', + 'MENTION_BATCH_SIZE_EXPLAIN' => 'Examples of sources: friends, topic repliers, group members etc.', 'MENTION_NAMES_LIMIT' => 'Maximum number of names in dropdown list', 'MIN_CHAR_LIMIT' => 'Minimum characters per post/message', 'MIN_CHAR_LIMIT_EXPLAIN' => 'The minimum number of characters the user need to enter within a post/private message. The minimum for this setting is 1.', diff --git a/phpBB/phpbb/db/migration/data/v330/add_mention_settings.php b/phpBB/phpbb/db/migration/data/v330/add_mention_settings.php index 1f38d919b2..c0e9a8cf58 100644 --- a/phpBB/phpbb/db/migration/data/v330/add_mention_settings.php +++ b/phpBB/phpbb/db/migration/data/v330/add_mention_settings.php @@ -19,6 +19,7 @@ class add_mention_settings extends \phpbb\db\migration\migration { return array( array('config.add', array('allow_mentions', true)), + array('config.add', array('mention_batch_size', 50)), array('config.add', array('mention_names_limit', 10)), // Set up user permissions diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index 770f84b015..42e0d217ef 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -52,12 +52,16 @@ class mention $keyword = $this->request->variable('keyword', '', true); $topic_id = $this->request->variable('topic_id', 0); $names = []; + $hasNamesRemaining = false; foreach ($this->mention_sources as $source) { - $source->get($names, $keyword, $topic_id); + $hasNamesRemaining = !$source->get($names, $keyword, $topic_id) || $hasNamesRemaining; } - return new JsonResponse(array_values($names)); + return new JsonResponse([ + 'names' => array_values($names), + 'all' => !$hasNamesRemaining, + ]); } } diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index e6281bcbac..c8d498ce81 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -18,6 +18,9 @@ abstract class base_group implements source_interface /** @var \phpbb\db\driver\driver_interface */ protected $db; + /** @var \phpbb\config\config */ + protected $config; + /** @var \phpbb\group\helper */ protected $helper; @@ -39,9 +42,10 @@ abstract class base_group implements source_interface /** * Constructor */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\group\helper $helper, \phpbb\user $user, \phpbb\auth\auth $auth, $phpbb_root_path, $phpEx) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\group\helper $helper, \phpbb\user $user, \phpbb\auth\auth $auth, $phpbb_root_path, $phpEx) { $this->db = $db; + $this->config = $config; $this->helper = $helper; $this->user = $user; $this->auth = $auth; @@ -138,8 +142,15 @@ abstract class base_group implements source_interface $matches = preg_grep('/^' . preg_quote($keyword) . '.*/i', $groups['names']); $group_ids = array_intersect($group_ids, array_flip($matches)); + $i = 0; foreach ($group_ids as $group_id) { + if ($i >= $this->config['mention_batch_size']) + { + // Do not exceed the names limit + return false; + } + $group_rank = phpbb_get_user_rank($groups[$group_id], false); array_push($names, [ 'name' => $groups[$group_id]['group_name'], @@ -152,6 +163,10 @@ abstract class base_group implements source_interface 'rank' => (isset($group_rank['title'])) ? $group_rank['title'] : '', 'priority' => $this->get_priority($groups[$group_id]), ]); + + $i++; } + + return true; } } diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index 2fd6cf5f98..9a7f484379 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -15,9 +15,6 @@ namespace phpbb\mention\source; abstract class base_user implements source_interface { - /** @var int */ - const NAMES_BATCH_SIZE = 100; - /** @var \phpbb\db\driver\driver_interface */ protected $db; @@ -73,6 +70,7 @@ abstract class base_user implements source_interface */ public function get(array &$names, $keyword, $topic_id) { + $fetched_all = false; $keyword = utf8_clean_string($keyword); // Do not query all possible users (just a moderate amount), cache results for 5 minutes @@ -81,12 +79,13 @@ abstract class base_user implements source_interface $i = 0; $users = []; $user_ids = []; - while ($i < self::NAMES_BATCH_SIZE) + while ($i < $this->config['mention_batch_size']) { $row = $this->db->sql_fetchrow($result); if (!$row) { + $fetched_all = true; break; } @@ -100,6 +99,24 @@ abstract class base_user implements source_interface $user_ids[] = $row['user_id']; } + // Determine whether all usernames were fetched in current batch + if (!$fetched_all) + { + $fetched_all = true; + + while ($row = $this->db->sql_fetchrow($result)) + { + if (!empty($keyword) && strpos($row['username_clean'], $keyword) !== 0) + { + continue; + } + + // At least one username hasn't been fetched - exit loop + $fetched_all = false; + break; + } + } + $this->db->sql_freeresult($result); // Load all user data with a single SQL query, needed for ranks and avatars @@ -120,5 +137,7 @@ abstract class base_user implements source_interface 'priority' => $this->get_priority($user), ]); } + + return $fetched_all; } } diff --git a/phpBB/phpbb/mention/source/source_interface.php b/phpBB/phpbb/mention/source/source_interface.php index 075bea295d..b9e126324b 100644 --- a/phpBB/phpbb/mention/source/source_interface.php +++ b/phpBB/phpbb/mention/source/source_interface.php @@ -22,6 +22,7 @@ interface source_interface * @param array $names Array of already fetched data with names * @param string $keyword Search string * @param int $topic_id Current topic ID + * @return bool Whether there are no more satisfying names left */ public function get(array &$names, $keyword, $topic_id); diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 697035b850..22b9e8a7b0 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -39,7 +39,7 @@
    -
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-mention-batch-size="{S_MENTION_BATCH_SIZE}" data-topic-id="{S_TOPIC_ID}" data-user-id="{S_USER_ID}"> +
    data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-topic-id="{S_TOPIC_ID}" data-user-id="{S_USER_ID}"> diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php index ffc65e7c18..4294e7365d 100644 --- a/tests/mention/controller_test.php +++ b/tests/mention/controller_test.php @@ -55,6 +55,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case // Config $config = new \phpbb\config\config(array( 'allow_mentions' => true, + 'mention_batch_size' => 8, 'mention_names_limit' => 3, )); @@ -143,491 +144,493 @@ class phpbb_mention_controller_test extends phpbb_database_test_case /** * NOTE: * 1) in production comparison with 'myself' is being done in JS - * 2) mention_names_limit does not limit the number of returned items - * 3) team members of hidden groups can also be mentioned (because they are shown on teampage) + * 2) team members of hidden groups can also be mentioned (because they are shown on teampage) */ return [ ['', 0, [ - [ - 'name' => 'friend', - 'type' => 'u', - 'id' => 7, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + 'names' => [ + [ + 'name' => 'friend', + 'type' => 'u', + 'id' => 7, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'Group we are a member of', - 'type' => 'g', - 'id' => 3, - 'avatar' => [ - 'type' => 'group', - 'img' => '', + [ + 'name' => 'Group we are a member of', + 'type' => 'g', + 'id' => 3, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'Normal group', - 'type' => 'g', - 'id' => 1, - 'avatar' => [ - 'type' => 'group', - 'img' => '', + [ + 'name' => 'Normal group', + 'type' => 'g', + 'id' => 1, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'team_member_hidden', - 'type' => 'u', - 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'team_member_normal', - 'type' => 'u', - 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'myself', - 'type' => 'u', - 'id' => 2, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'myself', + 'type' => 'u', + 'id' => 2, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'poster', - 'type' => 'u', - 'id' => 3, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'poster', + 'type' => 'u', + 'id' => 3, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'replier', - 'type' => 'u', - 'id' => 4, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'replier', + 'type' => 'u', + 'id' => 4, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'team_member_normal', - 'type' => 'u', - 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'team_member_hidden', - 'type' => 'u', - 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'friend', - 'type' => 'u', - 'id' => 7, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'friend', + 'type' => 'u', + 'id' => 7, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test', - 'type' => 'u', - 'id' => 8, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'test', + 'type' => 'u', + 'id' => 8, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test1', - 'type' => 'u', - 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test2', - 'type' => 'u', - 'id' => 10, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'Group we are a member of', + 'type' => 'g', + 'id' => 3, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test3', - 'type' => 'u', - 'id' => 11, - 'avatar' => [ - 'type' => 'user', - 'img' => '', - ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'Group we are a member of', - 'type' => 'g', - 'id' => 3, - 'avatar' => [ - 'type' => 'group', - 'img' => '', - ], - 'rank' => '', - 'priority' => 1, ], + 'all' => false, ]], ['', 1, [ - [ - 'name' => 'friend', - 'type' => 'u', - 'id' => 7, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + 'names' => [ + [ + 'name' => 'friend', + 'type' => 'u', + 'id' => 7, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'Group we are a member of', - 'type' => 'g', - 'id' => 3, - 'avatar' => [ - 'type' => 'group', - 'img' => '', + [ + 'name' => 'Group we are a member of', + 'type' => 'g', + 'id' => 3, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'Normal group', - 'type' => 'g', - 'id' => 1, - 'avatar' => [ - 'type' => 'group', - 'img' => '', + [ + 'name' => 'Normal group', + 'type' => 'g', + 'id' => 1, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'team_member_hidden', - 'type' => 'u', - 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'team_member_normal', - 'type' => 'u', - 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'replier', - 'type' => 'u', - 'id' => 4, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'replier', + 'type' => 'u', + 'id' => 4, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'poster', - 'type' => 'u', - 'id' => 3, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'poster', + 'type' => 'u', + 'id' => 3, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 5, ], - 'rank' => '', - 'priority' => 5, - ], - [ - 'name' => 'myself', - 'type' => 'u', - 'id' => 2, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'myself', + 'type' => 'u', + 'id' => 2, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'poster', - 'type' => 'u', - 'id' => 3, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'poster', + 'type' => 'u', + 'id' => 3, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'replier', - 'type' => 'u', - 'id' => 4, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'replier', + 'type' => 'u', + 'id' => 4, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'team_member_normal', - 'type' => 'u', - 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'team_member_hidden', - 'type' => 'u', - 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'friend', - 'type' => 'u', - 'id' => 7, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'friend', + 'type' => 'u', + 'id' => 7, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test', - 'type' => 'u', - 'id' => 8, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'test', + 'type' => 'u', + 'id' => 8, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test1', - 'type' => 'u', - 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test2', - 'type' => 'u', - 'id' => 10, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'Group we are a member of', + 'type' => 'g', + 'id' => 3, + 'avatar' => [ + 'type' => 'group', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test3', - 'type' => 'u', - 'id' => 11, - 'avatar' => [ - 'type' => 'user', - 'img' => '', - ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'Group we are a member of', - 'type' => 'g', - 'id' => 3, - 'avatar' => [ - 'type' => 'group', - 'img' => '', - ], - 'rank' => '', - 'priority' => 1, ], + 'all' => false, ]], ['t', 1, [ - [ - 'name' => 'team_member_hidden', - 'type' => 'u', - 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + 'names' => [ + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'team_member_normal', - 'type' => 'u', - 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 1, ], - 'rank' => '', - 'priority' => 1, - ], - [ - 'name' => 'team_member_normal', - 'type' => 'u', - 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_normal', + 'type' => 'u', + 'id' => 5, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'team_member_hidden', - 'type' => 'u', - 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'team_member_hidden', + 'type' => 'u', + 'id' => 6, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test', - 'type' => 'u', - 'id' => 8, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'test', + 'type' => 'u', + 'id' => 8, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test1', - 'type' => 'u', - 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test2', - 'type' => 'u', - 'id' => 10, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'test2', + 'type' => 'u', + 'id' => 10, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test3', - 'type' => 'u', - 'id' => 11, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + [ + 'name' => 'test3', + 'type' => 'u', + 'id' => 11, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, ], + 'all' => true, ]], ['test', 1, [ - [ - 'name' => 'test', - 'type' => 'u', - 'id' => 8, - 'avatar' => [ - 'type' => 'user', - 'img' => '', + 'names' => [ + [ + 'name' => 'test', + 'type' => 'u', + 'id' => 8, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test1', + 'type' => 'u', + 'id' => 9, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test2', + 'type' => 'u', + 'id' => 10, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, + ], + [ + 'name' => 'test3', + 'type' => 'u', + 'id' => 11, + 'avatar' => [ + 'type' => 'user', + 'img' => '', + ], + 'rank' => '', + 'priority' => 0, ], - 'rank' => '', - 'priority' => 0, ], - [ + 'all' => true, + ]], + ['test1', 1, [ + 'names' => [[ 'name' => 'test1', 'type' => 'u', 'id' => 9, @@ -637,41 +640,9 @@ class phpbb_mention_controller_test extends phpbb_database_test_case ], 'rank' => '', 'priority' => 0, - ], - [ - 'name' => 'test2', - 'type' => 'u', - 'id' => 10, - 'avatar' => [ - 'type' => 'user', - 'img' => '', - ], - 'rank' => '', - 'priority' => 0, - ], - [ - 'name' => 'test3', - 'type' => 'u', - 'id' => 11, - 'avatar' => [ - 'type' => 'user', - 'img' => '', - ], - 'rank' => '', - 'priority' => 0, - ], + ]], + 'all' => true, ]], - ['test1', 1, [[ - 'name' => 'test1', - 'type' => 'u', - 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => '', - ], - 'rank' => '', - 'priority' => 0, - ]]], ]; } From de356232f32530cc3efafe33e4836447087304d6 Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 7 Aug 2018 19:25:39 +0300 Subject: [PATCH 067/113] [ticket/13713] Do not use test_case_helpers for helper test PHPBB3-13713 --- tests/text_formatter/s9e/mention_helper_test.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index 5fd3762524..1d322fb3aa 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -68,9 +68,17 @@ class mention_helper_test extends phpbb_database_test_case $phpbb_container->set('auth', $auth); $phpbb_container->set('user', $user); - $this->get_test_case_helpers()->set_s9e_services($phpbb_container); + // Create and register a mention_helper + $mention_helper = new \phpbb\textformatter\s9e\mention_helper( + $db, + $auth, + $user, + $phpbb_root_path, + $phpEx + ); + $phpbb_container->set('text_formatter.s9e.mention_helper', $mention_helper); - $this->mention_helper = $phpbb_container->get('text_formatter.s9e.mention_helper'); + $this->mention_helper = $mention_helper; } public function inject_metadata_data() From 99e93a5a04e52f4ffe3196bb4224368249833c21 Mon Sep 17 00:00:00 2001 From: lavigor Date: Wed, 8 Aug 2018 00:23:44 +0300 Subject: [PATCH 068/113] [ticket/13713] Ensure that cache variable is initialized properly PHPBB3-13713 --- .../text_formatter/s9e/mention_helper_test.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index 1d322fb3aa..0cbb978649 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -31,7 +31,10 @@ class mention_helper_test extends phpbb_database_test_case { parent::setUp(); - global $auth, $db, $phpbb_container, $phpEx, $phpbb_root_path; + global $auth, $db, $cache, $phpbb_container, $phpEx, $phpbb_root_path; + + // Disable caching for this test class + $cache = null; // Database $this->db = $this->new_dbal(); @@ -68,17 +71,9 @@ class mention_helper_test extends phpbb_database_test_case $phpbb_container->set('auth', $auth); $phpbb_container->set('user', $user); - // Create and register a mention_helper - $mention_helper = new \phpbb\textformatter\s9e\mention_helper( - $db, - $auth, - $user, - $phpbb_root_path, - $phpEx - ); - $phpbb_container->set('text_formatter.s9e.mention_helper', $mention_helper); + $this->get_test_case_helpers()->set_s9e_services($phpbb_container); - $this->mention_helper = $mention_helper; + $this->mention_helper = $phpbb_container->get('text_formatter.s9e.mention_helper'); } public function inject_metadata_data() From f64dbf530381ad947e58a30e721a8b2cbd70f864 Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 14 Aug 2018 02:47:16 +0300 Subject: [PATCH 069/113] [ticket/13713] Fix multiple dropdown issues PHPBB3-13713 --- phpBB/adm/style/admin.css | 4 ++-- phpBB/assets/javascript/editor.js | 4 ++-- phpBB/styles/prosilver/theme/mentions.css | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index f9d5c8257f..7afffed2de 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1686,7 +1686,6 @@ fieldset.submit-buttons legend { 0 1px 5px 0 rgba(0, 0, 0, 0.12); position: absolute; z-index: 999; - overflow: auto; transition: all 0.2s ease; } @@ -1695,6 +1694,8 @@ fieldset.submit-buttons legend { } .atwho-view-ul { /* mention-list */ + overflow: auto; + max-height: 200px; margin: 0; padding: 0; list-style-type: none; @@ -1736,7 +1737,6 @@ svg.mention-media-avatar { /* TODO: remove it after general normalization */ overflow: hidden; justify-content: flex-start; align-items: center; - max-width: 300px; padding: 16px; cursor: pointer; } diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 9a05b0a458..d57cb7ed51 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -443,7 +443,7 @@ function getCaretPosition(txtarea) { } let cachedKeyword = getCachedKeyword(query), - cachedNamesForQuery = (cachedKeyword) ? cachedNames[cachedKeyword] : null; + cachedNamesForQuery = (cachedKeyword != null) ? cachedNames[cachedKeyword] : null; /* * Use cached values when we can: @@ -465,7 +465,7 @@ function getCaretPosition(txtarea) { $.getJSON(mentionURL, params, function(data) { cachedNames[query] = data.names; cachedAll[query] = data.all; - callback(data); + callback(data.names); }).always(function() { queryInProgress = null; }); diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 262e83eecf..b3d9950f28 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -18,11 +18,12 @@ border-radius: 2px; position: absolute; z-index: 999; - overflow: auto; transition: all 0.2s ease; } .atwho-view-ul { /* mention-list */ + overflow: auto; + max-height: 200px; margin: 0; padding: 0; list-style-type: none; @@ -57,7 +58,6 @@ svg.mention-media-avatar { /* TODO: remove it after general normalization */ overflow: hidden; justify-content: flex-start; align-items: center; - max-width: 300px; padding: 16px; cursor: pointer; } From c2720792acd4a3be8cfac348d6b51df44f50d6ed Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 14 Aug 2018 02:48:28 +0300 Subject: [PATCH 070/113] [ticket/13713] Handle TODOs PHPBB3-13713 --- phpBB/adm/style/admin.css | 2 +- phpBB/assets/javascript/editor.js | 1 - phpBB/phpbb/notification/type/mention.php | 1 - phpBB/phpbb/textformatter/data_access.php | 1 - phpBB/styles/prosilver/theme/mentions.css | 2 +- 5 files changed, 2 insertions(+), 5 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 7afffed2de..ccf6a0f51a 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1721,7 +1721,7 @@ fieldset.submit-buttons legend { height: 40px; } -svg.mention-media-avatar { /* TODO: remove it after general normalization */ +svg { /* TODO: remove it after general normalization */ fill: currentColor; } diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index d57cb7ed51..5b3725d63f 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -480,7 +480,6 @@ function getCaretPosition(txtarea) { at: "@", acceptSpaceBar: true, displayTpl: function(data) { - // TODO: handle image scaling let avatar = (data.avatar.img) ? "" + data.avatar.img + "" : defaultAvatar(data.avatar.type), rank = (data.rank) ? "" + data.rank + "" : ''; return "
  • " + avatar + "" + data.name + rank + "
  • "; diff --git a/phpBB/phpbb/notification/type/mention.php b/phpBB/phpbb/notification/type/mention.php index 1ec8cc51ce..2dda929d5b 100644 --- a/phpBB/phpbb/notification/type/mention.php +++ b/phpBB/phpbb/notification/type/mention.php @@ -91,7 +91,6 @@ class mention extends \phpbb\notification\type\post /** * Update a notification - * TODO: decide what to do with this stuff * * @param array $post Data specific for this type that will be updated * @return true diff --git a/phpBB/phpbb/textformatter/data_access.php b/phpBB/phpbb/textformatter/data_access.php index 808c1ea04e..27ce778904 100644 --- a/phpBB/phpbb/textformatter/data_access.php +++ b/phpBB/phpbb/textformatter/data_access.php @@ -138,7 +138,6 @@ class data_access 'email' => 10, 'flash' => 11, 'attachment' => 12, - 'mention' => 13, // TODO: change ID/remove? ); $styles = array(); diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index b3d9950f28..291f8ed092 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -43,7 +43,7 @@ height: 40px; } -svg.mention-media-avatar { /* TODO: remove it after general normalization */ +svg { /* TODO: remove it after general normalization */ fill: currentColor; } From e13f22f84159af057f455d3e22b2265cda9e5b5e Mon Sep 17 00:00:00 2001 From: lavigor Date: Tue, 14 Aug 2018 18:44:01 +0300 Subject: [PATCH 071/113] [ticket/13713] Make style code changes requested by @hanakin PHPBB3-13713 --- phpBB/styles/prosilver/theme/colours.css | 2 +- phpBB/styles/prosilver/theme/mentions.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/phpBB/styles/prosilver/theme/colours.css b/phpBB/styles/prosilver/theme/colours.css index 96cac04ee1..7e56e06982 100644 --- a/phpBB/styles/prosilver/theme/colours.css +++ b/phpBB/styles/prosilver/theme/colours.css @@ -369,7 +369,7 @@ p.post-notice { background-image: none; } -/* colours and backgrounds for mentions.css */ +/* colours and backgrounds for mentions.css */ /* mention dropdown */ .atwho-view { /* mention-container */ diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 291f8ed092..e7f17bc03c 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -1,5 +1,5 @@ /* -------------------------------------------------------------- /* - $Mentions + $Mentions /* -------------------------------------------------------------- */ /* stylelint-disable selector-max-compound-selectors */ From 2d08e570f7f28856f7637a85b77572ca15451084 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 13 Sep 2018 16:13:23 +0300 Subject: [PATCH 072/113] [ticket/13713] Fix paddings PHPBB3-13713 --- phpBB/adm/style/admin.css | 4 ++-- phpBB/styles/prosilver/theme/mentions.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index ccf6a0f51a..75c98b05e0 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1707,7 +1707,7 @@ fieldset.submit-buttons legend { flex-shrink: 0; justify-content: center; align-items: center; - margin-right: 16px; + margin-right: 8px; margin-left: 0; } @@ -1737,7 +1737,7 @@ svg { /* TODO: remove it after general normalization */ overflow: hidden; justify-content: flex-start; align-items: center; - padding: 16px; + padding: 8px; cursor: pointer; } diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index e7f17bc03c..83f177083e 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -34,7 +34,7 @@ flex-shrink: 0; justify-content: center; align-items: center; - margin-right: 16px; + margin-right: 8px; margin-left: 0; } @@ -58,7 +58,7 @@ svg { /* TODO: remove it after general normalization */ overflow: hidden; justify-content: flex-start; align-items: center; - padding: 16px; + padding: 8px; cursor: pointer; } From 652e5e0753c8d3bc759a015d22a648dd0c929e23 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 13 Sep 2018 16:53:15 +0300 Subject: [PATCH 073/113] [ticket/13713] Fix Firefox style issue and add comments PHPBB3-13713 --- phpBB/adm/style/admin.css | 3 ++- phpBB/styles/prosilver/theme/mentions.css | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 75c98b05e0..19a277e18d 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1694,7 +1694,7 @@ fieldset.submit-buttons legend { } .atwho-view-ul { /* mention-list */ - overflow: auto; + overflow: auto; /* placed here for list to scroll with arrow key press */ max-height: 200px; margin: 0; padding: 0; @@ -1760,6 +1760,7 @@ svg { /* TODO: remove it after general normalization */ .mention-name { line-height: 1.25; + margin-right: 20px; /* needed to account for scrollbar bug on Firefox for Windows */ } .mention-rank { diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 83f177083e..bb9ffff8d7 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -22,7 +22,7 @@ } .atwho-view-ul { /* mention-list */ - overflow: auto; + overflow: auto; /* placed here for list to scroll with arrow key press */ max-height: 200px; margin: 0; padding: 0; @@ -73,6 +73,7 @@ svg { /* TODO: remove it after general normalization */ .mention-name { line-height: 1.25; + margin-right: 20px; /* needed to account for scrollbar bug on Firefox for Windows */ } .mention-rank { From 1d7d906771d5594e3ecefe3e1f330f9b72c4e720 Mon Sep 17 00:00:00 2001 From: lavigor Date: Thu, 13 Sep 2018 18:53:00 +0300 Subject: [PATCH 074/113] [ticket/13713] Fix variable name in controller PHPBB3-13713 --- phpBB/phpbb/mention/controller/mention.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index 42e0d217ef..eec3fbc5d1 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -52,16 +52,16 @@ class mention $keyword = $this->request->variable('keyword', '', true); $topic_id = $this->request->variable('topic_id', 0); $names = []; - $hasNamesRemaining = false; + $has_names_remaining = false; foreach ($this->mention_sources as $source) { - $hasNamesRemaining = !$source->get($names, $keyword, $topic_id) || $hasNamesRemaining; + $has_names_remaining = !$source->get($names, $keyword, $topic_id) || $has_names_remaining; } return new JsonResponse([ 'names' => array_values($names), - 'all' => !$hasNamesRemaining, + 'all' => !$has_names_remaining, ]); } } From 5ed207c4a0a0e1b2676cd5a2cebec767e6d16bbd Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 14 Sep 2018 11:58:14 +0300 Subject: [PATCH 075/113] [ticket/13713] Fix comment regarding the query of user IDs PHPBB3-13713 --- phpBB/phpbb/mention/source/base_user.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index 9a7f484379..40e42947b4 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -73,7 +73,7 @@ abstract class base_user implements source_interface $fetched_all = false; $keyword = utf8_clean_string($keyword); - // Do not query all possible users (just a moderate amount), cache results for 5 minutes + // Grab all necessary user IDs, cache results for 5 minutes $result = $this->db->sql_query($this->query($keyword, $topic_id), 300); $i = 0; From e036c40b67f2fad336d4da4eeac2f1eaafd9b8b2 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 14 Sep 2018 12:09:30 +0300 Subject: [PATCH 076/113] [ticket/13713] Get usernames from the data fetched by user_loader class PHPBB3-13713 --- phpBB/phpbb/mention/source/base_user.php | 6 +++--- phpBB/phpbb/mention/source/friend.php | 2 +- phpBB/phpbb/mention/source/team.php | 2 +- phpBB/phpbb/mention/source/topic.php | 2 +- phpBB/phpbb/mention/source/user.php | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index 40e42947b4..e002840614 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -124,14 +124,14 @@ abstract class base_user implements source_interface foreach ($users as $user) { - $user_rank = $this->user_loader->get_rank($user['user_id'], true); + $user_rank = $this->user_loader->get_rank($user['user_id']); array_push($names, [ - 'name' => $user['username'], + 'name' => $this->user_loader->get_username($user['user_id'], 'username'), 'type' => 'u', 'id' => $user['user_id'], 'avatar' => [ 'type' => 'user', - 'img' => $this->user_loader->get_avatar($user['user_id'], true), + 'img' => $this->user_loader->get_avatar($user['user_id']), ], 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', 'priority' => $this->get_priority($user), diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index dfd90a813c..f68bea175a 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -39,7 +39,7 @@ class friend extends base_user * Results will be cached on a per-user basis */ $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username_clean, u.username, u.user_id', + 'SELECT' => 'u.username_clean, u.user_id', 'FROM' => [ USERS_TABLE => 'u', ], diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index ab59cc7733..481fd1a132 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -29,7 +29,7 @@ class team extends base_user * Results will be cached in a single file */ $query = $this->db->sql_build_query('SELECT_DISTINCT', [ - 'SELECT' => 'u.username_clean, u.username, u.user_id', + 'SELECT' => 'u.username_clean, u.user_id', 'FROM' => [ USERS_TABLE => 'u', USER_GROUP_TABLE => 'ug', diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index c1b78542c8..5418e4af85 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -46,7 +46,7 @@ class topic extends base_user * Results will be cached on a per-topic basis */ $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username_clean, u.username, u.user_id, t.topic_poster', + 'SELECT' => 'u.username_clean, u.user_id, t.topic_poster', 'FROM' => [ USERS_TABLE => 'u', ], diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index dedafd7d24..57f1a23eab 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -35,7 +35,7 @@ class user extends base_user protected function query($keyword, $topic_id) { $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username_clean, u.username, u.user_id', + 'SELECT' => 'u.username_clean, u.user_id', 'FROM' => [ USERS_TABLE => 'u', ], From 3c27b47236edf6a6436c1e98090b05a744baeb29 Mon Sep 17 00:00:00 2001 From: lavigor Date: Fri, 14 Sep 2018 12:33:16 +0300 Subject: [PATCH 077/113] [ticket/13713] Specify color as a BBCode attribute PHPBB3-13713 --- phpBB/phpbb/textformatter/s9e/factory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php index 66c6b5132e..56b98e079f 100644 --- a/phpBB/phpbb/textformatter/s9e/factory.php +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -87,6 +87,7 @@ class factory implements \phpbb\textformatter\cache_interface 'mention' => "[MENTION={PARSE=/^(?[ug]):(?\d+)$/} profile_url={URL;optional;postFilter=#false} + color={COLOR;optional} ]{TEXT}[/MENTION]", 'quote' => "[QUOTE From 1f8e91cf113d5b2be6de26b96c92ed1021207c30 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 15 Sep 2018 12:06:31 +0300 Subject: [PATCH 078/113] [ticket/13713] Fix SELECT DISTINCT query for team usernames PHPBB3-13713 --- phpBB/phpbb/mention/source/team.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index 481fd1a132..4b40d7f224 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -37,7 +37,7 @@ class team extends base_user ], 'WHERE' => 'ug.group_id = t.group_id AND ug.user_id = u.user_id AND ug.user_pending = 0 AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), - 'ORDER_BY' => 'u.username' + 'ORDER_BY' => 'u.username_clean' ]); return $query; } From 149d0aa5d3163cc089c3606670819e18d9928dff Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 15 Sep 2018 12:07:30 +0300 Subject: [PATCH 079/113] [ticket/13713] Cast topic_id to integer in the topic source PHPBB3-13713 --- phpBB/phpbb/mention/source/topic.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index 5418e4af85..51b855e97b 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -60,7 +60,7 @@ class topic extends base_user 'ON' => 't.topic_id = p.topic_id' ], ], - 'WHERE' => 'p.topic_id = ' . $topic_id . ' + 'WHERE' => 'p.topic_id = ' . (int) $topic_id . ' AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), 'ORDER_BY' => 'p.post_time DESC' ]); From b84c10733bc75018620232ff73833026c72554bd Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 15 Sep 2018 12:26:06 +0300 Subject: [PATCH 080/113] [ticket/13713] Remove mention colors to reduce the cache size PHPBB3-13713 --- phpBB/phpbb/textformatter/s9e/factory.php | 4 -- .../textformatter/s9e/mention_helper.php | 69 ------------------- tests/text_formatter/s9e/fixtures/mention.xml | 4 +- .../s9e/mention_helper_test.php | 5 +- 4 files changed, 3 insertions(+), 79 deletions(-) diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php index 56b98e079f..58c1f2f16d 100644 --- a/phpBB/phpbb/textformatter/s9e/factory.php +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -87,7 +87,6 @@ class factory implements \phpbb\textformatter\cache_interface 'mention' => "[MENTION={PARSE=/^(?[ug]):(?\d+)$/} profile_url={URL;optional;postFilter=#false} - color={COLOR;optional} ]{TEXT}[/MENTION]", 'quote' => "[QUOTE @@ -136,9 +135,6 @@ class factory implements \phpbb\textformatter\cache_interface - - color: #; - diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index bd641b8b24..e7b58f0b81 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -42,11 +42,6 @@ class mention_helper */ protected $group_profile_url; - /** - * @var array Array of users' and groups' colours for each cached ID - */ - protected $cached_colours = []; - /** * @var array Array of group IDs allowed to be mentioned by current user */ @@ -70,63 +65,6 @@ class mention_helper $this->group_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=group&g={ID}', false); } - /** - * Returns SQL query data for colour SELECT request - * - * @param string $type Name type ('u' for users, 'g' for groups) - * @return array Array of SQL SELECT query data for extracting colours for names - */ - protected function get_colours_sql($type) - { - switch ($type) - { - default: - case 'u': - return [ - 'SELECT' => 'u.user_colour as colour, u.user_id as id', - 'FROM' => [ - USERS_TABLE => 'u', - ], - 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), - ]; - case 'g': - return [ - 'SELECT' => 'g.group_colour as colour, g.group_id as id', - 'FROM' => [ - GROUPS_TABLE => 'g', - ], - ]; - } - } - - /** - * Caches colours of users and groups - */ - protected function cache_colours() - { - if (count($this->cached_colours) > 0) - { - return; - } - - $types = ['u', 'g']; - - foreach ($types as $type) - { - $this->cached_colours[$type] = []; - - $query = $this->db->sql_build_query('SELECT', $this->get_colours_sql($type)); - $result = $this->db->sql_query($query, 300); - - while ($row = $this->db->sql_fetchrow($result)) - { - $this->cached_colours[$type][$row['id']] = $row['colour']; - } - - $this->db->sql_freeresult($result); - } - } - /** * Inject dynamic metadata into MENTION tags in given XML * @@ -140,8 +78,6 @@ class mention_helper 'g' => $this->group_profile_url, ]; - $this->cache_colours(); - return TextFormatterUtils::replaceAttributes( $xml, 'MENTION', @@ -153,11 +89,6 @@ class mention_helper $id = $attributes['id']; $attributes['profile_url'] = str_replace('{ID}', $id, $profile_urls[$type]); - - if (!empty($this->cached_colours[$type][$id])) - { - $attributes['color'] = $this->cached_colours[$type][$id]; - } } return $attributes; diff --git a/tests/text_formatter/s9e/fixtures/mention.xml b/tests/text_formatter/s9e/fixtures/mention.xml index 9174029fd8..13553b1081 100644 --- a/tests/text_formatter/s9e/fixtures/mention.xml +++ b/tests/text_formatter/s9e/fixtures/mention.xml @@ -24,7 +24,7 @@ 3 Hidden group we are a member of 2 - FF0000 + @@ -53,7 +53,7 @@ test 0 0 - 00FF00 + diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index 0cbb978649..0665cd3ce5 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -82,12 +82,10 @@ class mention_helper_test extends phpbb_database_test_case [ '[mention=u:3]test[/mention]', 'mode=viewprofile&u=3', - 'color="00FF00"', ], [ '[mention=g:3]test[/mention]', 'mode=group&g=3', - 'color="FF0000"', ], ]; } @@ -95,11 +93,10 @@ class mention_helper_test extends phpbb_database_test_case /** * @dataProvider inject_metadata_data */ - public function test_inject_metadata($incoming_xml, $expected_profile_substring, $expected_colour) + public function test_inject_metadata($incoming_xml, $expected_profile_substring) { $result = $this->mention_helper->inject_metadata($incoming_xml); $this->assertContains($expected_profile_substring, $result); - $this->assertContains($expected_colour, $result); } public function get_mentioned_user_ids_data() From 5e26380da77a830785cdf41f2052b10073f86edb Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 15 Sep 2018 14:39:25 +0300 Subject: [PATCH 081/113] [ticket/13713] Fix tests (specify user lastvisit) PHPBB3-13713 --- tests/mention/fixtures/mention.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/mention/fixtures/mention.xml b/tests/mention/fixtures/mention.xml index 106398e610..36d6bfab04 100644 --- a/tests/mention/fixtures/mention.xml +++ b/tests/mention/fixtures/mention.xml @@ -83,7 +83,7 @@ myself myself 0 - 0 + 19 @@ -92,7 +92,7 @@ poster poster 0 - 0 + 18 @@ -101,7 +101,7 @@ replier replier 0 - 0 + 17 @@ -110,7 +110,7 @@ team_member_normal team_member_normal 0 - 0 + 16 @@ -119,7 +119,7 @@ team_member_hidden team_member_hidden 0 - 0 + 15 @@ -128,7 +128,7 @@ friend friend 0 - 0 + 14 @@ -137,7 +137,7 @@ test test 0 - 0 + 13 @@ -146,7 +146,7 @@ test1 test1 0 - 0 + 12 @@ -155,7 +155,7 @@ test2 test2 0 - 0 + 11 @@ -164,7 +164,7 @@ test3 test3 0 - 0 + 10 From e769a036b6f05dce81f1ab03f3bf2076dee39421 Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 15 Sep 2018 16:03:34 +0300 Subject: [PATCH 082/113] [ticket/13713] Rework names caching PHPBB3-13713 --- phpBB/phpbb/mention/source/base_group.php | 7 +- phpBB/phpbb/mention/source/base_user.php | 79 +++++++++++++++-------- phpBB/phpbb/mention/source/friend.php | 3 +- phpBB/phpbb/mention/source/group.php | 3 + phpBB/phpbb/mention/source/team.php | 3 + phpBB/phpbb/mention/source/topic.php | 3 +- phpBB/phpbb/mention/source/user.php | 3 +- 7 files changed, 68 insertions(+), 33 deletions(-) diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index c8d498ce81..1f3f063e33 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -36,6 +36,9 @@ abstract class base_group implements source_interface /** @var string */ protected $php_ext; + /** @var string|false */ + protected $cache_ttl = false; + /** @var array Fetched groups' data */ protected $groups = null; @@ -125,8 +128,8 @@ abstract class base_group implements source_interface */ public function get(array &$names, $keyword, $topic_id) { - // Grab all group IDs, cache for 5 minutes - $result = $this->db->sql_query($this->query($keyword, $topic_id), 300); + // Grab all group IDs and cache them if needed + $result = $this->db->sql_query($this->query($keyword, $topic_id), $this->cache_ttl); $group_ids = []; while ($row = $this->db->sql_fetchrow($result)) diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index e002840614..8b6c7a8540 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -30,6 +30,9 @@ abstract class base_user implements source_interface /** @var string */ protected $php_ext; + /** @var string|false */ + protected $cache_ttl = false; + /** * Constructor */ @@ -73,47 +76,67 @@ abstract class base_user implements source_interface $fetched_all = false; $keyword = utf8_clean_string($keyword); - // Grab all necessary user IDs, cache results for 5 minutes - $result = $this->db->sql_query($this->query($keyword, $topic_id), 300); - $i = 0; $users = []; $user_ids = []; - while ($i < $this->config['mention_batch_size']) + + // Grab all necessary user IDs and cache them if needed + if ($this->cache_ttl) { - $row = $this->db->sql_fetchrow($result); + $result = $this->db->sql_query($this->query($keyword, $topic_id), $this->cache_ttl); - if (!$row) + while ($i < $this->config['mention_batch_size']) { - $fetched_all = true; - break; - } + $row = $this->db->sql_fetchrow($result); - if (!empty($keyword) && strpos($row['username_clean'], $keyword) !== 0) - { - continue; - } + if (!$row) + { + $fetched_all = true; + break; + } - $i++; - $users[] = $row; - $user_ids[] = $row['user_id']; - } - - // Determine whether all usernames were fetched in current batch - if (!$fetched_all) - { - $fetched_all = true; - - while ($row = $this->db->sql_fetchrow($result)) - { if (!empty($keyword) && strpos($row['username_clean'], $keyword) !== 0) { continue; } - // At least one username hasn't been fetched - exit loop - $fetched_all = false; - break; + $i++; + $users[] = $row; + $user_ids[] = $row['user_id']; + } + + // Determine whether all usernames were fetched in current batch + if (!$fetched_all) + { + $fetched_all = true; + + while ($row = $this->db->sql_fetchrow($result)) + { + if (!empty($keyword) && strpos($row['username_clean'], $keyword) !== 0) + { + continue; + } + + // At least one username hasn't been fetched - exit loop + $fetched_all = false; + break; + } + } + } + else + { + $result = $this->db->sql_query_limit($this->query($keyword, $topic_id), $this->config['mention_batch_size'], 0); + + while ($row = $this->db->sql_fetchrow($result)) + { + $users[] = $row; + $user_ids[] = $row['user_id']; + } + + // Determine whether all usernames were fetched in current batch + if (count($user_ids) < $this->config['mention_batch_size']) + { + $fetched_all = true; } } diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index f68bea175a..ca16a374b5 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -50,7 +50,8 @@ class friend extends base_user ] ], 'WHERE' => 'z.friend = 1 AND z.user_id = ' . (int) $this->user->data['user_id'] . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), 'ORDER_BY' => 'u.user_lastvisit DESC' ]); return $query; diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index e74948d402..2b13ca7be0 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -15,6 +15,9 @@ namespace phpbb\mention\source; class group extends base_group { + /** @var string|false */ + protected $cache_ttl = 300; + /** * {@inheritdoc} */ diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index 4b40d7f224..14281a85bf 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -15,6 +15,9 @@ namespace phpbb\mention\source; class team extends base_user { + /** @var string|false */ + protected $cache_ttl = 300; + /** * {@inheritdoc} */ diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index 51b855e97b..0c630aa22e 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -61,7 +61,8 @@ class topic extends base_user ], ], 'WHERE' => 'p.topic_id = ' . (int) $topic_id . ' - AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), + AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), 'ORDER_BY' => 'p.post_time DESC' ]); return $query; diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index 57f1a23eab..a85d3d3be4 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -39,7 +39,8 @@ class user extends base_user 'FROM' => [ USERS_TABLE => 'u', ], - 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), + 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), 'ORDER_BY' => 'u.user_lastvisit DESC' ]); return $query; From 6572b735b8bc9ef7c4af8e2b6268f1ff318496dd Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 15 Sep 2018 17:28:38 +0300 Subject: [PATCH 083/113] [ticket/13713] Fix for current avatars PHPBB3-13713 --- phpBB/adm/style/admin.css | 5 +++++ phpBB/styles/prosilver/theme/mentions.css | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 19a277e18d..b7c521a385 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1721,6 +1721,11 @@ fieldset.submit-buttons legend { height: 40px; } +.mention-media-avatar img { /* TODO: reconsider for the future design */ + max-width: 100%; + max-height: 100%; +} + svg { /* TODO: remove it after general normalization */ fill: currentColor; } diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index bb9ffff8d7..3400dde454 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -43,6 +43,11 @@ height: 40px; } +.mention-media-avatar img { /* TODO: reconsider for the future design */ + max-width: 100%; + max-height: 100%; +} + svg { /* TODO: remove it after general normalization */ fill: currentColor; } From d5c9508aed24a7983c92bba861cf2e9b15f9021e Mon Sep 17 00:00:00 2001 From: lavigor Date: Sat, 15 Sep 2018 17:39:17 +0300 Subject: [PATCH 084/113] [ticket/13713] Add an explanation regarding the future avatar ratio PHPBB3-13713 --- phpBB/adm/style/admin.css | 2 +- phpBB/styles/prosilver/theme/mentions.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index b7c521a385..1421c9fa7b 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1721,7 +1721,7 @@ fieldset.submit-buttons legend { height: 40px; } -.mention-media-avatar img { /* TODO: reconsider for the future design */ +.mention-media-avatar img { /* TODO: reconsider for the future design with 1:1 ratio images */ max-width: 100%; max-height: 100%; } diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 3400dde454..e35512f27a 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -43,7 +43,7 @@ height: 40px; } -.mention-media-avatar img { /* TODO: reconsider for the future design */ +.mention-media-avatar img { /* TODO: reconsider for the future design with 1:1 ratio images */ max-width: 100%; max-height: 100%; } From 8083846850e4b76a51e9d6ff605232616fe6e532 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Thu, 3 Jan 2019 22:09:11 +0100 Subject: [PATCH 085/113] [ticket/13713] Add jsdoc docblocks to Mentions PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 44 ++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 5b3725d63f..03f42493a2 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -385,6 +385,22 @@ function getCaretPosition(txtarea) { } (function($) { + 'use strict'; + + /** + * Mentions data returned from ajax requests + * @typedef {Object} MentionsData + * @property {string} name User/group name + * @property {string} id User/group ID + * @property {{img: string, group: string}} avatar Avatar data + * @property {string} rank User rank or empty string for groups + * @property {number} priority Priority of data entry + */ + + /** + * Mentions class + * @constructor + */ function Mentions() { let $mentionDataContainer = $('[data-mention-url]:first'); let mentionURL = $mentionDataContainer.data('mentionUrl'); @@ -396,10 +412,20 @@ function getCaretPosition(txtarea) { let cachedAll = []; let cachedSearchKey = 'name'; + /** + * Get default avatar + * @param {string} type Type of avatar; either group or user on any other value + * @returns {string} Default avatar svg code + */ function defaultAvatar(type) { return (type === 'group') ? '' : ''; } + /** + * Get cached keyword for query string + * @param {string} query Query string + * @returns {?string} Cached keyword if one fits query, else empty string if cached keywords exist, null if cached keywords do not exist + */ function getCachedKeyword(query) { if (!cachedNames) { return null; @@ -417,6 +443,13 @@ function getCaretPosition(txtarea) { return ''; } + /** + * Get names matching query + * @param {string} query Query string + * @param {Object.} items List of {@link MentionsData} items + * @param {string} searchKey Key to use for matching items + * @returns {Object.} List of {@link MentionsData} items filtered with query and by searchKey + */ function getMatchedNames(query, items, searchKey) { let i; let len; @@ -430,6 +463,11 @@ function getCaretPosition(txtarea) { return _results; } + /** + * atwho.js remoteFilter callback filter function + * @param {string} query Query string + * @param {function} callback Callback function for filtered items + */ function remoteFilter(query, callback) { /* * Do not make a new request until the previous one for the same query is returned @@ -448,7 +486,7 @@ function getCaretPosition(txtarea) { /* * Use cached values when we can: * 1) There are some names in the cache relevant for the query - * (cache for the query with the same first characters cointains some data) + * (cache for the query with the same first characters contains some data) * 2) We have enough names to display OR * all relevant names have been fetched from the server */ @@ -475,8 +513,8 @@ function getCaretPosition(txtarea) { return $mentionDataContainer.length; }; - this.handle = function(txtarea) { - $(txtarea).atwho({ + this.handle = function(textarea) { + $(textarea).atwho({ at: "@", acceptSpaceBar: true, displayTpl: function(data) { From 4ed4e336fa50a9370aaec068f8a338b1791b9347 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Thu, 3 Jan 2019 22:19:16 +0100 Subject: [PATCH 086/113] [ticket/13713] Fix compatibility with phpunit 7 PHPBB3-13713 --- tests/mention/controller_test.php | 2 +- tests/notification/submit_post_type_mention_test.php | 2 +- tests/text_formatter/s9e/mention_helper_test.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php index 4294e7365d..f3ed3babd1 100644 --- a/tests/mention/controller_test.php +++ b/tests/mention/controller_test.php @@ -34,7 +34,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/mention.xml'); } - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/notification/submit_post_type_mention_test.php b/tests/notification/submit_post_type_mention_test.php index 642ef83ec3..f660a38ecc 100644 --- a/tests/notification/submit_post_type_mention_test.php +++ b/tests/notification/submit_post_type_mention_test.php @@ -17,7 +17,7 @@ class phpbb_notification_submit_post_type_mention_test extends phpbb_notificatio { protected $item_type = 'notification.type.mention'; - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index 0665cd3ce5..e0293c9747 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -27,7 +27,7 @@ class mention_helper_test extends phpbb_database_test_case return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/mention.xml'); } - public function setUp() + public function setUp(): void { parent::setUp(); From 9905e540131e4e739ac3514682fa0e36f4125e2c Mon Sep 17 00:00:00 2001 From: lavigor Date: Mon, 18 May 2020 03:34:00 +0300 Subject: [PATCH 087/113] [ticket/13713] Fix mention notification update PHPBB3-13713 --- phpBB/includes/functions_posting.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpBB/includes/functions_posting.php b/phpBB/includes/functions_posting.php index 14b50a4a31..cb5cf655bd 100644 --- a/phpBB/includes/functions_posting.php +++ b/phpBB/includes/functions_posting.php @@ -2429,12 +2429,12 @@ function submit_post($mode, $subject, $username, $topic_type, &$poll_ary, &$data if ($user->data['user_id'] == $poster_id) { $phpbb_notifications->update_notifications(array( + 'notification.type.mention', 'notification.type.quote', ), $notification_data); } $phpbb_notifications->update_notifications(array( - 'notification.type.mention', 'notification.type.bookmark', 'notification.type.topic', 'notification.type.post', From e2c50eae68708e5fefa6adfe34fcb8819b1f4b59 Mon Sep 17 00:00:00 2001 From: lavigor Date: Mon, 18 May 2020 03:35:17 +0300 Subject: [PATCH 088/113] [ticket/13713] Avoid same attributes for different types of IDs PHPBB3-13713 --- phpBB/phpbb/textformatter/s9e/factory.php | 4 +- .../textformatter/s9e/mention_helper.php | 39 ++++++++----------- .../s9e/mention_helper_test.php | 8 ++-- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php index 58c1f2f16d..d872a7094c 100644 --- a/phpBB/phpbb/textformatter/s9e/factory.php +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -85,8 +85,10 @@ class factory implements \phpbb\textformatter\cache_interface 'list' => '[LIST type={HASHMAP=1:decimal,a:lower-alpha,A:upper-alpha,i:lower-roman,I:upper-roman;optional;postFilter=#simpletext} #createChild=LI]{TEXT}[/LIST]', 'li' => '[* $tagName=LI]{TEXT}[/*]', 'mention' => - "[MENTION={PARSE=/^(?[ug]):(?\d+)$/} + "[MENTION={PARSE=/^g:(?'group_id'\d+)|u:(?'user_id'\d+)$/} + group_id={UINT;optional} profile_url={URL;optional;postFilter=#false} + user_id={UINT;optional} ]{TEXT}[/MENTION]", 'quote' => "[QUOTE diff --git a/phpBB/phpbb/textformatter/s9e/mention_helper.php b/phpBB/phpbb/textformatter/s9e/mention_helper.php index e7b58f0b81..3094dabb4a 100644 --- a/phpBB/phpbb/textformatter/s9e/mention_helper.php +++ b/phpBB/phpbb/textformatter/s9e/mention_helper.php @@ -33,12 +33,12 @@ class mention_helper protected $user; /** - * @var string Base URL for a user profile link, uses {ID} as placeholder + * @var string Base URL for a user profile link, uses {USER_ID} as placeholder */ protected $user_profile_url; /** - * @var string Base URL for a group profile link, uses {ID} as placeholder + * @var string Base URL for a group profile link, uses {GROUP_ID} as placeholder */ protected $group_profile_url; @@ -61,8 +61,8 @@ class mention_helper $this->db = $db; $this->auth = $auth; $this->user = $user; - $this->user_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={ID}', false); - $this->group_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=group&g={ID}', false); + $this->user_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={USER_ID}', false); + $this->group_profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=group&g={GROUP_ID}', false); } /** @@ -83,12 +83,13 @@ class mention_helper 'MENTION', function ($attributes) use ($profile_urls) { - if (isset($attributes['type']) && isset($attributes['id'])) + if (isset($attributes['user_id'])) { - $type = $attributes['type']; - $id = $attributes['id']; - - $attributes['profile_url'] = str_replace('{ID}', $id, $profile_urls[$type]); + $attributes['profile_url'] = str_replace('{USER_ID}', $attributes['user_id'], $profile_urls['u']); + } + else if (isset($attributes['group_id'])) + { + $attributes['profile_url'] = str_replace('{GROUP_ID}', $attributes['group_id'], $profile_urls['g']); } return $attributes; @@ -186,21 +187,15 @@ class mention_helper return $ids; } - $dom = new \DOMDocument; - $dom->loadXML($xml); - $xpath = new \DOMXPath($dom); + // Add IDs of users mentioned directly + $user_ids = TextFormatterUtils::getAttributeValues($xml, 'MENTION', 'user_id'); + $ids = array_merge($ids, array_map('intval', $user_ids)); - /** @var \DOMElement $mention */ - foreach ($xpath->query('//MENTION') as $mention) + // Add IDs of users mentioned as group members + $group_ids = TextFormatterUtils::getAttributeValues($xml, 'MENTION', 'group_id'); + foreach ($group_ids as $group_id) { - if ($mention->getAttribute('type') === 'u') - { - $ids[] = (int) $mention->getAttribute('id'); - } - else if ($mention->getAttribute('type') === 'g') - { - $this->get_user_ids_for_group($ids, (int) $mention->getAttribute('id')); - } + $this->get_user_ids_for_group($ids, (int) $group_id); } return $ids; diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index e0293c9747..7bd89fa843 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -80,11 +80,11 @@ class mention_helper_test extends phpbb_database_test_case { return [ [ - '[mention=u:3]test[/mention]', + '[mention=u:3]test[/mention]', 'mode=viewprofile&u=3', ], [ - '[mention=g:3]test[/mention]', + '[mention=g:3]test[/mention]', 'mode=group&g=3', ], ]; @@ -103,11 +103,11 @@ class mention_helper_test extends phpbb_database_test_case { return [ [ - '[mention=u:3]test[/mention][mention=u:4]test[/mention][mention=u:5]test[/mention]', + '[mention=u:3]test[/mention][mention=u:4]test[/mention][mention=u:5]test[/mention]', [3, 4, 5], ], [ - '[mention=g:1]test[/mention][mention=g:2]test[/mention][mention=g:3]test[/mention]', + '[mention=g:1]test[/mention][mention=g:2]test[/mention][mention=g:3]test[/mention]', [4, 2, 6], ], ]; From d3e1deb3e537186a96c3d7398461b694b1491316 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Thu, 28 Jan 2021 20:42:56 +0100 Subject: [PATCH 089/113] [ticket/13713] Add missing services in tests PHPBB3-13713 --- tests/mention/controller_test.php | 19 ++++++++++++++++++- .../notification_method_email_test.php | 10 ++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php index f3ed3babd1..74c6a08f0c 100644 --- a/tests/mention/controller_test.php +++ b/tests/mention/controller_test.php @@ -111,7 +111,24 @@ class phpbb_mention_controller_test extends phpbb_database_test_case $phpbb_container->set('cache.driver', $cache_driver); $phpbb_container->set('cache', $cache); $phpbb_container->set('request', $request); - $phpbb_container->set('group_helper', new \phpbb\group\helper($lang)); + $phpbb_container->set('group_helper', new \phpbb\group\helper( + $this->getMockBuilder('\phpbb\auth\auth')->disableOriginalConstructor()->getMock(), + $cache, + $config, + new \phpbb\language\language( + new phpbb\language\language_file_loader($phpbb_root_path, $phpEx) + ), + new phpbb_mock_event_dispatcher(), + new \phpbb\path_helper( + new \phpbb\symfony_request( + new phpbb_mock_request() + ), + $this->getMockBuilder('\phpbb\request\request')->disableOriginalConstructor()->getMock(), + $phpbb_root_path, + $phpEx + ), + $user + )); $phpbb_container->set('text_formatter.utils', new \phpbb\textformatter\s9e\utils()); $phpbb_container->set( 'text_formatter.s9e.mention_helper', diff --git a/tests/notification/notification_method_email_test.php b/tests/notification/notification_method_email_test.php index 87db91aa1c..2c71b0eda9 100644 --- a/tests/notification/notification_method_email_test.php +++ b/tests/notification/notification_method_email_test.php @@ -91,6 +91,16 @@ class notification_method_email_test extends phpbb_tests_notification_base $phpbb_container->setParameter('tables.user_notifications', 'phpbb_user_notifications'); $phpbb_container->setParameter('tables.notification_types', 'phpbb_notification_types'); $phpbb_container->setParameter('tables.notification_emails', 'phpbb_notification_emails'); + $phpbb_container->set( + 'text_formatter.s9e.mention_helper', + new \phpbb\textformatter\s9e\mention_helper( + $this->db, + $auth, + $this->user, + $phpbb_root_path, + $phpEx + ) + ); $this->notification_method_email = $this->getMockBuilder('\phpbb\notification\method\email') ->setConstructorArgs([ From a271d0cacbe33eb4fd65f0802f8fae398fa7c299 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Thu, 28 Jan 2021 20:58:35 +0100 Subject: [PATCH 090/113] [ticket/13713] Fix incorrect parameter to mention_helper constructor PHPBB3-13713 --- tests/notification/submit_post_base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/notification/submit_post_base.php b/tests/notification/submit_post_base.php index 82ea9609c3..c0ef8f3caa 100644 --- a/tests/notification/submit_post_base.php +++ b/tests/notification/submit_post_base.php @@ -140,7 +140,7 @@ abstract class phpbb_notification_submit_post_base extends phpbb_database_test_c new \phpbb\textformatter\s9e\mention_helper( $this->db, $auth, - $this->user, + $user, $phpbb_root_path, $phpEx ) From 640444c8d70f02087fffaba8a62c35204acc2a7b Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Thu, 28 Jan 2021 22:27:26 +0100 Subject: [PATCH 091/113] [ticket/13713] Resolve issues with PHPUnit calls in PHP >= 8.0 PHPBB3-13713 --- tests/mention/controller_test.php | 19 +++++++++---------- .../s9e/mention_helper_test.php | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php index 74c6a08f0c..3b50731791 100644 --- a/tests/mention/controller_test.php +++ b/tests/mention/controller_test.php @@ -668,16 +668,15 @@ class phpbb_mention_controller_test extends phpbb_database_test_case */ public function test_handle($keyword, $topic_id, $expected_result) { - $this->request->expects($this->at(1)) - ->method('variable') - ->with('keyword', '', true) - ->willReturn($keyword) - ; - $this->request->expects($this->at(2)) - ->method('variable') - ->with('topic_id', 0) - ->willReturn($topic_id) - ; + $this->request->expects($this->atLeast(2)) + ->method('variable') + ->withConsecutive( + ['keyword', '', true], + ['topic_id', 0]) + ->willReturnOnConsecutiveCalls( + $keyword, + $topic_id + ); $data = json_decode($this->controller->handle()->getContent(), true); $this->assertEquals($expected_result, $data); } diff --git a/tests/text_formatter/s9e/mention_helper_test.php b/tests/text_formatter/s9e/mention_helper_test.php index 7bd89fa843..ea53557c78 100644 --- a/tests/text_formatter/s9e/mention_helper_test.php +++ b/tests/text_formatter/s9e/mention_helper_test.php @@ -96,7 +96,7 @@ class mention_helper_test extends phpbb_database_test_case public function test_inject_metadata($incoming_xml, $expected_profile_substring) { $result = $this->mention_helper->inject_metadata($incoming_xml); - $this->assertContains($expected_profile_substring, $result); + $this->assertStringContainsString($expected_profile_substring, $result); } public function get_mentioned_user_ids_data() From 1a5e3966e6ea274f0f43a2e37a7213fc3a455e44 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Thu, 11 Mar 2021 16:50:15 +0100 Subject: [PATCH 092/113] [ticket/13713] Start switching over to zurb tribute PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 67 +- phpBB/assets/javascript/jquery.tribute.js | 1898 +++++++++++++++++ .../prosilver/template/posting_buttons.html | 1 + phpBB/styles/prosilver/theme/colours.css | 2 +- phpBB/styles/prosilver/theme/mentions.css | 4 +- 5 files changed, 1960 insertions(+), 12 deletions(-) create mode 100644 phpBB/assets/javascript/jquery.tribute.js diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 03f42493a2..ea06496ed2 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -383,6 +383,7 @@ function getCaretPosition(txtarea) { return caretPos; } +/* import Tribute from './jquery.tribute'; */ (function($) { 'use strict'; @@ -411,6 +412,7 @@ function getCaretPosition(txtarea) { let cachedNames = []; let cachedAll = []; let cachedSearchKey = 'name'; + let tribute = null; /** * Get default avatar @@ -418,7 +420,11 @@ function getCaretPosition(txtarea) { * @returns {string} Default avatar svg code */ function defaultAvatar(type) { - return (type === 'group') ? '' : ''; + if (type === 'group') { + return ''; + } else { + return ''; + } } /** @@ -452,15 +458,19 @@ function getCaretPosition(txtarea) { */ function getMatchedNames(query, items, searchKey) { let i; - let len; - let _results = []; - for (i = 0, len = items.length; i < len; i++) { + let itemsLength; + let matchedNames = []; + for (i = 0, itemsLength = items.length; i < itemsLength; i++) { let item = items[i]; - if (String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()) === 0) { - _results.push(item); + if (isItemMatched(query, item, searchKey)) { + matchedNames.push(item); } } - return _results; + return matchedNames; + } + + function isItemMatched(query, item, searchKey) { + return String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()) === 0; } /** @@ -481,7 +491,7 @@ function getCaretPosition(txtarea) { } let cachedKeyword = getCachedKeyword(query), - cachedNamesForQuery = (cachedKeyword != null) ? cachedNames[cachedKeyword] : null; + cachedNamesForQuery = (cachedKeyword !== null) ? cachedNames[cachedKeyword] : null; /* * Use cached values when we can: @@ -514,6 +524,45 @@ function getCaretPosition(txtarea) { }; this.handle = function(textarea) { + tribute = new Tribute({ + trigger: '@', + allowSpaces: true, + containerClass: 'mention-container', + selectClass: 'cur', + itemClass: 'mention-item', + menuItemTemplate: function (data) { + const itemData = data.original; + let avatar = (itemData.avatar.img) ? "" + itemData.avatar.img + "" : defaultAvatar(itemData.avatar.type), + rank = (itemData.rank) ? "" + itemData.rank + "" : ''; + return "" + avatar + "" + itemData.name + rank + ""; + }, + selectTemplate: function (item) { + return '[mention=' + item.original.type + ':' + item.original.id + ']' + item.original.name + '[/mention]'; + }, + menuItemLimit: mentionNamesLimit, + values: function (text, cb) { + remoteFilter(text, users => cb(users)); + }, + noMatchTemplate: function (t) { + console.log('No match:'); + console.log(t); + }, + lookup: function (element, mentionText) { + return element.hasOwnProperty('name') ? element.name : ''; + } + }); + + tribute.attach($(textarea)); + + /* + var tribute = new Tribute({ + values: [ + { key: "Phil Heartman", value: "pheartman" }, + { key: "Gordon Ramsey", value: "gramsey" } + ] + }); + */ +/* $(textarea).atwho({ at: "@", acceptSpaceBar: true, @@ -600,6 +649,7 @@ function getCaretPosition(txtarea) { } } }); + */ }; } phpbb.mentions = new Mentions(); @@ -642,4 +692,3 @@ function getCaretPosition(txtarea) { }); }); })(jQuery); - diff --git a/phpBB/assets/javascript/jquery.tribute.js b/phpBB/assets/javascript/jquery.tribute.js new file mode 100644 index 0000000000..9c46e63577 --- /dev/null +++ b/phpBB/assets/javascript/jquery.tribute.js @@ -0,0 +1,1898 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.Tribute = factory()); +}(this, (function () { 'use strict'; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); + } + + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; + } + + function _iterableToArrayLimit(arr, i) { + if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(n); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); + } + + function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; + } + + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + if (!Array.prototype.find) { + Array.prototype.find = function (predicate) { + if (this === null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } + + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + + var list = Object(this); + var length = list.length >>> 0; + var thisArg = arguments[1]; + var value; + + for (var i = 0; i < length; i++) { + value = list[i]; + + if (predicate.call(thisArg, value, i, list)) { + return value; + } + } + + return undefined; + }; + } + + if (window && typeof window.CustomEvent !== "function") { + var CustomEvent$1 = function CustomEvent(event, params) { + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + }; + + if (typeof window.Event !== 'undefined') { + CustomEvent$1.prototype = window.Event.prototype; + } + + window.CustomEvent = CustomEvent$1; + } + + var TributeEvents = /*#__PURE__*/function () { + function TributeEvents(tribute) { + _classCallCheck(this, TributeEvents); + + this.tribute = tribute; + this.tribute.events = this; + } + + _createClass(TributeEvents, [{ + key: "bind", + value: function bind(element) { + element.boundKeydown = this.keydown.bind(element, this); + element.boundKeyup = this.keyup.bind(element, this); + element.boundInput = this.input.bind(element, this); + element.addEventListener("keydown", element.boundKeydown, false); + element.addEventListener("keyup", element.boundKeyup, false); + element.addEventListener("input", element.boundInput, false); + } + }, { + key: "unbind", + value: function unbind(element) { + element.removeEventListener("keydown", element.boundKeydown, false); + element.removeEventListener("keyup", element.boundKeyup, false); + element.removeEventListener("input", element.boundInput, false); + delete element.boundKeydown; + delete element.boundKeyup; + delete element.boundInput; + } + }, { + key: "keydown", + value: function keydown(instance, event) { + if (instance.shouldDeactivate(event)) { + instance.tribute.isActive = false; + instance.tribute.hideMenu(); + } + + var element = this; + instance.commandEvent = false; + TributeEvents.keys().forEach(function (o) { + if (o.key === event.keyCode) { + instance.commandEvent = true; + instance.callbacks()[o.value.toLowerCase()](event, element); + } + }); + } + }, { + key: "input", + value: function input(instance, event) { + instance.inputEvent = true; + instance.keyup.call(this, instance, event); + } + }, { + key: "click", + value: function click(instance, event) { + var tribute = instance.tribute; + + if (tribute.menu && tribute.menu.contains(event.target)) { + var li = event.target; + event.preventDefault(); + event.stopPropagation(); + + while (li.nodeName.toLowerCase() !== "li") { + li = li.parentNode; + + if (!li || li === tribute.menu) { + throw new Error("cannot find the
  • container for the click"); + } + } + + tribute.selectItemAtIndex(li.getAttribute("data-index"), event); + tribute.hideMenu(); // TODO: should fire with externalTrigger and target is outside of menu + } else if (tribute.current.element && !tribute.current.externalTrigger) { + tribute.current.externalTrigger = false; + setTimeout(function () { + return tribute.hideMenu(); + }); + } + } + }, { + key: "keyup", + value: function keyup(instance, event) { + if (instance.inputEvent) { + instance.inputEvent = false; + } + + instance.updateSelection(this); + if (event.keyCode === 27) return; + + if (!instance.tribute.allowSpaces && instance.tribute.hasTrailingSpace) { + instance.tribute.hasTrailingSpace = false; + instance.commandEvent = true; + instance.callbacks()["space"](event, this); + return; + } + + if (!instance.tribute.isActive) { + if (instance.tribute.autocompleteMode) { + instance.callbacks().triggerChar(event, this, ""); + } else { + var keyCode = instance.getKeyCode(instance, this, event); + if (isNaN(keyCode) || !keyCode) return; + var trigger = instance.tribute.triggers().find(function (trigger) { + return trigger.charCodeAt(0) === keyCode; + }); + + if (typeof trigger !== "undefined") { + instance.callbacks().triggerChar(event, this, trigger); + } + } + } + + if (instance.tribute.current.mentionText.length < instance.tribute.current.collection.menuShowMinLength) { + return; + } + + if ((instance.tribute.current.trigger || instance.tribute.autocompleteMode) && instance.commandEvent === false || instance.tribute.isActive && event.keyCode === 8) { + instance.tribute.showMenuFor(this, true); + } + } + }, { + key: "shouldDeactivate", + value: function shouldDeactivate(event) { + if (!this.tribute.isActive) return false; + + if (this.tribute.current.mentionText.length === 0) { + var eventKeyPressed = false; + TributeEvents.keys().forEach(function (o) { + if (event.keyCode === o.key) eventKeyPressed = true; + }); + return !eventKeyPressed; + } + + return false; + } + }, { + key: "getKeyCode", + value: function getKeyCode(instance, el, event) { + + var tribute = instance.tribute; + var info = tribute.range.getTriggerInfo(false, tribute.hasTrailingSpace, true, tribute.allowSpaces, tribute.autocompleteMode); + + if (info) { + return info.mentionTriggerChar.charCodeAt(0); + } else { + return false; + } + } + }, { + key: "updateSelection", + value: function updateSelection(el) { + this.tribute.current.element = el; + var info = this.tribute.range.getTriggerInfo(false, this.tribute.hasTrailingSpace, true, this.tribute.allowSpaces, this.tribute.autocompleteMode); + + if (info) { + this.tribute.current.selectedPath = info.mentionSelectedPath; + this.tribute.current.mentionText = info.mentionText; + this.tribute.current.selectedOffset = info.mentionSelectedOffset; + } + } + }, { + key: "callbacks", + value: function callbacks() { + var _this = this; + + return { + triggerChar: function triggerChar(e, el, trigger) { + var tribute = _this.tribute; + tribute.current.trigger = trigger; + var collectionItem = tribute.collection.find(function (item) { + return item.trigger === trigger; + }); + tribute.current.collection = collectionItem; + + if (tribute.current.mentionText.length >= tribute.current.collection.menuShowMinLength && tribute.inputEvent) { + tribute.showMenuFor(el, true); + } + }, + enter: function enter(e, el) { + // choose selection + if (_this.tribute.isActive && _this.tribute.current.filteredItems) { + e.preventDefault(); + e.stopPropagation(); + setTimeout(function () { + _this.tribute.selectItemAtIndex(_this.tribute.menuSelected, e); + + _this.tribute.hideMenu(); + }, 0); + } + }, + escape: function escape(e, el) { + if (_this.tribute.isActive) { + e.preventDefault(); + e.stopPropagation(); + _this.tribute.isActive = false; + + _this.tribute.hideMenu(); + } + }, + tab: function tab(e, el) { + // choose first match + _this.callbacks().enter(e, el); + }, + space: function space(e, el) { + if (_this.tribute.isActive) { + if (_this.tribute.spaceSelectsMatch) { + _this.callbacks().enter(e, el); + } else if (!_this.tribute.allowSpaces) { + e.stopPropagation(); + setTimeout(function () { + _this.tribute.hideMenu(); + + _this.tribute.isActive = false; + }, 0); + } + } + }, + up: function up(e, el) { + // navigate up ul + if (_this.tribute.isActive && _this.tribute.current.filteredItems) { + e.preventDefault(); + e.stopPropagation(); + var count = _this.tribute.current.filteredItems.length, + selected = _this.tribute.menuSelected; + + if (count > selected && selected > 0) { + _this.tribute.menuSelected--; + + _this.setActiveLi(); + } else if (selected === 0) { + _this.tribute.menuSelected = count - 1; + + _this.setActiveLi(); + + _this.tribute.menu.scrollTop = _this.tribute.menu.scrollHeight; + } + } + }, + down: function down(e, el) { + // navigate down ul + if (_this.tribute.isActive && _this.tribute.current.filteredItems) { + e.preventDefault(); + e.stopPropagation(); + var count = _this.tribute.current.filteredItems.length - 1, + selected = _this.tribute.menuSelected; + + if (count > selected) { + _this.tribute.menuSelected++; + + _this.setActiveLi(); + } else if (count === selected) { + _this.tribute.menuSelected = 0; + + _this.setActiveLi(); + + _this.tribute.menu.scrollTop = 0; + } + } + }, + "delete": function _delete(e, el) { + if (_this.tribute.isActive && _this.tribute.current.mentionText.length < 1) { + _this.tribute.hideMenu(); + } else if (_this.tribute.isActive) { + _this.tribute.showMenuFor(el); + } + } + }; + } + }, { + key: "setActiveLi", + value: function setActiveLi(index) { + var lis = this.tribute.menu.querySelectorAll("li"), + length = lis.length >>> 0; + if (index) this.tribute.menuSelected = parseInt(index); + + for (var i = 0; i < length; i++) { + var li = lis[i]; + + if (i === this.tribute.menuSelected) { + li.classList.add(this.tribute.current.collection.selectClass); + var liClientRect = li.getBoundingClientRect(); + var menuClientRect = this.tribute.menu.getBoundingClientRect(); + + if (liClientRect.bottom > menuClientRect.bottom) { + var scrollDistance = liClientRect.bottom - menuClientRect.bottom; + this.tribute.menu.scrollTop += scrollDistance; + } else if (liClientRect.top < menuClientRect.top) { + var _scrollDistance = menuClientRect.top - liClientRect.top; + + this.tribute.menu.scrollTop -= _scrollDistance; + } + } else { + li.classList.remove(this.tribute.current.collection.selectClass); + } + } + } + }, { + key: "getFullHeight", + value: function getFullHeight(elem, includeMargin) { + var height = elem.getBoundingClientRect().height; + + if (includeMargin) { + var style = elem.currentStyle || window.getComputedStyle(elem); + return height + parseFloat(style.marginTop) + parseFloat(style.marginBottom); + } + + return height; + } + }], [{ + key: "keys", + value: function keys() { + return [{ + key: 9, + value: "TAB" + }, { + key: 8, + value: "DELETE" + }, { + key: 13, + value: "ENTER" + }, { + key: 27, + value: "ESCAPE" + }, { + key: 32, + value: "SPACE" + }, { + key: 38, + value: "UP" + }, { + key: 40, + value: "DOWN" + }]; + } + }]); + + return TributeEvents; + }(); + + var TributeMenuEvents = /*#__PURE__*/function () { + function TributeMenuEvents(tribute) { + _classCallCheck(this, TributeMenuEvents); + + this.tribute = tribute; + this.tribute.menuEvents = this; + this.menu = this.tribute.menu; + } + + _createClass(TributeMenuEvents, [{ + key: "bind", + value: function bind(menu) { + var _this = this; + + this.menuClickEvent = this.tribute.events.click.bind(null, this); + this.menuContainerScrollEvent = this.debounce(function () { + if (_this.tribute.isActive) { + _this.tribute.showMenuFor(_this.tribute.current.element, false); + } + }, 300, false); + this.windowResizeEvent = this.debounce(function () { + if (_this.tribute.isActive) { + _this.tribute.range.positionMenuAtCaret(true); + } + }, 300, false); // fixes IE11 issues with mousedown + + this.tribute.range.getDocument().addEventListener("MSPointerDown", this.menuClickEvent, false); + this.tribute.range.getDocument().addEventListener("mousedown", this.menuClickEvent, false); + window.addEventListener("resize", this.windowResizeEvent); + + if (this.menuContainer) { + this.menuContainer.addEventListener("scroll", this.menuContainerScrollEvent, false); + } else { + window.addEventListener("scroll", this.menuContainerScrollEvent); + } + } + }, { + key: "unbind", + value: function unbind(menu) { + this.tribute.range.getDocument().removeEventListener("mousedown", this.menuClickEvent, false); + this.tribute.range.getDocument().removeEventListener("MSPointerDown", this.menuClickEvent, false); + window.removeEventListener("resize", this.windowResizeEvent); + + if (this.menuContainer) { + this.menuContainer.removeEventListener("scroll", this.menuContainerScrollEvent, false); + } else { + window.removeEventListener("scroll", this.menuContainerScrollEvent); + } + } + }, { + key: "debounce", + value: function debounce(func, wait, immediate) { + var _arguments = arguments, + _this2 = this; + + var timeout; + return function () { + var context = _this2, + args = _arguments; + + var later = function later() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + } + }]); + + return TributeMenuEvents; + }(); + + var TributeRange = /*#__PURE__*/function () { + function TributeRange(tribute) { + _classCallCheck(this, TributeRange); + + this.tribute = tribute; + this.tribute.range = this; + } + + _createClass(TributeRange, [{ + key: "getDocument", + value: function getDocument() { + var iframe; + + if (this.tribute.current.collection) { + iframe = this.tribute.current.collection.iframe; + } + + if (!iframe) { + return document; + } + + return iframe.contentWindow.document; + } + }, { + key: "positionMenuAtCaret", + value: function positionMenuAtCaret(scrollTo) { + var _this = this; + + var context = this.tribute.current, + coordinates; + var info = this.getTriggerInfo(false, this.tribute.hasTrailingSpace, true, this.tribute.allowSpaces, this.tribute.autocompleteMode); + + if (typeof info !== 'undefined') { + if (!this.tribute.positionMenu) { + this.tribute.menu.style.cssText = "display: block;"; + return; + } + + if (!this.isContentEditable(context.element)) { + coordinates = this.getTextAreaOrInputUnderlinePosition(this.tribute.current.element, info.mentionPosition); + } else { + coordinates = this.getContentEditableCaretPosition(info.mentionPosition); + } + + this.tribute.menu.style.cssText = "top: ".concat(coordinates.top, "px;\n left: ").concat(coordinates.left, "px;\n right: ").concat(coordinates.right, "px;\n bottom: ").concat(coordinates.bottom, "px;\n position: absolute;\n display: block;"); + + if (coordinates.left === 'auto') { + this.tribute.menu.style.left = 'auto'; + } + + if (coordinates.top === 'auto') { + this.tribute.menu.style.top = 'auto'; + } + + if (scrollTo) this.scrollIntoView(); + window.setTimeout(function () { + var menuDimensions = { + width: _this.tribute.menu.offsetWidth, + height: _this.tribute.menu.offsetHeight + }; + + var menuIsOffScreen = _this.isMenuOffScreen(coordinates, menuDimensions); + + var menuIsOffScreenHorizontally = window.innerWidth > menuDimensions.width && (menuIsOffScreen.left || menuIsOffScreen.right); + var menuIsOffScreenVertically = window.innerHeight > menuDimensions.height && (menuIsOffScreen.top || menuIsOffScreen.bottom); + + if (menuIsOffScreenHorizontally || menuIsOffScreenVertically) { + _this.tribute.menu.style.cssText = 'display: none'; + + _this.positionMenuAtCaret(scrollTo); + } + }, 0); + } else { + this.tribute.menu.style.cssText = 'display: none'; + } + } + }, { + key: "selectElement", + value: function selectElement(targetElement, path, offset) { + var range; + var elem = targetElement; + + if (path) { + for (var i = 0; i < path.length; i++) { + elem = elem.childNodes[path[i]]; + + if (elem === undefined) { + return; + } + + while (elem.length < offset) { + offset -= elem.length; + elem = elem.nextSibling; + } + + if (elem.childNodes.length === 0 && !elem.length) { + elem = elem.previousSibling; + } + } + } + + var sel = this.getWindowSelection(); + range = this.getDocument().createRange(); + range.setStart(elem, offset); + range.setEnd(elem, offset); + range.collapse(true); + + try { + sel.removeAllRanges(); + } catch (error) {} + + sel.addRange(range); + targetElement.focus(); + } + }, { + key: "replaceTriggerText", + value: function replaceTriggerText(text, requireLeadingSpace, hasTrailingSpace, originalEvent, item) { + var info = this.getTriggerInfo(true, hasTrailingSpace, requireLeadingSpace, this.tribute.allowSpaces, this.tribute.autocompleteMode); + + if (info !== undefined) { + var context = this.tribute.current; + var replaceEvent = new CustomEvent('tribute-replaced', { + detail: { + item: item, + instance: context, + context: info, + event: originalEvent + } + }); + + if (!this.isContentEditable(context.element)) { + var myField = this.tribute.current.element; + var textSuffix = typeof this.tribute.replaceTextSuffix == 'string' ? this.tribute.replaceTextSuffix : ' '; + text += textSuffix; + var startPos = info.mentionPosition; + var endPos = info.mentionPosition + info.mentionText.length + textSuffix.length; + + if (!this.tribute.autocompleteMode) { + endPos += info.mentionTriggerChar.length - 1; + } + + myField.value = myField.value.substring(0, startPos) + text + myField.value.substring(endPos, myField.value.length); + myField.selectionStart = startPos + text.length; + myField.selectionEnd = startPos + text.length; + } else { + // add a space to the end of the pasted text + var _textSuffix = typeof this.tribute.replaceTextSuffix == 'string' ? this.tribute.replaceTextSuffix : '\xA0'; + + text += _textSuffix; + + var _endPos = info.mentionPosition + info.mentionText.length; + + if (!this.tribute.autocompleteMode) { + _endPos += info.mentionTriggerChar.length; + } + + this.pasteHtml(text, info.mentionPosition, _endPos); + } + + context.element.dispatchEvent(new CustomEvent('input', { + bubbles: true + })); + context.element.dispatchEvent(replaceEvent); + } + } + }, { + key: "pasteHtml", + value: function pasteHtml(html, startPos, endPos) { + var range, sel; + sel = this.getWindowSelection(); + range = this.getDocument().createRange(); + range.setStart(sel.anchorNode, startPos); + range.setEnd(sel.anchorNode, endPos); + range.deleteContents(); + var el = this.getDocument().createElement('div'); + el.innerHTML = html; + var frag = this.getDocument().createDocumentFragment(), + node, + lastNode; + + while (node = el.firstChild) { + lastNode = frag.appendChild(node); + } + + range.insertNode(frag); // Preserve the selection + + if (lastNode) { + range = range.cloneRange(); + range.setStartAfter(lastNode); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + } + } + }, { + key: "getWindowSelection", + value: function getWindowSelection() { + if (this.tribute.collection.iframe) { + return this.tribute.collection.iframe.contentWindow.getSelection(); + } + + return window.getSelection(); + } + }, { + key: "getNodePositionInParent", + value: function getNodePositionInParent(element) { + if (element.parentNode === null) { + return 0; + } + + for (var i = 0; i < element.parentNode.childNodes.length; i++) { + var node = element.parentNode.childNodes[i]; + + if (node === element) { + return i; + } + } + } + }, { + key: "getContentEditableSelectedPath", + value: function getContentEditableSelectedPath(ctx) { + var sel = this.getWindowSelection(); + var selected = sel.anchorNode; + var path = []; + var offset; + + if (selected != null) { + var i; + var ce = selected.contentEditable; + + while (selected !== null && ce !== 'true') { + i = this.getNodePositionInParent(selected); + path.push(i); + selected = selected.parentNode; + + if (selected !== null) { + ce = selected.contentEditable; + } + } + + path.reverse(); // getRangeAt may not exist, need alternative + + offset = sel.getRangeAt(0).startOffset; + return { + selected: selected, + path: path, + offset: offset + }; + } + } + }, { + key: "getTextPrecedingCurrentSelection", + value: function getTextPrecedingCurrentSelection() { + var context = this.tribute.current, + text = ''; + + if (!this.isContentEditable(context.element)) { + var textComponent = this.tribute.current.element; + + if (textComponent) { + var startPos = textComponent.selectionStart; + + if (textComponent.value && startPos >= 0) { + text = textComponent.value.substring(0, startPos); + } + } + } else { + var selectedElem = this.getWindowSelection().anchorNode; + + if (selectedElem != null) { + var workingNodeContent = selectedElem.textContent; + var selectStartOffset = this.getWindowSelection().getRangeAt(0).startOffset; + + if (workingNodeContent && selectStartOffset >= 0) { + text = workingNodeContent.substring(0, selectStartOffset); + } + } + } + + return text; + } + }, { + key: "getLastWordInText", + value: function getLastWordInText(text) { + text = text.replace(/\u00A0/g, ' '); // https://stackoverflow.com/questions/29850407/how-do-i-replace-unicode-character-u00a0-with-a-space-in-javascript + + var wordsArray; + + if (this.tribute.autocompleteSeparator) { + wordsArray = text.split(this.tribute.autocompleteSeparator); + } else { + wordsArray = text.split(/\s+/); + } + + var worldsCount = wordsArray.length - 1; + return wordsArray[worldsCount].trim(); + } + }, { + key: "getTriggerInfo", + value: function getTriggerInfo(menuAlreadyActive, hasTrailingSpace, requireLeadingSpace, allowSpaces, isAutocomplete) { + var _this2 = this; + + var ctx = this.tribute.current; + var selected, path, offset; + + if (!this.isContentEditable(ctx.element)) { + selected = this.tribute.current.element; + } else { + var selectionInfo = this.getContentEditableSelectedPath(ctx); + + if (selectionInfo) { + selected = selectionInfo.selected; + path = selectionInfo.path; + offset = selectionInfo.offset; + } + } + + var effectiveRange = this.getTextPrecedingCurrentSelection(); + var lastWordOfEffectiveRange = this.getLastWordInText(effectiveRange); + + if (isAutocomplete) { + return { + mentionPosition: effectiveRange.length - lastWordOfEffectiveRange.length, + mentionText: lastWordOfEffectiveRange, + mentionSelectedElement: selected, + mentionSelectedPath: path, + mentionSelectedOffset: offset + }; + } + + if (effectiveRange !== undefined && effectiveRange !== null) { + var mostRecentTriggerCharPos = -1; + var triggerChar; + this.tribute.collection.forEach(function (config) { + var c = config.trigger; + var idx = config.requireLeadingSpace ? _this2.lastIndexWithLeadingSpace(effectiveRange, c) : effectiveRange.lastIndexOf(c); + + if (idx > mostRecentTriggerCharPos) { + mostRecentTriggerCharPos = idx; + triggerChar = c; + requireLeadingSpace = config.requireLeadingSpace; + } + }); + + if (mostRecentTriggerCharPos >= 0 && (mostRecentTriggerCharPos === 0 || !requireLeadingSpace || /[\xA0\s]/g.test(effectiveRange.substring(mostRecentTriggerCharPos - 1, mostRecentTriggerCharPos)))) { + var currentTriggerSnippet = effectiveRange.substring(mostRecentTriggerCharPos + triggerChar.length, effectiveRange.length); + triggerChar = effectiveRange.substring(mostRecentTriggerCharPos, mostRecentTriggerCharPos + triggerChar.length); + var firstSnippetChar = currentTriggerSnippet.substring(0, 1); + var leadingSpace = currentTriggerSnippet.length > 0 && (firstSnippetChar === ' ' || firstSnippetChar === '\xA0'); + + if (hasTrailingSpace) { + currentTriggerSnippet = currentTriggerSnippet.trim(); + } + + var regex = allowSpaces ? /[^\S ]/g : /[\xA0\s]/g; + this.tribute.hasTrailingSpace = regex.test(currentTriggerSnippet); + + if (!leadingSpace && (menuAlreadyActive || !regex.test(currentTriggerSnippet))) { + return { + mentionPosition: mostRecentTriggerCharPos, + mentionText: currentTriggerSnippet, + mentionSelectedElement: selected, + mentionSelectedPath: path, + mentionSelectedOffset: offset, + mentionTriggerChar: triggerChar + }; + } + } + } + } + }, { + key: "lastIndexWithLeadingSpace", + value: function lastIndexWithLeadingSpace(str, trigger) { + var reversedStr = str.split('').reverse().join(''); + var index = -1; + + for (var cidx = 0, len = str.length; cidx < len; cidx++) { + var firstChar = cidx === str.length - 1; + var leadingSpace = /\s/.test(reversedStr[cidx + 1]); + var match = true; + + for (var triggerIdx = trigger.length - 1; triggerIdx >= 0; triggerIdx--) { + if (trigger[triggerIdx] !== reversedStr[cidx - triggerIdx]) { + match = false; + break; + } + } + + if (match && (firstChar || leadingSpace)) { + index = str.length - 1 - cidx; + break; + } + } + + return index; + } + }, { + key: "isContentEditable", + value: function isContentEditable(element) { + return element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA'; + } + }, { + key: "isMenuOffScreen", + value: function isMenuOffScreen(coordinates, menuDimensions) { + var windowWidth = window.innerWidth; + var windowHeight = window.innerHeight; + var doc = document.documentElement; + var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + var menuTop = typeof coordinates.top === 'number' ? coordinates.top : windowTop + windowHeight - coordinates.bottom - menuDimensions.height; + var menuRight = typeof coordinates.right === 'number' ? coordinates.right : coordinates.left + menuDimensions.width; + var menuBottom = typeof coordinates.bottom === 'number' ? coordinates.bottom : coordinates.top + menuDimensions.height; + var menuLeft = typeof coordinates.left === 'number' ? coordinates.left : windowLeft + windowWidth - coordinates.right - menuDimensions.width; + return { + top: menuTop < Math.floor(windowTop), + right: menuRight > Math.ceil(windowLeft + windowWidth), + bottom: menuBottom > Math.ceil(windowTop + windowHeight), + left: menuLeft < Math.floor(windowLeft) + }; + } + }, { + key: "getMenuDimensions", + value: function getMenuDimensions() { + // Width of the menu depends of its contents and position + // We must check what its width would be without any obstruction + // This way, we can achieve good positioning for flipping the menu + var dimensions = { + width: null, + height: null + }; + this.tribute.menu.style.cssText = "top: 0px;\n left: 0px;\n position: fixed;\n display: block;\n visibility; hidden;"; + dimensions.width = this.tribute.menu.offsetWidth; + dimensions.height = this.tribute.menu.offsetHeight; + this.tribute.menu.style.cssText = "display: none;"; + return dimensions; + } + }, { + key: "getTextAreaOrInputUnderlinePosition", + value: function getTextAreaOrInputUnderlinePosition(element, position, flipped) { + var properties = ['direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', 'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing']; + var isFirefox = window.mozInnerScreenX !== null; + var div = this.getDocument().createElement('div'); + div.id = 'input-textarea-caret-position-mirror-div'; + this.getDocument().body.appendChild(div); + var style = div.style; + var computed = window.getComputedStyle ? getComputedStyle(element) : element.currentStyle; + style.whiteSpace = 'pre-wrap'; + + if (element.nodeName !== 'INPUT') { + style.wordWrap = 'break-word'; + } // position off-screen + + + style.position = 'absolute'; + style.visibility = 'hidden'; // transfer the element's properties to the div + + properties.forEach(function (prop) { + style[prop] = computed[prop]; + }); + + if (isFirefox) { + style.width = "".concat(parseInt(computed.width) - 2, "px"); + if (element.scrollHeight > parseInt(computed.height)) style.overflowY = 'scroll'; + } else { + style.overflow = 'hidden'; + } + + div.textContent = element.value.substring(0, position); + + if (element.nodeName === 'INPUT') { + div.textContent = div.textContent.replace(/\s/g, ' '); + } + + var span = this.getDocument().createElement('span'); + span.textContent = element.value.substring(position) || '.'; + div.appendChild(span); + var rect = element.getBoundingClientRect(); + var doc = document.documentElement; + var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + var top = 0; + var left = 0; + + if (this.menuContainerIsBody) { + top = rect.top; + left = rect.left; + } + + var coordinates = { + top: top + windowTop + span.offsetTop + parseInt(computed.borderTopWidth) + parseInt(computed.fontSize) - element.scrollTop, + left: left + windowLeft + span.offsetLeft + parseInt(computed.borderLeftWidth) + }; + var windowWidth = window.innerWidth; + var windowHeight = window.innerHeight; + var menuDimensions = this.getMenuDimensions(); + var menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); + + if (menuIsOffScreen.right) { + coordinates.right = windowWidth - coordinates.left; + coordinates.left = 'auto'; + } + + var parentHeight = this.tribute.menuContainer ? this.tribute.menuContainer.offsetHeight : this.getDocument().body.offsetHeight; + + if (menuIsOffScreen.bottom) { + var parentRect = this.tribute.menuContainer ? this.tribute.menuContainer.getBoundingClientRect() : this.getDocument().body.getBoundingClientRect(); + var scrollStillAvailable = parentHeight - (windowHeight - parentRect.top); + coordinates.bottom = scrollStillAvailable + (windowHeight - rect.top - span.offsetTop); + coordinates.top = 'auto'; + } + + menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); + + if (menuIsOffScreen.left) { + coordinates.left = windowWidth > menuDimensions.width ? windowLeft + windowWidth - menuDimensions.width : windowLeft; + delete coordinates.right; + } + + if (menuIsOffScreen.top) { + coordinates.top = windowHeight > menuDimensions.height ? windowTop + windowHeight - menuDimensions.height : windowTop; + delete coordinates.bottom; + } + + this.getDocument().body.removeChild(div); + return coordinates; + } + }, { + key: "getContentEditableCaretPosition", + value: function getContentEditableCaretPosition(selectedNodePosition) { + var range; + var sel = this.getWindowSelection(); + range = this.getDocument().createRange(); + range.setStart(sel.anchorNode, selectedNodePosition); + range.setEnd(sel.anchorNode, selectedNodePosition); + range.collapse(false); + var rect = range.getBoundingClientRect(); + var doc = document.documentElement; + var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + var left = rect.left; + var top = rect.top; + var coordinates = { + left: left + windowLeft, + top: top + rect.height + windowTop + }; + var windowWidth = window.innerWidth; + var windowHeight = window.innerHeight; + var menuDimensions = this.getMenuDimensions(); + var menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); + + if (menuIsOffScreen.right) { + coordinates.left = 'auto'; + coordinates.right = windowWidth - rect.left - windowLeft; + } + + var parentHeight = this.tribute.menuContainer ? this.tribute.menuContainer.offsetHeight : this.getDocument().body.offsetHeight; + + if (menuIsOffScreen.bottom) { + var parentRect = this.tribute.menuContainer ? this.tribute.menuContainer.getBoundingClientRect() : this.getDocument().body.getBoundingClientRect(); + var scrollStillAvailable = parentHeight - (windowHeight - parentRect.top); + coordinates.top = 'auto'; + coordinates.bottom = scrollStillAvailable + (windowHeight - rect.top); + } + + menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); + + if (menuIsOffScreen.left) { + coordinates.left = windowWidth > menuDimensions.width ? windowLeft + windowWidth - menuDimensions.width : windowLeft; + delete coordinates.right; + } + + if (menuIsOffScreen.top) { + coordinates.top = windowHeight > menuDimensions.height ? windowTop + windowHeight - menuDimensions.height : windowTop; + delete coordinates.bottom; + } + + if (!this.menuContainerIsBody) { + coordinates.left = coordinates.left ? coordinates.left - this.tribute.menuContainer.offsetLeft : coordinates.left; + coordinates.top = coordinates.top ? coordinates.top - this.tribute.menuContainer.offsetTop : coordinates.top; + } + + return coordinates; + } + }, { + key: "scrollIntoView", + value: function scrollIntoView(elem) { + var reasonableBuffer = 20, + clientRect; + var maxScrollDisplacement = 100; + var e = this.menu; + if (typeof e === 'undefined') return; + + while (clientRect === undefined || clientRect.height === 0) { + clientRect = e.getBoundingClientRect(); + + if (clientRect.height === 0) { + e = e.childNodes[0]; + + if (e === undefined || !e.getBoundingClientRect) { + return; + } + } + } + + var elemTop = clientRect.top; + var elemBottom = elemTop + clientRect.height; + + if (elemTop < 0) { + window.scrollTo(0, window.pageYOffset + clientRect.top - reasonableBuffer); + } else if (elemBottom > window.innerHeight) { + var maxY = window.pageYOffset + clientRect.top - reasonableBuffer; + + if (maxY - window.pageYOffset > maxScrollDisplacement) { + maxY = window.pageYOffset + maxScrollDisplacement; + } + + var targetY = window.pageYOffset - (window.innerHeight - elemBottom); + + if (targetY > maxY) { + targetY = maxY; + } + + window.scrollTo(0, targetY); + } + } + }, { + key: "menuContainerIsBody", + get: function get() { + return this.tribute.menuContainer === document.body || !this.tribute.menuContainer; + } + }]); + + return TributeRange; + }(); + + // Thanks to https://github.com/mattyork/fuzzy + var TributeSearch = /*#__PURE__*/function () { + function TributeSearch(tribute) { + _classCallCheck(this, TributeSearch); + + this.tribute = tribute; + this.tribute.search = this; + } + + _createClass(TributeSearch, [{ + key: "simpleFilter", + value: function simpleFilter(pattern, array) { + var _this = this; + + return array.filter(function (string) { + return _this.test(pattern, string); + }); + } + }, { + key: "test", + value: function test(pattern, string) { + return this.match(pattern, string) !== null; + } + }, { + key: "match", + value: function match(pattern, string, opts) { + opts = opts || {}; + var len = string.length, + pre = opts.pre || '', + post = opts.post || '', + compareString = opts.caseSensitive && string || string.toLowerCase(); + + if (opts.skip) { + return { + rendered: string, + score: 0 + }; + } + + pattern = opts.caseSensitive && pattern || pattern.toLowerCase(); + var patternCache = this.traverse(compareString, pattern, 0, 0, []); + + if (!patternCache) { + return null; + } + + return { + rendered: this.render(string, patternCache.cache, pre, post), + score: patternCache.score + }; + } + }, { + key: "traverse", + value: function traverse(string, pattern, stringIndex, patternIndex, patternCache) { + if (this.tribute.autocompleteSeparator) { + // if the pattern search at end + pattern = pattern.split(this.tribute.autocompleteSeparator).splice(-1)[0]; + } + + if (pattern.length === patternIndex) { + // calculate score and copy the cache containing the indices where it's found + return { + score: this.calculateScore(patternCache), + cache: patternCache.slice() + }; + } // if string at end or remaining pattern > remaining string + + + if (string.length === stringIndex || pattern.length - patternIndex > string.length - stringIndex) { + return undefined; + } + + var c = pattern[patternIndex]; + var index = string.indexOf(c, stringIndex); + var best, temp; + + while (index > -1) { + patternCache.push(index); + temp = this.traverse(string, pattern, index + 1, patternIndex + 1, patternCache); + patternCache.pop(); // if downstream traversal failed, return best answer so far + + if (!temp) { + return best; + } + + if (!best || best.score < temp.score) { + best = temp; + } + + index = string.indexOf(c, index + 1); + } + + return best; + } + }, { + key: "calculateScore", + value: function calculateScore(patternCache) { + var score = 0; + var temp = 1; + patternCache.forEach(function (index, i) { + if (i > 0) { + if (patternCache[i - 1] + 1 === index) { + temp += temp + 1; + } else { + temp = 1; + } + } + + score += temp; + }); + return score; + } + }, { + key: "render", + value: function render(string, indices, pre, post) { + var rendered = string.substring(0, indices[0]); + indices.forEach(function (index, i) { + rendered += pre + string[index] + post + string.substring(index + 1, indices[i + 1] ? indices[i + 1] : string.length); + }); + return rendered; + } + }, { + key: "filter", + value: function filter(pattern, arr, opts) { + var _this2 = this; + + opts = opts || {}; + return arr.reduce(function (prev, element, idx, arr) { + var str = element; + + if (opts.extract) { + str = opts.extract(element); + + if (!str) { + // take care of undefineds / nulls / etc. + str = ''; + } + } + + var rendered = _this2.match(pattern, str, opts); + + if (rendered != null) { + prev[prev.length] = { + string: rendered.rendered, + score: rendered.score, + index: idx, + original: element + }; + } + + return prev; + }, []).sort(function (a, b) { + var compare = b.score - a.score; + if (compare) return compare; + return a.index - b.index; + }); + } + }]); + + return TributeSearch; + }(); + + var Tribute = /*#__PURE__*/function () { + function Tribute(_ref) { + var _this = this; + + var _ref$values = _ref.values, + values = _ref$values === void 0 ? null : _ref$values, + _ref$loadingItemTempl = _ref.loadingItemTemplate, + loadingItemTemplate = _ref$loadingItemTempl === void 0 ? null : _ref$loadingItemTempl, + _ref$iframe = _ref.iframe, + iframe = _ref$iframe === void 0 ? null : _ref$iframe, + _ref$selectClass = _ref.selectClass, + selectClass = _ref$selectClass === void 0 ? "highlight" : _ref$selectClass, + _ref$containerClass = _ref.containerClass, + containerClass = _ref$containerClass === void 0 ? "tribute-container" : _ref$containerClass, + _ref$itemClass = _ref.itemClass, + itemClass = _ref$itemClass === void 0 ? "" : _ref$itemClass, + _ref$trigger = _ref.trigger, + trigger = _ref$trigger === void 0 ? "@" : _ref$trigger, + _ref$autocompleteMode = _ref.autocompleteMode, + autocompleteMode = _ref$autocompleteMode === void 0 ? false : _ref$autocompleteMode, + _ref$autocompleteSepa = _ref.autocompleteSeparator, + autocompleteSeparator = _ref$autocompleteSepa === void 0 ? null : _ref$autocompleteSepa, + _ref$selectTemplate = _ref.selectTemplate, + selectTemplate = _ref$selectTemplate === void 0 ? null : _ref$selectTemplate, + _ref$menuItemTemplate = _ref.menuItemTemplate, + menuItemTemplate = _ref$menuItemTemplate === void 0 ? null : _ref$menuItemTemplate, + _ref$lookup = _ref.lookup, + lookup = _ref$lookup === void 0 ? "key" : _ref$lookup, + _ref$fillAttr = _ref.fillAttr, + fillAttr = _ref$fillAttr === void 0 ? "value" : _ref$fillAttr, + _ref$collection = _ref.collection, + collection = _ref$collection === void 0 ? null : _ref$collection, + _ref$menuContainer = _ref.menuContainer, + menuContainer = _ref$menuContainer === void 0 ? null : _ref$menuContainer, + _ref$noMatchTemplate = _ref.noMatchTemplate, + noMatchTemplate = _ref$noMatchTemplate === void 0 ? null : _ref$noMatchTemplate, + _ref$requireLeadingSp = _ref.requireLeadingSpace, + requireLeadingSpace = _ref$requireLeadingSp === void 0 ? true : _ref$requireLeadingSp, + _ref$allowSpaces = _ref.allowSpaces, + allowSpaces = _ref$allowSpaces === void 0 ? false : _ref$allowSpaces, + _ref$replaceTextSuffi = _ref.replaceTextSuffix, + replaceTextSuffix = _ref$replaceTextSuffi === void 0 ? null : _ref$replaceTextSuffi, + _ref$positionMenu = _ref.positionMenu, + positionMenu = _ref$positionMenu === void 0 ? true : _ref$positionMenu, + _ref$spaceSelectsMatc = _ref.spaceSelectsMatch, + spaceSelectsMatch = _ref$spaceSelectsMatc === void 0 ? false : _ref$spaceSelectsMatc, + _ref$searchOpts = _ref.searchOpts, + searchOpts = _ref$searchOpts === void 0 ? {} : _ref$searchOpts, + _ref$menuItemLimit = _ref.menuItemLimit, + menuItemLimit = _ref$menuItemLimit === void 0 ? null : _ref$menuItemLimit, + _ref$menuShowMinLengt = _ref.menuShowMinLength, + menuShowMinLength = _ref$menuShowMinLengt === void 0 ? 0 : _ref$menuShowMinLengt; + + _classCallCheck(this, Tribute); + + this.autocompleteMode = autocompleteMode; + this.autocompleteSeparator = autocompleteSeparator; + this.menuSelected = 0; + this.current = {}; + this.inputEvent = false; + this.isActive = false; + this.menuContainer = menuContainer; + this.allowSpaces = allowSpaces; + this.replaceTextSuffix = replaceTextSuffix; + this.positionMenu = positionMenu; + this.hasTrailingSpace = false; + this.spaceSelectsMatch = spaceSelectsMatch; + + if (this.autocompleteMode) { + trigger = ""; + allowSpaces = false; + } + + if (values) { + this.collection = [{ + // symbol that starts the lookup + trigger: trigger, + // is it wrapped in an iframe + iframe: iframe, + // class applied to selected item + selectClass: selectClass, + // class applied to the Container + containerClass: containerClass, + // class applied to each item + itemClass: itemClass, + // function called on select that retuns the content to insert + selectTemplate: (selectTemplate || Tribute.defaultSelectTemplate).bind(this), + // function called that returns content for an item + menuItemTemplate: (menuItemTemplate || Tribute.defaultMenuItemTemplate).bind(this), + // function called when menu is empty, disables hiding of menu. + noMatchTemplate: function (t) { + if (typeof t === "string") { + if (t.trim() === "") return null; + return t; + } + + if (typeof t === "function") { + return t.bind(_this); + } + + return noMatchTemplate || function () { + return "
  • No Match Found!
  • "; + }.bind(_this); + }(noMatchTemplate), + // column to search against in the object + lookup: lookup, + // column that contains the content to insert by default + fillAttr: fillAttr, + // array of objects or a function returning an array of objects + values: values, + // useful for when values is an async function + loadingItemTemplate: loadingItemTemplate, + requireLeadingSpace: requireLeadingSpace, + searchOpts: searchOpts, + menuItemLimit: menuItemLimit, + menuShowMinLength: menuShowMinLength + }]; + } else if (collection) { + if (this.autocompleteMode) console.warn("Tribute in autocomplete mode does not work for collections"); + this.collection = collection.map(function (item) { + return { + trigger: item.trigger || trigger, + iframe: item.iframe || iframe, + selectClass: item.selectClass || selectClass, + containerClass: item.containerClass || containerClass, + itemClass: item.itemClass || itemClass, + selectTemplate: (item.selectTemplate || Tribute.defaultSelectTemplate).bind(_this), + menuItemTemplate: (item.menuItemTemplate || Tribute.defaultMenuItemTemplate).bind(_this), + // function called when menu is empty, disables hiding of menu. + noMatchTemplate: function (t) { + if (typeof t === "string") { + if (t.trim() === "") return null; + return t; + } + + if (typeof t === "function") { + return t.bind(_this); + } + + return noMatchTemplate || function () { + return "
  • No Match Found!
  • "; + }.bind(_this); + }(noMatchTemplate), + lookup: item.lookup || lookup, + fillAttr: item.fillAttr || fillAttr, + values: item.values, + loadingItemTemplate: item.loadingItemTemplate, + requireLeadingSpace: item.requireLeadingSpace, + searchOpts: item.searchOpts || searchOpts, + menuItemLimit: item.menuItemLimit || menuItemLimit, + menuShowMinLength: item.menuShowMinLength || menuShowMinLength + }; + }); + } else { + throw new Error("[Tribute] No collection specified."); + } + + new TributeRange(this); + new TributeEvents(this); + new TributeMenuEvents(this); + new TributeSearch(this); + } + + _createClass(Tribute, [{ + key: "triggers", + value: function triggers() { + return this.collection.map(function (config) { + return config.trigger; + }); + } + }, { + key: "attach", + value: function attach(el) { + if (!el) { + throw new Error("[Tribute] Must pass in a DOM node or NodeList."); + } // Check if it is a jQuery collection + + + if (typeof jQuery !== "undefined" && el instanceof jQuery) { + el = el.get(); + } // Is el an Array/Array-like object? + + + if (el.constructor === NodeList || el.constructor === HTMLCollection || el.constructor === Array) { + var length = el.length; + + for (var i = 0; i < length; ++i) { + this._attach(el[i]); + } + } else { + this._attach(el); + } + } + }, { + key: "_attach", + value: function _attach(el) { + if (el.hasAttribute("data-tribute")) { + console.warn("Tribute was already bound to " + el.nodeName); + } + + this.ensureEditable(el); + this.events.bind(el); + el.setAttribute("data-tribute", true); + } + }, { + key: "ensureEditable", + value: function ensureEditable(element) { + if (Tribute.inputTypes().indexOf(element.nodeName) === -1) { + if (element.contentEditable) { + element.contentEditable = true; + } else { + throw new Error("[Tribute] Cannot bind to " + element.nodeName); + } + } + } + }, { + key: "createMenu", + value: function createMenu(containerClass) { + var wrapper = this.range.getDocument().createElement("div"), + ul = this.range.getDocument().createElement("ul"); + wrapper.className = containerClass; + wrapper.appendChild(ul); + + if (this.menuContainer) { + return this.menuContainer.appendChild(wrapper); + } + + return this.range.getDocument().body.appendChild(wrapper); + } + }, { + key: "showMenuFor", + value: function showMenuFor(element, scrollTo) { + var _this2 = this; + + // Only proceed if menu isn't already shown for the current element & mentionText + if (this.isActive && this.current.element === element && this.current.mentionText === this.currentMentionTextSnapshot) { + return; + } + + this.currentMentionTextSnapshot = this.current.mentionText; // create the menu if it doesn't exist. + + if (!this.menu) { + this.menu = this.createMenu(this.current.collection.containerClass); + element.tributeMenu = this.menu; + this.menuEvents.bind(this.menu); + } + + this.isActive = true; + this.menuSelected = 0; + + if (!this.current.mentionText) { + this.current.mentionText = ""; + } + + var processValues = function processValues(values) { + // Tribute may not be active any more by the time the value callback returns + if (!_this2.isActive) { + return; + } + + var items = _this2.search.filter(_this2.current.mentionText, values, { + pre: _this2.current.collection.searchOpts.pre || "", + post: _this2.current.collection.searchOpts.post || "", + skip: _this2.current.collection.searchOpts.skip, + extract: function extract(el) { + if (typeof _this2.current.collection.lookup === "string") { + return el[_this2.current.collection.lookup]; + } else if (typeof _this2.current.collection.lookup === "function") { + return _this2.current.collection.lookup(el, _this2.current.mentionText); + } else { + throw new Error("Invalid lookup attribute, lookup must be string or function."); + } + } + }); + + if (_this2.current.collection.menuItemLimit) { + items = items.slice(0, _this2.current.collection.menuItemLimit); + } + + _this2.current.filteredItems = items; + + var ul = _this2.menu.querySelector("ul"); + + _this2.range.positionMenuAtCaret(scrollTo); + + if (!items.length) { + var noMatchEvent = new CustomEvent("tribute-no-match", { + detail: _this2.menu + }); + + _this2.current.element.dispatchEvent(noMatchEvent); + + if (typeof _this2.current.collection.noMatchTemplate === "function" && !_this2.current.collection.noMatchTemplate() || !_this2.current.collection.noMatchTemplate) { + _this2.hideMenu(); + } else { + typeof _this2.current.collection.noMatchTemplate === "function" ? ul.innerHTML = _this2.current.collection.noMatchTemplate() : ul.innerHTML = _this2.current.collection.noMatchTemplate; + } + + return; + } + + ul.innerHTML = ""; + + var fragment = _this2.range.getDocument().createDocumentFragment(); + + items.forEach(function (item, index) { + var li = _this2.range.getDocument().createElement("li"); + + li.setAttribute("data-index", index); + li.className = _this2.current.collection.itemClass; + li.addEventListener("mousemove", function (e) { + var _this2$_findLiTarget = _this2._findLiTarget(e.target), + _this2$_findLiTarget2 = _slicedToArray(_this2$_findLiTarget, 2), + li = _this2$_findLiTarget2[0], + index = _this2$_findLiTarget2[1]; + + if (e.movementY !== 0) { + _this2.events.setActiveLi(index); + } + }); + + if (_this2.menuSelected === index) { + li.classList.add(_this2.current.collection.selectClass); + } + + li.innerHTML = _this2.current.collection.menuItemTemplate(item); + fragment.appendChild(li); + }); + ul.appendChild(fragment); + }; + + if (typeof this.current.collection.values === "function") { + if (this.current.collection.loadingItemTemplate) { + this.menu.querySelector("ul").innerHTML = this.current.collection.loadingItemTemplate; + this.range.positionMenuAtCaret(scrollTo); + } + + this.current.collection.values(this.current.mentionText, processValues); + } else { + processValues(this.current.collection.values); + } + } + }, { + key: "_findLiTarget", + value: function _findLiTarget(el) { + if (!el) return []; + var index = el.getAttribute("data-index"); + return !index ? this._findLiTarget(el.parentNode) : [el, index]; + } + }, { + key: "showMenuForCollection", + value: function showMenuForCollection(element, collectionIndex) { + if (element !== document.activeElement) { + this.placeCaretAtEnd(element); + } + + this.current.collection = this.collection[collectionIndex || 0]; + this.current.externalTrigger = true; + this.current.element = element; + if (element.isContentEditable) this.insertTextAtCursor(this.current.collection.trigger);else this.insertAtCaret(element, this.current.collection.trigger); + this.showMenuFor(element); + } // TODO: make sure this works for inputs/textareas + + }, { + key: "placeCaretAtEnd", + value: function placeCaretAtEnd(el) { + el.focus(); + + if (typeof window.getSelection != "undefined" && typeof document.createRange != "undefined") { + var range = document.createRange(); + range.selectNodeContents(el); + range.collapse(false); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } else if (typeof document.body.createTextRange != "undefined") { + var textRange = document.body.createTextRange(); + textRange.moveToElementText(el); + textRange.collapse(false); + textRange.select(); + } + } // for contenteditable + + }, { + key: "insertTextAtCursor", + value: function insertTextAtCursor(text) { + var sel, range; + sel = window.getSelection(); + range = sel.getRangeAt(0); + range.deleteContents(); + var textNode = document.createTextNode(text); + range.insertNode(textNode); + range.selectNodeContents(textNode); + range.collapse(false); + sel.removeAllRanges(); + sel.addRange(range); + } // for regular inputs + + }, { + key: "insertAtCaret", + value: function insertAtCaret(textarea, text) { + var scrollPos = textarea.scrollTop; + var caretPos = textarea.selectionStart; + var front = textarea.value.substring(0, caretPos); + var back = textarea.value.substring(textarea.selectionEnd, textarea.value.length); + textarea.value = front + text + back; + caretPos = caretPos + text.length; + textarea.selectionStart = caretPos; + textarea.selectionEnd = caretPos; + textarea.focus(); + textarea.scrollTop = scrollPos; + } + }, { + key: "hideMenu", + value: function hideMenu() { + if (this.menu) { + this.menu.style.cssText = "display: none;"; + this.isActive = false; + this.menuSelected = 0; + this.current = {}; + } + } + }, { + key: "selectItemAtIndex", + value: function selectItemAtIndex(index, originalEvent) { + index = parseInt(index); + if (typeof index !== "number" || isNaN(index)) return; + var item = this.current.filteredItems[index]; + var content = this.current.collection.selectTemplate(item); + if (content !== null) this.replaceText(content, originalEvent, item); + } + }, { + key: "replaceText", + value: function replaceText(content, originalEvent, item) { + this.range.replaceTriggerText(content, true, true, originalEvent, item); + } + }, { + key: "_append", + value: function _append(collection, newValues, replace) { + if (typeof collection.values === "function") { + throw new Error("Unable to append to values, as it is a function."); + } else if (!replace) { + collection.values = collection.values.concat(newValues); + } else { + collection.values = newValues; + } + } + }, { + key: "append", + value: function append(collectionIndex, newValues, replace) { + var index = parseInt(collectionIndex); + if (typeof index !== "number") throw new Error("please provide an index for the collection to update."); + var collection = this.collection[index]; + + this._append(collection, newValues, replace); + } + }, { + key: "appendCurrent", + value: function appendCurrent(newValues, replace) { + if (this.isActive) { + this._append(this.current.collection, newValues, replace); + } else { + throw new Error("No active state. Please use append instead and pass an index."); + } + } + }, { + key: "detach", + value: function detach(el) { + if (!el) { + throw new Error("[Tribute] Must pass in a DOM node or NodeList."); + } // Check if it is a jQuery collection + + + if (typeof jQuery !== "undefined" && el instanceof jQuery) { + el = el.get(); + } // Is el an Array/Array-like object? + + + if (el.constructor === NodeList || el.constructor === HTMLCollection || el.constructor === Array) { + var length = el.length; + + for (var i = 0; i < length; ++i) { + this._detach(el[i]); + } + } else { + this._detach(el); + } + } + }, { + key: "_detach", + value: function _detach(el) { + var _this3 = this; + + this.events.unbind(el); + + if (el.tributeMenu) { + this.menuEvents.unbind(el.tributeMenu); + } + + setTimeout(function () { + el.removeAttribute("data-tribute"); + _this3.isActive = false; + + if (el.tributeMenu) { + el.tributeMenu.remove(); + } + }); + } + }, { + key: "isActive", + get: function get() { + return this._isActive; + }, + set: function set(val) { + if (this._isActive != val) { + this._isActive = val; + + if (this.current.element) { + var noMatchEvent = new CustomEvent("tribute-active-".concat(val)); + this.current.element.dispatchEvent(noMatchEvent); + } + } + } + }], [{ + key: "defaultSelectTemplate", + value: function defaultSelectTemplate(item) { + if (typeof item === "undefined") return "".concat(this.current.collection.trigger).concat(this.current.mentionText); + + if (this.range.isContentEditable(this.current.element)) { + return '' + (this.current.collection.trigger + item.original[this.current.collection.fillAttr]) + ""; + } + + return this.current.collection.trigger + item.original[this.current.collection.fillAttr]; + } + }, { + key: "defaultMenuItemTemplate", + value: function defaultMenuItemTemplate(matchItem) { + return matchItem.string; + } + }, { + key: "inputTypes", + value: function inputTypes() { + return ["TEXTAREA", "INPUT"]; + } + }]); + + return Tribute; + }(); + + /** + * Tribute.js + * Native ES6 JavaScript @mention Plugin + **/ + + return Tribute; + +}))); diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 22b9e8a7b0..944940045d 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -28,6 +28,7 @@ + diff --git a/phpBB/styles/prosilver/theme/colours.css b/phpBB/styles/prosilver/theme/colours.css index 7e56e06982..1c299b73ee 100644 --- a/phpBB/styles/prosilver/theme/colours.css +++ b/phpBB/styles/prosilver/theme/colours.css @@ -372,7 +372,7 @@ p.post-notice { /* colours and backgrounds for mentions.css */ /* mention dropdown */ -.atwho-view { /* mention-container */ +.mention-container { /* mention-container */ background-color: #ffffff; box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index e35512f27a..4b1e36f800 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -13,7 +13,7 @@ /* Mention dropdown ---------------------------------------- */ -.atwho-view { /* mention-container */ +.mention-container { /* mention-container */ text-align: left; border-radius: 2px; position: absolute; @@ -21,7 +21,7 @@ transition: all 0.2s ease; } -.atwho-view-ul { /* mention-list */ +.mention-container ul { /* mention-list */ overflow: auto; /* placed here for list to scroll with arrow key press */ max-height: 200px; margin: 0; From ae1f556b745fb4dc2c07d400983b8206149d8f57 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Fri, 12 Mar 2021 17:23:03 +0100 Subject: [PATCH 093/113] [ticket/13713] Fix scrolling not working and not needed includes PHPBB3-13713 --- phpBB/styles/prosilver/template/posting_buttons.html | 2 -- phpBB/styles/prosilver/theme/mentions.css | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 944940045d..a2e9920b9e 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -26,8 +26,6 @@ } - - diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 4b1e36f800..5163530b96 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -14,6 +14,8 @@ /* Mention dropdown ---------------------------------------- */ .mention-container { /* mention-container */ + overflow: auto; /* placed here for list to scroll with arrow key press */ + max-height: 200px; text-align: left; border-radius: 2px; position: absolute; @@ -22,8 +24,6 @@ } .mention-container ul { /* mention-list */ - overflow: auto; /* placed here for list to scroll with arrow key press */ - max-height: 200px; margin: 0; padding: 0; list-style-type: none; From 7a86977d0180a2d0df1df9abff3a168169212d08 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Sat, 13 Mar 2021 20:48:21 +0100 Subject: [PATCH 094/113] [ticket/13713] Use our filter function for zurb tribute PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 197 ++++++++++++++---------------- 1 file changed, 93 insertions(+), 104 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index ea06496ed2..0fc3fad2e3 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -469,10 +469,98 @@ function getCaretPosition(txtarea) { return matchedNames; } + /** + * Return whether item is matched by query + * + * @param {string} query Search query string + * @param {MentionsData} item Mentions data item + * @param {string }searchKey Key to use for matching items + * @return {boolean} True if items is matched, false otherwise + */ function isItemMatched(query, item, searchKey) { return String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()) === 0; } + /** + * Filter items by search query + * + * @param {string} query Search query string + * @param {Object.} items List of {@link MentionsData} items + * @return {Object.} List of {@link MentionsData} items filtered with query and by searchKey + */ + function itemFilter(query, items) { + let i; + let len; + let highestPriorities = {u: 1, g: 1}; + let _unsorted = {u: {}, g: {}}; + let _exactMatch = []; + let _results = []; + + // Reduce the items array to the relevant ones + items = getMatchedNames(query, items, 'name'); + + // Group names by their types and calculate priorities + for (i = 0, len = items.length; i < len; i++) { + let item = items[i]; + + // Check for unsupported type - in general, this should never happen + if (!_unsorted[item.type]) { + continue; + } + + // Current user doesn't want to mention themselves with "@" in most cases - + // do not waste list space with their own name + if (item.type === 'u' && item.id === String(mentionUserId)) { + continue; + } + + // Exact matches should not be prioritised - they always come first + if (item.name === query) { + _exactMatch.push(items[i]); + continue; + } + + // If the item hasn't been added yet - add it + if (!_unsorted[item.type][item.id]) { + _unsorted[item.type][item.id] = item; + continue; + } + + // Priority is calculated as the sum of priorities from different sources + _unsorted[item.type][item.id].priority += parseFloat(item.priority.toString()); + + // Calculate the highest priority - we'll give it to group names + highestPriorities[item.type] = Math.max(highestPriorities[item.type], _unsorted[item.type][item.id].priority); + } + + // All types of names should come at the same level of importance, + // otherwise they will be unlikely to be shown + // That's why we normalize priorities and push names to a single results array + $.each(['u', 'g'], function(key, type) { + if (_unsorted[type]) { + $.each(_unsorted[type], function(name, value) { + // Normalize priority + value.priority /= highestPriorities[type]; + + // Add item to all results + _results.push(value); + }); + } + }); + + // Sort names by priorities - higher values come first + _results = _results.sort(function(a, b) { + return b.priority - a.priority; + }); + + // Exact match is the most important - should come above anything else + $.each(_exactMatch, function(name, value) { + _results.unshift(value); + }); + + return _results; + } + /** * atwho.js remoteFilter callback filter function * @param {string} query Query string @@ -531,125 +619,26 @@ function getCaretPosition(txtarea) { selectClass: 'cur', itemClass: 'mention-item', menuItemTemplate: function (data) { - const itemData = data.original; + const itemData = data; let avatar = (itemData.avatar.img) ? "" + itemData.avatar.img + "" : defaultAvatar(itemData.avatar.type), rank = (itemData.rank) ? "" + itemData.rank + "" : ''; return "" + avatar + "" + itemData.name + rank + ""; }, selectTemplate: function (item) { - return '[mention=' + item.original.type + ':' + item.original.id + ']' + item.original.name + '[/mention]'; + return '[mention=' + item.type + ':' + item.id + ']' + item.name + '[/mention]'; }, menuItemLimit: mentionNamesLimit, values: function (text, cb) { remoteFilter(text, users => cb(users)); }, - noMatchTemplate: function (t) { - console.log('No match:'); - console.log(t); - }, - lookup: function (element, mentionText) { + lookup: function (element) { return element.hasOwnProperty('name') ? element.name : ''; } }); + tribute.search.filter = itemFilter; + tribute.attach($(textarea)); - - /* - var tribute = new Tribute({ - values: [ - { key: "Phil Heartman", value: "pheartman" }, - { key: "Gordon Ramsey", value: "gramsey" } - ] - }); - */ -/* - $(textarea).atwho({ - at: "@", - acceptSpaceBar: true, - displayTpl: function(data) { - let avatar = (data.avatar.img) ? "" + data.avatar.img + "" : defaultAvatar(data.avatar.type), - rank = (data.rank) ? "" + data.rank + "" : ''; - return "
  • " + avatar + "" + data.name + rank + "
  • "; - }, - insertTpl: "[mention=${type}:${id}]${name}[/mention]", - limit: mentionNamesLimit, - callbacks: { - remoteFilter: remoteFilter, - sorter: function(query, items, searchKey) { - let i; - let len; - let highestPriorities = {u: 1, g: 1}; - let _unsorted = {u: {}, g: {}}; - let _exactMatch = []; - let _results = []; - - // Reduce the items array to the relevant ones - items = getMatchedNames(query, items, searchKey); - - // Group names by their types and calculate priorities - for (i = 0, len = items.length; i < len; i++) { - let item = items[i]; - - // Check for unsupported type - in general, this should never happen - if (!_unsorted[item.type]) { - continue; - } - - // Current user doesn't want to mention themselves with "@" in most cases - - // do not waste list space with their own name - if (item.type === 'u' && item.id === String(mentionUserId)) { - continue; - } - - // Exact matches should not be prioritised - they always come first - if (item.name === query) { - _exactMatch.push(items[i]); - continue; - } - - // If the item hasn't been added yet - add it - if (!_unsorted[item.type][item.id]) { - _unsorted[item.type][item.id] = item; - continue; - } - - // Priority is calculated as the sum of priorities from different sources - _unsorted[item.type][item.id].priority += parseFloat(item.priority.toString()); - - // Calculate the highest priority - we'll give it to group names - highestPriorities[item.type] = Math.max(highestPriorities[item.type], _unsorted[item.type][item.id].priority); - } - - // All types of names should come at the same level of importance, - // otherwise they will be unlikely to be shown - // That's why we normalize priorities and push names to a single results array - $.each(['u', 'g'], function(key, type) { - if (_unsorted[type]) { - $.each(_unsorted[type], function(name, value) { - // Normalize priority - value.priority /= highestPriorities[type]; - - // Add item to all results - _results.push(value); - }); - } - }); - - // Sort names by priorities - higher values come first - _results = _results.sort(function(a, b) { - return b.priority - a.priority; - }); - - // Exact match is the most important - should come above anything else - $.each(_exactMatch, function(name, value) { - _results.unshift(value); - }); - - return _results; - } - } - }); - */ }; } phpbb.mentions = new Mentions(); From c317368a393f6f1b92c4cbeadb84e043dbaf4c23 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Sat, 13 Mar 2021 21:07:04 +0100 Subject: [PATCH 095/113] [ticket/13713] Enable zurb tribute 5.1.3 also in ACP and remove old files PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 3 +- phpBB/adm/style/admin.css | 10 +- phpBB/assets/javascript/editor.js | 2 +- phpBB/assets/javascript/jquery.atwho.min.js | 1 - phpBB/assets/javascript/jquery.caret.min.js | 2 - phpBB/assets/javascript/jquery.tribute.js | 1898 ----------------- phpBB/assets/javascript/tribute.min.js | 2 + .../prosilver/template/posting_buttons.html | 2 +- phpBB/styles/prosilver/theme/bidi.css | 2 +- 9 files changed, 11 insertions(+), 1911 deletions(-) delete mode 100644 phpBB/assets/javascript/jquery.atwho.min.js delete mode 100644 phpBB/assets/javascript/jquery.caret.min.js delete mode 100644 phpBB/assets/javascript/jquery.tribute.js create mode 100644 phpBB/assets/javascript/tribute.min.js diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index 08c81de0c0..82c35dc65c 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -8,8 +8,7 @@ // ]]> - - + diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 1421c9fa7b..d0d97dba72 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1676,7 +1676,9 @@ fieldset.submit-buttons legend { font-weight: bold; } -.atwho-view { /* mention-container */ +.mention-container { /* mention-container */ + overflow: auto; /* placed here for list to scroll with arrow key press */ + max-height: 200px; text-align: left; background-color: #ffffff; border-radius: 2px; @@ -1689,13 +1691,11 @@ fieldset.submit-buttons legend { transition: all 0.2s ease; } -.rtl .atwho-view { /* mention-container */ +.rtl .mention-container { /* mention-container */ text-align: right; } -.atwho-view-ul { /* mention-list */ - overflow: auto; /* placed here for list to scroll with arrow key press */ - max-height: 200px; +.mention-container ul { /* mention-list */ margin: 0; padding: 0; list-style-type: none; diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 0fc3fad2e3..2e726868bf 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -562,7 +562,7 @@ function getCaretPosition(txtarea) { } /** - * atwho.js remoteFilter callback filter function + * remoteFilter callback filter function * @param {string} query Query string * @param {function} callback Callback function for filtered items */ diff --git a/phpBB/assets/javascript/jquery.atwho.min.js b/phpBB/assets/javascript/jquery.atwho.min.js deleted file mode 100644 index 857bb93126..0000000000 --- a/phpBB/assets/javascript/jquery.atwho.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){"function"==typeof define&&define.amd?define(["jquery"],function(t){return e(t)}):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(this,function(t){var e,i;i={ESC:27,TAB:9,ENTER:13,CTRL:17,A:65,P:80,N:78,LEFT:37,UP:38,RIGHT:39,DOWN:40,BACKSPACE:8,SPACE:32},e={beforeSave:function(t){return r.arrayToDefaultHash(t)},matcher:function(t,e,i,n){var r,o,s,a,h;return t=t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),i&&(t="(?:^|\\s)"+t),r=decodeURI("%C3%80"),o=decodeURI("%C3%BF"),h=n?" ":"",a=new RegExp(t+"([A-Za-z"+r+"-"+o+"0-9_"+h+"'.+-]*)$|"+t+"([^\\x00-\\xff]*)$","gi"),s=a.exec(e),s?s[2]||s[1]:null},filter:function(t,e,i){var n,r,o,s;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],~new String(o[i]).toLowerCase().indexOf(t.toLowerCase())&&n.push(o);return n},remoteFilter:null,sorter:function(t,e,i){var n,r,o,s;if(!t)return e;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],o.atwho_order=new String(o[i]).toLowerCase().indexOf(t.toLowerCase()),o.atwho_order>-1&&n.push(o);return n.sort(function(t,e){return t.atwho_order-e.atwho_order})},tplEval:function(t,e){var i,n,r;r=t;try{return"string"!=typeof t&&(r=t(e)),r.replace(/\$\{([^\}]*)\}/g,function(t,i,n){return e[i]})}catch(n){return i=n,""}},highlighter:function(t,e){var i;return e?(i=new RegExp(">\\s*([^<]*?)("+e.replace("+","\\+")+")([^<]*)\\s*<","ig"),t.replace(i,function(t,e,i,n){return"> "+e+""+i+""+n+" <"})):t},beforeInsert:function(t,e,i){return t},beforeReposition:function(t){return t},afterMatchFailed:function(t,e){}};var n;n=function(){function e(e){this.currentFlag=null,this.controllers={},this.aliasMaps={},this.$inputor=t(e),this.setupRootElement(),this.listen()}return e.prototype.createContainer=function(e){var i;return null!=(i=this.$el)&&i.remove(),t(e.body).append(this.$el=t("
    "))},e.prototype.setupRootElement=function(e,i){var n,r;if(null==i&&(i=!1),e)this.window=e.contentWindow,this.document=e.contentDocument||this.window.document,this.iframe=e;else{this.document=this.$inputor[0].ownerDocument,this.window=this.document.defaultView||this.document.parentWindow;try{this.iframe=this.window.frameElement}catch(r){if(n=r,this.iframe=null,t.fn.atwho.debug)throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n"+n)}}return this.createContainer((this.iframeAsRoot=i)?this.document:document)},e.prototype.controller=function(t){var e,i,n,r;if(this.aliasMaps[t])i=this.controllers[this.aliasMaps[t]];else{r=this.controllers;for(n in r)if(e=r[n],n===t){i=e;break}}return i?i:this.controllers[this.currentFlag]},e.prototype.setContextFor=function(t){return this.currentFlag=t,this},e.prototype.reg=function(t,e){var i,n;return n=(i=this.controllers)[t]||(i[t]=this.$inputor.is("[contentEditable]")?new l(this,t):new s(this,t)),e.alias&&(this.aliasMaps[e.alias]=t),n.init(e),this},e.prototype.listen=function(){return this.$inputor.on("compositionstart",function(t){return function(e){var i;return null!=(i=t.controller())&&i.view.hide(),t.isComposing=!0,null}}(this)).on("compositionend",function(t){return function(e){return t.isComposing=!1,setTimeout(function(e){return t.dispatch(e)}),null}}(this)).on("keyup.atwhoInner",function(t){return function(e){return t.onKeyup(e)}}(this)).on("keydown.atwhoInner",function(t){return function(e){return t.onKeydown(e)}}(this)).on("blur.atwhoInner",function(t){return function(e){var i;return(i=t.controller())?(i.expectedQueryCBId=null,i.view.hide(e,i.getOpt("displayTimeout"))):void 0}}(this)).on("click.atwhoInner",function(t){return function(e){return t.dispatch(e)}}(this)).on("scroll.atwhoInner",function(t){return function(){var e;return e=t.$inputor.scrollTop(),function(i){var n,r;return n=i.target.scrollTop,e!==n&&null!=(r=t.controller())&&r.view.hide(i),e=n,!0}}}(this)())},e.prototype.shutdown=function(){var t,e,i;i=this.controllers;for(t in i)e=i[t],e.destroy(),delete this.controllers[t];return this.$inputor.off(".atwhoInner"),this.$el.remove()},e.prototype.dispatch=function(t){var e,i,n,r;if(void 0!==t){n=this.controllers,r=[];for(e in n)i=n[e],r.push(i.lookUp(t));return r}},e.prototype.onKeyup=function(e){var n;switch(e.keyCode){case i.ESC:e.preventDefault(),null!=(n=this.controller())&&n.view.hide();break;case i.DOWN:case i.UP:case i.CTRL:case i.ENTER:t.noop();break;case i.P:case i.N:e.ctrlKey||this.dispatch(e);break;default:this.dispatch(e)}},e.prototype.onKeydown=function(e){var n,r;if(r=null!=(n=this.controller())?n.view:void 0,r&&r.visible())switch(e.keyCode){case i.ESC:e.preventDefault(),r.hide(e);break;case i.UP:e.preventDefault(),r.prev();break;case i.DOWN:e.preventDefault(),r.next();break;case i.P:if(!e.ctrlKey)return;e.preventDefault(),r.prev();break;case i.N:if(!e.ctrlKey)return;e.preventDefault(),r.next();break;case i.TAB:case i.ENTER:case i.SPACE:if(!r.visible())return;if(!this.controller().getOpt("spaceSelectsMatch")&&e.keyCode===i.SPACE)return;if(!this.controller().getOpt("tabSelectsMatch")&&e.keyCode===i.TAB)return;r.highlighted()?(e.preventDefault(),r.choose(e)):r.hide(e);break;default:t.noop()}},e}();var r,o=[].slice;r=function(){function i(e,i){this.app=e,this.at=i,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.expectedQueryCBId=null,this.setting=null,this.query=null,this.pos=0,this.range=null,0===(this.$el=t("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=t("
    ")),this.model=new u(this),this.view=new c(this)}return i.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},i.prototype.init=function(e){return this.setting=t.extend({},this.setting||t.fn.atwho["default"],e),this.view.init(),this.model.reload(this.setting.data)},i.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},i.prototype.callDefault=function(){var i,n,r,s;s=arguments[0],i=2<=arguments.length?o.call(arguments,1):[];try{return e[s].apply(this,i)}catch(r){return n=r,t.error(n+" Or maybe At.js doesn't have function "+s)}},i.prototype.trigger=function(t,e){var i,n;return null==e&&(e=[]),e.push(this),i=this.getOpt("alias"),n=i?t+"-"+i+".atwho":t+".atwho",this.$inputor.trigger(n,e)},i.prototype.callbacks=function(t){return this.getOpt("callbacks")[t]||e[t]},i.prototype.getOpt=function(t,e){var i,n;try{return this.setting[t]}catch(n){return i=n,null}},i.prototype.insertContentFor=function(e){var i,n;return n=this.getOpt("insertTpl"),i=t.extend({},e.data("item-data"),{"atwho-at":this.at}),this.callbacks("tplEval").call(this,n,i,"onInsert")},i.prototype.renderView=function(t){var e;return e=this.getOpt("searchKey"),t=this.callbacks("sorter").call(this,this.query.text,t.slice(0,1001),e),this.view.render(t.slice(0,this.getOpt("limit")))},i.arrayToDefaultHash=function(e){var i,n,r,o;if(!t.isArray(e))return e;for(o=[],i=0,r=e.length;r>i;i++)n=e[i],t.isPlainObject(n)?o.push(n):o.push({name:n});return o},i.prototype.lookUp=function(t){var e,i;if((!t||"click"!==t.type||this.getOpt("lookUpOnClick"))&&(!this.getOpt("suspendOnComposing")||!this.app.isComposing))return(e=this.catchQuery(t))?(this.app.setContextFor(this.at),(i=this.getOpt("delay"))?this._delayLookUp(e,i):this._lookUp(e),e):(this.expectedQueryCBId=null,e)},i.prototype._delayLookUp=function(t,e){var i,n;return i=Date.now?Date.now():(new Date).getTime(),this.previousCallTime||(this.previousCallTime=i),n=e-(i-this.previousCallTime),n>0&&e>n?(this.previousCallTime=i,this._stopDelayedCall(),this.delayedCallTimeout=setTimeout(function(e){return function(){return e.previousCallTime=0,e.delayedCallTimeout=null,e._lookUp(t)}}(this),e)):(this._stopDelayedCall(),this.previousCallTime!==i&&(this.previousCallTime=0),this._lookUp(t))},i.prototype._stopDelayedCall=function(){return this.delayedCallTimeout?(clearTimeout(this.delayedCallTimeout),this.delayedCallTimeout=null):void 0},i.prototype._generateQueryCBId=function(){return{}},i.prototype._lookUp=function(e){var i;return i=function(t,e){return t===this.expectedQueryCBId?e&&e.length>0?this.renderView(this.constructor.arrayToDefaultHash(e)):this.view.hide():void 0},this.expectedQueryCBId=this._generateQueryCBId(),this.model.query(e.text,t.proxy(i,this,this.expectedQueryCBId))},i}();var s,a=function(t,e){function i(){this.constructor=t}for(var n in e)h.call(e,n)&&(t[n]=e[n]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},h={}.hasOwnProperty;s=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return a(i,e),i.prototype.catchQuery=function(){var t,e,i,n,r,o,s;return e=this.$inputor.val(),t=this.$inputor.caret("pos",{iframe:this.app.iframe}),s=e.slice(0,t),r=this.callbacks("matcher").call(this,this.at,s,this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),n="string"==typeof r,n&&r.length0?t.getRangeAt(0):void 0},n.prototype._setRange=function(e,i,n){return null==n&&(n=this._getRange()),n&&i?(i=t(i)[0],"after"===e?(n.setEndAfter(i),n.setStartAfter(i)):(n.setEndBefore(i),n.setStartBefore(i)),n.collapse(!1),this._clearRange(n)):void 0},n.prototype._clearRange=function(t){var e;return null==t&&(t=this._getRange()),e=this.app.window.getSelection(),null==this.ctrl_a_pressed?(e.removeAllRanges(),e.addRange(t)):void 0},n.prototype._movingEvent=function(t){var e;return"click"===t.type||(e=t.which)===i.RIGHT||e===i.LEFT||e===i.UP||e===i.DOWN},n.prototype._unwrap=function(e){var i;return e=t(e).unwrap().get(0),(i=e.nextSibling)&&i.nodeValue&&(e.nodeValue+=i.nodeValue,t(i).remove()),e},n.prototype.catchQuery=function(e){var n,r,o,s,a,h,l,u,c,p,f,d;if((d=this._getRange())&&d.collapsed){if(e.which===i.ENTER)return(r=t(d.startContainer).closest(".atwho-query")).contents().unwrap(),r.is(":empty")&&r.remove(),(r=t(".atwho-query",this.app.document)).text(r.text()).contents().last().unwrap(),void this._clearRange();if(/firefox/i.test(navigator.userAgent)){if(t(d.startContainer).is(this.$inputor))return void this._clearRange();e.which===i.BACKSPACE&&d.startContainer.nodeType===document.ELEMENT_NODE&&(c=d.startOffset-1)>=0?(o=d.cloneRange(),o.setStart(d.startContainer,c),t(o.cloneContents()).contents().last().is(".atwho-inserted")&&(a=t(d.startContainer).contents().get(c),this._setRange("after",t(a).contents().last()))):e.which===i.LEFT&&d.startContainer.nodeType===document.TEXT_NODE&&(n=t(d.startContainer.previousSibling),n.is(".atwho-inserted")&&0===d.startOffset&&this._setRange("after",n.contents().last()))}if(t(d.startContainer).closest(".atwho-inserted").addClass("atwho-query").siblings().removeClass("atwho-query"),(r=t(".atwho-query",this.app.document)).length>0&&r.is(":empty")&&0===r.text().length&&r.remove(),this._movingEvent(e)||r.removeClass("atwho-inserted"),r.length>0)switch(e.which){case i.LEFT:return this._setRange("before",r.get(0),d),void r.removeClass("atwho-query");case i.RIGHT:return this._setRange("after",r.get(0).nextSibling,d),void r.removeClass("atwho-query")}if(r.length>0&&(f=r.attr("data-atwho-at-query"))&&(r.empty().html(f).attr("data-atwho-at-query",null),this._setRange("after",r.get(0),d)),o=d.cloneRange(),o.setStart(d.startContainer,0),u=this.callbacks("matcher").call(this,this.at,o.toString(),this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),h="string"==typeof u,0===r.length&&h&&(s=d.startOffset-this.at.length-u.length)>=0&&(d.setStart(d.startContainer,s),r=t("",this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass("atwho-query"),d.surroundContents(r.get(0)),l=r.contents().last().get(0),l&&(/firefox/i.test(navigator.userAgent)?(d.setStart(l,l.length),d.setEnd(l,l.length),this._clearRange(d)):this._setRange("after",l,d))),!(h&&u.length=0&&(this._movingEvent(e)&&r.hasClass("atwho-inserted")?r.removeClass("atwho-query"):!1!==this.callbacks("afterMatchFailed").call(this,this.at,r)&&this._setRange("after",this._unwrap(r.text(r.text()).contents().first()))),null)}},n.prototype.rect=function(){var e,i,n;return n=this.query.el.offset(),n&&this.query.el[0].getClientRects().length?(this.app.iframe&&!this.app.iframeAsRoot&&(i=(e=t(this.app.iframe)).offset(),n.left+=i.left-this.$inputor.scrollLeft(),n.top+=i.top-this.$inputor.scrollTop()),n.bottom=n.top+this.query.el.height(),n):void 0},n.prototype.insert=function(t,e){var i,n,r,o,s;return this.$inputor.is(":focus")||this.$inputor.focus(),n=this.getOpt("functionOverrides"),n.insert?n.insert.call(this,t,e):(o=""===(o=this.getOpt("suffix"))?o:o||" ",i=e.data("item-data"),this.query.el.removeClass("atwho-query").addClass("atwho-inserted").html(t).attr("data-atwho-at-query",""+i["atwho-at"]+this.query.text).attr("contenteditable","false"),(r=this._getRange())&&(this.query.el.length&&r.setEndAfter(this.query.el[0]),r.collapse(!1),r.insertNode(s=this.app.document.createTextNode(""+o)),this._setRange("after",s,r)),this.$inputor.is(":focus")||this.$inputor.focus(),this.$inputor.change())},n}(r);var u;u=function(){function e(t){this.context=t,this.at=this.context.at,this.storage=this.context.$inputor}return e.prototype.destroy=function(){return this.storage.data(this.at,null)},e.prototype.saved=function(){return this.fetch()>0},e.prototype.query=function(t,e){var i,n,r;return n=this.fetch(),r=this.context.getOpt("searchKey"),n=this.context.callbacks("filter").call(this.context,t,n,r)||[],i=this.context.callbacks("remoteFilter"),n.length>0||!i&&0===n.length?e(n):i.call(this.context,t,e)},e.prototype.fetch=function(){return this.storage.data(this.at)||[]},e.prototype.save=function(t){return this.storage.data(this.at,this.context.callbacks("beforeSave").call(this.context,t||[]))},e.prototype.load=function(t){return!this.saved()&&t?this._load(t):void 0},e.prototype.reload=function(t){return this._load(t)},e.prototype._load=function(e){return"string"==typeof e?t.ajax(e,{dataType:"json"}).done(function(t){return function(e){return t.save(e)}}(this)):this.save(e)},e}();var c;c=function(){function e(e){this.context=e,this.$el=t("
      "),this.$elUl=this.$el.children(),this.timeoutID=null,this.context.$el.append(this.$el),this.bindEvent()}return e.prototype.init=function(){var t,e;return e=this.context.getOpt("alias")||this.context.at.charCodeAt(0),t=this.context.getOpt("headerTpl"),t&&1===this.$el.children().length&&this.$el.prepend(t),this.$el.attr({id:"at-view-"+e})},e.prototype.destroy=function(){return this.$el.remove()},e.prototype.bindEvent=function(){var e,i,n;return e=this.$el.find("ul"),i=0,n=0,e.on("mousemove.atwho-view","li",function(r){return function(r){var o;if((i!==r.clientX||n!==r.clientY)&&(i=r.clientX,n=r.clientY,o=t(r.currentTarget),!o.hasClass("cur")))return e.find(".cur").removeClass("cur"),o.addClass("cur")}}(this)).on("click.atwho-view","li",function(i){return function(n){return e.find(".cur").removeClass("cur"),t(n.currentTarget).addClass("cur"),i.choose(n),n.preventDefault()}}(this))},e.prototype.visible=function(){return t.expr.filters.visible(this.$el[0])},e.prototype.highlighted=function(){return this.$el.find(".cur").length>0},e.prototype.choose=function(t){var e,i;return(e=this.$el.find(".cur")).length&&(i=this.context.insertContentFor(e),this.context._stopDelayedCall(),this.context.insert(this.context.callbacks("beforeInsert").call(this.context,i,e,t),e),this.context.trigger("inserted",[e,t]),this.hide(t)),this.context.getOpt("hideWithoutSuffix")?this.stopShowing=!0:void 0},e.prototype.reposition=function(e){var i,n,r,o;return i=this.context.app.iframeAsRoot?this.context.app.window:window,e.bottom+this.$el.height()-t(i).scrollTop()>t(i).height()&&(e.bottom=e.top-this.$el.height()),e.left>(r=t(i).width()-this.$el.width()-5)&&(e.left=r),n={left:e.left,top:e.bottom},null!=(o=this.context.callbacks("beforeReposition"))&&o.call(this.context,n),this.$el.offset(n),this.context.trigger("reposition",[n])},e.prototype.next=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),e=t.next(),e.length||(e=this.$el.find("li:first")),e.addClass("cur"),i=e[0],n=i.offsetTop+i.offsetHeight+(i.nextSibling?i.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,n-this.$el.height()))},e.prototype.prev=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),i=t.prev(),i.length||(i=this.$el.find("li:last")),i.addClass("cur"),n=i[0],e=n.offsetTop+n.offsetHeight+(n.nextSibling?n.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,e-this.$el.height()))},e.prototype.scrollTop=function(t){var e;return e=this.context.getOpt("scrollDuration"),e?this.$elUl.animate({scrollTop:t},e):this.$elUl.scrollTop(t)},e.prototype.show=function(){var t;return this.stopShowing?void(this.stopShowing=!1):(this.visible()||(this.$el.show(),this.$el.scrollTop(0),this.context.trigger("shown")),(t=this.context.rect())?this.reposition(t):void 0)},e.prototype.hide=function(t,e){var i;if(this.visible())return isNaN(e)?(this.$el.hide(),this.context.trigger("hidden",[t])):(i=function(t){return function(){return t.hide()}}(this),clearTimeout(this.timeoutID),this.timeoutID=setTimeout(i,e))},e.prototype.render=function(e){var i,n,r,o,s,a,h;if(!(t.isArray(e)&&e.length>0))return void this.hide();for(this.$el.find("ul").empty(),n=this.$el.find("ul"),h=this.context.getOpt("displayTpl"),r=0,s=e.length;s>r;r++)o=e[r],o=t.extend({},o,{"atwho-at":this.context.at}),a=this.context.callbacks("tplEval").call(this.context,h,o,"onDisplay"),i=t(this.context.callbacks("highlighter").call(this.context,a,this.context.query.text)),i.data("item-data",o),n.append(i);return this.show(),this.context.getOpt("highlightFirst")?n.find("li:first").addClass("cur"):void 0},e}();var p;p={load:function(t,e){var i;return(i=this.controller(t))?i.model.load(e):void 0},isSelecting:function(){var t;return!!(null!=(t=this.controller())?t.view.visible():void 0)},hide:function(){var t;return null!=(t=this.controller())?t.view.hide():void 0},reposition:function(){var t;return(t=this.controller())?t.view.reposition(t.rect()):void 0},setIframe:function(t,e){return this.setupRootElement(t,e),null},run:function(){return this.dispatch()},destroy:function(){return this.shutdown(),this.$inputor.data("atwho",null)}},t.fn.atwho=function(e){var i,r;return i=arguments,r=null,this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function(){var o,s;return(s=(o=t(this)).data("atwho"))||o.data("atwho",s=new n(this)),"object"!=typeof e&&e?p[e]&&s?r=p[e].apply(s,Array.prototype.slice.call(i,1)):t.error("Method "+e+" does not exist on jQuery.atwho"):s.reg(e.at,e)}),null!=r?r:this},t.fn.atwho["default"]={at:void 0,alias:void 0,data:null,displayTpl:"
    • ${name}
    • ",insertTpl:"${atwho-at}${name}",headerTpl:null,callbacks:e,functionOverrides:{},searchKey:"name",suffix:void 0,hideWithoutSuffix:!1,startWithSpace:!0,acceptSpaceBar:!1,highlightFirst:!0,limit:5,maxLen:20,minLen:0,displayTimeout:300,delay:null,spaceSelectsMatch:!1,tabSelectsMatch:!0,editableAtwhoQueryAttrs:{},scrollDuration:150,suspendOnComposing:!0,lookUpOnClick:!0},t.fn.atwho.debug=!1}); \ No newline at end of file diff --git a/phpBB/assets/javascript/jquery.caret.min.js b/phpBB/assets/javascript/jquery.caret.min.js deleted file mode 100644 index a25584e2ae..0000000000 --- a/phpBB/assets/javascript/jquery.caret.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jquery.caret 2016-02-27 */ -!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(c){return a.returnExportsGlobal=b(c)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){"use strict";var b,c,d,e,f,g,h,i,j,k,l;k="caret",b=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.setPos=function(a){var b,c,d,e;return(e=j.getSelection())&&(d=0,c=!1,(b=function(a,f){var g,i,j,k,l,m;for(l=f.childNodes,m=[],j=0,k=l.length;k>j&&(g=l[j],!c);j++)if(3===g.nodeType){if(d+g.length>=a){c=!0,i=h.createRange(),i.setStart(g,a-d),e.removeAllRanges(),e.addRange(i);break}m.push(d+=g.length)}else m.push(b(a,g));return m})(a,this.domInputor)),this.domInputor},b.prototype.getIEPosition=function(){return this.getPosition()},b.prototype.getPosition=function(){var a,b;return b=this.getOffset(),a=this.$inputor.offset(),b.left-=a.left,b.top-=a.top,b},b.prototype.getOldIEPos=function(){var a,b;return b=h.selection.createRange(),a=h.body.createTextRange(),a.moveToElementText(this.domInputor),a.setEndPoint("EndToEnd",b),a.text.length},b.prototype.getPos=function(){var a,b,c;return(c=this.range())?(a=c.cloneRange(),a.selectNodeContents(this.domInputor),a.setEnd(c.endContainer,c.endOffset),b=a.toString().length,a.detach(),b):h.selection?this.getOldIEPos():void 0},b.prototype.getOldIEOffset=function(){var a,b;return a=h.selection.createRange().duplicate(),a.moveStart("character",-1),b=a.getBoundingClientRect(),{height:b.bottom-b.top,left:b.left,top:b.top}},b.prototype.getOffset=function(){var b,c,d,e,f;return j.getSelection&&(d=this.range())?(d.endOffset-1>0&&d.endContainer!==this.domInputor&&(b=d.cloneRange(),b.setStart(d.endContainer,d.endOffset-1),b.setEnd(d.endContainer,d.endOffset),e=b.getBoundingClientRect(),c={height:e.height,left:e.left+e.width,top:e.top},b.detach()),c&&0!==(null!=c?c.height:void 0)||(b=d.cloneRange(),f=a(h.createTextNode("|")),b.insertNode(f[0]),b.selectNode(f[0]),e=b.getBoundingClientRect(),c={height:e.height,left:e.left,top:e.top},f.remove(),b.detach())):h.selection&&(c=this.getOldIEOffset()),c&&(c.top+=a(j).scrollTop(),c.left+=a(j).scrollLeft()),c},b.prototype.range=function(){var a;if(j.getSelection)return a=j.getSelection(),a.rangeCount>0?a.getRangeAt(0):null},b}(),c=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.getIEPos=function(){var a,b,c,d,e,f,g;return b=this.domInputor,f=h.selection.createRange(),e=0,f&&f.parentElement()===b&&(d=b.value.replace(/\r\n/g,"\n"),c=d.length,g=b.createTextRange(),g.moveToBookmark(f.getBookmark()),a=b.createTextRange(),a.collapse(!1),e=g.compareEndPoints("StartToEnd",a)>-1?c:-g.moveStart("character",-c)),e},b.prototype.getPos=function(){return h.selection?this.getIEPos():this.domInputor.selectionStart},b.prototype.setPos=function(a){var b,c;return b=this.domInputor,h.selection?(c=b.createTextRange(),c.move("character",a),c.select()):b.setSelectionRange&&b.setSelectionRange(a,a),b},b.prototype.getIEOffset=function(a){var b,c,d,e;return c=this.domInputor.createTextRange(),a||(a=this.getPos()),c.move("character",a),d=c.boundingLeft,e=c.boundingTop,b=c.boundingHeight,{left:d,top:e,height:b}},b.prototype.getOffset=function(b){var c,d,e;return c=this.$inputor,h.selection?(d=this.getIEOffset(b),d.top+=a(j).scrollTop()+c.scrollTop(),d.left+=a(j).scrollLeft()+c.scrollLeft(),d):(d=c.offset(),e=this.getPosition(b),d={left:d.left+e.left-c.scrollLeft(),top:d.top+e.top-c.scrollTop(),height:e.height})},b.prototype.getPosition=function(a){var b,c,e,f,g,h,i;return b=this.$inputor,f=function(a){return a=a.replace(/<|>|`|"|&/g,"?").replace(/\r\n|\r|\n/g,"
      "),/firefox/i.test(navigator.userAgent)&&(a=a.replace(/\s/g," ")),a},void 0===a&&(a=this.getPos()),i=b.val().slice(0,a),e=b.val().slice(a),g=""+f(i)+"",g+="|",g+=""+f(e)+"",h=new d(b),c=h.create(g).rect()},b.prototype.getIEPosition=function(a){var b,c,d,e,f;return d=this.getIEOffset(a),c=this.$inputor.offset(),e=d.left-c.left,f=d.top-c.top,b=d.height,{left:e,top:f,height:b}},b}(),d=function(){function b(a){this.$inputor=a}return b.prototype.css_attr=["borderBottomWidth","borderLeftWidth","borderRightWidth","borderTopStyle","borderRightStyle","borderBottomStyle","borderLeftStyle","borderTopWidth","boxSizing","fontFamily","fontSize","fontWeight","height","letterSpacing","lineHeight","marginBottom","marginLeft","marginRight","marginTop","outlineWidth","overflow","overflowX","overflowY","paddingBottom","paddingLeft","paddingRight","paddingTop","textAlign","textOverflow","textTransform","whiteSpace","wordBreak","wordWrap"],b.prototype.mirrorCss=function(){var b,c=this;return b={position:"absolute",left:-9999,top:0,zIndex:-2e4},"TEXTAREA"===this.$inputor.prop("tagName")&&this.css_attr.push("width"),a.each(this.css_attr,function(a,d){return b[d]=c.$inputor.css(d)}),b},b.prototype.create=function(b){return this.$mirror=a("
      "),this.$mirror.css(this.mirrorCss()),this.$mirror.html(b),this.$inputor.after(this.$mirror),this},b.prototype.rect=function(){var a,b,c;return a=this.$mirror.find("#caret"),b=a.position(),c={left:b.left,top:b.top,height:a.height()},this.$mirror.remove(),c},b}(),e={contentEditable:function(a){return!(!a[0].contentEditable||"true"!==a[0].contentEditable)}},g={pos:function(a){return a||0===a?this.setPos(a):this.getPos()},position:function(a){return h.selection?this.getIEPosition(a):this.getPosition(a)},offset:function(a){var b;return b=this.getOffset(a)}},h=null,j=null,i=null,l=function(a){var b;return(b=null!=a?a.iframe:void 0)?(i=b,j=b.contentWindow,h=b.contentDocument||j.document):(i=void 0,j=window,h=document)},f=function(a){var b;h=a[0].ownerDocument,j=h.defaultView||h.parentWindow;try{return i=j.frameElement}catch(c){b=c}},a.fn.caret=function(d,f,h){var i;return g[d]?(a.isPlainObject(f)?(l(f),f=void 0):l(h),i=e.contentEditable(this)?new b(this):new c(this),g[d].apply(i,[f])):a.error("Method "+d+" does not exist on jQuery.caret")},a.fn.caret.EditableCaret=b,a.fn.caret.InputCaret=c,a.fn.caret.Utils=e,a.fn.caret.apis=g}); \ No newline at end of file diff --git a/phpBB/assets/javascript/jquery.tribute.js b/phpBB/assets/javascript/jquery.tribute.js deleted file mode 100644 index 9c46e63577..0000000000 --- a/phpBB/assets/javascript/jquery.tribute.js +++ /dev/null @@ -1,1898 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = global || self, global.Tribute = factory()); -}(this, (function () { 'use strict'; - - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - } - - function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - return Constructor; - } - - function _slicedToArray(arr, i) { - return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); - } - - function _arrayWithHoles(arr) { - if (Array.isArray(arr)) return arr; - } - - function _iterableToArrayLimit(arr, i) { - if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; - var _arr = []; - var _n = true; - var _d = false; - var _e = undefined; - - try { - for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { - _arr.push(_s.value); - - if (i && _arr.length === i) break; - } - } catch (err) { - _d = true; - _e = err; - } finally { - try { - if (!_n && _i["return"] != null) _i["return"](); - } finally { - if (_d) throw _e; - } - } - - return _arr; - } - - function _unsupportedIterableToArray(o, minLen) { - if (!o) return; - if (typeof o === "string") return _arrayLikeToArray(o, minLen); - var n = Object.prototype.toString.call(o).slice(8, -1); - if (n === "Object" && o.constructor) n = o.constructor.name; - if (n === "Map" || n === "Set") return Array.from(n); - if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); - } - - function _arrayLikeToArray(arr, len) { - if (len == null || len > arr.length) len = arr.length; - - for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; - - return arr2; - } - - function _nonIterableRest() { - throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); - } - - if (!Array.prototype.find) { - Array.prototype.find = function (predicate) { - if (this === null) { - throw new TypeError('Array.prototype.find called on null or undefined'); - } - - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - var list = Object(this); - var length = list.length >>> 0; - var thisArg = arguments[1]; - var value; - - for (var i = 0; i < length; i++) { - value = list[i]; - - if (predicate.call(thisArg, value, i, list)) { - return value; - } - } - - return undefined; - }; - } - - if (window && typeof window.CustomEvent !== "function") { - var CustomEvent$1 = function CustomEvent(event, params) { - params = params || { - bubbles: false, - cancelable: false, - detail: undefined - }; - var evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; - }; - - if (typeof window.Event !== 'undefined') { - CustomEvent$1.prototype = window.Event.prototype; - } - - window.CustomEvent = CustomEvent$1; - } - - var TributeEvents = /*#__PURE__*/function () { - function TributeEvents(tribute) { - _classCallCheck(this, TributeEvents); - - this.tribute = tribute; - this.tribute.events = this; - } - - _createClass(TributeEvents, [{ - key: "bind", - value: function bind(element) { - element.boundKeydown = this.keydown.bind(element, this); - element.boundKeyup = this.keyup.bind(element, this); - element.boundInput = this.input.bind(element, this); - element.addEventListener("keydown", element.boundKeydown, false); - element.addEventListener("keyup", element.boundKeyup, false); - element.addEventListener("input", element.boundInput, false); - } - }, { - key: "unbind", - value: function unbind(element) { - element.removeEventListener("keydown", element.boundKeydown, false); - element.removeEventListener("keyup", element.boundKeyup, false); - element.removeEventListener("input", element.boundInput, false); - delete element.boundKeydown; - delete element.boundKeyup; - delete element.boundInput; - } - }, { - key: "keydown", - value: function keydown(instance, event) { - if (instance.shouldDeactivate(event)) { - instance.tribute.isActive = false; - instance.tribute.hideMenu(); - } - - var element = this; - instance.commandEvent = false; - TributeEvents.keys().forEach(function (o) { - if (o.key === event.keyCode) { - instance.commandEvent = true; - instance.callbacks()[o.value.toLowerCase()](event, element); - } - }); - } - }, { - key: "input", - value: function input(instance, event) { - instance.inputEvent = true; - instance.keyup.call(this, instance, event); - } - }, { - key: "click", - value: function click(instance, event) { - var tribute = instance.tribute; - - if (tribute.menu && tribute.menu.contains(event.target)) { - var li = event.target; - event.preventDefault(); - event.stopPropagation(); - - while (li.nodeName.toLowerCase() !== "li") { - li = li.parentNode; - - if (!li || li === tribute.menu) { - throw new Error("cannot find the
    • container for the click"); - } - } - - tribute.selectItemAtIndex(li.getAttribute("data-index"), event); - tribute.hideMenu(); // TODO: should fire with externalTrigger and target is outside of menu - } else if (tribute.current.element && !tribute.current.externalTrigger) { - tribute.current.externalTrigger = false; - setTimeout(function () { - return tribute.hideMenu(); - }); - } - } - }, { - key: "keyup", - value: function keyup(instance, event) { - if (instance.inputEvent) { - instance.inputEvent = false; - } - - instance.updateSelection(this); - if (event.keyCode === 27) return; - - if (!instance.tribute.allowSpaces && instance.tribute.hasTrailingSpace) { - instance.tribute.hasTrailingSpace = false; - instance.commandEvent = true; - instance.callbacks()["space"](event, this); - return; - } - - if (!instance.tribute.isActive) { - if (instance.tribute.autocompleteMode) { - instance.callbacks().triggerChar(event, this, ""); - } else { - var keyCode = instance.getKeyCode(instance, this, event); - if (isNaN(keyCode) || !keyCode) return; - var trigger = instance.tribute.triggers().find(function (trigger) { - return trigger.charCodeAt(0) === keyCode; - }); - - if (typeof trigger !== "undefined") { - instance.callbacks().triggerChar(event, this, trigger); - } - } - } - - if (instance.tribute.current.mentionText.length < instance.tribute.current.collection.menuShowMinLength) { - return; - } - - if ((instance.tribute.current.trigger || instance.tribute.autocompleteMode) && instance.commandEvent === false || instance.tribute.isActive && event.keyCode === 8) { - instance.tribute.showMenuFor(this, true); - } - } - }, { - key: "shouldDeactivate", - value: function shouldDeactivate(event) { - if (!this.tribute.isActive) return false; - - if (this.tribute.current.mentionText.length === 0) { - var eventKeyPressed = false; - TributeEvents.keys().forEach(function (o) { - if (event.keyCode === o.key) eventKeyPressed = true; - }); - return !eventKeyPressed; - } - - return false; - } - }, { - key: "getKeyCode", - value: function getKeyCode(instance, el, event) { - - var tribute = instance.tribute; - var info = tribute.range.getTriggerInfo(false, tribute.hasTrailingSpace, true, tribute.allowSpaces, tribute.autocompleteMode); - - if (info) { - return info.mentionTriggerChar.charCodeAt(0); - } else { - return false; - } - } - }, { - key: "updateSelection", - value: function updateSelection(el) { - this.tribute.current.element = el; - var info = this.tribute.range.getTriggerInfo(false, this.tribute.hasTrailingSpace, true, this.tribute.allowSpaces, this.tribute.autocompleteMode); - - if (info) { - this.tribute.current.selectedPath = info.mentionSelectedPath; - this.tribute.current.mentionText = info.mentionText; - this.tribute.current.selectedOffset = info.mentionSelectedOffset; - } - } - }, { - key: "callbacks", - value: function callbacks() { - var _this = this; - - return { - triggerChar: function triggerChar(e, el, trigger) { - var tribute = _this.tribute; - tribute.current.trigger = trigger; - var collectionItem = tribute.collection.find(function (item) { - return item.trigger === trigger; - }); - tribute.current.collection = collectionItem; - - if (tribute.current.mentionText.length >= tribute.current.collection.menuShowMinLength && tribute.inputEvent) { - tribute.showMenuFor(el, true); - } - }, - enter: function enter(e, el) { - // choose selection - if (_this.tribute.isActive && _this.tribute.current.filteredItems) { - e.preventDefault(); - e.stopPropagation(); - setTimeout(function () { - _this.tribute.selectItemAtIndex(_this.tribute.menuSelected, e); - - _this.tribute.hideMenu(); - }, 0); - } - }, - escape: function escape(e, el) { - if (_this.tribute.isActive) { - e.preventDefault(); - e.stopPropagation(); - _this.tribute.isActive = false; - - _this.tribute.hideMenu(); - } - }, - tab: function tab(e, el) { - // choose first match - _this.callbacks().enter(e, el); - }, - space: function space(e, el) { - if (_this.tribute.isActive) { - if (_this.tribute.spaceSelectsMatch) { - _this.callbacks().enter(e, el); - } else if (!_this.tribute.allowSpaces) { - e.stopPropagation(); - setTimeout(function () { - _this.tribute.hideMenu(); - - _this.tribute.isActive = false; - }, 0); - } - } - }, - up: function up(e, el) { - // navigate up ul - if (_this.tribute.isActive && _this.tribute.current.filteredItems) { - e.preventDefault(); - e.stopPropagation(); - var count = _this.tribute.current.filteredItems.length, - selected = _this.tribute.menuSelected; - - if (count > selected && selected > 0) { - _this.tribute.menuSelected--; - - _this.setActiveLi(); - } else if (selected === 0) { - _this.tribute.menuSelected = count - 1; - - _this.setActiveLi(); - - _this.tribute.menu.scrollTop = _this.tribute.menu.scrollHeight; - } - } - }, - down: function down(e, el) { - // navigate down ul - if (_this.tribute.isActive && _this.tribute.current.filteredItems) { - e.preventDefault(); - e.stopPropagation(); - var count = _this.tribute.current.filteredItems.length - 1, - selected = _this.tribute.menuSelected; - - if (count > selected) { - _this.tribute.menuSelected++; - - _this.setActiveLi(); - } else if (count === selected) { - _this.tribute.menuSelected = 0; - - _this.setActiveLi(); - - _this.tribute.menu.scrollTop = 0; - } - } - }, - "delete": function _delete(e, el) { - if (_this.tribute.isActive && _this.tribute.current.mentionText.length < 1) { - _this.tribute.hideMenu(); - } else if (_this.tribute.isActive) { - _this.tribute.showMenuFor(el); - } - } - }; - } - }, { - key: "setActiveLi", - value: function setActiveLi(index) { - var lis = this.tribute.menu.querySelectorAll("li"), - length = lis.length >>> 0; - if (index) this.tribute.menuSelected = parseInt(index); - - for (var i = 0; i < length; i++) { - var li = lis[i]; - - if (i === this.tribute.menuSelected) { - li.classList.add(this.tribute.current.collection.selectClass); - var liClientRect = li.getBoundingClientRect(); - var menuClientRect = this.tribute.menu.getBoundingClientRect(); - - if (liClientRect.bottom > menuClientRect.bottom) { - var scrollDistance = liClientRect.bottom - menuClientRect.bottom; - this.tribute.menu.scrollTop += scrollDistance; - } else if (liClientRect.top < menuClientRect.top) { - var _scrollDistance = menuClientRect.top - liClientRect.top; - - this.tribute.menu.scrollTop -= _scrollDistance; - } - } else { - li.classList.remove(this.tribute.current.collection.selectClass); - } - } - } - }, { - key: "getFullHeight", - value: function getFullHeight(elem, includeMargin) { - var height = elem.getBoundingClientRect().height; - - if (includeMargin) { - var style = elem.currentStyle || window.getComputedStyle(elem); - return height + parseFloat(style.marginTop) + parseFloat(style.marginBottom); - } - - return height; - } - }], [{ - key: "keys", - value: function keys() { - return [{ - key: 9, - value: "TAB" - }, { - key: 8, - value: "DELETE" - }, { - key: 13, - value: "ENTER" - }, { - key: 27, - value: "ESCAPE" - }, { - key: 32, - value: "SPACE" - }, { - key: 38, - value: "UP" - }, { - key: 40, - value: "DOWN" - }]; - } - }]); - - return TributeEvents; - }(); - - var TributeMenuEvents = /*#__PURE__*/function () { - function TributeMenuEvents(tribute) { - _classCallCheck(this, TributeMenuEvents); - - this.tribute = tribute; - this.tribute.menuEvents = this; - this.menu = this.tribute.menu; - } - - _createClass(TributeMenuEvents, [{ - key: "bind", - value: function bind(menu) { - var _this = this; - - this.menuClickEvent = this.tribute.events.click.bind(null, this); - this.menuContainerScrollEvent = this.debounce(function () { - if (_this.tribute.isActive) { - _this.tribute.showMenuFor(_this.tribute.current.element, false); - } - }, 300, false); - this.windowResizeEvent = this.debounce(function () { - if (_this.tribute.isActive) { - _this.tribute.range.positionMenuAtCaret(true); - } - }, 300, false); // fixes IE11 issues with mousedown - - this.tribute.range.getDocument().addEventListener("MSPointerDown", this.menuClickEvent, false); - this.tribute.range.getDocument().addEventListener("mousedown", this.menuClickEvent, false); - window.addEventListener("resize", this.windowResizeEvent); - - if (this.menuContainer) { - this.menuContainer.addEventListener("scroll", this.menuContainerScrollEvent, false); - } else { - window.addEventListener("scroll", this.menuContainerScrollEvent); - } - } - }, { - key: "unbind", - value: function unbind(menu) { - this.tribute.range.getDocument().removeEventListener("mousedown", this.menuClickEvent, false); - this.tribute.range.getDocument().removeEventListener("MSPointerDown", this.menuClickEvent, false); - window.removeEventListener("resize", this.windowResizeEvent); - - if (this.menuContainer) { - this.menuContainer.removeEventListener("scroll", this.menuContainerScrollEvent, false); - } else { - window.removeEventListener("scroll", this.menuContainerScrollEvent); - } - } - }, { - key: "debounce", - value: function debounce(func, wait, immediate) { - var _arguments = arguments, - _this2 = this; - - var timeout; - return function () { - var context = _this2, - args = _arguments; - - var later = function later() { - timeout = null; - if (!immediate) func.apply(context, args); - }; - - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); - }; - } - }]); - - return TributeMenuEvents; - }(); - - var TributeRange = /*#__PURE__*/function () { - function TributeRange(tribute) { - _classCallCheck(this, TributeRange); - - this.tribute = tribute; - this.tribute.range = this; - } - - _createClass(TributeRange, [{ - key: "getDocument", - value: function getDocument() { - var iframe; - - if (this.tribute.current.collection) { - iframe = this.tribute.current.collection.iframe; - } - - if (!iframe) { - return document; - } - - return iframe.contentWindow.document; - } - }, { - key: "positionMenuAtCaret", - value: function positionMenuAtCaret(scrollTo) { - var _this = this; - - var context = this.tribute.current, - coordinates; - var info = this.getTriggerInfo(false, this.tribute.hasTrailingSpace, true, this.tribute.allowSpaces, this.tribute.autocompleteMode); - - if (typeof info !== 'undefined') { - if (!this.tribute.positionMenu) { - this.tribute.menu.style.cssText = "display: block;"; - return; - } - - if (!this.isContentEditable(context.element)) { - coordinates = this.getTextAreaOrInputUnderlinePosition(this.tribute.current.element, info.mentionPosition); - } else { - coordinates = this.getContentEditableCaretPosition(info.mentionPosition); - } - - this.tribute.menu.style.cssText = "top: ".concat(coordinates.top, "px;\n left: ").concat(coordinates.left, "px;\n right: ").concat(coordinates.right, "px;\n bottom: ").concat(coordinates.bottom, "px;\n position: absolute;\n display: block;"); - - if (coordinates.left === 'auto') { - this.tribute.menu.style.left = 'auto'; - } - - if (coordinates.top === 'auto') { - this.tribute.menu.style.top = 'auto'; - } - - if (scrollTo) this.scrollIntoView(); - window.setTimeout(function () { - var menuDimensions = { - width: _this.tribute.menu.offsetWidth, - height: _this.tribute.menu.offsetHeight - }; - - var menuIsOffScreen = _this.isMenuOffScreen(coordinates, menuDimensions); - - var menuIsOffScreenHorizontally = window.innerWidth > menuDimensions.width && (menuIsOffScreen.left || menuIsOffScreen.right); - var menuIsOffScreenVertically = window.innerHeight > menuDimensions.height && (menuIsOffScreen.top || menuIsOffScreen.bottom); - - if (menuIsOffScreenHorizontally || menuIsOffScreenVertically) { - _this.tribute.menu.style.cssText = 'display: none'; - - _this.positionMenuAtCaret(scrollTo); - } - }, 0); - } else { - this.tribute.menu.style.cssText = 'display: none'; - } - } - }, { - key: "selectElement", - value: function selectElement(targetElement, path, offset) { - var range; - var elem = targetElement; - - if (path) { - for (var i = 0; i < path.length; i++) { - elem = elem.childNodes[path[i]]; - - if (elem === undefined) { - return; - } - - while (elem.length < offset) { - offset -= elem.length; - elem = elem.nextSibling; - } - - if (elem.childNodes.length === 0 && !elem.length) { - elem = elem.previousSibling; - } - } - } - - var sel = this.getWindowSelection(); - range = this.getDocument().createRange(); - range.setStart(elem, offset); - range.setEnd(elem, offset); - range.collapse(true); - - try { - sel.removeAllRanges(); - } catch (error) {} - - sel.addRange(range); - targetElement.focus(); - } - }, { - key: "replaceTriggerText", - value: function replaceTriggerText(text, requireLeadingSpace, hasTrailingSpace, originalEvent, item) { - var info = this.getTriggerInfo(true, hasTrailingSpace, requireLeadingSpace, this.tribute.allowSpaces, this.tribute.autocompleteMode); - - if (info !== undefined) { - var context = this.tribute.current; - var replaceEvent = new CustomEvent('tribute-replaced', { - detail: { - item: item, - instance: context, - context: info, - event: originalEvent - } - }); - - if (!this.isContentEditable(context.element)) { - var myField = this.tribute.current.element; - var textSuffix = typeof this.tribute.replaceTextSuffix == 'string' ? this.tribute.replaceTextSuffix : ' '; - text += textSuffix; - var startPos = info.mentionPosition; - var endPos = info.mentionPosition + info.mentionText.length + textSuffix.length; - - if (!this.tribute.autocompleteMode) { - endPos += info.mentionTriggerChar.length - 1; - } - - myField.value = myField.value.substring(0, startPos) + text + myField.value.substring(endPos, myField.value.length); - myField.selectionStart = startPos + text.length; - myField.selectionEnd = startPos + text.length; - } else { - // add a space to the end of the pasted text - var _textSuffix = typeof this.tribute.replaceTextSuffix == 'string' ? this.tribute.replaceTextSuffix : '\xA0'; - - text += _textSuffix; - - var _endPos = info.mentionPosition + info.mentionText.length; - - if (!this.tribute.autocompleteMode) { - _endPos += info.mentionTriggerChar.length; - } - - this.pasteHtml(text, info.mentionPosition, _endPos); - } - - context.element.dispatchEvent(new CustomEvent('input', { - bubbles: true - })); - context.element.dispatchEvent(replaceEvent); - } - } - }, { - key: "pasteHtml", - value: function pasteHtml(html, startPos, endPos) { - var range, sel; - sel = this.getWindowSelection(); - range = this.getDocument().createRange(); - range.setStart(sel.anchorNode, startPos); - range.setEnd(sel.anchorNode, endPos); - range.deleteContents(); - var el = this.getDocument().createElement('div'); - el.innerHTML = html; - var frag = this.getDocument().createDocumentFragment(), - node, - lastNode; - - while (node = el.firstChild) { - lastNode = frag.appendChild(node); - } - - range.insertNode(frag); // Preserve the selection - - if (lastNode) { - range = range.cloneRange(); - range.setStartAfter(lastNode); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - } - } - }, { - key: "getWindowSelection", - value: function getWindowSelection() { - if (this.tribute.collection.iframe) { - return this.tribute.collection.iframe.contentWindow.getSelection(); - } - - return window.getSelection(); - } - }, { - key: "getNodePositionInParent", - value: function getNodePositionInParent(element) { - if (element.parentNode === null) { - return 0; - } - - for (var i = 0; i < element.parentNode.childNodes.length; i++) { - var node = element.parentNode.childNodes[i]; - - if (node === element) { - return i; - } - } - } - }, { - key: "getContentEditableSelectedPath", - value: function getContentEditableSelectedPath(ctx) { - var sel = this.getWindowSelection(); - var selected = sel.anchorNode; - var path = []; - var offset; - - if (selected != null) { - var i; - var ce = selected.contentEditable; - - while (selected !== null && ce !== 'true') { - i = this.getNodePositionInParent(selected); - path.push(i); - selected = selected.parentNode; - - if (selected !== null) { - ce = selected.contentEditable; - } - } - - path.reverse(); // getRangeAt may not exist, need alternative - - offset = sel.getRangeAt(0).startOffset; - return { - selected: selected, - path: path, - offset: offset - }; - } - } - }, { - key: "getTextPrecedingCurrentSelection", - value: function getTextPrecedingCurrentSelection() { - var context = this.tribute.current, - text = ''; - - if (!this.isContentEditable(context.element)) { - var textComponent = this.tribute.current.element; - - if (textComponent) { - var startPos = textComponent.selectionStart; - - if (textComponent.value && startPos >= 0) { - text = textComponent.value.substring(0, startPos); - } - } - } else { - var selectedElem = this.getWindowSelection().anchorNode; - - if (selectedElem != null) { - var workingNodeContent = selectedElem.textContent; - var selectStartOffset = this.getWindowSelection().getRangeAt(0).startOffset; - - if (workingNodeContent && selectStartOffset >= 0) { - text = workingNodeContent.substring(0, selectStartOffset); - } - } - } - - return text; - } - }, { - key: "getLastWordInText", - value: function getLastWordInText(text) { - text = text.replace(/\u00A0/g, ' '); // https://stackoverflow.com/questions/29850407/how-do-i-replace-unicode-character-u00a0-with-a-space-in-javascript - - var wordsArray; - - if (this.tribute.autocompleteSeparator) { - wordsArray = text.split(this.tribute.autocompleteSeparator); - } else { - wordsArray = text.split(/\s+/); - } - - var worldsCount = wordsArray.length - 1; - return wordsArray[worldsCount].trim(); - } - }, { - key: "getTriggerInfo", - value: function getTriggerInfo(menuAlreadyActive, hasTrailingSpace, requireLeadingSpace, allowSpaces, isAutocomplete) { - var _this2 = this; - - var ctx = this.tribute.current; - var selected, path, offset; - - if (!this.isContentEditable(ctx.element)) { - selected = this.tribute.current.element; - } else { - var selectionInfo = this.getContentEditableSelectedPath(ctx); - - if (selectionInfo) { - selected = selectionInfo.selected; - path = selectionInfo.path; - offset = selectionInfo.offset; - } - } - - var effectiveRange = this.getTextPrecedingCurrentSelection(); - var lastWordOfEffectiveRange = this.getLastWordInText(effectiveRange); - - if (isAutocomplete) { - return { - mentionPosition: effectiveRange.length - lastWordOfEffectiveRange.length, - mentionText: lastWordOfEffectiveRange, - mentionSelectedElement: selected, - mentionSelectedPath: path, - mentionSelectedOffset: offset - }; - } - - if (effectiveRange !== undefined && effectiveRange !== null) { - var mostRecentTriggerCharPos = -1; - var triggerChar; - this.tribute.collection.forEach(function (config) { - var c = config.trigger; - var idx = config.requireLeadingSpace ? _this2.lastIndexWithLeadingSpace(effectiveRange, c) : effectiveRange.lastIndexOf(c); - - if (idx > mostRecentTriggerCharPos) { - mostRecentTriggerCharPos = idx; - triggerChar = c; - requireLeadingSpace = config.requireLeadingSpace; - } - }); - - if (mostRecentTriggerCharPos >= 0 && (mostRecentTriggerCharPos === 0 || !requireLeadingSpace || /[\xA0\s]/g.test(effectiveRange.substring(mostRecentTriggerCharPos - 1, mostRecentTriggerCharPos)))) { - var currentTriggerSnippet = effectiveRange.substring(mostRecentTriggerCharPos + triggerChar.length, effectiveRange.length); - triggerChar = effectiveRange.substring(mostRecentTriggerCharPos, mostRecentTriggerCharPos + triggerChar.length); - var firstSnippetChar = currentTriggerSnippet.substring(0, 1); - var leadingSpace = currentTriggerSnippet.length > 0 && (firstSnippetChar === ' ' || firstSnippetChar === '\xA0'); - - if (hasTrailingSpace) { - currentTriggerSnippet = currentTriggerSnippet.trim(); - } - - var regex = allowSpaces ? /[^\S ]/g : /[\xA0\s]/g; - this.tribute.hasTrailingSpace = regex.test(currentTriggerSnippet); - - if (!leadingSpace && (menuAlreadyActive || !regex.test(currentTriggerSnippet))) { - return { - mentionPosition: mostRecentTriggerCharPos, - mentionText: currentTriggerSnippet, - mentionSelectedElement: selected, - mentionSelectedPath: path, - mentionSelectedOffset: offset, - mentionTriggerChar: triggerChar - }; - } - } - } - } - }, { - key: "lastIndexWithLeadingSpace", - value: function lastIndexWithLeadingSpace(str, trigger) { - var reversedStr = str.split('').reverse().join(''); - var index = -1; - - for (var cidx = 0, len = str.length; cidx < len; cidx++) { - var firstChar = cidx === str.length - 1; - var leadingSpace = /\s/.test(reversedStr[cidx + 1]); - var match = true; - - for (var triggerIdx = trigger.length - 1; triggerIdx >= 0; triggerIdx--) { - if (trigger[triggerIdx] !== reversedStr[cidx - triggerIdx]) { - match = false; - break; - } - } - - if (match && (firstChar || leadingSpace)) { - index = str.length - 1 - cidx; - break; - } - } - - return index; - } - }, { - key: "isContentEditable", - value: function isContentEditable(element) { - return element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA'; - } - }, { - key: "isMenuOffScreen", - value: function isMenuOffScreen(coordinates, menuDimensions) { - var windowWidth = window.innerWidth; - var windowHeight = window.innerHeight; - var doc = document.documentElement; - var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); - var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); - var menuTop = typeof coordinates.top === 'number' ? coordinates.top : windowTop + windowHeight - coordinates.bottom - menuDimensions.height; - var menuRight = typeof coordinates.right === 'number' ? coordinates.right : coordinates.left + menuDimensions.width; - var menuBottom = typeof coordinates.bottom === 'number' ? coordinates.bottom : coordinates.top + menuDimensions.height; - var menuLeft = typeof coordinates.left === 'number' ? coordinates.left : windowLeft + windowWidth - coordinates.right - menuDimensions.width; - return { - top: menuTop < Math.floor(windowTop), - right: menuRight > Math.ceil(windowLeft + windowWidth), - bottom: menuBottom > Math.ceil(windowTop + windowHeight), - left: menuLeft < Math.floor(windowLeft) - }; - } - }, { - key: "getMenuDimensions", - value: function getMenuDimensions() { - // Width of the menu depends of its contents and position - // We must check what its width would be without any obstruction - // This way, we can achieve good positioning for flipping the menu - var dimensions = { - width: null, - height: null - }; - this.tribute.menu.style.cssText = "top: 0px;\n left: 0px;\n position: fixed;\n display: block;\n visibility; hidden;"; - dimensions.width = this.tribute.menu.offsetWidth; - dimensions.height = this.tribute.menu.offsetHeight; - this.tribute.menu.style.cssText = "display: none;"; - return dimensions; - } - }, { - key: "getTextAreaOrInputUnderlinePosition", - value: function getTextAreaOrInputUnderlinePosition(element, position, flipped) { - var properties = ['direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', 'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing']; - var isFirefox = window.mozInnerScreenX !== null; - var div = this.getDocument().createElement('div'); - div.id = 'input-textarea-caret-position-mirror-div'; - this.getDocument().body.appendChild(div); - var style = div.style; - var computed = window.getComputedStyle ? getComputedStyle(element) : element.currentStyle; - style.whiteSpace = 'pre-wrap'; - - if (element.nodeName !== 'INPUT') { - style.wordWrap = 'break-word'; - } // position off-screen - - - style.position = 'absolute'; - style.visibility = 'hidden'; // transfer the element's properties to the div - - properties.forEach(function (prop) { - style[prop] = computed[prop]; - }); - - if (isFirefox) { - style.width = "".concat(parseInt(computed.width) - 2, "px"); - if (element.scrollHeight > parseInt(computed.height)) style.overflowY = 'scroll'; - } else { - style.overflow = 'hidden'; - } - - div.textContent = element.value.substring(0, position); - - if (element.nodeName === 'INPUT') { - div.textContent = div.textContent.replace(/\s/g, ' '); - } - - var span = this.getDocument().createElement('span'); - span.textContent = element.value.substring(position) || '.'; - div.appendChild(span); - var rect = element.getBoundingClientRect(); - var doc = document.documentElement; - var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); - var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); - var top = 0; - var left = 0; - - if (this.menuContainerIsBody) { - top = rect.top; - left = rect.left; - } - - var coordinates = { - top: top + windowTop + span.offsetTop + parseInt(computed.borderTopWidth) + parseInt(computed.fontSize) - element.scrollTop, - left: left + windowLeft + span.offsetLeft + parseInt(computed.borderLeftWidth) - }; - var windowWidth = window.innerWidth; - var windowHeight = window.innerHeight; - var menuDimensions = this.getMenuDimensions(); - var menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); - - if (menuIsOffScreen.right) { - coordinates.right = windowWidth - coordinates.left; - coordinates.left = 'auto'; - } - - var parentHeight = this.tribute.menuContainer ? this.tribute.menuContainer.offsetHeight : this.getDocument().body.offsetHeight; - - if (menuIsOffScreen.bottom) { - var parentRect = this.tribute.menuContainer ? this.tribute.menuContainer.getBoundingClientRect() : this.getDocument().body.getBoundingClientRect(); - var scrollStillAvailable = parentHeight - (windowHeight - parentRect.top); - coordinates.bottom = scrollStillAvailable + (windowHeight - rect.top - span.offsetTop); - coordinates.top = 'auto'; - } - - menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); - - if (menuIsOffScreen.left) { - coordinates.left = windowWidth > menuDimensions.width ? windowLeft + windowWidth - menuDimensions.width : windowLeft; - delete coordinates.right; - } - - if (menuIsOffScreen.top) { - coordinates.top = windowHeight > menuDimensions.height ? windowTop + windowHeight - menuDimensions.height : windowTop; - delete coordinates.bottom; - } - - this.getDocument().body.removeChild(div); - return coordinates; - } - }, { - key: "getContentEditableCaretPosition", - value: function getContentEditableCaretPosition(selectedNodePosition) { - var range; - var sel = this.getWindowSelection(); - range = this.getDocument().createRange(); - range.setStart(sel.anchorNode, selectedNodePosition); - range.setEnd(sel.anchorNode, selectedNodePosition); - range.collapse(false); - var rect = range.getBoundingClientRect(); - var doc = document.documentElement; - var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); - var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); - var left = rect.left; - var top = rect.top; - var coordinates = { - left: left + windowLeft, - top: top + rect.height + windowTop - }; - var windowWidth = window.innerWidth; - var windowHeight = window.innerHeight; - var menuDimensions = this.getMenuDimensions(); - var menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); - - if (menuIsOffScreen.right) { - coordinates.left = 'auto'; - coordinates.right = windowWidth - rect.left - windowLeft; - } - - var parentHeight = this.tribute.menuContainer ? this.tribute.menuContainer.offsetHeight : this.getDocument().body.offsetHeight; - - if (menuIsOffScreen.bottom) { - var parentRect = this.tribute.menuContainer ? this.tribute.menuContainer.getBoundingClientRect() : this.getDocument().body.getBoundingClientRect(); - var scrollStillAvailable = parentHeight - (windowHeight - parentRect.top); - coordinates.top = 'auto'; - coordinates.bottom = scrollStillAvailable + (windowHeight - rect.top); - } - - menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); - - if (menuIsOffScreen.left) { - coordinates.left = windowWidth > menuDimensions.width ? windowLeft + windowWidth - menuDimensions.width : windowLeft; - delete coordinates.right; - } - - if (menuIsOffScreen.top) { - coordinates.top = windowHeight > menuDimensions.height ? windowTop + windowHeight - menuDimensions.height : windowTop; - delete coordinates.bottom; - } - - if (!this.menuContainerIsBody) { - coordinates.left = coordinates.left ? coordinates.left - this.tribute.menuContainer.offsetLeft : coordinates.left; - coordinates.top = coordinates.top ? coordinates.top - this.tribute.menuContainer.offsetTop : coordinates.top; - } - - return coordinates; - } - }, { - key: "scrollIntoView", - value: function scrollIntoView(elem) { - var reasonableBuffer = 20, - clientRect; - var maxScrollDisplacement = 100; - var e = this.menu; - if (typeof e === 'undefined') return; - - while (clientRect === undefined || clientRect.height === 0) { - clientRect = e.getBoundingClientRect(); - - if (clientRect.height === 0) { - e = e.childNodes[0]; - - if (e === undefined || !e.getBoundingClientRect) { - return; - } - } - } - - var elemTop = clientRect.top; - var elemBottom = elemTop + clientRect.height; - - if (elemTop < 0) { - window.scrollTo(0, window.pageYOffset + clientRect.top - reasonableBuffer); - } else if (elemBottom > window.innerHeight) { - var maxY = window.pageYOffset + clientRect.top - reasonableBuffer; - - if (maxY - window.pageYOffset > maxScrollDisplacement) { - maxY = window.pageYOffset + maxScrollDisplacement; - } - - var targetY = window.pageYOffset - (window.innerHeight - elemBottom); - - if (targetY > maxY) { - targetY = maxY; - } - - window.scrollTo(0, targetY); - } - } - }, { - key: "menuContainerIsBody", - get: function get() { - return this.tribute.menuContainer === document.body || !this.tribute.menuContainer; - } - }]); - - return TributeRange; - }(); - - // Thanks to https://github.com/mattyork/fuzzy - var TributeSearch = /*#__PURE__*/function () { - function TributeSearch(tribute) { - _classCallCheck(this, TributeSearch); - - this.tribute = tribute; - this.tribute.search = this; - } - - _createClass(TributeSearch, [{ - key: "simpleFilter", - value: function simpleFilter(pattern, array) { - var _this = this; - - return array.filter(function (string) { - return _this.test(pattern, string); - }); - } - }, { - key: "test", - value: function test(pattern, string) { - return this.match(pattern, string) !== null; - } - }, { - key: "match", - value: function match(pattern, string, opts) { - opts = opts || {}; - var len = string.length, - pre = opts.pre || '', - post = opts.post || '', - compareString = opts.caseSensitive && string || string.toLowerCase(); - - if (opts.skip) { - return { - rendered: string, - score: 0 - }; - } - - pattern = opts.caseSensitive && pattern || pattern.toLowerCase(); - var patternCache = this.traverse(compareString, pattern, 0, 0, []); - - if (!patternCache) { - return null; - } - - return { - rendered: this.render(string, patternCache.cache, pre, post), - score: patternCache.score - }; - } - }, { - key: "traverse", - value: function traverse(string, pattern, stringIndex, patternIndex, patternCache) { - if (this.tribute.autocompleteSeparator) { - // if the pattern search at end - pattern = pattern.split(this.tribute.autocompleteSeparator).splice(-1)[0]; - } - - if (pattern.length === patternIndex) { - // calculate score and copy the cache containing the indices where it's found - return { - score: this.calculateScore(patternCache), - cache: patternCache.slice() - }; - } // if string at end or remaining pattern > remaining string - - - if (string.length === stringIndex || pattern.length - patternIndex > string.length - stringIndex) { - return undefined; - } - - var c = pattern[patternIndex]; - var index = string.indexOf(c, stringIndex); - var best, temp; - - while (index > -1) { - patternCache.push(index); - temp = this.traverse(string, pattern, index + 1, patternIndex + 1, patternCache); - patternCache.pop(); // if downstream traversal failed, return best answer so far - - if (!temp) { - return best; - } - - if (!best || best.score < temp.score) { - best = temp; - } - - index = string.indexOf(c, index + 1); - } - - return best; - } - }, { - key: "calculateScore", - value: function calculateScore(patternCache) { - var score = 0; - var temp = 1; - patternCache.forEach(function (index, i) { - if (i > 0) { - if (patternCache[i - 1] + 1 === index) { - temp += temp + 1; - } else { - temp = 1; - } - } - - score += temp; - }); - return score; - } - }, { - key: "render", - value: function render(string, indices, pre, post) { - var rendered = string.substring(0, indices[0]); - indices.forEach(function (index, i) { - rendered += pre + string[index] + post + string.substring(index + 1, indices[i + 1] ? indices[i + 1] : string.length); - }); - return rendered; - } - }, { - key: "filter", - value: function filter(pattern, arr, opts) { - var _this2 = this; - - opts = opts || {}; - return arr.reduce(function (prev, element, idx, arr) { - var str = element; - - if (opts.extract) { - str = opts.extract(element); - - if (!str) { - // take care of undefineds / nulls / etc. - str = ''; - } - } - - var rendered = _this2.match(pattern, str, opts); - - if (rendered != null) { - prev[prev.length] = { - string: rendered.rendered, - score: rendered.score, - index: idx, - original: element - }; - } - - return prev; - }, []).sort(function (a, b) { - var compare = b.score - a.score; - if (compare) return compare; - return a.index - b.index; - }); - } - }]); - - return TributeSearch; - }(); - - var Tribute = /*#__PURE__*/function () { - function Tribute(_ref) { - var _this = this; - - var _ref$values = _ref.values, - values = _ref$values === void 0 ? null : _ref$values, - _ref$loadingItemTempl = _ref.loadingItemTemplate, - loadingItemTemplate = _ref$loadingItemTempl === void 0 ? null : _ref$loadingItemTempl, - _ref$iframe = _ref.iframe, - iframe = _ref$iframe === void 0 ? null : _ref$iframe, - _ref$selectClass = _ref.selectClass, - selectClass = _ref$selectClass === void 0 ? "highlight" : _ref$selectClass, - _ref$containerClass = _ref.containerClass, - containerClass = _ref$containerClass === void 0 ? "tribute-container" : _ref$containerClass, - _ref$itemClass = _ref.itemClass, - itemClass = _ref$itemClass === void 0 ? "" : _ref$itemClass, - _ref$trigger = _ref.trigger, - trigger = _ref$trigger === void 0 ? "@" : _ref$trigger, - _ref$autocompleteMode = _ref.autocompleteMode, - autocompleteMode = _ref$autocompleteMode === void 0 ? false : _ref$autocompleteMode, - _ref$autocompleteSepa = _ref.autocompleteSeparator, - autocompleteSeparator = _ref$autocompleteSepa === void 0 ? null : _ref$autocompleteSepa, - _ref$selectTemplate = _ref.selectTemplate, - selectTemplate = _ref$selectTemplate === void 0 ? null : _ref$selectTemplate, - _ref$menuItemTemplate = _ref.menuItemTemplate, - menuItemTemplate = _ref$menuItemTemplate === void 0 ? null : _ref$menuItemTemplate, - _ref$lookup = _ref.lookup, - lookup = _ref$lookup === void 0 ? "key" : _ref$lookup, - _ref$fillAttr = _ref.fillAttr, - fillAttr = _ref$fillAttr === void 0 ? "value" : _ref$fillAttr, - _ref$collection = _ref.collection, - collection = _ref$collection === void 0 ? null : _ref$collection, - _ref$menuContainer = _ref.menuContainer, - menuContainer = _ref$menuContainer === void 0 ? null : _ref$menuContainer, - _ref$noMatchTemplate = _ref.noMatchTemplate, - noMatchTemplate = _ref$noMatchTemplate === void 0 ? null : _ref$noMatchTemplate, - _ref$requireLeadingSp = _ref.requireLeadingSpace, - requireLeadingSpace = _ref$requireLeadingSp === void 0 ? true : _ref$requireLeadingSp, - _ref$allowSpaces = _ref.allowSpaces, - allowSpaces = _ref$allowSpaces === void 0 ? false : _ref$allowSpaces, - _ref$replaceTextSuffi = _ref.replaceTextSuffix, - replaceTextSuffix = _ref$replaceTextSuffi === void 0 ? null : _ref$replaceTextSuffi, - _ref$positionMenu = _ref.positionMenu, - positionMenu = _ref$positionMenu === void 0 ? true : _ref$positionMenu, - _ref$spaceSelectsMatc = _ref.spaceSelectsMatch, - spaceSelectsMatch = _ref$spaceSelectsMatc === void 0 ? false : _ref$spaceSelectsMatc, - _ref$searchOpts = _ref.searchOpts, - searchOpts = _ref$searchOpts === void 0 ? {} : _ref$searchOpts, - _ref$menuItemLimit = _ref.menuItemLimit, - menuItemLimit = _ref$menuItemLimit === void 0 ? null : _ref$menuItemLimit, - _ref$menuShowMinLengt = _ref.menuShowMinLength, - menuShowMinLength = _ref$menuShowMinLengt === void 0 ? 0 : _ref$menuShowMinLengt; - - _classCallCheck(this, Tribute); - - this.autocompleteMode = autocompleteMode; - this.autocompleteSeparator = autocompleteSeparator; - this.menuSelected = 0; - this.current = {}; - this.inputEvent = false; - this.isActive = false; - this.menuContainer = menuContainer; - this.allowSpaces = allowSpaces; - this.replaceTextSuffix = replaceTextSuffix; - this.positionMenu = positionMenu; - this.hasTrailingSpace = false; - this.spaceSelectsMatch = spaceSelectsMatch; - - if (this.autocompleteMode) { - trigger = ""; - allowSpaces = false; - } - - if (values) { - this.collection = [{ - // symbol that starts the lookup - trigger: trigger, - // is it wrapped in an iframe - iframe: iframe, - // class applied to selected item - selectClass: selectClass, - // class applied to the Container - containerClass: containerClass, - // class applied to each item - itemClass: itemClass, - // function called on select that retuns the content to insert - selectTemplate: (selectTemplate || Tribute.defaultSelectTemplate).bind(this), - // function called that returns content for an item - menuItemTemplate: (menuItemTemplate || Tribute.defaultMenuItemTemplate).bind(this), - // function called when menu is empty, disables hiding of menu. - noMatchTemplate: function (t) { - if (typeof t === "string") { - if (t.trim() === "") return null; - return t; - } - - if (typeof t === "function") { - return t.bind(_this); - } - - return noMatchTemplate || function () { - return "
    • No Match Found!
    • "; - }.bind(_this); - }(noMatchTemplate), - // column to search against in the object - lookup: lookup, - // column that contains the content to insert by default - fillAttr: fillAttr, - // array of objects or a function returning an array of objects - values: values, - // useful for when values is an async function - loadingItemTemplate: loadingItemTemplate, - requireLeadingSpace: requireLeadingSpace, - searchOpts: searchOpts, - menuItemLimit: menuItemLimit, - menuShowMinLength: menuShowMinLength - }]; - } else if (collection) { - if (this.autocompleteMode) console.warn("Tribute in autocomplete mode does not work for collections"); - this.collection = collection.map(function (item) { - return { - trigger: item.trigger || trigger, - iframe: item.iframe || iframe, - selectClass: item.selectClass || selectClass, - containerClass: item.containerClass || containerClass, - itemClass: item.itemClass || itemClass, - selectTemplate: (item.selectTemplate || Tribute.defaultSelectTemplate).bind(_this), - menuItemTemplate: (item.menuItemTemplate || Tribute.defaultMenuItemTemplate).bind(_this), - // function called when menu is empty, disables hiding of menu. - noMatchTemplate: function (t) { - if (typeof t === "string") { - if (t.trim() === "") return null; - return t; - } - - if (typeof t === "function") { - return t.bind(_this); - } - - return noMatchTemplate || function () { - return "
    • No Match Found!
    • "; - }.bind(_this); - }(noMatchTemplate), - lookup: item.lookup || lookup, - fillAttr: item.fillAttr || fillAttr, - values: item.values, - loadingItemTemplate: item.loadingItemTemplate, - requireLeadingSpace: item.requireLeadingSpace, - searchOpts: item.searchOpts || searchOpts, - menuItemLimit: item.menuItemLimit || menuItemLimit, - menuShowMinLength: item.menuShowMinLength || menuShowMinLength - }; - }); - } else { - throw new Error("[Tribute] No collection specified."); - } - - new TributeRange(this); - new TributeEvents(this); - new TributeMenuEvents(this); - new TributeSearch(this); - } - - _createClass(Tribute, [{ - key: "triggers", - value: function triggers() { - return this.collection.map(function (config) { - return config.trigger; - }); - } - }, { - key: "attach", - value: function attach(el) { - if (!el) { - throw new Error("[Tribute] Must pass in a DOM node or NodeList."); - } // Check if it is a jQuery collection - - - if (typeof jQuery !== "undefined" && el instanceof jQuery) { - el = el.get(); - } // Is el an Array/Array-like object? - - - if (el.constructor === NodeList || el.constructor === HTMLCollection || el.constructor === Array) { - var length = el.length; - - for (var i = 0; i < length; ++i) { - this._attach(el[i]); - } - } else { - this._attach(el); - } - } - }, { - key: "_attach", - value: function _attach(el) { - if (el.hasAttribute("data-tribute")) { - console.warn("Tribute was already bound to " + el.nodeName); - } - - this.ensureEditable(el); - this.events.bind(el); - el.setAttribute("data-tribute", true); - } - }, { - key: "ensureEditable", - value: function ensureEditable(element) { - if (Tribute.inputTypes().indexOf(element.nodeName) === -1) { - if (element.contentEditable) { - element.contentEditable = true; - } else { - throw new Error("[Tribute] Cannot bind to " + element.nodeName); - } - } - } - }, { - key: "createMenu", - value: function createMenu(containerClass) { - var wrapper = this.range.getDocument().createElement("div"), - ul = this.range.getDocument().createElement("ul"); - wrapper.className = containerClass; - wrapper.appendChild(ul); - - if (this.menuContainer) { - return this.menuContainer.appendChild(wrapper); - } - - return this.range.getDocument().body.appendChild(wrapper); - } - }, { - key: "showMenuFor", - value: function showMenuFor(element, scrollTo) { - var _this2 = this; - - // Only proceed if menu isn't already shown for the current element & mentionText - if (this.isActive && this.current.element === element && this.current.mentionText === this.currentMentionTextSnapshot) { - return; - } - - this.currentMentionTextSnapshot = this.current.mentionText; // create the menu if it doesn't exist. - - if (!this.menu) { - this.menu = this.createMenu(this.current.collection.containerClass); - element.tributeMenu = this.menu; - this.menuEvents.bind(this.menu); - } - - this.isActive = true; - this.menuSelected = 0; - - if (!this.current.mentionText) { - this.current.mentionText = ""; - } - - var processValues = function processValues(values) { - // Tribute may not be active any more by the time the value callback returns - if (!_this2.isActive) { - return; - } - - var items = _this2.search.filter(_this2.current.mentionText, values, { - pre: _this2.current.collection.searchOpts.pre || "", - post: _this2.current.collection.searchOpts.post || "", - skip: _this2.current.collection.searchOpts.skip, - extract: function extract(el) { - if (typeof _this2.current.collection.lookup === "string") { - return el[_this2.current.collection.lookup]; - } else if (typeof _this2.current.collection.lookup === "function") { - return _this2.current.collection.lookup(el, _this2.current.mentionText); - } else { - throw new Error("Invalid lookup attribute, lookup must be string or function."); - } - } - }); - - if (_this2.current.collection.menuItemLimit) { - items = items.slice(0, _this2.current.collection.menuItemLimit); - } - - _this2.current.filteredItems = items; - - var ul = _this2.menu.querySelector("ul"); - - _this2.range.positionMenuAtCaret(scrollTo); - - if (!items.length) { - var noMatchEvent = new CustomEvent("tribute-no-match", { - detail: _this2.menu - }); - - _this2.current.element.dispatchEvent(noMatchEvent); - - if (typeof _this2.current.collection.noMatchTemplate === "function" && !_this2.current.collection.noMatchTemplate() || !_this2.current.collection.noMatchTemplate) { - _this2.hideMenu(); - } else { - typeof _this2.current.collection.noMatchTemplate === "function" ? ul.innerHTML = _this2.current.collection.noMatchTemplate() : ul.innerHTML = _this2.current.collection.noMatchTemplate; - } - - return; - } - - ul.innerHTML = ""; - - var fragment = _this2.range.getDocument().createDocumentFragment(); - - items.forEach(function (item, index) { - var li = _this2.range.getDocument().createElement("li"); - - li.setAttribute("data-index", index); - li.className = _this2.current.collection.itemClass; - li.addEventListener("mousemove", function (e) { - var _this2$_findLiTarget = _this2._findLiTarget(e.target), - _this2$_findLiTarget2 = _slicedToArray(_this2$_findLiTarget, 2), - li = _this2$_findLiTarget2[0], - index = _this2$_findLiTarget2[1]; - - if (e.movementY !== 0) { - _this2.events.setActiveLi(index); - } - }); - - if (_this2.menuSelected === index) { - li.classList.add(_this2.current.collection.selectClass); - } - - li.innerHTML = _this2.current.collection.menuItemTemplate(item); - fragment.appendChild(li); - }); - ul.appendChild(fragment); - }; - - if (typeof this.current.collection.values === "function") { - if (this.current.collection.loadingItemTemplate) { - this.menu.querySelector("ul").innerHTML = this.current.collection.loadingItemTemplate; - this.range.positionMenuAtCaret(scrollTo); - } - - this.current.collection.values(this.current.mentionText, processValues); - } else { - processValues(this.current.collection.values); - } - } - }, { - key: "_findLiTarget", - value: function _findLiTarget(el) { - if (!el) return []; - var index = el.getAttribute("data-index"); - return !index ? this._findLiTarget(el.parentNode) : [el, index]; - } - }, { - key: "showMenuForCollection", - value: function showMenuForCollection(element, collectionIndex) { - if (element !== document.activeElement) { - this.placeCaretAtEnd(element); - } - - this.current.collection = this.collection[collectionIndex || 0]; - this.current.externalTrigger = true; - this.current.element = element; - if (element.isContentEditable) this.insertTextAtCursor(this.current.collection.trigger);else this.insertAtCaret(element, this.current.collection.trigger); - this.showMenuFor(element); - } // TODO: make sure this works for inputs/textareas - - }, { - key: "placeCaretAtEnd", - value: function placeCaretAtEnd(el) { - el.focus(); - - if (typeof window.getSelection != "undefined" && typeof document.createRange != "undefined") { - var range = document.createRange(); - range.selectNodeContents(el); - range.collapse(false); - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - } else if (typeof document.body.createTextRange != "undefined") { - var textRange = document.body.createTextRange(); - textRange.moveToElementText(el); - textRange.collapse(false); - textRange.select(); - } - } // for contenteditable - - }, { - key: "insertTextAtCursor", - value: function insertTextAtCursor(text) { - var sel, range; - sel = window.getSelection(); - range = sel.getRangeAt(0); - range.deleteContents(); - var textNode = document.createTextNode(text); - range.insertNode(textNode); - range.selectNodeContents(textNode); - range.collapse(false); - sel.removeAllRanges(); - sel.addRange(range); - } // for regular inputs - - }, { - key: "insertAtCaret", - value: function insertAtCaret(textarea, text) { - var scrollPos = textarea.scrollTop; - var caretPos = textarea.selectionStart; - var front = textarea.value.substring(0, caretPos); - var back = textarea.value.substring(textarea.selectionEnd, textarea.value.length); - textarea.value = front + text + back; - caretPos = caretPos + text.length; - textarea.selectionStart = caretPos; - textarea.selectionEnd = caretPos; - textarea.focus(); - textarea.scrollTop = scrollPos; - } - }, { - key: "hideMenu", - value: function hideMenu() { - if (this.menu) { - this.menu.style.cssText = "display: none;"; - this.isActive = false; - this.menuSelected = 0; - this.current = {}; - } - } - }, { - key: "selectItemAtIndex", - value: function selectItemAtIndex(index, originalEvent) { - index = parseInt(index); - if (typeof index !== "number" || isNaN(index)) return; - var item = this.current.filteredItems[index]; - var content = this.current.collection.selectTemplate(item); - if (content !== null) this.replaceText(content, originalEvent, item); - } - }, { - key: "replaceText", - value: function replaceText(content, originalEvent, item) { - this.range.replaceTriggerText(content, true, true, originalEvent, item); - } - }, { - key: "_append", - value: function _append(collection, newValues, replace) { - if (typeof collection.values === "function") { - throw new Error("Unable to append to values, as it is a function."); - } else if (!replace) { - collection.values = collection.values.concat(newValues); - } else { - collection.values = newValues; - } - } - }, { - key: "append", - value: function append(collectionIndex, newValues, replace) { - var index = parseInt(collectionIndex); - if (typeof index !== "number") throw new Error("please provide an index for the collection to update."); - var collection = this.collection[index]; - - this._append(collection, newValues, replace); - } - }, { - key: "appendCurrent", - value: function appendCurrent(newValues, replace) { - if (this.isActive) { - this._append(this.current.collection, newValues, replace); - } else { - throw new Error("No active state. Please use append instead and pass an index."); - } - } - }, { - key: "detach", - value: function detach(el) { - if (!el) { - throw new Error("[Tribute] Must pass in a DOM node or NodeList."); - } // Check if it is a jQuery collection - - - if (typeof jQuery !== "undefined" && el instanceof jQuery) { - el = el.get(); - } // Is el an Array/Array-like object? - - - if (el.constructor === NodeList || el.constructor === HTMLCollection || el.constructor === Array) { - var length = el.length; - - for (var i = 0; i < length; ++i) { - this._detach(el[i]); - } - } else { - this._detach(el); - } - } - }, { - key: "_detach", - value: function _detach(el) { - var _this3 = this; - - this.events.unbind(el); - - if (el.tributeMenu) { - this.menuEvents.unbind(el.tributeMenu); - } - - setTimeout(function () { - el.removeAttribute("data-tribute"); - _this3.isActive = false; - - if (el.tributeMenu) { - el.tributeMenu.remove(); - } - }); - } - }, { - key: "isActive", - get: function get() { - return this._isActive; - }, - set: function set(val) { - if (this._isActive != val) { - this._isActive = val; - - if (this.current.element) { - var noMatchEvent = new CustomEvent("tribute-active-".concat(val)); - this.current.element.dispatchEvent(noMatchEvent); - } - } - } - }], [{ - key: "defaultSelectTemplate", - value: function defaultSelectTemplate(item) { - if (typeof item === "undefined") return "".concat(this.current.collection.trigger).concat(this.current.mentionText); - - if (this.range.isContentEditable(this.current.element)) { - return '' + (this.current.collection.trigger + item.original[this.current.collection.fillAttr]) + ""; - } - - return this.current.collection.trigger + item.original[this.current.collection.fillAttr]; - } - }, { - key: "defaultMenuItemTemplate", - value: function defaultMenuItemTemplate(matchItem) { - return matchItem.string; - } - }, { - key: "inputTypes", - value: function inputTypes() { - return ["TEXTAREA", "INPUT"]; - } - }]); - - return Tribute; - }(); - - /** - * Tribute.js - * Native ES6 JavaScript @mention Plugin - **/ - - return Tribute; - -}))); diff --git a/phpBB/assets/javascript/tribute.min.js b/phpBB/assets/javascript/tribute.min.js new file mode 100644 index 0000000000..4d11a82932 --- /dev/null +++ b/phpBB/assets/javascript/tribute.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Tribute=t()}(this,(function(){"use strict";function e(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function t(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,i=new Array(t);n>>0,r=arguments[1],o=0;o container for the click");n.selectItemAtIndex(i.getAttribute("data-index"),t),n.hideMenu()}else n.current.element&&!n.current.externalTrigger&&(n.current.externalTrigger=!1,setTimeout((function(){return n.hideMenu()})))}},{key:"keyup",value:function(e,t){if(e.inputEvent&&(e.inputEvent=!1),e.updateSelection(this),27!==t.keyCode){if(!e.tribute.allowSpaces&&e.tribute.hasTrailingSpace)return e.tribute.hasTrailingSpace=!1,e.commandEvent=!0,void e.callbacks().space(t,this);if(!e.tribute.isActive)if(e.tribute.autocompleteMode)e.callbacks().triggerChar(t,this,"");else{var n=e.getKeyCode(e,this,t);if(isNaN(n)||!n)return;var i=e.tribute.triggers().find((function(e){return e.charCodeAt(0)===n}));void 0!==i&&e.callbacks().triggerChar(t,this,i)}e.tribute.current.mentionText.length=r.current.collection.menuShowMinLength&&r.inputEvent&&r.showMenuFor(n,!0)},enter:function(t,n){e.tribute.isActive&&e.tribute.current.filteredItems&&(t.preventDefault(),t.stopPropagation(),setTimeout((function(){e.tribute.selectItemAtIndex(e.tribute.menuSelected,t),e.tribute.hideMenu()}),0))},escape:function(t,n){e.tribute.isActive&&(t.preventDefault(),t.stopPropagation(),e.tribute.isActive=!1,e.tribute.hideMenu())},tab:function(t,n){e.callbacks().enter(t,n)},space:function(t,n){e.tribute.isActive&&(e.tribute.spaceSelectsMatch?e.callbacks().enter(t,n):e.tribute.allowSpaces||(t.stopPropagation(),setTimeout((function(){e.tribute.hideMenu(),e.tribute.isActive=!1}),0)))},up:function(t,n){if(e.tribute.isActive&&e.tribute.current.filteredItems){t.preventDefault(),t.stopPropagation();var i=e.tribute.current.filteredItems.length,r=e.tribute.menuSelected;i>r&&r>0?(e.tribute.menuSelected--,e.setActiveLi()):0===r&&(e.tribute.menuSelected=i-1,e.setActiveLi(),e.tribute.menu.scrollTop=e.tribute.menu.scrollHeight)}},down:function(t,n){if(e.tribute.isActive&&e.tribute.current.filteredItems){t.preventDefault(),t.stopPropagation();var i=e.tribute.current.filteredItems.length-1,r=e.tribute.menuSelected;i>r?(e.tribute.menuSelected++,e.setActiveLi()):i===r&&(e.tribute.menuSelected=0,e.setActiveLi(),e.tribute.menu.scrollTop=0)}},delete:function(t,n){e.tribute.isActive&&e.tribute.current.mentionText.length<1?e.tribute.hideMenu():e.tribute.isActive&&e.tribute.showMenuFor(n)}}}},{key:"setActiveLi",value:function(e){var t=this.tribute.menu.querySelectorAll("li"),n=t.length>>>0;e&&(this.tribute.menuSelected=parseInt(e));for(var i=0;iu.bottom){var l=o.bottom-u.bottom;this.tribute.menu.scrollTop+=l}else if(o.topi.width&&(r.left||r.right),u=window.innerHeight>i.height&&(r.top||r.bottom);(o||u)&&(n.tribute.menu.style.cssText="display: none",n.positionMenuAtCaret(e))}),0)}else this.tribute.menu.style.cssText="display: none"}},{key:"selectElement",value:function(e,t,n){var i,r=e;if(t)for(var o=0;o=0&&(t=i.substring(0,r))}}else{var o=this.tribute.current.element;if(o){var u=o.selectionStart;o.value&&u>=0&&(t=o.value.substring(0,u))}}return t}},{key:"getLastWordInText",value:function(e){var t;return e=e.replace(/\u00A0/g," "),(t=this.tribute.autocompleteSeparator?e.split(this.tribute.autocompleteSeparator):e.split(/\s+/))[t.length-1].trim()}},{key:"getTriggerInfo",value:function(e,t,n,i,r){var o,u,l,a=this,s=this.tribute.current;if(this.isContentEditable(s.element)){var c=this.getContentEditableSelectedPath(s);c&&(o=c.selected,u=c.path,l=c.offset)}else o=this.tribute.current.element;var h=this.getTextPrecedingCurrentSelection(),d=this.getLastWordInText(h);if(r)return{mentionPosition:h.length-d.length,mentionText:d,mentionSelectedElement:o,mentionSelectedPath:u,mentionSelectedOffset:l};if(null!=h){var f,m=-1;if(this.tribute.collection.forEach((function(e){var t=e.trigger,i=e.requireLeadingSpace?a.lastIndexWithLeadingSpace(h,t):h.lastIndexOf(t);i>m&&(m=i,f=t,n=e.requireLeadingSpace)})),m>=0&&(0===m||!n||/[\xA0\s]/g.test(h.substring(m-1,m)))){var p=h.substring(m+f.length,h.length);f=h.substring(m,m+f.length);var v=p.substring(0,1),g=p.length>0&&(" "===v||" "===v);t&&(p=p.trim());var b=i?/[^\S ]/g:/[\xA0\s]/g;if(this.tribute.hasTrailingSpace=b.test(p),!g&&(e||!b.test(p)))return{mentionPosition:m,mentionText:p,mentionSelectedElement:o,mentionSelectedPath:u,mentionSelectedOffset:l,mentionTriggerChar:f}}}}},{key:"lastIndexWithLeadingSpace",value:function(e,t){for(var n=e.split("").reverse().join(""),i=-1,r=0,o=e.length;r=0;s--)if(t[s]!==n[r-s]){a=!1;break}if(a&&(u||l)){i=e.length-1-r;break}}return i}},{key:"isContentEditable",value:function(e){return"INPUT"!==e.nodeName&&"TEXTAREA"!==e.nodeName}},{key:"isMenuOffScreen",value:function(e,t){var n=window.innerWidth,i=window.innerHeight,r=document.documentElement,o=(window.pageXOffset||r.scrollLeft)-(r.clientLeft||0),u=(window.pageYOffset||r.scrollTop)-(r.clientTop||0),l="number"==typeof e.top?e.top:u+i-e.bottom-t.height,a="number"==typeof e.right?e.right:e.left+t.width,s="number"==typeof e.bottom?e.bottom:e.top+t.height,c="number"==typeof e.left?e.left:o+n-e.right-t.width;return{top:lMath.ceil(o+n),bottom:s>Math.ceil(u+i),left:cparseInt(u.height)&&(o.overflowY="scroll")):o.overflow="hidden",r.textContent=e.value.substring(0,t),"INPUT"===e.nodeName&&(r.textContent=r.textContent.replace(/\s/g," "));var l=this.getDocument().createElement("span");l.textContent=e.value.substring(t)||".",r.appendChild(l);var a=e.getBoundingClientRect(),s=document.documentElement,c=(window.pageXOffset||s.scrollLeft)-(s.clientLeft||0),h=(window.pageYOffset||s.scrollTop)-(s.clientTop||0),d=0,f=0;this.menuContainerIsBody&&(d=a.top,f=a.left);var m={top:d+h+l.offsetTop+parseInt(u.borderTopWidth)+parseInt(u.fontSize)-e.scrollTop,left:f+c+l.offsetLeft+parseInt(u.borderLeftWidth)},p=window.innerWidth,v=window.innerHeight,g=this.getMenuDimensions(),b=this.isMenuOffScreen(m,g);b.right&&(m.right=p-m.left,m.left="auto");var y=this.tribute.menuContainer?this.tribute.menuContainer.offsetHeight:this.getDocument().body.offsetHeight;if(b.bottom){var w=y-(v-(this.tribute.menuContainer?this.tribute.menuContainer.getBoundingClientRect():this.getDocument().body.getBoundingClientRect()).top);m.bottom=w+(v-a.top-l.offsetTop),m.top="auto"}return(b=this.isMenuOffScreen(m,g)).left&&(m.left=p>g.width?c+p-g.width:c,delete m.right),b.top&&(m.top=v>g.height?h+v-g.height:h,delete m.bottom),this.getDocument().body.removeChild(r),m}},{key:"getContentEditableCaretPosition",value:function(e){var t,n=this.getWindowSelection();(t=this.getDocument().createRange()).setStart(n.anchorNode,e),t.setEnd(n.anchorNode,e),t.collapse(!1);var i=t.getBoundingClientRect(),r=document.documentElement,o=(window.pageXOffset||r.scrollLeft)-(r.clientLeft||0),u=(window.pageYOffset||r.scrollTop)-(r.clientTop||0),l={left:i.left+o,top:i.top+i.height+u},a=window.innerWidth,s=window.innerHeight,c=this.getMenuDimensions(),h=this.isMenuOffScreen(l,c);h.right&&(l.left="auto",l.right=a-i.left-o);var d=this.tribute.menuContainer?this.tribute.menuContainer.offsetHeight:this.getDocument().body.offsetHeight;if(h.bottom){var f=d-(s-(this.tribute.menuContainer?this.tribute.menuContainer.getBoundingClientRect():this.getDocument().body.getBoundingClientRect()).top);l.top="auto",l.bottom=f+(s-i.top)}return(h=this.isMenuOffScreen(l,c)).left&&(l.left=a>c.width?o+a-c.width:o,delete l.right),h.top&&(l.top=s>c.height?u+s-c.height:u,delete l.bottom),this.menuContainerIsBody||(l.left=l.left?l.left-this.tribute.menuContainer.offsetLeft:l.left,l.top=l.top?l.top-this.tribute.menuContainer.offsetTop:l.top),l}},{key:"scrollIntoView",value:function(e){var t,n=this.menu;if(void 0!==n){for(;void 0===t||0===t.height;)if(0===(t=n.getBoundingClientRect()).height&&(void 0===(n=n.childNodes[0])||!n.getBoundingClientRect))return;var i=t.top,r=i+t.height;if(i<0)window.scrollTo(0,window.pageYOffset+t.top-20);else if(r>window.innerHeight){var o=window.pageYOffset+t.top-20;o-window.pageYOffset>100&&(o=window.pageYOffset+100);var u=window.pageYOffset-(window.innerHeight-r);u>o&&(u=o),window.scrollTo(0,u)}}}},{key:"menuContainerIsBody",get:function(){return this.tribute.menuContainer===document.body||!this.tribute.menuContainer}}]),t}(),s=function(){function t(n){e(this,t),this.tribute=n,this.tribute.search=this}return n(t,[{key:"simpleFilter",value:function(e,t){var n=this;return t.filter((function(t){return n.test(e,t)}))}},{key:"test",value:function(e,t){return null!==this.match(e,t)}},{key:"match",value:function(e,t,n){n=n||{};t.length;var i=n.pre||"",r=n.post||"",o=n.caseSensitive&&t||t.toLowerCase();if(n.skip)return{rendered:t,score:0};e=n.caseSensitive&&e||e.toLowerCase();var u=this.traverse(o,e,0,0,[]);return u?{rendered:this.render(t,u.cache,i,r),score:u.score}:null}},{key:"traverse",value:function(e,t,n,i,r){if(this.tribute.autocompleteSeparator&&(t=t.split(this.tribute.autocompleteSeparator).splice(-1)[0]),t.length===i)return{score:this.calculateScore(r),cache:r.slice()};if(!(e.length===n||t.length-i>e.length-n)){for(var o,u,l=t[i],a=e.indexOf(l,n);a>-1;){if(r.push(a),u=this.traverse(e,t,a+1,i+1,r),r.pop(),!u)return o;(!o||o.score0&&(e[r-1]+1===i?n+=n+1:n=1),t+=n})),t}},{key:"render",value:function(e,t,n,i){var r=e.substring(0,t[0]);return t.forEach((function(o,u){r+=n+e[o]+i+e.substring(o+1,t[u+1]?t[u+1]:e.length)})),r}},{key:"filter",value:function(e,t,n){var i=this;return n=n||{},t.reduce((function(t,r,o,u){var l=r;n.extract&&((l=n.extract(r))||(l=""));var a=i.match(e,l,n);return null!=a&&(t[t.length]={string:a.rendered,score:a.score,index:o,original:r}),t}),[]).sort((function(e,t){var n=t.score-e.score;return n||e.index-t.index}))}}]),t}();return function(){function t(n){var i,r=this,o=n.values,c=void 0===o?null:o,h=n.loadingItemTemplate,d=void 0===h?null:h,f=n.iframe,m=void 0===f?null:f,p=n.selectClass,v=void 0===p?"highlight":p,g=n.containerClass,b=void 0===g?"tribute-container":g,y=n.itemClass,w=void 0===y?"":y,T=n.trigger,C=void 0===T?"@":T,S=n.autocompleteMode,E=void 0!==S&&S,k=n.autocompleteSeparator,x=void 0===k?null:k,M=n.selectTemplate,A=void 0===M?null:M,L=n.menuItemTemplate,I=void 0===L?null:L,N=n.lookup,O=void 0===N?"key":N,D=n.fillAttr,P=void 0===D?"value":D,R=n.collection,W=void 0===R?null:R,H=n.menuContainer,B=void 0===H?null:H,F=n.noMatchTemplate,_=void 0===F?null:F,j=n.requireLeadingSpace,Y=void 0===j||j,z=n.allowSpaces,K=void 0!==z&&z,q=n.replaceTextSuffix,U=void 0===q?null:q,X=n.positionMenu,Q=void 0===X||X,V=n.spaceSelectsMatch,$=void 0!==V&&V,G=n.searchOpts,J=void 0===G?{}:G,Z=n.menuItemLimit,ee=void 0===Z?null:Z,te=n.menuShowMinLength,ne=void 0===te?0:te;if(e(this,t),this.autocompleteMode=E,this.autocompleteSeparator=x,this.menuSelected=0,this.current={},this.inputEvent=!1,this.isActive=!1,this.menuContainer=B,this.allowSpaces=K,this.replaceTextSuffix=U,this.positionMenu=Q,this.hasTrailingSpace=!1,this.spaceSelectsMatch=$,this.autocompleteMode&&(C="",K=!1),c)this.collection=[{trigger:C,iframe:m,selectClass:v,containerClass:b,itemClass:w,selectTemplate:(A||t.defaultSelectTemplate).bind(this),menuItemTemplate:(I||t.defaultMenuItemTemplate).bind(this),noMatchTemplate:(i=_,"string"==typeof i?""===i.trim()?null:i:"function"==typeof i?i.bind(r):_||function(){return"
    • No Match Found!
    • "}.bind(r)),lookup:O,fillAttr:P,values:c,loadingItemTemplate:d,requireLeadingSpace:Y,searchOpts:J,menuItemLimit:ee,menuShowMinLength:ne}];else{if(!W)throw new Error("[Tribute] No collection specified.");this.autocompleteMode&&console.warn("Tribute in autocomplete mode does not work for collections"),this.collection=W.map((function(e){return{trigger:e.trigger||C,iframe:e.iframe||m,selectClass:e.selectClass||v,containerClass:e.containerClass||b,itemClass:e.itemClass||w,selectTemplate:(e.selectTemplate||t.defaultSelectTemplate).bind(r),menuItemTemplate:(e.menuItemTemplate||t.defaultMenuItemTemplate).bind(r),noMatchTemplate:function(e){return"string"==typeof e?""===e.trim()?null:e:"function"==typeof e?e.bind(r):_||function(){return"
    • No Match Found!
    • "}.bind(r)}(_),lookup:e.lookup||O,fillAttr:e.fillAttr||P,values:e.values,loadingItemTemplate:e.loadingItemTemplate,requireLeadingSpace:e.requireLeadingSpace,searchOpts:e.searchOpts||J,menuItemLimit:e.menuItemLimit||ee,menuShowMinLength:e.menuShowMinLength||ne}}))}new a(this),new u(this),new l(this),new s(this)}return n(t,[{key:"triggers",value:function(){return this.collection.map((function(e){return e.trigger}))}},{key:"attach",value:function(e){if(!e)throw new Error("[Tribute] Must pass in a DOM node or NodeList.");if("undefined"!=typeof jQuery&&e instanceof jQuery&&(e=e.get()),e.constructor===NodeList||e.constructor===HTMLCollection||e.constructor===Array)for(var t=e.length,n=0;n",post:n.current.collection.searchOpts.post||"
      ",skip:n.current.collection.searchOpts.skip,extract:function(e){if("string"==typeof n.current.collection.lookup)return e[n.current.collection.lookup];if("function"==typeof n.current.collection.lookup)return n.current.collection.lookup(e,n.current.mentionText);throw new Error("Invalid lookup attribute, lookup must be string or function.")}});n.current.collection.menuItemLimit&&(r=r.slice(0,n.current.collection.menuItemLimit)),n.current.filteredItems=r;var o=n.menu.querySelector("ul");if(n.range.positionMenuAtCaret(t),!r.length){var u=new CustomEvent("tribute-no-match",{detail:n.menu});return n.current.element.dispatchEvent(u),void("function"==typeof n.current.collection.noMatchTemplate&&!n.current.collection.noMatchTemplate()||!n.current.collection.noMatchTemplate?n.hideMenu():"function"==typeof n.current.collection.noMatchTemplate?o.innerHTML=n.current.collection.noMatchTemplate():o.innerHTML=n.current.collection.noMatchTemplate)}o.innerHTML="";var l=n.range.getDocument().createDocumentFragment();r.forEach((function(e,t){var r=n.range.getDocument().createElement("li");r.setAttribute("data-index",t),r.className=n.current.collection.itemClass,r.addEventListener("mousemove",(function(e){var t=i(n._findLiTarget(e.target),2),r=(t[0],t[1]);0!==e.movementY&&n.events.setActiveLi(r)})),n.menuSelected===t&&r.classList.add(n.current.collection.selectClass),r.innerHTML=n.current.collection.menuItemTemplate(e),l.appendChild(r)})),o.appendChild(l)}};"function"==typeof this.current.collection.values?(this.current.collection.loadingItemTemplate&&(this.menu.querySelector("ul").innerHTML=this.current.collection.loadingItemTemplate,this.range.positionMenuAtCaret(t)),this.current.collection.values(this.current.mentionText,r)):r(this.current.collection.values)}}},{key:"_findLiTarget",value:function(e){if(!e)return[];var t=e.getAttribute("data-index");return t?[e,t]:this._findLiTarget(e.parentNode)}},{key:"showMenuForCollection",value:function(e,t){e!==document.activeElement&&this.placeCaretAtEnd(e),this.current.collection=this.collection[t||0],this.current.externalTrigger=!0,this.current.element=e,e.isContentEditable?this.insertTextAtCursor(this.current.collection.trigger):this.insertAtCaret(e,this.current.collection.trigger),this.showMenuFor(e)}},{key:"placeCaretAtEnd",value:function(e){if(e.focus(),void 0!==window.getSelection&&void 0!==document.createRange){var t=document.createRange();t.selectNodeContents(e),t.collapse(!1);var n=window.getSelection();n.removeAllRanges(),n.addRange(t)}else if(void 0!==document.body.createTextRange){var i=document.body.createTextRange();i.moveToElementText(e),i.collapse(!1),i.select()}}},{key:"insertTextAtCursor",value:function(e){var t,n;(n=(t=window.getSelection()).getRangeAt(0)).deleteContents();var i=document.createTextNode(e);n.insertNode(i),n.selectNodeContents(i),n.collapse(!1),t.removeAllRanges(),t.addRange(n)}},{key:"insertAtCaret",value:function(e,t){var n=e.scrollTop,i=e.selectionStart,r=e.value.substring(0,i),o=e.value.substring(e.selectionEnd,e.value.length);e.value=r+t+o,i+=t.length,e.selectionStart=i,e.selectionEnd=i,e.focus(),e.scrollTop=n}},{key:"hideMenu",value:function(){this.menu&&(this.menu.style.cssText="display: none;",this.isActive=!1,this.menuSelected=0,this.current={})}},{key:"selectItemAtIndex",value:function(e,t){if("number"==typeof(e=parseInt(e))&&!isNaN(e)){var n=this.current.filteredItems[e],i=this.current.collection.selectTemplate(n);null!==i&&this.replaceText(i,t,n)}}},{key:"replaceText",value:function(e,t,n){this.range.replaceTriggerText(e,!0,!0,t,n)}},{key:"_append",value:function(e,t,n){if("function"==typeof e.values)throw new Error("Unable to append to values, as it is a function.");e.values=n?t:e.values.concat(t)}},{key:"append",value:function(e,t,n){var i=parseInt(e);if("number"!=typeof i)throw new Error("please provide an index for the collection to update.");var r=this.collection[i];this._append(r,t,n)}},{key:"appendCurrent",value:function(e,t){if(!this.isActive)throw new Error("No active state. Please use append instead and pass an index.");this._append(this.current.collection,e,t)}},{key:"detach",value:function(e){if(!e)throw new Error("[Tribute] Must pass in a DOM node or NodeList.");if("undefined"!=typeof jQuery&&e instanceof jQuery&&(e=e.get()),e.constructor===NodeList||e.constructor===HTMLCollection||e.constructor===Array)for(var t=e.length,n=0;n'+(this.current.collection.trigger+e.original[this.current.collection.fillAttr])+"":this.current.collection.trigger+e.original[this.current.collection.fillAttr]}},{key:"defaultMenuItemTemplate",value:function(e){return e.string}},{key:"inputTypes",value:function(){return["TEXTAREA","INPUT"]}}]),t}()})); +//# sourceMappingURL=tribute.min.js.map diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index a2e9920b9e..2a76bd272e 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -26,7 +26,7 @@ } - + diff --git a/phpBB/styles/prosilver/theme/bidi.css b/phpBB/styles/prosilver/theme/bidi.css index 7f9642b7cc..29fc0aac7c 100644 --- a/phpBB/styles/prosilver/theme/bidi.css +++ b/phpBB/styles/prosilver/theme/bidi.css @@ -374,7 +374,7 @@ /* Mention dropdown ---------------------------------------- */ -.rtl .atwho-view { /* mention-container */ +.rtl .mention-container { /* mention-container */ text-align: right; } From f27be9a4f1a8d7b24a8d406ecc9d440fabf41a65 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Mon, 3 May 2021 22:16:15 +0200 Subject: [PATCH 096/113] [ticket/13713] Clean up declarations and adjust tests for latest master PHPBB3-13713 --- phpBB/phpbb/notification/type/mention.php | 58 ++++++---------- tests/mention/controller_test.php | 84 ++++++++++++----------- 2 files changed, 66 insertions(+), 76 deletions(-) diff --git a/phpBB/phpbb/notification/type/mention.php b/phpBB/phpbb/notification/type/mention.php index 2dda929d5b..fad31b9912 100644 --- a/phpBB/phpbb/notification/type/mention.php +++ b/phpBB/phpbb/notification/type/mention.php @@ -13,22 +13,22 @@ namespace phpbb\notification\type; +use phpbb\textformatter\s9e\mention_helper; + /** * Post mentioning notifications class * This class handles notifying users when they have been mentioned in a post */ -class mention extends \phpbb\notification\type\post +class mention extends post { /** - * @var \phpbb\textformatter\s9e\mention_helper + * @var mention_helper */ protected $helper; /** - * Get notification type name - * - * @return string + * {@inheritDoc} */ public function get_type() { @@ -36,39 +36,29 @@ class mention extends \phpbb\notification\type\post } /** - * Language key used to output the text - * - * @var string - */ + * {@inheritDoc} + */ protected $language_key = 'NOTIFICATION_MENTION'; /** - * Notification option data (for outputting to the user) - * - * @var bool|array False if the service should use it's default data - * Array of data (including keys 'id', 'lang', and 'group') - */ - static public $notification_option = array( + * {@inheritDoc} + */ + public static $notification_option = [ 'lang' => 'NOTIFICATION_TYPE_MENTION', 'group' => 'NOTIFICATION_GROUP_POSTING', - ); + ]; /** - * Is available - */ + * {@inheritDoc} + */ public function is_available() { return $this->config['allow_mentions'] && $this->auth->acl_get('u_mention'); } /** - * Find the users who want to receive notifications - * - * @param array $post Data from submit_post - * @param array $options Options for finding users for notification - * - * @return array - */ + * {@inheritDoc} + */ public function find_users_for_notification($post, $options = array()) { $options = array_merge(array( @@ -128,7 +118,7 @@ class mention extends \phpbb\notification\type\post } /** - * {inheritDoc} + * {@inheritDoc} */ public function get_redirect_url() { @@ -136,20 +126,16 @@ class mention extends \phpbb\notification\type\post } /** - * Get email template - * - * @return string|bool - */ + * {@inheritDoc} + */ public function get_email_template() { return 'mention'; } /** - * Get email template variables - * - * @return array - */ + * {@inheritDoc} + */ public function get_email_template_variables() { $user_data = $this->user_loader->get_user($this->get_data('poster_id')); @@ -162,9 +148,9 @@ class mention extends \phpbb\notification\type\post /** * Set the helper service used to retrieve mentioned used * - * @param \phpbb\textformatter\s9e\mention_helper $helper + * @param mention_helper $helper */ - public function set_helper(\phpbb\textformatter\s9e\mention_helper $helper) + public function set_helper(mention_helper $helper): void { $this->helper = $helper; } diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php index 3b50731791..2e52c0cea7 100644 --- a/tests/mention/controller_test.php +++ b/tests/mention/controller_test.php @@ -92,10 +92,12 @@ class phpbb_mention_controller_test extends phpbb_database_test_case $request->expects($this->any()) ->method('is_ajax') - ->willReturn(true) - ; + ->willReturn(true); + $avatar_helper = $this->getMockBuilder('\phpbb\avatar\helper') + ->disableOriginalConstructor() + ->getMock(); - $user_loader = new \phpbb\user_loader($db, $phpbb_root_path, $phpEx, USERS_TABLE); + $user_loader = new \phpbb\user_loader($avatar_helper, $db, $phpbb_root_path, $phpEx, USERS_TABLE); // Container $phpbb_container = new ContainerBuilder(); @@ -113,6 +115,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case $phpbb_container->set('request', $request); $phpbb_container->set('group_helper', new \phpbb\group\helper( $this->getMockBuilder('\phpbb\auth\auth')->disableOriginalConstructor()->getMock(), + $avatar_helper, $cache, $config, new \phpbb\language\language( @@ -142,6 +145,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case ); $phpbb_container->setParameter('core.root_path', $phpbb_root_path); $phpbb_container->setParameter('core.php_ext', $phpEx); + $phpbb_container->addCompilerPass(new phpbb\di\pass\markpublic_pass()); $phpbb_container->compile(); // Mention Sources @@ -172,7 +176,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 7, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -205,7 +209,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 6, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -216,7 +220,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 5, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -227,7 +231,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 2, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -238,7 +242,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 3, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -249,7 +253,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 4, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -260,7 +264,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 5, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -271,7 +275,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 6, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -282,7 +286,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 7, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -293,7 +297,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 8, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -304,7 +308,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 9, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -331,7 +335,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 7, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -364,7 +368,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 6, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -375,7 +379,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 5, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -386,7 +390,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 4, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -397,7 +401,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 3, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 5, @@ -408,7 +412,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 2, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -419,7 +423,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 3, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -430,7 +434,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 4, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -441,7 +445,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 5, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -452,7 +456,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 6, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -463,7 +467,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 7, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -474,7 +478,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 8, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -485,7 +489,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 9, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -512,7 +516,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 6, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -523,7 +527,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 5, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 1, @@ -534,7 +538,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 5, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -545,7 +549,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 6, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -556,7 +560,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 8, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -567,7 +571,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 9, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -578,7 +582,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 10, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -589,7 +593,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 11, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -605,7 +609,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 8, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -616,7 +620,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 9, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -627,7 +631,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 10, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -638,7 +642,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 11, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, @@ -653,7 +657,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'id' => 9, 'avatar' => [ 'type' => 'user', - 'img' => '', + 'img' => [], ], 'rank' => '', 'priority' => 0, From 17ce85d0121d4ee4065ce8e30ec07511958e1376 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Tue, 4 May 2021 19:54:51 +0200 Subject: [PATCH 097/113] [ticket/13713] Fix stylelint infractions in mentions stylesheet PHPBB3-13713 --- phpBB/adm/style/admin.css | 4 ++-- phpBB/styles/prosilver/theme/mentions.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index d0d97dba72..a645ea7947 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1677,8 +1677,6 @@ fieldset.submit-buttons legend { } .mention-container { /* mention-container */ - overflow: auto; /* placed here for list to scroll with arrow key press */ - max-height: 200px; text-align: left; background-color: #ffffff; border-radius: 2px; @@ -1688,6 +1686,8 @@ fieldset.submit-buttons legend { 0 1px 5px 0 rgba(0, 0, 0, 0.12); position: absolute; z-index: 999; + overflow: auto; /* placed here for list to scroll with arrow key press */ + max-height: 200px; transition: all 0.2s ease; } diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 5163530b96..a4207a9e49 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -14,12 +14,12 @@ /* Mention dropdown ---------------------------------------- */ .mention-container { /* mention-container */ - overflow: auto; /* placed here for list to scroll with arrow key press */ - max-height: 200px; text-align: left; border-radius: 2px; position: absolute; z-index: 999; + overflow: auto; /* placed here for list to scroll with arrow key press */ + max-height: 200px; transition: all 0.2s ease; } From 169015eab42fc0fc4391688ea11195b86d3ed9a9 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Tue, 4 May 2021 20:31:40 +0200 Subject: [PATCH 098/113] [ticket/13713] Use is-active class instead of cur for current items PHPBB3-13713 --- phpBB/adm/style/admin.css | 4 ++-- phpBB/assets/javascript/editor.js | 2 +- phpBB/styles/prosilver/theme/colours.css | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index a645ea7947..a6d29868ab 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1747,14 +1747,14 @@ svg { /* TODO: remove it after general normalization */ } .mention-item:hover, -.mention-item.cur { +.mention-item.is-active { text-decoration: none; background-color: #eeeeee; color: #2d80d2; } .mention-item:hover .mention-media-avatar, -.mention-item.cur .mention-media-avatar { +.mention-item.is-active .mention-media-avatar { color: #2d80d2; } diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 2e726868bf..1e652cadea 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -616,7 +616,7 @@ function getCaretPosition(txtarea) { trigger: '@', allowSpaces: true, containerClass: 'mention-container', - selectClass: 'cur', + selectClass: 'is-active', itemClass: 'mention-item', menuItemTemplate: function (data) { const itemData = data; diff --git a/phpBB/styles/prosilver/theme/colours.css b/phpBB/styles/prosilver/theme/colours.css index 1c299b73ee..2499908b44 100644 --- a/phpBB/styles/prosilver/theme/colours.css +++ b/phpBB/styles/prosilver/theme/colours.css @@ -390,13 +390,13 @@ p.post-notice { } .mention-item:hover, -.mention-item.cur { +.mention-item.is-active { background-color: #eeeeee; color: #2d80d2; } .mention-item:hover .mention-media-avatar, -.mention-item.cur .mention-media-avatar { +.mention-item.is-active .mention-media-avatar { color: #2d80d2; } From 9ae015569c41201a82f3a7c268d947cf79b56df8 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Tue, 4 May 2021 21:06:41 +0200 Subject: [PATCH 099/113] [ticket/13713] Fix avatar display with new helper methods PHPBB3-13713 --- phpBB/assets/javascript/editor.js | 27 +++++++++++++++++++---- phpBB/phpbb/mention/source/base_group.php | 5 +---- phpBB/phpbb/mention/source/base_user.php | 5 +---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 1e652cadea..7945924b84 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -416,17 +416,36 @@ function getCaretPosition(txtarea) { /** * Get default avatar - * @param {string} type Type of avatar; either group or user on any other value + * @param {string} type Type of avatar; either 'g' for group or user on any other value * @returns {string} Default avatar svg code */ function defaultAvatar(type) { - if (type === 'group') { + if (type === 'g') { return ''; } else { return ''; } } + /** + * Get avatar HTML for data and type of avatar + * + * @param {object} data + * @param {string} type + * @return {string} Avatar HTML + */ + function getAvatar(data, type) { + const avatarToHtml = (avatarData) => { + if (avatarData.html !== '') { + return avatarData.html; + } else { + return '' + avatarData.title + ''; + } + } + + return data.html === '' && data.src === '' ? defaultAvatar(type) : "" + avatarToHtml(data)+ ""; + } + /** * Get cached keyword for query string * @param {string} query Query string @@ -620,8 +639,8 @@ function getCaretPosition(txtarea) { itemClass: 'mention-item', menuItemTemplate: function (data) { const itemData = data; - let avatar = (itemData.avatar.img) ? "" + itemData.avatar.img + "" : defaultAvatar(itemData.avatar.type), - rank = (itemData.rank) ? "" + itemData.rank + "" : ''; + let avatar = getAvatar(itemData.avatar, itemData.type); + let rank = (itemData.rank) ? "" + itemData.rank + "" : ''; return "" + avatar + "" + itemData.name + rank + ""; }, selectTemplate: function (item) { diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index 1f3f063e33..126c01fa58 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -159,10 +159,7 @@ abstract class base_group implements source_interface 'name' => $groups[$group_id]['group_name'], 'type' => 'g', 'id' => $group_id, - 'avatar' => [ - 'type' => 'group', - 'img' => phpbb_get_group_avatar($groups[$group_id]), - ], + 'avatar' => $this->helper->get_avatar($groups[$group_id]), 'rank' => (isset($group_rank['title'])) ? $group_rank['title'] : '', 'priority' => $this->get_priority($groups[$group_id]), ]); diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index 8b6c7a8540..f0d01fa8e1 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -152,10 +152,7 @@ abstract class base_user implements source_interface 'name' => $this->user_loader->get_username($user['user_id'], 'username'), 'type' => 'u', 'id' => $user['user_id'], - 'avatar' => [ - 'type' => 'user', - 'img' => $this->user_loader->get_avatar($user['user_id']), - ], + 'avatar' => $this->user_loader->get_avatar($user['user_id']), 'rank' => (isset($user_rank['rank_title'])) ? $user_rank['rank_title'] : '', 'priority' => $this->get_priority($user), ]); From 794b77971cc83ecfa47daef0f20f5c9459475c21 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Tue, 4 May 2021 21:10:32 +0200 Subject: [PATCH 100/113] [ticket/13713] Move profile_url to a tag in textformatter factory PHPBB3-13713 --- phpBB/phpbb/textformatter/s9e/factory.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php index d872a7094c..6ccd15ab96 100644 --- a/phpBB/phpbb/textformatter/s9e/factory.php +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -135,8 +135,7 @@ class factory implements \phpbb\textformatter\cache_interface 'mention' => '@ - - + From ed291843f2b00fca43180313c3b2d10bbb66f7cb Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Tue, 4 May 2021 21:23:21 +0200 Subject: [PATCH 101/113] [ticket/13713] Add type hints and clean up code PHPBB3-13713 --- phpBB/phpbb/mention/controller/mention.php | 17 +++++++-- phpBB/phpbb/mention/source/base_group.php | 37 +++++++++++++------ phpBB/phpbb/mention/source/base_user.php | 26 +++++++++---- phpBB/phpbb/mention/source/friend.php | 19 +++++----- phpBB/phpbb/mention/source/group.php | 11 +++--- .../phpbb/mention/source/source_interface.php | 4 +- phpBB/phpbb/mention/source/team.php | 5 +-- phpBB/phpbb/mention/source/topic.php | 32 +++++++--------- phpBB/phpbb/mention/source/user.php | 15 ++++---- phpBB/phpbb/mention/source/usergroup.php | 15 ++++---- 10 files changed, 102 insertions(+), 79 deletions(-) diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index eec3fbc5d1..10e21f5cfc 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -13,15 +13,17 @@ namespace phpbb\mention\controller; +use phpbb\di\service_collection; +use phpbb\request\request_interface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; class mention { - /** @var \phpbb\di\service_collection */ + /** @var service_collection */ protected $mention_sources; - /** @var \phpbb\request\request_interface */ + /** @var request_interface */ protected $request; /** @var string */ @@ -33,8 +35,12 @@ class mention /** * Constructor * + * @param array $mention_sources + * @param request_interface $request + * @param string $phpbb_root_path + * @param string $phpEx */ - public function __construct($mention_sources, \phpbb\request\request_interface $request, $phpbb_root_path, $phpEx) + public function __construct(array $mention_sources, request_interface $request, string $phpbb_root_path, string $phpEx) { $this->mention_sources = $mention_sources; $this->request = $request; @@ -42,6 +48,11 @@ class mention $this->php_ext = $phpEx; } + /** + * Handle requests to mention controller + * + * @return JsonResponse|RedirectResponse + */ public function handle() { if (!$this->request->is_ajax()) diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php index 126c01fa58..58fca4d054 100644 --- a/phpBB/phpbb/mention/source/base_group.php +++ b/phpBB/phpbb/mention/source/base_group.php @@ -13,21 +13,26 @@ namespace phpbb\mention\source; +use phpbb\auth\auth; +use phpbb\config\config; +use phpbb\db\driver\driver_interface; +use phpbb\group\helper; + abstract class base_group implements source_interface { - /** @var \phpbb\db\driver\driver_interface */ + /** @var driver_interface */ protected $db; - /** @var \phpbb\config\config */ + /** @var config */ protected $config; - /** @var \phpbb\group\helper */ + /** @var helper */ protected $helper; /** @var \phpbb\user */ protected $user; - /** @var \phpbb\auth\auth */ + /** @var auth */ protected $auth; /** @var string */ @@ -43,9 +48,17 @@ abstract class base_group implements source_interface protected $groups = null; /** - * Constructor + * base_group constructor. + * + * @param driver_interface $db + * @param config $config + * @param helper $helper + * @param \phpbb\user $user + * @param auth $auth + * @param string $phpbb_root_path + * @param string $phpEx */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\group\helper $helper, \phpbb\user $user, \phpbb\auth\auth $auth, $phpbb_root_path, $phpEx) + public function __construct(driver_interface $db, config $config, helper $helper, \phpbb\user $user, auth $auth, string $phpbb_root_path, string $phpEx) { $this->db = $db; $this->config = $config; @@ -66,13 +79,13 @@ abstract class base_group implements source_interface * * @return array Array of groups' data */ - protected function get_groups() + protected function get_groups(): array { if (is_null($this->groups)) { $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'g.*, ug.user_id as ug_user_id', - 'FROM' => [ + 'SELECT' => 'g.*, ug.user_id as ug_user_id', + 'FROM' => [ GROUPS_TABLE => 'g', ], 'LEFT_JOIN' => [ @@ -112,12 +125,12 @@ abstract class base_group implements source_interface * @param int $topic_id Current topic ID * @return string Query ready for execution */ - abstract protected function query($keyword, $topic_id); + abstract protected function query(string $keyword, int $topic_id): string; /** * {@inheritdoc} */ - public function get_priority($row) + public function get_priority(array $row): int { // By default every result from the source increases the priority by a fixed value return 1; @@ -126,7 +139,7 @@ abstract class base_group implements source_interface /** * {@inheritdoc} */ - public function get(array &$names, $keyword, $topic_id) + public function get(array &$names, string $keyword, int $topic_id): bool { // Grab all group IDs and cache them if needed $result = $this->db->sql_query($this->query($keyword, $topic_id), $this->cache_ttl); diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php index f0d01fa8e1..7e9b41d67d 100644 --- a/phpBB/phpbb/mention/source/base_user.php +++ b/phpBB/phpbb/mention/source/base_user.php @@ -13,15 +13,19 @@ namespace phpbb\mention\source; +use phpbb\config\config; +use phpbb\db\driver\driver_interface; +use phpbb\user_loader; + abstract class base_user implements source_interface { - /** @var \phpbb\db\driver\driver_interface */ + /** @var driver_interface */ protected $db; - /** @var \phpbb\config\config */ + /** @var config */ protected $config; - /** @var \phpbb\user_loader */ + /** @var user_loader */ protected $user_loader; /** @var string */ @@ -34,9 +38,15 @@ abstract class base_user implements source_interface protected $cache_ttl = false; /** - * Constructor + * base_user constructor. + * + * @param driver_interface $db + * @param config $config + * @param user_loader $user_loader + * @param string $phpbb_root_path + * @param string $phpEx */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\user_loader $user_loader, $phpbb_root_path, $phpEx) + public function __construct(driver_interface $db, config $config, user_loader $user_loader, string $phpbb_root_path, string $phpEx) { $this->db = $db; $this->config = $config; @@ -57,12 +67,12 @@ abstract class base_user implements source_interface * @param int $topic_id Current topic ID * @return string Query ready for execution */ - abstract protected function query($keyword, $topic_id); + abstract protected function query(string $keyword, int $topic_id): string; /** * {@inheritdoc} */ - public function get_priority($row) + public function get_priority(array $row): int { // By default every result from the source increases the priority by a fixed value return 1; @@ -71,7 +81,7 @@ abstract class base_user implements source_interface /** * {@inheritdoc} */ - public function get(array &$names, $keyword, $topic_id) + public function get(array &$names, string $keyword, int $topic_id): bool { $fetched_all = false; $keyword = utf8_clean_string($keyword); diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php index ca16a374b5..5c7a3f91ef 100644 --- a/phpBB/phpbb/mention/source/friend.php +++ b/phpBB/phpbb/mention/source/friend.php @@ -23,7 +23,7 @@ class friend extends base_user * * @param \phpbb\user $user */ - public function set_user(\phpbb\user $user) + public function set_user(\phpbb\user $user): void { $this->user = $user; } @@ -31,29 +31,28 @@ class friend extends base_user /** * {@inheritdoc} */ - protected function query($keyword, $topic_id) + protected function query(string $keyword, int $topic_id): string { /* * For optimization purposes all friends are returned regardless of the keyword * Names filtering is done on the frontend * Results will be cached on a per-user basis */ - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username_clean, u.user_id', - 'FROM' => [ + return $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.username_clean, u.user_id', + 'FROM' => [ USERS_TABLE => 'u', ], 'LEFT_JOIN' => [ [ - 'FROM' => [ZEBRA_TABLE => 'z'], - 'ON' => 'u.user_id = z.zebra_id' + 'FROM' => [ZEBRA_TABLE => 'z'], + 'ON' => 'u.user_id = z.zebra_id' ] ], - 'WHERE' => 'z.friend = 1 AND z.user_id = ' . (int) $this->user->data['user_id'] . ' + 'WHERE' => 'z.friend = 1 AND z.user_id = ' . (int) $this->user->data['user_id'] . ' AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), - 'ORDER_BY' => 'u.user_lastvisit DESC' + 'ORDER_BY' => 'u.user_lastvisit DESC' ]); - return $query; } } diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php index 2b13ca7be0..11a8e02e94 100644 --- a/phpBB/phpbb/mention/source/group.php +++ b/phpBB/phpbb/mention/source/group.php @@ -21,7 +21,7 @@ class group extends base_group /** * {@inheritdoc} */ - public function get_priority($row) + public function get_priority(array $row): int { /* * Presence in array with all names for this type should not increase the priority @@ -35,15 +35,14 @@ class group extends base_group /** * {@inheritdoc} */ - protected function query($keyword, $topic_id) + protected function query(string $keyword, int $topic_id): string { - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'g.group_id', - 'FROM' => [ + return $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'g.group_id', + 'FROM' => [ GROUPS_TABLE => 'g', ], 'ORDER_BY' => 'g.group_name', ]); - return $query; } } diff --git a/phpBB/phpbb/mention/source/source_interface.php b/phpBB/phpbb/mention/source/source_interface.php index b9e126324b..2fe45ef234 100644 --- a/phpBB/phpbb/mention/source/source_interface.php +++ b/phpBB/phpbb/mention/source/source_interface.php @@ -24,7 +24,7 @@ interface source_interface * @param int $topic_id Current topic ID * @return bool Whether there are no more satisfying names left */ - public function get(array &$names, $keyword, $topic_id); + public function get(array &$names, string $keyword, int $topic_id): bool; /** * Returns the priority of the currently selected name @@ -34,5 +34,5 @@ interface source_interface * @param array $row Array of fetched data for the name type (e.g. user row) * @return int Priority (defaults to 1) */ - public function get_priority($row); + public function get_priority(array $row): int; } diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php index 14281a85bf..02fd8cefbb 100644 --- a/phpBB/phpbb/mention/source/team.php +++ b/phpBB/phpbb/mention/source/team.php @@ -21,7 +21,7 @@ class team extends base_user /** * {@inheritdoc} */ - protected function query($keyword, $topic_id) + protected function query(string $keyword, int $topic_id): string { /* * Select unique names of team members: each name should be selected only once @@ -31,7 +31,7 @@ class team extends base_user * Names filtering is done on the frontend * Results will be cached in a single file */ - $query = $this->db->sql_build_query('SELECT_DISTINCT', [ + return $this->db->sql_build_query('SELECT_DISTINCT', [ 'SELECT' => 'u.username_clean, u.user_id', 'FROM' => [ USERS_TABLE => 'u', @@ -42,6 +42,5 @@ class team extends base_user AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]), 'ORDER_BY' => 'u.username_clean' ]); - return $query; } } diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php index 0c630aa22e..842d38c4ef 100644 --- a/phpBB/phpbb/mention/source/topic.php +++ b/phpBB/phpbb/mention/source/topic.php @@ -18,53 +18,47 @@ class topic extends base_user /** * {@inheritdoc} */ - public function get_priority($row) + public function get_priority(array $row): int { /* * Topic's open poster is probably the most mentionable user in the topic * so we give him a significant priority */ - if ($row['user_id'] === $row['topic_poster']) - { - return 5; - } - - return 1; + return $row['user_id'] === $row['topic_poster'] ? 5 : 1; } /** * {@inheritdoc} */ - protected function query($keyword, $topic_id) + protected function query(string $keyword, int $topic_id): string { /* * Select poster's username together with topic author's ID - * that will be later used for priotirisation + * that will be later used for prioritisation * * For optimization purposes all users are returned regardless of the keyword * Names filtering is done on the frontend * Results will be cached on a per-topic basis */ - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username_clean, u.user_id, t.topic_poster', - 'FROM' => [ + return $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.username_clean, u.user_id, t.topic_poster', + 'FROM' => [ USERS_TABLE => 'u', ], 'LEFT_JOIN' => [ [ - 'FROM' => [POSTS_TABLE => 'p'], - 'ON' => 'u.user_id = p.poster_id' + 'FROM' => [POSTS_TABLE => 'p'], + 'ON' => 'u.user_id = p.poster_id' ], [ - 'FROM' => [TOPICS_TABLE => 't'], - 'ON' => 't.topic_id = p.topic_id' + 'FROM' => [TOPICS_TABLE => 't'], + 'ON' => 't.topic_id = p.topic_id' ], ], - 'WHERE' => 'p.topic_id = ' . (int) $topic_id . ' + 'WHERE' => 'p.topic_id = ' . (int) $topic_id . ' AND ' . $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), - 'ORDER_BY' => 'p.post_time DESC' + 'ORDER_BY' => 'p.post_time DESC' ]); - return $query; } } diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php index a85d3d3be4..3189f32b83 100644 --- a/phpBB/phpbb/mention/source/user.php +++ b/phpBB/phpbb/mention/source/user.php @@ -18,7 +18,7 @@ class user extends base_user /** * {@inheritdoc} */ - public function get_priority($row) + public function get_priority(array $row): int { /* * Presence in array with all names for this type should not increase the priority @@ -32,17 +32,16 @@ class user extends base_user /** * {@inheritdoc} */ - protected function query($keyword, $topic_id) + protected function query(string $keyword, int $topic_id): string { - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'u.username_clean, u.user_id', - 'FROM' => [ + return $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'u.username_clean, u.user_id', + 'FROM' => [ USERS_TABLE => 'u', ], - 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' + 'WHERE' => $this->db->sql_in_set('u.user_type', [USER_NORMAL, USER_FOUNDER]) . ' AND u.username_clean ' . $this->db->sql_like_expression($keyword . $this->db->get_any_char()), - 'ORDER_BY' => 'u.user_lastvisit DESC' + 'ORDER_BY' => 'u.user_lastvisit DESC' ]); - return $query; } } diff --git a/phpBB/phpbb/mention/source/usergroup.php b/phpBB/phpbb/mention/source/usergroup.php index b3b3e71ded..de02cd76d6 100644 --- a/phpBB/phpbb/mention/source/usergroup.php +++ b/phpBB/phpbb/mention/source/usergroup.php @@ -18,22 +18,21 @@ class usergroup extends base_group /** * {@inheritdoc} */ - protected function query($keyword, $topic_id) + protected function query(string $keyword, int $topic_id): string { - $query = $this->db->sql_build_query('SELECT', [ - 'SELECT' => 'g.group_id', - 'FROM' => [ + return $this->db->sql_build_query('SELECT', [ + 'SELECT' => 'g.group_id', + 'FROM' => [ GROUPS_TABLE => 'g', ], 'LEFT_JOIN' => [ [ - 'FROM' => [USER_GROUP_TABLE => 'ug'], - 'ON' => 'g.group_id = ug.group_id' + 'FROM' => [USER_GROUP_TABLE => 'ug'], + 'ON' => 'g.group_id = ug.group_id' ] ], - 'WHERE' => 'ug.user_pending = 0 AND ug.user_id = ' . (int) $this->user->data['user_id'], + 'WHERE' => 'ug.user_pending = 0 AND ug.user_id = ' . (int) $this->user->data['user_id'], 'ORDER_BY' => 'g.group_name', ]); - return $query; } } From 8513b8d932eed5cd6591ed58e74dc937989c499a Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Tue, 4 May 2021 21:58:29 +0200 Subject: [PATCH 102/113] [ticket/13713] Update expected test results PHPBB3-13713 --- tests/mention/controller_test.php | 215 ++++++------------------------ 1 file changed, 43 insertions(+), 172 deletions(-) diff --git a/tests/mention/controller_test.php b/tests/mention/controller_test.php index 2e52c0cea7..c473da9db6 100644 --- a/tests/mention/controller_test.php +++ b/tests/mention/controller_test.php @@ -174,10 +174,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'friend', 'type' => 'u', 'id' => 7, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -185,10 +182,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'Group we are a member of', 'type' => 'g', 'id' => 3, - 'avatar' => [ - 'type' => 'group', - 'img' => '', - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -196,10 +190,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'Normal group', 'type' => 'g', 'id' => 1, - 'avatar' => [ - 'type' => 'group', - 'img' => '', - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -207,10 +198,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_hidden', 'type' => 'u', 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -218,10 +206,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_normal', 'type' => 'u', 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -229,10 +214,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'myself', 'type' => 'u', 'id' => 2, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -240,10 +222,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'poster', 'type' => 'u', 'id' => 3, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -251,10 +230,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'replier', 'type' => 'u', 'id' => 4, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -262,10 +238,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_normal', 'type' => 'u', 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -273,10 +246,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_hidden', 'type' => 'u', 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -284,10 +254,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'friend', 'type' => 'u', 'id' => 7, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -295,10 +262,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test', 'type' => 'u', 'id' => 8, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -306,10 +270,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test1', 'type' => 'u', 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -317,10 +278,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'Group we are a member of', 'type' => 'g', 'id' => 3, - 'avatar' => [ - 'type' => 'group', - 'img' => '', - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -333,10 +291,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'friend', 'type' => 'u', 'id' => 7, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -344,10 +299,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'Group we are a member of', 'type' => 'g', 'id' => 3, - 'avatar' => [ - 'type' => 'group', - 'img' => '', - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -355,10 +307,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'Normal group', 'type' => 'g', 'id' => 1, - 'avatar' => [ - 'type' => 'group', - 'img' => '', - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -366,10 +315,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_hidden', 'type' => 'u', 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -377,10 +323,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_normal', 'type' => 'u', 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -388,10 +331,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'replier', 'type' => 'u', 'id' => 4, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -399,10 +339,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'poster', 'type' => 'u', 'id' => 3, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 5, ], @@ -410,10 +347,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'myself', 'type' => 'u', 'id' => 2, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -421,10 +355,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'poster', 'type' => 'u', 'id' => 3, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -432,10 +363,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'replier', 'type' => 'u', 'id' => 4, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -443,10 +371,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_normal', 'type' => 'u', 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -454,10 +379,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_hidden', 'type' => 'u', 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -465,10 +387,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'friend', 'type' => 'u', 'id' => 7, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -476,10 +395,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test', 'type' => 'u', 'id' => 8, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -487,10 +403,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test1', 'type' => 'u', 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -498,10 +411,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'Group we are a member of', 'type' => 'g', 'id' => 3, - 'avatar' => [ - 'type' => 'group', - 'img' => '', - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -514,10 +424,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_hidden', 'type' => 'u', 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -525,10 +432,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_normal', 'type' => 'u', 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 1, ], @@ -536,10 +440,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_normal', 'type' => 'u', 'id' => 5, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -547,10 +448,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'team_member_hidden', 'type' => 'u', 'id' => 6, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -558,10 +456,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test', 'type' => 'u', 'id' => 8, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -569,10 +464,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test1', 'type' => 'u', 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -580,10 +472,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test2', 'type' => 'u', 'id' => 10, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -591,10 +480,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test3', 'type' => 'u', 'id' => 11, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -607,10 +493,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test', 'type' => 'u', 'id' => 8, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -618,10 +501,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test1', 'type' => 'u', 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -629,10 +509,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test2', 'type' => 'u', 'id' => 10, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -640,10 +517,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test3', 'type' => 'u', 'id' => 11, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ], @@ -655,10 +529,7 @@ class phpbb_mention_controller_test extends phpbb_database_test_case 'name' => 'test1', 'type' => 'u', 'id' => 9, - 'avatar' => [ - 'type' => 'user', - 'img' => [], - ], + 'avatar' => [], 'rank' => '', 'priority' => 0, ]], From 4f116db48e2da2e4afd6b04684ce8d8c00433243 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Sat, 15 May 2021 21:26:52 +0200 Subject: [PATCH 103/113] [ticket/13713] Remove not needed svg rule PHPBB3-13713 --- phpBB/adm/style/admin.css | 4 ---- phpBB/styles/prosilver/theme/mentions.css | 4 ---- 2 files changed, 8 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index a6d29868ab..7b5257859f 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1726,10 +1726,6 @@ fieldset.submit-buttons legend { max-height: 100%; } -svg { /* TODO: remove it after general normalization */ - fill: currentColor; -} - .mention-item { font-size: 16px; font-weight: 400; diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index a4207a9e49..23816982b5 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -48,10 +48,6 @@ max-height: 100%; } -svg { /* TODO: remove it after general normalization */ - fill: currentColor; -} - .mention-item { font-size: 16px; font-weight: 400; From 643bab44ea1a20ebc44e761f95a6e073effcae3d Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Sat, 15 May 2021 22:23:42 +0200 Subject: [PATCH 104/113] [ticket/13713] Make sure service collection is accepted by controller PHPBB3-13713 --- phpBB/phpbb/mention/controller/mention.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpBB/phpbb/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php index 10e21f5cfc..37a5cfd323 100644 --- a/phpBB/phpbb/mention/controller/mention.php +++ b/phpBB/phpbb/mention/controller/mention.php @@ -35,12 +35,12 @@ class mention /** * Constructor * - * @param array $mention_sources + * @param service_collection|array $mention_sources * @param request_interface $request * @param string $phpbb_root_path * @param string $phpEx */ - public function __construct(array $mention_sources, request_interface $request, string $phpbb_root_path, string $phpEx) + public function __construct($mention_sources, request_interface $request, string $phpbb_root_path, string $phpEx) { $this->mention_sources = $mention_sources; $this->request = $request; From 6eeb22cdb5753151aa42f71bf1187e835894417d Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Sun, 16 May 2021 10:45:48 +0200 Subject: [PATCH 105/113] [ticket/13713] Set mention-list class if needed PHPBB3-13713 --- phpBB/adm/style/admin.css | 6 ++--- phpBB/assets/javascript/editor.js | 27 ++++++++++++++++++----- phpBB/styles/prosilver/theme/mentions.css | 4 ++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 7b5257859f..e48edf49cf 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1676,7 +1676,7 @@ fieldset.submit-buttons legend { font-weight: bold; } -.mention-container { /* mention-container */ +.mention-container { text-align: left; background-color: #ffffff; border-radius: 2px; @@ -1691,11 +1691,11 @@ fieldset.submit-buttons legend { transition: all 0.2s ease; } -.rtl .mention-container { /* mention-container */ +.rtl .mention-container { text-align: right; } -.mention-container ul { /* mention-list */ +.mention-list { margin: 0; padding: 0; list-style-type: none; diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 7945924b84..a96a291a20 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -626,6 +626,26 @@ function getCaretPosition(txtarea) { }); } + /** + * Generate menu item HTML representation. Also ensures that mention-list + * class is set for unordered list in mention container + * + * @param {object} data Item data + * @returns {string} HTML representation of menu item + */ + function menuItemTemplate(data) { + const itemData = data; + const avatar = getAvatar(itemData.avatar, itemData.type); + const rank = (itemData.rank) ? "" + itemData.rank + "" : ''; + const $mentionContainer = $('.' + tribute.current.collection.containerClass); + + if (typeof $mentionContainer !== 'undefined' && $mentionContainer.children('ul').hasClass('mention-list') === false) { + $mentionContainer.children('ul').addClass('mention-list'); + } + + return "" + avatar + "" + itemData.name + rank + ""; + } + this.isEnabled = function() { return $mentionDataContainer.length; }; @@ -637,12 +657,7 @@ function getCaretPosition(txtarea) { containerClass: 'mention-container', selectClass: 'is-active', itemClass: 'mention-item', - menuItemTemplate: function (data) { - const itemData = data; - let avatar = getAvatar(itemData.avatar, itemData.type); - let rank = (itemData.rank) ? "" + itemData.rank + "" : ''; - return "" + avatar + "" + itemData.name + rank + ""; - }, + menuItemTemplate: menuItemTemplate, selectTemplate: function (item) { return '[mention=' + item.type + ':' + item.id + ']' + item.name + '[/mention]'; }, diff --git a/phpBB/styles/prosilver/theme/mentions.css b/phpBB/styles/prosilver/theme/mentions.css index 23816982b5..cec8e3fe5d 100644 --- a/phpBB/styles/prosilver/theme/mentions.css +++ b/phpBB/styles/prosilver/theme/mentions.css @@ -13,7 +13,7 @@ /* Mention dropdown ---------------------------------------- */ -.mention-container { /* mention-container */ +.mention-container { text-align: left; border-radius: 2px; position: absolute; @@ -23,7 +23,7 @@ transition: all 0.2s ease; } -.mention-container ul { /* mention-list */ +.mention-list { margin: 0; padding: 0; list-style-type: none; From a86d9699a5850e4a791bf9b5e4ccc99bdc974471 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Mon, 17 May 2021 22:14:11 +0200 Subject: [PATCH 106/113] [ticket/13713] Add mentions code to mentions.js PHPBB3-13713 --- phpBB/adm/style/acp_posting_buttons.html | 3 +- phpBB/assets/javascript/editor.js | 330 ++---------------- phpBB/assets/javascript/mentions.js | 309 ++++++++++++++++ .../prosilver/template/posting_buttons.html | 3 +- 4 files changed, 337 insertions(+), 308 deletions(-) create mode 100644 phpBB/assets/javascript/mentions.js diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index 82c35dc65c..c99a168f12 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -8,8 +8,9 @@ // ]]> - + +
      data-mention-url="{U_MENTION_URL}" data-mention-names-limit="{S_MENTION_NAMES_LIMIT}" data-topic-id="{S_TOPIC_ID}" data-user-id="{S_USER_ID}"> diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index a96a291a20..d1734564b5 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -383,317 +383,39 @@ function getCaretPosition(txtarea) { return caretPos; } -/* import Tribute from './jquery.tribute'; */ + +/** + * Get editor text area element + * + * @return {HTMLElement|null} Text area element or null if textarea couldn't be found + */ +function getEditorTextArea() { + let doc; + + // find textarea, make sure browser supports necessary functions + if (document.forms[form_name]) { + doc = document; + } else { + doc = opener.document; + } + + if (!doc.forms[form_name]) { + return; + } + + return doc.forms[form_name].elements[text_name]; +} (function($) { 'use strict'; - /** - * Mentions data returned from ajax requests - * @typedef {Object} MentionsData - * @property {string} name User/group name - * @property {string} id User/group ID - * @property {{img: string, group: string}} avatar Avatar data - * @property {string} rank User rank or empty string for groups - * @property {number} priority Priority of data entry - */ - - /** - * Mentions class - * @constructor - */ - function Mentions() { - let $mentionDataContainer = $('[data-mention-url]:first'); - let mentionURL = $mentionDataContainer.data('mentionUrl'); - let mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'); - let mentionTopicId = $mentionDataContainer.data('topicId'); - let mentionUserId = $mentionDataContainer.data('userId'); - let queryInProgress = null; - let cachedNames = []; - let cachedAll = []; - let cachedSearchKey = 'name'; - let tribute = null; - - /** - * Get default avatar - * @param {string} type Type of avatar; either 'g' for group or user on any other value - * @returns {string} Default avatar svg code - */ - function defaultAvatar(type) { - if (type === 'g') { - return ''; - } else { - return ''; - } - } - - /** - * Get avatar HTML for data and type of avatar - * - * @param {object} data - * @param {string} type - * @return {string} Avatar HTML - */ - function getAvatar(data, type) { - const avatarToHtml = (avatarData) => { - if (avatarData.html !== '') { - return avatarData.html; - } else { - return '' + avatarData.title + ''; - } - } - - return data.html === '' && data.src === '' ? defaultAvatar(type) : "" + avatarToHtml(data)+ ""; - } - - /** - * Get cached keyword for query string - * @param {string} query Query string - * @returns {?string} Cached keyword if one fits query, else empty string if cached keywords exist, null if cached keywords do not exist - */ - function getCachedKeyword(query) { - if (!cachedNames) { - return null; - } - - let i; - - for (i = query.length; i > 0; i--) { - let startStr = query.substr(0, i); - if (cachedNames[startStr]) { - return startStr; - } - } - - return ''; - } - - /** - * Get names matching query - * @param {string} query Query string - * @param {Object.} items List of {@link MentionsData} items - * @param {string} searchKey Key to use for matching items - * @returns {Object.} List of {@link MentionsData} items filtered with query and by searchKey - */ - function getMatchedNames(query, items, searchKey) { - let i; - let itemsLength; - let matchedNames = []; - for (i = 0, itemsLength = items.length; i < itemsLength; i++) { - let item = items[i]; - if (isItemMatched(query, item, searchKey)) { - matchedNames.push(item); - } - } - return matchedNames; - } - - /** - * Return whether item is matched by query - * - * @param {string} query Search query string - * @param {MentionsData} item Mentions data item - * @param {string }searchKey Key to use for matching items - * @return {boolean} True if items is matched, false otherwise - */ - function isItemMatched(query, item, searchKey) { - return String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()) === 0; - } - - /** - * Filter items by search query - * - * @param {string} query Search query string - * @param {Object.} items List of {@link MentionsData} items - * @return {Object.} List of {@link MentionsData} items filtered with query and by searchKey - */ - function itemFilter(query, items) { - let i; - let len; - let highestPriorities = {u: 1, g: 1}; - let _unsorted = {u: {}, g: {}}; - let _exactMatch = []; - let _results = []; - - // Reduce the items array to the relevant ones - items = getMatchedNames(query, items, 'name'); - - // Group names by their types and calculate priorities - for (i = 0, len = items.length; i < len; i++) { - let item = items[i]; - - // Check for unsupported type - in general, this should never happen - if (!_unsorted[item.type]) { - continue; - } - - // Current user doesn't want to mention themselves with "@" in most cases - - // do not waste list space with their own name - if (item.type === 'u' && item.id === String(mentionUserId)) { - continue; - } - - // Exact matches should not be prioritised - they always come first - if (item.name === query) { - _exactMatch.push(items[i]); - continue; - } - - // If the item hasn't been added yet - add it - if (!_unsorted[item.type][item.id]) { - _unsorted[item.type][item.id] = item; - continue; - } - - // Priority is calculated as the sum of priorities from different sources - _unsorted[item.type][item.id].priority += parseFloat(item.priority.toString()); - - // Calculate the highest priority - we'll give it to group names - highestPriorities[item.type] = Math.max(highestPriorities[item.type], _unsorted[item.type][item.id].priority); - } - - // All types of names should come at the same level of importance, - // otherwise they will be unlikely to be shown - // That's why we normalize priorities and push names to a single results array - $.each(['u', 'g'], function(key, type) { - if (_unsorted[type]) { - $.each(_unsorted[type], function(name, value) { - // Normalize priority - value.priority /= highestPriorities[type]; - - // Add item to all results - _results.push(value); - }); - } - }); - - // Sort names by priorities - higher values come first - _results = _results.sort(function(a, b) { - return b.priority - a.priority; - }); - - // Exact match is the most important - should come above anything else - $.each(_exactMatch, function(name, value) { - _results.unshift(value); - }); - - return _results; - } - - /** - * remoteFilter callback filter function - * @param {string} query Query string - * @param {function} callback Callback function for filtered items - */ - function remoteFilter(query, callback) { - /* - * Do not make a new request until the previous one for the same query is returned - * This fixes duplicate server queries e.g. when arrow keys are pressed - */ - if (queryInProgress === query) { - setTimeout(function() { - remoteFilter(query, callback); - }, 1000); - return; - } - - let cachedKeyword = getCachedKeyword(query), - cachedNamesForQuery = (cachedKeyword !== null) ? cachedNames[cachedKeyword] : null; - - /* - * Use cached values when we can: - * 1) There are some names in the cache relevant for the query - * (cache for the query with the same first characters contains some data) - * 2) We have enough names to display OR - * all relevant names have been fetched from the server - */ - if (cachedNamesForQuery && - (getMatchedNames(query, cachedNamesForQuery, cachedSearchKey).length >= mentionNamesLimit || - cachedAll[cachedKeyword])) { - callback(cachedNamesForQuery); - return; - } - - queryInProgress = query; - - let params = {keyword: query, topic_id: mentionTopicId, _referer: location.href}; - $.getJSON(mentionURL, params, function(data) { - cachedNames[query] = data.names; - cachedAll[query] = data.all; - callback(data.names); - }).always(function() { - queryInProgress = null; - }); - } - - /** - * Generate menu item HTML representation. Also ensures that mention-list - * class is set for unordered list in mention container - * - * @param {object} data Item data - * @returns {string} HTML representation of menu item - */ - function menuItemTemplate(data) { - const itemData = data; - const avatar = getAvatar(itemData.avatar, itemData.type); - const rank = (itemData.rank) ? "" + itemData.rank + "" : ''; - const $mentionContainer = $('.' + tribute.current.collection.containerClass); - - if (typeof $mentionContainer !== 'undefined' && $mentionContainer.children('ul').hasClass('mention-list') === false) { - $mentionContainer.children('ul').addClass('mention-list'); - } - - return "" + avatar + "" + itemData.name + rank + ""; - } - - this.isEnabled = function() { - return $mentionDataContainer.length; - }; - - this.handle = function(textarea) { - tribute = new Tribute({ - trigger: '@', - allowSpaces: true, - containerClass: 'mention-container', - selectClass: 'is-active', - itemClass: 'mention-item', - menuItemTemplate: menuItemTemplate, - selectTemplate: function (item) { - return '[mention=' + item.type + ':' + item.id + ']' + item.name + '[/mention]'; - }, - menuItemLimit: mentionNamesLimit, - values: function (text, cb) { - remoteFilter(text, users => cb(users)); - }, - lookup: function (element) { - return element.hasOwnProperty('name') ? element.name : ''; - } - }); - - tribute.search.filter = itemFilter; - - tribute.attach($(textarea)); - }; - } - phpbb.mentions = new Mentions(); - $(document).ready(function() { - let doc; - let textarea; + const textarea = getEditorTextArea(); - // find textarea, make sure browser supports necessary functions - if (document.forms[form_name]) { - doc = document; - } else { - doc = opener.document; - } - - if (!doc.forms[form_name]) { + if (typeof textarea === 'undefined') { return; } - textarea = doc.forms[form_name].elements[text_name]; - /** * Allow to use tab character when typing code * Keep indentation of last line of code when typing code @@ -704,10 +426,6 @@ function getCaretPosition(txtarea) { phpbb.showDragNDrop(textarea); } - if (phpbb.mentions.isEnabled()) { - phpbb.mentions.handle(textarea); - } - $('textarea').on('keydown', function (e) { if (e.which === 13 && (e.metaKey || e.ctrlKey)) { $(this).closest('form').find(':submit').click(); diff --git a/phpBB/assets/javascript/mentions.js b/phpBB/assets/javascript/mentions.js new file mode 100644 index 0000000000..42de121b6b --- /dev/null +++ b/phpBB/assets/javascript/mentions.js @@ -0,0 +1,309 @@ +/* global phpbb */ +/* import Tribute from './tribute.min'; */ + +(function($) { + 'use strict'; + + /** + * Mentions data returned from ajax requests + * @typedef {Object} MentionsData + * @property {string} name User/group name + * @property {string} id User/group ID + * @property {{img: string, group: string}} avatar Avatar data + * @property {string} rank User rank or empty string for groups + * @property {number} priority Priority of data entry + */ + + /** + * Mentions class + * @constructor + */ + function Mentions() { + const $mentionDataContainer = $('[data-mention-url]:first'); + const mentionURL = $mentionDataContainer.data('mentionUrl'); + const mentionNamesLimit = $mentionDataContainer.data('mentionNamesLimit'); + const mentionTopicId = $mentionDataContainer.data('topicId'); + const mentionUserId = $mentionDataContainer.data('userId'); + let queryInProgress = null; + const cachedNames = []; + const cachedAll = []; + const cachedSearchKey = 'name'; + let tribute = null; + + /** + * Get default avatar + * @param {string} type Type of avatar; either 'g' for group or user on any other value + * @returns {string} Default avatar svg code + */ + function defaultAvatar(type) { + if (type === 'g') { + return ''; + } + + return ''; + } + + /** + * Get avatar HTML for data and type of avatar + * + * @param {object} data + * @param {string} type + * @return {string} Avatar HTML + */ + function getAvatar(data, type) { + const avatarToHtml = avatarData => { + if (avatarData.html === '') { + return '' + avatarData.title + ''; + } + + return avatarData.html; + }; + + return data.html === '' && data.src === '' ? defaultAvatar(type) : '' + avatarToHtml(data) + ''; + } + + /** + * Get cached keyword for query string + * @param {string} query Query string + * @returns {?string} Cached keyword if one fits query, else empty string if cached keywords exist, null if cached keywords do not exist + */ + function getCachedKeyword(query) { + if (!cachedNames) { + return null; + } + + let i; + + for (i = query.length; i > 0; i--) { + const startStr = query.substr(0, i); + if (cachedNames[startStr]) { + return startStr; + } + } + + return ''; + } + + /** + * Get names matching query + * @param {string} query Query string + * @param {Object.} items List of {@link MentionsData} items + * @param {string} searchKey Key to use for matching items + * @returns {Object.} List of {@link MentionsData} items filtered with query and by searchKey + */ + function getMatchedNames(query, items, searchKey) { + let i; + let itemsLength; + const matchedNames = []; + for (i = 0, itemsLength = items.length; i < itemsLength; i++) { + const item = items[i]; + if (isItemMatched(query, item, searchKey)) { + matchedNames.push(item); + } + } + + return matchedNames; + } + + /** + * Return whether item is matched by query + * + * @param {string} query Search query string + * @param {MentionsData} item Mentions data item + * @param {string }searchKey Key to use for matching items + * @return {boolean} True if items is matched, false otherwise + */ + function isItemMatched(query, item, searchKey) { + return String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()) === 0; + } + + /** + * Filter items by search query + * + * @param {string} query Search query string + * @param {Object.} items List of {@link MentionsData} items + * @return {Object.} List of {@link MentionsData} items filtered with query and by searchKey + */ + function itemFilter(query, items) { + let i; + let len; + const highestPriorities = { u: 1, g: 1 }; + const _unsorted = { u: {}, g: {} }; + const _exactMatch = []; + let _results = []; + + // Reduce the items array to the relevant ones + items = getMatchedNames(query, items, 'name'); + + // Group names by their types and calculate priorities + for (i = 0, len = items.length; i < len; i++) { + const item = items[i]; + + // Check for unsupported type - in general, this should never happen + if (!_unsorted[item.type]) { + continue; + } + + // Current user doesn't want to mention themselves with "@" in most cases - + // do not waste list space with their own name + if (item.type === 'u' && item.id === String(mentionUserId)) { + continue; + } + + // Exact matches should not be prioritised - they always come first + if (item.name === query) { + _exactMatch.push(items[i]); + continue; + } + + // If the item hasn't been added yet - add it + if (!_unsorted[item.type][item.id]) { + _unsorted[item.type][item.id] = item; + continue; + } + + // Priority is calculated as the sum of priorities from different sources + _unsorted[item.type][item.id].priority += parseFloat(item.priority.toString()); + + // Calculate the highest priority - we'll give it to group names + highestPriorities[item.type] = Math.max(highestPriorities[item.type], _unsorted[item.type][item.id].priority); + } + + // All types of names should come at the same level of importance, + // otherwise they will be unlikely to be shown + // That's why we normalize priorities and push names to a single results array + $.each([ 'u', 'g' ], (key, type) => { + if (_unsorted[type]) { + $.each(_unsorted[type], (name, value) => { + // Normalize priority + value.priority /= highestPriorities[type]; + + // Add item to all results + _results.push(value); + }); + } + }); + + // Sort names by priorities - higher values come first + _results = _results.sort((a, b) => { + return b.priority - a.priority; + }); + + // Exact match is the most important - should come above anything else + $.each(_exactMatch, (name, value) => { + _results.unshift(value); + }); + + return _results; + } + + /** + * remoteFilter callback filter function + * @param {string} query Query string + * @param {function} callback Callback function for filtered items + */ + function remoteFilter(query, callback) { + /* + * Do not make a new request until the previous one for the same query is returned + * This fixes duplicate server queries e.g. when arrow keys are pressed + */ + if (queryInProgress === query) { + setTimeout(() => { + remoteFilter(query, callback); + }, 1000); + return; + } + + const cachedKeyword = getCachedKeyword(query); + const cachedNamesForQuery = (cachedKeyword !== null) ? cachedNames[cachedKeyword] : null; + + /* + * Use cached values when we can: + * 1) There are some names in the cache relevant for the query + * (cache for the query with the same first characters contains some data) + * 2) We have enough names to display OR + * all relevant names have been fetched from the server + */ + if (cachedNamesForQuery && + (getMatchedNames(query, cachedNamesForQuery, cachedSearchKey).length >= mentionNamesLimit || + cachedAll[cachedKeyword])) { + callback(cachedNamesForQuery); + return; + } + + queryInProgress = query; + + const params = { keyword: query, topic_id: mentionTopicId, _referer: location.href }; + $.getJSON(mentionURL, params, data => { + cachedNames[query] = data.names; + cachedAll[query] = data.all; + callback(data.names); + }).always(() => { + queryInProgress = null; + }); + } + + /** + * Generate menu item HTML representation. Also ensures that mention-list + * class is set for unordered list in mention container + * + * @param {object} data Item data + * @returns {string} HTML representation of menu item + */ + function menuItemTemplate(data) { + const itemData = data; + const avatar = getAvatar(itemData.avatar, itemData.type); + const rank = (itemData.rank) ? '' + itemData.rank + '' : ''; + const $mentionContainer = $('.' + tribute.current.collection.containerClass); + + if (typeof $mentionContainer !== 'undefined' && $mentionContainer.children('ul').hasClass('mention-list') === false) { + $mentionContainer.children('ul').addClass('mention-list'); + } + + return '' + avatar + '' + itemData.name + rank + ''; + } + + this.isEnabled = function() { + return $mentionDataContainer.length; + }; + + this.handle = function(textarea) { + tribute = new Tribute({ + trigger: '@', + allowSpaces: true, + containerClass: 'mention-container', + selectClass: 'is-active', + itemClass: 'mention-item', + menuItemTemplate, + selectTemplate(item) { + return '[mention=' + item.type + ':' + item.id + ']' + item.name + '[/mention]'; + }, + menuItemLimit: mentionNamesLimit, + values(text, cb) { + remoteFilter(text, users => cb(users)); + }, + lookup(element) { + return Object.prototype.hasOwnProperty.call(element, 'name') ? element.name : ''; + }, + }); + + tribute.search.filter = itemFilter; + + tribute.attach($(textarea)); + }; + } + + phpbb.mentions = new Mentions(); + + $(document).ready(() => { + const textarea = getEditorTextArea(); + + if (typeof textarea === 'undefined') { + return; + } + + if (phpbb.mentions.isEnabled()) { + phpbb.mentions.handle(textarea); + } + }); +})(jQuery); diff --git a/phpBB/styles/prosilver/template/posting_buttons.html b/phpBB/styles/prosilver/template/posting_buttons.html index 2a76bd272e..fa36310198 100644 --- a/phpBB/styles/prosilver/template/posting_buttons.html +++ b/phpBB/styles/prosilver/template/posting_buttons.html @@ -26,8 +26,9 @@ } - + +