Path: blob/master/src/applications/people/markup/PhabricatorMentionRemarkupRule.php
12256 views
<?php12final class PhabricatorMentionRemarkupRule extends PhutilRemarkupRule {34const KEY_RULE_MENTION = 'rule.mention';5const KEY_RULE_MENTION_ORIGINAL = 'rule.mention.original';67const KEY_MENTIONED = 'phabricator.mentioned-user-phids';8910// NOTE: The negative lookbehind prevents matches like "mail@lists", while11// allowing constructs like "@tomo/@mroch". Since we now allow periods in12// usernames, we can't reasonably distinguish that "@company.com" isn't a13// username, so we'll incorrectly pick it up, but there's little to be done14// about that. We forbid terminal periods so that we can correctly capture15// "@joe" instead of "@joe." in "Hey, @joe.".16//17// We disallow "@@joe" because it creates a false positive in the common18// construction "l@@k", made popular by eBay.19const REGEX = '/(?<!\w|@)@([a-zA-Z0-9._-]*[a-zA-Z0-9_-])/';2021public function apply($text) {22return preg_replace_callback(23self::REGEX,24array($this, 'markupMention'),25$text);26}2728protected function markupMention(array $matches) {29$engine = $this->getEngine();3031if ($engine->isTextMode()) {32return $engine->storeText($matches[0]);33}3435$token = $engine->storeText('');3637// Store the original text exactly so we can preserve casing if it doesn't38// resolve into a username.39$original_key = self::KEY_RULE_MENTION_ORIGINAL;40$original = $engine->getTextMetadata($original_key, array());41$original[$token] = $matches[1];42$engine->setTextMetadata($original_key, $original);4344$metadata_key = self::KEY_RULE_MENTION;45$metadata = $engine->getTextMetadata($metadata_key, array());46$username = strtolower($matches[1]);47if (empty($metadata[$username])) {48$metadata[$username] = array();49}50$metadata[$username][] = $token;51$engine->setTextMetadata($metadata_key, $metadata);5253return $token;54}5556public function didMarkupText() {57$engine = $this->getEngine();5859$metadata_key = self::KEY_RULE_MENTION;60$metadata = $engine->getTextMetadata($metadata_key, array());61if (empty($metadata)) {62// No mentions, or we already processed them.63return;64}6566$original_key = self::KEY_RULE_MENTION_ORIGINAL;67$original = $engine->getTextMetadata($original_key, array());6869$usernames = array_keys($metadata);7071$users = id(new PhabricatorPeopleQuery())72->setViewer($this->getEngine()->getConfig('viewer'))73->withUsernames($usernames)74->needAvailability(true)75->execute();7677$actual_users = array();7879$mentioned_key = self::KEY_MENTIONED;80$mentioned = $engine->getTextMetadata($mentioned_key, array());81foreach ($users as $row) {82$actual_users[strtolower($row->getUserName())] = $row;83$mentioned[$row->getPHID()] = $row->getPHID();84}8586$engine->setTextMetadata($mentioned_key, $mentioned);87$context_object = $engine->getConfig('contextObject');8889$policy_object = null;90if ($context_object) {91if ($context_object instanceof PhabricatorPolicyInterface) {92$policy_object = $context_object;93}94}9596if ($policy_object) {97$policy_set = new PhabricatorPolicyFilterSet();98foreach ($actual_users as $user) {99$policy_set->addCapability(100$user,101$policy_object,102PhabricatorPolicyCapability::CAN_VIEW);103}104}105106foreach ($metadata as $username => $tokens) {107$exists = isset($actual_users[$username]);108$user_can_not_view = false;109110if ($exists) {111$user = $actual_users[$username];112113// Check if the user has view access to the object she was mentioned in114if ($policy_object) {115$user_can_not_view = !$policy_set->hasCapability(116$user,117$policy_object,118PhabricatorPolicyCapability::CAN_VIEW);119}120121$user_href = '/p/'.$user->getUserName().'/';122123if ($engine->isHTMLMailMode()) {124$user_href = PhabricatorEnv::getProductionURI($user_href);125126if ($user_can_not_view) {127$colors = '128border-color: #92969D;129color: #92969D;130background-color: #F7F7F7;';131} else {132$colors = '133border-color: #f1f7ff;134color: #19558d;135background-color: #f1f7ff;';136}137138$tag = phutil_tag(139'a',140array(141'href' => $user_href,142'style' => $colors.'143border: 1px solid transparent;144border-radius: 3px;145font-weight: bold;146padding: 0 4px;',147),148'@'.$user->getUserName());149} else {150if ($engine->getConfig('uri.full')) {151$user_href = PhabricatorEnv::getURI($user_href);152}153154$tag = id(new PHUITagView())155->setType(PHUITagView::TYPE_PERSON)156->setPHID($user->getPHID())157->setName('@'.$user->getUserName())158->setHref($user_href);159160if ($context_object) {161$tag->setContextObject($context_object);162}163164if ($user_can_not_view) {165$tag->setIcon('fa-eye-slash red');166$tag->setIsExiled(true);167}168169if ($user->getIsDisabled()) {170$tag->setDotColor(PHUITagView::COLOR_GREY);171} else if (!$user->isResponsive()) {172$tag->setDotColor(PHUITagView::COLOR_VIOLET);173} else {174if ($user->getAwayUntil()) {175$away = PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY;176if ($user->getDisplayAvailability() == $away) {177$tag->setDotColor(PHUITagView::COLOR_RED);178} else {179$tag->setDotColor(PHUITagView::COLOR_ORANGE);180}181}182}183}184185foreach ($tokens as $token) {186$engine->overwriteStoredText($token, $tag);187}188} else {189// NOTE: The structure here is different from the 'exists' branch,190// because we want to preserve the original text capitalization and it191// may differ for each token.192foreach ($tokens as $token) {193$tag = phutil_tag(194'span',195array(196'class' => 'phabricator-remarkup-mention-unknown',197),198'@'.idx($original, $token, $username));199$engine->overwriteStoredText($token, $tag);200}201}202}203204// Don't re-process these mentions.205$engine->setTextMetadata($metadata_key, array());206}207208}209210211