From 36c198f84dc43f5eb1040dbf14bb15c48cf362a3 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 16 Jan 2019 23:00:11 +0100 Subject: [PATCH] Support mentions of user groups See #2804 --- com.woltlab.wcf/bbcode.xml | 9 +++ com.woltlab.wcf/templates/groupBBCodeTag.tpl | 5 ++ .../files/acp/templates/userGroupAdd.tpl | 9 +++ .../WoltLabSuite/Core/Ui/Redactor/Mention.js | 3 +- .../lib/acp/form/UserGroupAddForm.class.php | 14 +++- .../lib/acp/form/UserGroupEditForm.class.php | 25 ++++++- .../files/lib/data/user/UserAction.class.php | 11 +++ .../lib/data/user/group/UserGroup.class.php | 60 +++++++++++++++-- .../lib/system/bbcode/GroupBBCode.class.php | 31 +++++++++ .../node/HtmlInputNodeTextParser.class.php | 67 ++++++++++++++++--- .../files/lib/util/MessageUtil.class.php | 40 ++++++++++- .../files/style/bbcode/groupMention.scss | 17 +++++ wcfsetup/install/lang/de.xml | 1 + wcfsetup/install/lang/en.xml | 1 + wcfsetup/setup/db/install.sql | 3 +- 15 files changed, 272 insertions(+), 24 deletions(-) create mode 100644 com.woltlab.wcf/templates/groupBBCodeTag.tpl create mode 100644 wcfsetup/install/files/lib/system/bbcode/GroupBBCode.class.php create mode 100644 wcfsetup/install/files/style/bbcode/groupMention.scss diff --git a/com.woltlab.wcf/bbcode.xml b/com.woltlab.wcf/bbcode.xml index a9ee4c8622..a74e5f21e3 100644 --- a/com.woltlab.wcf/bbcode.xml +++ b/com.woltlab.wcf/bbcode.xml @@ -233,5 +233,14 @@ + + wcf\system\bbcode\GroupBBCode + + + + 1 + + + diff --git a/com.woltlab.wcf/templates/groupBBCodeTag.tpl b/com.woltlab.wcf/templates/groupBBCodeTag.tpl new file mode 100644 index 0000000000..74ae8bfc52 --- /dev/null +++ b/com.woltlab.wcf/templates/groupBBCodeTag.tpl @@ -0,0 +1,5 @@ +{if $group} + {$group->getName()} +{else} + @{$groupName} +{/if} diff --git a/wcfsetup/install/files/acp/templates/userGroupAdd.tpl b/wcfsetup/install/files/acp/templates/userGroupAdd.tpl index 055bc8d8ba..bd03a7537c 100644 --- a/wcfsetup/install/files/acp/templates/userGroupAdd.tpl +++ b/wcfsetup/install/files/acp/templates/userGroupAdd.tpl @@ -151,6 +151,15 @@ {/if} + + {if $action === 'add' || !$isUnmentionableGroup} +
+
+
+ +
+
+ {/if} {event name='dataFields'} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js index a1bf7505e2..65ec70a03f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js @@ -373,7 +373,8 @@ define(['Ajax', 'Environment', 'StringUtil', 'Ui/CloseOverlay'], function(Ajax, interfaceName: 'wcf\\data\\ISearchAction', parameters: { data: { - includeUserGroups: false + includeUserGroups: true, + scope: 'mention' } } }, diff --git a/wcfsetup/install/files/lib/acp/form/UserGroupAddForm.class.php b/wcfsetup/install/files/lib/acp/form/UserGroupAddForm.class.php index e35528e613..0fff208d71 100755 --- a/wcfsetup/install/files/lib/acp/form/UserGroupAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserGroupAddForm.class.php @@ -80,6 +80,11 @@ class UserGroupAddForm extends AbstractOptionListForm { */ protected $showOnTeamPage = 0; + /** + * @var int + */ + protected $allowMention = 0; + /** * @inheritDoc */ @@ -104,6 +109,7 @@ class UserGroupAddForm extends AbstractOptionListForm { if (isset($_POST['priority'])) $this->priority = intval($_POST['priority']); if (isset($_POST['userOnlineMarking'])) $this->userOnlineMarking = StringUtil::trim($_POST['userOnlineMarking']); if (isset($_POST['showOnTeamPage'])) $this->showOnTeamPage = intval($_POST['showOnTeamPage']); + if (isset($_POST['allowMention'])) $this->allowMention = intval($_POST['allowMention']); } /** @@ -150,7 +156,8 @@ class UserGroupAddForm extends AbstractOptionListForm { 'groupDescription' => $this->groupDescription, 'priority' => $this->priority, 'userOnlineMarking' => $this->userOnlineMarking, - 'showOnTeamPage' => $this->showOnTeamPage + 'showOnTeamPage' => $this->showOnTeamPage, + 'allowMention' => $this->allowMention ? 1 : 0, ]), 'options' => $optionValues ]; @@ -185,7 +192,7 @@ class UserGroupAddForm extends AbstractOptionListForm { // reset values $this->groupName = ''; - $this->priority = 0; + $this->allowMention = $this->priority = $this->showOnTeamPage = 0; I18nHandler::getInstance()->reset(); } @@ -219,7 +226,8 @@ class UserGroupAddForm extends AbstractOptionListForm { 'userOnlineMarking' => $this->userOnlineMarking, 'showOnTeamPage' => $this->showOnTeamPage, 'groupIsGuest' => false, - 'isBlankForm' => empty($_POST) + 'isBlankForm' => empty($_POST), + 'allowMention' => $this->allowMention, ]); } diff --git a/wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php b/wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php index 79f81dc5a6..1582b7f526 100755 --- a/wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php @@ -40,6 +40,11 @@ class UserGroupEditForm extends UserGroupAddForm { */ public $group; + /** + * @var bool + */ + public $isUnmentionableGroup = false; + /** * @inheritDoc */ @@ -62,6 +67,8 @@ class UserGroupEditForm extends UserGroupAddForm { $this->optionHandler->setUserGroup($group); /** @noinspection PhpUndefinedMethodInspection */ $this->optionHandler->init(); + + $this->isUnmentionableGroup = $this->group->isUnmentionableGroup(); } /** @@ -72,6 +79,17 @@ class UserGroupEditForm extends UserGroupAddForm { // user group } + /** + * @inheritDoc + */ + public function validate() { + parent::validate(); + + if ($this->allowMention && $this->isUnmentionableGroup) { + $this->allowMention = false; + } + } + /** * @inheritDoc */ @@ -84,6 +102,7 @@ class UserGroupEditForm extends UserGroupAddForm { $this->priority = $this->group->priority; $this->userOnlineMarking = $this->group->userOnlineMarking; $this->showOnTeamPage = $this->group->showOnTeamPage; + $this->allowMention = $this->group->allowMention; } parent::readData(); @@ -104,7 +123,8 @@ class UserGroupEditForm extends UserGroupAddForm { 'availableUserGroups' => UserGroup::getAccessibleGroups(), 'groupIsEveryone' => $this->group->groupType == UserGroup::EVERYONE, 'groupIsGuest' => $this->group->groupType == UserGroup::GUESTS, - 'groupIsUsers' => $this->group->groupType == UserGroup::USERS + 'groupIsUsers' => $this->group->groupType == UserGroup::USERS, + 'isUnmentionableGroup' => $this->isUnmentionableGroup ? 1 : 0, ]); // add warning when the initiator is in the group @@ -149,7 +169,8 @@ class UserGroupEditForm extends UserGroupAddForm { 'groupDescription' => $this->groupDescription, 'priority' => $this->priority, 'userOnlineMarking' => $this->userOnlineMarking, - 'showOnTeamPage' => $this->showOnTeamPage + 'showOnTeamPage' => $this->showOnTeamPage, + 'allowMention' => $this->allowMention, ]), 'options' => $optionValues ]); diff --git a/wcfsetup/install/files/lib/data/user/UserAction.class.php b/wcfsetup/install/files/lib/data/user/UserAction.class.php index c83b9d4f2a..af4e7916dd 100644 --- a/wcfsetup/install/files/lib/data/user/UserAction.class.php +++ b/wcfsetup/install/files/lib/data/user/UserAction.class.php @@ -480,10 +480,17 @@ class UserAction extends AbstractDatabaseObjectAction implements IClipboardActio $this->readBoolean('includeUserGroups', false, 'data'); $this->readString('searchString', false, 'data'); $this->readIntegerArray('restrictUserGroupIDs', true, 'data'); + $this->readString('scope', true, 'data'); if (isset($this->parameters['data']['excludedSearchValues']) && !is_array($this->parameters['data']['excludedSearchValues'])) { throw new UserInputException('excludedSearchValues'); } + + if ($this->parameters['data']['scope']) { + if (!in_array($this->parameters['data']['scope'], ['mention'])) { + throw new UserInputException('scope'); + } + } } /** @@ -504,6 +511,10 @@ class UserAction extends AbstractDatabaseObjectAction implements IClipboardActio continue; } + if ($this->parameters['data']['scope'] === 'mention' && !$group->canBeMentioned()) { + continue; + } + $groupName = $group->getName(); if (!in_array($groupName, $excludedSearchValues)) { $pos = mb_strripos($groupName, $searchString); diff --git a/wcfsetup/install/files/lib/data/user/group/UserGroup.class.php b/wcfsetup/install/files/lib/data/user/group/UserGroup.class.php index 6d6087baf9..9cfa83c24e 100644 --- a/wcfsetup/install/files/lib/data/user/group/UserGroup.class.php +++ b/wcfsetup/install/files/lib/data/user/group/UserGroup.class.php @@ -17,12 +17,19 @@ use wcf\system\WCF; * @package WoltLabSuite\Core\Data\User\Group * * @property-read integer $groupID unique id of the user group - * @property-read string $groupName name of the user group or name of language item which contains the name - * @property-read string $groupDescription description of the user group or name of language item which contains the description + * @property-read string $groupName name of the user group or name of language + * item which contains the name + * @property-read string $groupDescription description of the user group or name of + * language item which contains the description * @property-read integer $groupType identifier of the type of user group - * @property-read integer $priority priority of the user group used to determine member's user rank and online marking - * @property-read string $userOnlineMarking HTML code used to print the formatted name of a user group member - * @property-read integer $showOnTeamPage is `1` if the user group and its members should be shown on the team page, otherwise `0` + * @property-read integer $priority priority of the user group used to determine + * member's user rank and online marking + * @property-read string $userOnlineMarking HTML code used to print the formatted name of + * a user group member + * @property-read integer $showOnTeamPage is `1` if the user group and its members + * should be shown on the team page, otherwise `0` + * @property-read int $allowMention is `1` if the user group can be mentioned in messages, + * otherwise `0` */ class UserGroup extends DatabaseObject implements ITitledObject { /** @@ -388,4 +395,47 @@ class UserGroup extends DatabaseObject implements ITitledObject { public function getTitle() { return WCF::getLanguage()->get($this->groupName); } + + /** + * The `Everyone`, `Guests` and `Users` group can never be mentioned. + * + * @return bool + * @since 5.2 + */ + public function isUnmentionableGroup() { + return in_array($this->groupType, [self::EVERYONE, self::GUESTS, self::USERS]); + } + + /** + * Returns true if this group can be mentioned, is always false for the + * `Everyone`, `Guests` and `Users` group. + * + * @return bool + * @since 5.2 + */ + public function canBeMentioned() { + if ($this->isUnmentionableGroup()) { + return false; + } + + return !!$this->allowMention; + } + + /** + * @return UserGroup[] + * @since 5.2 + */ + public static function getMentionableGroups() { + self::getCache(); + + $groups = []; + /** @var UserGroup $group */ + foreach (self::$cache['groups'] as $group) { + if ($group->canBeMentioned()) { + $groups[] = $group; + } + } + + return $groups; + } } diff --git a/wcfsetup/install/files/lib/system/bbcode/GroupBBCode.class.php b/wcfsetup/install/files/lib/system/bbcode/GroupBBCode.class.php new file mode 100644 index 0000000000..cbb82e13a9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/bbcode/GroupBBCode.class.php @@ -0,0 +1,31 @@ + + * @package WoltLabSuite\Core\System\Bbcode + * @since 5.0 + */ +class GroupBBCode extends AbstractBBCode { + /** + * @inheritDoc + */ + public function getParsedTag(array $openingTag, $content, array $closingTag, BBCodeParser $parser) { + $groupID = (!empty($openingTag['attributes'][0])) ? intval($openingTag['attributes'][0]) : 0; + $group = UserGroup::getGroupByID($groupID); + if ($group === null || !$group->canBeMentioned()) { + return "[group]{$content}[/group]"; + } + + return WCF::getTPL()->fetch('groupBBCodeTag', 'wcf', [ + 'group' => $group, + 'groupName' => $content, + ], true); + } +} diff --git a/wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeTextParser.class.php b/wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeTextParser.class.php index 6259bd2e94..e815306a99 100644 --- a/wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeTextParser.class.php +++ b/wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeTextParser.class.php @@ -3,6 +3,7 @@ namespace wcf\system\html\input\node; use wcf\data\bbcode\media\provider\BBCodeMediaProvider; use wcf\data\smiley\Smiley; use wcf\data\smiley\SmileyCache; +use wcf\data\user\group\UserGroup; use wcf\system\bbcode\BBCodeHandler; use wcf\system\bbcode\HtmlBBCodeParser; use wcf\system\database\util\PreparedStatementConditionBuilder; @@ -174,9 +175,11 @@ class HtmlInputNodeTextParser { $this->detectMention($node, $value, $usernames); } - $users = []; + $groups = $users = []; if (!empty($usernames)) { $users = $this->lookupUsernames($usernames); + $groups = $this->lookupGroups($usernames); + } $allowEmail = BBCodeHandler::getInstance()->isAvailableBBCode('email'); @@ -188,8 +191,8 @@ class HtmlInputNodeTextParser { $node = $nodes[$i]; $oldValue = $value = $node->textContent; - if (!empty($users)) { - $value = $this->parseMention($node, $value, $users); + if (!empty($users) || !empty($groups)) { + $value = $this->parseMention($node, $value, $users, $groups); } if ($allowURL || $allowMedia) { @@ -305,36 +308,72 @@ class HtmlInputNodeTextParser { return $users; } + /** + * @param string[] $usernames + * @return UserGroup[] + * @since 5.2 + */ + protected function lookupGroups(array $usernames) { + /** @var UserGroup[] $availableUserGroups */ + $availableUserGroups = []; + foreach (UserGroup::getMentionableGroups() as $group) { + $availableUserGroups[] = $group; + } + + if (empty($availableUserGroups)) { + return []; + } + + // Sorting the usernames by length allows for more precise matches. + usort($usernames, function ($usernameA, $usernameB) { + return mb_strlen($usernameA) - mb_strlen($usernameB); + }); + + $groups = []; + foreach ($usernames as $username) { + foreach ($availableUserGroups as $group) { + if (strcasecmp($group->getName(), $username) === 0) { + $groups[$group->groupID] = $group->getName(); + + continue 2; + } + } + } + + return$groups; + } + /** * Parses text nodes and searches for mentions. * * @param \DOMText $text text node * @param string $value node value * @param string[] $users list of usernames by user id + * @param string[] $groups list of group names by group id * @return string modified node value with replacement placeholders */ - protected function parseMention(\DOMText $text, $value, array $users) { + protected function parseMention(\DOMText $text, $value, array $users, array $groups) { if (mb_strpos($value, '@') === false) { return $value; } - foreach ($users as $userID => $username) { + $replaceMatch = function($objectID, $objectTitle, $bbcodeTagName) use ($text, &$value) { $offset = 0; do { - $needle = '@' . $username; + $needle = '@' . $objectTitle; $pos = mb_stripos($value, $needle, $offset); // username not found, maybe it is quoted if ($pos === false) { - $needle = "@'" . str_ireplace("'", "''", $username) . "'"; + $needle = "@'" . str_ireplace("'", "''", $objectTitle) . "'"; $pos = mb_stripos($value, $needle, $offset); } if ($pos !== false) { $element = $text->ownerDocument->createElement('woltlab-metacode'); - $element->setAttribute('data-name', 'user'); - $element->setAttribute('data-attributes', base64_encode(JSON::encode([$userID]))); - $element->appendChild($text->ownerDocument->createTextNode($username)); + $element->setAttribute('data-name', $bbcodeTagName); + $element->setAttribute('data-attributes', base64_encode(JSON::encode([$objectID]))); + $element->appendChild($text->ownerDocument->createTextNode($objectTitle)); $marker = $this->addReplacement($text, $element); @@ -347,6 +386,14 @@ class HtmlInputNodeTextParser { } } while ($pos); + }; + + foreach ($users as $userID => $username) { + $replaceMatch($userID, $username, 'user'); + } + + foreach ($groups as $groupID => $name) { + $replaceMatch($groupID, $name, 'group'); } return $value; diff --git a/wcfsetup/install/files/lib/util/MessageUtil.class.php b/wcfsetup/install/files/lib/util/MessageUtil.class.php index 589c066993..f6357a0fcf 100644 --- a/wcfsetup/install/files/lib/util/MessageUtil.class.php +++ b/wcfsetup/install/files/lib/util/MessageUtil.class.php @@ -1,8 +1,11 @@ getHtmlInputNodeProcessor()->getDocument()->getElementsByTagName('woltlab-metacode'); /** @var \DOMElement $element */ foreach ($elements as $element) { - if ($element->getAttribute('data-name') != 'user') { + $type = $element->getAttribute('data-name'); + if ($type !== 'user' && $type !== 'group') { continue; } @@ -58,7 +63,38 @@ class MessageUtil { continue; } - $usernames[] = $element->textContent; + if ($type === 'user') { + $usernames[] = $element->textContent; + } + else if ($type === 'group') { + $attributes = $htmlInputProcessor->getHtmlInputNodeProcessor()->parseAttributes( + $element->getAttribute('data-attributes') + ); + + if (!empty($attributes[0])) { + $group = UserGroup::getGroupByID($attributes[0]); + if ($group !== null && $group->canBeMentioned() && !isset($groups[$group->groupID])) { + $groups[$group->groupID] = $group; + } + } + } + } + + if (!empty($groups)) { + $conditions = new PreparedStatementConditionBuilder(); + $conditions->add('user_to_group.groupID IN (?)', [array_keys($groups)]); + if (!empty($usernames)) $conditions->add('user_table.username NOT IN (?)', [$usernames]); + + $sql = "SELECT user_table.username + FROM wcf".WCF_N."_user_to_group user_to_group + LEFT JOIN wcf".WCF_N."_user user_table + ON (user_table.userID = user_to_group.userID) + ".$conditions; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($conditions->getParameters()); + while ($username = $statement->fetchSingleColumn()) { + $usernames[] = $username; + } } return $usernames; diff --git a/wcfsetup/install/files/style/bbcode/groupMention.scss b/wcfsetup/install/files/style/bbcode/groupMention.scss new file mode 100644 index 0000000000..caf946cd6b --- /dev/null +++ b/wcfsetup/install/files/style/bbcode/groupMention.scss @@ -0,0 +1,17 @@ +.groupMention { + background-color: $wcfSidebarBackground; + border-radius: 2px; + color: $wcfSidebarLink; + padding: 1px 5px; + + &::before { + content: '@'; + /* Avoids breaks between the '@' and the group name, but still allows + wrapping inside the name itself */ + display: inline-block; + } + + &:hover { + color: $wcfSidebarLinkActive; + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 6c74ece735..a609611eac 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -844,6 +844,7 @@ + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index fc1fc97b9e..657bd02fae 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -821,6 +821,7 @@ + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index e482e00aea..7cb83622b5 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1558,7 +1558,8 @@ CREATE TABLE wcf1_user_group ( groupType TINYINT(1) NOT NULL DEFAULT 4, priority MEDIUMINT(8) NOT NULL DEFAULT 0, userOnlineMarking VARCHAR(255) NOT NULL DEFAULT '%s', - showOnTeamPage TINYINT(1) NOT NULL DEFAULT 0 + showOnTeamPage TINYINT(1) NOT NULL DEFAULT 0, + allowMention TINYINT(1) NOT NULL DEFAULT 0 ); DROP TABLE IF EXISTS wcf1_user_group_assignment; -- 2.20.1