PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/src/applications/feed/PhabricatorFeedStoryPublisher.php

http://github.com/facebook/phabricator
PHP | 341 lines | 251 code | 55 blank | 35 comment | 24 complexity | dafbf26c93fb1cd39d4623d9ce171b80 MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. final class PhabricatorFeedStoryPublisher extends Phobject {
  3. private $relatedPHIDs;
  4. private $storyType;
  5. private $storyData;
  6. private $storyTime;
  7. private $storyAuthorPHID;
  8. private $primaryObjectPHID;
  9. private $subscribedPHIDs = array();
  10. private $mailRecipientPHIDs = array();
  11. private $notifyAuthor;
  12. private $mailTags = array();
  13. private $unexpandablePHIDs = array();
  14. public function setMailTags(array $mail_tags) {
  15. $this->mailTags = $mail_tags;
  16. return $this;
  17. }
  18. public function getMailTags() {
  19. return $this->mailTags;
  20. }
  21. public function setNotifyAuthor($notify_author) {
  22. $this->notifyAuthor = $notify_author;
  23. return $this;
  24. }
  25. public function getNotifyAuthor() {
  26. return $this->notifyAuthor;
  27. }
  28. public function setRelatedPHIDs(array $phids) {
  29. $this->relatedPHIDs = $phids;
  30. return $this;
  31. }
  32. public function setSubscribedPHIDs(array $phids) {
  33. $this->subscribedPHIDs = $phids;
  34. return $this;
  35. }
  36. public function setPrimaryObjectPHID($phid) {
  37. $this->primaryObjectPHID = $phid;
  38. return $this;
  39. }
  40. public function setUnexpandablePHIDs(array $unexpandable_phids) {
  41. $this->unexpandablePHIDs = $unexpandable_phids;
  42. return $this;
  43. }
  44. public function getUnexpandablePHIDs() {
  45. return $this->unexpandablePHIDs;
  46. }
  47. public function setStoryType($story_type) {
  48. $this->storyType = $story_type;
  49. return $this;
  50. }
  51. public function setStoryData(array $data) {
  52. $this->storyData = $data;
  53. return $this;
  54. }
  55. public function setStoryTime($time) {
  56. $this->storyTime = $time;
  57. return $this;
  58. }
  59. public function setStoryAuthorPHID($phid) {
  60. $this->storyAuthorPHID = $phid;
  61. return $this;
  62. }
  63. public function setMailRecipientPHIDs(array $phids) {
  64. $this->mailRecipientPHIDs = $phids;
  65. return $this;
  66. }
  67. public function publish() {
  68. $class = $this->storyType;
  69. if (!$class) {
  70. throw new Exception(
  71. pht(
  72. 'Call %s before publishing!',
  73. 'setStoryType()'));
  74. }
  75. if (!class_exists($class)) {
  76. throw new Exception(
  77. pht(
  78. "Story type must be a valid class name and must subclass %s. ".
  79. "'%s' is not a loadable class.",
  80. 'PhabricatorFeedStory',
  81. $class));
  82. }
  83. if (!is_subclass_of($class, 'PhabricatorFeedStory')) {
  84. throw new Exception(
  85. pht(
  86. "Story type must be a valid class name and must subclass %s. ".
  87. "'%s' is not a subclass of %s.",
  88. 'PhabricatorFeedStory',
  89. $class,
  90. 'PhabricatorFeedStory'));
  91. }
  92. $chrono_key = $this->generateChronologicalKey();
  93. $story = new PhabricatorFeedStoryData();
  94. $story->setStoryType($this->storyType);
  95. $story->setStoryData($this->storyData);
  96. $story->setAuthorPHID((string)$this->storyAuthorPHID);
  97. $story->setChronologicalKey($chrono_key);
  98. $story->save();
  99. if ($this->relatedPHIDs) {
  100. $ref = new PhabricatorFeedStoryReference();
  101. $sql = array();
  102. $conn = $ref->establishConnection('w');
  103. foreach (array_unique($this->relatedPHIDs) as $phid) {
  104. $sql[] = qsprintf(
  105. $conn,
  106. '(%s, %s)',
  107. $phid,
  108. $chrono_key);
  109. }
  110. queryfx(
  111. $conn,
  112. 'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %LQ',
  113. $ref->getTableName(),
  114. $sql);
  115. }
  116. $subscribed_phids = $this->subscribedPHIDs;
  117. if ($subscribed_phids) {
  118. $subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids);
  119. $this->insertNotifications($chrono_key, $subscribed_phids);
  120. $this->sendNotification($chrono_key, $subscribed_phids);
  121. }
  122. PhabricatorWorker::scheduleTask(
  123. 'FeedPublisherWorker',
  124. array(
  125. 'key' => $chrono_key,
  126. ));
  127. return $story;
  128. }
  129. private function insertNotifications($chrono_key, array $subscribed_phids) {
  130. if (!$this->primaryObjectPHID) {
  131. throw new Exception(
  132. pht(
  133. 'You must call %s if you %s!',
  134. 'setPrimaryObjectPHID()',
  135. 'setSubscribedPHIDs()'));
  136. }
  137. $notif = new PhabricatorFeedStoryNotification();
  138. $sql = array();
  139. $conn = $notif->establishConnection('w');
  140. $will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true);
  141. $user_phids = array_unique($subscribed_phids);
  142. foreach ($user_phids as $user_phid) {
  143. if (isset($will_receive_mail[$user_phid])) {
  144. $mark_read = 1;
  145. } else {
  146. $mark_read = 0;
  147. }
  148. $sql[] = qsprintf(
  149. $conn,
  150. '(%s, %s, %s, %d)',
  151. $this->primaryObjectPHID,
  152. $user_phid,
  153. $chrono_key,
  154. $mark_read);
  155. }
  156. if ($sql) {
  157. queryfx(
  158. $conn,
  159. 'INSERT INTO %T '.
  160. '(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '.
  161. 'VALUES %LQ',
  162. $notif->getTableName(),
  163. $sql);
  164. }
  165. PhabricatorUserCache::clearCaches(
  166. PhabricatorUserNotificationCountCacheType::KEY_COUNT,
  167. $user_phids);
  168. }
  169. private function sendNotification($chrono_key, array $subscribed_phids) {
  170. $data = array(
  171. 'key' => (string)$chrono_key,
  172. 'type' => 'notification',
  173. 'subscribers' => $subscribed_phids,
  174. );
  175. PhabricatorNotificationClient::tryToPostMessage($data);
  176. }
  177. /**
  178. * Remove PHIDs who should not receive notifications from a subscriber list.
  179. *
  180. * @param list<phid> List of potential subscribers.
  181. * @return list<phid> List of actual subscribers.
  182. */
  183. private function filterSubscribedPHIDs(array $phids) {
  184. $phids = $this->expandRecipients($phids);
  185. $tags = $this->getMailTags();
  186. if ($tags) {
  187. $all_prefs = id(new PhabricatorUserPreferencesQuery())
  188. ->setViewer(PhabricatorUser::getOmnipotentUser())
  189. ->withUserPHIDs($phids)
  190. ->needSyntheticPreferences(true)
  191. ->execute();
  192. $all_prefs = mpull($all_prefs, null, 'getUserPHID');
  193. }
  194. $pref_default = PhabricatorEmailTagsSetting::VALUE_EMAIL;
  195. $pref_ignore = PhabricatorEmailTagsSetting::VALUE_IGNORE;
  196. $keep = array();
  197. foreach ($phids as $phid) {
  198. if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) {
  199. continue;
  200. }
  201. if ($tags && isset($all_prefs[$phid])) {
  202. $mailtags = $all_prefs[$phid]->getSettingValue(
  203. PhabricatorEmailTagsSetting::SETTINGKEY);
  204. $notify = false;
  205. foreach ($tags as $tag) {
  206. // If this is set to "email" or "notify", notify the user.
  207. if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) {
  208. $notify = true;
  209. break;
  210. }
  211. }
  212. if (!$notify) {
  213. continue;
  214. }
  215. }
  216. $keep[] = $phid;
  217. }
  218. return array_values(array_unique($keep));
  219. }
  220. private function expandRecipients(array $phids) {
  221. $expanded_phids = id(new PhabricatorMetaMTAMemberQuery())
  222. ->setViewer(PhabricatorUser::getOmnipotentUser())
  223. ->withPHIDs($phids)
  224. ->executeExpansion();
  225. // Filter out unexpandable PHIDs from the results. The typical case for
  226. // this is that resigned reviewers should not be notified just because
  227. // they are a member of some project or package reviewer.
  228. $original_map = array_fuse($phids);
  229. $unexpandable_map = array_fuse($this->unexpandablePHIDs);
  230. foreach ($expanded_phids as $key => $phid) {
  231. // We can keep this expanded PHID if it was present originally.
  232. if (isset($original_map[$phid])) {
  233. continue;
  234. }
  235. // We can also keep it if it isn't marked as unexpandable.
  236. if (!isset($unexpandable_map[$phid])) {
  237. continue;
  238. }
  239. // If it's unexpandable and we produced it by expanding recipients,
  240. // throw it away.
  241. unset($expanded_phids[$key]);
  242. }
  243. $expanded_phids = array_values($expanded_phids);
  244. return $expanded_phids;
  245. }
  246. /**
  247. * We generate a unique chronological key for each story type because we want
  248. * to be able to page through the stream with a cursor (i.e., select stories
  249. * after ID = X) so we can efficiently perform filtering after selecting data,
  250. * and multiple stories with the same ID make this cumbersome without putting
  251. * a bunch of logic in the client. We could use the primary key, but that
  252. * would prevent publishing stories which happened in the past. Since it's
  253. * potentially useful to do that (e.g., if you're importing another data
  254. * source) build a unique key for each story which has chronological ordering.
  255. *
  256. * @return string A unique, time-ordered key which identifies the story.
  257. */
  258. private function generateChronologicalKey() {
  259. // Use the epoch timestamp for the upper 32 bits of the key. Default to
  260. // the current time if the story doesn't have an explicit timestamp.
  261. $time = nonempty($this->storyTime, time());
  262. // Generate a random number for the lower 32 bits of the key.
  263. $rand = head(unpack('L', Filesystem::readRandomBytes(4)));
  264. // On 32-bit machines, we have to get creative.
  265. if (PHP_INT_SIZE < 8) {
  266. // We're on a 32-bit machine.
  267. if (function_exists('bcadd')) {
  268. // Try to use the 'bc' extension.
  269. return bcadd(bcmul($time, bcpow(2, 32)), $rand);
  270. } else {
  271. // Do the math in MySQL. TODO: If we formalize a bc dependency, get
  272. // rid of this.
  273. $conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r');
  274. $result = queryfx_one(
  275. $conn_r,
  276. 'SELECT (%d << 32) + %d as N',
  277. $time,
  278. $rand);
  279. return $result['N'];
  280. }
  281. } else {
  282. // This is a 64 bit machine, so we can just do the math.
  283. return ($time << 32) + $rand;
  284. }
  285. }
  286. }