diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 6b953b9de4..7a152a1e2e 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -1,244 +1,241 @@ getUser(); $conpherence_id = $request->getURIData('id'); if (!$conpherence_id) { return new Aphront404Response(); } $query = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->needProfileImage(true) ->needTransactions(true) ->setTransactionLimit($this->getMainQueryLimit()); $before_transaction_id = $request->getInt('oldest_transaction_id'); $after_transaction_id = $request->getInt('newest_transaction_id'); $old_message_id = $request->getURIData('messageID'); if ($before_transaction_id && ($old_message_id || $after_transaction_id)) { throw new Aphront400Response(); } if ($old_message_id && $after_transaction_id) { throw new Aphront400Response(); } $marker_type = 'older'; if ($before_transaction_id) { $query ->setBeforeTransactionID($before_transaction_id); } if ($old_message_id) { $marker_type = 'olderandnewer'; $query ->setAfterTransactionID($old_message_id - 1); } if ($after_transaction_id) { $marker_type = 'newer'; $query ->setAfterTransactionID($after_transaction_id); } $conpherence = $query->executeOne(); if (!$conpherence) { return new Aphront404Response(); } $this->setConpherence($conpherence); $transactions = $this->getNeededTransactions( $conpherence, $old_message_id); $latest_transaction = head($transactions); $participant = $conpherence->getParticipantIfExists($user->getPHID()); if ($participant) { if (!$participant->isUpToDate($conpherence)) { $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); $participant->markUpToDate($conpherence, $latest_transaction); $user->clearCacheData(PhabricatorUserMessageCountCacheType::KEY_COUNT); unset($write_guard); } } $data = ConpherenceTransactionRenderer::renderTransactions( $user, $conpherence, $marker_type); $messages = ConpherenceTransactionRenderer::renderMessagePaneContent( $data['transactions'], $data['oldest_transaction_id'], $data['newest_transaction_id']); if ($before_transaction_id || $after_transaction_id) { $header = null; $form = null; $content = array('transactions' => $messages); } else { $policy_objects = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($conpherence) ->execute(); $header = $this->buildHeaderPaneContent($conpherence, $policy_objects); $search = $this->buildSearchForm(); $form = $this->renderFormContent(); $content = array( 'header' => $header, 'search' => $search, 'transactions' => $messages, 'form' => $form, ); } $d_data = $conpherence->getDisplayData($user); $content['title'] = $title = $d_data['title']; if ($request->isAjax()) { $dropdown_query = id(new AphlictDropdownDataQuery()) ->setViewer($user); $dropdown_query->execute(); $content['threadID'] = $conpherence->getID(); $content['threadPHID'] = $conpherence->getPHID(); $content['latestTransactionID'] = $data['latest_transaction_id']; $content['canEdit'] = PhabricatorPolicyFilter::hasCapability( $user, $conpherence, PhabricatorPolicyCapability::CAN_EDIT); $content['aphlictDropdownData'] = array( $dropdown_query->getNotificationData(), $dropdown_query->getConpherenceData(), ); return id(new AphrontAjaxResponse())->setContent($content); } $layout = id(new ConpherenceLayoutView()) ->setUser($user) ->setBaseURI($this->getApplicationURI()) ->setThread($conpherence) ->setHeader($header) ->setSearch($search) ->setMessages($messages) ->setReplyForm($form) ->setLatestTransactionID($data['latest_transaction_id']) ->setRole('thread'); $participating = $conpherence->getParticipantIfExists($user->getPHID()); if (!$user->isLoggedIn()) { $layout->addClass('conpherence-no-pontificate'); } return $this->newPage() ->setTitle($title) ->setPageObjectPHIDs(array($conpherence->getPHID())) ->appendChild($layout); } private function renderFormContent() { $conpherence = $this->getConpherence(); $user = $this->getRequest()->getUser(); $participating = $conpherence->getParticipantIfExists($user->getPHID()); - if (!$participating && $user->isLoggedIn()) { - return null; - } $draft = PhabricatorDraft::newFromUserAndKey( $user, $conpherence->getPHID()); $update_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); if ($user->isLoggedIn()) { $this->initBehavior('conpherence-pontificate'); if ($participating) { $action = ConpherenceUpdateActions::MESSAGE; $status = new PhabricatorNotificationStatusView(); } else { $action = ConpherenceUpdateActions::JOIN_ROOM; $status = pht('Sending a message will also join the room.'); } $form = id(new AphrontFormView()) ->setUser($user) ->setAction($update_uri) ->addSigil('conpherence-pontificate') ->setWorkflow(true) ->addHiddenInput('action', $action) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($user) ->setName('text') ->setSendOnEnter(true) ->setValue($draft->getDraft())); $status_view = phutil_tag( 'div', array( 'class' => 'conpherence-room-status', 'id' => 'conpherence-room-status', ), $status); $view = phutil_tag_div( 'pontificate-container', array($form, $status_view)); return $view; } else { // user not logged in so give them a login button. $login_href = id(new PhutilURI('/auth/start/')) ->setQueryParam('next', '/'.$conpherence->getMonogram()); return id(new PHUIFormLayoutView()) ->addClass('login-to-participate') ->appendInstructions(pht('Log in to join this room and participate.')) ->appendChild( id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Login to Participate')) ->setHref((string)$login_href)); } } private function getNeededTransactions( ConpherenceThread $conpherence, $message_id) { if ($message_id) { $newer_transactions = $conpherence->getTransactions(); $query = id(new ConpherenceTransactionQuery()) ->setViewer($this->getRequest()->getUser()) ->withObjectPHIDs(array($conpherence->getPHID())) ->setAfterID($message_id) ->needHandles(true) ->setLimit(self::OLDER_FETCH_LIMIT); $older_transactions = $query->execute(); $handles = array(); foreach ($older_transactions as $transaction) { $handles += $transaction->getHandles(); } $conpherence->attachHandles($conpherence->getHandles() + $handles); $transactions = array_merge($newer_transactions, $older_transactions); $conpherence->attachTransactions($transactions); } else { $transactions = $conpherence->getTransactions(); } return $transactions; } private function getMainQueryLimit() { $request = $this->getRequest(); $base_limit = ConpherenceThreadQuery::TRANSACTION_LIMIT; if ($request->getURIData('messageID')) { $base_limit = $base_limit - self::OLDER_FETCH_LIMIT; } return $base_limit; } } diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php index e358700348..c53aab7655 100644 --- a/src/applications/conpherence/editor/ConpherenceEditor.php +++ b/src/applications/conpherence/editor/ConpherenceEditor.php @@ -1,517 +1,527 @@ getPHID(); $participant_phids = array_unique($participant_phids); } if (empty($message)) { $errors[] = self::ERROR_EMPTY_MESSAGE; } if (!$errors) { $xactions = array(); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransaction::TYPE_PARTICIPANTS) ->setNewValue(array('+' => $participant_phids)); if ($title) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceThreadTitleTransaction::TRANSACTIONTYPE) ->setNewValue($title); } if (strlen($topic)) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceThreadTopicTransaction::TRANSACTIONTYPE) ->setNewValue($topic); } $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ConpherenceTransactionComment()) ->setContent($message) ->setConpherencePHID($conpherence->getPHID())); id(new ConpherenceEditor()) ->setActor($creator) ->setContentSource($source) ->setContinueOnNoEffect(true) ->applyTransactions($conpherence, $xactions); } return array($errors, $conpherence); } public function generateTransactionsFromText( PhabricatorUser $viewer, ConpherenceThread $conpherence, $text) { $xactions = array(); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ConpherenceTransactionComment()) ->setContent($text) ->setConpherencePHID($conpherence->getPHID())); return $xactions; } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = ConpherenceTransaction::TYPE_PARTICIPANTS; $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } public function getCreateObjectTitle($author, $object) { return pht('%s created this room.', $author); } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ConpherenceTransaction::TYPE_PARTICIPANTS: if ($this->getIsNewObject()) { return array(); } return $object->getParticipantPHIDs(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ConpherenceTransaction::TYPE_PARTICIPANTS: return $this->getPHIDTransactionNewValue($xaction); } } /** * We really only need a read lock if we have a comment. In that case, we * must update the messagesCount field on the conpherence and * seenMessagesCount(s) for the participant(s). */ protected function shouldReadLock( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $lock = false; switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $lock = true; break; } return $lock; } /** * We need to apply initial effects IFF the conpherence is new. We must * save the conpherence first thing to make sure we have an id and a phid, as * well as create the initial set of participants so that we pass policy * checks. */ protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { return $this->getIsNewObject(); } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { $object->save(); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case ConpherenceTransaction::TYPE_PARTICIPANTS: // Since this is a new ConpherenceThread, we have to create the // participation data asap to pass policy checks. For existing // ConpherenceThreads, the existing participation is correct // at this stage. Note that later in applyCustomExternalTransaction // this participation data will be updated, particularly the // behindTransactionPHID which is just a generated dummy for now. $participants = array(); $phids = $this->getPHIDTransactionNewValue($xaction, array()); foreach ($phids as $phid) { if ($phid == $this->getActor()->getPHID()) { $status = ConpherenceParticipationStatus::UP_TO_DATE; $message_count = 1; } else { $status = ConpherenceParticipationStatus::BEHIND; $message_count = 0; } $participants[$phid] = id(new ConpherenceParticipant()) ->setConpherencePHID($object->getPHID()) ->setParticipantPHID($phid) ->setParticipationStatus($status) ->setDateTouched(time()) ->setBehindTransactionPHID($xaction->generatePHID()) ->setSeenMessageCount($message_count) ->save(); $object->attachParticipants($participants); } break; } } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ConpherenceTransaction::TYPE_PARTICIPANTS: if (!$this->getIsNewObject()) {} break; } } protected function applyBuiltinInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $object->setMessageCount((int)$object->getMessageCount() + 1); break; } return parent::applyBuiltinInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ConpherenceTransaction::TYPE_PARTICIPANTS: if ($this->getIsNewObject()) { continue; } $participants = $object->getParticipants(); $old_map = array_fuse($xaction->getOldValue()); $new_map = array_fuse($xaction->getNewValue()); $remove = array_keys(array_diff_key($old_map, $new_map)); foreach ($remove as $phid) { $remove_participant = $participants[$phid]; $remove_participant->delete(); unset($participants[$phid]); } $add = array_keys(array_diff_key($new_map, $old_map)); foreach ($add as $phid) { if ($phid == $this->getActor()->getPHID()) { $status = ConpherenceParticipationStatus::UP_TO_DATE; $message_count = $object->getMessageCount(); } else { $status = ConpherenceParticipationStatus::BEHIND; $message_count = 0; } $participants[$phid] = id(new ConpherenceParticipant()) ->setConpherencePHID($object->getPHID()) ->setParticipantPHID($phid) ->setParticipationStatus($status) ->setDateTouched(time()) ->setBehindTransactionPHID($xaction->getPHID()) ->setSeenMessageCount($message_count) ->save(); } $object->attachParticipants($participants); break; } } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { if (!$xactions) { return $xactions; } $message_count = 0; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $message_count++; // update everyone's participation status on a message -only- $xaction_phid = $xaction->getPHID(); $behind = ConpherenceParticipationStatus::BEHIND; $up_to_date = ConpherenceParticipationStatus::UP_TO_DATE; $participants = $object->getParticipants(); $user = $this->getActor(); $time = time(); foreach ($participants as $phid => $participant) { if ($phid != $user->getPHID()) { if ($participant->getParticipationStatus() != $behind) { $participant->setBehindTransactionPHID($xaction_phid); $participant->setSeenMessageCount( $object->getMessageCount() - $message_count); } $participant->setParticipationStatus($behind); $participant->setDateTouched($time); } else { $participant->setSeenMessageCount($object->getMessageCount()); $participant->setBehindTransactionPHID($xaction_phid); $participant->setParticipationStatus($up_to_date); $participant->setDateTouched($time); } $participant->save(); } PhabricatorUserCache::clearCaches( PhabricatorUserMessageCountCacheType::KEY_COUNT, array_keys($participants)); break; } } if ($xactions) { $data = array( 'type' => 'message', 'threadPHID' => $object->getPHID(), 'messageID' => last($xactions)->getID(), 'subscribers' => array($object->getPHID()), ); PhabricatorNotificationClient::tryToPostMessage($data); } return $xactions; } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { parent::requireCapabilities($object, $xaction); switch ($xaction->getTransactionType()) { case ConpherenceTransaction::TYPE_PARTICIPANTS: $old_map = array_fuse($xaction->getOldValue()); $new_map = array_fuse($xaction->getNewValue()); $add = array_keys(array_diff_key($new_map, $old_map)); $rem = array_keys(array_diff_key($old_map, $new_map)); - $actor_phid = $this->requireActor()->getPHID(); - - // You need CAN_EDIT to change participants other than yourself. - PhabricatorPolicyFilter::requireCapability( - $this->requireActor(), - $object, - PhabricatorPolicyCapability::CAN_EDIT); + $actor_phid = $this->getActingAsPHID(); + + $is_join = (($add === array($actor_phid)) && !$rem); + $is_leave = (($rem === array($actor_phid)) && !$add); + + if ($is_join) { + // Anyone can join a thread they can see. + } else if ($is_leave) { + // Anyone can leave a thread. + } else { + // You need CAN_EDIT to add or remove participants. For additional + // discussion, see D17696 and T4411. + PhabricatorPolicyFilter::requireCapability( + $this->requireActor(), + $object, + PhabricatorPolicyCapability::CAN_EDIT); + } break; case ConpherenceThreadTitleTransaction::TRANSACTIONTYPE: case ConpherenceThreadTopicTransaction::TRANSACTIONTYPE: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); break; } } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case ConpherenceTransaction::TYPE_PARTICIPANTS: return $this->mergePHIDOrEdgeTransactions($u, $v); } return parent::mergeTransactions($u, $v); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new ConpherenceReplyHandler()) ->setActor($this->getActor()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); if (!$title) { $title = pht( '%s sent you a message.', $this->getActor()->getUserName()); } $phid = $object->getPHID(); return id(new PhabricatorMetaMTAMail()) ->setSubject("Z{$id}: {$title}") ->addHeader('Thread-Topic', "Z{$id}: {$phid}"); } protected function getMailTo(PhabricatorLiskDAO $object) { $to_phids = array(); $participants = $object->getParticipants(); if (!$participants) { return $to_phids; } $participant_phids = mpull($participants, 'getParticipantPHID'); $users = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($participant_phids) ->needUserSettings(true) ->execute(); $users = mpull($users, null, 'getPHID'); $notification_key = PhabricatorConpherenceNotificationsSetting::SETTINGKEY; $notification_email = PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_EMAIL; foreach ($participants as $phid => $participant) { $user = idx($users, $phid); if ($user) { $default = $user->getUserSetting($notification_key); } else { $default = $notification_email; } $settings = $participant->getSettings(); $notifications = idx($settings, 'notifications', $default); if ($notifications == $notification_email) { $to_phids[] = $phid; } } return $to_phids; } protected function getMailCC(PhabricatorLiskDAO $object) { return array(); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $body->addLinkSection( pht('CONPHERENCE DETAIL'), PhabricatorEnv::getProductionURI('/'.$object->getMonogram())); return $body; } protected function addEmailPreferenceSectionToMailBody( PhabricatorMetaMTAMailBody $body, PhabricatorLiskDAO $object, array $xactions) { $href = PhabricatorEnv::getProductionURI( '/'.$object->getMonogram().'?settings'); $label = pht('EMAIL PREFERENCES FOR THIS ROOM'); $body->addLinkSection($label, $href); } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.conpherence.subject-prefix'); } protected function supportsSearch() { return true; } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case ConpherenceTransaction::TYPE_PARTICIPANTS: foreach ($xactions as $xaction) { $new_phids = $this->getPHIDTransactionNewValue($xaction, array()); $old_phids = nonempty($object->getParticipantPHIDs(), array()); $phids = array_diff($new_phids, $old_phids); if (!$phids) { continue; } $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($phids) ->execute(); $users = mpull($users, null, 'getPHID'); foreach ($phids as $phid) { if (isset($users[$phid])) { continue; } $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('New room participant "%s" is not a valid user.', $phid), $xaction); } } break; } return $errors; } }