diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index f9537c48da..2b81af6555 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -1,190 +1,194 @@ getAPIMethodName(); } /** * Get the status for this method (e.g., stable, unstable or deprecated). * Should return a METHOD_STATUS_* constant. By default, methods are * "stable". * * @return const METHOD_STATUS_* constant. * @task status */ public function getMethodStatus() { return self::METHOD_STATUS_STABLE; } /** * Optional description to supplement the method status. In particular, if * a method is deprecated, you can return a string here describing the reason * for deprecation and stable alternatives. * * @return string|null Description of the method status, if available. * @task status */ public function getMethodStatusDescription() { return null; } public function getErrorDescription($error_code) { return idx($this->defineErrorTypes(), $error_code, 'Unknown Error'); } public function getRequiredScope() { // by default, conduit methods are not accessible via OAuth return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE; } public function executeMethod(ConduitAPIRequest $request) { return $this->execute($request); } public function getAPIMethodName() { return self::getAPIMethodNameFromClassName(get_class($this)); } /** * Return a key which sorts methods by application name, then method status, * then method name. */ public function getSortOrder() { $name = $this->getAPIMethodName(); $map = array( ConduitAPIMethod::METHOD_STATUS_STABLE => 0, ConduitAPIMethod::METHOD_STATUS_UNSTABLE => 1, ConduitAPIMethod::METHOD_STATUS_DEPRECATED => 2, ); $ord = idx($map, $this->getMethodStatus(), 0); list($head, $tail) = explode('.', $name, 2); return "{$head}.{$ord}.{$tail}"; } public function getApplicationName() { return head(explode('.', $this->getAPIMethodName(), 2)); } public static function getClassNameFromAPIMethodName($method_name) { $method_fragment = str_replace('.', '_', $method_name); return 'ConduitAPI_'.$method_fragment.'_Method'; } public function shouldRequireAuthentication() { return true; } public function shouldAllowUnguardedWrites() { return false; } /** * Optionally, return a @{class:PhabricatorApplication} which this call is * part of. The call will be disabled when the application is uninstalled. * * @return PhabricatorApplication|null Related application. */ public function getApplication() { return null; } public static function getAPIMethodNameFromClassName($class_name) { $match = null; $is_valid = preg_match( '/^ConduitAPI_(.*)_Method$/', $class_name, $match); if (!$is_valid) { throw new Exception( "Parameter '{$class_name}' is not a valid Conduit API method class."); } $method_fragment = $match[1]; return str_replace('_', '.', $method_fragment); } protected function validateHost($host) { if (!$host) { // If the client doesn't send a host key, don't complain. We should in // the future, but this change isn't severe enough to bump the protocol // version. // TODO: Remove this once the protocol version gets bumped past 2 (i.e., // require the host key be present and valid). return; } // NOTE: Compare domains only so we aren't sensitive to port specification // or omission. $host = new PhutilURI($host); $host = $host->getDomain(); $self = new PhutilURI(PhabricatorEnv::getURI('/')); $self = $self->getDomain(); if ($self !== $host) { throw new Exception( "Your client is connecting to this install as '{$host}', but it is ". "configured as '{$self}'. The client and server must use the exact ". "same URI to identify the install. Edit your .arcconfig or ". "phabricator/conf so they agree on the URI for the install."); } } /* -( PhabricatorPolicyInterface )----------------------------------------- */ + public function getPHID() { + return null; + } + public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // The policy interface on Conduit calls is currently just to let us hook // into ApplicationSearch. Calls are always visible (even to logged out // users). return true; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php index fa0a302db7..6401b0fd62 100644 --- a/src/applications/config/storage/PhabricatorConfigEntry.php +++ b/src/applications/config/storage/PhabricatorConfigEntry.php @@ -1,66 +1,64 @@ true, self::CONFIG_SERIALIZATION => array( 'value' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorConfigPHIDTypeConfig::TYPECONST); } public static function loadConfigEntry($key) { $config_entry = id(new PhabricatorConfigEntry()) ->loadOneWhere( 'configKey = %s AND namespace = %s', $key, 'default'); if (!$config_entry) { $config_entry = id(new PhabricatorConfigEntry()) ->setConfigKey($key) ->setNamespace('default'); } return $config_entry; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_ADMIN; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index fd4188e070..3e632fec27 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -1,196 +1,194 @@ true, self::CONFIG_SERIALIZATION => array( 'recentParticipantPHIDs' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorConpherencePHIDTypeThread::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function attachParticipants(array $participants) { assert_instances_of($participants, 'ConpherenceParticipant'); $this->participants = $participants; return $this; } public function getParticipants() { return $this->assertAttached($this->participants); } public function getParticipant($phid) { $participants = $this->getParticipants(); return $participants[$phid]; } public function getParticipantPHIDs() { $participants = $this->getParticipants(); return array_keys($participants); } public function attachHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function getHandles() { return $this->assertAttached($this->handles); } public function attachTransactions(array $transactions) { assert_instances_of($transactions, 'ConpherenceTransaction'); $this->transactions = $transactions; return $this; } public function getTransactions() { return $this->assertAttached($this->transactions); } public function getTransactionsFrom($begin = 0, $amount = null) { $length = count($this->transactions); return array_slice( $this->getTransactions(), $length - $begin - $amount, $amount); } public function attachFilePHIDs(array $file_phids) { $this->filePHIDs = $file_phids; return $this; } public function getFilePHIDs() { return $this->assertAttached($this->filePHIDs); } public function attachWidgetData(array $widget_data) { $this->widgetData = $widget_data; return $this; } public function getWidgetData() { return $this->assertAttached($this->widgetData); } public function getDisplayData(PhabricatorUser $user) { $recent_phids = $this->getRecentParticipantPHIDs(); $handles = $this->getHandles(); // luck has little to do with it really; most recent participant who isn't // the user.... $lucky_phid = null; $lucky_index = null; foreach ($recent_phids as $index => $phid) { if ($phid == $user->getPHID()) { continue; } $lucky_phid = $phid; break; } reset($recent_phids); if ($lucky_phid) { $lucky_handle = $handles[$lucky_phid]; // this will be just the user talking to themselves. weirdos. } else { $lucky_handle = reset($handles); } $title = $js_title = $this->getTitle(); if (!$title) { $title = $lucky_handle->getName(); $js_title = pht('[No Title]'); } $img_src = $lucky_handle->getImageURI(); $count = 0; $final = false; $subtitle = null; foreach ($recent_phids as $phid) { if ($phid == $user->getPHID()) { continue; } $handle = $handles[$phid]; if ($subtitle) { if ($final) { $subtitle .= '...'; break; } else { $subtitle .= ', '; } } $subtitle .= $handle->getName(); $count++; $final = $count == 3; } $participants = $this->getParticipants(); $user_participation = $participants[$user->getPHID()]; $unread_count = $this->getMessageCount() - $user_participation->getSeenMessageCount(); return array( 'title' => $title, 'js_title' => $js_title, 'subtitle' => $subtitle, 'unread_count' => $unread_count, 'epoch' => $this->getDateModified(), 'image' => $img_src, ); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // this bad boy isn't even created yet so go nuts $user if (!$this->getID()) { return true; } $participants = $this->getParticipants(); return isset($participants[$user->getPHID()]); } public function describeAutomaticCapability($capability) { return pht("Participants in a thread can always view and edit it."); } } diff --git a/src/applications/countdown/storage/PhabricatorCountdown.php b/src/applications/countdown/storage/PhabricatorCountdown.php index 551c2c848c..19a9227bca 100644 --- a/src/applications/countdown/storage/PhabricatorCountdown.php +++ b/src/applications/countdown/storage/PhabricatorCountdown.php @@ -1,60 +1,58 @@ true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCountdownPHIDTypeCountdown::TYPECONST); } public function getViewPolicy() { return PhabricatorPolicies::POLICY_USER; } /* -( 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 PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht('The author of a countdown can always view and edit it.'); } } diff --git a/src/applications/daemon/storage/PhabricatorDaemonLog.php b/src/applications/daemon/storage/PhabricatorDaemonLog.php index f951df99cc..9f435594fe 100644 --- a/src/applications/daemon/storage/PhabricatorDaemonLog.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLog.php @@ -1,48 +1,51 @@ array( 'argv' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ + public function getPHID() { + return null; + } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_ADMIN; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 3e12ad9e28..1600b0b201 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,410 +1,409 @@ setViewer($actor) ->withClasses(array('PhabricatorApplicationDifferential')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialCapabilityDefaultView::CAPABILITY); return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) ->attachRelationships(array()) ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } 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( DifferentialPHIDTypeRevision::TYPECONST); } public function loadComments() { if (!$this->getID()) { return array(); } return id(new DifferentialCommentQuery()) ->withRevisionIDs(array($this->getID())) ->execute(); } 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 delete() { $this->openTransaction(); $diffs = id(new DifferentialDiffQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($diffs as $diff) { $diff->delete(); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::RELATIONSHIP_TABLE, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $comments = id(new DifferentialCommentQuery()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($comments as $comment) { $comment->delete(); } $inlines = id(new DifferentialInlineCommentQuery()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($inlines as $inline) { $inline->delete(); } $fields = id(new DifferentialAuxiliaryField())->loadAllWhere( 'revisionPHID = %s', $this->getPHID()); foreach ($fields as $field) { $field->delete(); } // we have to do paths a little differentally 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()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function loadRelationships() { if (!$this->getID()) { $this->relationships = array(); return; } // Read "subscribed" and "unsubscribed" data out of the old relationship // table. $data = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE revisionID = %d AND relation != %s ORDER BY sequence', self::RELATIONSHIP_TABLE, $this->getID(), self::RELATION_REVIEWER); // Read "reviewer" data out of the new table. $reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER); $reviewer_phids = array_reverse($reviewer_phids); foreach ($reviewer_phids as $phid) { $data[] = array( 'relation' => self::RELATION_REVIEWER, 'objectPHID' => $phid, 'reasonPHID' => null, ); } return $this->attachRelationships($data); } public function attachRelationships(array $relationships) { $this->relationships = igroup($relationships, 'relation'); return $this; } public function getReviewers() { return $this->getRelatedPHIDs(self::RELATION_REVIEWER); } public function getCCPHIDs() { return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED); } private function getRelatedPHIDs($relation) { $this->assertAttached($this->relationships); return ipull($this->getRawRelations($relation), 'objectPHID'); } public function getRawRelations($relation) { return idx($this->relationships, $relation, array()); } public function loadUnsubscribedPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->phid, PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); } public function getPrimaryReviewer() { $reviewers = $this->getReviewers(); $last = $this->lastReviewerPHID; if (!$last || !in_array($last, $reviewers)) { return head($this->getReviewers()); } return $last; } public function loadReviewedBy() { $reviewer = null; if ($this->status == ArcanistDifferentialRevisionStatus::ACCEPTED || $this->status == ArcanistDifferentialRevisionStatus::CLOSED) { $comments = $this->loadComments(); foreach ($comments as $comment) { $action = $comment->getAction(); if ($action == DifferentialAction::ACTION_ACCEPT) { $reviewer = $comment->getAuthorPHID(); } else if ($action == DifferentialAction::ACTION_REJECT || $action == DifferentialAction::ACTION_ABANDON || $action == DifferentialAction::ACTION_RETHINK) { $reviewer = null; } } } return $reviewer; } public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } 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( "A revision's reviewers can always view it."); $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; } public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } public function getReviewerStatus() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewerStatus(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); $this->reviewerStatus = $reviewers; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } } diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php index 70affac00c..9099af58d1 100644 --- a/src/applications/diviner/storage/DivinerLiveBook.php +++ b/src/applications/diviner/storage/DivinerLiveBook.php @@ -1,68 +1,67 @@ true, self::CONFIG_SERIALIZATION => array( 'configurationData' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function getConfig($key, $default = null) { return idx($this->configurationData, $key, $default); } public function setConfig($key, $value) { $this->configurationData[$key] = $value; return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DivinerPHIDTypeBook::TYPECONST); } public function getTitle() { return $this->getConfig('title', $this->getName()); } public function getShortTitle() { return $this->getConfig('short', $this->getTitle()); } public function getGroupName($group) { $groups = $this->getConfig('groups'); $spec = idx($groups, $group, array()); return idx($spec, 'name', $group); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->viewPolicy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php index f2de0d0974..2e873808ab 100644 --- a/src/applications/diviner/storage/DivinerLiveSymbol.php +++ b/src/applications/diviner/storage/DivinerLiveSymbol.php @@ -1,181 +1,180 @@ true, self::CONFIG_TIMESTAMPS => false, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DivinerPHIDTypeAtom::TYPECONST); } public function getBook() { return $this->assertAttached($this->book); } public function attachBook(DivinerLiveBook $book) { $this->book = $book; return $this; } public function getAtom() { return $this->assertAttached($this->atom); } public function attachAtom(DivinerLiveAtom $atom) { $this->atom = DivinerAtom::newFromDictionary($atom->getAtomData()); return $this; } public function getURI() { $parts = array( 'book', $this->getBook()->getName(), $this->getType(), ); if ($this->getContext()) { $parts[] = $this->getContext(); } $parts[] = $this->getName(); if ($this->getAtomIndex()) { $parts[] = $this->getAtomIndex(); } return '/'.implode('/', $parts).'/'; } public function getSortKey() { return $this->getTitle(); } public function save() { // NOTE: The identity hash is just a sanity check because the unique tuple // on this table is way way too long to fit into a normal UNIQUE KEY. We // don't use it directly, but its existence prevents duplicate records. if (!$this->identityHash) { $this->identityHash = PhabricatorHash::digestForIndex( serialize( array( 'bookPHID' => $this->getBookPHID(), 'context' => $this->getContext(), 'type' => $this->getType(), 'name' => $this->getName(), 'index' => $this->getAtomIndex(), ))); } return parent::save(); } public function getTitle() { $title = parent::getTitle(); if (!strlen($title)) { $title = $this->getName(); } return $title; } public function attachExtends(array $extends) { assert_instances_of($extends, 'DivinerLiveSymbol'); $this->extends = $extends; return $this; } public function getExtends() { return $this->assertAttached($this->extends); } public function attachChildren(array $children) { assert_instances_of($children, 'DivinerLiveSymbol'); $this->children = $children; return $this; } public function getChildren() { return $this->assertAttached($this->children); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return $this->getBook()->getCapabilities(); } public function getPolicy($capability) { return $this->getBook()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBook()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Atoms inherit the policies of the books they are part of.'); } /* -( Markup Interface )--------------------------------------------------- */ public function getMarkupFieldKey($field) { return $this->getPHID().':'.$field.':'.$this->getGraphHash(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine('diviner'); } public function getMarkupText($field) { return $this->getAtom()->getDocblockText(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return false; } } diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php index eb6b928866..69fdf7c02c 100644 --- a/src/applications/feed/story/PhabricatorFeedStory.php +++ b/src/applications/feed/story/PhabricatorFeedStory.php @@ -1,331 +1,334 @@ List of @{class:PhabricatorFeedStoryData} rows from the * database. * @return list List of @{class:PhabricatorFeedStory} * objects. * @task load */ public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) { $stories = array(); $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows); foreach ($data as $story_data) { $class = $story_data->getStoryType(); try { $ok = class_exists($class) && is_subclass_of($class, 'PhabricatorFeedStory'); } catch (PhutilMissingSymbolException $ex) { $ok = false; } // If the story type isn't a valid class or isn't a subclass of // PhabricatorFeedStory, decline to load it. if (!$ok) { continue; } $key = $story_data->getChronologicalKey(); $stories[$key] = newv($class, array($story_data)); } $object_phids = array(); $key_phids = array(); foreach ($stories as $key => $story) { $phids = array(); foreach ($story->getRequiredObjectPHIDs() as $phid) { $phids[$phid] = true; } if ($story->getPrimaryObjectPHID()) { $phids[$story->getPrimaryObjectPHID()] = true; } $key_phids[$key] = $phids; $object_phids += $phids; } $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($object_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_objects = array_select_keys($objects, array_keys($phids)); if (count($story_objects) != count($phids)) { // An object this story requires either does not exist or is not visible // to the user. Decline to render the story. unset($stories[$key]); unset($key_phids[$key]); continue; } $stories[$key]->setObjects($story_objects); } $handle_phids = array(); foreach ($stories as $key => $story) { foreach ($story->getRequiredHandlePHIDs() as $phid) { $key_phids[$key][$phid] = true; } if ($story->getAuthorPHID()) { $key_phids[$key][$story->getAuthorPHID()] = true; } $handle_phids += $key_phids[$key]; } $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($handle_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_handles = array_select_keys($handles, array_keys($phids)); $stories[$key]->setHandles($story_handles); } return $stories; } public function setHovercard($hover) { $this->hovercard = $hover; return $this; } public function setObjects(array $objects) { $this->objects = $objects; return $this; } public function getObject($phid) { $object = idx($this->objects, $phid); if (!$object) { throw new Exception( "Story is asking for an object it did not request ('{$phid}')!"); } return $object; } public function getPrimaryObject() { $phid = $this->getPrimaryObjectPHID(); if (!$phid) { throw new Exception("Story has no primary object!"); } return $this->getObject($phid); } public function getPrimaryObjectPHID() { return null; } final public function __construct(PhabricatorFeedStoryData $data) { $this->data = $data; } abstract public function renderView(); public function getRequiredHandlePHIDs() { return array(); } public function getRequiredObjectPHIDs() { return array(); } public function setHasViewed($has_viewed) { $this->hasViewed = $has_viewed; return $this; } public function getHasViewed() { return $this->hasViewed; } final public function setFramed($framed) { $this->framed = $framed; return $this; } final public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } final protected function getObjects() { return $this->objects; } final protected function getHandles() { return $this->handles; } final protected function getHandle($phid) { if (isset($this->handles[$phid])) { if ($this->handles[$phid] instanceof PhabricatorObjectHandle) { return $this->handles[$phid]; } } $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setName("Unloaded Object '{$phid}'"); return $handle; } final public function getStoryData() { return $this->data; } final public function getEpoch() { return $this->getStoryData()->getEpoch(); } final public function getChronologicalKey() { return $this->getStoryData()->getChronologicalKey(); } final public function getValue($key, $default = null) { return $this->getStoryData()->getValue($key, $default); } final public function getAuthorPHID() { return $this->getStoryData()->getAuthorPHID(); } final protected function renderHandleList(array $phids) { $list = array(); foreach ($phids as $phid) { $list[] = $this->linkTo($phid); } return phutil_implode_html(', ', $list); } final protected function linkTo($phid) { $handle = $this->getHandle($phid); // NOTE: We render our own link here to customize the styling and add // the '_top' target for framed feeds. $class = null; if ($handle->getType() == PhabricatorPeoplePHIDTypeUser::TYPECONST) { $class = 'phui-link-person'; } return javelin_tag( 'a', array( 'href' => $handle->getURI(), 'target' => $this->framed ? '_top' : null, 'sigil' => $this->hovercard ? 'hovercard' : null, 'meta' => $this->hovercard ? array('hoverPHID' => $phid) : null, 'class' => $class, ), $handle->getLinkName()); } final protected function renderString($str) { return phutil_tag('strong', array(), $str); } final protected function renderSummary($text, $len = 128) { if ($len) { $text = phutil_utf8_shorten($text, $len); } $text = phutil_escape_html_newlines($text); return $text; } public function getNotificationAggregations() { return array(); } protected function newStoryView() { return id(new PHUIFeedStoryView()) ->setChronologicalKey($this->getChronologicalKey()) ->setEpoch($this->getEpoch()) ->setViewed($this->getHasViewed()); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ + public function getPHID() { + return null; + } /** * @task policy */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } /** * @task policy */ public function getPolicy($capability) { // If this story's primary object is a policy-aware object, use its policy // to control story visiblity. $primary_phid = $this->getPrimaryObjectPHID(); if (isset($this->objects[$primary_phid])) { $object = $this->objects[$primary_phid]; if ($object instanceof PhabricatorPolicyInterface) { return $object->getPolicy($capability); } } // TODO: Remove this once all objects are policy-aware. For now, keep // respecting the `feed.public` setting. return PhabricatorEnv::getEnvConfig('feed.public') ? PhabricatorPolicies::POLICY_PUBLIC : PhabricatorPolicies::POLICY_USER; } /** * @task policy */ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index fdd2a0641f..51e2a55fca 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1,949 +1,948 @@ true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorFilePHIDTypeFile::TYPECONST); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey($this->generateSecretKey()); } if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public static function readUploadedFileData($spec) { if (!$spec) { throw new Exception("No file was uploaded!"); } $err = idx($spec, 'error'); if ($err) { throw new PhabricatorFileUploadException($err); } $tmp_name = idx($spec, 'tmp_name'); $is_valid = @is_uploaded_file($tmp_name); if (!$is_valid) { throw new Exception("File is not an uploaded file."); } $file_data = Filesystem::readFile($tmp_name); $file_size = idx($spec, 'size'); if (strlen($file_data) != $file_size) { throw new Exception("File size disagrees with uploaded size."); } self::validateFileSize(strlen($file_data)); return $file_data; } public static function newFromPHPUpload($spec, array $params = array()) { $file_data = self::readUploadedFileData($spec); $file_name = nonempty( idx($params, 'name'), idx($spec, 'name')); $params = array( 'name' => $file_name, ) + $params; return self::newFromFileData($file_data, $params); } public static function newFromXHRUpload($data, array $params = array()) { self::validateFileSize(strlen($data)); return self::newFromFileData($data, $params); } private static function validateFileSize($size) { $limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit'); if (!$limit) { return; } $limit = phabricator_parse_bytes($limit); if ($size > $limit) { throw new PhabricatorFileUploadException(-1000); } } /** * Given a block of data, try to load an existing file with the same content * if one exists. If it does not, build a new file. * * This method is generally used when we have some piece of semi-trusted data * like a diff or a file from a repository that we want to show to the user. * We can't just dump it out because it may be dangerous for any number of * reasons; instead, we need to serve it through the File abstraction so it * ends up on the CDN domain if one is configured and so on. However, if we * simply wrote a new file every time we'd potentially end up with a lot * of redundant data in file storage. * * To solve these problems, we use file storage as a cache and reuse the * same file again if we've previously written it. * * NOTE: This method unguards writes. * * @param string Raw file data. * @param dict Dictionary of file information. */ public static function buildFromFileDataOrHash( $data, array $params = array()) { $file = id(new PhabricatorFile())->loadOneWhere( 'name = %s AND contentHash = %s LIMIT 1', self::normalizeFileName(idx($params, 'name')), self::hashFileContent($data)); if (!$file) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file = PhabricatorFile::newFromFileData($data, $params); unset($unguarded); } return $file; } public static function newFileFromContentHash($hash, $params) { // Check to see if a file with same contentHash exist $file = id(new PhabricatorFile())->loadOneWhere( 'contentHash = %s LIMIT 1', $hash); if ($file) { // copy storageEngine, storageHandle, storageFormat $copy_of_storage_engine = $file->getStorageEngine(); $copy_of_storage_handle = $file->getStorageHandle(); $copy_of_storage_format = $file->getStorageFormat(); $copy_of_byteSize = $file->getByteSize(); $copy_of_mimeType = $file->getMimeType(); $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); $file_ttl = idx($params, 'ttl'); $authorPHID = idx($params, 'authorPHID'); $new_file = new PhabricatorFile(); $new_file->setName($file_name); $new_file->setByteSize($copy_of_byteSize); $new_file->setAuthorPHID($authorPHID); $new_file->setTtl($file_ttl); $new_file->setContentHash($hash); $new_file->setStorageEngine($copy_of_storage_engine); $new_file->setStorageHandle($copy_of_storage_handle); $new_file->setStorageFormat($copy_of_storage_format); $new_file->setMimeType($copy_of_mimeType); $new_file->copyDimensions($file); $new_file->save(); return $new_file; } return $file; } private static function buildFromFileData($data, array $params = array()) { $selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector'); if (isset($params['storageEngines'])) { $engines = $params['storageEngines']; } else { $selector = PhabricatorEnv::newObjectFromConfig( 'storage.engine-selector'); $engines = $selector->selectStorageEngines($data, $params); } assert_instances_of($engines, 'PhabricatorFileStorageEngine'); if (!$engines) { throw new Exception("No valid storage engines are available!"); } $file = new PhabricatorFile(); $data_handle = null; $engine_identifier = null; $exceptions = array(); foreach ($engines as $engine) { $engine_class = get_class($engine); try { list($engine_identifier, $data_handle) = $file->writeToEngine( $engine, $data, $params); // We stored the file somewhere so stop trying to write it to other // places. break; } catch (PhabricatorFileStorageConfigurationException $ex) { // If an engine is outright misconfigured (or misimplemented), raise // that immediately since it probably needs attention. throw $ex; } catch (Exception $ex) { phlog($ex); // If an engine doesn't work, keep trying all the other valid engines // in case something else works. $exceptions[$engine_class] = $ex; } } if (!$data_handle) { throw new PhutilAggregateException( "All storage engines failed to write file:", $exceptions); } $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); $file_ttl = idx($params, 'ttl'); // If for whatever reason, authorPHID isn't passed as a param // (always the case with newFromFileDownload()), store a '' $authorPHID = idx($params, 'authorPHID'); $file->setName($file_name); $file->setByteSize(strlen($data)); $file->setAuthorPHID($authorPHID); $file->setTtl($file_ttl); $file->setContentHash(self::hashFileContent($data)); $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); // TODO: This is probably YAGNI, but allows for us to do encryption or // compression later if we want. $file->setStorageFormat(self::STORAGE_FORMAT_RAW); $file->setIsExplicitUpload(idx($params, 'isExplicitUpload') ? 1 : 0); if (isset($params['mime-type'])) { $file->setMimeType($params['mime-type']); } else { $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); $file->setMimeType(Filesystem::getMimeType($tmp)); } try { $file->updateDimensions(false); } catch (Exception $ex) { // Do nothing } $file->save(); return $file; } public static function newFromFileData($data, array $params = array()) { $hash = self::hashFileContent($data); $file = self::newFileFromContentHash($hash, $params); if ($file) { return $file; } return self::buildFromFileData($data, $params); } public function migrateToEngine(PhabricatorFileStorageEngine $engine) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( "You can not migrate a file which hasn't yet been saved."); } $data = $this->loadFileData(); $params = array( 'name' => $this->getName(), ); list($new_identifier, $new_handle) = $this->writeToEngine( $engine, $data, $params); $old_engine = $this->instantiateStorageEngine(); $old_handle = $this->getStorageHandle(); $this->setStorageEngine($new_identifier); $this->setStorageHandle($new_handle); $this->save(); $old_engine->deleteFile($old_handle); return $this; } private function writeToEngine( PhabricatorFileStorageEngine $engine, $data, array $params) { $engine_class = get_class($engine); $data_handle = $engine->writeFile($data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new PhabricatorFileStorageConfigurationException( "Storage engine '{$engine_class}' executed writeFile() but did ". "not return a valid handle ('{$data_handle}') to the data: it ". "must be nonempty and no longer than 255 characters."); } $engine_identifier = $engine->getEngineIdentifier(); if (!$engine_identifier || strlen($engine_identifier) > 32) { throw new PhabricatorFileStorageConfigurationException( "Storage engine '{$engine_class}' returned an improper engine ". "identifier '{$engine_identifier}': it must be nonempty ". "and no longer than 32 characters."); } return array($engine_identifier, $data_handle); } public static function newFromFileDownload($uri, array $params = array()) { // Make sure we're allowed to make a request first if (!PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) { throw new Exception("Outbound HTTP requests are disabled!"); } $uri = new PhutilURI($uri); $protocol = $uri->getProtocol(); switch ($protocol) { case 'http': case 'https': break; default: // Make sure we are not accessing any file:// URIs or similar. return null; } $timeout = 5; list($file_data) = id(new HTTPSFuture($uri)) ->setTimeout($timeout) ->resolvex(); $params = $params + array( 'name' => basename($uri), ); return self::newFromFileData($file_data, $params); } public static function normalizeFileName($file_name) { $pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@"; $file_name = preg_replace($pattern, '_', $file_name); $file_name = preg_replace('@_+@', '_', $file_name); $file_name = trim($file_name, '_'); $disallowed_filenames = array( '.' => 'dot', '..' => 'dotdot', '' => 'file', ); $file_name = idx($disallowed_filenames, $file_name, $file_name); return $file_name; } public function delete() { // We want to delete all the rows which mark this file as the transformation // of some other file (since we're getting rid of it). We also delete all // the transformations of this file, so that a user who deletes an image // doesn't need to separately hunt down and delete a bunch of thumbnails and // resizes of it. $outbound_xforms = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withTransforms( array( array( 'originalPHID' => $this->getPHID(), 'transform' => true, ), )) ->execute(); foreach ($outbound_xforms as $outbound_xform) { $outbound_xform->delete(); } $inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 'transformedPHID = %s', $this->getPHID()); $this->openTransaction(); foreach ($inbound_xforms as $inbound_xform) { $inbound_xform->delete(); } $ret = parent::delete(); $this->saveTransaction(); // Check to see if other files are using storage $other_file = id(new PhabricatorFile())->loadAllWhere( 'storageEngine = %s AND storageHandle = %s AND storageFormat = %s AND id != %d LIMIT 1', $this->getStorageEngine(), $this->getStorageHandle(), $this->getStorageFormat(), $this->getID()); // If this is the only file using the storage, delete storage if (!$other_file) { $engine = $this->instantiateStorageEngine(); try { $engine->deleteFile($this->getStorageHandle()); } catch (Exception $ex) { // In the worst case, we're leaving some data stranded in a storage // engine, which is fine. phlog($ex); } } return $ret; } public static function hashFileContent($data) { return sha1($data); } public function loadFileData() { $engine = $this->instantiateStorageEngine(); $data = $engine->readFile($this->getStorageHandle()); switch ($this->getStorageFormat()) { case self::STORAGE_FORMAT_RAW: $data = $data; break; default: throw new Exception("Unknown storage format."); } return $data; } public function getViewURI() { if (!$this->getPHID()) { throw new Exception( "You must save a file before you can generate a view URI."); } $name = phutil_escape_uri($this->getName()); $path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name; return PhabricatorEnv::getCDNURI($path); } public function getInfoURI() { return '/file/info/'.$this->getPHID().'/'; } public function getBestURI() { if ($this->isViewableInBrowser()) { return $this->getViewURI(); } else { return $this->getInfoURI(); } } public function getDownloadURI() { $uri = id(new PhutilURI($this->getViewURI())) ->setQueryParam('download', true); return (string) $uri; } public function getProfileThumbURI() { $path = '/file/xform/thumb-profile/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb60x45URI() { $path = '/file/xform/thumb-60x45/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb160x120URI() { $path = '/file/xform/thumb-160x120/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getPreview140URI() { $path = '/file/xform/preview-140/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getPreview220URI() { $path = '/file/xform/preview-220/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb220x165URI() { $path = '/file/xform/thumb-220x165/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb280x210URI() { $path = '/file/xform/thumb-280x210/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } public function isViewableImage() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isAudio() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isTransformableImage() { // NOTE: The way the 'gd' extension works in PHP is that you can install it // with support for only some file types, so it might be able to handle // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup // warns you if you don't have complete support. $matches = null; $ok = preg_match( '@^image/(gif|png|jpe?g)@', $this->getViewableMimeType(), $matches); if (!$ok) { return false; } switch ($matches[1]) { case 'jpg'; case 'jpeg': return function_exists('imagejpeg'); break; case 'png': return function_exists('imagepng'); break; case 'gif': return function_exists('imagegif'); break; default: throw new Exception('Unknown type matched as image MIME type.'); } } public static function getTransformableImageFormats() { $supported = array(); if (function_exists('imagejpeg')) { $supported[] = 'jpg'; } if (function_exists('imagepng')) { $supported[] = 'png'; } if (function_exists('imagegif')) { $supported[] = 'gif'; } return $supported; } protected function instantiateStorageEngine() { return self::buildEngine($this->getStorageEngine()); } public static function buildEngine($engine_identifier) { $engines = self::buildAllEngines(); foreach ($engines as $engine) { if ($engine->getEngineIdentifier() == $engine_identifier) { return $engine; } } throw new Exception( "Storage engine '{$engine_identifier}' could not be located!"); } public static function buildAllEngines() { $engines = id(new PhutilSymbolLoader()) ->setType('class') ->setConcreteOnly(true) ->setAncestorClass('PhabricatorFileStorageEngine') ->selectAndLoadSymbols(); $results = array(); foreach ($engines as $engine_class) { $results[] = newv($engine_class['name'], array()); } return $results; } public function getViewableMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); $mime_type = $this->getMimeType(); $mime_parts = explode(';', $mime_type); $mime_type = trim(reset($mime_parts)); return idx($mime_map, $mime_type); } public function getDisplayIconForMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type, 'docs_file'); } public function validateSecretKey($key) { return ($key == $this->getSecretKey()); } public function generateSecretKey() { return Filesystem::readRandomCharacters(20); } public function updateDimensions($save = true) { if (!$this->isViewableImage()) { throw new Exception( "This file is not a viewable image."); } if (!function_exists("imagecreatefromstring")) { throw new Exception( "Cannot retrieve image information."); } $data = $this->loadFileData(); $img = imagecreatefromstring($data); if ($img === false) { throw new Exception( "Error when decoding image."); } $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img); $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img); if ($save) { $this->save(); } return $this; } public function copyDimensions(PhabricatorFile $file) { $metadata = $file->getMetadata(); $width = idx($metadata, self::METADATA_IMAGE_WIDTH); if ($width) { $this->metadata[self::METADATA_IMAGE_WIDTH] = $width; } $height = idx($metadata, self::METADATA_IMAGE_HEIGHT); if ($height) { $this->metadata[self::METADATA_IMAGE_HEIGHT] = $height; } return $this; } public static function getMetadataName($metadata) { switch ($metadata) { case self::METADATA_IMAGE_WIDTH: $name = pht('Width'); break; case self::METADATA_IMAGE_HEIGHT: $name = pht('Height'); break; default: $name = ucfirst($metadata); break; } return $name; } /** * Load (or build) the {@class:PhabricatorFile} objects for builtin file * resources. The builtin mechanism allows files shipped with Phabricator * to be treated like normal files so that APIs do not need to special case * things like default images or deleted files. * * Builtins are located in `resources/builtin/` and identified by their * name. * * @param PhabricatorUser Viewing user. * @param list List of builtin file names. * @return dict Dictionary of named builtins. */ public static function loadBuiltins(PhabricatorUser $user, array $names) { $specs = array(); foreach ($names as $name) { $specs[] = array( 'originalPHID' => PhabricatorPHIDConstants::PHID_VOID, 'transform' => 'builtin:'.$name, ); } $files = id(new PhabricatorFileQuery()) ->setViewer($user) ->withTransforms($specs) ->execute(); $files = mpull($files, null, 'getName'); $root = dirname(phutil_get_library_root('phabricator')); $root = $root.'/resources/builtin/'; $build = array(); foreach ($names as $name) { if (isset($files[$name])) { continue; } // This is just a sanity check to prevent loading arbitrary files. if (basename($name) != $name) { throw new Exception("Invalid builtin name '{$name}'!"); } $path = $root.$name; if (!Filesystem::pathExists($path)) { throw new Exception("Builtin '{$path}' does not exist!"); } $data = Filesystem::readFile($path); $params = array( 'name' => $name, 'ttl' => time() + (60 * 60 * 24 * 7), ); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file = PhabricatorFile::newFromFileData($data, $params); $xform = id(new PhabricatorTransformedFile()) ->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID) ->setTransform('builtin:'.$name) ->setTransformedPHID($file->getPHID()) ->save(); unset($unguarded); $file->attachObjectPHIDs(array()); $file->attachObjects(array()); $files[$name] = $file; } return $files; } /** * Convenience wrapper for @{method:loadBuiltins}. * * @param PhabricatorUser Viewing user. * @param string Single builtin name to load. * @return PhabricatorFile Corresponding builtin file. */ public static function loadBuiltin(PhabricatorUser $user, $name) { return idx(self::loadBuiltins($user, array($name)), $name); } public function getObjects() { return $this->assertAttached($this->objects); } public function attachObjects(array $objects) { $this->objects = $objects; return $this; } public function getObjectPHIDs() { return $this->assertAttached($this->objectPHIDs); } public function attachObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function getImageHeight() { if (!$this->isViewableImage()) { return null; } return idx($this->metadata, self::METADATA_IMAGE_HEIGHT); } public function getImageWidth() { if (!$this->isViewableImage()) { return null; } return idx($this->metadata, self::METADATA_IMAGE_WIDTH); } /** * Write the policy edge between this file and some object. * * @param PhabricatorUser Acting user. * @param phid Object PHID to attach to. * @return this */ public function attachToObject(PhabricatorUser $actor, $phid) { $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; id(new PhabricatorEdgeEditor()) ->setActor($actor) ->setSuppressEvents(true) ->addEdge($phid, $edge_type, $this->getPHID()) ->save(); return $this; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // TODO: Implement proper per-object policies. return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid) { if ($this->getAuthorPHID() == $viewer_phid) { return true; } } switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // If you can see any object this file is attached to, you can see // the file. return (count($this->getObjects()) > 0); } return false; } public function describeAutomaticCapability($capability) { $out = array(); $out[] = pht('The user who uploaded a file can always view and edit it.'); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $out[] = pht( 'Files attached to objects are visible to users who can view '. 'those objects.'); break; } return $out; } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } } diff --git a/src/applications/herald/storage/transcript/HeraldTranscript.php b/src/applications/herald/storage/transcript/HeraldTranscript.php index 8b04a8fcc9..be35b9ee02 100644 --- a/src/applications/herald/storage/transcript/HeraldTranscript.php +++ b/src/applications/herald/storage/transcript/HeraldTranscript.php @@ -1,196 +1,193 @@ applyTranscripts as $xscript) { if ($xscript->getApplied()) { if ($xscript->getRuleID()) { $ids[] = $xscript->getRuleID(); } } } if (!$ids) { return 'none'; } // A rule may have multiple effects, which will cause it to be listed // multiple times. $ids = array_unique($ids); foreach ($ids as $k => $id) { $ids[$k] = '<'.$id.'>'; } return implode(', ', $ids); } public static function saveXHeraldRulesHeader($phid, $header) { // Combine any existing header with the new header, listing all rules // which have ever triggered for this object. $header = self::combineXHeraldRulesHeaders( self::loadXHeraldRulesHeader($phid), $header); queryfx( id(new HeraldTranscript())->establishConnection('w'), 'INSERT INTO %T (phid, header) VALUES (%s, %s) ON DUPLICATE KEY UPDATE header = VALUES(header)', self::TABLE_SAVED_HEADER, $phid, $header); return $header; } private static function combineXHeraldRulesHeaders($u, $v) { $u = preg_split('/[, ]+/', $u); $v = preg_split('/[, ]+/', $v); $combined = array_unique(array_filter(array_merge($u, $v))); return implode(', ', $combined); } public static function loadXHeraldRulesHeader($phid) { $header = queryfx_one( id(new HeraldTranscript())->establishConnection('r'), 'SELECT * FROM %T WHERE phid = %s', self::TABLE_SAVED_HEADER, $phid); if ($header) { return idx($header, 'header'); } return null; } protected function getConfiguration() { // Ugh. Too much of a mess to deal with. return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_SERIALIZATION => array( 'objectTranscript' => self::SERIALIZATION_PHP, 'ruleTranscripts' => self::SERIALIZATION_PHP, 'conditionTranscripts' => self::SERIALIZATION_PHP, 'applyTranscripts' => self::SERIALIZATION_PHP, ), ) + parent::getConfiguration(); } public function __construct() { $this->time = time(); $this->host = php_uname('n'); } public function addApplyTranscript(HeraldApplyTranscript $transcript) { $this->applyTranscripts[] = $transcript; return $this; } public function getApplyTranscripts() { return nonempty($this->applyTranscripts, array()); } public function setDuration($duration) { $this->duration = $duration; return $this; } public function setObjectTranscript(HeraldObjectTranscript $transcript) { $this->objectTranscript = $transcript; return $this; } public function getObjectTranscript() { return $this->objectTranscript; } public function addRuleTranscript(HeraldRuleTranscript $transcript) { $this->ruleTranscripts[$transcript->getRuleID()] = $transcript; return $this; } public function discardDetails() { $this->applyTranscripts = null; $this->ruleTranscripts = null; $this->objectTranscript = null; $this->conditionTranscripts = null; } public function getRuleTranscripts() { return nonempty($this->ruleTranscripts, array()); } public function addConditionTranscript( HeraldConditionTranscript $transcript) { $rule_id = $transcript->getRuleID(); $cond_id = $transcript->getConditionID(); $this->conditionTranscripts[$rule_id][$cond_id] = $transcript; return $this; } public function getConditionTranscriptsForRule($rule_id) { return idx($this->conditionTranscripts, $rule_id, array()); } public function getMetadataMap() { return array( 'Run At Epoch' => date('F jS, g:i:s A', $this->time), 'Run On Host' => $this->host, 'Run Duration' => (int)(1000 * $this->duration).' ms', ); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('HLXS'); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_USER; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return pht( 'To view a transcript, you must be able to view the object the '. 'transcript is about.'); } } diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index 45bdbefc87..1b1764acc0 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -1,115 +1,114 @@ true, self::CONFIG_SERIALIZATION => array( 'recentContributorPHIDs' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorLegalpadPHIDTypeDocument::TYPECONST); } public function getDocumentBody() { return $this->assertAttached($this->documentBody); } public function attachDocumentBody(LegalpadDocumentBody $body) { $this->documentBody = $body; return $this; } public function getContributors() { return $this->assertAttached($this->contributors); } public function attachContributors(array $contributors) { $this->contributors = $contributors; return $this; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->creatorPHID == $phid); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $policy = $this->viewPolicy; break; case PhabricatorPolicyCapability::CAN_EDIT: $policy = $this->editPolicy; break; default: $policy = PhabricatorPolicies::POLICY_NOONE; break; } return $policy; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return ($user->getPHID() == $this->getCreatorPHID()); } public function describeAutomaticCapability($capability) { return pht( 'The author of a document can always view and edit it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new LegalpadDocumentEditor(); } public function getApplicationTransactionObject() { return new LegalpadTransaction(); } } diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index 272d5abdd0..dab1ac59f3 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -1,85 +1,84 @@ file = $file; return $this; } public function getFile() { return $this->assertAttached($this->file); } public function attachAudio(PhabricatorFile $audio = null) { $this->audio = $audio; return $this; } public function getAudio() { return $this->assertAttached($this->audio); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorMacroPHIDTypeMacro::TYPECONST); } public function isAutomaticallySubscribed($phid) { return false; } public function getApplicationTransactionEditor() { return new PhabricatorMacroEditor(); } public function getApplicationTransactionObject() { return new PhabricatorMacroTransaction(); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php b/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php index 2770a93b30..b24ca55185 100644 --- a/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php +++ b/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php @@ -1,44 +1,43 @@ true, ) + parent::getConfiguration(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 171f504b7b..3309f467a9 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -1,276 +1,275 @@ setViewer($actor) ->withClasses(array('PhabricatorApplicationManiphest')) ->executeOne(); $view_policy = $app->getPolicy(ManiphestCapabilityDefaultView::CAPABILITY); $edit_policy = $app->getPolicy(ManiphestCapabilityDefaultEdit::CAPABILITY); return id(new ManiphestTask()) ->setPriority(ManiphestTaskPriority::getDefaultPriority()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'ccPHIDs' => self::SERIALIZATION_JSON, 'attached' => self::SERIALIZATION_JSON, 'projectPHIDs' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function loadDependsOnTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK); } public function loadDependedOnByTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK); } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestPHIDTypeTask::TYPECONST); } public function getCCPHIDs() { return array_values(nonempty($this->ccPHIDs, array())); } public function setProjectPHIDs(array $phids) { $this->projectPHIDs = array_values($phids); $this->projectsNeedUpdate = true; return $this; } public function getProjectPHIDs() { return array_values(nonempty($this->projectPHIDs, array())); } public function setCCPHIDs(array $phids) { $this->ccPHIDs = array_values($phids); $this->subscribersNeedUpdate = true; return $this; } public function setOwnerPHID($phid) { $this->ownerPHID = nonempty($phid, null); $this->subscribersNeedUpdate = true; return $this; } public function setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } 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(); if ($this->projectsNeedUpdate) { // If we've changed the project PHIDs for this task, update the link // table. ManiphestTaskProject::updateTaskProjects($this); $this->projectsNeedUpdate = false; } if ($this->subscribersNeedUpdate) { // If we've changed the subscriber PHIDs for this task, update the link // table. ManiphestTaskSubscriber::updateTaskSubscribers($this); $this->subscribersNeedUpdate = false; } return $result; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "maniphest:T{$id}:{$field}:{$hash}"; } /** * @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_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) { // 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; } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index f4132aa3b1..c784a1a224 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -1,422 +1,421 @@ false, self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('OPKG'); } public function attachUnsavedOwners(array $owners) { $this->unsavedOwners = $owners; return $this; } public function attachUnsavedPaths(array $paths) { $this->unsavedPaths = $paths; return $this; } public function attachActorPHID($actor_phid) { $this->actorPHID = $actor_phid; return $this; } public function getActorPHID() { return $this->actorPHID; } public function attachOldPrimaryOwnerPHID($old_primary) { $this->oldPrimaryOwnerPHID = $old_primary; return $this; } public function getOldPrimaryOwnerPHID() { return $this->oldPrimaryOwnerPHID; } public function attachOldAuditingEnabled($auditing_enabled) { $this->oldAuditingEnabled = $auditing_enabled; return $this; } public function getOldAuditingEnabled() { return $this->oldAuditingEnabled; } public function setName($name) { $this->name = $name; if (!$this->getID()) { $this->originalName = $name; } return $this; } public function loadOwners() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID = %d', $this->getID()); } public function loadPaths() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID = %d', $this->getID()); } public static function loadAffectedPackages( PhabricatorRepository $repository, array $paths) { if (!$paths) { return array(); } return self::loadPackagesForPaths($repository, $paths); } public static function loadOwningPackages($repository, $path) { if (empty($path)) { return array(); } return self::loadPackagesForPaths($repository, array($path), 1); } private static function loadPackagesForPaths( PhabricatorRepository $repository, array $paths, $limit = 0) { $fragments = array(); foreach ($paths as $path) { foreach (self::splitPath($path) as $fragment) { $fragments[$fragment][$path] = true; } } $package = new PhabricatorOwnersPackage(); $path = new PhabricatorOwnersPath(); $conn = $package->establishConnection('r'); $repository_clause = qsprintf( $conn, 'AND p.repositoryPHID = %s', $repository->getPHID()); // NOTE: The list of $paths may be very large if we're coming from // the OwnersWorker and processing, e.g., an SVN commit which created a new // branch. Break it apart so that it will fit within 'max_allowed_packet', // and then merge results in PHP. $rows = array(); foreach (array_chunk(array_keys($fragments), 128) as $chunk) { $rows[] = queryfx_all( $conn, 'SELECT pkg.id, p.excluded, p.path FROM %T pkg JOIN %T p ON p.packageID = pkg.id WHERE p.path IN (%Ls) %Q', $package->getTableName(), $path->getTableName(), $chunk, $repository_clause); } $rows = array_mergev($rows); $ids = self::findLongestPathsPerPackage($rows, $fragments); if (!$ids) { return array(); } arsort($ids); if ($limit) { $ids = array_slice($ids, 0, $limit, $preserve_keys = true); } $ids = array_keys($ids); $packages = $package->loadAllWhere('id in (%Ld)', $ids); $packages = array_select_keys($packages, $ids); return $packages; } public static function loadPackagesForRepository($repository) { $package = new PhabricatorOwnersPackage(); $ids = ipull( queryfx_all( $package->establishConnection('r'), 'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s', id(new PhabricatorOwnersPath())->getTableName(), $repository->getPHID()), 'packageID'); return $package->loadAllWhere('id in (%Ld)', $ids); } public static function findLongestPathsPerPackage(array $rows, array $paths) { $ids = array(); foreach (igroup($rows, 'id') as $id => $package_paths) { $relevant_paths = array_select_keys( $paths, ipull($package_paths, 'path')); // For every package, remove all excluded paths. $remove = array(); foreach ($package_paths as $package_path) { if ($package_path['excluded']) { $remove += $relevant_paths[$package_path['path']]; unset($relevant_paths[$package_path['path']]); } } if ($remove) { foreach ($relevant_paths as $fragment => $fragment_paths) { $relevant_paths[$fragment] = array_diff_key($fragment_paths, $remove); } } $relevant_paths = array_filter($relevant_paths); if ($relevant_paths) { $ids[$id] = max(array_map('strlen', array_keys($relevant_paths))); } } return $ids; } private function getActor() { // TODO: This should be cleaner, but we'd likely need to move the whole // thing to an Editor (T603). return PhabricatorUser::getOmnipotentUser(); } public function save() { if ($this->getID()) { $is_new = false; } else { $is_new = true; } $this->openTransaction(); $ret = parent::save(); $add_owners = array(); $remove_owners = array(); $all_owners = array(); if ($this->unsavedOwners) { $new_owners = array_fill_keys($this->unsavedOwners, true); $cur_owners = array(); foreach ($this->loadOwners() as $owner) { if (empty($new_owners[$owner->getUserPHID()])) { $remove_owners[$owner->getUserPHID()] = true; $owner->delete(); continue; } $cur_owners[$owner->getUserPHID()] = true; } $add_owners = array_diff_key($new_owners, $cur_owners); $all_owners = array_merge( array($this->getPrimaryOwnerPHID() => true), $new_owners, $remove_owners); foreach ($add_owners as $phid => $ignored) { $owner = new PhabricatorOwnersOwner(); $owner->setPackageID($this->getID()); $owner->setUserPHID($phid); $owner->save(); } unset($this->unsavedOwners); } $add_paths = array(); $remove_paths = array(); $touched_repos = array(); if ($this->unsavedPaths) { $new_paths = igroup($this->unsavedPaths, 'repositoryPHID', 'path'); $cur_paths = $this->loadPaths(); foreach ($cur_paths as $key => $path) { $repository_phid = $path->getRepositoryPHID(); $new_path = head(idx( idx($new_paths, $repository_phid, array()), $path->getPath(), array())); $excluded = $path->getExcluded(); if (!$new_path || idx($new_path, 'excluded') != $excluded) { $touched_repos[$repository_phid] = true; $remove_paths[$repository_phid][$path->getPath()] = $excluded; $path->delete(); unset($cur_paths[$key]); } } $cur_paths = mgroup($cur_paths, 'getRepositoryPHID', 'getPath'); foreach ($new_paths as $repository_phid => $paths) { // TODO: (T603) Thread policy stuff in here. // get repository object for path validation $repository = id(new PhabricatorRepository())->loadOneWhere( 'phid = %s', $repository_phid); if (!$repository) { continue; } foreach ($paths as $path => $dicts) { $path = ltrim($path, '/'); // build query to validate path $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $this->getActor(), 'repository' => $repository, 'path' => $path, )); $results = DiffusionBrowseResultSet::newFromConduit( DiffusionQuery::callConduitWithDiffusionRequest( $this->getActor(), $drequest, 'diffusion.browsequery', array( 'commit' => $drequest->getCommit(), 'path' => $path, 'needValidityOnly' => true))); $valid = $results->isValidResults(); $is_directory = true; if (!$valid) { switch ($results->getReasonForEmptyResultSet()) { case DiffusionBrowseResultSet::REASON_IS_FILE: $valid = true; $is_directory = false; break; case DiffusionBrowseResultSet::REASON_IS_EMPTY: $valid = true; break; } } if ($is_directory && substr($path, -1) != '/') { $path .= '/'; } if (substr($path, 0, 1) != '/') { $path = '/'.$path; } if (empty($cur_paths[$repository_phid][$path]) && $valid) { $touched_repos[$repository_phid] = true; $excluded = idx(reset($dicts), 'excluded', 0); $add_paths[$repository_phid][$path] = $excluded; $obj = new PhabricatorOwnersPath(); $obj->setPackageID($this->getID()); $obj->setRepositoryPHID($repository_phid); $obj->setPath($path); $obj->setExcluded($excluded); $obj->save(); } } } unset($this->unsavedPaths); } $this->saveTransaction(); if ($is_new) { $mail = new PackageCreateMail($this); } else { $mail = new PackageModifyMail( $this, array_keys($add_owners), array_keys($remove_owners), array_keys($all_owners), array_keys($touched_repos), $add_paths, $remove_paths); } $mail->setActor($this->getActor()); $mail->send(); return $ret; } public function delete() { $mails = id(new PackageDeleteMail($this)) ->setActor($this->getActor()) ->prepareMails(); $this->openTransaction(); foreach ($this->loadOwners() as $owner) { $owner->delete(); } foreach ($this->loadPaths() as $path) { $path->delete(); } $ret = parent::delete(); $this->saveTransaction(); foreach ($mails as $mail) { $mail->saveAndSend(); } return $ret; } private static function splitPath($path) { $result = array('/'); $trailing_slash = preg_match('@/$@', $path) ? '/' : ''; $path = trim($path, '/'); $parts = explode('/', $path); while (count($parts)) { $result[] = '/'.implode('/', $parts).$trailing_slash; $trailing_slash = '/'; array_pop($parts); } return $result; } } diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index 25678e008f..4bb560ee13 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -1,111 +1,110 @@ getID(); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPastePHIDTypePaste::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return $this->viewPolicy; } return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return ($user->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht( 'The author of a paste can always view and edit it.'); } public function getFullName() { $title = $this->getTitle(); if (!$title) { $title = pht('(An Untitled Masterwork)'); } return 'P'.$this->getID().' '.$title; } public function getContent() { return $this->assertAttached($this->content); } public function attachContent($content) { $this->content = $content; return $this; } public function getRawContent() { return $this->assertAttached($this->rawContent); } public function attachRawContent($raw_content) { $this->rawContent = $raw_content; return $this; } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index cf6942fb62..a0e0552635 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,869 +1,868 @@ timezoneIdentifier, date_default_timezone_get()); // Make sure these return booleans. case 'isAdmin': return (bool)$this->isAdmin; case 'isDisabled': return (bool)$this->isDisabled; case 'isSystemAgent': return (bool)$this->isSystemAgent; default: return parent::readField($field); } } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_PARTIAL_OBJECTS => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPeoplePHIDTypeUser::TYPECONST); } public function setPassword(PhutilOpaqueEnvelope $envelope) { if (!$this->getPHID()) { throw new Exception( "You can not set a password for an unsaved user because their PHID ". "is a salt component in the password hash."); } if (!strlen($envelope->openEnvelope())) { $this->setPasswordHash(''); } else { $this->setPasswordSalt(md5(mt_rand())); $hash = $this->hashPassword($envelope); $this->setPasswordHash($hash); } return $this; } // To satisfy PhutilPerson. public function getSex() { return $this->sex; } public function getTranslation() { try { if ($this->translation && class_exists($this->translation) && is_subclass_of($this->translation, 'PhabricatorTranslation')) { return $this->translation; } } catch (PhutilMissingSymbolException $ex) { return null; } return null; } public function isLoggedIn() { return !($this->getPHID() === null); } public function save() { if (!$this->getConduitCertificate()) { $this->setConduitCertificate($this->generateConduitCertificate()); } $result = parent::save(); if ($this->profile) { $this->profile->save(); } $this->updateNameTokens(); id(new PhabricatorSearchIndexer()) ->indexDocumentByPHID($this->getPHID()); return $result; } private function generateConduitCertificate() { return Filesystem::readRandomCharacters(255); } public function comparePassword(PhutilOpaqueEnvelope $envelope) { if (!strlen($envelope->openEnvelope())) { return false; } if (!strlen($this->getPasswordHash())) { return false; } $password_hash = $this->hashPassword($envelope); return ($password_hash === $this->getPasswordHash()); } private function hashPassword(PhutilOpaqueEnvelope $envelope) { $hash = $this->getUsername(). $envelope->openEnvelope(). $this->getPHID(). $this->getPasswordSalt(); for ($ii = 0; $ii < 1000; $ii++) { $hash = md5($hash); } return $hash; } const CSRF_CYCLE_FREQUENCY = 3600; const CSRF_SALT_LENGTH = 8; const CSRF_TOKEN_LENGTH = 16; const CSRF_BREACH_PREFIX = 'B@'; const EMAIL_CYCLE_FREQUENCY = 86400; const EMAIL_TOKEN_LENGTH = 24; private function getRawCSRFToken($offset = 0) { return $this->generateToken( time() + (self::CSRF_CYCLE_FREQUENCY * $offset), self::CSRF_CYCLE_FREQUENCY, PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), self::CSRF_TOKEN_LENGTH); } /** * @phutil-external-symbol class PhabricatorStartup */ public function getCSRFToken() { $salt = PhabricatorStartup::getGlobal('csrf.salt'); if (!$salt) { $salt = Filesystem::readRandomCharacters(self::CSRF_SALT_LENGTH); PhabricatorStartup::setGlobal('csrf.salt', $salt); } // Generate a token hash to mitigate BREACH attacks against SSL. See // discussion in T3684. $token = $this->getRawCSRFToken(); $hash = PhabricatorHash::digest($token, $salt); return 'B@'.$salt.substr($hash, 0, self::CSRF_TOKEN_LENGTH); } public function validateCSRFToken($token) { if (!$this->getPHID()) { return true; } $salt = null; $version = 'plain'; // This is a BREACH-mitigating token. See T3684. $breach_prefix = self::CSRF_BREACH_PREFIX; $breach_prelen = strlen($breach_prefix); if (!strncmp($token, $breach_prefix, $breach_prelen)) { $version = 'breach'; $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH); $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH); } // When the user posts a form, we check that it contains a valid CSRF token. // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept // either the current token, the next token (users can submit a "future" // token if you have two web frontends that have some clock skew) or any of // the last 6 tokens. This means that pages are valid for up to 7 hours. // There is also some Javascript which periodically refreshes the CSRF // tokens on each page, so theoretically pages should be valid indefinitely. // However, this code may fail to run (if the user loses their internet // connection, or there's a JS problem, or they don't have JS enabled). // Choosing the size of the window in which we accept old CSRF tokens is // an issue of balancing concerns between security and usability. We could // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to // attacks using captured CSRF tokens, but it's also more likely that real // users will be affected by this, e.g. if they close their laptop for an // hour, open it back up, and try to submit a form before the CSRF refresh // can kick in. Since the user experience of submitting a form with expired // CSRF is often quite bad (you basically lose data, or it's a big pain to // recover at least) and I believe we gain little additional protection // by keeping the window very short (the overwhelming value here is in // preventing blind attacks, and most attacks which can capture CSRF tokens // can also just capture authentication information [sniffing networks] // or act as the user [xss]) the 7 hour default seems like a reasonable // balance. Other major platforms have much longer CSRF token lifetimes, // like Rails (session duration) and Django (forever), which suggests this // is a reasonable analysis. $csrf_window = 6; for ($ii = -$csrf_window; $ii <= 1; $ii++) { $valid = $this->getRawCSRFToken($ii); switch ($version) { // TODO: We can remove this after the BREACH version has been in the // wild for a while. case 'plain': if ($token == $valid) { return true; } break; case 'breach': $digest = PhabricatorHash::digest($valid, $salt); if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) { return true; } break; default: throw new Exception("Unknown CSRF token format!"); } } return false; } private function generateToken($epoch, $frequency, $key, $len) { $time_block = floor($epoch / $frequency); $vec = $this->getPHID().$this->getPasswordHash().$key.$time_block; return substr(PhabricatorHash::digest($vec), 0, $len); } /** * Issue a new session key to this user. Phabricator supports different * types of sessions (like "web" and "conduit") and each session type may * have multiple concurrent sessions (this allows a user to be logged in on * multiple browsers at the same time, for instance). * * Note that this method is transport-agnostic and does not set cookies or * issue other types of tokens, it ONLY generates a new session key. * * You can configure the maximum number of concurrent sessions for various * session types in the Phabricator configuration. * * @param string Session type, like "web". * @return string Newly generated session key. */ public function establishSession($session_type) { $conn_w = $this->establishConnection('w'); if (strpos($session_type, '-') !== false) { throw new Exception("Session type must not contain hyphen ('-')!"); } // We allow multiple sessions of the same type, so when a caller requests // a new session of type "web", we give them the first available session in // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none // of these sessions is available, we overwrite the oldest session and // reissue a new one in its place. $session_limit = 1; switch ($session_type) { case 'web': $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web'); break; case 'conduit': $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit'); break; default: throw new Exception("Unknown session type '{$session_type}'!"); } $session_limit = (int)$session_limit; if ($session_limit <= 0) { throw new Exception( "Session limit for '{$session_type}' must be at least 1!"); } // NOTE: Session establishment is sensitive to race conditions, as when // piping `arc` to `arc`: // // arc export ... | arc paste ... // // To avoid this, we overwrite an old session only if it hasn't been // re-established since we read it. // Consume entropy to generate a new session key, forestalling the eventual // heat death of the universe. $session_key = Filesystem::readRandomCharacters(40); // Load all the currently active sessions. $sessions = queryfx_all( $conn_w, 'SELECT type, sessionKey, sessionStart FROM %T WHERE userPHID = %s AND type LIKE %>', PhabricatorUser::SESSION_TABLE, $this->getPHID(), $session_type.'-'); $sessions = ipull($sessions, null, 'type'); $sessions = isort($sessions, 'sessionStart'); $existing_sessions = array_keys($sessions); // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $retries = 0; while (true) { // Choose which 'type' we'll actually establish, i.e. what number we're // going to append to the basic session type. To do this, just check all // the numbers sequentially until we find an available session. $establish_type = null; for ($ii = 1; $ii <= $session_limit; $ii++) { $try_type = $session_type.'-'.$ii; if (!in_array($try_type, $existing_sessions)) { $establish_type = $try_type; $expect_key = PhabricatorHash::digest($session_key); $existing_sessions[] = $try_type; // Ensure the row exists so we can issue an update below. We don't // care if we race here or not. queryfx( $conn_w, 'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart) VALUES (%s, %s, %s, 0)', self::SESSION_TABLE, $this->getPHID(), $establish_type, PhabricatorHash::digest($session_key)); break; } } // If we didn't find an available session, choose the oldest session and // overwrite it. if (!$establish_type) { $oldest = reset($sessions); $establish_type = $oldest['type']; $expect_key = $oldest['sessionKey']; } // This is so that we'll only overwrite the session if it hasn't been // refreshed since we read it. If it has, the session key will be // different and we know we're racing other processes. Whichever one // won gets the session, we go back and try again. queryfx( $conn_w, 'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP() WHERE userPHID = %s AND type = %s AND sessionKey = %s', self::SESSION_TABLE, PhabricatorHash::digest($session_key), $this->getPHID(), $establish_type, $expect_key); if ($conn_w->getAffectedRows()) { // The update worked, so the session is valid. break; } else { // We know this just got grabbed, so don't try it again. unset($sessions[$establish_type]); } if (++$retries > $session_limit) { throw new Exception("Failed to establish a session!"); } } $log = PhabricatorUserLog::newLog( $this, $this, PhabricatorUserLog::ACTION_LOGIN); $log->setDetails( array( 'session_type' => $session_type, 'session_issued' => $establish_type, )); $log->setSession($session_key); $log->save(); return $session_key; } public function destroySession($session_key) { $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s', self::SESSION_TABLE, $this->getPHID(), PhabricatorHash::digest($session_key)); } private function generateEmailToken( PhabricatorUserEmail $email, $offset = 0) { $key = implode( '-', array( PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), $this->getPHID(), $email->getVerificationCode(), )); return $this->generateToken( time() + ($offset * self::EMAIL_CYCLE_FREQUENCY), self::EMAIL_CYCLE_FREQUENCY, $key, self::EMAIL_TOKEN_LENGTH); } public function validateEmailToken( PhabricatorUserEmail $email, $token) { for ($ii = -1; $ii <= 1; $ii++) { $valid = $this->generateEmailToken($email, $ii); if ($token == $valid) { return true; } } return false; } public function getEmailLoginURI(PhabricatorUserEmail $email = null) { if (!$email) { $email = $this->loadPrimaryEmail(); if (!$email) { throw new Exception("User has no primary email!"); } } $token = $this->generateEmailToken($email); $uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/'); $uri = new PhutilURI($uri); return $uri->alter('email', $email->getAddress()); } public function attachUserProfile(PhabricatorUserProfile $profile) { $this->profile = $profile; return $this; } public function loadUserProfile() { if ($this->profile) { return $this->profile; } $profile_dao = new PhabricatorUserProfile(); $this->profile = $profile_dao->loadOneWhere('userPHID = %s', $this->getPHID()); if (!$this->profile) { $profile_dao->setUserPHID($this->getPHID()); $this->profile = $profile_dao; } return $this->profile; } public function loadPrimaryEmailAddress() { $email = $this->loadPrimaryEmail(); if (!$email) { throw new Exception("User has no primary email address!"); } return $email->getAddress(); } public function loadPrimaryEmail() { return $this->loadOneRelative( new PhabricatorUserEmail(), 'userPHID', 'getPHID', '(isPrimary = 1)'); } public function loadPreferences() { if ($this->preferences) { return $this->preferences; } $preferences = null; if ($this->getPHID()) { $preferences = id(new PhabricatorUserPreferences())->loadOneWhere( 'userPHID = %s', $this->getPHID()); } if (!$preferences) { $preferences = new PhabricatorUserPreferences(); $preferences->setUserPHID($this->getPHID()); $default_dict = array( PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph', PhabricatorUserPreferences::PREFERENCE_EDITOR => '', PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '', PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0); $preferences->setPreferences($default_dict); } $this->preferences = $preferences; return $preferences; } public function loadEditorLink($path, $line, $callsign) { $editor = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_EDITOR); if (is_array($path)) { $multiedit = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_MULTIEDIT); switch ($multiedit) { case '': $path = implode(' ', $path); break; case 'disable': return null; } } if ($editor) { return strtr($editor, array( '%%' => '%', '%f' => phutil_escape_uri($path), '%l' => phutil_escape_uri($line), '%r' => phutil_escape_uri($callsign), )); } } private static function tokenizeName($name) { if (function_exists('mb_strtolower')) { $name = mb_strtolower($name, 'UTF-8'); } else { $name = strtolower($name); } $name = trim($name); if (!strlen($name)) { return array(); } return preg_split('/\s+/', $name); } /** * Populate the nametoken table, which used to fetch typeahead results. When * a user types "linc", we want to match "Abraham Lincoln" from on-demand * typeahead sources. To do this, we need a separate table of name fragments. */ public function updateNameTokens() { $tokens = array_merge( self::tokenizeName($this->getRealName()), self::tokenizeName($this->getUserName())); $tokens = array_unique($tokens); $table = self::NAMETOKEN_TABLE; $conn_w = $this->establishConnection('w'); $sql = array(); foreach ($tokens as $token) { $sql[] = qsprintf( $conn_w, '(%d, %s)', $this->getID(), $token); } queryfx( $conn_w, 'DELETE FROM %T WHERE userID = %d', $table, $this->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (userID, token) VALUES %Q', $table, implode(', ', $sql)); } } public function sendWelcomeEmail(PhabricatorUser $admin) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $user_username = $this->getUserName(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $base_uri = PhabricatorEnv::getProductionURI('/'); $uri = $this->getEmailLoginURI(); $body = <<addTos(array($this->getPHID())) ->setSubject('[Phabricator] Welcome to Phabricator') ->setBody($body) ->saveAndSend(); } public function sendUsernameChangeEmail( PhabricatorUser $admin, $old_username) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $new_username = $this->getUserName(); $password_instructions = null; if (PhabricatorAuthProviderPassword::getPasswordProvider()) { $uri = $this->getEmailLoginURI(); $password_instructions = <<addTos(array($this->getPHID())) ->setSubject('[Phabricator] Username Changed') ->setBody($body) ->saveAndSend(); } public static function describeValidUsername() { return pht( 'Usernames must contain only numbers, letters, period, underscore and '. 'hyphen, and can not end with a period. They must have no more than %d '. 'characters.', new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH)); } public static function validateUsername($username) { // NOTE: If you update this, make sure to update: // // - Remarkup rule for @mentions. // - Routing rule for "/p/username/". // - Unit tests, obviously. // - describeValidUsername() method, above. if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) { return false; } return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]$/', $username); } public static function getDefaultProfileImageURI() { return celerity_get_resource_uri('/rsrc/image/avatar.png'); } public function attachStatus(PhabricatorUserStatus $status) { $this->status = $status; return $this; } public function getStatus() { $this->assertAttached($this->status); return $this->status; } public function hasStatus() { return $this->status !== self::ATTACHABLE; } public function attachProfileImageURI($uri) { $this->profileImage = $uri; return $this; } public function loadProfileImageURI() { if ($this->profileImage) { return $this->profileImage; } $src_phid = $this->getProfileImagePHID(); if ($src_phid) { // TODO: (T603) Can we get rid of this entirely and move it to // PeopleQuery with attach/attachable? $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); if ($file) { $this->profileImage = $file->getBestURI(); } } if (!$this->profileImage) { $this->profileImage = self::getDefaultProfileImageURI(); } return $this->profileImage; } public function getFullName() { return $this->getUsername().' ('.$this->getRealName().')'; } public function __toString() { return $this->getUsername(); } public static function loadOneWithEmailAddress($address) { $email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $address); if (!$email) { return null; } return id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $email->getUserPHID()); } /* -( Omnipotence )-------------------------------------------------------- */ /** * Returns true if this user is omnipotent. Omnipotent users bypass all policy * checks. * * @return bool True if the user bypasses policy checks. */ public function isOmnipotent() { return $this->omnipotent; } /** * Get an omnipotent user object for use in contexts where there is no acting * user, notably daemons. * * @return PhabricatorUser An omnipotent user. */ public static function getOmnipotentUser() { static $user = null; if (!$user) { $user = new PhabricatorUser(); $user->omnipotent = true; $user->makeEphemeral(); } return $user; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_PUBLIC; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getPHID() && ($viewer->getPHID() === $this->getPHID()); } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return pht('Only you can edit your information.'); default: return null; } } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('user.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorUserCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index 9270843d5c..2a5ce6832c 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -1,235 +1,233 @@ true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhamePHIDTypeBlog::TYPECONST); } public function getSkinRenderer(AphrontRequest $request) { $spec = PhameSkinSpecification::loadOneSkinSpecification( $this->getSkin()); if (!$spec) { $spec = PhameSkinSpecification::loadOneSkinSpecification( self::SKIN_DEFAULT); } if (!$spec) { throw new Exception( "This blog has an invalid skin, and the default skin failed to ". "load."); } $skin = newv($spec->getSkinClass(), array($request)); $skin->setSpecification($spec); return $skin; } /** * Makes sure a given custom blog uri is properly configured in DNS * to point at this Phabricator instance. If there is an error in * the configuration, return a string describing the error and how * to fix it. If there is no error, return an empty string. * * @return string */ public function validateCustomDomain($custom_domain) { $example_domain = '(e.g. blog.example.com)'; $valid = ''; // note this "uri" should be pretty busted given the desired input // so just use it to test if there's a protocol specified $uri = new PhutilURI($custom_domain); if ($uri->getProtocol()) { return 'Do not specify a protocol, just the domain. '.$example_domain; } if (strpos($custom_domain, '/') !== false) { return 'Do not specify a path, just the domain. '.$example_domain; } if (strpos($custom_domain, '.') === false) { return 'Custom domain must contain at least one dot (.) because '. 'some browsers fail to set cookies on domains such as '. 'http://example. '.$example_domain; } return $valid; } public function getBloggerPHIDs() { return $this->assertAttached($this->bloggerPHIDs); } public function attachBloggers(array $bloggers) { assert_instances_of($bloggers, 'PhabricatorObjectHandle'); $this->bloggers = $bloggers; return $this; } public function getBloggers() { return $this->assertAttached($this->bloggers); } public function getSkin() { $config = coalesce($this->getConfigData(), array()); return idx($config, 'skin', self::SKIN_DEFAULT); } public function setSkin($skin) { $config = coalesce($this->getConfigData(), array()); $config['skin'] = $skin; return $this->setConfigData($config); } static public function getSkinOptionsForSelect() { $classes = id(new PhutilSymbolLoader()) ->setAncestorClass('PhameBlogSkin') ->setType('class') ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); return ipull($classes, 'name', 'name'); } public static function setRequestBlog(PhameBlog $blog) { self::$requestBlog = $blog; } public static function getRequestBlog() { return self::$requestBlog; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_JOIN, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case PhabricatorPolicyCapability::CAN_JOIN: return $this->getJoinPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { $can_edit = PhabricatorPolicyCapability::CAN_EDIT; $can_join = PhabricatorPolicyCapability::CAN_JOIN; switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // Users who can edit or post to a blog can always view it. if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) { return true; } if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_join)) { return true; } break; case PhabricatorPolicyCapability::CAN_JOIN: // Users who can edit a blog can always post to it. if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) { return true; } break; } return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht( 'Users who can edit or post on a blog can always view it.'); case PhabricatorPolicyCapability::CAN_JOIN: return pht( 'Users who can edit a blog can always post on it.'); } return null; } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return $this->getPHID().':'.$field.':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { return $this->getDescription(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index fc3b1d1721..5ae396f4c8 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -1,202 +1,200 @@ blog = $blog; return $this; } public function getBlog() { return $this->blog; } public function getViewURI() { // go for the pretty uri if we can $domain = ($this->blog ? $this->blog->getDomain() : ''); if ($domain) { $phame_title = PhabricatorSlug::normalize($this->getPhameTitle()); return 'http://'.$domain.'/post/'.$phame_title; } $uri = '/phame/post/view/'.$this->getID().'/'; return PhabricatorEnv::getProductionURI($uri); } public function isDraft() { return $this->getVisibility() == self::VISIBILITY_DRAFT; } public function getHumanName() { if ($this->isDraft()) { $name = 'draft'; } else { $name = 'post'; } return $name; } public function getCommentsWidget() { $config_data = $this->getConfigData(); if (empty($config_data)) { return 'none'; } return idx($config_data, 'comments_widget', 'none'); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhamePHIDTypePost::TYPECONST); } public static function getVisibilityOptionsForSelect() { return array( self::VISIBILITY_DRAFT => 'Draft: visible only to me.', self::VISIBILITY_PUBLISHED => 'Published: visible to the whole world.', ); } public function getCommentsWidgetOptionsForSelect() { $current = $this->getCommentsWidget(); $options = array(); if ($current == 'facebook' || PhabricatorAuthProviderOAuthFacebook::getFacebookApplicationID()) { $options['facebook'] = 'Facebook'; } if ($current == 'disqus' || PhabricatorEnv::getEnvConfig('disqus.shortname')) { $options['disqus'] = 'Disqus'; } $options['none'] = 'None'; return $options; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // Draft posts are visible only to the author. Published posts are visible // to whoever the blog is visible to. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->isDraft() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } break; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A blog post's author can always view it, and is the only user allowed // to edit it. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return ($user->getPHID() == $this->getBloggerPHID()); } } public function describeAutomaticCapability($capability) { return pht( 'The author of a blog post can always view and edit it.'); } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return $this->getPHID().':'.$field.':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_BODY: return $this->getBody(); case self::MARKUP_FIELD_SUMMARY: return PhabricatorMarkupEngine::summarize($this->getBody()); } } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getBloggerPHID(), ); } } diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index 24e3e7bfa7..4be1043ff9 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -1,146 +1,144 @@ true, self::CONFIG_TIMESTAMPS => false, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhrictionPHIDTypeDocument::TYPECONST); } public static function getSlugURI($slug, $type = 'document') { static $types = array( 'document' => '/w/', 'history' => '/phriction/history/', ); if (empty($types[$type])) { throw new Exception("Unknown URI type '{$type}'!"); } $prefix = $types[$type]; if ($slug == '/') { return $prefix; } else { // NOTE: The effect here is to escape non-latin characters, since modern // browsers deal with escaped UTF8 characters in a reasonable way (showing // the user a readable URI) but older programs may not. $slug = phutil_escape_uri($slug); return $prefix.$slug; } } public function setSlug($slug) { $this->slug = PhabricatorSlug::normalize($slug); $this->depth = PhabricatorSlug::getDepth($slug); return $this; } public function attachContent(PhrictionContent $content) { $this->contentObject = $content; return $this; } public function getContent() { return $this->assertAttached($this->contentObject); } public function getProject() { return $this->assertAttached($this->project); } public function attachProject(PhabricatorProject $project = null) { $this->project = $project; return $this; } public function hasProject() { return (bool)$this->getProject(); } public static function isProjectSlug($slug) { $slug = PhabricatorSlug::normalize($slug); $prefix = 'projects/'; if ($slug == $prefix) { // The 'projects/' document is not itself a project slug. return false; } return !strncmp($slug, $prefix, strlen($prefix)); } public static function getProjectSlugIdentifier($slug) { if (!self::isProjectSlug($slug)) { throw new Exception("Slug '{$slug}' is not a project slug!"); } $slug = PhabricatorSlug::normalize($slug); $parts = explode('/', $slug); return $parts[1].'/'; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($this->hasProject()) { return $this->getProject()->getPolicy($capability); } return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { if ($this->hasProject()) { return $this->getProject()->hasAutomaticCapability($capability, $user); } return false; } public function describeAutomaticCapability($capability) { if ($this->hasProject()) { return pht( "This is a project wiki page, and inherits the project's policies."); } return null; } public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return PhabricatorSubscribersQuery::loadSubscribersForPHID($this->phid); } } diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php b/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php index b010f88d01..041b612757 100644 --- a/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php +++ b/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php @@ -1,45 +1,49 @@ capabilities; } public function getPolicy($capability) { return idx($this->policies, $capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $auto = idx($this->automaticCapabilities, $capability, array()); return idx($auto, $viewer->getPHID()); } public function setCapabilities(array $capabilities) { $this->capabilities = $capabilities; return $this; } public function setPolicies(array $policy_map) { $this->policies = $policy_map; return $this; } public function setAutomaticCapabilities(array $auto_map) { $this->automaticCapabilities = $auto_map; return $this; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index b962049d71..cdaf8af816 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -1,406 +1,396 @@ setViewer($user); $filter->requireCapabilities(array($capability)); $filter->raisePolicyExceptions(true); $filter->apply(array($object)); } public static function hasCapability( PhabricatorUser $user, PhabricatorPolicyInterface $object, $capability) { $filter = new PhabricatorPolicyFilter(); $filter->setViewer($user); $filter->requireCapabilities(array($capability)); $result = $filter->apply(array($object)); return (count($result) == 1); } public function setViewer(PhabricatorUser $user) { $this->viewer = $user; return $this; } public function requireCapabilities(array $capabilities) { $this->capabilities = $capabilities; return $this; } public function raisePolicyExceptions($raise) { $this->raisePolicyExceptions = $raise; return $this; } public function apply(array $objects) { assert_instances_of($objects, 'PhabricatorPolicyInterface'); $viewer = $this->viewer; $capabilities = $this->capabilities; if (!$viewer || !$capabilities) { throw new Exception( 'Call setViewer() and requireCapabilities() before apply()!'); } // If the viewer is omnipotent, short circuit all the checks and just // return the input unmodified. This is an optimization; we know the // result already. if ($viewer->isOmnipotent()) { return $objects; } $filtered = array(); $viewer_phid = $viewer->getPHID(); if (empty($this->userProjects[$viewer_phid])) { $this->userProjects[$viewer_phid] = array(); } $need_projects = array(); $need_policies = array(); foreach ($objects as $key => $object) { $object_capabilities = $object->getCapabilities(); foreach ($capabilities as $capability) { if (!in_array($capability, $object_capabilities)) { throw new Exception( "Testing for capability '{$capability}' on an object which does ". "not have that capability!"); } $policy = $object->getPolicy($capability); $type = phid_get_type($policy); if ($type == PhabricatorProjectPHIDTypeProject::TYPECONST) { $need_projects[$policy] = $policy; } if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { $need_policies[$policy] = $policy; } } } if ($need_policies) { $this->loadCustomPolicies(array_keys($need_policies)); } // If we need projects, check if any of the projects we need are also the // objects we're filtering. Because of how project rules work, this is a // common case. if ($need_projects) { foreach ($objects as $object) { if ($object instanceof PhabricatorProject) { $project_phid = $object->getPHID(); if (isset($need_projects[$project_phid])) { $is_member = $object->isUserMember($viewer_phid); $this->userProjects[$viewer_phid][$project_phid] = $is_member; unset($need_projects[$project_phid]); } } } } if ($need_projects) { $need_projects = array_unique($need_projects); // NOTE: We're using the omnipotent user here to avoid a recursive // descent into madness. We don't actually need to know if the user can // see these projects or not, since: the check is "user is member of // project", not "user can see project"; and membership implies // visibility anyway. Without this, we may load other projects and // re-enter the policy filter and generally create a huge mess. $projects = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withMemberPHIDs(array($viewer->getPHID())) ->withPHIDs($need_projects) ->execute(); foreach ($projects as $project) { $this->userProjects[$viewer_phid][$project->getPHID()] = true; } } foreach ($objects as $key => $object) { $object_capabilities = $object->getCapabilities(); foreach ($capabilities as $capability) { if (!$this->checkCapability($object, $capability)) { // If we're missing any capability, move on to the next object. continue 2; } // If we make it here, we have all of the required capabilities. $filtered[$key] = $object; } } return $filtered; } private function checkCapability( PhabricatorPolicyInterface $object, $capability) { $policy = $object->getPolicy($capability); if (!$policy) { // TODO: Formalize this somehow? $policy = PhabricatorPolicies::POLICY_USER; } if ($policy == PhabricatorPolicies::POLICY_PUBLIC) { // If the object is set to "public" but that policy is disabled for this // install, restrict the policy to "user". if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) { $policy = PhabricatorPolicies::POLICY_USER; } // If the object is set to "public" but the capability is not a public // capability, restrict the policy to "user". $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { $policy = PhabricatorPolicies::POLICY_USER; } } $viewer = $this->viewer; if ($viewer->isOmnipotent()) { return true; } if ($object->hasAutomaticCapability($capability, $viewer)) { return true; } switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return true; case PhabricatorPolicies::POLICY_USER: if ($viewer->getPHID()) { return true; } else { $this->rejectObject($object, $policy, $capability); } break; case PhabricatorPolicies::POLICY_ADMIN: if ($viewer->getIsAdmin()) { return true; } else { $this->rejectObject($object, $policy, $capability); } break; case PhabricatorPolicies::POLICY_NOONE: $this->rejectObject($object, $policy, $capability); break; default: $type = phid_get_type($policy); if ($type == PhabricatorProjectPHIDTypeProject::TYPECONST) { if (!empty($this->userProjects[$viewer->getPHID()][$policy])) { return true; } else { $this->rejectObject($object, $policy, $capability); } } else if ($type == PhabricatorPeoplePHIDTypeUser::TYPECONST) { if ($viewer->getPHID() == $policy) { return true; } else { $this->rejectObject($object, $policy, $capability); } } else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { if ($this->checkCustomPolicy($policy)) { return true; } else { $this->rejectObject($object, $policy, $capability); } } else { // Reject objects with unknown policies. $this->rejectObject($object, false, $capability); } } return false; } public function rejectObject( PhabricatorPolicyInterface $object, $policy, $capability) { if (!$this->raisePolicyExceptions) { return; } $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); $rejection = null; if ($capobj) { $rejection = $capobj->describeCapabilityRejection(); $capability_name = $capobj->getCapabilityName(); } else { $capability_name = $capability; } if (!$rejection) { // We couldn't find the capability object, or it doesn't provide a // tailored rejection string. $rejection = pht( 'You do not have the required capability ("%s") to do whatever you '. 'are trying to do.', $capability); } $more = PhabricatorPolicy::getPolicyExplanation($this->viewer, $policy); $exceptions = $object->describeAutomaticCapability($capability); $details = array_filter(array_merge(array($more), (array)$exceptions)); - // NOTE: Not every policy object has a PHID, just pull an arbitrary - // "unknown object" handle if this fails. We're just using this to provide - // a better error message if we can. - - $phid = '?'; - if (($object instanceof PhabricatorLiskDAO) || - (method_exists($object, 'getPHID'))) { - try { - $phid = $object->getPHID(); - } catch (Exception $ignored) { - // Ignore. - } - } + // NOTE: Not every type of policy object has a real PHID; just load an + // empty handle if a real PHID isn't available. + $phid = nonempty($object->getPHID(), PhabricatorPHIDConstants::PHID_VOID); $handle = id(new PhabricatorHandleQuery()) ->setViewer($this->viewer) ->withPHIDs(array($phid)) ->executeOne(); $object_name = pht( '%s %s', $handle->getTypeName(), $handle->getObjectName()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($is_serious) { $title = pht( 'Access Denied: %s', $object_name); } else { $title = pht( 'You Shall Not Pass: %s', $object_name); } $full_message = pht( '[%s] (%s) %s // %s', $title, $capability_name, $rejection, implode(' ', $details)); $exception = id(new PhabricatorPolicyException($full_message)) ->setTitle($title) ->setRejection($rejection) ->setCapabilityName($capability_name) ->setMoreInfo($details); throw $exception; } private function loadCustomPolicies(array $phids) { $viewer = $this->viewer; $viewer_phid = $viewer->getPHID(); $custom_policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); $custom_policies = mpull($custom_policies, null, 'getPHID'); $classes = array(); $values = array(); foreach ($custom_policies as $policy) { foreach ($policy->getCustomRuleClasses() as $class) { $classes[$class] = $class; $values[$class][] = $policy->getCustomRuleValues($class); } } foreach ($classes as $class => $ignored) { $object = newv($class, array()); $object->willApplyRules($viewer, array_mergev($values[$class])); $classes[$class] = $object; } foreach ($custom_policies as $policy) { $policy->attachRuleObjects($classes); } if (empty($this->customPolicies[$viewer_phid])) { $this->customPolicies[$viewer_phid] = array(); } $this->customPolicies[$viewer->getPHID()] += $custom_policies; } private function checkCustomPolicy($policy_phid) { $viewer = $this->viewer; $viewer_phid = $viewer->getPHID(); $policy = $this->customPolicies[$viewer_phid][$policy_phid]; $objects = $policy->getRuleObjects(); $action = null; foreach ($policy->getRules() as $rule) { $object = idx($objects, idx($rule, 'rule')); if (!$object) { // Reject, this policy has a bogus rule. return false; } // If the user matches this rule, use this action. if ($object->applyRule($viewer, idx($rule, 'value'))) { $action = idx($rule, 'action'); break; } } if ($action === null) { $action = $policy->getDefaultAction(); } if ($action === PhabricatorPolicy::ACTION_ALLOW) { return true; } return false; } } diff --git a/src/applications/policy/interface/PhabricatorPolicyInterface.php b/src/applications/policy/interface/PhabricatorPolicyInterface.php index 73905c7f63..1cab9c29e9 100644 --- a/src/applications/policy/interface/PhabricatorPolicyInterface.php +++ b/src/applications/policy/interface/PhabricatorPolicyInterface.php @@ -1,65 +1,66 @@ getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case PhabricatorPolicyCapability::CAN_JOIN: return $this->getJoinPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isUserMember($viewer->getPHID())) { // Project members can always view a project. return true; } break; case PhabricatorPolicyCapability::CAN_EDIT: break; case PhabricatorPolicyCapability::CAN_JOIN: $can_edit = PhabricatorPolicyCapability::CAN_EDIT; if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) { // Project editors can always join a project. return true; } break; } return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht("Members of a project can always view it."); case PhabricatorPolicyCapability::CAN_JOIN: return pht("Users who can edit a project can always join it."); } return null; } public function isUserMember($user_phid) { return $this->assertAttachedKey($this->sparseMembers, $user_phid); } public function setIsUserMember($user_phid, $is_member) { if ($this->sparseMembers === self::ATTACHABLE) { $this->sparseMembers = array(); } $this->sparseMembers[$user_phid] = $is_member; return $this; } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'subprojectPHIDs' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProjectPHIDTypeProject::TYPECONST); } public function getProfile() { return $this->assertAttached($this->profile); } public function attachProfile(PhabricatorProjectProfile $profile) { $this->profile = $profile; return $this; } public function attachMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } public function getMemberPHIDs() { return $this->assertAttached($this->memberPHIDs); } public function setPhrictionSlug($slug) { // NOTE: We're doing a little magic here and stripping out '/' so that // project pages always appear at top level under projects/ even if the // display name is "Hack / Slash" or similar (it will become // 'hack_slash' instead of 'hack/slash'). $slug = str_replace('/', ' ', $slug); $slug = PhabricatorSlug::normalize($slug); $this->phrictionSlug = $slug; return $this; } public function getFullPhrictionSlug() { $slug = $this->getPhrictionSlug(); return 'projects/'.$slug; } } diff --git a/src/applications/releeph/storage/ReleephBranch.php b/src/applications/releeph/storage/ReleephBranch.php index 799e658b2c..6ebda21414 100644 --- a/src/applications/releeph/storage/ReleephBranch.php +++ b/src/applications/releeph/storage/ReleephBranch.php @@ -1,197 +1,196 @@ true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ReleephPHIDTypeBranch::TYPECONST); } public function getDetail($key, $default = null) { return idx($this->getDetails(), $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function willWriteData(array &$data) { // If symbolicName is omitted, set it to the basename. // // This means that we can enforce symbolicName as a UNIQUE column in the // DB. We'll interpret symbolicName === basename as meaning "no symbolic // name". // // SYMBOLIC_NAME_NOTE if (!$data['symbolicName']) { $data['symbolicName'] = $data['basename']; } parent::willWriteData($data); } public function getSymbolicName() { // See SYMBOLIC_NAME_NOTE above for why this is needed if ($this->symbolicName == $this->getBasename()) { return ''; } return $this->symbolicName; } public function setSymbolicName($name) { if ($name) { parent::setSymbolicName($name); } else { parent::setSymbolicName($this->getBasename()); } return $this; } public function getDisplayName() { if ($sn = $this->getSymbolicName()) { return $sn; } return $this->getBasename(); } public function getDisplayNameWithDetail() { $n = $this->getBasename(); if ($sn = $this->getSymbolicName()) { return "{$sn} ({$n})"; } else { return $n; } } public function getURI($path = null) { $components = array( '/releeph', rawurlencode($this->loadReleephProject()->getName()), rawurlencode($this->getBasename()), $path ); return implode('/', $components); } public function loadReleephProject() { return $this->loadOneRelative( new ReleephProject(), 'id', 'getReleephProjectID'); } private function loadReleephRequestHandles(PhabricatorUser $user, $reqs) { $phids_to_phetch = array(); foreach ($reqs as $rr) { $phids_to_phetch[] = $rr->getRequestCommitPHID(); $phids_to_phetch[] = $rr->getRequestUserPHID(); $phids_to_phetch[] = $rr->getCommitPHID(); $intents = $rr->getUserIntents(); if ($intents) { foreach ($intents as $user_phid => $intent) { $phids_to_phetch[] = $user_phid; } } $request_commit = $rr->loadPhabricatorRepositoryCommit(); if ($request_commit) { $phids_to_phetch[] = $request_commit->getAuthorPHID(); $phids_to_phetch[] = $rr->loadRequestCommitDiffPHID(); } } $handles = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($phids_to_phetch) ->execute(); return $handles; } public function populateReleephRequestHandles(PhabricatorUser $user, $reqs) { $handles = $this->loadReleephRequestHandles($user, $reqs); foreach ($reqs as $req) { $req->setHandles($handles); } } public function loadReleephRequests(PhabricatorUser $user) { $reqs = $this->loadRelatives(new ReleephRequest(), 'branchID'); $this->populateReleephRequestHandles($user, $reqs); return $reqs; } public function isActive() { return $this->getIsActive(); } public function attachProject(ReleephProject $project) { $this->project = $project; return $this; } public function getProject() { return $this->assertAttached($this->project); } public function attachCutPointCommit( PhabricatorRepositoryCommit $commit = null) { $this->cutPointCommit = $commit; return $this; } public function getCutPointCommit() { return $this->assertAttached($this->cutPointCommit); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return $this->getProject()->getCapabilities(); } public function getPolicy($capability) { return $this->getProject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getProject()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/releeph/storage/ReleephProject.php b/src/applications/releeph/storage/ReleephProject.php index eed3cca7a9..acd2c2f788 100644 --- a/src/applications/releeph/storage/ReleephProject.php +++ b/src/applications/releeph/storage/ReleephProject.php @@ -1,201 +1,200 @@ true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ReleephPHIDTypeProject::TYPECONST); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function getURI($path = null) { $components = array( '/releeph/project', $this->getID(), $path ); return implode('/', $components); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function willSaveObject() { // Do this first, to generate the PHID parent::willSaveObject(); $banned_names = $this->getBannedNames(); if (in_array($this->name, $banned_names)) { throw new Exception(sprintf( "The name '%s' is in the list of banned project names!", $this->name, implode(', ', $banned_names))); } if (!$this->getDetail('releaseCounter')) { $this->setDetail('releaseCounter', 0); } } public function loadArcanistProject() { return $this->loadOneRelative( new PhabricatorRepositoryArcanistProject(), 'id', 'getArcanistProjectID'); } public function getPushers() { return $this->getDetail('pushers', array()); } public function isPusher(PhabricatorUser $user) { // TODO Deprecate this once `isPusher` is out of the Facebook codebase. return $this->isAuthoritative($user); } public function isAuthoritative(PhabricatorUser $user) { return $this->isAuthoritativePHID($user->getPHID()); } public function isAuthoritativePHID($phid) { $pushers = $this->getPushers(); if (!$pushers) { return true; } else { return in_array($phid, $pushers); } } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } // TODO: Remove once everything uses ProjectQuery. Also, T603. public function loadPhabricatorRepository() { return $this->loadOneRelative( new PhabricatorRepository(), 'phid', 'getRepositoryPHID'); } public function getCurrentReleaseNumber() { $current_release_numbers = array(); // From the project... $current_release_numbers[] = $this->getDetail('releaseCounter', 0); // From any branches... $branches = id(new ReleephBranch())->loadAllWhere( 'releephProjectID = %d', $this->getID()); if ($branches) { $release_numbers = array(); foreach ($branches as $branch) { $current_release_numbers[] = $branch->getDetail('releaseNumber', 0); } } return max($current_release_numbers); } public function getReleephFieldSelector() { return new ReleephDefaultFieldSelector(); } /** * Wrapper to setIsActive() that logs who deactivated a project */ public function deactivate(PhabricatorUser $actor) { return $this ->setIsActive(0) ->setDetail('last_deactivated_user', $actor->getPHID()) ->setDetail('last_deactivated_time', time()); } // Hide this from the public private function setIsActive($v) { return parent::setIsActive($v); } private function getBannedNames() { return array( 'branch', // no one's tried this... yet! ); } public function isTestFile($filename) { $test_paths = $this->getDetail('testPaths', array()); foreach ($test_paths as $test_path) { if (preg_match($test_path, $filename)) { return true; } } return false; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index fd2486055b..a46e365aa2 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -1,336 +1,335 @@ getPusherIntent() == self::INTENT_WANT && /** * We use "!= pass" instead of "== want" in case the requestor intent is * not present. In other words, only revert if the requestor explicitly * passed. */ $this->getRequestorIntent() != self::INTENT_PASS; } /** * Will return INTENT_WANT if any pusher wants this request, and no pusher * passes on this request. */ public function getPusherIntent() { $project = $this->loadReleephProject(); if (!$project->getPushers()) { return self::INTENT_WANT; } $found_pusher_want = false; foreach ($this->userIntents as $phid => $intent) { if ($project->isAuthoritativePHID($phid)) { if ($intent == self::INTENT_PASS) { return self::INTENT_PASS; } $found_pusher_want = true; } } if ($found_pusher_want) { return self::INTENT_WANT; } else { return null; } } public function getRequestorIntent() { return idx($this->userIntents, $this->requestUserPHID); } public function getStatus() { return $this->calculateStatus(); } private function calculateStatus() { if ($this->shouldBeInBranch()) { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_PICKED; } else { return ReleephRequestStatus::STATUS_NEEDS_PICK; } } else { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_NEEDS_REVERT; } else { $has_been_in_branch = $this->getCommitIdentifier(); // Regardless of why we reverted something, always say reverted if it // was once in the branch. if ($has_been_in_branch) { return ReleephRequestStatus::STATUS_REVERTED; } elseif ($this->getPusherIntent() === ReleephRequest::INTENT_PASS) { // Otherwise, if it has never been in the branch, explicitly say why: return ReleephRequestStatus::STATUS_REJECTED; } elseif ($this->getRequestorIntent() === ReleephRequest::INTENT_WANT) { return ReleephRequestStatus::STATUS_REQUESTED; } else { return ReleephRequestStatus::STATUS_ABANDONED; } } } } /* -( Lisk mechanics )----------------------------------------------------- */ public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'userIntents' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( ReleephPHIDTypeRequest::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( Helpful accessors )--------------------------------------------------- */ public function setHandles($handles) { $this->handles = $handles; return $this; } public function getHandles() { return $this->assertAttached($this->handles); } public function getDetail($key, $default = null) { return idx($this->getDetails(), $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getReason() { // Backward compatibility: reason used to be called comments $reason = $this->getDetail('reason'); if (!$reason) { return $this->getDetail('comments'); } return $reason; } public function getSummary() { /** * Instead, you can use: * - getDetail('summary') // the actual user-chosen summary * - getSummaryForDisplay() // falls back to the original commit title * * Or for the fastidious: * - id(new ReleephSummaryFieldSpecification()) * ->setReleephRequest($rr) * ->getValue() // programmatic equivalent to getDetail() */ throw new Exception( "getSummary() has been deprecated!"); } /** * Allow a null summary, and fall back to the title of the commit. */ public function getSummaryForDisplay() { $summary = $this->getDetail('summary'); if (!$summary) { $pr_commit_data = $this->loadPhabricatorRepositoryCommitData(); if ($pr_commit_data) { $message_lines = explode("\n", $pr_commit_data->getCommitMessage()); $message_lines = array_filter($message_lines); $summary = head($message_lines); } } if (!$summary) { $summary = '(no summary given and commit message empty or unparsed)'; } return $summary; } public function loadRequestCommitDiffPHID() { $phids = array(); $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $commit->getPHID(), PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV); } return head($phids); } /* -( Loading external objects )------------------------------------------- */ public function loadReleephBranch() { return $this->loadOneRelative( new ReleephBranch(), 'id', 'getBranchID'); } public function loadReleephProject() { return $this->loadReleephBranch()->loadReleephProject(); } public function loadPhabricatorRepositoryCommit() { return $this->loadOneRelative( new PhabricatorRepositoryCommit(), 'phid', 'getRequestCommitPHID'); } public function loadPhabricatorRepositoryCommitData() { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { return $commit->loadOneRelative( new PhabricatorRepositoryCommitData(), 'commitID'); } } // TODO: (T603) Get rid of all this one-off ad-hoc loading. public function loadDifferentialRevision() { $diff_phid = $this->loadRequestCommitDiffPHID(); if (!$diff_phid) { return null; } return $this->loadOneRelative( new DifferentialRevision(), 'phid', 'loadRequestCommitDiffPHID'); } /* -( State change helpers )----------------------------------------------- */ public function setUserIntent(PhabricatorUser $user, $intent) { $this->userIntents[$user->getPHID()] = $intent; return $this; } /* -( Migrating to status-less ReleephRequests )--------------------------- */ protected function didReadData() { if ($this->userIntents === null) { $this->userIntents = array(); } } public function setStatus($value) { throw new Exception('`status` is now deprecated!'); } /* -( Make magic Lisk methods private )------------------------------------ */ private function setUserIntents(array $ar) { return parent::setUserIntents($ar); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('releeph.fields'); } public function getCustomFieldBaseClass() { return 'ReleephFieldSpecification'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 32782b65d4..46f7db39de 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1,743 +1,742 @@ true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPHIDTypeRepository::TYPECONST); } public function toDictionary() { return array( 'name' => $this->getName(), 'phid' => $this->getPHID(), 'callsign' => $this->getCallsign(), 'vcs' => $this->getVersionControlSystem(), 'uri' => PhabricatorEnv::getProductionURI($this->getURI()), 'remoteURI' => (string)$this->getPublicRemoteURI(), 'tracking' => $this->getDetail('tracking-enabled'), 'description' => $this->getDetail('description'), ); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function attachCommitCount($count) { $this->commitCount = $count; return $this; } public function getCommitCount() { return $this->assertAttached($this->commitCount); } public function attachMostRecentCommit( PhabricatorRepositoryCommit $commit = null) { $this->mostRecentCommit = $commit; return $this; } public function getMostRecentCommit() { return $this->assertAttached($this->mostRecentCommit); } public function getDiffusionBrowseURIForPath( PhabricatorUser $user, $path, $line = null, $branch = null) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $this, 'path' => $path, 'branch' => $branch, )); return $drequest->generateURI( array( 'action' => 'browse', 'line' => $line, )); } public function getLocalPath() { return $this->getDetail('local-path'); } public function getSubversionBaseURI() { $vcs = $this->getVersionControlSystem(); if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) { throw new Exception("Not a subversion repository!"); } $uri = $this->getDetail('remote-uri'); $subpath = $this->getDetail('svn-subpath'); return $uri.$subpath; } public function execRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('exec_manual', $args); } public function execxRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('execx', $args); } public function getRemoteCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return newv('ExecFuture', $args); } public function passthruRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('phutil_passthru', $args); } public function execLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('exec_manual', $args); } public function execxLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('execx', $args); } public function getLocalCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatLocalCommand($args); return newv('ExecFuture', $args); } public function passthruLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('phutil_passthru', $args); } private function formatRemoteCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); $empty = $this->getEmptyReadableDirectoryPath(); if ($this->shouldUseSSH()) { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "SVN_SSH=%s svn --non-interactive {$pattern}"; array_unshift( $args, csprintf( 'ssh -l %P -i %P', new PhutilOpaqueEnvelope($this->getSSHLogin()), new PhutilOpaqueEnvelope($this->getSSHKeyfile()))); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $command = call_user_func_array( 'csprintf', array_merge( array( "(ssh-add %P && HOME=%s git {$pattern})", new PhutilOpaqueEnvelope($this->getSSHKeyfile()), $empty, ), $args)); $pattern = "ssh-agent sh -c %s"; $args = array($command); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $pattern = "hg --config ui.ssh=%s {$pattern}"; array_unshift( $args, csprintf( 'ssh -l %P -i %P', new PhutilOpaqueEnvelope($this->getSSHLogin()), new PhutilOpaqueEnvelope($this->getSSHKeyfile()))); break; default: throw new Exception("Unrecognized version control system."); } } else if ($this->shouldUseHTTP()) { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn ". "--non-interactive ". "--no-auth-cache ". "--trust-server-cert ". "--username %P ". "--password %P ". $pattern; array_unshift( $args, new PhutilOpaqueEnvelope($this->getDetail('http-login')), new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); break; default: throw new Exception( "No support for HTTP Basic Auth in this version control system."); } } else if ($this->shouldUseSVNProtocol()) { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn ". "--non-interactive ". "--no-auth-cache ". "--username %P ". "--password %P ". $pattern; array_unshift( $args, new PhutilOpaqueEnvelope($this->getDetail('http-login')), new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); break; default: throw new Exception( "SVN protocol is SVN only."); } } else { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn --non-interactive {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "HOME=%s git {$pattern}"; array_unshift($args, $empty); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $pattern = "hg {$pattern}"; break; default: throw new Exception("Unrecognized version control system."); } } array_unshift($args, $pattern); return $args; } private function formatLocalCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); $empty = $this->getEmptyReadableDirectoryPath(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "(cd %s && svn --non-interactive {$pattern})"; array_unshift($args, $this->getLocalPath()); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "(cd %s && HOME=%s git {$pattern})"; array_unshift($args, $this->getLocalPath(), $empty); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $hgplain = (phutil_is_windows() ? "set HGPLAIN=1 &&" : "HGPLAIN=1"); $pattern = "(cd %s && {$hgplain} hg {$pattern})"; array_unshift($args, $this->getLocalPath()); break; default: throw new Exception("Unrecognized version control system."); } array_unshift($args, $pattern); return $args; } private function getEmptyReadableDirectoryPath() { // See T2965. Some time after Git 1.7.5.4, Git started fataling if it can // not read $HOME. For many users, $HOME points at /root (this seems to be // a default result of Apache setup). Instead, explicitly point $HOME at a // readable, empty directory so that Git looks for the config file it's // after, fails to locate it, and moves on. This is really silly, but seems // like the least damaging approach to mitigating the issue. $root = dirname(phutil_get_library_root('phabricator')); return $root.'/support/empty/'; } private function getSSHLogin() { return $this->getDetail('ssh-login'); } private function getSSHKeyfile() { if ($this->sshKeyfile === null) { $key = $this->getDetail('ssh-key'); $keyfile = $this->getDetail('ssh-keyfile'); if ($keyfile) { // Make sure we can read the file, that it exists, etc. Filesystem::readFile($keyfile); $this->sshKeyfile = $keyfile; } else if ($key) { $keyfile = new TempFile('phabricator-repository-ssh-key'); chmod($keyfile, 0600); Filesystem::writeFile($keyfile, $key); $this->sshKeyfile = $keyfile; } else { $this->sshKeyfile = ''; } } return (string)$this->sshKeyfile; } public function getURI() { return '/diffusion/'.$this->getCallsign().'/'; } public function isTracked() { return $this->getDetail('tracking-enabled', false); } public function getDefaultBranch() { $default = $this->getDetail('default-branch'); if (strlen($default)) { return $default; } $default_branches = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master', PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default', ); return idx($default_branches, $this->getVersionControlSystem()); } public function getDefaultArcanistBranch() { return coalesce($this->getDefaultBranch(), 'svn'); } private function isBranchInFilter($branch, $filter_key) { $vcs = $this->getVersionControlSystem(); $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); $use_filter = ($is_git); if ($use_filter) { $filter = $this->getDetail($filter_key, array()); if ($filter && empty($filter[$branch])) { return false; } } // By default, all branches pass. return true; } public function shouldTrackBranch($branch) { return $this->isBranchInFilter($branch, 'branch-filter'); } public function shouldAutocloseBranch($branch) { if ($this->getDetail('disable-autoclose', false)) { return false; } return $this->isBranchInFilter($branch, 'close-commits-filter'); } public function shouldAutocloseCommit( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { if ($this->getDetail('disable-autoclose', false)) { return false; } switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return true; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return true; default: throw new Exception("Unrecognized version control system."); } $branches = $data->getCommitDetail('seenOnBranches', array()); foreach ($branches as $branch) { if ($this->shouldAutocloseBranch($branch)) { return true; } } return false; } public function formatCommitName($commit_identifier) { $vcs = $this->getVersionControlSystem(); $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; $is_git = ($vcs == $type_git); $is_hg = ($vcs == $type_hg); if ($is_git || $is_hg) { $short_identifier = substr($commit_identifier, 0, 12); } else { $short_identifier = $commit_identifier; } return 'r'.$this->getCallsign().$short_identifier; } /* -( Repository URI Management )------------------------------------------ */ /** * Get the remote URI for this repository. * * @return string * @task uri */ public function getRemoteURI() { return (string)$this->getRemoteURIObject(); } /** * Get the remote URI for this repository, without authentication information. * * @return string Repository URI. * @task uri */ public function getPublicRemoteURI() { $uri = $this->getRemoteURIObject(); // Make sure we don't leak anything if this repo is using HTTP Basic Auth // with the credentials in the URI or something zany like that. if ($uri instanceof PhutilGitURI) { if (!$this->getDetail('show-user', false)) { $uri->setUser(null); } } else { if (!$this->getDetail('show-user', false)) { $uri->setUser(null); } $uri->setPass(null); } return (string)$uri; } /** * Get the protocol for the repository's remote. * * @return string Protocol, like "ssh" or "git". * @task uri */ public function getRemoteProtocol() { $uri = $this->getRemoteURIObject(); if ($uri instanceof PhutilGitURI) { return 'ssh'; } else { return $uri->getProtocol(); } } /** * Get a parsed object representation of the repository's remote URI. This * may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git * URI (returned as a @{class@libphutil:PhutilGitURI}). * * @return wild A @{class@libphutil:PhutilURI} or * @{class@libphutil:PhutilGitURI}. * @task uri */ private function getRemoteURIObject() { $raw_uri = $this->getDetail('remote-uri'); if (!$raw_uri) { return new PhutilURI(''); } if (!strncmp($raw_uri, '/', 1)) { return new PhutilURI('file://'.$raw_uri); } $uri = new PhutilURI($raw_uri); if ($uri->getProtocol()) { if ($this->isSSHProtocol($uri->getProtocol())) { if ($this->getSSHLogin()) { $uri->setUser($this->getSSHLogin()); } } return $uri; } $uri = new PhutilGitURI($raw_uri); if ($uri->getDomain()) { if ($this->getSSHLogin()) { $uri->setUser($this->getSSHLogin()); } return $uri; } throw new Exception("Remote URI '{$raw_uri}' could not be parsed!"); } /** * Determine if we should connect to the remote using SSH flags and * credentials. * * @return bool True to use the SSH protocol. * @task uri */ private function shouldUseSSH() { $protocol = $this->getRemoteProtocol(); if ($this->isSSHProtocol($protocol)) { return (bool)$this->getSSHKeyfile(); } else { return false; } } /** * Determine if we should connect to the remote using HTTP flags and * credentials. * * @return bool True to use the HTTP protocol. * @task uri */ private function shouldUseHTTP() { $protocol = $this->getRemoteProtocol(); if ($protocol == 'http' || $protocol == 'https') { return (bool)$this->getDetail('http-login'); } else { return false; } } /** * Determine if we should connect to the remote using SVN flags and * credentials. * * @return bool True to use the SVN protocol. * @task uri */ private function shouldUseSVNProtocol() { $protocol = $this->getRemoteProtocol(); if ($protocol == 'svn') { return (bool)$this->getDetail('http-login'); } else { return false; } } /** * Determine if a protocol is SSH or SSH-like. * * @param string A protocol string, like "http" or "ssh". * @return bool True if the protocol is SSH-like. * @task uri */ private function isSSHProtocol($protocol) { return ($protocol == 'ssh' || $protocol == 'svn+ssh'); } public function delete() { $this->openTransaction(); $paths = id(new PhabricatorOwnersPath()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($paths as $path) { $path->delete(); } $projects = id(new PhabricatorRepositoryArcanistProject()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($projects as $project) { // note each project deletes its PhabricatorRepositorySymbols $project->delete(); } $commits = id(new PhabricatorRepositoryCommit()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($commits as $commit) { // note PhabricatorRepositoryAuditRequests and // PhabricatorRepositoryCommitData are deleted here too. $commit->delete(); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_FILESYSTEM, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_PATHCHANGE, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_SUMMARY, $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function isGit() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); } public function isSVN() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); } public function isHg() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL); } /* -( 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) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field)); return "repo:{$hash}"; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { return $this->getDetail('description'); } 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 true; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php b/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php index 37ee0f6ebc..444cb10c4a 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php +++ b/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php @@ -1,94 +1,93 @@ true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_SERIALIZATION => array( 'symbolIndexLanguages' => self::SERIALIZATION_JSON, 'symbolIndexProjects' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPHIDTypeArcanistProject::TYPECONST); } // TODO: Remove. Also, T603. public function loadRepository() { if (!$this->getRepositoryID()) { return null; } return id(new PhabricatorRepository())->load($this->getRepositoryID()); } public function delete() { $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE arcanistProjectID = %d', id(new PhabricatorRepositorySymbol())->getTableName(), $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_USER; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_ADMIN; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index ad83b8c32e..4755b2f6ee 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1,1791 +1,1796 @@ setName('Sawyer') * ->setBreed('Pug') * ->save(); * * Note that **Lisk automatically builds getters and setters for all of your * object's protected properties** via @{method:__call}. If you want to add * custom behavior to your getters or setters, you can do so by overriding the * @{method:readField} and @{method:writeField} methods. * * Calling @{method:save} will persist the object to the database. After calling * @{method:save}, you can call @{method:getID} to retrieve the object's ID. * * To load objects by ID, use the @{method:load} method: * * $dog = id(new Dog())->load($id); * * This will load the Dog record with ID $id into $dog, or ##null## if no such * record exists (@{method:load} is an instance method rather than a static * method because PHP does not support late static binding, at least until PHP * 5.3). * * To update an object, change its properties and save it: * * $dog->setBreed('Lab')->save(); * * To delete an object, call @{method:delete}: * * $dog->delete(); * * That's Lisk CRUD in a nutshell. * * = Queries = * * Often, you want to load a bunch of objects, or execute a more specialized * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this: * * $pugs = $dog->loadAllWhere('breed = %s', 'Pug'); * $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer'); * * These methods work like @{function@libphutil:queryfx}, but only take half of * a query (the part after the WHERE keyword). Lisk will handle the connection, * columns, and object construction; you are responsible for the rest of it. * @{method:loadAllWhere} returns a list of objects, while * @{method:loadOneWhere} returns a single object (or `null`). * * There's also a @{method:loadRelatives} method which helps to prevent the 1+N * queries problem. * * = Managing Transactions = * * Lisk uses a transaction stack, so code does not generally need to be aware * of the transactional state of objects to implement correct transaction * semantics: * * $obj->openTransaction(); * $obj->save(); * $other->save(); * // ... * $other->openTransaction(); * $other->save(); * $another->save(); * if ($some_condition) { * $other->saveTransaction(); * } else { * $other->killTransaction(); * } * // ... * $obj->saveTransaction(); * * Assuming ##$obj##, ##$other## and ##$another## live on the same database, * this code will work correctly by establishing savepoints. * * Selects whose data are used later in the transaction should be included in * @{method:beginReadLocking} or @{method:beginWriteLocking} block. * * @task conn Managing Connections * @task config Configuring Lisk * @task load Loading Objects * @task info Examining Objects * @task save Writing Objects * @task hook Hooks and Callbacks * @task util Utilities * @task xaction Managing Transactions * @task isolate Isolation for Unit Testing * * @group storage */ abstract class LiskDAO { const CONFIG_IDS = 'id-mechanism'; const CONFIG_TIMESTAMPS = 'timestamps'; const CONFIG_AUX_PHID = 'auxiliary-phid'; const CONFIG_SERIALIZATION = 'col-serialization'; const CONFIG_PARTIAL_OBJECTS = 'partial-objects'; const SERIALIZATION_NONE = 'id'; const SERIALIZATION_JSON = 'json'; const SERIALIZATION_PHP = 'php'; const IDS_AUTOINCREMENT = 'ids-auto'; const IDS_COUNTER = 'ids-counter'; const IDS_MANUAL = 'ids-manual'; const COUNTER_TABLE_NAME = 'lisk_counter'; private $__dirtyFields = array(); private $__missingFields = array(); private static $processIsolationLevel = 0; private static $transactionIsolationLevel = 0; private $__ephemeral = false; private static $connections = array(); private $inSet = null; protected $id; protected $phid; protected $dateCreated; protected $dateModified; /** * Build an empty object. * * @return obj Empty object. */ public function __construct() { $id_key = $this->getIDKey(); if ($id_key) { $this->$id_key = null; } if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { $this->resetDirtyFields(); } } /* -( Managing Connections )----------------------------------------------- */ /** * Establish a live connection to a database service. This method should * return a new connection. Lisk handles connection caching and management; * do not perform caching deeper in the stack. * * @param string Mode, either 'r' (reading) or 'w' (reading and writing). * @return AphrontDatabaseConnection New database connection. * @task conn */ abstract protected function establishLiveConnection($mode); /** * Return a namespace for this object's connections in the connection cache. * Generally, the database name is appropriate. Two connections are considered * equivalent if they have the same connection namespace and mode. * * @return string Connection namespace for cache * @task conn */ abstract protected function getConnectionNamespace(); /** * Get an existing, cached connection for this object. * * @param mode Connection mode. * @return AprontDatabaseConnection|null Connection, if it exists in cache. * @task conn */ protected function getEstablishedConnection($mode) { $key = $this->getConnectionNamespace().':'.$mode; if (isset(self::$connections[$key])) { return self::$connections[$key]; } return null; } /** * Store a connection in the connection cache. * * @param mode Connection mode. * @param AphrontDatabaseConnection Connection to cache. * @return this * @task conn */ protected function setEstablishedConnection( $mode, AphrontDatabaseConnection $connection, $force_unique = false) { $key = $this->getConnectionNamespace().':'.$mode; if ($force_unique) { $key .= ':unique'; while (isset(self::$connections[$key])) { $key .= '!'; } } self::$connections[$key] = $connection; return $this; } /* -( Configuring Lisk )--------------------------------------------------- */ /** * Change Lisk behaviors, like ID configuration and timestamps. If you want * to change these behaviors, you should override this method in your child * class and change the options you're interested in. For example: * * public function getConfiguration() { * return array( * Lisk_DataAccessObject::CONFIG_EXAMPLE => true, * ) + parent::getConfiguration(); * } * * The available options are: * * CONFIG_IDS * Lisk objects need to have a unique identifying ID. The three mechanisms * available for generating this ID are IDS_AUTOINCREMENT (default, assumes * the ID column is an autoincrement primary key), IDS_MANUAL (you are taking * full responsibility for ID management), or IDS_COUNTER (see below). * * InnoDB does not persist the value of `auto_increment` across restarts, * and instead initializes it to `MAX(id) + 1` during startup. This means it * may reissue the same autoincrement ID more than once, if the row is deleted * and then the database is restarted. To avoid this, you can set an object to * use a counter table with IDS_COUNTER. This will generally behave like * IDS_AUTOINCREMENT, except that the counter value will persist across * restarts and inserts will be slightly slower. If a database stores any * DAOs which use this mechanism, you must create a table there with this * schema: * * CREATE TABLE lisk_counter ( * counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY, * counterValue BIGINT UNSIGNED NOT NULL * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; * * CONFIG_TIMESTAMPS * Lisk can automatically handle keeping track of a `dateCreated' and * `dateModified' column, which it will update when it creates or modifies * an object. If you don't want to do this, you may disable this option. * By default, this option is ON. * * CONFIG_AUX_PHID * This option can be enabled by being set to some truthy value. The meaning * of this value is defined by your PHID generation mechanism. If this option * is enabled, a `phid' property will be populated with a unique PHID when an * object is created (or if it is saved and does not currently have one). You * need to override generatePHID() and hook it into your PHID generation * mechanism for this to work. By default, this option is OFF. * * CONFIG_SERIALIZATION * You can optionally provide a column serialization map that will be applied * to values when they are written to the database. For example: * * self::CONFIG_SERIALIZATION => array( * 'complex' => self::SERIALIZATION_JSON, * ) * * This will cause Lisk to JSON-serialize the 'complex' field before it is * written, and unserialize it when it is read. * * CONFIG_PARTIAL_OBJECTS * Sometimes, it is useful to load only some fields of an object (such as * when you are loading all objects of a class, but only care about a few * fields). Turning on this option (by setting it to a truthy value) allows * users of the class to create/use partial objects, but it comes with some * side effects: your class cannot override the setters and getters provided * by Lisk (use readField and writeField instead), and you should not * directly access or assign protected members of your class (use the getters * and setters). * * @return dictionary Map of configuration options to values. * * @task config */ protected function getConfiguration() { return array( self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, self::CONFIG_PARTIAL_OBJECTS => false, ); } /** * Determine the setting of a configuration option for this class of objects. * * @param const Option name, one of the CONFIG_* constants. * @return mixed Option value, if configured (null if unavailable). * * @task config */ public function getConfigOption($option_name) { static $options = null; if (!isset($options)) { $options = $this->getConfiguration(); } return idx($options, $option_name); } /* -( Loading Objects )---------------------------------------------------- */ /** * Load an object by ID. You need to invoke this as an instance method, not * a class method, because PHP doesn't have late static binding (until * PHP 5.3.0). For example: * * $dog = id(new Dog())->load($dog_id); * * @param int Numeric ID identifying the object to load. * @return obj|null Identified object, or null if it does not exist. * * @task load */ public function load($id) { if (is_object($id)) { $id = (string)$id; } if (!$id || (!is_int($id) && !ctype_digit($id))) { return null; } return $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $id); } /** * Loads all of the objects, unconditionally. * * @return dict Dictionary of all persisted objects of this type, keyed * on object ID. * * @task load */ public function loadAll() { return $this->loadAllWhere('1 = 1'); } /** * Loads all objects, but only fetches the specified columns. * * @param array Array of canonical column names as strings * @return dict Dictionary of all objects, keyed by ID. * * @task load */ public function loadColumns(array $columns) { return $this->loadColumnsWhere($columns, '1 = 1'); } /** * Load all objects which match a WHERE clause. You provide everything after * the 'WHERE'; Lisk handles everything up to it. For example: * * $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7); * * The pattern and arguments are as per queryfx(). * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objects, keyed on ID. * * @task load */ public function loadAllWhere($pattern/* , $arg, $arg, $arg ... */) { $args = func_get_args(); array_unshift($args, null); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Loads selected columns from objects that match a WHERE clause. You must * provide everything after the WHERE. See loadAllWhere(). * * @param array List of column names. * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objecks, keyed by ID. * * @task load */ public function loadColumnsWhere(array $columns, $pattern/* , $args... */) { if (!$this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { throw new BadMethodCallException( "This class does not support partial objects."); } $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Load a single object identified by a 'WHERE' clause. You provide * everything after the 'WHERE', and Lisk builds the first half of the * query. See loadAllWhere(). This method is similar, but returns a single * result instead of a list. * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return obj|null Matching object, or null if no object matches. * * @task load */ public function loadOneWhere($pattern/* , $arg, $arg, $arg ... */) { $args = func_get_args(); array_unshift($args, null); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); if (count($data) > 1) { throw new AphrontQueryCountException( "More than 1 result from loadOneWhere()!"); } $data = reset($data); if (!$data) { return null; } return $this->loadFromArray($data); } protected function loadRawDataWhere($columns, $pattern/* , $args... */) { $connection = $this->establishConnection('r'); $lock_clause = ''; if ($connection->isReadLocking()) { $lock_clause = 'FOR UPDATE'; } else if ($connection->isWriteLocking()) { $lock_clause = 'LOCK IN SHARE MODE'; } $args = func_get_args(); $args = array_slice($args, 2); if (!$columns) { $column = '*'; } else { $column = '%LC'; $columns[] = $this->getIDKey(); $properties = $this->getProperties(); $this->__missingFields = array_diff_key( array_flip($properties), array_flip($columns)); } $pattern = 'SELECT '.$column.' FROM %T WHERE '.$pattern.' %Q'; array_unshift($args, $this->getTableName()); if ($columns) { array_unshift($args, $columns); } array_push($args, $lock_clause); array_unshift($args, $pattern); return call_user_func_array( array($connection, 'queryData'), $args); } /** * Reload an object from the database, discarding any changes to persistent * properties. This is primarily useful after entering a transaction but * before applying changes to an object. * * @return this * * @task load */ public function reload() { if (!$this->getID()) { throw new Exception("Unable to reload object that hasn't been loaded!"); } $result = $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $this->getID()); if (!$result) { throw new AphrontQueryObjectMissingException(); } return $this; } /** * Initialize this object's properties from a dictionary. Generally, you * load single objects with loadOneWhere(), but sometimes it may be more * convenient to pull data from elsewhere directly (e.g., a complicated * join via queryData()) and then load from an array representation. * * @param dict Dictionary of properties, which should be equivalent to * selecting a row from the table or calling getProperties(). * @return this * * @task load */ public function loadFromArray(array $row) { static $valid_properties = array(); $map = array(); foreach ($row as $k => $v) { // We permit (but ignore) extra properties in the array because a // common approach to building the array is to issue a raw SELECT query // which may include extra explicit columns or joins. // This pathway is very hot on some pages, so we're inlining a cache // and doing some microoptimization to avoid a strtolower() call for each // assignment. The common path (assigning a valid property which we've // already seen) always incurs only one empty(). The second most common // path (assigning an invalid property which we've already seen) costs // an empty() plus an isset(). if (empty($valid_properties[$k])) { if (isset($valid_properties[$k])) { // The value is set but empty, which means it's false, so we've // already determined it's not valid. We don't need to check again. continue; } $valid_properties[$k] = $this->hasProperty($k); if (!$valid_properties[$k]) { continue; } } $map[$k] = $v; } $this->willReadData($map); foreach ($map as $prop => $value) { $this->$prop = $value; } $this->didReadData(); return $this; } /** * Initialize a list of objects from a list of dictionaries. Usually you * load lists of objects with loadAllWhere(), but sometimes that isn't * flexible enough. One case is if you need to do joins to select the right * objects: * * function loadAllWithOwner($owner) { * $data = $this->queryData( * 'SELECT d.* * FROM owner o * JOIN owner_has_dog od ON o.id = od.ownerID * JOIN dog d ON od.dogID = d.id * WHERE o.id = %d', * $owner); * return $this->loadAllFromArray($data); * } * * This is a lot messier than loadAllWhere(), but more flexible. * * @param list List of property dictionaries. * @return dict List of constructed objects, keyed on ID. * * @task load */ public function loadAllFromArray(array $rows) { $result = array(); $id_key = $this->getIDKey(); foreach ($rows as $row) { $obj = clone $this; if ($id_key && isset($row[$id_key])) { $result[$row[$id_key]] = $obj->loadFromArray($row); } else { $result[] = $obj->loadFromArray($row); } if ($this->inSet) { $this->inSet->addToSet($obj); } } return $result; } /** * This method helps to prevent the 1+N queries problem. It happens when you * execute a query for each row in a result set. Like in this code: * * COUNTEREXAMPLE, name=Easy to write but expensive to execute * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * foreach ($diffs as $diff) { * $changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID = %d', * $diff->getID()); * // Do something with $changesets. * } * * One can solve this problem by reading all the dependent objects at once and * assigning them later: * * COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * $all_changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID IN (%Ld)', * mpull($diffs, 'getID')); * $all_changesets = mgroup($all_changesets, 'getDiffID'); * foreach ($diffs as $diff) { * $changesets = idx($all_changesets, $diff->getID(), array()); * // Do something with $changesets. * } * * The method @{method:loadRelatives} abstracts this approach which allows * writing a code which is simple and efficient at the same time: * * name=Easy to write and cheap to execute * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * // Do something with $changesets. * } * * This will load dependent objects for all diffs in the first call of * @{method:loadRelatives} and use this result for all following calls. * * The method supports working with set of sets, like in this code: * * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * foreach ($changesets as $changeset) { * $hunks = $changeset->loadRelatives( * new DifferentialHunk(), * 'changesetID'); * // Do something with hunks. * } * } * * This code will execute just three queries - one to load all diffs, one to * load all their related changesets and one to load all their related hunks. * You can try to write an equivalent code without using this method as * a homework. * * The method also supports retrieving referenced objects, for example authors * of all diffs (using shortcut @{method:loadOneRelative}): * * foreach ($diffs as $diff) { * $author = $diff->loadOneRelative( * new PhabricatorUser(), * 'phid', * 'getAuthorPHID'); * // Do something with author. * } * * It is also possible to specify additional conditions for the `WHERE` * clause. Similarly to @{method:loadAllWhere}, you can specify everything * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is * allowed to pass only a constant string (`%` doesn't have a special * meaning). This is intentional to avoid mistakes with using data from one * row in retrieving other rows. Example of a correct usage: * * $status = $author->loadOneRelative( * new PhabricatorUserStatus(), * 'userPHID', * 'getPHID', * '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)'); * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return list Objects of type $object. * * @task load */ public function loadRelatives( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { if (!$this->inSet) { id(new LiskDAOSet())->addToSet($this); } $relatives = $this->inSet->loadRelatives( $object, $foreign_column, $key_method, $where); return idx($relatives, $this->$key_method(), array()); } /** * Load referenced row. See @{method:loadRelatives} for details. * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return LiskDAO Object of type $object or null if there's no such object. * * @task load */ final public function loadOneRelative( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { $relatives = $this->loadRelatives( $object, $foreign_column, $key_method, $where); if (!$relatives) { return null; } if (count($relatives) > 1) { throw new AphrontQueryCountException( "More than 1 result from loadOneRelative()!"); } return reset($relatives); } final public function putInSet(LiskDAOSet $set) { $this->inSet = $set; return $this; } final protected function getInSet() { return $this->inSet; } /* -( Examining Objects )-------------------------------------------------- */ /** * Set unique ID identifying this object. You normally don't need to call this * method unless with `IDS_MANUAL`. * * @param mixed Unique ID. * @return this * @task save */ public function setID($id) { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } $this->$id_key = $id; return $this; } /** * Retrieve the unique ID identifying this object. This value will be null if * the object hasn't been persisted and you didn't set it manually. * * @return mixed Unique ID. * * @task info */ public function getID() { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } return $this->$id_key; } + public function getPHID() { + return $this->phid; + } + + /** * Test if a property exists. * * @param string Property name. * @return bool True if the property exists. * @task info */ public function hasProperty($property) { return (bool)$this->checkProperty($property); } /** * Retrieve a list of all object properties. This list only includes * properties that are declared as protected, and it is expected that * all properties returned by this function should be persisted to the * database. * Properties that should not be persisted must be declared as private. * * @return dict Dictionary of normalized (lowercase) to canonical (original * case) property names. * * @task info */ protected function getProperties() { static $properties = null; if (!isset($properties)) { $class = new ReflectionClass(get_class($this)); $properties = array(); foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { $properties[strtolower($p->getName())] = $p->getName(); } $id_key = $this->getIDKey(); if ($id_key != 'id') { unset($properties['id']); } if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) { unset($properties['datecreated']); unset($properties['datemodified']); } if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) { unset($properties['phid']); } } return $properties; } /** * Check if a property exists on this object. * * @return string|null Canonical property name, or null if the property * does not exist. * * @task info */ protected function checkProperty($property) { static $properties = null; if ($properties === null) { $properties = $this->getProperties(); } $property = strtolower($property); if (empty($properties[$property])) { return null; } return $properties[$property]; } /** * Get or build the database connection for this object. * * @param string 'r' for read, 'w' for read/write. * @param bool True to force a new connection. The connection will not * be retrieved from or saved into the connection cache. * @return LiskDatabaseConnection Lisk connection object. * * @task info */ public function establishConnection($mode, $force_new = false) { if ($mode != 'r' && $mode != 'w') { throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'."); } if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) { $mode = 'isolate-'.$mode; $connection = $this->getEstablishedConnection($mode); if (!$connection) { $connection = $this->establishIsolatedConnection($mode); $this->setEstablishedConnection($mode, $connection); } return $connection; } if (self::shouldIsolateAllLiskEffectsToTransactions()) { // If we're doing fixture transaction isolation, force the mode to 'w' // so we always get the same connection for reads and writes, and thus // can see the writes inside the transaction. $mode = 'w'; } // TODO: There is currently no protection on 'r' queries against writing. $connection = null; if (!$force_new) { if ($mode == 'r') { // If we're requesting a read connection but already have a write // connection, reuse the write connection so that reads can take place // inside transactions. $connection = $this->getEstablishedConnection('w'); } if (!$connection) { $connection = $this->getEstablishedConnection($mode); } } if (!$connection) { $connection = $this->establishLiveConnection($mode); if (self::shouldIsolateAllLiskEffectsToTransactions()) { $connection->openTransaction(); } $this->setEstablishedConnection( $mode, $connection, $force_unique = $force_new); } return $connection; } /** * Convert this object into a property dictionary. This dictionary can be * restored into an object by using loadFromArray() (unless you're using * legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should * just go ahead and die in a fire). * * @return dict Dictionary of object properties. * * @task info */ protected function getPropertyValues() { $map = array(); foreach ($this->getProperties() as $p) { // We may receive a warning here for properties we've implicitly added // through configuration; squelch it. $map[$p] = @$this->$p; } return $map; } /* -( Writing Objects )---------------------------------------------------- */ /** * Make an object read-only. * * Making an object ephemeral indicates that you will be changing state in * such a way that you would never ever want it to be written back to the * storage. */ public function makeEphemeral() { $this->__ephemeral = true; return $this; } private function isEphemeralCheck() { if ($this->__ephemeral) { throw new LiskEphemeralObjectException(); } } /** * Persist this object to the database. In most cases, this is the only * method you need to call to do writes. If the object has not yet been * inserted this will do an insert; if it has, it will do an update. * * @return this * * @task save */ public function save() { if ($this->shouldInsertWhenSaved()) { return $this->insert(); } else { return $this->update(); } } /** * Save this object, forcing the query to use REPLACE regardless of object * state. * * @return this * * @task save */ public function replace() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('REPLACE'); } /** * Save this object, forcing the query to use INSERT regardless of object * state. * * @return this * * @task save */ public function insert() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('INSERT'); } /** * Save this object, forcing the query to use UPDATE regardless of object * state. * * @return this * * @task save */ public function update() { $this->isEphemeralCheck(); $this->willSaveObject(); $data = $this->getPropertyValues(); if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { $data = array_intersect_key($data, $this->__dirtyFields); } $this->willWriteData($data); $map = array(); foreach ($data as $k => $v) { $map[$k] = $v; } $conn = $this->establishConnection('w'); foreach ($map as $key => $value) { $map[$key] = qsprintf($conn, '%C = %ns', $key, $value); } $map = implode(', ', $map); $id = $this->getID(); $conn->query( 'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'), $this->getTableName(), $map, $this->getIDKeyForUse(), $id); // We can't detect a missing object because updating an object without // changing any values doesn't affect rows. We could jiggle timestamps // to catch this for objects which track them if we wanted. $this->didWriteData(); if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { $this->resetDirtyFields(); } return $this; } /** * Delete this object, permanently. * * @return this * * @task save */ public function delete() { $this->isEphemeralCheck(); $this->willDelete(); $conn = $this->establishConnection('w'); $conn->query( 'DELETE FROM %T WHERE %C = %d', $this->getTableName(), $this->getIDKeyForUse(), $this->getID()); $this->didDelete(); return $this; } /** * Internal implementation of INSERT and REPLACE. * * @param const Either "INSERT" or "REPLACE", to force the desired mode. * * @task save */ protected function insertRecordIntoDatabase($mode) { $this->willSaveObject(); $data = $this->getPropertyValues(); $conn = $this->establishConnection('w'); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); switch ($id_mechanism) { case self::IDS_AUTOINCREMENT: // If we are using autoincrement IDs, let MySQL assign the value for the // ID column, if it is empty. If the caller has explicitly provided a // value, use it. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { unset($data[$id_key]); } break; case self::IDS_COUNTER: // If we are using counter IDs, assign a new ID if we don't already have // one. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { $counter_name = $this->getTableName(); $id = self::loadNextCounterID($conn, $counter_name); $this->setID($id); $data[$id_key] = $id; } break; case self::IDS_MANUAL: break; default: throw new Exception('Unknown CONFIG_IDs mechanism!'); } $this->willWriteData($data); $columns = array_keys($data); foreach ($data as $key => $value) { $data[$key] = qsprintf($conn, '%ns', $value); } $data = implode(', ', $data); $conn->query( '%Q INTO %T (%LC) VALUES (%Q)', $mode, $this->getTableName(), $columns, $data); // Only use the insert id if this table is using auto-increment ids if ($id_mechanism === self::IDS_AUTOINCREMENT) { $this->setID($conn->getInsertID()); } $this->didWriteData(); if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { $this->resetDirtyFields(); } return $this; } /** * Method used to determine whether to insert or update when saving. * * @return bool true if the record should be inserted */ protected function shouldInsertWhenSaved() { $key_type = $this->getConfigOption(self::CONFIG_IDS); if ($key_type == self::IDS_MANUAL) { throw new Exception( 'You are using manual IDs. You must override the '. 'shouldInsertWhenSaved() method to properly detect '. 'when to insert a new record.'); } else { return !$this->getID(); } } /* -( Hooks and Callbacks )------------------------------------------------ */ /** * Retrieve the database table name. By default, this is the class name. * * @return string Table name for object storage. * * @task hook */ public function getTableName() { return get_class($this); } /** * Retrieve the primary key column, "id" by default. If you can not * reasonably name your ID column "id", override this method. * * @return string Name of the ID column. * * @task hook */ public function getIDKey() { return 'id'; } protected function getIDKeyForUse() { $id_key = $this->getIDKey(); if (!$id_key) { throw new Exception( "This DAO does not have a single-part primary key. The method you ". "called requires a single-part primary key."); } return $id_key; } /** * Generate a new PHID, used by CONFIG_AUX_PHID. * * @return phid Unique, newly allocated PHID. * * @task hook */ protected function generatePHID() { throw new Exception( "To use CONFIG_AUX_PHID, you need to overload ". "generatePHID() to perform PHID generation."); } /** * Hook to apply serialization or validation to data before it is written to * the database. See also willReadData(). * * @task hook */ protected function willWriteData(array &$data) { $this->applyLiskDataSerialization($data, false); } /** * Hook to perform actions after data has been written to the database. * * @task hook */ protected function didWriteData() {} /** * Hook to make internal object state changes prior to INSERT, REPLACE or * UPDATE. * * @task hook */ protected function willSaveObject() { $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS); if ($use_timestamps) { if (!$this->getDateCreated()) { $this->setDateCreated(time()); } $this->setDateModified(time()); } if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) { $this->setPHID($this->generatePHID()); } } /** * Hook to apply serialization or validation to data as it is read from the * database. See also willWriteData(). * * @task hook */ protected function willReadData(array &$data) { $this->applyLiskDataSerialization($data, $deserialize = true); } /** * Hook to perform an action on data after it is read from the database. * * @task hook */ protected function didReadData() {} /** * Hook to perform an action before the deletion of an object. * * @task hook */ protected function willDelete() {} /** * Hook to perform an action after the deletion of an object. * * @task hook */ protected function didDelete() {} /** * Reads the value from a field. Override this method for custom behavior * of getField() instead of overriding getField directly. * * @param string Canonical field name * @return mixed Value of the field * * @task hook */ protected function readField($field) { if (isset($this->$field)) { return $this->$field; } return null; } /** * Writes a value to a field. Override this method for custom behavior of * setField($value) instead of overriding setField directly. * * @param string Canonical field name * @param mixed Value to write * * @task hook */ protected function writeField($field, $value) { $this->$field = $value; } /* -( Manging Transactions )----------------------------------------------- */ /** * Increase transaction stack depth. * * @return this */ public function openTransaction() { $this->establishConnection('w')->openTransaction(); return $this; } /** * Decrease transaction stack depth, saving work. * * @return this */ public function saveTransaction() { $this->establishConnection('w')->saveTransaction(); return $this; } /** * Decrease transaction stack depth, discarding work. * * @return this */ public function killTransaction() { $this->establishConnection('w')->killTransaction(); return $this; } /** * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that * other connections can not read them (this is an enormous oversimplification * of FOR UPDATE semantics; consult the MySQL documentation for details). To * end read locking, call @{method:endReadLocking}. For example: * * $beach->openTransaction(); * $beach->beginReadLocking(); * * $beach->reload(); * $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1); * $beach->save(); * * $beach->endReadLocking(); * $beach->saveTransaction(); * * @return this * @task xaction */ public function beginReadLocking() { $this->establishConnection('w')->beginReadLocking(); return $this; } /** * Ends read-locking that began at an earlier @{method:beginReadLocking} call. * * @return this * @task xaction */ public function endReadLocking() { $this->establishConnection('w')->endReadLocking(); return $this; } /** * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so * that other connections can not update or delete them (this is an * oversimplification of LOCK IN SHARE MODE semantics; consult the * MySQL documentation for details). To end write locking, call * @{method:endWriteLocking}. * * @return this * @task xaction */ public function beginWriteLocking() { $this->establishConnection('w')->beginWriteLocking(); return $this; } /** * Ends write-locking that began at an earlier @{method:beginWriteLocking} * call. * * @return this * @task xaction */ public function endWriteLocking() { $this->establishConnection('w')->endWriteLocking(); return $this; } /* -( Isolation )---------------------------------------------------------- */ /** * @task isolate */ public static function beginIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel--; if (self::$processIsolationLevel < 0) { throw new Exception( "Lisk process isolation level was reduced below 0."); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToCurrentProcess() { return (bool)self::$processIsolationLevel; } /** * @task isolate */ private function establishIsolatedConnection($mode) { $config = array(); return new AphrontIsolatedDatabaseConnection($config); } /** * @task isolate */ public static function beginIsolateAllLiskEffectsToTransactions() { if (self::$transactionIsolationLevel === 0) { self::closeAllConnections(); } self::$transactionIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToTransactions() { self::$transactionIsolationLevel--; if (self::$transactionIsolationLevel < 0) { throw new Exception( "Lisk transaction isolation level was reduced below 0."); } else if (self::$transactionIsolationLevel == 0) { foreach (self::$connections as $key => $conn) { if ($conn) { $conn->killTransaction(); } } self::closeAllConnections(); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToTransactions() { return (bool)self::$transactionIsolationLevel; } public static function closeAllConnections() { self::$connections = array(); } /* -( Utilities )---------------------------------------------------------- */ /** * Applies configured serialization to a dictionary of values. * * @task util */ protected function applyLiskDataSerialization(array &$data, $deserialize) { $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if ($serialization) { foreach (array_intersect_key($serialization, $data) as $col => $format) { switch ($format) { case self::SERIALIZATION_NONE: break; case self::SERIALIZATION_PHP: if ($deserialize) { $data[$col] = unserialize($data[$col]); } else { $data[$col] = serialize($data[$col]); } break; case self::SERIALIZATION_JSON: if ($deserialize) { $data[$col] = json_decode($data[$col], true); } else { $data[$col] = json_encode($data[$col]); } break; default: throw new Exception("Unknown serialization format '{$format}'."); } } } } /** * Resets the dirty fields (fields which need to be written on next save/ * update/insert/replace). If this DAO has timestamps, the modified time * is always a dirty field. * * @task util */ private function resetDirtyFields() { $this->__dirtyFields = array(); if ($this->getConfigOption(self::CONFIG_TIMESTAMPS)) { $this->__dirtyFields['dateModified'] = true; } } /** * Black magic. Builds implied get*() and set*() for all properties. * * @param string Method name. * @param list Argument vector. * @return mixed get*() methods return the property value. set*() methods * return $this. * @task util */ public function __call($method, $args) { // NOTE: PHP has a bug that static variables defined in __call() are shared // across all children classes. Call a different method to work around this // bug. return $this->call($method, $args); } /** * @task util */ final protected function call($method, $args) { // NOTE: This method is very performance-sensitive (many thousands of calls // per page on some pages), and thus has some silliness in the name of // optimizations. static $dispatch_map = array(); static $partial = null; if ($partial === null) { $partial = $this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS); } if ($method[0] === 'g') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'get') { throw new Exception("Unable to resolve method '{$method}'!"); } $property = substr($method, 3); if (!($property = $this->checkProperty($property))) { throw new Exception("Bad getter call: {$method}"); } $dispatch_map[$method] = $property; } if ($partial && isset($this->__missingFields[$property])) { throw new Exception("Cannot get field that wasn't loaded: {$property}"); } return $this->readField($property); } if ($method[0] === 's') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'set') { throw new Exception("Unable to resolve method '{$method}'!"); } $property = substr($method, 3); $property = $this->checkProperty($property); if (!$property) { throw new Exception("Bad setter call: {$method}"); } $dispatch_map[$method] = $property; } if ($partial) { // Accept writes to fields that weren't initially loaded unset($this->__missingFields[$property]); $this->__dirtyFields[$property] = true; } $this->writeField($property, $args[0]); return $this; } throw new Exception("Unable to resolve method '{$method}'."); } /** * Warns against writing to undeclared property. * * @task util */ public function __set($name, $value) { phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.'); $this->$name = $value; } /** * Increments a named counter and returns the next value. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to create or increment. * @return int Next counter value. * * @task util */ public static function loadNextCounterID( AphrontDatabaseConnection $conn_w, $counter_name) { // NOTE: If an insert does not touch an autoincrement row or call // LAST_INSERT_ID(), MySQL normally does not change the value of // LAST_INSERT_ID(). This can cause a counter's value to leak to a // new counter if the second counter is created after the first one is // updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the // LAST_INSERT_ID() is always updated and always set correctly after the // query completes. queryfx( $conn_w, 'INSERT INTO %T (counterName, counterValue) VALUES (%s, LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE counterValue = LAST_INSERT_ID(counterValue + 1)', self::COUNTER_TABLE_NAME, $counter_name); return $conn_w->getInsertID(); } }