diff --git a/resources/sql/autopatches/20180207.mail.01.task.sql b/resources/sql/autopatches/20180207.mail.01.task.sql new file mode 100644 index 0000000000..f04b90c809 --- /dev/null +++ b/resources/sql/autopatches/20180207.mail.01.task.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + DROP originalTitle; diff --git a/resources/sql/autopatches/20180207.mail.02.revision.sql b/resources/sql/autopatches/20180207.mail.02.revision.sql new file mode 100644 index 0000000000..881efbcc94 --- /dev/null +++ b/resources/sql/autopatches/20180207.mail.02.revision.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_revision + DROP originalTitle; diff --git a/resources/sql/autopatches/20180207.mail.03.mock.sql b/resources/sql/autopatches/20180207.mail.03.mock.sql new file mode 100644 index 0000000000..360d7cf9a7 --- /dev/null +++ b/resources/sql/autopatches/20180207.mail.03.mock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_pholio.pholio_mock + DROP originalName; diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 2c82de164a..938c588857 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,1065 +1,1055 @@ setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { $initial_state = DifferentialRevisionStatus::DRAFT; } else { $initial_state = DifferentialRevisionStatus::NEEDS_REVIEW; } return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) ->attachRepository(null) ->attachActiveDiff(null) ->attachReviewers(array()) ->setModernRevisionStatus($initial_state); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', - 'originalTitle' => 'text255', 'status' => 'text32', 'summary' => 'text', 'testPlan' => 'text', 'authorPHID' => 'phid?', 'lastReviewerPHID' => 'phid?', 'lineCount' => 'uint32?', 'mailKey' => 'bytes40', 'branchName' => 'text255?', 'repositoryPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'repositoryPHID' => array( 'columns' => array('repositoryPHID'), ), // If you (or a project you are a member of) is reviewing a significant // fraction of the revisions on an install, the result set of open // revisions may be smaller than the result set of revisions where you // are a reviewer. In these cases, this key is better than keys on the // edge table. 'key_status' => array( 'columns' => array('status', 'phid'), ), ), ) + parent::getConfiguration(); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function hasRevisionProperty($key) { return array_key_exists($key, $this->properties); } public function getMonogram() { $id = $this->getID(); return "D{$id}"; } public function getURI() { return '/'.$this->getMonogram(); } - public function setTitle($title) { - $this->title = $title; - if (!$this->getID()) { - $this->originalTitle = $title; - } - return $this; - } - public function loadIDsByCommitPHIDs($phids) { if (!$phids) { return array(); } $revision_ids = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE commitPHID IN (%Ls)', self::TABLE_COMMIT, $phids); return ipull($revision_ids, 'revisionID', 'commitPHID'); } public function loadCommitPHIDs() { if (!$this->getID()) { return ($this->commits = array()); } $commits = queryfx_all( $this->establishConnection('r'), 'SELECT commitPHID FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $commits = ipull($commits, 'commitPHID'); return ($this->commits = $commits); } public function getCommitPHIDs() { return $this->assertAttached($this->commits); } public function getActiveDiff() { // TODO: Because it's currently technically possible to create a revision // without an associated diff, we allow an attached-but-null active diff. // It would be good to get rid of this once we make diff-attaching // transactional. return $this->assertAttached($this->activeDiff); } public function attachActiveDiff($diff) { $this->activeDiff = $diff; return $this; } public function getDiffIDs() { return $this->assertAttached($this->diffIDs); } public function attachDiffIDs(array $ids) { rsort($ids); $this->diffIDs = array_values($ids); return $this; } public function attachCommitPHIDs(array $phids) { $this->commits = array_values($phids); return $this; } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function setAttachedPHIDs($type, array $phids) { $this->attached[$type] = array_fill_keys($phids, array()); return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialRevisionPHIDType::TYPECONST); } public function loadActiveDiff() { return id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d ORDER BY id DESC LIMIT 1', $this->getID()); } public function save() { if (!$this->getMailKey()) { $this->mailKey = Filesystem::readRandomCharacters(40); } return parent::save(); } public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } public function canReviewerForceAccept( PhabricatorUser $viewer, DifferentialReviewer $reviewer) { if (!$reviewer->isPackage()) { return false; } $map = $this->getReviewerForceAcceptMap($viewer); if (!$map) { return false; } if (isset($map[$reviewer->getReviewerPHID()])) { return true; } return false; } private function getReviewerForceAcceptMap(PhabricatorUser $viewer) { $fragment = $viewer->getCacheFragment(); if (!array_key_exists($fragment, $this->forceMap)) { $map = $this->newReviewerForceAcceptMap($viewer); $this->forceMap[$fragment] = $map; } return $this->forceMap[$fragment]; } private function newReviewerForceAcceptMap(PhabricatorUser $viewer) { $diff = $this->getActiveDiff(); if (!$diff) { return null; } $repository_phid = $diff->getRepositoryPHID(); if (!$repository_phid) { return null; } $paths = array(); try { $changesets = $diff->getChangesets(); } catch (Exception $ex) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($viewer) ->withDiffs(array($diff)) ->execute(); } foreach ($changesets as $changeset) { $paths[] = $changeset->getOwnersFilename(); } if (!$paths) { return null; } $reviewer_phids = array(); foreach ($this->getReviewers() as $reviewer) { if (!$reviewer->isPackage()) { continue; } $reviewer_phids[] = $reviewer->getReviewerPHID(); } if (!$reviewer_phids) { return null; } // Load all the reviewing packages which have control over some of the // paths in the change. These are packages which the actor may be able // to force-accept on behalf of. $control_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withPHIDs($reviewer_phids) ->withControl($repository_phid, $paths); $control_packages = $control_query->execute(); if (!$control_packages) { return null; } // Load all the packages which have potential control over some of the // paths in the change and are owned by the actor. These are packages // which the actor may be able to use their authority over to gain the // ability to force-accept for other packages. This query doesn't apply // dominion rules yet, and we'll bypass those rules later on. $authority_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withAuthorityPHIDs(array($viewer->getPHID())) ->withControl($repository_phid, $paths); $authority_packages = $authority_query->execute(); if (!$authority_packages) { return null; } $authority_packages = mpull($authority_packages, null, 'getPHID'); // Build a map from each path in the revision to the reviewer packages // which control it. $control_map = array(); foreach ($paths as $path) { $control_packages = $control_query->getControllingPackagesForPath( $repository_phid, $path); // Remove packages which the viewer has authority over. We don't need // to check these for force-accept because they can just accept them // normally. $control_packages = mpull($control_packages, null, 'getPHID'); foreach ($control_packages as $phid => $control_package) { if (isset($authority_packages[$phid])) { unset($control_packages[$phid]); } } if (!$control_packages) { continue; } $control_map[$path] = $control_packages; } if (!$control_map) { return null; } // From here on out, we only care about paths which we have at least one // controlling package for. $paths = array_keys($control_map); // Now, build a map from each path to the packages which would control it // if there were no dominion rules. $authority_map = array(); foreach ($paths as $path) { $authority_packages = $authority_query->getControllingPackagesForPath( $repository_phid, $path, $ignore_dominion = true); $authority_map[$path] = mpull($authority_packages, null, 'getPHID'); } // For each path, find the most general package that the viewer has // authority over. For example, we'll prefer a package that owns "/" to a // package that owns "/src/". $force_map = array(); foreach ($authority_map as $path => $package_map) { $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); // Find the package that we have authority over which has the most // general match for this path. $best_match = null; $best_package = null; foreach ($package_map as $package_phid => $package) { $package_paths = $package->getPathsForRepository($repository_phid); foreach ($package_paths as $package_path) { // NOTE: A strength of 0 means "no match". A strength of 1 means // that we matched "/", so we can not possibly find another stronger // match. $strength = $package_path->getPathMatchStrength( $path_fragments, $fragment_count); if (!$strength) { continue; } if ($strength < $best_match || !$best_package) { $best_match = $strength; $best_package = $package; if ($strength == 1) { break 2; } } } } if ($best_package) { $force_map[$path] = array( 'strength' => $best_match, 'package' => $best_package, ); } } // For each path which the viewer owns a package for, find other packages // which that authority can be used to force-accept. Once we find a way to // force-accept a package, we don't need to keep looking. $has_control = array(); foreach ($force_map as $path => $spec) { $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); $authority_strength = $spec['strength']; $control_packages = $control_map[$path]; foreach ($control_packages as $control_phid => $control_package) { if (isset($has_control[$control_phid])) { continue; } $control_paths = $control_package->getPathsForRepository( $repository_phid); foreach ($control_paths as $control_path) { $strength = $control_path->getPathMatchStrength( $path_fragments, $fragment_count); if (!$strength) { continue; } if ($strength > $authority_strength) { $authority = $spec['package']; $has_control[$control_phid] = array( 'authority' => $authority, 'phid' => $authority->getPHID(), ); break; } } } } // Return a map from packages which may be force accepted to the packages // which permit that forced acceptance. return ipull($has_control, 'phid'); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A revision's author (which effectively means "owner" after we added // commandeering) can always view and edit it. $author_phid = $this->getAuthorPHID(); if ($author_phid) { if ($user->getPHID() == $author_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { $description = array( pht('The owner of a revision can always view and edit it.'), ); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $description[] = pht( 'If a revision belongs to a repository, other users must be able '. 'to view the repository in order to view the revision.'); break; } return $description; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $repository_phid = $this->getRepositoryPHID(); $repository = $this->getRepository(); // Try to use the object if we have it, since it will save us some // data fetching later on. In some cases, we might not have it. $repository_ref = nonempty($repository, $repository_phid); if ($repository_ref) { $extended[] = array( $repository_ref, PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } public function getReviewers() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewers(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); $reviewers = mpull($reviewers, null, 'getReviewerPHID'); $this->reviewerStatus = $reviewers; return $this; } public function getReviewerPHIDs() { $reviewers = $this->getReviewers(); return mpull($reviewers, 'getReviewerPHID'); } public function getReviewerPHIDsForEdit() { $reviewers = $this->getReviewers(); $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; $value = array(); foreach ($reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); if ($reviewer->getReviewerStatus() == $status_blocking) { $value[] = 'blocking('.$phid.')'; } else { $value[] = $phid; } } return $value; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } public function setModernRevisionStatus($status) { return $this->setStatus($status); } public function getModernRevisionStatus() { return $this->getStatus(); } public function getLegacyRevisionStatus() { return $this->getStatusObject()->getLegacyKey(); } public function isClosed() { return $this->getStatusObject()->isClosedStatus(); } public function isAbandoned() { return $this->getStatusObject()->isAbandoned(); } public function isAccepted() { return $this->getStatusObject()->isAccepted(); } public function isNeedsReview() { return $this->getStatusObject()->isNeedsReview(); } public function isNeedsRevision() { return $this->getStatusObject()->isNeedsRevision(); } public function isChangePlanned() { return $this->getStatusObject()->isChangePlanned(); } public function isPublished() { return $this->getStatusObject()->isPublished(); } public function isDraft() { return $this->getStatusObject()->isDraft(); } public function getStatusIcon() { return $this->getStatusObject()->getIcon(); } public function getStatusDisplayName() { return $this->getStatusObject()->getDisplayName(); } public function getStatusIconColor() { return $this->getStatusObject()->getIconColor(); } public function getStatusObject() { $status = $this->getStatus(); return DifferentialRevisionStatus::newForStatus($status); } public function getFlag(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->flags, $viewer->getPHID()); } public function attachFlag( PhabricatorUser $viewer, PhabricatorFlag $flag = null) { $this->flags[$viewer->getPHID()] = $flag; return $this; } public function getHasDraft(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment()); } public function attachHasDraft(PhabricatorUser $viewer, $has_draft) { $this->drafts[$viewer->getCacheFragment()] = $has_draft; return $this; } public function shouldBroadcast() { if (!$this->isDraft()) { return true; } return false; } public function getHoldAsDraft() { return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false); } public function setHoldAsDraft($hold) { return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold); } public function getHasBroadcast() { return $this->getProperty(self::PROPERTY_HAS_BROADCAST, false); } public function setHasBroadcast($has_broadcast) { return $this->setProperty(self::PROPERTY_HAS_BROADCAST, $has_broadcast); } public function setAddedLineCount($count) { return $this->setProperty(self::PROPERTY_LINES_ADDED, $count); } public function getAddedLineCount() { return $this->getProperty(self::PROPERTY_LINES_ADDED); } public function setRemovedLineCount($count) { return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count); } public function getRemovedLineCount() { return $this->getProperty(self::PROPERTY_LINES_REMOVED); } public function loadActiveBuilds(PhabricatorUser $viewer) { $diff = $this->getActiveDiff(); $buildables = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withContainerPHIDs(array($this->getPHID())) ->withBuildablePHIDs(array($diff->getPHID())) ->withManualBuildables(false) ->execute(); if (!$buildables) { return array(); } return id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs(mpull($buildables, 'getPHID')) ->withAutobuilds(false) ->withBuildStatuses( array( HarbormasterBuildStatus::STATUS_INACTIVE, HarbormasterBuildStatus::STATUS_PENDING, HarbormasterBuildStatus::STATUS_BUILDING, HarbormasterBuildStatus::STATUS_FAILED, HarbormasterBuildStatus::STATUS_ABORTED, HarbormasterBuildStatus::STATUS_ERROR, HarbormasterBuildStatus::STATUS_PAUSED, HarbormasterBuildStatus::STATUS_DEADLOCKED, )) ->execute(); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterContainerPHID(); } public function getHarbormasterBuildablePHID() { return $this->loadActiveDiff()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getPHID(); } public function getHarbormasterPublishablePHID() { return $this->getPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { if ($phid == $this->getAuthorPHID()) { return true; } // TODO: This only happens when adding or removing CCs, and is safe from a // policy perspective, but the subscription pathway should have some // opportunity to load this data properly. For now, this is the only case // where implicit subscription is not an intrinsic property of the object. if ($this->reviewerStatus == self::ATTACHABLE) { $reviewers = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->needReviewers(true) ->executeOne() ->getReviewers(); } else { $reviewers = $this->getReviewers(); } foreach ($reviewers as $reviewer) { if ($reviewer->getReviewerPHID() == $phid) { return true; } } return false; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('differential.fields'); } public function getCustomFieldBaseClass() { return 'DifferentialCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DifferentialTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { $viewer = $request->getViewer(); $render_data = $timeline->getRenderData(); $left = $request->getInt('left', idx($render_data, 'left')); $right = $request->getInt('right', idx($render_data, 'right')); $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withIDs(array($left, $right)) ->execute(); $diffs = mpull($diffs, null, 'getID'); $left_diff = $diffs[$left]; $right_diff = $diffs[$right]; $old_ids = $request->getStr('old', idx($render_data, 'old')); $new_ids = $request->getStr('new', idx($render_data, 'new')); $old_ids = array_filter(explode(',', $old_ids)); $new_ids = array_filter(explode(',', $new_ids)); $type_inline = DifferentialTransaction::TYPE_INLINE; $changeset_ids = array_merge($old_ids, $new_ids); $inlines = array(); foreach ($timeline->getTransactions() as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction->getComment(); $changeset_ids[] = $xaction->getComment()->getChangesetID(); } } if ($changeset_ids) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($request->getUser()) ->withIDs($changeset_ids) ->execute(); $changesets = mpull($changesets, null, 'getID'); } else { $changesets = array(); } foreach ($inlines as $key => $inline) { $inlines[$key] = DifferentialInlineComment::newFromModernComment( $inline); } $query = id(new DifferentialInlineCommentQuery()) ->needHidden(true) ->setViewer($viewer); // NOTE: This is a bit sketchy: this method adjusts the inlines as a // side effect, which means it will ultimately adjust the transaction // comments and affect timeline rendering. $query->adjustInlinesForChangesets( $inlines, array_select_keys($changesets, $old_ids), array_select_keys($changesets, $new_ids), $this); return $timeline ->setChangesets($changesets) ->setRevision($this) ->setLeftDiff($left_diff) ->setRightDiff($right_diff); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $diffs = id(new DifferentialDiffQuery()) ->setViewer($engine->getViewer()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($diffs as $diff) { $engine->destroyObject($diff); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); // we have to do paths a little differently as they do not have // an id or phid column for delete() to act on $dummy_path = new DifferentialAffectedPath(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $dummy_path->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DifferentialRevisionFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new DifferentialRevisionFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The revision title.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Revision author PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Information about revision status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid?') ->setDescription(pht('Revision repository PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('diffPHID') ->setType('phid') ->setDescription(pht('Active diff PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('summary') ->setType('string') ->setDescription(pht('Revision summary.')), ); } public function getFieldValuesForConduit() { $status = $this->getStatusObject(); $status_info = array( 'value' => $status->getKey(), 'name' => $status->getDisplayName(), 'closed' => $status->isClosedStatus(), 'color.ansi' => $status->getANSIColor(), ); return array( 'title' => $this->getTitle(), 'authorPHID' => $this->getAuthorPHID(), 'status' => $status_info, 'repositoryPHID' => $this->getRepositoryPHID(), 'diffPHID' => $this->getActiveDiffPHID(), 'summary' => $this->getSummary(), ); } public function getConduitSearchAttachments() { return array( id(new DifferentialReviewersSearchEngineAttachment()) ->setAttachmentKey('reviewers'), ); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DifferentialRevisionDraftEngine(); } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 9c8e3869dc..66247ca6d0 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,1060 +1,1059 @@ getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: return null; } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: return $xaction->getNewValue(); } } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: return (bool)$new; } return parent::transactionHasEffect($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: return; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COLUMNS: foreach ($xaction->getNewValue() as $move) { $this->applyBoardMove($object, $move); } break; } } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // When we change the status of a task, update tasks this tasks blocks // with a message to the effect of "alincoln resolved blocking task Txxx." $unblock_xaction = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: $unblock_xaction = $xaction; break; } } if ($unblock_xaction !== null) { $blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); if ($blocked_phids) { // In theory we could apply these through policies, but that seems a // little bit surprising. For now, use the actor's vision. $blocked_tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withPHIDs($blocked_phids) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->execute(); $old = $unblock_xaction->getOldValue(); $new = $unblock_xaction->getNewValue(); foreach ($blocked_tasks as $blocked_task) { $parent_xaction = id(new ManiphestTransaction()) ->setTransactionType( ManiphestTaskUnblockTransaction::TRANSACTIONTYPE) ->setOldValue(array($object->getPHID() => $old)) ->setNewValue(array($object->getPHID() => $new)); if ($this->getIsNewObject()) { $parent_xaction->setMetadataValue('blocker.new', true); } id(new ManiphestTransactionEditor()) ->setActor($this->getActor()) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($blocked_task, array($parent_xaction)); } } } return $xactions; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { return 'maniphest-task-'.$object->getPHID(); } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); if ($object->getOwnerPHID()) { $phids[] = $object->getOwnerPHID(); } $phids[] = $this->getActingAsPHID(); return $phids; } public function getMailTagsMap() { return array( ManiphestTransaction::MAILTAG_STATUS => pht("A task's status changes."), ManiphestTransaction::MAILTAG_OWNER => pht("A task's owner changes."), ManiphestTransaction::MAILTAG_PRIORITY => pht("A task's priority changes."), ManiphestTransaction::MAILTAG_CC => pht("A task's subscribers change."), ManiphestTransaction::MAILTAG_PROJECTS => pht("A task's associated projects change."), ManiphestTransaction::MAILTAG_UNBLOCK => pht("One of a task's subtasks changes status."), ManiphestTransaction::MAILTAG_COLUMN => pht('A task is moved between columns on a workboard.'), ManiphestTransaction::MAILTAG_COMMENT => pht('Someone comments on a task.'), ManiphestTransaction::MAILTAG_OTHER => pht('Other task activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new ManiphestReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject("T{$id}: {$title}"); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); if ($this->getIsNewObject()) { $body->addRemarkupSection( pht('TASK DESCRIPTION'), $object->getDescription()); } $body->addLinkSection( pht('TASK DETAIL'), PhabricatorEnv::getProductionURI('/T'.$object->getID())); $board_phids = array(); $type_columns = PhabricatorTransactions::TYPE_COLUMNS; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_columns) { $moves = $xaction->getNewValue(); foreach ($moves as $move) { $board_phids[] = $move['boardPHID']; } } } if ($board_phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->requireActor()) ->withPHIDs($board_phids) ->execute(); foreach ($projects as $project) { $body->addLinkSection( pht('WORKBOARD'), PhabricatorEnv::getProductionURI( '/project/board/'.$project->getID().'/')); } } return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return true; } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldManiphestTaskAdapter()) ->setTask($object); } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { parent::requireCapabilities($object, $xaction); $app_capability_map = array( ManiphestTaskPriorityTransaction::TRANSACTIONTYPE => ManiphestEditPriorityCapability::CAPABILITY, ManiphestTaskStatusTransaction::TRANSACTIONTYPE => ManiphestEditStatusCapability::CAPABILITY, ManiphestTaskOwnerTransaction::TRANSACTIONTYPE => ManiphestEditAssignCapability::CAPABILITY, PhabricatorTransactions::TYPE_EDIT_POLICY => ManiphestEditPoliciesCapability::CAPABILITY, PhabricatorTransactions::TYPE_VIEW_POLICY => ManiphestEditPoliciesCapability::CAPABILITY, ); $transaction_type = $xaction->getTransactionType(); $app_capability = null; if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) { switch ($xaction->getMetadataValue('edge:type')) { case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST: $app_capability = ManiphestEditProjectsCapability::CAPABILITY; break; } } else { $app_capability = idx($app_capability_map, $transaction_type); } if ($app_capability) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($this->getActor()) ->withClasses(array('PhabricatorManiphestApplication')) ->executeOne(); PhabricatorPolicyFilter::requireCapability( $this->getActor(), $app, $app_capability); } } protected function adjustObjectForPolicyChecks( PhabricatorLiskDAO $object, array $xactions) { $copy = parent::adjustObjectForPolicyChecks($object, $xactions); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: $copy->setOwnerPHID($xaction->getNewValue()); break; default: continue; } } return $copy; } /** * Get priorities for moving a task to a new priority. */ public static function getEdgeSubpriority( $priority, $is_end) { $query = id(new ManiphestTaskQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPriorities(array($priority)) ->setLimit(1); if ($is_end) { $query->setOrderVector(array('-priority', '-subpriority', '-id')); } else { $query->setOrderVector(array('priority', 'subpriority', 'id')); } $result = $query->executeOne(); $step = (double)(2 << 32); if ($result) { $base = $result->getSubpriority(); if ($is_end) { $sub = ($base - $step); } else { $sub = ($base + $step); } } else { $sub = 0; } return array($priority, $sub); } /** * Get priorities for moving a task before or after another task. */ public static function getAdjacentSubpriority( ManiphestTask $dst, $is_after) { $query = id(new ManiphestTaskQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) ->withPriorities(array($dst->getPriority())) ->setLimit(1); if ($is_after) { $query->setAfterID($dst->getID()); } else { $query->setBeforeID($dst->getID()); } $adjacent = $query->executeOne(); $base = $dst->getSubpriority(); $step = (double)(2 << 32); // If we find an adjacent task, we average the two subpriorities and // return the result. if ($adjacent) { $epsilon = 1.0; // If the adjacent task has a subpriority that is identical or very // close to the task we're looking at, we're going to spread out all // the nearby tasks. $adjacent_sub = $adjacent->getSubpriority(); if ((abs($adjacent_sub - $base) < $epsilon)) { $base = self::disperseBlock( $dst, $epsilon * 2); if ($is_after) { $sub = $base - $epsilon; } else { $sub = $base + $epsilon; } } else { $sub = ($adjacent_sub + $base) / 2; } } else { // Otherwise, we take a step away from the target's subpriority and // use that. if ($is_after) { $sub = ($base - $step); } else { $sub = ($base + $step); } } return array($dst->getPriority(), $sub); } /** * Distribute a cluster of tasks with similar subpriorities. */ private static function disperseBlock( ManiphestTask $task, $spacing) { $conn = $task->establishConnection('w'); // Find a block of subpriority space which is, on average, sparse enough // to hold all the tasks that are inside it with a reasonable level of // separation between them. // We'll start by looking near the target task for a range of numbers // which has more space available than tasks. For example, if the target // task has subpriority 33 and we want to separate each task by at least 1, // we might start by looking in the range [23, 43]. // If we find fewer than 20 tasks there, we have room to reassign them // with the desired level of separation. We space them out, then we're // done. // However: if we find more than 20 tasks, we don't have enough room to // distribute them. We'll widen our search and look in a bigger range, // maybe [13, 53]. This range has more space, so if we find fewer than // 40 tasks in this range we can spread them out. If we still find too // many tasks, we keep widening the search. $base = $task->getSubpriority(); $scale = 4.0; while (true) { $range = ($spacing * $scale) / 2.0; $min = ($base - $range); $max = ($base + $range); $result = queryfx_one( $conn, 'SELECT COUNT(*) N FROM %T WHERE priority = %d AND subpriority BETWEEN %f AND %f', $task->getTableName(), $task->getPriority(), $min, $max); $count = $result['N']; if ($count < $scale) { // We have found a block which we can make sparse enough, so bail and // continue below with our selection. break; } // This block had too many tasks for its size, so try again with a // bigger block. $scale *= 2.0; } $rows = queryfx_all( $conn, 'SELECT id FROM %T WHERE priority = %d AND subpriority BETWEEN %f AND %f ORDER BY priority, subpriority, id', $task->getTableName(), $task->getPriority(), $min, $max); $task_id = $task->getID(); $result = null; // NOTE: In strict mode (which we encourage enabling) we can't structure // this bulk update as an "INSERT ... ON DUPLICATE KEY UPDATE" unless we // provide default values for ALL of the columns that don't have defaults. // This is gross, but we may be moving enough rows that individual // queries are unreasonably slow. An alternate construction which might // be worth evaluating is to use "CASE". Another approach is to disable // strict mode for this query. $extra_columns = array( 'phid' => '""', 'authorPHID' => '""', 'status' => '""', 'priority' => 0, 'title' => '""', - 'originalTitle' => '""', 'description' => '""', 'dateCreated' => 0, 'dateModified' => 0, 'mailKey' => '""', 'viewPolicy' => '""', 'editPolicy' => '""', 'ownerOrdering' => '""', 'spacePHID' => '""', 'bridgedObjectPHID' => '""', 'properties' => '""', 'points' => 0, 'subtype' => '""', ); $defaults = implode(', ', $extra_columns); $sql = array(); $offset = 0; // Often, we'll have more room than we need in the range. Distribute the // tasks evenly over the whole range so that we're less likely to end up // with tasks spaced exactly the minimum distance apart, which may // get shifted again later. We have one fewer space to distribute than we // have tasks. $divisor = (double)(count($rows) - 1.0); if ($divisor > 0) { $available_distance = (($max - $min) / $divisor); } else { $available_distance = 0.0; } foreach ($rows as $row) { $subpriority = $min + ($offset * $available_distance); // If this is the task that we're spreading out relative to, keep track // of where it is ending up so we can return the new subpriority. $id = $row['id']; if ($id == $task_id) { $result = $subpriority; } $sql[] = qsprintf( $conn, '(%d, %Q, %f)', $id, $defaults, $subpriority); $offset++; } foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn, 'INSERT INTO %T (id, %Q, subpriority) VALUES %Q ON DUPLICATE KEY UPDATE subpriority = VALUES(subpriority)', $task->getTableName(), implode(', ', array_keys($extra_columns)), $chunk); } return $result; } protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { $errors = parent::validateAllTransactions($object, $xactions); if ($this->moreValidationErrors) { $errors = array_merge($errors, $this->moreValidationErrors); } return $errors; } protected function expandTransactions( PhabricatorLiskDAO $object, array $xactions) { $actor = $this->getActor(); $actor_phid = $actor->getPHID(); $results = parent::expandTransactions($object, $xactions); $is_unassigned = ($object->getOwnerPHID() === null); $any_assign = false; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) { $any_assign = true; break; } } $is_open = !$object->isClosed(); $new_status = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: $new_status = $xaction->getNewValue(); break; } } if ($new_status === null) { $is_closing = false; } else { $is_closing = ManiphestTaskStatus::isClosedStatus($new_status); } // If the task is not assigned, not being assigned, currently open, and // being closed, try to assign the actor as the owner. if ($is_unassigned && !$any_assign && $is_open && $is_closing) { $is_claim = ManiphestTaskStatus::isClaimStatus($new_status); // Don't assign the actor if they aren't a real user. // Don't claim the task if the status is configured to not claim. if ($actor_phid && $is_claim) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) ->setNewValue($actor_phid); } } // Automatically subscribe the author when they create a task. if ($this->getIsNewObject()) { if ($actor_phid) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue( array( '+' => array($actor_phid => $actor_phid), )); } } return $results; } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $results = parent::expandTransaction($object, $xaction); $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorTransactions::TYPE_COLUMNS: try { $more_xactions = $this->buildMoveTransaction($object, $xaction); foreach ($more_xactions as $more_xaction) { $results[] = $more_xaction; } } catch (Exception $ex) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), $ex->getMessage(), $xaction); $this->moreValidationErrors[] = $error; } break; case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: // If this is a no-op update, don't expand it. $old_value = $object->getOwnerPHID(); $new_value = $xaction->getNewValue(); if ($old_value === $new_value) { continue; } // When a task is reassigned, move the old owner to the subscriber // list so they're still in the loop. if ($old_value) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setIgnoreOnNoEffect(true) ->setNewValue( array( '+' => array($old_value => $old_value), )); } break; } return $results; } private function buildMoveTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $new = $xaction->getNewValue(); if (!is_array($new)) { $this->validateColumnPHID($new); $new = array($new); } $nearby_phids = array(); foreach ($new as $key => $value) { if (!is_array($value)) { $this->validateColumnPHID($value); $value = array( 'columnPHID' => $value, ); } PhutilTypeSpec::checkMap( $value, array( 'columnPHID' => 'string', 'beforePHID' => 'optional string', 'afterPHID' => 'optional string', )); $new[$key] = $value; if (!empty($value['beforePHID'])) { $nearby_phids[] = $value['beforePHID']; } if (!empty($value['afterPHID'])) { $nearby_phids[] = $value['afterPHID']; } } if ($nearby_phids) { $nearby_objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withPHIDs($nearby_phids) ->execute(); $nearby_objects = mpull($nearby_objects, null, 'getPHID'); } else { $nearby_objects = array(); } $column_phids = ipull($new, 'columnPHID'); if ($column_phids) { $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($this->getActor()) ->withPHIDs($column_phids) ->execute(); $columns = mpull($columns, null, 'getPHID'); } else { $columns = array(); } $board_phids = mpull($columns, 'getProjectPHID'); $object_phid = $object->getPHID(); $object_phids = $nearby_phids; // Note that we may not have an object PHID if we're creating a new // object. if ($object_phid) { $object_phids[] = $object_phid; } if ($object_phids) { $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($this->getActor()) ->setBoardPHIDs($board_phids) ->setObjectPHIDs($object_phids) ->setFetchAllBoards(true) ->executeLayout(); } foreach ($new as $key => $spec) { $column_phid = $spec['columnPHID']; $column = idx($columns, $column_phid); if (!$column) { throw new Exception( pht( 'Column move transaction specifies column PHID "%s", but there '. 'is no corresponding column with this PHID.', $column_phid)); } $board_phid = $column->getProjectPHID(); $nearby = array(); if (!empty($spec['beforePHID'])) { $nearby['beforePHID'] = $spec['beforePHID']; } if (!empty($spec['afterPHID'])) { $nearby['afterPHID'] = $spec['afterPHID']; } if (count($nearby) > 1) { throw new Exception( pht( 'Column move transaction moves object to multiple positions. '. 'Specify only "beforePHID" or "afterPHID", not both.')); } foreach ($nearby as $where => $nearby_phid) { if (empty($nearby_objects[$nearby_phid])) { throw new Exception( pht( 'Column move transaction specifies object "%s" as "%s", but '. 'there is no corresponding object with this PHID.', $object_phid, $where)); } $nearby_columns = $layout_engine->getObjectColumns( $board_phid, $nearby_phid); $nearby_columns = mpull($nearby_columns, null, 'getPHID'); if (empty($nearby_columns[$column_phid])) { throw new Exception( pht( 'Column move transaction specifies object "%s" as "%s" in '. 'column "%s", but this object is not in that column!', $nearby_phid, $where, $column_phid)); } } if ($object_phid) { $old_columns = $layout_engine->getObjectColumns( $board_phid, $object_phid); $old_column_phids = mpull($old_columns, 'getPHID'); } else { $old_column_phids = array(); } $spec += array( 'boardPHID' => $board_phid, 'fromColumnPHIDs' => $old_column_phids, ); // Check if the object is already in this column, and isn't being moved. // We can just drop this column change if it has no effect. $from_map = array_fuse($spec['fromColumnPHIDs']); $already_here = isset($from_map[$column_phid]); $is_reordering = (bool)$nearby; if ($already_here && !$is_reordering) { unset($new[$key]); } else { $new[$key] = $spec; } } $new = array_values($new); $xaction->setNewValue($new); $more = array(); // If we're moving the object into a column and it does not already belong // in the column, add the appropriate board. For normal columns, this // is the board PHID. For proxy columns, it is the proxy PHID, unless the // object is already a member of some descendant of the proxy PHID. // The major case where this can happen is moves via the API, but it also // happens when a user drags a task from the "Backlog" to a milestone // column. if ($object_phid) { $current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object_phid, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $current_phids = array_fuse($current_phids); } else { $current_phids = array(); } $add_boards = array(); foreach ($new as $move) { $column_phid = $move['columnPHID']; $board_phid = $move['boardPHID']; $column = $columns[$column_phid]; $proxy_phid = $column->getProxyPHID(); // If this is a normal column, add the board if the object isn't already // associated. if (!$proxy_phid) { if (!isset($current_phids[$board_phid])) { $add_boards[] = $board_phid; } continue; } // If this is a proxy column but the object is already associated with // the proxy board, we don't need to do anything. if (isset($current_phids[$proxy_phid])) { continue; } // If this a proxy column and the object is already associated with some // descendant of the proxy board, we also don't need to do anything. $descendants = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAncestorProjectPHIDs(array($proxy_phid)) ->execute(); $found_descendant = false; foreach ($descendants as $descendant) { if (isset($current_phids[$descendant->getPHID()])) { $found_descendant = true; break; } } if ($found_descendant) { continue; } // Otherwise, we're moving the object to a proxy column which it is not // a member of yet, so add an association to the column's proxy board. $add_boards[] = $proxy_phid; } if ($add_boards) { $more[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) ->setIgnoreOnNoEffect(true) ->setNewValue( array( '+' => array_fuse($add_boards), )); } return $more; } private function applyBoardMove($object, array $move) { $board_phid = $move['boardPHID']; $column_phid = $move['columnPHID']; $before_phid = idx($move, 'beforePHID'); $after_phid = idx($move, 'afterPHID'); $object_phid = $object->getPHID(); // We're doing layout with the omnipotent viewer to make sure we don't // remove positions in columns that exist, but which the actual actor // can't see. $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); $select_phids = array($board_phid); $descendants = id(new PhabricatorProjectQuery()) ->setViewer($omnipotent_viewer) ->withAncestorProjectPHIDs($select_phids) ->execute(); foreach ($descendants as $descendant) { $select_phids[] = $descendant->getPHID(); } $board_tasks = id(new ManiphestTaskQuery()) ->setViewer($omnipotent_viewer) ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_ANCESTOR, array($select_phids)) ->execute(); $board_tasks = mpull($board_tasks, null, 'getPHID'); $board_tasks[$object_phid] = $object; // Make sure tasks are sorted by ID, so we lay out new positions in // a consistent way. $board_tasks = msort($board_tasks, 'getID'); $object_phids = array_keys($board_tasks); $engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($omnipotent_viewer) ->setBoardPHIDs(array($board_phid)) ->setObjectPHIDs($object_phids) ->executeLayout(); // TODO: This logic needs to be revised when we legitimately support // multiple column positions. $columns = $engine->getObjectColumns($board_phid, $object_phid); foreach ($columns as $column) { $engine->queueRemovePosition( $board_phid, $column->getPHID(), $object_phid); } if ($before_phid) { $engine->queueAddPositionBefore( $board_phid, $column_phid, $object_phid, $before_phid); } else if ($after_phid) { $engine->queueAddPositionAfter( $board_phid, $column_phid, $object_phid, $after_phid); } else { $engine->queueAddPosition( $board_phid, $column_phid, $object_phid); } $engine->applyPositionUpdates(); } private function validateColumnPHID($value) { if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) { return; } throw new Exception( pht( 'When moving objects between columns on a board, columns must '. 'be identified by PHIDs. This transaction uses "%s" to identify '. 'a column, but that is not a valid column PHID.', $value)); } } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index f72977c5b2..e19886d3ff 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -1,615 +1,605 @@ setViewer($actor) ->withClasses(array('PhabricatorManiphestApplication')) ->executeOne(); $view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY); return id(new ManiphestTask()) ->setStatus(ManiphestTaskStatus::getDefaultStatus()) ->setPriority(ManiphestTaskPriority::getDefaultPriority()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT) ->attachProjectPHIDs(array()) ->attachSubscriberPHIDs(array()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', 'status' => 'text64', 'priority' => 'uint32', 'title' => 'sort', - 'originalTitle' => 'text', 'description' => 'text', 'mailKey' => 'bytes20', 'ownerOrdering' => 'text64?', 'originalEmailSource' => 'text255?', 'subpriority' => 'double', 'points' => 'double?', 'bridgedObjectPHID' => 'phid?', 'subtype' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'priority' => array( 'columns' => array('priority', 'status'), ), 'status' => array( 'columns' => array('status'), ), 'ownerPHID' => array( 'columns' => array('ownerPHID', 'status'), ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'ownerOrdering' => array( 'columns' => array('ownerOrdering'), ), 'priority_2' => array( 'columns' => array('priority', 'subpriority'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_dateModified' => array( 'columns' => array('dateModified'), ), 'key_title' => array( 'columns' => array('title(64)'), ), 'key_bridgedobject' => array( 'columns' => array('bridgedObjectPHID'), 'unique' => true, ), 'key_subtype' => array( 'columns' => array('subtype'), ), ), ) + parent::getConfiguration(); } public function loadDependsOnTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), ManiphestTaskDependsOnTaskEdgeType::EDGECONST); } public function loadDependedOnByTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST); } public function getSubscriberPHIDs() { return $this->assertAttached($this->subscriberPHIDs); } public function getProjectPHIDs() { return $this->assertAttached($this->edgeProjectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->edgeProjectPHIDs = $phids; return $this; } public function attachSubscriberPHIDs(array $phids) { $this->subscriberPHIDs = $phids; return $this; } public function setOwnerPHID($phid) { $this->ownerPHID = nonempty($phid, null); return $this; } - public function setTitle($title) { - $this->title = $title; - if (!$this->getID()) { - $this->originalTitle = $title; - } - return $this; - } - public function getMonogram() { return 'T'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } public function attachGroupByProjectPHID($phid) { $this->groupByProjectPHID = $phid; return $this; } public function getGroupByProjectPHID() { return $this->assertAttached($this->groupByProjectPHID); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } $result = parent::save(); return $result; } public function isClosed() { return ManiphestTaskStatus::isClosedStatus($this->getStatus()); } public function isLocked() { return ManiphestTaskStatus::isLockedStatus($this->getStatus()); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function getCoverImageFilePHID() { return idx($this->properties, 'cover.filePHID'); } public function getCoverImageThumbnailPHID() { return idx($this->properties, 'cover.thumbnailPHID'); } public function getWorkboardOrderVectors() { return array( PhabricatorProjectColumn::ORDER_PRIORITY => array( (int)-$this->getPriority(), (double)-$this->getSubpriority(), (int)-$this->getID(), ), ); } public function getPriorityKeyword() { $priority = $this->getPriority(); $keyword = ManiphestTaskPriority::getKeywordForTaskPriority($priority); if ($keyword !== null) { return $keyword; } return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD; } private function comparePriorityTo(ManiphestTask $other) { $upri = $this->getPriority(); $vpri = $other->getPriority(); if ($upri != $vpri) { return ($upri - $vpri); } $usub = $this->getSubpriority(); $vsub = $other->getSubpriority(); if ($usub != $vsub) { return ($usub - $vsub); } $uid = $this->getID(); $vid = $other->getID(); if ($uid != $vid) { return ($uid - $vid); } return 0; } public function isLowerPriorityThan(ManiphestTask $other) { return ($this->comparePriorityTo($other) < 0); } public function isHigherPriorityThan(ManiphestTask $other) { return ($this->comparePriorityTo($other) > 0); } public function getWorkboardProperties() { return array( 'status' => $this->getStatus(), 'points' => (double)$this->getPoints(), ); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getOwnerPHID()); } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newManiphestMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_INTERACT, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_INTERACT: if ($this->isLocked()) { return PhabricatorPolicies::POLICY_NOONE; } else { return $this->getViewPolicy(); } case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // The owner of a task can always view and edit it. $owner_phid = $this->getOwnerPHID(); if ($owner_phid) { $user_phid = $user->getPHID(); if ($user_phid == $owner_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of a task can always view and edit it.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { // Sort of ambiguous who this was intended for; just let them both know. return array_filter( array_unique( array( $this->getAuthorPHID(), $this->getOwnerPHID(), ))); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('maniphest.fields'); } public function getCustomFieldBaseClass() { return 'ManiphestCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ManiphestTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new ManiphestTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The title of the task.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('remarkup') ->setDescription(pht('The task description.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Original task author.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('ownerPHID') ->setType('phid?') ->setDescription(pht('Current task owner, if task is assigned.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Information about task status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('priority') ->setType('map') ->setDescription(pht('Information about task priority.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('points') ->setType('points') ->setDescription(pht('Point value of the task.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('subtype') ->setType('string') ->setDescription(pht('Subtype of the task.')), ); } public function getFieldValuesForConduit() { $status_value = $this->getStatus(); $status_info = array( 'value' => $status_value, 'name' => ManiphestTaskStatus::getTaskStatusName($status_value), 'color' => ManiphestTaskStatus::getStatusColor($status_value), ); $priority_value = (int)$this->getPriority(); $priority_info = array( 'value' => $priority_value, 'subpriority' => (double)$this->getSubpriority(), 'name' => ManiphestTaskPriority::getTaskPriorityName($priority_value), 'color' => ManiphestTaskPriority::getTaskPriorityColor($priority_value), ); return array( 'name' => $this->getTitle(), 'description' => array( 'raw' => $this->getDescription(), ), 'authorPHID' => $this->getAuthorPHID(), 'ownerPHID' => $this->getOwnerPHID(), 'status' => $status_info, 'priority' => $priority_info, 'points' => $this->getPoints(), 'subtype' => $this->getSubtype(), ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorBoardColumnsSearchEngineAttachment()) ->setAttachmentKey('columns'), ); } public function newSubtypeObject() { $subtype_key = $this->getEditEngineSubtype(); $subtype_map = $this->newEditEngineSubtypeMap(); return idx($subtype_map, $subtype_key); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new ManiphestTaskFulltextEngine(); } /* -( DoorkeeperBridgedObjectInterface )----------------------------------- */ public function getBridgedObject() { return $this->assertAttached($this->bridgedObject); } public function attachBridgedObject( DoorkeeperExternalObject $object = null) { $this->bridgedObject = $object; return $this; } /* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */ public function getEditEngineSubtype() { return $this->getSubtype(); } public function setEditEngineSubtype($value) { return $this->setSubtype($value); } public function newEditEngineSubtypeMap() { $config = PhabricatorEnv::getEnvConfig('maniphest.subtypes'); return PhabricatorEditEngineSubtype::newSubtypeMap($config); } /* -( PhabricatorEditEngineLockableInterface )----------------------------- */ public function newEditEngineLock() { return new ManiphestTaskEditEngineLock(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new ManiphestTaskFerretEngine(); } } diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 4aa9ef4055..523733b3df 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -1,331 +1,329 @@ setViewer($actor) ->withClasses(array('PhabricatorPholioApplication')) ->executeOne(); $view_policy = $app->getPolicy(PholioDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PholioDefaultEditCapability::CAPABILITY); return id(new PholioMock()) ->setAuthorPHID($actor->getPHID()) ->attachImages(array()) ->setStatus(self::STATUS_OPEN) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } public function getMonogram() { return 'M'.$this->getID(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'description' => 'text', - 'originalName' => 'text128', 'mailKey' => 'bytes20', 'status' => 'text12', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('MOCK'); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /** * These should be the images currently associated with the Mock. */ public function attachImages(array $images) { assert_instances_of($images, 'PholioImage'); $this->images = $images; return $this; } public function getImages() { $this->assertAttached($this->images); return $this->images; } /** * These should be *all* images associated with the Mock. This includes * images which have been removed and / or replaced from the Mock. */ public function attachAllImages(array $images) { assert_instances_of($images, 'PholioImage'); $this->allImages = $images; return $this; } public function getAllImages() { $this->assertAttached($this->images); return $this->allImages; } public function attachCoverFile(PhabricatorFile $file) { $this->coverFile = $file; return $this; } public function getCoverFile() { $this->assertAttached($this->coverFile); return $this->coverFile; } public function getTokenCount() { $this->assertAttached($this->tokenCount); return $this->tokenCount; } public function attachTokenCount($count) { $this->tokenCount = $count; return $this; } public function getImageHistorySet($image_id) { $images = $this->getAllImages(); $images = mpull($images, null, 'getID'); $selected_image = $images[$image_id]; $replace_map = mpull($images, null, 'getReplacesImagePHID'); $phid_map = mpull($images, null, 'getPHID'); // find the earliest image $image = $selected_image; while (isset($phid_map[$image->getReplacesImagePHID()])) { $image = $phid_map[$image->getReplacesImagePHID()]; } // now build history moving forward $history = array($image->getID() => $image); while (isset($replace_map[$image->getPHID()])) { $image = $replace_map[$image->getPHID()]; $history[$image->getID()] = $image; } return $history; } public function getStatuses() { $options = array(); $options[self::STATUS_OPEN] = pht('Open'); $options[self::STATUS_CLOSED] = pht('Closed'); return $options; } public function isClosed() { return ($this->getStatus() == 'closed'); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht("A mock's owner can always view and edit it."); } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { if ($this->getDescription()) { return $this->getDescription(); } return null; } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PholioMockEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PholioTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { PholioMockQuery::loadImages( $request->getUser(), array($this), $need_inline_comments = true); $timeline->setMock($this); return $timeline; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $images = id(new PholioImage())->loadAllWhere( 'mockID = %d', $this->getID()); foreach ($images as $image) { $image->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PholioMockFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PholioMockFerretEngine(); } } diff --git a/src/applications/pholio/xaction/PholioMockNameTransaction.php b/src/applications/pholio/xaction/PholioMockNameTransaction.php index d1231636af..82fb92fe40 100644 --- a/src/applications/pholio/xaction/PholioMockNameTransaction.php +++ b/src/applications/pholio/xaction/PholioMockNameTransaction.php @@ -1,92 +1,89 @@ getName(); } public function getActionStrength() { return 1.4; } public function applyInternalEffects($object, $value) { $object->setName($value); - if ($object->getOriginalName() === null) { - $object->setOriginalName($this->getNewValue()); - } } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); if ($old === null) { return pht( '%s created %s.', $this->renderAuthor(), $this->renderValue($new)); } else { return pht( '%s renamed this mock from %s to %s.', $this->renderAuthor(), $this->renderValue($old), $this->renderValue($new)); } } public function getTitleForFeed() { $old = $this->getOldValue(); $new = $this->getNewValue(); if ($old === null) { return pht( '%s created %s.', $this->renderAuthor(), $this->renderObject()); } else { return pht( '%s renamed %s from %s to %s.', $this->renderAuthor(), $this->renderObject(), $this->renderValue($old), $this->renderValue($new)); } } public function getColor() { $old = $this->getOldValue(); if ($old === null) { return PhabricatorTransactions::COLOR_GREEN; } return parent::getColor(); } public function validateTransactions($object, array $xactions) { $errors = array(); if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { $errors[] = $this->newRequiredError(pht('Mocks must have a name.')); } $max_length = $object->getColumnMaximumByteLength('name'); foreach ($xactions as $xaction) { $new_value = $xaction->getNewValue(); $new_length = strlen($new_value); if ($new_length > $max_length) { $errors[] = $this->newInvalidError( pht( 'Mock names must not be longer than %s character(s).', new PhutilNumber($max_length))); } } return $errors; } } diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index eefcdba9be..17f7ee3fdc 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -1,313 +1,308 @@ setViewer($actor) ->withClasses(array('PhabricatorPonderApplication')) ->executeOne(); $view_policy = $app->getPolicy( PonderDefaultViewCapability::CAPABILITY); return id(new PonderQuestion()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setStatus(PonderQuestionStatus::STATUS_OPEN) ->setAnswerCount(0) ->setAnswerWiki('') ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'status' => 'text32', 'content' => 'text', 'answerWiki' => 'text', 'answerCount' => 'uint32', 'mailKey' => 'bytes20', // T6203/NULLABILITY // This should always exist. 'contentSource' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } public function getMonogram() { return 'Q'.$this->getID(); } public function getViewURI() { return '/'.$this->getMonogram(); } public function attachAnswers(array $answers) { assert_instances_of($answers, 'PonderAnswer'); $this->answers = $answers; return $this; } public function getAnswers() { return $this->answers; } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->projectPHIDs = $phids; return $this; } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PonderQuestionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PonderQuestionTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } // Markup interface public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } - public function getOriginalTitle() { - // TODO: Make this actually save/return the original title. - return $this->getTitle(); - } - public function getFullTitle() { $id = $this->getID(); $title = $this->getTitle(); return "Q{$id}: {$title}"; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: $app = PhabricatorApplication::getByClass( 'PhabricatorPonderApplication'); return $app->getPolicy(PonderModerateCapability::CAPABILITY); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { if (PhabricatorPolicyFilter::hasCapability( $viewer, $this, PhabricatorPolicyCapability::CAN_EDIT)) { return true; } } return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { $out = array(); $out[] = pht('The user who asked a question can always view and edit it.'); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $out[] = pht( 'A moderator can always view the question.'); break; } return $out; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $answers = id(new PonderAnswer())->loadAllWhere( 'questionID = %d', $this->getID()); foreach ($answers as $answer) { $engine->destroyObject($answer); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PonderQuestionFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PonderQuestionFerretEngine(); } }