public function destroyObject(PhabricatorDestructionEngine $engine, $object)
 {
     $src_phid = $object->getPHID();
     try {
         $edges = id(new PhabricatorEdgeQuery())->withSourcePHIDs(array($src_phid))->execute();
     } catch (Exception $ex) {
         // This is (presumably) a "no edges for this PHID type" exception.
         return;
     }
     $editor = new PhabricatorEdgeEditor();
     foreach ($edges as $type => $type_edges) {
         foreach ($type_edges as $src => $src_edges) {
             foreach ($src_edges as $dst => $edge) {
                 try {
                     $editor->removeEdge($edge['src'], $edge['type'], $edge['dst']);
                 } catch (Exception $ex) {
                     // We can run into an exception while removing the edge if the
                     // edge type no longer exists. This prevents us from figuring out
                     // if there's an inverse type. Just ignore any errors here and
                     // continue, since the best we can do is clean up all the edges
                     // we still have information about. See T11201.
                     phlog($ex);
                 }
             }
         }
     }
     $editor->save();
 }
 public function save()
 {
     if (!$this->object) {
         throw new Exception('Call setObject() before save()!');
     }
     $actor = $this->requireActor();
     $src = $this->object->getPHID();
     if ($this->implicitSubscribePHIDs) {
         $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs($src, PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
         $unsub = array_fill_keys($unsub, true);
         $this->implicitSubscribePHIDs = array_diff_key($this->implicitSubscribePHIDs, $unsub);
     }
     $add = $this->implicitSubscribePHIDs + $this->explicitSubscribePHIDs;
     $del = $this->unsubscribePHIDs;
     // If a PHID is marked for both subscription and unsubscription, treat
     // unsubscription as the stronger action.
     $add = array_diff_key($add, $del);
     if ($add || $del) {
         $u_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER;
         $s_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER;
         $editor = new PhabricatorEdgeEditor();
         foreach ($add as $phid => $ignored) {
             $editor->removeEdge($src, $u_type, $phid);
             $editor->addEdge($src, $s_type, $phid);
         }
         foreach ($del as $phid => $ignored) {
             $editor->removeEdge($src, $s_type, $phid);
             $editor->addEdge($src, $u_type, $phid);
         }
         $editor->save();
     }
 }
 public function save()
 {
     if (!$this->object) {
         throw new PhutilInvalidStateException('setObject');
     }
     $actor = $this->requireActor();
     $src = $this->object->getPHID();
     if ($this->implicitSubscribePHIDs) {
         $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs($src, PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
         $unsub = array_fill_keys($unsub, true);
         $this->implicitSubscribePHIDs = array_diff_key($this->implicitSubscribePHIDs, $unsub);
     }
     $add = $this->implicitSubscribePHIDs + $this->explicitSubscribePHIDs;
     $del = $this->unsubscribePHIDs;
     // If a PHID is marked for both subscription and unsubscription, treat
     // unsubscription as the stronger action.
     $add = array_diff_key($add, $del);
     if ($add || $del) {
         $u_type = PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST;
         $s_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
         $editor = new PhabricatorEdgeEditor();
         foreach ($add as $phid => $ignored) {
             $editor->removeEdge($src, $u_type, $phid);
             $editor->addEdge($src, $s_type, $phid);
         }
         foreach ($del as $phid => $ignored) {
             $editor->removeEdge($src, $s_type, $phid);
             $editor->addEdge($src, $u_type, $phid);
         }
         $editor->save();
     }
 }
 /**
  * Edit a transaction's comment. This method effects the required create,
  * update or delete to set the transaction's comment to the provided comment.
  */
 public function applyEdit(PhabricatorApplicationTransaction $xaction, PhabricatorApplicationTransactionComment $comment)
 {
     $this->validateEdit($xaction, $comment);
     $actor = $this->requireActor();
     $comment->setContentSource($this->getContentSource());
     $comment->setAuthorPHID($this->getActingAsPHID());
     // TODO: This needs to be more sophisticated once we have meta-policies.
     $comment->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
     $comment->setEditPolicy($this->getActingAsPHID());
     $file_phids = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles($actor, array($comment->getContent()));
     $xaction->openTransaction();
     $xaction->beginReadLocking();
     if ($xaction->getID()) {
         $xaction->reload();
     }
     $new_version = $xaction->getCommentVersion() + 1;
     $comment->setCommentVersion($new_version);
     $comment->setTransactionPHID($xaction->getPHID());
     $comment->save();
     $old_comment = $xaction->getComment();
     $comment->attachOldComment($old_comment);
     $xaction->setCommentVersion($new_version);
     $xaction->setCommentPHID($comment->getPHID());
     $xaction->setViewPolicy($comment->getViewPolicy());
     $xaction->setEditPolicy($comment->getEditPolicy());
     $xaction->save();
     $xaction->attachComment($comment);
     // For comment edits, we need to make sure there are no automagical
     // transactions like adding mentions or projects.
     if ($new_version > 1) {
         $object = id(new PhabricatorObjectQuery())->withPHIDs(array($xaction->getObjectPHID()))->setViewer($this->getActor())->executeOne();
         if ($object && $object instanceof PhabricatorApplicationTransactionInterface) {
             $editor = $object->getApplicationTransactionEditor();
             $editor->setActor($this->getActor());
             $support_xactions = $editor->getExpandedSupportTransactions($object, $xaction);
             if ($support_xactions) {
                 $editor->setContentSource($this->getContentSource())->setContinueOnNoEffect(true)->applyTransactions($object, $support_xactions);
             }
         }
     }
     $xaction->endReadLocking();
     $xaction->saveTransaction();
     // Add links to any files newly referenced by the edit.
     if ($file_phids) {
         $editor = new PhabricatorEdgeEditor();
         foreach ($file_phids as $file_phid) {
             $editor->addEdge($xaction->getObjectPHID(), PhabricatorObjectHasFileEdgeType::EDGECONST, $file_phid);
         }
         $editor->save();
     }
     return $this;
 }
 private function destroyEdges($src_phid)
 {
     try {
         $edges = id(new PhabricatorEdgeQuery())->withSourcePHIDs(array($src_phid))->execute();
     } catch (Exception $ex) {
         // This is (presumably) a "no edges for this PHID type" exception.
         return;
     }
     $editor = new PhabricatorEdgeEditor();
     foreach ($edges as $type => $type_edges) {
         foreach ($type_edges as $src => $src_edges) {
             foreach ($src_edges as $dst => $edge) {
                 $editor->removeEdge($edge['src'], $edge['type'], $edge['dst']);
             }
         }
     }
     $editor->save();
 }
 public function destroyObject(PhabricatorDestructionEngine $engine, $object)
 {
     $src_phid = $object->getPHID();
     try {
         $edges = id(new PhabricatorEdgeQuery())->withSourcePHIDs(array($src_phid))->execute();
     } catch (Exception $ex) {
         // This is (presumably) a "no edges for this PHID type" exception.
         return;
     }
     $editor = new PhabricatorEdgeEditor();
     foreach ($edges as $type => $type_edges) {
         foreach ($type_edges as $src => $src_edges) {
             foreach ($src_edges as $dst => $edge) {
                 $editor->removeEdge($edge['src'], $edge['type'], $edge['dst']);
             }
         }
     }
     $editor->save();
 }
 /**
  * Edit a transaction's comment. This method effects the required create,
  * update or delete to set the transaction's comment to the provided comment.
  */
 public function applyEdit(PhabricatorApplicationTransaction $xaction, PhabricatorApplicationTransactionComment $comment)
 {
     $this->validateEdit($xaction, $comment);
     $actor = $this->requireActor();
     $comment->setContentSource($this->getContentSource());
     $comment->setAuthorPHID($this->getActingAsPHID());
     // TODO: This needs to be more sophisticated once we have meta-policies.
     $comment->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
     $comment->setEditPolicy($this->getActingAsPHID());
     $file_phids = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles($actor, array($comment->getContent()));
     $xaction->openTransaction();
     $xaction->beginReadLocking();
     if ($xaction->getID()) {
         $xaction->reload();
     }
     $new_version = $xaction->getCommentVersion() + 1;
     $comment->setCommentVersion($new_version);
     $comment->setTransactionPHID($xaction->getPHID());
     $comment->save();
     $xaction->setCommentVersion($new_version);
     $xaction->setCommentPHID($comment->getPHID());
     $xaction->setViewPolicy($comment->getViewPolicy());
     $xaction->setEditPolicy($comment->getEditPolicy());
     $xaction->save();
     $xaction->endReadLocking();
     $xaction->saveTransaction();
     // Add links to any files newly referenced by the edit.
     if ($file_phids) {
         $editor = new PhabricatorEdgeEditor();
         foreach ($file_phids as $file_phid) {
             $editor->addEdge($xaction->getObjectPHID(), PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE, $file_phid);
         }
         $editor->save();
     }
     $xaction->attachComment($comment);
     return $this;
 }
 public function processRequest()
 {
     $request = $this->getRequest();
     $user = $request->getUser();
     $e_phame_title = null;
     $e_title = null;
     $errors = array();
     if ($this->isPostEdit()) {
         $posts = id(new PhamePostQuery())->withPHIDs(array($this->getPostPHID()))->execute();
         $post = reset($posts);
         if (empty($post)) {
             return new Aphront404Response();
         }
         if ($post->getBloggerPHID() != $user->getPHID()) {
             return new Aphront403Response();
         }
         $post_noun = ucfirst($post->getHumanName());
         $cancel_uri = $post->getViewURI($user->getUsername());
         $submit_button = 'Save Changes';
         $delete_button = javelin_render_tag('a', array('href' => $post->getDeleteURI(), 'class' => 'grey button', 'sigil' => 'workflow'), 'Delete ' . $post_noun);
         $page_title = 'Edit ' . $post_noun;
     } else {
         $post = id(new PhamePost())->setBloggerPHID($user->getPHID())->setVisibility(PhamePost::VISIBILITY_DRAFT);
         $cancel_uri = '/phame/draft/';
         $submit_button = 'Create Draft';
         $delete_button = null;
         $page_title = 'Create Draft';
     }
     $this->setPost($post);
     $this->loadEdgesAndBlogs();
     if ($request->isFormPost()) {
         $saved = true;
         $visibility = $request->getInt('visibility');
         $comments = $request->getStr('comments_widget');
         $data = array('comments_widget' => $comments);
         $phame_title = $request->getStr('phame_title');
         $phame_title = PhabricatorSlug::normalize($phame_title);
         $title = $request->getStr('title');
         $post->setTitle($title);
         $post->setPhameTitle($phame_title);
         $post->setBody($request->getStr('body'));
         $post->setVisibility($visibility);
         $post->setConfigData($data);
         // only publish once...!
         if ($visibility == PhamePost::VISIBILITY_PUBLISHED) {
             if (!$post->getDatePublished()) {
                 $post->setDatePublished(time());
             }
             // this is basically a cast of null to 0 if its a new post
         } else {
             if (!$post->getDatePublished()) {
                 $post->setDatePublished(0);
             }
         }
         if ($phame_title == '/') {
             $errors[] = 'Phame title must be nonempty.';
             $e_phame_title = 'Required';
         }
         if (empty($title)) {
             $errors[] = 'Title must be nonempty.';
             $e_title = 'Required';
         }
         $blogs_published = array_keys($this->getPostBlogs());
         $blogs_to_publish = array();
         $blogs_to_depublish = array();
         if ($visibility == PhamePost::VISIBILITY_PUBLISHED) {
             $blogs_arr = $request->getArr('blogs');
             $blogs_to_publish = array_values($blogs_arr);
             $blogs_to_depublish = array_diff($blogs_published, $blogs_to_publish);
         } else {
             $blogs_to_depublish = $blogs_published;
         }
         if (empty($errors)) {
             try {
                 $post->save();
                 $editor = new PhabricatorEdgeEditor();
                 $edge_type = PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG;
                 $editor->setUser($user);
                 foreach ($blogs_to_publish as $phid) {
                     $editor->addEdge($post->getPHID(), $edge_type, $phid);
                 }
                 foreach ($blogs_to_depublish as $phid) {
                     $editor->removeEdge($post->getPHID(), $edge_type, $phid);
                 }
                 $editor->save();
             } catch (AphrontQueryDuplicateKeyException $e) {
                 $saved = false;
                 $e_phame_title = 'Not Unique';
                 $errors[] = 'Another post already uses this slug. ' . 'Each post must have a unique slug.';
             }
         } else {
             $saved = false;
         }
         if ($saved) {
             $uri = new PhutilURI($post->getViewURI($user->getUsername()));
             $uri->setQueryParam('saved', true);
             return id(new AphrontRedirectResponse())->setURI($uri);
         }
     }
     $panel = new AphrontPanelView();
     $panel->setHeader($page_title);
     $panel->setWidth(AphrontPanelView::WIDTH_FULL);
     if ($delete_button) {
         $panel->addButton($delete_button);
     }
     $form = id(new AphrontFormView())->setUser($user)->appendChild(id(new AphrontFormTextControl())->setLabel('Title')->setName('title')->setValue($post->getTitle())->setID('post-title')->setError($e_title))->appendChild(id(new AphrontFormTextControl())->setLabel('Phame Title')->setName('phame_title')->setValue(rtrim($post->getPhameTitle(), '/'))->setID('post-phame-title')->setCaption('Up to 64 alphanumeric characters ' . 'with underscores for spaces. ' . 'Formatting is enforced.')->setError($e_phame_title))->appendChild(id(new PhabricatorRemarkupControl())->setLabel('Body')->setName('body')->setValue($post->getBody())->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)->setID('post-body'))->appendChild(id(new AphrontFormSelectControl())->setLabel('Visibility')->setName('visibility')->setValue($post->getVisibility())->setOptions(PhamePost::getVisibilityOptionsForSelect())->setID('post-visibility'))->appendChild($this->getBlogCheckboxControl($post))->appendChild(id(new AphrontFormSelectControl())->setLabel('Comments Widget')->setName('comments_widget')->setvalue($post->getCommentsWidget())->setOptions($post->getCommentsWidgetOptionsForSelect()))->appendChild(id(new AphrontFormSubmitControl())->addCancelButton($cancel_uri)->setValue($submit_button));
     $panel->appendChild($form);
     $preview_panel = '<div class="aphront-panel-preview ">
      <div class="phame-post-preview-header">
        Post Preview
      </div>
      <div id="post-preview">
        <div class="aphront-panel-preview-loading-text">
          Loading preview...
        </div>
      </div>
    </div>';
     Javelin::initBehavior('phame-post-preview', array('preview' => 'post-preview', 'body' => 'post-body', 'title' => 'post-title', 'phame_title' => 'post-phame-title', 'uri' => '/phame/post/preview/'));
     $visibility_data = array('select_id' => 'post-visibility', 'current' => $post->getVisibility(), 'published' => PhamePost::VISIBILITY_PUBLISHED, 'draft' => PhamePost::VISIBILITY_DRAFT, 'change_uri' => $post->getChangeVisibilityURI());
     $blogs_data = array('checkbox_id' => 'post-blogs', 'have_published' => (bool) count($this->getPostBlogs()));
     Javelin::initBehavior('phame-post-blogs', array('blogs' => $blogs_data, 'visibility' => $visibility_data));
     if ($errors) {
         $error_view = id(new AphrontErrorView())->setTitle('Errors saving post.')->setErrors($errors);
     } else {
         $error_view = null;
     }
     $this->setShowSideNav(true);
     return $this->buildStandardPageResponse(array($error_view, $panel, $preview_panel), array('title' => $page_title));
 }
 public function save()
 {
     if ($this->getID()) {
         return parent::save();
     }
     // NOTE: When mail is sent from CLI scripts that run tasks in-process, we
     // may re-enter this method from within scheduleTask(). The implementation
     // is intended to avoid anything awkward if we end up reentering this
     // method.
     $this->openTransaction();
     // Save to generate a mail ID and PHID.
     $result = parent::save();
     // Write the recipient edges.
     $editor = new PhabricatorEdgeEditor();
     $edge_type = PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST;
     $recipient_phids = array_merge($this->getToPHIDs(), $this->getCcPHIDs());
     $expanded_phids = $this->expandRecipients($recipient_phids);
     $all_phids = array_unique(array_merge($recipient_phids, $expanded_phids));
     foreach ($all_phids as $curr_phid) {
         $editor->addEdge($this->getPHID(), $edge_type, $curr_phid);
     }
     $editor->save();
     // Queue a task to send this mail.
     $mailer_task = PhabricatorWorker::scheduleTask('PhabricatorMetaMTAWorker', $this->getID(), array('priority' => PhabricatorWorker::PRIORITY_ALERTS));
     $this->saveTransaction();
     return $result;
 }
Пример #10
0
 protected function applyCustomExternalTransaction(PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction)
 {
     switch ($xaction->getTransactionType()) {
         case ConpherenceTransaction::TYPE_FILES:
             $editor = new PhabricatorEdgeEditor();
             $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
             $old = array_fill_keys($xaction->getOldValue(), true);
             $new = array_fill_keys($xaction->getNewValue(), true);
             $add_edges = array_keys(array_diff_key($new, $old));
             $remove_edges = array_keys(array_diff_key($old, $new));
             foreach ($add_edges as $file_phid) {
                 $editor->addEdge($object->getPHID(), $edge_type, $file_phid);
             }
             foreach ($remove_edges as $file_phid) {
                 $editor->removeEdge($object->getPHID(), $edge_type, $file_phid);
             }
             $editor->save();
             break;
         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;
     }
 }
 public function applyApplicationTransactionExternalEffects(PhabricatorApplicationTransaction $xaction)
 {
     // Update the CustomField storage.
     parent::applyApplicationTransactionExternalEffects($xaction);
     // Now, synchronize the Doorkeeper edges.
     $revision = $this->getObject();
     $revision_phid = $revision->getPHID();
     $edge_type = PhabricatorJiraIssueHasObjectEdgeType::EDGECONST;
     $xobjs = $this->loadDoorkeeperExternalObjects($xaction->getNewValue());
     $edge_dsts = mpull($xobjs, 'getPHID');
     $edges = PhabricatorEdgeQuery::loadDestinationPHIDs($revision_phid, $edge_type);
     $editor = new PhabricatorEdgeEditor();
     foreach (array_diff($edges, $edge_dsts) as $rem_edge) {
         $editor->removeEdge($revision_phid, $edge_type, $rem_edge);
     }
     foreach (array_diff($edge_dsts, $edges) as $add_edge) {
         $editor->addEdge($revision_phid, $edge_type, $add_edge);
     }
     $editor->save();
 }
 /**
  * @task files
  */
 private function attachFiles(PhabricatorLiskDAO $object, array $file_phids)
 {
     if (!$file_phids) {
         return;
     }
     $editor = new PhabricatorEdgeEditor();
     $src = $object->getPHID();
     $type = PhabricatorObjectHasFileEdgeType::EDGECONST;
     foreach ($file_phids as $dst) {
         $editor->addEdge($src, $type, $dst);
     }
     $editor->save();
 }
 protected function applyBuiltinExternalTransaction(PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction)
 {
     switch ($xaction->getTransactionType()) {
         case PhabricatorTransactions::TYPE_EDGE:
             $edge_type = $xaction->getMetadataValue('edge:type');
             switch ($edge_type) {
                 case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
                 case PhabricatorObjectHasWatcherEdgeType::EDGECONST:
                     $old = $xaction->getOldValue();
                     $new = $xaction->getNewValue();
                     // When adding members or watchers, we add subscriptions.
                     $add = array_keys(array_diff_key($new, $old));
                     // When removing members, we remove their subscription too.
                     // When unwatching, we leave subscriptions, since it's fine to be
                     // subscribed to a project but not be a member of it.
                     $edge_const = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
                     if ($edge_type == $edge_const) {
                         $rem = array_keys(array_diff_key($old, $new));
                     } else {
                         $rem = array();
                     }
                     // NOTE: The subscribe is "explicit" because there's no implicit
                     // unsubscribe, so Join -> Leave -> Join doesn't resubscribe you
                     // if we use an implicit subscribe, even though you never willfully
                     // unsubscribed. Not sure if adding implicit unsubscribe (which
                     // would not write the unsubscribe row) is justified to deal with
                     // this, which is a fairly weird edge case and pretty arguable both
                     // ways.
                     // Subscriptions caused by watches should also clearly be explicit,
                     // and that case is unambiguous.
                     id(new PhabricatorSubscriptionsEditor())->setActor($this->requireActor())->setObject($object)->subscribeExplicit($add)->unsubscribe($rem)->save();
                     if ($rem) {
                         // When removing members, also remove any watches on the project.
                         $edge_editor = new PhabricatorEdgeEditor();
                         foreach ($rem as $rem_phid) {
                             $edge_editor->removeEdge($object->getPHID(), PhabricatorObjectHasWatcherEdgeType::EDGECONST, $rem_phid);
                         }
                         $edge_editor->save();
                     }
                     break;
             }
             break;
     }
     return parent::applyBuiltinExternalTransaction($object, $xaction);
 }
 protected function applyCustomExternalTransaction(PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction)
 {
     switch ($xaction->getTransactionType()) {
         case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL:
             // Adjust the object <-> credential edge for this repository.
             $old_phid = $xaction->getOldValue();
             $new_phid = $xaction->getNewValue();
             $editor = new PhabricatorEdgeEditor();
             $edge_type = PhabricatorObjectUsesCredentialsEdgeType::EDGECONST;
             $src_phid = $object->getPHID();
             if ($old_phid) {
                 $editor->removeEdge($src_phid, $edge_type, $old_phid);
             }
             if ($new_phid) {
                 $editor->addEdge($src_phid, $edge_type, $new_phid);
             }
             $editor->save();
             break;
         case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
             DrydockAuthorization::applyAuthorizationChanges($this->getActor(), $object->getPHID(), $xaction->getOldValue(), $xaction->getNewValue());
             break;
     }
 }
 protected function applyCustomExternalTransaction(PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction)
 {
     switch ($xaction->getTransactionType()) {
         case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL:
             // Adjust the object <-> credential edge for this repository.
             $old_phid = $xaction->getOldValue();
             $new_phid = $xaction->getNewValue();
             $editor = new PhabricatorEdgeEditor();
             $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_USES_CREDENTIAL;
             $src_phid = $object->getPHID();
             if ($old_phid) {
                 $editor->removeEdge($src_phid, $edge_type, $old_phid);
             }
             if ($new_phid) {
                 $editor->addEdge($src_phid, $edge_type, $new_phid);
             }
             $editor->save();
             break;
     }
 }
 /**
  * Publish stories into Asana using the Asana API.
  */
 protected function publishFeedStory()
 {
     $story = $this->getFeedStory();
     $data = $story->getStoryData();
     $viewer = $this->getViewer();
     $provider = $this->getProvider();
     $workspace_id = $this->getWorkspaceID();
     $object = $this->getStoryObject();
     $src_phid = $object->getPHID();
     $publisher = $this->getPublisher();
     // Figure out all the users related to the object. Users go into one of
     // four buckets:
     //
     //   - Owner: the owner of the object. This user becomes the assigned owner
     //     of the parent task.
     //   - Active: users who are responsible for the object and need to act on
     //     it. For example, reviewers of a "needs review" revision.
     //   - Passive: users who are responsible for the object, but do not need
     //     to act on it right now. For example, reviewers of a "needs revision"
     //     revision.
     //   - Follow: users who are following the object; generally CCs.
     $owner_phid = $publisher->getOwnerPHID($object);
     $active_phids = $publisher->getActiveUserPHIDs($object);
     $passive_phids = $publisher->getPassiveUserPHIDs($object);
     $follow_phids = $publisher->getCCUserPHIDs($object);
     $all_phids = array();
     $all_phids = array_merge(array($owner_phid), $active_phids, $passive_phids, $follow_phids);
     $all_phids = array_unique(array_filter($all_phids));
     $phid_aid_map = $this->lookupAsanaUserIDs($all_phids);
     if (!$phid_aid_map) {
         throw new PhabricatorWorkerPermanentFailureException('No related users have linked Asana accounts.');
     }
     $owner_asana_id = idx($phid_aid_map, $owner_phid);
     $all_asana_ids = array_select_keys($phid_aid_map, $all_phids);
     $all_asana_ids = array_values($all_asana_ids);
     // Even if the actor isn't a reviewer, etc., try to use their account so
     // we can post in the correct voice. If we miss, we'll try all the other
     // related users.
     $try_users = array_merge(array($data->getAuthorPHID()), array_keys($phid_aid_map));
     $try_users = array_filter($try_users);
     $access_info = $this->findAnyValidAsanaAccessToken($try_users);
     list($possessed_user, $possessed_asana_id, $oauth_token) = $access_info;
     if (!$oauth_token) {
         throw new PhabricatorWorkerPermanentFailureException('Unable to find any Asana user with valid credentials to ' . 'pull an OAuth token out of.');
     }
     $etype_main = PhabricatorEdgeConfig::TYPE_PHOB_HAS_ASANATASK;
     $etype_sub = PhabricatorEdgeConfig::TYPE_PHOB_HAS_ASANASUBTASK;
     $equery = id(new PhabricatorEdgeQuery())->withSourcePHIDs(array($src_phid))->withEdgeTypes(array($etype_main, $etype_sub))->needEdgeData(true);
     $edges = $equery->execute();
     $main_edge = head($edges[$src_phid][$etype_main]);
     $main_data = $this->getAsanaTaskData($object) + array('assignee' => $owner_asana_id);
     $projects = $this->getAsanaProjectIDs();
     $extra_data = array();
     if ($main_edge) {
         $extra_data = $main_edge['data'];
         $refs = id(new DoorkeeperImportEngine())->setViewer($possessed_user)->withPHIDs(array($main_edge['dst']))->execute();
         $parent_ref = head($refs);
         if (!$parent_ref) {
             throw new PhabricatorWorkerPermanentFailureException('DoorkeeperExternalObject could not be loaded.');
         }
         if ($parent_ref->getSyncFailed()) {
             throw new Exception('Synchronization of parent task from Asana failed!');
         } else {
             if (!$parent_ref->getIsVisible()) {
                 $this->log("Skipping main task update, object is no longer visible.\n");
                 $extra_data['gone'] = true;
             } else {
                 $edge_cursor = idx($main_edge['data'], 'cursor', 0);
                 // TODO: This probably breaks, very rarely, on 32-bit systems.
                 if ($edge_cursor <= $story->getChronologicalKey()) {
                     $this->log("Updating main task.\n");
                     $task_id = $parent_ref->getObjectID();
                     $this->makeAsanaAPICall($oauth_token, 'tasks/' . $parent_ref->getObjectID(), 'PUT', $main_data);
                 } else {
                     $this->log("Skipping main task update, cursor is ahead of the story.\n");
                 }
             }
         }
     } else {
         // If there are no followers (CCs), and no active or passive users
         // (reviewers or auditors), and we haven't synchronized the object before,
         // don't synchronize the object.
         if (!$active_phids && !$passive_phids && !$follow_phids) {
             $this->log("Object has no followers or active/passive users.\n");
             return;
         }
         $parent = $this->makeAsanaAPICall($oauth_token, 'tasks', 'POST', array('workspace' => $workspace_id, 'projects' => $projects, 'assignee_status' => 'later') + $main_data);
         $parent_ref = $this->newRefFromResult(DoorkeeperBridgeAsana::OBJTYPE_TASK, $parent);
         $extra_data = array('workspace' => $workspace_id);
     }
     // Synchronize main task followers.
     $task_id = $parent_ref->getObjectID();
     // Reviewers are added as followers of the parent task silently, because
     // they receive a notification when they are assigned as the owner of their
     // subtask, so the follow notification is redundant / non-actionable.
     $silent_followers = array_select_keys($phid_aid_map, $active_phids) + array_select_keys($phid_aid_map, $passive_phids);
     $silent_followers = array_values($silent_followers);
     // CCs are added as followers of the parent task with normal notifications,
     // since they won't get a secondary subtask notification.
     $noisy_followers = array_select_keys($phid_aid_map, $follow_phids);
     $noisy_followers = array_values($noisy_followers);
     // To synchronize follower data, just add all the followers. The task might
     // have additional followers, but we can't really tell how they got there:
     // were they CC'd and then unsubscribed, or did they manually follow the
     // task? Assume the latter since it's easier and less destructive and the
     // former is rare. To be fully consistent, we should enumerate followers
     // and remove unknown followers, but that's a fair amount of work for little
     // benefit, and creates a wider window for race conditions.
     // Add the silent followers first so that a user who is both a reviewer and
     // a CC gets silently added and then implicitly skipped by then noisy add.
     // They will get a subtask notification.
     // We only do this if the task still exists.
     if (empty($extra_data['gone'])) {
         $this->addFollowers($oauth_token, $task_id, $silent_followers, true);
         $this->addFollowers($oauth_token, $task_id, $noisy_followers);
         // We're also going to synchronize project data here.
         $this->addProjects($oauth_token, $task_id, $projects);
     }
     $dst_phid = $parent_ref->getExternalObject()->getPHID();
     // Update the main edge.
     $edge_data = array('cursor' => $story->getChronologicalKey()) + $extra_data;
     $edge_options = array('data' => $edge_data);
     id(new PhabricatorEdgeEditor())->addEdge($src_phid, $etype_main, $dst_phid, $edge_options)->save();
     if (!$parent_ref->getIsVisible()) {
         throw new PhabricatorWorkerPermanentFailureException('DoorkeeperExternalObject has no visible object on the other side; ' . 'this likely indicates the Asana task has been deleted.');
     }
     // Now, handle the subtasks.
     $sub_editor = new PhabricatorEdgeEditor();
     // First, find all the object references in Phabricator for tasks that we
     // know about and import their objects from Asana.
     $sub_edges = $edges[$src_phid][$etype_sub];
     $sub_refs = array();
     $subtask_data = $this->getAsanaSubtaskData($object);
     $have_phids = array();
     if ($sub_edges) {
         $refs = id(new DoorkeeperImportEngine())->setViewer($possessed_user)->withPHIDs(array_keys($sub_edges))->execute();
         foreach ($refs as $ref) {
             if ($ref->getSyncFailed()) {
                 throw new Exception('Synchronization of child task from Asana failed!');
             }
             if (!$ref->getIsVisible()) {
                 $ref->getExternalObject()->delete();
                 continue;
             }
             $have_phids[$ref->getExternalObject()->getPHID()] = $ref;
         }
     }
     // Remove any edges in Phabricator which don't have valid tasks in Asana.
     // These are likely tasks which have been deleted. We're going to respawn
     // them.
     foreach ($sub_edges as $sub_phid => $sub_edge) {
         if (isset($have_phids[$sub_phid])) {
             continue;
         }
         $this->log("Removing subtask edge to %s, foreign object is not visible.\n", $sub_phid);
         $sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid);
         unset($sub_edges[$sub_phid]);
     }
     // For each active or passive user, we're looking for an existing, valid
     // task. If we find one we're going to update it; if we don't, we'll
     // create one. We ignore extra subtasks that we didn't create (we gain
     // nothing by deleting them and might be nuking something important) and
     // ignore subtasks which have been moved across workspaces or replanted
     // under new parents (this stuff is too edge-casey to bother checking for
     // and complicated to fix, as it needs extra API calls). However, we do
     // clean up subtasks we created whose owners are no longer associated
     // with the object.
     $subtask_states = array_fill_keys($active_phids, false) + array_fill_keys($passive_phids, true);
     // Continue with only those users who have Asana credentials.
     $subtask_states = array_select_keys($subtask_states, array_keys($phid_aid_map));
     $need_subtasks = $subtask_states;
     $user_to_ref_map = array();
     $nuke_refs = array();
     foreach ($sub_edges as $sub_phid => $sub_edge) {
         $user_phid = idx($sub_edge['data'], 'userPHID');
         if (isset($need_subtasks[$user_phid])) {
             unset($need_subtasks[$user_phid]);
             $user_to_ref_map[$user_phid] = $have_phids[$sub_phid];
         } else {
             // This user isn't associated with the object anymore, so get rid
             // of their task and edge.
             $nuke_refs[$sub_phid] = $have_phids[$sub_phid];
         }
     }
     // These are tasks we know about but which are no longer relevant -- for
     // example, because a user has been removed as a reviewer. Remove them and
     // their edges.
     foreach ($nuke_refs as $sub_phid => $ref) {
         $sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid);
         $this->makeAsanaAPICall($oauth_token, 'tasks/' . $ref->getObjectID(), 'DELETE', array());
         $ref->getExternalObject()->delete();
     }
     // For each user that we don't have a subtask for, create a new subtask.
     foreach ($need_subtasks as $user_phid => $is_completed) {
         $subtask = $this->makeAsanaAPICall($oauth_token, 'tasks', 'POST', $subtask_data + array('assignee' => $phid_aid_map[$user_phid], 'completed' => $is_completed, 'parent' => $parent_ref->getObjectID()));
         $subtask_ref = $this->newRefFromResult(DoorkeeperBridgeAsana::OBJTYPE_TASK, $subtask);
         $user_to_ref_map[$user_phid] = $subtask_ref;
         // We don't need to synchronize this subtask's state because we just
         // set it when we created it.
         unset($subtask_states[$user_phid]);
         // Add an edge to track this subtask.
         $sub_editor->addEdge($src_phid, $etype_sub, $subtask_ref->getExternalObject()->getPHID(), array('data' => array('userPHID' => $user_phid)));
     }
     // Synchronize all the previously-existing subtasks.
     foreach ($subtask_states as $user_phid => $is_completed) {
         $this->makeAsanaAPICall($oauth_token, 'tasks/' . $user_to_ref_map[$user_phid]->getObjectID(), 'PUT', $subtask_data + array('assignee' => $phid_aid_map[$user_phid], 'completed' => $is_completed));
     }
     foreach ($user_to_ref_map as $user_phid => $ref) {
         // For each subtask, if the acting user isn't the same user as the subtask
         // owner, remove the acting user as a follower. Currently, the acting user
         // will be added as a follower only when they create the task, but this
         // may change in the future (e.g., closing the task may also mark them
         // as a follower). Wipe every subtask to be sure. The intent here is to
         // leave only the owner as a follower so that the acting user doesn't
         // receive notifications about changes to subtask state. Note that
         // removing followers is silent in all cases in Asana and never produces
         // any kind of notification, so this isn't self-defeating.
         if ($user_phid != $possessed_user->getPHID()) {
             $this->makeAsanaAPICall($oauth_token, 'tasks/' . $ref->getObjectID() . '/removeFollowers', 'POST', array('followers' => array($possessed_asana_id)));
         }
     }
     // Update edges on our side.
     $sub_editor->save();
     // Don't publish the "create" story, since pushing the object into Asana
     // naturally generates a notification which effectively serves the same
     // purpose as the "create" story. Similarly, "close" stories generate a
     // close notification.
     if (!$publisher->isStoryAboutObjectCreation($object) && !$publisher->isStoryAboutObjectClosure($object)) {
         // Post the feed story itself to the main Asana task. We do this last
         // because everything else is idempotent, so this is the only effect we
         // can't safely run more than once.
         $text = $publisher->setRenderWithImpliedContext(true)->getStoryText($object);
         $this->makeAsanaAPICall($oauth_token, 'tasks/' . $parent_ref->getObjectID() . '/stories', 'POST', array('text' => $text));
     }
 }
