diff --git a/package.json b/package.json index 0dcb915ddd..1904ada244 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,44 @@ "jquery" ] }, + "eslintConfig": { + "extends": "xo", + "rules": { + "quotes": [ + "error", + "single" + ], + "comma-dangle": [ + "error", + "always-multiline" + ], + "block-spacing": "error", + "array-bracket-spacing": [ + "error", + "always" + ], + "multiline-comment-style": "off", + "computed-property-spacing": "off", + "space-in-parens": "off", + "capitalized-comments": "off", + "object-curly-spacing": [ + "error", + "always" + ], + "no-lonely-if": "off", + "unicorn/prefer-module": "off", + "space-before-function-paren": [ + "error", + "never" + ] + }, + "env": { + "es6": true, + "browser": true, + "node": true, + "jquery": true + } + }, "browserslist": [ "> 1%", "not ie 11", diff --git a/phpBB/adm/style/acp_posting_buttons.html b/phpBB/adm/style/acp_posting_buttons.html index dfe09ae12e..e032b8e77b 100644 --- a/phpBB/adm/style/acp_posting_buttons.html +++ b/phpBB/adm/style/acp_posting_buttons.html @@ -8,10 +8,14 @@ // ]]> +{% include 'mentions_templates.html' %} + + + -
[URL]
BBCode tag and automatic/magic URLs are disabled.',
'ALLOWED_SCHEMES_LINKS' => 'Allowed schemes in links',
@@ -187,6 +188,10 @@ $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_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.',
'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/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..95bc4c8601
--- /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..f42992ca0b 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 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',
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..c0e9a8cf58
--- /dev/null
+++ b/phpBB/phpbb/db/migration/data/v330/add_mention_settings.php
@@ -0,0 +1,43 @@
+
+* @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_batch_size', 50)),
+ 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/mention/controller/mention.php b/phpBB/phpbb/mention/controller/mention.php
new file mode 100644
index 0000000000..37a5cfd323
--- /dev/null
+++ b/phpBB/phpbb/mention/controller/mention.php
@@ -0,0 +1,78 @@
+
+ * @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 phpbb\di\service_collection;
+use phpbb\request\request_interface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+class mention
+{
+ /** @var service_collection */
+ protected $mention_sources;
+
+ /** @var request_interface */
+ protected $request;
+
+ /** @var string */
+ protected $phpbb_root_path;
+
+ /** @var string */
+ protected $php_ext;
+
+ /**
+ * Constructor
+ *
+ * @param service_collection|array $mention_sources
+ * @param request_interface $request
+ * @param string $phpbb_root_path
+ * @param string $phpEx
+ */
+ public function __construct($mention_sources, request_interface $request, string $phpbb_root_path, string $phpEx)
+ {
+ $this->mention_sources = $mention_sources;
+ $this->request = $request;
+ $this->phpbb_root_path = $phpbb_root_path;
+ $this->php_ext = $phpEx;
+ }
+
+ /**
+ * Handle requests to mention controller
+ *
+ * @return JsonResponse|RedirectResponse
+ */
+ public function handle()
+ {
+ if (!$this->request->is_ajax())
+ {
+ return 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);
+ $names = [];
+ $has_names_remaining = false;
+
+ foreach ($this->mention_sources as $source)
+ {
+ $has_names_remaining = !$source->get($names, $keyword, $topic_id) || $has_names_remaining;
+ }
+
+ return new JsonResponse([
+ 'names' => array_values($names),
+ 'all' => !$has_names_remaining,
+ ]);
+ }
+}
diff --git a/phpBB/phpbb/mention/source/base_group.php b/phpBB/phpbb/mention/source/base_group.php
new file mode 100644
index 0000000000..58fca4d054
--- /dev/null
+++ b/phpBB/phpbb/mention/source/base_group.php
@@ -0,0 +1,185 @@
+
+ * @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;
+
+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 driver_interface */
+ protected $db;
+
+ /** @var config */
+ protected $config;
+
+ /** @var helper */
+ protected $helper;
+
+ /** @var \phpbb\user */
+ protected $user;
+
+ /** @var auth */
+ protected $auth;
+
+ /** @var string */
+ protected $phpbb_root_path;
+
+ /** @var string */
+ protected $php_ext;
+
+ /** @var string|false */
+ protected $cache_ttl = false;
+
+ /** @var array Fetched groups' data */
+ protected $groups = null;
+
+ /**
+ * 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(driver_interface $db, config $config, helper $helper, \phpbb\user $user, auth $auth, string $phpbb_root_path, string $phpEx)
+ {
+ $this->db = $db;
+ $this->config = $config;
+ $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(): array
+ {
+ if (is_null($this->groups))
+ {
+ $query = $this->db->sql_build_query('SELECT', [
+ 'SELECT' => 'g.*, ug.user_id as ug_user_id',
+ 'FROM' => [
+ GROUPS_TABLE => 'g',
+ ],
+ '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'],
+ ],
+ ],
+ ]);
+ // Cache results for 5 minutes
+ $result = $this->db->sql_query($query, 600);
+
+ $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'])
+ {
+ // Skip the group that we should not be able to mention.
+ continue;
+ }
+
+ $group_name = $this->helper->get_name($row['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 $this->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(string $keyword, int $topic_id): string;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_priority(array $row): int
+ {
+ // By default every result from the source increases the priority by a fixed value
+ return 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ 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);
+
+ $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('/^' . 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'],
+ 'type' => 'g',
+ 'id' => $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]),
+ ]);
+
+ $i++;
+ }
+
+ return true;
+ }
+}
diff --git a/phpBB/phpbb/mention/source/base_user.php b/phpBB/phpbb/mention/source/base_user.php
new file mode 100644
index 0000000000..7e9b41d67d
--- /dev/null
+++ b/phpBB/phpbb/mention/source/base_user.php
@@ -0,0 +1,173 @@
+
+ * @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;
+
+use phpbb\config\config;
+use phpbb\db\driver\driver_interface;
+use phpbb\user_loader;
+
+abstract class base_user implements source_interface
+{
+ /** @var driver_interface */
+ protected $db;
+
+ /** @var config */
+ protected $config;
+
+ /** @var user_loader */
+ protected $user_loader;
+
+ /** @var string */
+ protected $phpbb_root_path;
+
+ /** @var string */
+ protected $php_ext;
+
+ /** @var string|false */
+ protected $cache_ttl = false;
+
+ /**
+ * 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(driver_interface $db, config $config, user_loader $user_loader, string $phpbb_root_path, string $phpEx)
+ {
+ $this->db = $db;
+ $this->config = $config;
+ $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(string $keyword, int $topic_id): string;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_priority(array $row): int
+ {
+ // By default every result from the source increases the priority by a fixed value
+ return 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get(array &$names, string $keyword, int $topic_id): bool
+ {
+ $fetched_all = false;
+ $keyword = utf8_clean_string($keyword);
+
+ $i = 0;
+ $users = [];
+ $user_ids = [];
+
+ // Grab all necessary user IDs and cache them if needed
+ if ($this->cache_ttl)
+ {
+ $result = $this->db->sql_query($this->query($keyword, $topic_id), $this->cache_ttl);
+
+ while ($i < $this->config['mention_batch_size'])
+ {
+ $row = $this->db->sql_fetchrow($result);
+
+ if (!$row)
+ {
+ $fetched_all = true;
+ break;
+ }
+
+ if (!empty($keyword) && strpos($row['username_clean'], $keyword) !== 0)
+ {
+ continue;
+ }
+
+ $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;
+ }
+ }
+
+ $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);
+
+ foreach ($users as $user)
+ {
+ $user_rank = $this->user_loader->get_rank($user['user_id']);
+ array_push($names, [
+ 'name' => $this->user_loader->get_username($user['user_id'], 'username'),
+ 'type' => 'u',
+ 'id' => $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),
+ ]);
+ }
+
+ return $fetched_all;
+ }
+}
diff --git a/phpBB/phpbb/mention/source/friend.php b/phpBB/phpbb/mention/source/friend.php
new file mode 100644
index 0000000000..5c7a3f91ef
--- /dev/null
+++ b/phpBB/phpbb/mention/source/friend.php
@@ -0,0 +1,58 @@
+
+ * @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 base_user
+{
+ /** @var \phpbb\user */
+ protected $user;
+
+ /**
+ * Set the user service used to retrieve current user ID
+ *
+ * @param \phpbb\user $user
+ */
+ public function set_user(\phpbb\user $user): void
+ {
+ $this->user = $user;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ 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
+ */
+ 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'
+ ]
+ ],
+ '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'
+ ]);
+ }
+}
diff --git a/phpBB/phpbb/mention/source/group.php b/phpBB/phpbb/mention/source/group.php
new file mode 100644
index 0000000000..11a8e02e94
--- /dev/null
+++ b/phpBB/phpbb/mention/source/group.php
@@ -0,0 +1,48 @@
+
+ * @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 group extends base_group
+{
+ /** @var string|false */
+ protected $cache_ttl = 300;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get_priority(array $row): int
+ {
+ /*
+ * 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(string $keyword, int $topic_id): string
+ {
+ return $this->db->sql_build_query('SELECT', [
+ 'SELECT' => 'g.group_id',
+ 'FROM' => [
+ GROUPS_TABLE => 'g',
+ ],
+ 'ORDER_BY' => 'g.group_name',
+ ]);
+ }
+}
diff --git a/phpBB/phpbb/mention/source/source_interface.php b/phpBB/phpbb/mention/source/source_interface.php
new file mode 100644
index 0000000000..2fe45ef234
--- /dev/null
+++ b/phpBB/phpbb/mention/source/source_interface.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;
+
+interface source_interface
+{
+ /**
+ * Searches database for names to mention
+ * 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 bool Whether there are no more satisfying names left
+ */
+ public function get(array &$names, string $keyword, int $topic_id): bool;
+
+ /**
+ * 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)
+ */
+ public function get_priority(array $row): int;
+}
diff --git a/phpBB/phpbb/mention/source/team.php b/phpBB/phpbb/mention/source/team.php
new file mode 100644
index 0000000000..02fd8cefbb
--- /dev/null
+++ b/phpBB/phpbb/mention/source/team.php
@@ -0,0 +1,46 @@
+
+ * @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 base_user
+{
+ /** @var string|false */
+ protected $cache_ttl = 300;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function query(string $keyword, int $topic_id): string
+ {
+ /*
+ * 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
+ */
+ return $this->db->sql_build_query('SELECT_DISTINCT', [
+ 'SELECT' => 'u.username_clean, 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]),
+ 'ORDER_BY' => 'u.username_clean'
+ ]);
+ }
+}
diff --git a/phpBB/phpbb/mention/source/topic.php b/phpBB/phpbb/mention/source/topic.php
new file mode 100644
index 0000000000..842d38c4ef
--- /dev/null
+++ b/phpBB/phpbb/mention/source/topic.php
@@ -0,0 +1,64 @@
+
+ * @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 topic extends base_user
+{
+ /**
+ * {@inheritdoc}
+ */
+ 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
+ */
+ return $row['user_id'] === $row['topic_poster'] ? 5 : 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ 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 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
+ */
+ 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' => [TOPICS_TABLE => 't'],
+ 'ON' => 't.topic_id = p.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'
+ ]);
+ }
+}
diff --git a/phpBB/phpbb/mention/source/user.php b/phpBB/phpbb/mention/source/user.php
new file mode 100644
index 0000000000..3189f32b83
--- /dev/null
+++ b/phpBB/phpbb/mention/source/user.php
@@ -0,0 +1,47 @@
+
+ * @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 user extends base_user
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function get_priority(array $row): int
+ {
+ /*
+ * 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(string $keyword, int $topic_id): string
+ {
+ 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]) . '
+ 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/usergroup.php b/phpBB/phpbb/mention/source/usergroup.php
new file mode 100644
index 0000000000..de02cd76d6
--- /dev/null
+++ b/phpBB/phpbb/mention/source/usergroup.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 usergroup extends base_group
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function query(string $keyword, int $topic_id): string
+ {
+ 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'
+ ]
+ ],
+ 'WHERE' => 'ug.user_pending = 0 AND ug.user_id = ' . (int) $this->user->data['user_id'],
+ 'ORDER_BY' => 'g.group_name',
+ ]);
+ }
+}
diff --git a/phpBB/phpbb/notification/type/mention.php b/phpBB/phpbb/notification/type/mention.php
new file mode 100644
index 0000000000..fad31b9912
--- /dev/null
+++ b/phpBB/phpbb/notification/type/mention.php
@@ -0,0 +1,157 @@
+
+* @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;
+
+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 post
+{
+ /**
+ * @var mention_helper
+ */
+ protected $helper;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get_type()
+ {
+ return 'notification.type.mention';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected $language_key = 'NOTIFICATION_MENTION';
+
+ /**
+ * {@inheritDoc}
+ */
+ public static $notification_option = [
+ 'lang' => 'NOTIFICATION_TYPE_MENTION',
+ 'group' => 'NOTIFICATION_GROUP_POSTING',
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function is_available()
+ {
+ return $this->config['allow_mentions'] && $this->auth->acl_get('u_mention');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function find_users_for_notification($post, $options = array())
+ {
+ $options = array_merge(array(
+ 'ignore_users' => array(),
+ ), $options);
+
+ $user_ids = $this->helper->get_mentioned_user_ids($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
+ *
+ * @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();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get_email_template()
+ {
+ return 'mention';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ 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 mention_helper $helper
+ */
+ public function set_helper(mention_helper $helper): void
+ {
+ $this->helper = $helper;
+ }
+}
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 721549cf72..6ccd15ab96 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={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
author={TEXT1;optional}
@@ -108,13 +114,13 @@ class factory implements \phpbb\textformatter\cache_interface
* @var array Default templates, taken from bbcode::bbcode_tpl()
*/
protected $default_templates = array(
- 'b' => '