[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/feed/ -> PhabricatorFeedStoryPublisher.php (source)

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


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1