Пример #17
0
<?php

echo pht('Migrating %s to edges...', 'differential.revisionPHID') . "\n";
$commit_table = new PhabricatorRepositoryCommit();
$data_table = new PhabricatorRepositoryCommitData();
$editor = new PhabricatorEdgeEditor();
$commit_table->establishConnection('w');
$edges = 0;
foreach (new LiskMigrationIterator($commit_table) as $commit) {
    $data = $commit->loadOneRelative($data_table, 'commitID');
    if (!$data) {
        continue;
    }
    $revision_phid = $data->getCommitDetail('differential.revisionPHID');
    if (!$revision_phid) {
        continue;
    }
    $commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST;
    $editor->addEdge($commit->getPHID(), $commit_drev, $revision_phid);
    $edges++;
    if ($edges % 256 == 0) {
        echo '.';
        $editor->save();
        $editor = new PhabricatorEdgeEditor();
    }
}
echo '.';
$editor->save();
echo "\n" . pht('Done.') . "\n";
 protected function applyCustomExternalTransaction(PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction)
 {
     $old = $xaction->getOldValue();
     $new = $xaction->getNewValue();
     switch ($xaction->getTransactionType()) {
         case PhabricatorProjectTransaction::TYPE_NAME:
             // First, remove the old and new slugs. Removing the old slug is
             // important when changing the project's capitalization or puctuation.
             // Removing the new slug is important when changing the project's name
             // so that one of its secondary slugs is now the primary slug.
             if ($old !== null) {
                 $this->removeSlug($object, $old);
             }
             $this->removeSlug($object, $new);
             $new_slug = id(new PhabricatorProjectSlug())->setSlug($object->getPrimarySlug())->setProjectPHID($object->getPHID())->save();
             // TODO -- delete all of the below once we sever automagical project
             // to phriction stuff
             if ($xaction->getOldValue() === null) {
                 // Project was just created, we don't need to move anything.
                 return;
             }
             $clone_object = clone $object;
             $clone_object->setPhrictionSlug($xaction->getOldValue());
             $old_slug = $clone_object->getFullPhrictionSlug();
             $old_document = id(new PhrictionDocument())->loadOneWhere('slug = %s', $old_slug);
             if ($old_document && $old_document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) {
                 $content = id(new PhrictionContent())->load($old_document->getContentID());
                 $from_editor = id(PhrictionDocumentEditor::newForSlug($old_slug))->setActor($this->getActor())->setTitle($content->getTitle())->setContent($content->getContent())->setDescription($content->getDescription());
                 $target_editor = id(PhrictionDocumentEditor::newForSlug($object->getFullPhrictionSlug()))->setActor($this->getActor())->setTitle($content->getTitle())->setContent($content->getContent())->setDescription($content->getDescription())->moveHere($old_document->getID(), $old_document->getPHID());
                 $target_document = $target_editor->getDocument();
                 $from_editor->moveAway($target_document->getID());
             }
             return;
         case PhabricatorProjectTransaction::TYPE_SLUGS:
             $old = $xaction->getOldValue();
             $new = $xaction->getNewValue();
             $add = array_diff($new, $old);
             $rem = array_diff($old, $new);
             if ($add) {
                 $add_slug_template = id(new PhabricatorProjectSlug())->setProjectPHID($object->getPHID());
                 foreach ($add as $add_slug_str) {
                     $add_slug = id(clone $add_slug_template)->setSlug($add_slug_str)->save();
                 }
             }
             if ($rem) {
                 $rem_slugs = id(new PhabricatorProjectSlug())->loadAllWhere('slug IN (%Ls)', $rem);
                 foreach ($rem_slugs as $rem_slug) {
                     $rem_slug->delete();
                 }
             }
             return;
         case PhabricatorTransactions::TYPE_VIEW_POLICY:
         case PhabricatorTransactions::TYPE_EDIT_POLICY:
         case PhabricatorTransactions::TYPE_JOIN_POLICY:
         case PhabricatorProjectTransaction::TYPE_STATUS:
         case PhabricatorProjectTransaction::TYPE_IMAGE:
         case PhabricatorProjectTransaction::TYPE_ICON:
         case PhabricatorProjectTransaction::TYPE_COLOR:
             return;
         case PhabricatorTransactions::TYPE_EDGE:
             $edge_type = $xaction->getMetadataValue('edge:type');
             switch ($edge_type) {
                 case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
                 case PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER:
                     $old = $xaction->getOldValue();
                     $new = $xaction->getNewValue();
                     // When adding members or watchers, we add subscriptions.
                     $add = array_keys(array_diff_key($new, $old));
                     // When removing members, we remove their subscription too.
                     // When unwatching, we leave subscriptions, since it's fine to be
                     // subscribed to a project but not be a member of it.
                     if ($edge_type == PhabricatorEdgeConfig::TYPE_PROJ_MEMBER) {
                         $rem = array_keys(array_diff_key($old, $new));
                     } else {
                         $rem = array();
                     }
                     // NOTE: The subscribe is "explicit" because there's no implicit
                     // unsubscribe, so Join -> Leave -> Join doesn't resubscribe you
                     // if we use an implicit subscribe, even though you never willfully
                     // unsubscribed. Not sure if adding implicit unsubscribe (which
                     // would not write the unsubscribe row) is justified to deal with
                     // this, which is a fairly weird edge case and pretty arguable both
                     // ways.
                     // Subscriptions caused by watches should also clearly be explicit,
                     // and that case is unambiguous.
                     id(new PhabricatorSubscriptionsEditor())->setActor($this->requireActor())->setObject($object)->subscribeExplicit($add)->unsubscribe($rem)->save();
                     if ($rem) {
                         // When removing members, also remove any watches on the project.
                         $edge_editor = new PhabricatorEdgeEditor();
                         foreach ($rem as $rem_phid) {
                             $edge_editor->removeEdge($object->getPHID(), PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER, $rem_phid);
                         }
                         $edge_editor->save();
                     }
                     break;
             }
             return;
     }
     return parent::applyCustomExternalTransaction($object, $xaction);
 }
