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; }
/** * 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; }
<?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";
* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ echo "Migrating task dependencies to edges...\n"; foreach (new LiskMigrationIterator(new ManiphestTask()) as $task) { $id = $task->getID(); echo "Task {$id}: "; $deps = $task->getAttachedPHIDs(PhabricatorPHIDConstants::PHID_TYPE_TASK); if (!$deps) { echo "-\n"; continue; } $editor = new PhabricatorEdgeEditor(); $editor->setSuppressEvents(true); foreach ($deps as $dep) { $editor->addEdge($task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK, $dep); } $editor->save(); echo "OKAY\n"; } echo "Done.\n";
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; } }
/** * @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(); }
<?php echo pht('Migrating differential dependencies to edges...') . "\n"; $table = new DifferentialRevision(); $table->openTransaction(); foreach (new LiskMigrationIterator($table) as $rev) { $id = $rev->getID(); echo pht('Revision %d: ', $id); $deps = $rev->getAttachedPHIDs(DifferentialRevisionPHIDType::TYPECONST); if (!$deps) { echo "-\n"; continue; } $editor = new PhabricatorEdgeEditor(); foreach ($deps as $dep) { $editor->addEdge($rev->getPHID(), DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, $dep); } $editor->save(); echo pht('OKAY') . "\n"; } $table->saveTransaction(); echo pht('Done.') . "\n";
<?php $table = new DifferentialRevision(); $conn_w = $table->establishConnection('w'); // NOTE: We migrate by revision because the relationship table doesn't have // an "id" column. foreach (new LiskMigrationIterator($table) as $revision) { $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); echo "Migrating reviewers for D{$revision_id}...\n"; $reviewer_phids = queryfx_all($conn_w, 'SELECT objectPHID FROM %T WHERE revisionID = %d AND relation = %s ORDER BY sequence', 'differential_relationship', $revision_id, 'revw'); $reviewer_phids = ipull($reviewer_phids, 'objectPHID'); if (!$reviewer_phids) { continue; } $editor = new PhabricatorEdgeEditor(); foreach ($reviewer_phids as $dst) { if (phid_get_type($dst) == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { // At least one old install ran into some issues here. Skip the row if we // can't figure out what the destination PHID is. See here: // https://github.com/phacility/phabricator/pull/507 continue; } $editor->addEdge($revision_phid, PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $dst, array('data' => array('status' => DifferentialReviewerStatus::STATUS_ADDED))); } $editor->save(); } echo "Done.\n";
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)); }
<?php echo pht('Migrating project members to edges...') . "\n"; $table = new PhabricatorProject(); $table->establishConnection('w'); foreach (new LiskMigrationIterator($table) as $proj) { $id = $proj->getID(); echo pht('Project %d: ', $id); $members = queryfx_all($proj->establishConnection('w'), 'SELECT userPHID FROM %T WHERE projectPHID = %s', 'project_affiliation', $proj->getPHID()); if (!$members) { echo "-\n"; continue; } $members = ipull($members, 'userPHID'); $editor = new PhabricatorEdgeEditor(); foreach ($members as $user_phid) { $editor->addEdge($proj->getPHID(), PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $user_phid); } $editor->save(); echo pht('OKAY') . "\n"; } echo pht('Done.') . "\n";
<?php $table = new DifferentialRevision(); $conn_w = $table->establishConnection('w'); // NOTE: We migrate by revision because the relationship table doesn't have // an "id" column. foreach (new LiskMigrationIterator($table) as $revision) { $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); echo pht('Migrating reviewers for %s...', "D{$revision_id}") . "\n"; $reviewer_phids = queryfx_all($conn_w, 'SELECT objectPHID FROM %T WHERE revisionID = %d AND relation = %s ORDER BY sequence', 'differential_relationship', $revision_id, 'revw'); $reviewer_phids = ipull($reviewer_phids, 'objectPHID'); if (!$reviewer_phids) { continue; } $editor = new PhabricatorEdgeEditor(); foreach ($reviewer_phids as $dst) { if (phid_get_type($dst) == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { // At least one old install ran into some issues here. Skip the row if we // can't figure out what the destination PHID is. See here: // https://github.com/phacility/phabricator/pull/507 continue; } $editor->addEdge($revision_phid, DifferentialRevisionHasReviewerEdgeType::EDGECONST, $dst, array('data' => array('status' => DifferentialReviewerStatus::STATUS_ADDED))); } $editor->save(); } echo pht('Done.') . "\n";
/** * 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)); } }
<?php echo "Migrating task revisions to edges...\n"; $table = new ManiphestTask(); $table->establishConnection('w'); foreach (new LiskMigrationIterator($table) as $task) { $id = $task->getID(); echo "Task {$id}: "; $revs = $task->getAttachedPHIDs(DifferentialRevisionPHIDType::TYPECONST); if (!$revs) { echo "-\n"; continue; } $editor = new PhabricatorEdgeEditor(); foreach ($revs as $rev) { $editor->addEdge($task->getPHID(), ManiphestTaskHasRevisionEdgeType::EDGECONST, $rev); } $editor->save(); echo "OKAY\n"; } echo "Done.\n";
* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ echo "Migrating task revisions to edges...\n"; foreach (new LiskMigrationIterator(new ManiphestTask()) as $task) { $id = $task->getID(); echo "Task {$id}: "; $revs = $task->getAttachedPHIDs(PhabricatorPHIDConstants::PHID_TYPE_DREV); if (!$revs) { echo "-\n"; continue; } $editor = new PhabricatorEdgeEditor(); $editor->setSuppressEvents(true); foreach ($revs as $rev) { $editor->addEdge($task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV, $rev); } $editor->save(); echo "OKAY\n"; } echo "Done.\n";
<?php echo pht('Migrating task dependencies to edges...') . "\n"; $table = new ManiphestTask(); $table->openTransaction(); foreach (new LiskMigrationIterator($table) as $task) { $id = $task->getID(); echo pht('Task %d: ', $id); $deps = $task->getAttachedPHIDs(ManiphestTaskPHIDType::TYPECONST); if (!$deps) { echo "-\n"; continue; } $editor = new PhabricatorEdgeEditor(); foreach ($deps as $dep) { $editor->addEdge($task->getPHID(), ManiphestTaskDependsOnTaskEdgeType::EDGECONST, $dep); } $editor->save(); echo pht('OKAY') . "\n"; } $table->saveTransaction(); echo pht('Done.') . "\n";
* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ echo "Migrating project members to edges...\n"; foreach (new LiskMigrationIterator(new PhabricatorProject()) as $proj) { $id = $proj->getID(); echo "Project {$id}: "; $members = queryfx_all($proj->establishConnection('r'), 'SELECT userPHID FROM %T WHERE projectPHID = %s', 'project_affiliation', $proj->getPHID()); if (!$members) { echo "-\n"; continue; } $members = ipull($members, 'userPHID'); $editor = new PhabricatorEdgeEditor(); $editor->setSuppressEvents(true); foreach ($members as $user_phid) { $editor->addEdge($proj->getPHID(), PhabricatorEdgeConfig::TYPE_PROJ_MEMBER, $user_phid); } $editor->save(); echo "OKAY\n"; } echo "Done.\n";
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(); }
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; }
$edge_type = PhabricatorSubscribedToObjectEdgeType::EDGECONST; $edge_inverse = PhabricatorObjectHasSubscriberEdgeType::EDGECONST; $map = PhabricatorPHIDType::getAllTypes(); foreach ($map as $type => $spec) { try { $object = $spec->newObject(); if (!$object) { continue; } $object_conn_w = $object->establishConnection('w'); queryfx($object_conn_w, 'UPDATE %T SET dst = %s WHERE dst = %s AND type = %s', PhabricatorEdgeConfig::TABLE_NAME_EDGE, $new_phid, $old_phid, $edge_inverse); } catch (Exception $ex) { // Just ignore these; they're mostly tables not existing. continue; } } try { $dst_phids = queryfx_all($conn_w, 'SELECT dst FROM %T WHERE src = %s AND type = %s', PhabricatorEdgeConfig::TABLE_NAME_EDGE, $old_phid, $edge_type); if ($dst_phids) { $editor = new PhabricatorEdgeEditor(); foreach ($dst_phids as $dst_phid) { $editor->addEdge($new_phid, $edge_type, $dst_phid['dst']); } $editor->save(); } } catch (Exception $ex) { echo pht('Unable to migrate some inverse edges for mailing list "%s": %s.', $name, $ex->getMessage()) . "\n"; continue; } echo pht('Migrated mailing list "%s" to mailing list user "%s".', $name, $user->getUsername()) . "\n"; }
<?php echo pht('Migrating Differential unsubscribed users to edges...') . "\n"; $table = new DifferentialRevision(); $table->openTransaction(); // We couldn't use new LiskMigrationIterator($table) because the $unsubscribed // property gets deleted. $revs = queryfx_all($table->establishConnection('w'), 'SELECT id, phid, unsubscribed FROM differential_revision'); foreach ($revs as $rev) { echo '.'; $unsubscribed = json_decode($rev['unsubscribed']); if (!$unsubscribed) { continue; } $editor = new PhabricatorEdgeEditor(); foreach ($unsubscribed as $user_phid => $_) { $editor->addEdge($rev['phid'], PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST, $user_phid); } $editor->save(); } $table->saveTransaction(); echo pht('Done.') . "\n";
<?php echo "Migrating differential dependencies to edges...\n"; $table = new DifferentialRevision(); $table->openTransaction(); foreach (new LiskMigrationIterator($table) as $rev) { $id = $rev->getID(); echo "Revision {$id}: "; $deps = $rev->getAttachedPHIDs(DifferentialRevisionPHIDType::TYPECONST); if (!$deps) { echo "-\n"; continue; } $editor = new PhabricatorEdgeEditor(); foreach ($deps as $dep) { $editor->addEdge($rev->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV, $dep); } $editor->save(); echo "OKAY\n"; } $table->saveTransaction(); echo "Done.\n";
<?php $table = new PhabricatorFileImageMacro(); foreach (new LiskMigrationIterator($table) as $macro) { $name = $macro->getName(); echo "Linking macro '{$name}'...\n"; $editor = new PhabricatorEdgeEditor(); $phids[] = $macro->getFilePHID(); $phids[] = $macro->getAudioPHID(); $phids = array_filter($phids); if ($phids) { foreach ($phids as $phid) { $editor->addEdge($macro->getPHID(), PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE, $phid); } $editor->save(); } } echo "Done.\n";
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; } }
<?php $table = new PhabricatorFileImageMacro(); foreach (new LiskMigrationIterator($table) as $macro) { $name = $macro->getName(); echo pht("Linking macro '%s'...", $name) . "\n"; $editor = new PhabricatorEdgeEditor(); $phids[] = $macro->getFilePHID(); $phids[] = $macro->getAudioPHID(); $phids = array_filter($phids); if ($phids) { foreach ($phids as $phid) { $editor->addEdge($macro->getPHID(), PhabricatorObjectHasFileEdgeType::EDGECONST, $phid); } $editor->save(); } } echo pht('Done.') . "\n";
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; } }
/** * @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; }