</attribute>
</attributes>
</bbcode>
+ <bbcode name="group">
+ <classname>wcf\system\bbcode\GroupBBCode</classname>
+ <attributes>
+ <attribute name="0">
+ <validationpattern><![CDATA[^\d+$]]></validationpattern>
+ <required>1</required>
+ </attribute>
+ </attributes>
+ </bbcode>
</import>
</data>
--- /dev/null
+{if $group}
+ <span class="groupMention">{$group->getName()}</span>
+{else}
+ @{$groupName}
+{/if}
</dd>
</dl>
{/if}
+
+ {if $action === 'add' || !$isUnmentionableGroup}
+ <dl>
+ <dt></dt>
+ <dd>
+ <label><input type="checkbox" id="allowMention" name="allowMention" value="1"{if $allowMention} checked{/if}> {lang}wcf.acp.group.allowMention{/lang}</label>
+ </dd>
+ </dl>
+ {/if}
{event name='dataFields'}
</div>
interfaceName: 'wcf\\data\\ISearchAction',
parameters: {
data: {
- includeUserGroups: false
+ includeUserGroups: true,
+ scope: 'mention'
}
}
},
*/
protected $showOnTeamPage = 0;
+ /**
+ * @var int
+ */
+ protected $allowMention = 0;
+
/**
* @inheritDoc
*/
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']);
}
/**
'groupDescription' => $this->groupDescription,
'priority' => $this->priority,
'userOnlineMarking' => $this->userOnlineMarking,
- 'showOnTeamPage' => $this->showOnTeamPage
+ 'showOnTeamPage' => $this->showOnTeamPage,
+ 'allowMention' => $this->allowMention ? 1 : 0,
]),
'options' => $optionValues
];
// reset values
$this->groupName = '';
- $this->priority = 0;
+ $this->allowMention = $this->priority = $this->showOnTeamPage = 0;
I18nHandler::getInstance()->reset();
}
'userOnlineMarking' => $this->userOnlineMarking,
'showOnTeamPage' => $this->showOnTeamPage,
'groupIsGuest' => false,
- 'isBlankForm' => empty($_POST)
+ 'isBlankForm' => empty($_POST),
+ 'allowMention' => $this->allowMention,
]);
}
*/
public $group;
+ /**
+ * @var bool
+ */
+ public $isUnmentionableGroup = false;
+
/**
* @inheritDoc
*/
$this->optionHandler->setUserGroup($group);
/** @noinspection PhpUndefinedMethodInspection */
$this->optionHandler->init();
+
+ $this->isUnmentionableGroup = $this->group->isUnmentionableGroup();
}
/**
// user group
}
+ /**
+ * @inheritDoc
+ */
+ public function validate() {
+ parent::validate();
+
+ if ($this->allowMention && $this->isUnmentionableGroup) {
+ $this->allowMention = false;
+ }
+ }
+
/**
* @inheritDoc
*/
$this->priority = $this->group->priority;
$this->userOnlineMarking = $this->group->userOnlineMarking;
$this->showOnTeamPage = $this->group->showOnTeamPage;
+ $this->allowMention = $this->group->allowMention;
}
parent::readData();
'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
'groupDescription' => $this->groupDescription,
'priority' => $this->priority,
'userOnlineMarking' => $this->userOnlineMarking,
- 'showOnTeamPage' => $this->showOnTeamPage
+ 'showOnTeamPage' => $this->showOnTeamPage,
+ 'allowMention' => $this->allowMention,
]),
'options' => $optionValues
]);
$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');
+ }
+ }
}
/**
continue;
}
+ if ($this->parameters['data']['scope'] === 'mention' && !$group->canBeMentioned()) {
+ continue;
+ }
+
$groupName = $group->getName();
if (!in_array($groupName, $excludedSearchValues)) {
$pos = mb_strripos($groupName, $searchString);
* @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 {
/**
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;
+ }
}
--- /dev/null
+<?php
+namespace wcf\system\bbcode;
+use wcf\data\user\group\UserGroup;
+use wcf\system\WCF;
+
+/**
+ * Parses the [user] bbcode tag.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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);
+ }
+}
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;
$this->detectMention($node, $value, $usernames);
}
- $users = [];
+ $groups = $users = [];
if (!empty($usernames)) {
$users = $this->lookupUsernames($usernames);
+ $groups = $this->lookupGroups($usernames);
+
}
$allowEmail = BBCodeHandler::getInstance()->isAvailableBBCode('email');
$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) {
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);
}
}
while ($pos);
+ };
+
+ foreach ($users as $userID => $username) {
+ $replaceMatch($userID, $username, 'user');
+ }
+
+ foreach ($groups as $groupID => $name) {
+ $replaceMatch($groupID, $name, 'group');
}
return $value;
<?php
namespace wcf\util;
+use wcf\data\user\group\UserGroup;
use wcf\system\application\ApplicationHandler;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
use wcf\system\html\input\HtmlInputProcessor;
use wcf\system\Regex;
+use wcf\system\WCF;
/**
* Contains message-related functions.
*/
public static function getMentionedUsers(HtmlInputProcessor $htmlInputProcessor) {
$usernames = [];
+ $groups = [];
$elements = $htmlInputProcessor->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;
}
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;
--- /dev/null
+.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;
+ }
+}
<item name="wcf.acp.group.option.user.contactForm.attachment.allowedExtensions.description"><![CDATA[Eine Dateiendung pro Zeile]]></item>
<item name="wcf.acp.group.option.user.contactForm.attachment.maxCount"><![CDATA[Maximale Dateianhänge pro Nachricht]]></item>
<item name="wcf.acp.group.option.user.profile.canEditUserProfile"><![CDATA[Kann eigenes Profil bearbeiten]]></item>
+ <item name="wcf.acp.group.allowMention"><![CDATA[Benutzergruppe kann erwähnt werden]]></item>
</category>
<category name="wcf.acp.index">
<item name="wcf.acp.index.credits"><![CDATA[Über WoltLab Suite™]]></item>
<item name="wcf.acp.group.option.user.contactForm.attachment.allowedExtensions.description"><![CDATA[Enter one extension per line.]]></item>
<item name="wcf.acp.group.option.user.contactForm.attachment.maxCount"><![CDATA[Maximum Attachments per Message]]></item>
<item name="wcf.acp.group.option.user.profile.canEditUserProfile"><![CDATA[Can edit their profile]]></item>
+ <item name="wcf.acp.group.allowMention"><![CDATA[User group can be mentioned]]></item>
</category>
<category name="wcf.acp.index">
<item name="wcf.acp.index.credits"><![CDATA[About WoltLab Suite™]]></item>
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;