Пример #19
0
 public function processRequest()
 {
     $request = $this->getRequest();
     $user = $request->getUser();
     $e_name = null;
     $e_bloggers = null;
     $errors = array();
     if ($this->isBlogEdit()) {
         $blogs = id(new PhameBlogQuery())->withPHIDs(array($this->getBlogPHID()))->execute();
         $blog = reset($blogs);
         if (empty($blog)) {
             return new Aphront404Response();
         }
         $bloggers = $blog->loadBloggers()->getBloggers();
         // TODO -- make this check use a policy
         if (!isset($bloggers[$user->getPHID()]) && !$user->isAdmin()) {
             return new Aphront403Response();
         }
         $blogger_tokens = mpull($bloggers, 'getFullName', 'getPHID');
         $submit_button = 'Save Changes';
         $delete_button = javelin_render_tag('a', array('href' => $blog->getDeleteURI(), 'class' => 'grey button', 'sigil' => 'workflow'), 'Delete Blog');
         $page_title = 'Edit Blog';
     } else {
         $blog = id(new PhameBlog())->setCreatorPHID($user->getPHID());
         $blogger_tokens = array($user->getPHID() => $user->getFullName());
         $submit_button = 'Create Blog';
         $delete_button = null;
         $page_title = 'Create Blog';
     }
     if ($request->isFormPost()) {
         $saved = true;
         $name = $request->getStr('name');
         $description = $request->getStr('description');
         $blogger_arr = $request->getArr('bloggers');
         if (empty($blogger_arr)) {
             $error = 'Bloggers must be nonempty.';
             if ($this->isBlogEdit()) {
                 $error .= ' To delete the blog, use the delete button.';
             } else {
                 $error .= ' A blog cannot exist without bloggers.';
             }
             $e_bloggers = 'Required';
             $errors[] = $error;
         }
         $new_bloggers = array_values($blogger_arr);
         if ($this->isBlogEdit()) {
             $old_bloggers = array_keys($blogger_tokens);
         } else {
             $old_bloggers = array();
         }
         if (empty($name)) {
             $errors[] = 'Name must be nonempty.';
             $e_name = 'Required';
         }
         $blog->setName($name);
         $blog->setDescription($description);
         if (empty($errors)) {
             $blog->save();
             $add_phids = $new_bloggers;
             $rem_phids = array_diff($old_bloggers, $new_bloggers);
             $editor = new PhabricatorEdgeEditor();
             $edge_type = PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER;
             $editor->setUser($user);
             foreach ($add_phids as $phid) {
                 $editor->addEdge($blog->getPHID(), $edge_type, $phid);
             }
             foreach ($rem_phids as $phid) {
                 $editor->removeEdge($blog->getPHID(), $edge_type, $phid);
             }
             $editor->save();
         } else {
             $saved = false;
         }
         if ($saved) {
             $uri = new PhutilURI($blog->getViewURI());
             $uri->setQueryParam('new', true);
             return id(new AphrontRedirectResponse())->setURI($uri);
         }
     }
     $panel = new AphrontPanelView();
     $panel->setHeader($page_title);
     $panel->setWidth(AphrontPanelView::WIDTH_FULL);
     if ($delete_button) {
         $panel->addButton($delete_button);
     }
     $remarkup_reference = phutil_render_tag('a', array('href' => PhabricatorEnv::getDoclink('article/Remarkup_Reference.html'), 'tabindex' => '-1', 'target' => '_blank'), 'Formatting Reference');
     $form = id(new AphrontFormView())->setUser($user)->appendChild(id(new AphrontFormTextControl())->setLabel('Name')->setName('name')->setValue($blog->getName())->setID('blog-name')->setError($e_name))->appendChild(id(new AphrontFormTextAreaControl())->setLabel('Description')->setName('description')->setValue($blog->getDescription())->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)->setID('blog-description')->setCaption($remarkup_reference))->appendChild(id(new AphrontFormTokenizerControl())->setLabel('Bloggers')->setName('bloggers')->setValue($blogger_tokens)->setUser($user)->setDatasource('/typeahead/common/users/')->setError($e_bloggers))->appendChild(id(new AphrontFormSubmitControl())->addCancelButton('/phame/blog/')->setValue($submit_button));
     $panel->appendChild($form);
     if ($errors) {
         $error_view = id(new AphrontErrorView())->setTitle('Errors saving blog.')->setErrors($errors);
     } else {
         $error_view = null;
     }
     $this->setShowSideNav(true);
     return $this->buildStandardPageResponse(array($error_view, $panel), array('title' => $page_title));
 }
 /**
  * @task files
  */
 private function attachFiles(PhabricatorLiskDAO $object, array $file_phids)
 {
     if (!$file_phids) {
         return;
     }
     $editor = new PhabricatorEdgeEditor();
     $src = $object->getPHID();
     $type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
     foreach ($file_phids as $dst) {
         $editor->addEdge($src, $type, $dst);
     }
     $editor->save();
 }
 public function applyTransactions(array $transactions)
 {
     assert_instances_of($transactions, 'PhabricatorProjectTransaction');
     if (!$this->user) {
         throw new Exception('Call setUser() before save()!');
     }
     $user = $this->user;
     $project = $this->project;
     $is_new = !$project->getID();
     if ($is_new) {
         $project->setAuthorPHID($user->getPHID());
     }
     foreach ($transactions as $key => $xaction) {
         $this->setTransactionOldValue($project, $xaction);
         if (!$this->transactionHasEffect($xaction)) {
             unset($transactions[$key]);
             continue;
         }
     }
     if (!$is_new) {
         // You must be able to view a project in order to edit it in any capacity.
         PhabricatorPolicyFilter::requireCapability($user, $project, PhabricatorPolicyCapability::CAN_VIEW);
         $need_edit = false;
         $need_join = false;
         foreach ($transactions as $key => $xaction) {
             if ($this->getTransactionRequiresEditCapability($xaction)) {
                 $need_edit = true;
             }
             if ($this->getTransactionRequiresJoinCapability($xaction)) {
                 $need_join = true;
             }
         }
         if ($need_edit) {
             PhabricatorPolicyFilter::requireCapability($user, $project, PhabricatorPolicyCapability::CAN_EDIT);
         }
         if ($need_join) {
             PhabricatorPolicyFilter::requireCapability($user, $project, PhabricatorPolicyCapability::CAN_JOIN);
         }
     }
     if (!$transactions) {
         return $this;
     }
     foreach ($transactions as $xaction) {
         $this->applyTransactionEffect($project, $xaction);
     }
     try {
         $project->openTransaction();
         $project->save();
         $edge_type = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
         $editor = new PhabricatorEdgeEditor();
         $editor->setUser($this->user);
         foreach ($this->remEdges as $phid) {
             $editor->removeEdge($project->getPHID(), $edge_type, $phid);
         }
         foreach ($this->addEdges as $phid) {
             $editor->addEdge($project->getPHID(), $edge_type, $phid);
         }
         $editor->save();
         foreach ($transactions as $xaction) {
             $xaction->setAuthorPHID($user->getPHID());
             $xaction->setProjectID($project->getID());
             $xaction->save();
         }
         $project->saveTransaction();
         foreach ($transactions as $xaction) {
             $this->publishTransactionStory($project, $xaction);
         }
     } catch (AphrontQueryDuplicateKeyException $ex) {
         // We already validated the slug, but might race. Try again to see if
         // that's the issue. If it is, we'll throw a more specific exception. If
         // not, throw the original exception.
         $this->validateName($project);
         throw $ex;
     }
     // TODO: If we rename a project, we should move its Phriction page. Do
     // that once Phriction supports document moves.
     return $this;
 }