diff --git a/src/applications/audit/storage/PhabricatorAuditTransactionComment.php b/src/applications/audit/storage/PhabricatorAuditTransactionComment.php index 7b5cebbc39..55508d6243 100644 --- a/src/applications/audit/storage/PhabricatorAuditTransactionComment.php +++ b/src/applications/audit/storage/PhabricatorAuditTransactionComment.php @@ -1,41 +1,58 @@ getTransactionPHID() != null); } public function getConfiguration() { $config = parent::getConfiguration(); + $config[self::CONFIG_COLUMN_SCHEMA] = array( 'commitPHID' => 'phid?', 'pathID' => 'id?', 'isNewFile' => 'bool', 'lineNumber' => 'uint32', 'lineLength' => 'uint32', 'fixedState' => 'text12?', 'hasReplies' => 'bool', 'replyToCommentPHID' => 'phid?', 'legacyCommentID' => 'id?', ) + $config[self::CONFIG_COLUMN_SCHEMA]; + + $config[self::CONFIG_KEY_SCHEMA] = array( + 'key_path' => array( + 'columns' => array('pathID'), + ), + 'key_draft' => array( + 'columns' => array('authorPHID', 'transactionPHID'), + ), + 'key_commit' => array( + 'columns' => array('commitPHID'), + ), + 'key_legacy' => array( + 'columns' => array('legacyCommentID'), + ), + ) + $config[self::CONFIG_KEY_SCHEMA]; + return $config; } } diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php index 662ab9f47e..2e2870ef65 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -1,47 +1,52 @@ array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'factorKey' => 'text64', 'factorName' => 'text', 'factorSecret' => 'text', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_user' => array( + 'columns' => array('userPHID'), + ), + ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorAuthAuthFactorPHIDType::TYPECONST); } public function getImplementation() { return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey()); } public function requireImplementation() { $impl = $this->getImplementation(); if (!$impl) { throw new Exception( pht( 'Attempting to operate on multi-factor auth which has no '. 'corresponding implementation (factor key is "%s").', $this->getFactorKey())); } return $impl; } } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php index 7dd425f477..c60aace712 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php @@ -1,100 +1,109 @@ true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'isEnabled' => 'bool', 'providerClass' => 'text128', 'providerType' => 'text64', 'providerDomain' => 'text128', 'shouldAllowLogin' => 'bool', 'shouldAllowRegistration' => 'bool', 'shouldAllowLink' => 'bool', 'shouldAllowUnlink' => 'bool', 'shouldTrustEmails' => 'bool', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_provider' => array( + 'columns' => array('providerType', 'providerDomain'), + 'unique' => true, + ), + 'key_class' => array( + 'columns' => array('providerClass'), + ), + ), ) + parent::getConfiguration(); } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProvider() { if (!$this->provider) { $base = PhabricatorAuthProvider::getAllBaseProviders(); $found = null; foreach ($base as $provider) { if (get_class($provider) == $this->providerClass) { $found = $provider; break; } } if ($found) { $this->provider = id(clone $found)->attachProviderConfig($this); } } return $this->provider; } /* -( 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/applications/auth/storage/PhabricatorAuthTemporaryToken.php b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php index 43a617d810..8a71f6024b 100644 --- a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php +++ b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php @@ -1,104 +1,108 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'tokenType' => 'text64', 'tokenExpires' => 'epoch', 'tokenCode' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_token' => array( 'columns' => array('objectPHID', 'tokenType', 'tokenCode'), + 'unique' => true, + ), + 'key_expires' => array( + 'columns' => array('tokenExpires'), ), ), ) + parent::getConfiguration(); } public function getTokenReadableTypeName() { // Eventually, it would be nice to let applications implement token types // so we can put this in modular subclasses. switch ($this->tokenType) { case PhabricatorAuthSessionEngine::ONETIME_TEMPORARY_TOKEN_TYPE: return pht('One-Time Login Token'); case PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE: return pht('Password Reset Token'); } return $this->tokenType; } public function isRevocable() { if ($this->tokenExpires < time()) { return false; } switch ($this->tokenType) { case PhabricatorAuthSessionEngine::ONETIME_TEMPORARY_TOKEN_TYPE: case PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE: return true; } return false; } public function revokeToken() { if ($this->isRevocable()) { $this->setTokenExpires(PhabricatorTime::getNow() - 1)->save(); } return $this; } public static function revokeTokens( PhabricatorUser $viewer, array $object_phids, array $token_types) { $tokens = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer($viewer) ->withObjectPHIDs($object_phids) ->withTokenTypes($token_types) ->withExpired(false) ->execute(); foreach ($tokens as $token) { $token->revokeToken(); } } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // We're just implement this interface to get access to the standard // query infrastructure. return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/cache/storage/PhabricatorCacheSchemaSpec.php b/src/applications/cache/storage/PhabricatorCacheSchemaSpec.php index ed481a25df..4abd8fd388 100644 --- a/src/applications/cache/storage/PhabricatorCacheSchemaSpec.php +++ b/src/applications/cache/storage/PhabricatorCacheSchemaSpec.php @@ -1,31 +1,39 @@ buildLiskSchemata('PhabricatorCacheDAO'); $this->buildRawSchema( 'cache', id(new PhabricatorKeyValueDatabaseCache())->getTableName(), array( 'id' => 'id64', 'cacheKeyHash' => 'bytes12', 'cacheKey' => 'text128', 'cacheFormat' => 'text16', 'cacheData' => 'bytes', 'cacheCreated' => 'epoch', 'cacheExpires' => 'epoch?', ), array( 'PRIMARY' => array( 'columns' => array('id'), + 'unique' => true, ), 'key_cacheKeyHash' => array( 'columns' => array('cacheKeyHash'), + 'unique' => true, + ), + 'key_cacheCreated' => array( + 'columns' => array('cacheCreated'), + ), + 'key_ttl' => array( + 'columns' => array('cacheExpires'), ), )); } } diff --git a/src/applications/cache/storage/PhabricatorMarkupCache.php b/src/applications/cache/storage/PhabricatorMarkupCache.php index a67e4d63ff..e4f7f6722a 100644 --- a/src/applications/cache/storage/PhabricatorMarkupCache.php +++ b/src/applications/cache/storage/PhabricatorMarkupCache.php @@ -1,29 +1,33 @@ array( 'cacheData' => self::SERIALIZATION_PHP, 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_BINARY => array( 'cacheData' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'cacheKey' => 'text128', ), self::CONFIG_KEY_SCHEMA => array( 'cacheKey' => array( 'columns' => array('cacheKey'), + 'unique' => true, + ), + 'dateCreated' => array( + 'columns' => array('dateCreated'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 481e5112b5..aba4fdbc96 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,121 +1,126 @@ 'away', self::STATUS_SPORADIC => 'sporadic', ); public function getTextStatus() { return self::$statusTexts[$this->status]; } public function getStatusOptions() { return array( self::STATUS_AWAY => pht('Away'), self::STATUS_SPORADIC => pht('Sporadic'), ); } public function getHumanStatus() { $options = $this->getStatusOptions(); return $options[$this->status]; } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'status' => 'uint32', 'description' => 'text', ), + self::CONFIG_KEY_SCHEMA => array( + 'userPHID_dateFrom' => array( + 'columns' => array('userPHID', 'dateTo'), + ), + ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getTerseSummary(PhabricatorUser $viewer) { $until = phabricator_date($this->dateTo, $viewer); if ($this->status == PhabricatorCalendarEvent::STATUS_SPORADIC) { return pht('Sporadic until %s', $until); } else { return pht('Away until %s', $until); } } public function setTextStatus($status) { $statuses = array_flip(self::$statusTexts); return $this->setStatus($statuses[$status]); } public function loadCurrentStatuses($user_phids) { if (!$user_phids) { return array(); } $statuses = $this->loadAllWhere( 'userPHID IN (%Ls) AND UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo', $user_phids); return mpull($statuses, null, 'getUserPHID'); } /** * Validates data and throws exceptions for non-sensical status * windows */ public function save() { if ($this->getDateTo() <= $this->getDateFrom()) { throw new PhabricatorCalendarEventInvalidEpochException(); } return parent::save(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getUserPHID(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarHoliday.php b/src/applications/calendar/storage/PhabricatorCalendarHoliday.php index e6d6a2c708..71549a1c0e 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarHoliday.php +++ b/src/applications/calendar/storage/PhabricatorCalendarHoliday.php @@ -1,41 +1,42 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'day' => 'date', 'name' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'day' => array( 'columns' => array('day'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public static function getNthBusinessDay($epoch, $n) { // Sadly, there are not many holidays. So we can load all of them. $holidays = id(new PhabricatorCalendarHoliday())->loadAll(); $holidays = mpull($holidays, null, 'getDay'); $interval = ($n > 0 ? 1 : -1) * 24 * 60 * 60; $return = $epoch; for ($i = abs($n); $i > 0; ) { $return += $interval; $weekday = date('w', $return); if ($weekday != 0 && $weekday != 6 && // Sunday and Saturday !isset($holidays[date('Y-m-d', $return)])) { $i--; } } return $return; } } diff --git a/src/applications/chatlog/storage/PhabricatorChatLogChannel.php b/src/applications/chatlog/storage/PhabricatorChatLogChannel.php index ce6117185a..7416f7b4e6 100644 --- a/src/applications/chatlog/storage/PhabricatorChatLogChannel.php +++ b/src/applications/chatlog/storage/PhabricatorChatLogChannel.php @@ -1,54 +1,55 @@ array( 'serviceName' => 'text64', 'serviceType' => 'text32', 'channelName' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_channel' => array( 'columns' => array('channelName', 'serviceType', 'serviceName'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->viewPolicy; break; case PhabricatorPolicyCapability::CAN_EDIT: return $this->editPolicy; break; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/chatlog/storage/PhabricatorChatLogEvent.php b/src/applications/chatlog/storage/PhabricatorChatLogEvent.php index 68c6968f77..39fd1d37e9 100644 --- a/src/applications/chatlog/storage/PhabricatorChatLogEvent.php +++ b/src/applications/chatlog/storage/PhabricatorChatLogEvent.php @@ -1,58 +1,63 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'author' => 'text64', 'type' => 'text4', 'message' => 'text', ), + self::CONFIG_KEY_SCHEMA => array( + 'channel' => array( + 'columns' => array('epoch'), + ), + ), ) + parent::getConfiguration(); } public function attachChannel(PhabricatorChatLogChannel $channel) { $this->channel = $channel; return $this; } public function getChannel() { return $this->assertAttached($this->channel); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getChannel()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getChannel()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php b/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php index c324e4fc08..0baf243e9a 100644 --- a/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php +++ b/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php @@ -1,24 +1,26 @@ array( 'token' => 'text64?', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID' => array( 'columns' => array('userPHID'), + 'unique' => true, ), 'token' => array( 'columns' => array('token'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php b/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php index 0d47fcf735..9f4bcf0be0 100644 --- a/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php +++ b/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php @@ -1,21 +1,26 @@ array( 'client' => 'text255?', 'clientVersion' => 'text255?', 'clientDescription' => 'text255?', 'username' => 'text255?', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_created' => array( + 'columns' => array('dateCreated'), + ), + ), ) + parent::getConfiguration(); } } diff --git a/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php b/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php index c3be27440b..6d6f3dfc4f 100644 --- a/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php +++ b/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php @@ -1,48 +1,59 @@ array( 'id' => 'id64', 'connectionID' => 'id64?', 'method' => 'text255', 'error' => 'text255', 'duration' => 'uint64', 'callerPHID' => 'phid?', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_date' => array( + 'columns' => array('dateCreated'), + ), + 'key_method' => array( + 'columns' => array('method'), + ), + 'key_callermethod' => array( + 'columns' => array('callerPHID', 'method'), + ), + ), ) + 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/config/controller/PhabricatorConfigDatabaseIssueController.php b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php index 199529e684..1997d660be 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php @@ -1,169 +1,176 @@ getRequest(); $viewer = $request->getUser(); $query = $this->buildSchemaQuery(); $actual = $query->loadActualSchema(); $expect = $query->loadExpectedSchema(); $comp = $query->buildComparisonSchema($expect, $actual); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Database Issues')); // Collect all open issues. $issues = array(); foreach ($comp->getDatabases() as $database_name => $database) { foreach ($database->getLocalIssues() as $issue) { $issues[] = array( $database_name, null, null, null, $issue); } foreach ($database->getTables() as $table_name => $table) { foreach ($table->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, null, null, $issue); } foreach ($table->getColumns() as $column_name => $column) { foreach ($column->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, 'column', $column_name, $issue); } } foreach ($table->getKeys() as $key_name => $key) { foreach ($key->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, 'key', $key_name, $issue); } } } } // Sort all open issues so that the most severe issues appear first. $order = array(); $counts = array(); foreach ($issues as $key => $issue) { $const = $issue[4]; $status = PhabricatorConfigStorageSchema::getIssueStatus($const); $severity = PhabricatorConfigStorageSchema::getStatusSeverity($status); $order[$key] = sprintf( '~%d~%s%s%s', 9 - $severity, $issue[0], $issue[1], $issue[3]); if (empty($counts[$status])) { $counts[$status] = 0; } $counts[$status]++; } asort($order); $issues = array_select_keys($issues, array_keys($order)); // Render the issues. $rows = array(); foreach ($issues as $issue) { $const = $issue[4]; + $database_link = phutil_tag( + 'a', + array( + 'href' => $this->getApplicationURI('/database/'.$issue[0].'/'), + ), + $issue[0]); + $rows[] = array( $this->renderIcon( PhabricatorConfigStorageSchema::getIssueStatus($const)), - $issue[0], + $database_link, $issue[1], $issue[2], $issue[3], PhabricatorConfigStorageSchema::getIssueDescription($const), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Database'), pht('Table'), pht('Type'), pht('Column/Key'), pht('Issue'), )) ->setColumnClasses( array( null, null, null, null, null, 'wide', )); $errors = array(); $errors[] = pht( 'IMPORTANT: This feature is in development and the information below '. 'is not accurate! Ignore it for now. See T1191.'); if (isset($counts[PhabricatorConfigStorageSchema::STATUS_FAIL])) { $errors[] = pht( 'Detected %s serious issue(s) with the schemata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_FAIL])); } if (isset($counts[PhabricatorConfigStorageSchema::STATUS_WARN])) { $errors[] = pht( 'Detected %s warning(s) with the schemata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_WARN])); } if (isset($counts[PhabricatorConfigStorageSchema::STATUS_NOTE])) { $errors[] = pht( 'Detected %s minor issue(s) with the scheamata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_NOTE])); } $title = pht('Database Issues'); $table_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->appendChild($table); $nav = $this->buildSideNavView(); $nav->selectFilter('dbissue/'); $nav->appendChild( array( $crumbs, $table_box, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } } diff --git a/src/applications/config/schema/PhabricatorConfigStorageSchema.php b/src/applications/config/schema/PhabricatorConfigStorageSchema.php index 9b4d9ee2b9..8b59d8f1fe 100644 --- a/src/applications/config/schema/PhabricatorConfigStorageSchema.php +++ b/src/applications/config/schema/PhabricatorConfigStorageSchema.php @@ -1,219 +1,219 @@ getName() != $expect->getName()) { throw new Exception(pht('Names must match to compare schemata!')); } return $this->compareToSimilarSchema($expect); } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setIssues(array $issues) { $this->issues = array_fuse($issues); return $this; } public function getIssues() { $issues = $this->issues; foreach ($this->getSubschemata() as $sub) { switch ($sub->getStatus()) { case self::STATUS_NOTE: $issues[self::ISSUE_SUBNOTE] = self::ISSUE_SUBNOTE; break; case self::STATUS_WARN: $issues[self::ISSUE_SUBWARN] = self::ISSUE_SUBWARN; break; case self::STATUS_FAIL: $issues[self::ISSUE_SUBFAIL] = self::ISSUE_SUBFAIL; break; } } return $issues; } public function getLocalIssues() { return $this->issues; } public function hasIssue($issue) { return (bool)idx($this->getIssues(), $issue); } public function getAllIssues() { $issues = $this->getIssues(); foreach ($this->getSubschemata() as $sub) { $issues += $sub->getAllIssues(); } return $issues; } public function getStatus() { $status = self::STATUS_OKAY; foreach ($this->getAllIssues() as $issue) { $issue_status = self::getIssueStatus($issue); $status = self::getStrongestStatus($status, $issue_status); } return $status; } public static function getIssueName($issue) { switch ($issue) { case self::ISSUE_MISSING: return pht('Missing'); case self::ISSUE_MISSINGKEY: return pht('Missing Key'); case self::ISSUE_SURPLUS: return pht('Surplus'); case self::ISSUE_SURPLUSKEY: return pht('Surplus Key'); case self::ISSUE_CHARSET: return pht('Better Character Set Available'); case self::ISSUE_COLLATION: return pht('Better Collation Available'); case self::ISSUE_COLUMNTYPE: return pht('Wrong Column Type'); case self::ISSUE_NULLABLE: return pht('Wrong Nullable Setting'); case self::ISSUE_KEYCOLUMNS: return pht('Key on Wrong Columns'); case self::ISSUE_UNIQUE: return pht('Key has Wrong Uniqueness'); case self::ISSUE_SUBNOTE: return pht('Subschemata Have Notices'); case self::ISSUE_SUBWARN: return pht('Subschemata Have Warnings'); case self::ISSUE_SUBFAIL: return pht('Subschemata Have Failures'); default: throw new Exception(pht('Unknown schema issue "%s"!', $issue)); } } public static function getIssueDescription($issue) { switch ($issue) { case self::ISSUE_MISSING: return pht('This schema is expected to exist, but does not.'); case self::ISSUE_MISSINGKEY: return pht('This key is expected to exist, but does not.'); case self::ISSUE_SURPLUS: return pht('This schema is not expected to exist.'); case self::ISSUE_SURPLUSKEY: return pht('This key is not expected to exist.'); case self::ISSUE_CHARSET: return pht('This schema can use a better character set.'); case self::ISSUE_COLLATION: return pht('This schema can use a better collation.'); case self::ISSUE_COLUMNTYPE: return pht('This schema can use a better column type.'); case self::ISSUE_NULLABLE: return pht('This schema has the wrong nullable setting.'); case self::ISSUE_KEYCOLUMNS: return pht('This schema is on the wrong columns.'); case self::ISSUE_UNIQUE: return pht('This key has the wrong uniqueness setting.'); case self::ISSUE_SUBNOTE: return pht('Subschemata have setup notices.'); case self::ISSUE_SUBWARN: return pht('Subschemata have setup warnings.'); case self::ISSUE_SUBFAIL: return pht('Subschemata have setup failures.'); default: throw new Exception(pht('Unknown schema issue "%s"!', $issue)); } } public static function getIssueStatus($issue) { switch ($issue) { case self::ISSUE_MISSING: - case self::ISSUE_MISSINGKEY: case self::ISSUE_SUBFAIL: - return self::STATUS_FAIL; case self::ISSUE_SURPLUS: - case self::ISSUE_SURPLUSKEY: + return self::STATUS_FAIL; case self::ISSUE_SUBWARN: case self::ISSUE_COLUMNTYPE: case self::ISSUE_KEYCOLUMNS: - case self::ISSUE_UNIQUE: case self::ISSUE_NULLABLE: return self::STATUS_WARN; case self::ISSUE_SUBNOTE: case self::ISSUE_CHARSET: case self::ISSUE_COLLATION: + case self::ISSUE_MISSINGKEY: + case self::ISSUE_SURPLUSKEY: + case self::ISSUE_UNIQUE: return self::STATUS_NOTE; default: throw new Exception(pht('Unknown schema issue "%s"!', $issue)); } } public static function getStatusSeverity($status) { switch ($status) { case self::STATUS_FAIL: return 3; case self::STATUS_WARN: return 2; case self::STATUS_NOTE: return 1; case self::STATUS_OKAY: return 0; default: throw new Exception(pht('Unknown schema status "%s"!', $status)); } } public static function getStrongestStatus($u, $v) { $u_sev = self::getStatusSeverity($u); $v_sev = self::getStatusSeverity($v); if ($u_sev >= $v_sev) { return $u; } else { return $v; } } } diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php index 823fe50b00..47801247a2 100644 --- a/src/applications/config/storage/PhabricatorConfigEntry.php +++ b/src/applications/config/storage/PhabricatorConfigEntry.php @@ -1,74 +1,75 @@ true, self::CONFIG_SERIALIZATION => array( 'value' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'namespace' => 'text64', 'configKey' => 'text64', 'isDeleted' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('namespace', 'configKey'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorConfigConfigPHIDType::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/ConpherenceParticipant.php b/src/applications/conpherence/storage/ConpherenceParticipant.php index 1310af135a..d8687d2ef8 100644 --- a/src/applications/conpherence/storage/ConpherenceParticipant.php +++ b/src/applications/conpherence/storage/ConpherenceParticipant.php @@ -1,55 +1,62 @@ array( 'settings' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'participationStatus' => 'uint32', 'dateTouched' => 'epoch', 'seenMessageCount' => 'uint64', ), self::CONFIG_KEY_SCHEMA => array( 'conpherencePHID' => array( 'columns' => array('conpherencePHID', 'participantPHID'), + 'unique' => true, + ), + 'unreadCount' => array( + 'columns' => array('participantPHID', 'participationStatus'), + ), + 'participationIndex' => array( + 'columns' => array('participantPHID', 'dateTouched', 'id'), ), ), ) + parent::getConfiguration(); } public function getSettings() { return nonempty($this->settings, array()); } public function markUpToDate( ConpherenceThread $conpherence, ConpherenceTransaction $xaction) { if (!$this->isUpToDate($conpherence)) { $this->setParticipationStatus(ConpherenceParticipationStatus::UP_TO_DATE); $this->setBehindTransactionPHID($xaction->getPHID()); $this->setSeenMessageCount($conpherence->getMessageCount()); $this->save(); } return $this; } private function isUpToDate(ConpherenceThread $conpherence) { return ($this->getSeenMessageCount() == $conpherence->getMessageCount()) && ($this->getParticipationStatus() == ConpherenceParticipationStatus::UP_TO_DATE); } } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 162442db46..629ed9b0e5 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -1,208 +1,209 @@ setMessageCount(0) ->setTitle(''); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'recentParticipantPHIDs' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255?', 'messageCount' => 'uint64', 'mailKey' => 'text20', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorConpherenceThreadPHIDType::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/conpherence/storage/ConpherenceTransactionComment.php b/src/applications/conpherence/storage/ConpherenceTransactionComment.php index 958731e855..50169d7cd3 100644 --- a/src/applications/conpherence/storage/ConpherenceTransactionComment.php +++ b/src/applications/conpherence/storage/ConpherenceTransactionComment.php @@ -1,20 +1,29 @@ 'phid?', ) + $config[self::CONFIG_COLUMN_SCHEMA]; + + $config[self::CONFIG_KEY_SCHEMA] = array( + 'key_draft' => array( + 'columns' => array('authorPHID', 'conpherencePHID', 'transactionPHID'), + 'unique' => true, + ), + ) + $config[self::CONFIG_KEY_SCHEMA]; + return $config; } } diff --git a/src/applications/daemon/storage/PhabricatorDaemonLog.php b/src/applications/daemon/storage/PhabricatorDaemonLog.php index a4f89c8477..641ab3aabd 100644 --- a/src/applications/daemon/storage/PhabricatorDaemonLog.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLog.php @@ -1,70 +1,78 @@ array( 'argv' => self::SERIALIZATION_JSON, 'explicitArgv' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'daemon' => 'text255', 'host' => 'text255', 'pid' => 'uint32', 'envHash' => 'bytes40', 'status' => 'text8', ), + self::CONFIG_KEY_SCHEMA => array( + 'status' => array( + 'columns' => array('status'), + ), + 'dateCreated' => array( + 'columns' => array('dateCreated'), + ), + ), ) + parent::getConfiguration(); } public function getExplicitArgv() { $argv = $this->explicitArgv; if (!is_array($argv)) { return array(); } return $argv; } /* -( 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/daemon/storage/PhabricatorDaemonLogEvent.php b/src/applications/daemon/storage/PhabricatorDaemonLogEvent.php index 6be65dd784..fd1110ae06 100644 --- a/src/applications/daemon/storage/PhabricatorDaemonLogEvent.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLogEvent.php @@ -1,20 +1,25 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'logType' => 'text4', 'message' => 'text', ), + self::CONFIG_KEY_SCHEMA => array( + 'logID' => array( + 'columns' => array('logID', 'epoch'), + ), + ), ) + parent::getConfiguration(); } } diff --git a/src/applications/dashboard/storage/PhabricatorDashboardInstall.php b/src/applications/dashboard/storage/PhabricatorDashboardInstall.php index 0064beb8bd..0f108fbac8 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboardInstall.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardInstall.php @@ -1,51 +1,52 @@ array( 'applicationClass' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'objectPHID' => array( 'columns' => array('objectPHID', 'applicationClass'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public static function getDashboard( PhabricatorUser $viewer, $object_phid, $application_class) { $dashboard = null; $dashboard_install = id(new PhabricatorDashboardInstall()) ->loadOneWhere( 'objectPHID = %s AND applicationClass = %s', $object_phid, $application_class); if ($dashboard_install) { $dashboard = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withPHIDs(array($dashboard_install->getDashboardPHID())) ->needPanels(true) ->executeOne(); } return $dashboard; } } diff --git a/src/applications/doorkeeper/storage/DoorkeeperExternalObject.php b/src/applications/doorkeeper/storage/DoorkeeperExternalObject.php index ce0a4f5eae..3719462ffb 100644 --- a/src/applications/doorkeeper/storage/DoorkeeperExternalObject.php +++ b/src/applications/doorkeeper/storage/DoorkeeperExternalObject.php @@ -1,98 +1,107 @@ true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'objectKey' => 'bytes12', 'applicationType' => 'text32', 'applicationDomain' => 'text32', 'objectType' => 'text32', 'objectID' => 'text64', 'objectURI' => 'text128?', 'importerPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectKey'), + 'unique' => true, + ), + 'key_full' => array( + 'columns' => array( + 'applicationType', + 'applicationDomain', + 'objectType', + 'objectID', + ), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_XOBJ); } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getObjectKey() { $key = parent::getObjectKey(); if ($key === null) { $key = $this->getRef()->getObjectKey(); } return $key; } public function getRef() { return id(new DoorkeeperObjectRef()) ->setApplicationType($this->getApplicationType()) ->setApplicationDomain($this->getApplicationDomain()) ->setObjectType($this->getObjectType()) ->setObjectID($this->getObjectID()); } public function save() { if (!$this->objectKey) { $this->objectKey = $this->getObjectKey(); } return parent::save(); } /* -( 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/draft/storage/PhabricatorDraft.php b/src/applications/draft/storage/PhabricatorDraft.php index e8d3748431..129d999298 100644 --- a/src/applications/draft/storage/PhabricatorDraft.php +++ b/src/applications/draft/storage/PhabricatorDraft.php @@ -1,100 +1,101 @@ array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'draftKey' => 'text64', 'draft' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'authorPHID' => array( 'columns' => array('authorPHID', 'draftKey'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function replaceOrDelete() { if ($this->draft == '' && !array_filter($this->metadata)) { queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE authorPHID = %s AND draftKey = %s', $this->getTableName(), $this->authorPHID, $this->draftKey); $this->deleted = true; return $this; } return parent::replace(); } protected function didDelete() { $this->deleted = true; } public function isDeleted() { return $this->deleted; } public static function newFromUserAndKey(PhabricatorUser $user, $key) { if ($user->getPHID() && strlen($key)) { $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $key); if ($draft) { return $draft; } } $draft = new PhabricatorDraft(); if ($user->getPHID()) { $draft ->setAuthorPHID($user->getPHID()) ->setDraftKey($key); } return $draft; } public static function buildFromRequest(AphrontRequest $request) { $user = $request->getUser(); if (!$user->getPHID()) { return null; } if (!$request->getStr('__draft__')) { return null; } $draft = id(new PhabricatorDraft()) ->setAuthorPHID($user->getPHID()) ->setDraftKey($request->getStr('__draft__')); // If this is a preview, add other data. If not, leave the draft empty so // that replaceOrDelete() will delete it. if ($request->isPreviewRequest()) { $other_data = $request->getPassthroughRequestData(); unset($other_data['comment']); $draft ->setDraft($request->getStr('comment')) ->setMetadata($other_data); } return $draft; } } diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index 6a971629e8..b9e79fe820 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -1,228 +1,229 @@ releaseOnDestruction = true; return $this; } public function __destruct() { if ($this->releaseOnDestruction) { if ($this->isActive()) { $this->release(); } } } public function getLeaseName() { return pht('Lease %d', $this->getID()); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attributes' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'uint32', 'until' => 'epoch?', 'resourceType' => 'text128', 'taskID' => 'id?', 'ownerPHID' => 'phid?', 'resourceID' => 'id?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function setAttribute($key, $value) { $this->attributes[$key] = $value; return $this; } public function getAttribute($key, $default = null) { return idx($this->attributes, $key, $default); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DrydockLeasePHIDType::TYPECONST); } public function getInterface($type) { return $this->getResource()->getInterface($this, $type); } public function getResource() { return $this->assertAttached($this->resource); } public function attachResource(DrydockResource $resource = null) { $this->resource = $resource; return $this; } public function hasAttachedResource() { return ($this->resource !== null); } public function loadResource() { return id(new DrydockResource())->loadOneWhere( 'id = %d', $this->getResourceID()); } public function queueForActivation() { if ($this->getID()) { throw new Exception( 'Only new leases may be queued for activation!'); } $this->setStatus(DrydockLeaseStatus::STATUS_PENDING); $this->save(); $task = PhabricatorWorker::scheduleTask( 'DrydockAllocatorWorker', $this->getID()); // NOTE: Scheduling the task might execute it in-process, if we're running // from a CLI script. Reload the lease to make sure we have the most // up-to-date information. Normally, this has no effect. $this->reload(); $this->setTaskID($task->getID()); $this->save(); return $this; } public function release() { $this->assertActive(); $this->setStatus(DrydockLeaseStatus::STATUS_RELEASED); $this->save(); $this->resource = null; return $this; } public function isActive() { switch ($this->status) { case DrydockLeaseStatus::STATUS_ACTIVE: case DrydockLeaseStatus::STATUS_ACQUIRING: return true; } return false; } private function assertActive() { if (!$this->isActive()) { throw new Exception( 'Lease is not active! You can not interact with resources through '. 'an inactive lease.'); } } public static function waitForLeases(array $leases) { assert_instances_of($leases, 'DrydockLease'); $task_ids = array_filter(mpull($leases, 'getTaskID')); PhabricatorWorker::waitForTasks($task_ids); $unresolved = $leases; while (true) { foreach ($unresolved as $key => $lease) { $lease->reload(); switch ($lease->getStatus()) { case DrydockLeaseStatus::STATUS_ACTIVE: unset($unresolved[$key]); break; case DrydockLeaseStatus::STATUS_RELEASED: throw new Exception('Lease has already been released!'); case DrydockLeaseStatus::STATUS_EXPIRED: throw new Exception('Lease has already expired!'); case DrydockLeaseStatus::STATUS_BROKEN: throw new Exception('Lease has been broken!'); case DrydockLeaseStatus::STATUS_PENDING: case DrydockLeaseStatus::STATUS_ACQUIRING: break; default: throw new Exception('Unknown status??'); } } if ($unresolved) { sleep(1); } else { break; } } foreach ($leases as $lease) { $lease->attachResource($lease->loadResource()); } } public function waitUntilActive() { if (!$this->getID()) { $this->queueForActivation(); } self::waitForLeases(array($this)); return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { if ($this->getResource()) { return $this->getResource()->getPolicy($capability); } return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getResource()) { return $this->getResource()->hasAutomaticCapability($capability, $viewer); } return false; } public function describeAutomaticCapability($capability) { return pht('Leases inherit policies from the resources they lease.'); } } diff --git a/src/applications/drydock/storage/DrydockLog.php b/src/applications/drydock/storage/DrydockLog.php index 1faa6f9bc4..763d531e42 100644 --- a/src/applications/drydock/storage/DrydockLog.php +++ b/src/applications/drydock/storage/DrydockLog.php @@ -1,71 +1,82 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'resourceID' => 'id?', 'leaseID' => 'id?', 'message' => 'text', ), + self::CONFIG_KEY_SCHEMA => array( + 'resourceID' => array( + 'columns' => array('resourceID', 'epoch'), + ), + 'leaseID' => array( + 'columns' => array('leaseID', 'epoch'), + ), + 'epoch' => array( + 'columns' => array('epoch'), + ), + ), ) + parent::getConfiguration(); } public function attachResource(DrydockResource $resource = null) { $this->resource = $resource; return $this; } public function getResource() { return $this->assertAttached($this->resource); } public function attachLease(DrydockLease $lease = null) { $this->lease = $lease; return $this; } public function getLease() { return $this->assertAttached($this->lease); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { if ($this->getResource()) { return $this->getResource()->getPolicy($capability); } return $this->getLease()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getResource()) { return $this->getResource()->hasAutomaticCapability($capability, $viewer); } return $this->getLease()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Logs inherit the policy of their resources.'); } } diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php index 0ad17d303f..84b8bc6d69 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -1,133 +1,134 @@ true, self::CONFIG_SERIALIZATION => array( 'attributes' => self::SERIALIZATION_JSON, 'capabilities' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'ownerPHID' => 'phid?', 'status' => 'uint32', 'type' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DrydockResourcePHIDType::TYPECONST); } public function getAttribute($key, $default = null) { return idx($this->attributes, $key, $default); } public function getAttributesForTypeSpec(array $attribute_names) { return array_select_keys($this->attributes, $attribute_names); } public function setAttribute($key, $value) { $this->attributes[$key] = $value; return $this; } public function getCapability($key, $default = null) { return idx($this->capbilities, $key, $default); } public function getInterface(DrydockLease $lease, $type) { return $this->getBlueprint()->getInterface($this, $lease, $type); } public function getBlueprint() { // TODO: Policy stuff. if (empty($this->blueprint)) { $blueprint = id(new DrydockBlueprint()) ->loadOneWhere('phid = %s', $this->blueprintPHID); $this->blueprint = $blueprint->getImplementation(); } return $this->blueprint; } public function closeResource() { $this->openTransaction(); $statuses = array( DrydockLeaseStatus::STATUS_PENDING, DrydockLeaseStatus::STATUS_ACTIVE, ); $leases = id(new DrydockLeaseQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withResourceIDs(array($this->getID())) ->withStatuses($statuses) ->execute(); foreach ($leases as $lease) { switch ($lease->getStatus()) { case DrydockLeaseStatus::STATUS_PENDING: $message = pht('Breaking pending lease (resource closing).'); $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN); break; case DrydockLeaseStatus::STATUS_ACTIVE: $message = pht('Releasing active lease (resource closing).'); $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED); break; } DrydockBlueprintImplementation::writeLog($this, $lease, $message); $lease->save(); } $this->setStatus(DrydockResourceStatus::STATUS_CLOSED); $this->save(); $this->saveTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/feed/storage/PhabricatorFeedStoryData.php b/src/applications/feed/storage/PhabricatorFeedStoryData.php index 161d8fe896..bf0aba84be 100644 --- a/src/applications/feed/storage/PhabricatorFeedStoryData.php +++ b/src/applications/feed/storage/PhabricatorFeedStoryData.php @@ -1,67 +1,69 @@ true, self::CONFIG_SERIALIZATION => array( 'storyData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'chronologicalKey' => 'uint64', 'storyType' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), + 'unique' => true, ), 'chronologicalKey' => array( 'columns' => array('chronologicalKey'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_STRY); } public function getEpoch() { if (PHP_INT_SIZE < 8) { // We're on a 32-bit machine. if (function_exists('bcadd')) { // Try to use the 'bc' extension. return bcdiv($this->chronologicalKey, bcpow(2, 32)); } else { // Do the math in MySQL. TODO: If we formalize a bc dependency, get // rid of this. // See: PhabricatorFeedStoryPublisher::generateChronologicalKey() $conn_r = id($this->establishConnection('r')); $result = queryfx_one( $conn_r, // Insert the chronologicalKey as a string since longs don't seem to // be supported by qsprintf and ints get maxed on 32 bit machines. 'SELECT (%s >> 32) as N', $this->chronologicalKey); return $result['N']; } } else { return $this->chronologicalKey >> 32; } } public function getValue($key, $default = null) { return idx($this->storyData, $key, $default); } } diff --git a/src/applications/feed/storage/PhabricatorFeedStoryReference.php b/src/applications/feed/storage/PhabricatorFeedStoryReference.php index f4b980a4a0..c5740b542d 100644 --- a/src/applications/feed/storage/PhabricatorFeedStoryReference.php +++ b/src/applications/feed/storage/PhabricatorFeedStoryReference.php @@ -1,25 +1,29 @@ self::IDS_MANUAL, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'chronologicalKey' => 'uint64', 'id' => null, ), self::CONFIG_KEY_SCHEMA => array( 'PRIMARY' => null, 'objectPHID' => array( 'columns' => array('objectPHID', 'chronologicalKey'), + 'unique' => true, + ), + 'chronologicalKey' => array( + 'columns' => array('chronologicalKey'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index a6171a4f39..703886f5d4 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1,1158 +1,1171 @@ attachOriginalFile(null) ->attachObjects(array()) ->attachObjectPHIDs(array()); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255?', 'mimeType' => 'text255?', - 'byteSize' => 'uint64?', + 'byteSize' => 'uint64', 'storageEngine' => 'text32', 'storageFormat' => 'text32', 'storageHandle' => 'text255', 'authorPHID' => 'phid?', 'secretKey' => 'bytes20?', 'contentHash' => 'bytes40?', 'ttl' => 'epoch?', 'isExplicitUpload' => 'bool?', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), + 'unique' => true, + ), + 'authorPHID' => array( + 'columns' => array('authorPHID'), + ), + 'contentHash' => array( + 'columns' => array('contentHash'), + ), + 'key_ttl' => array( + 'columns' => array('ttl'), + ), + 'key_dateCreated' => array( + 'columns' => array('dateCreated'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorFileFilePHIDType::TYPECONST); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey($this->generateSecretKey()); } if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getMonogram() { return 'F'.$this->getID(); } 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 = phutil_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, array $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(); $new_file = PhabricatorFile::initializeNewFile(); $new_file->setByteSize($copy_of_byteSize); $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->readPropertiesFromParameters($params); $new_file->save(); return $new_file; } return $file; } private static function buildFromFileData($data, array $params = array()) { 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 = PhabricatorFile::initializeNewFile(); $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->setByteSize(strlen($data)); $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->readPropertiesFromParameters($params); if (!$file->getMimeType()) { $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_identifier = $this->getStorageEngine(); $old_handle = $this->getStorageHandle(); $this->setStorageEngine($new_identifier); $this->setStorageHandle($new_handle); $this->save(); $this->deleteFileDataIfUnused( $old_engine, $old_identifier, $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(); $this->deleteFileDataIfUnused( $this->instantiateStorageEngine(), $this->getStorageEngine(), $this->getStorageHandle()); return $ret; } /** * Destroy stored file data if there are no remaining files which reference * it. */ public function deleteFileDataIfUnused( PhabricatorFileStorageEngine $engine, $engine_identifier, $handle) { // Check to see if any files are using storage. $usage = id(new PhabricatorFile())->loadAllWhere( 'storageEngine = %s AND storageHandle = %s LIMIT 1', $engine_identifier, $handle); // If there are no files using the storage, destroy the actual storage. if (!$usage) { try { $engine->deleteFile($handle); } catch (Exception $ex) { // In the worst case, we're leaving some data stranded in a storage // engine, which is not a big deal. phlog($ex); } } } 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 '/'.$this->getMonogram(); } 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 getPreview100URI() { $path = '/file/xform/preview-100/'.$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; } public 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; } /** * 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, ); } // NOTE: Anyone is allowed to access builtin files. $files = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->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 getOriginalFile() { return $this->assertAttached($this->originalFile); } public function attachOriginalFile(PhabricatorFile $file = null) { $this->originalFile = $file; 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); } public function getCanCDN() { if (!$this->isViewableImage()) { return false; } return idx($this->metadata, self::METADATA_CAN_CDN); } public function setCanCDN($can_cdn) { $this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0; return $this; } protected function generateOneTimeToken() { $key = Filesystem::readRandomCharacters(16); // Save the new secret. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $token = id(new PhabricatorAuthTemporaryToken()) ->setObjectPHID($this->getPHID()) ->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE) ->setTokenExpires(time() + phutil_units('1 hour in seconds')) ->setTokenCode(PhabricatorHash::digest($key)) ->save(); unset($unguarded); return $key; } public function validateOneTimeToken($token_code) { $token = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withObjectPHIDs(array($this->getPHID())) ->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE)) ->withExpired(false) ->withTokenCodes(array(PhabricatorHash::digest($token_code))) ->executeOne(); return $token; } /** Get the CDN uri for this file * This will generate a one-time-use token if * security.alternate_file_domain is set in the config. */ public function getCDNURIWithToken() { if (!$this->getPHID()) { throw new Exception( 'You must save a file before you can generate a CDN URI.'); } $name = phutil_escape_uri($this->getName()); $path = '/file/data' .'/'.$this->getSecretKey() .'/'.$this->getPHID() .'/'.$this->generateOneTimeToken() .'/'.$name; return PhabricatorEnv::getCDNURI($path); } /** * Write the policy edge between this file and some object. * * @param phid Object PHID to attach to. * @return this */ public function attachToObject($phid) { $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; id(new PhabricatorEdgeEditor()) ->addEdge($phid, $edge_type, $this->getPHID()) ->save(); return $this; } /** * Remove the policy edge between this file and some object. * * @param phid Object PHID to detach from. * @return this */ public function detachFromObject($phid) { $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; id(new PhabricatorEdgeEditor()) ->removeEdge($phid, $edge_type, $this->getPHID()) ->save(); return $this; } /** * Configure a newly created file object according to specified parameters. * * This method is called both when creating a file from fresh data, and * when creating a new file which reuses existing storage. * * @param map Bag of parameters, see @{class:PhabricatorFile} * for documentation. * @return this */ private function readPropertiesFromParameters(array $params) { $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); $this->setName($file_name); $author_phid = idx($params, 'authorPHID'); $this->setAuthorPHID($author_phid); $file_ttl = idx($params, 'ttl'); $this->setTtl($file_ttl); $view_policy = idx($params, 'viewPolicy'); if ($view_policy) { $this->setViewPolicy($params['viewPolicy']); } $is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0); $this->setIsExplicitUpload($is_explicit); $can_cdn = idx($params, 'canCDN'); if ($can_cdn) { $this->setCanCDN(true); } $mime_type = idx($params, 'mime-type'); if ($mime_type) { $this->setMimeType($mime_type); } return $this; } public function getRedirectResponse() { $uri = $this->getBestURI(); // TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI // (if the file is a viewable image) and sometimes a local URI (if not). // For now, just detect which one we got and configure the response // appropriately. In the long run, if this endpoint is served from a CDN // domain, we can't issue a local redirect to an info URI (which is not // present on the CDN domain). We probably never actually issue local // redirects here anyway, since we only ever transform viewable images // right now. $is_external = strlen(id(new PhutilURI($uri))->getDomain()); return id(new AphrontRedirectResponse()) ->setIsExternal($is_external) ->setURI($uri); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } 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 the file this file is a transform of, you can see // this file. if ($this->getOriginalFile()) { return true; } // 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.'); $out[] = pht( 'Thumbnails are visible only to users who can view the original '. 'file.'); break; } return $out; } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/files/storage/PhabricatorFileTransactionComment.php b/src/applications/files/storage/PhabricatorFileTransactionComment.php index 57fa7d7a33..f1a2211f7a 100644 --- a/src/applications/files/storage/PhabricatorFileTransactionComment.php +++ b/src/applications/files/storage/PhabricatorFileTransactionComment.php @@ -1,25 +1,26 @@ getTransactionPHID() != null); } public function getConfiguration() { $config = parent::getConfiguration(); $config[self::CONFIG_KEY_SCHEMA] = array( 'key_draft' => array( 'columns' => array('authorPHID', 'transactionPHID'), + 'unique' => true, ), ) + $config[self::CONFIG_KEY_SCHEMA]; return $config; } } diff --git a/src/applications/files/storage/PhabricatorTransformedFile.php b/src/applications/files/storage/PhabricatorTransformedFile.php index 52dde89add..5b0f0e2f1d 100644 --- a/src/applications/files/storage/PhabricatorTransformedFile.php +++ b/src/applications/files/storage/PhabricatorTransformedFile.php @@ -1,22 +1,26 @@ array( 'transform' => 'text255', ), self::CONFIG_KEY_SCHEMA => array( 'originalPHID' => array( 'columns' => array('originalPHID', 'transform'), + 'unique' => true, + ), + 'transformedPHID' => array( + 'columns' => array('transformedPHID'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/flag/storage/PhabricatorFlag.php b/src/applications/flag/storage/PhabricatorFlag.php index 0ef56a916c..535b9a9e8e 100644 --- a/src/applications/flag/storage/PhabricatorFlag.php +++ b/src/applications/flag/storage/PhabricatorFlag.php @@ -1,72 +1,76 @@ array( 'type' => 'text4', 'color' => 'uint32', 'note' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'ownerPHID' => array( 'columns' => array('ownerPHID', 'type', 'objectPHID'), + 'unique' => true, + ), + 'objectPHID' => array( + 'columns' => array('objectPHID'), ), ), ) + parent::getConfiguration(); } public function getObject() { return $this->assertAttached($this->object); } public function attachObject($object) { $this->object = $object; return $this; } public function getHandle() { return $this->assertAttached($this->handle); } public function attachHandle(PhabricatorObjectHandle $handle) { $this->handle = $handle; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getOwnerPHID()); } public function describeAutomaticCapability($capability) { return pht('Flags are private. Only you can view or edit your flags.'); } } diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php index 25b9765f01..ad34bbc9d8 100644 --- a/src/applications/fund/storage/FundBacker.php +++ b/src/applications/fund/storage/FundBacker.php @@ -1,114 +1,122 @@ setBackerPHID($actor->getPHID()) ->setStatus(self::STATUS_NEW); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', 'amountInCents' => 'uint32', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_initiative' => array( + 'columns' => array('initiativePHID'), + ), + 'key_backer' => array( + 'columns' => array('backerPHID'), + ), + ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(FundBackerPHIDType::TYPECONST); } protected function didReadData() { // The payment processing code is strict about types. $this->amountInCents = (int)$this->amountInCents; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getInitiative() { return $this->assertAttached($this->initiative); } public function attachInitiative(FundInitiative $initiative = null) { $this->initiative = $initiative; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // If we have the initiative, use the initiative's policy. // Otherwise, return NOONE. This allows the backer to continue seeing // a backer even if they're no longer allowed to see the initiative. $initiative = $this->getInitiative(); if ($initiative) { return $initiative->getPolicy($capability); } return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getBackerPHID()); } public function describeAutomaticCapability($capability) { return pht('A backer can always see what they have backed.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new FundBackerEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new FundBackerTransaction(); } } diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index 88d9ccea05..e0cb4b5fa8 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -1,163 +1,171 @@ pht('Open'), self::STATUS_CLOSED => pht('Closed'), ); } public static function initializeNewInitiative(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorFundApplication')) ->executeOne(); $view_policy = $app->getPolicy(FundDefaultViewCapability::CAPABILITY); return id(new FundInitiative()) ->setOwnerPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($actor->getPHID()) ->setStatus(self::STATUS_OPEN); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'description' => 'text', 'status' => 'text32', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_status' => array( + 'columns' => array('status'), + ), + 'key_owner' => array( + 'columns' => array('ownerPHID'), + ), + ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(FundInitiativePHIDType::TYPECONST); } public function getMonogram() { return 'I'.$this->getID(); } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->projectPHIDs = $phids; return $this; } public function isClosed() { return ($this->getStatus() == self::STATUS_CLOSED); } /* -( 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 $viewer) { return ($viewer->getPHID() == $this->getOwnerPHID()); } public function describeAutomaticCapability($capability) { return pht( 'The owner of an initiative can always view and edit it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new FundInitiativeEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new FundInitiativeTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getOwnerPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenRecevierInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getOwnerPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/herald/storage/transcript/HeraldTranscript.php b/src/applications/herald/storage/transcript/HeraldTranscript.php index 06b6bc6345..cafae6a424 100644 --- a/src/applications/herald/storage/transcript/HeraldTranscript.php +++ b/src/applications/herald/storage/transcript/HeraldTranscript.php @@ -1,232 +1,233 @@ 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, ), self::CONFIG_BINARY => array( 'objectTranscript' => true, 'ruleTranscripts' => true, 'conditionTranscripts' => true, 'applyTranscripts' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'time' => 'epoch', 'host' => 'text255', 'duration' => 'double', 'dryRun' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'objectPHID' => array( 'columns' => array('objectPHID'), ), 'garbageCollected' => array( 'columns' => array('garbageCollected', 'time'), ), ), ) + 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.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index 03285c1e3a..ca3b035cb0 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -1,238 +1,243 @@ setViewer($actor) ->withClasses(array('PhabricatorLegalpadApplication')) ->executeOne(); $view_policy = $app->getPolicy(LegalpadDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(LegalpadDefaultEditCapability::CAPABILITY); return id(new LegalpadDocument()) ->setVersions(0) ->setCreatorPHID($actor->getPHID()) ->setContributorCount(0) ->setRecentContributorPHIDs(array()) ->attachSignatures(array()) ->setSignatureType(self::SIGNATURE_TYPE_INDIVIDUAL) ->setPreamble('') ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'recentContributorPHIDs' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'contributorCount' => 'uint32', 'versions' => 'uint32', 'mailKey' => 'bytes20', 'signatureType' => 'text4', 'preamble' => 'text', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_creator' => array( + 'columns' => array('creatorPHID', 'dateModified'), + ), + ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorLegalpadDocumentPHIDType::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 getSignatures() { return $this->assertAttached($this->signatures); } public function attachSignatures(array $signatures) { $this->signatures = $signatures; return $this; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getMonogram() { return 'L'.$this->getID(); } public function getUserSignature($phid) { return $this->assertAttachedKey($this->userSignatures, $phid); } public function attachUserSignature( $user_phid, LegalpadDocumentSignature $signature = null) { $this->userSignatures[$user_phid] = $signature; return $this; } public static function getSignatureTypeMap() { return array( self::SIGNATURE_TYPE_INDIVIDUAL => pht('Individuals'), self::SIGNATURE_TYPE_CORPORATION => pht('Corporations'), ); } public function getSignatureTypeName() { $type = $this->getSignatureType(); return idx(self::getSignatureTypeMap(), $type, $type); } public function getSignatureTypeIcon() { $type = $this->getSignatureType(); $map = array( self::SIGNATURE_TYPE_INDIVIDUAL => 'fa-user grey', self::SIGNATURE_TYPE_CORPORATION => 'fa-building-o grey', ); return idx($map, $type, 'fa-user grey'); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->creatorPHID == $phid); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ 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 $this; } public function getApplicationTransactionTemplate() { return new LegalpadTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $bodies = id(new LegalpadDocumentBody())->loadAllWhere( 'documentPHID = %s', $this->getPHID()); foreach ($bodies as $body) { $body->delete(); } $signatures = id(new LegalpadDocumentSignature())->loadAllWhere( 'documentPHID = %s', $this->getPHID()); foreach ($signatures as $signature) { $signature->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/legalpad/storage/LegalpadDocumentBody.php b/src/applications/legalpad/storage/LegalpadDocumentBody.php index 97a1eb1183..a4800f2192 100644 --- a/src/applications/legalpad/storage/LegalpadDocumentBody.php +++ b/src/applications/legalpad/storage/LegalpadDocumentBody.php @@ -1,79 +1,80 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'version' => 'uint32', 'title' => 'text255', 'text' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_document' => array( 'columns' => array('documentPHID', 'version'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_LEGB); } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return 'LEGB:'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_TEXT: $text = $this->getText(); break; case self::MARKUP_FIELD_TITLE: $text = $this->getTitle(); break; default: throw new Exception('Unknown field: '.$field); break; } return $text; } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } } diff --git a/src/applications/legalpad/storage/LegalpadDocumentSignature.php b/src/applications/legalpad/storage/LegalpadDocumentSignature.php index 1a7d7dc824..e247073c6d 100644 --- a/src/applications/legalpad/storage/LegalpadDocumentSignature.php +++ b/src/applications/legalpad/storage/LegalpadDocumentSignature.php @@ -1,89 +1,100 @@ array( 'signatureData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'documentVersion' => 'uint32', 'signatureType' => 'text4', 'signerPHID' => 'phid?', 'signerName' => 'text255', 'signerEmail' => 'text255', 'secretKey' => 'bytes20', 'verified' => 'bool?', 'isExemption' => 'bool', 'exemptionPHID' => 'phid?', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_signer' => array( + 'columns' => array('signerPHID', 'dateModified'), + ), + 'secretKey' => array( + 'columns' => array('secretKey'), + ), + 'key_document' => array( + 'columns' => array('documentPHID', 'signerPHID', 'documentVersion'), + ), + ), ) + parent::getConfiguration(); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function isVerified() { return ($this->getVerified() != self::UNVERIFIED); } public function getDocument() { return $this->assertAttached($this->document); } public function attachDocument(LegalpadDocument $document) { $this->document = $document; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getDocument()->getPolicy( PhabricatorPolicyCapability::CAN_EDIT); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getSignerPHID()); } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/legalpad/storage/LegalpadTransactionComment.php b/src/applications/legalpad/storage/LegalpadTransactionComment.php index 800303009e..d8315ddaad 100644 --- a/src/applications/legalpad/storage/LegalpadTransactionComment.php +++ b/src/applications/legalpad/storage/LegalpadTransactionComment.php @@ -1,40 +1,41 @@ getTransactionPHID() != null); } public function getConfiguration() { $config = parent::getConfiguration(); $config[self::CONFIG_COLUMN_SCHEMA] = array( 'documentID' => 'id?', 'lineNumber' => 'uint32', 'lineLength' => 'uint32', 'fixedState' => 'text12?', 'hasReplies' => 'bool', 'replyToCommentPHID' => 'phid?', ) + $config[self::CONFIG_COLUMN_SCHEMA]; $config[self::CONFIG_KEY_SCHEMA] = array( 'key_draft' => array( 'columns' => array('authorPHID', 'documentID', 'transactionPHID'), + 'unique' => true, ), ) + $config[self::CONFIG_KEY_SCHEMA]; return $config; } } diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index 1ea5ac3704..61f0000779 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -1,129 +1,136 @@ 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, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'authorPHID' => 'phid?', 'isDisabled' => 'bool', 'audioPHID' => 'phid?', 'audioBehavior' => 'text64', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'name' => array( 'columns' => array('name'), + 'unique' => true, + ), + 'key_disabled' => array( + 'columns' => array('isDisabled'), + ), + 'key_dateCreated' => array( + 'columns' => array('dateCreated'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorMacroMacroPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorMacroEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorMacroTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php index dd500bb0d3..6018d02f8d 100644 --- a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php +++ b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php @@ -1,65 +1,69 @@ self::IDS_MANUAL, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'chronologicalKey' => 'uint64', 'hasViewed' => 'bool', 'id' => null, ), self::CONFIG_KEY_SCHEMA => array( 'PRIMARY' => null, 'userPHID' => array( 'columns' => array('userPHID', 'chronologicalKey'), + 'unique' => true, + ), + 'userPHID_2' => array( + 'columns' => array('userPHID', 'hasViewed', 'primaryObjectPHID'), ), ), ) + parent::getConfiguration(); } static public function updateObjectNotificationViews( PhabricatorUser $user, $object_phid) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $notification_table = new PhabricatorFeedStoryNotification(); $conn = $notification_table->establishConnection('w'); queryfx( $conn, 'UPDATE %T SET hasViewed = 1 WHERE userPHID = %s AND primaryObjectPHID = %s AND hasViewed = 0', $notification_table->getTableName(), $user->getPHID(), $object_phid); unset($unguarded); } public function countUnread(PhabricatorUser $user) { $conn = $this->establishConnection('r'); $data = queryfx_one( $conn, 'SELECT COUNT(*) as count FROM %T WHERE userPHID = %s AND hasViewed = 0', $this->getTableName(), $user->getPHID()); return $data['count']; } } diff --git a/src/applications/nuance/storage/NuanceRequestorSource.php b/src/applications/nuance/storage/NuanceRequestorSource.php index ea23bcc891..6c607a643c 100644 --- a/src/applications/nuance/storage/NuanceRequestorSource.php +++ b/src/applications/nuance/storage/NuanceRequestorSource.php @@ -1,18 +1,34 @@ array( 'data' => self::SERIALIZATION_JSON, ), + self::CONFIG_COLUMN_SCHEMA => array( + 'sourceKey' => 'text128', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_source_key' => array( + 'columns' => array('sourcePHID', 'sourceKey'), + 'unique' => true, + ), + 'key_requestor' => array( + 'columns' => array('requestorPHID', 'id'), + ), + 'key_source' => array( + 'columns' => array('sourcePHID', 'id'), + ), + ), ) + parent::getConfiguration(); } } diff --git a/src/applications/pholio/storage/PholioTransactionComment.php b/src/applications/pholio/storage/PholioTransactionComment.php index d5693e8a90..926b74eb7d 100644 --- a/src/applications/pholio/storage/PholioTransactionComment.php +++ b/src/applications/pholio/storage/PholioTransactionComment.php @@ -1,54 +1,55 @@ 'id?', 'x' => 'uint32?', 'y' => 'uint32?', 'width' => 'uint32?', 'height' => 'uint32?', ) + $config[self::CONFIG_COLUMN_SCHEMA]; $config[self::CONFIG_KEY_SCHEMA] = array( 'key_draft' => array( 'columns' => array('authorPHID', 'imageID', 'transactionPHID'), + 'unique' => true, ), ) + $config[self::CONFIG_KEY_SCHEMA]; return $config; } public function toDictionary() { return array( 'id' => $this->getID(), 'phid' => $this->getPHID(), 'transactionPHID' => $this->getTransactionPHID(), 'x' => $this->getX(), 'y' => $this->getY(), 'width' => $this->getWidth(), 'height' => $this->getHeight(), ); } public function shouldUseMarkupCache($field) { // Only cache submitted comments. return ($this->getTransactionPHID() != null); } } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php b/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php index 3afaf910f5..c325b8ddc7 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php @@ -1,9 +1,22 @@ array( + 'pollID' => array( + 'columns' => array('pollID'), + ), + 'authorPHID' => array( + 'columns' => array('authorPHID'), + ), + ), + ) + parent::getConfiguration(); + } + } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php b/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php index 97893632dc..bac8ab5332 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php @@ -1,16 +1,21 @@ array( 'name' => 'text255', ), + self::CONFIG_KEY_SCHEMA => array( + 'pollID' => array( + 'columns' => array('pollID'), + ), + ), ) + parent::getConfiguration(); } } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php index 587ce1959e..6b677da2bb 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -1,179 +1,180 @@ setViewer($actor) ->withClasses(array('PhabricatorSlowvoteApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorSlowvoteDefaultViewCapability::CAPABILITY); return id(new PhabricatorSlowvotePoll()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'question' => 'text255', 'responseVisibility' => 'uint32', 'shuffle' => 'uint32', 'method' => 'uint32', 'description' => 'text', 'isClosed' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorSlowvotePollPHIDType::TYPECONST); } public function getOptions() { return $this->assertAttached($this->options); } public function attachOptions(array $options) { assert_instances_of($options, 'PhabricatorSlowvoteOption'); $this->options = $options; return $this; } public function getChoices() { return $this->assertAttached($this->choices); } public function attachChoices(array $choices) { assert_instances_of($choices, 'PhabricatorSlowvoteChoice'); $this->choices = $choices; return $this; } public function getViewerChoices(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->viewerChoices, $viewer->getPHID()); } public function attachViewerChoices(PhabricatorUser $viewer, array $choices) { if ($this->viewerChoices === self::ATTACHABLE) { $this->viewerChoices = array(); } assert_instances_of($choices, 'PhabricatorSlowvoteChoice'); $this->viewerChoices[$viewer->getPHID()] = $choices; 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 $this->viewPolicy; 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 poll can always view and edit it.'); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getAuthorPHID()); } /* -( PhabricatorDestructableInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( 'pollID = %d', $this->getID()); foreach ($choices as $choice) { $choice->delete(); } $options = id(new PhabricatorSlowvoteOption())->loadAllWhere( 'pollID = %d', $this->getID()); foreach ($options as $option) { $option->delete(); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/system/storage/PhabricatorSystemActionLog.php b/src/applications/system/storage/PhabricatorSystemActionLog.php index 9b49759c00..438806a685 100644 --- a/src/applications/system/storage/PhabricatorSystemActionLog.php +++ b/src/applications/system/storage/PhabricatorSystemActionLog.php @@ -1,28 +1,36 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'actorHash' => 'bytes12', 'actorIdentity' => 'text255', 'action' => 'text32', 'score' => 'double', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_epoch' => array( + 'columns' => array('epoch'), + ), + 'key_action' => array( + 'columns' => array('actorHash', 'action', 'epoch'), + ), + ), ) + parent::getConfiguration(); } public function setActorIdentity($identity) { $this->setActorHash(PhabricatorHash::digestForIndex($identity)); return parent::setActorIdentity($identity); } } diff --git a/src/applications/system/storage/PhabricatorSystemDestructionLog.php b/src/applications/system/storage/PhabricatorSystemDestructionLog.php index fca4e04705..aca41a043c 100644 --- a/src/applications/system/storage/PhabricatorSystemDestructionLog.php +++ b/src/applications/system/storage/PhabricatorSystemDestructionLog.php @@ -1,23 +1,28 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'objectClass' => 'text128', 'rootLogID' => 'id?', 'objectPHID' => 'phid?', 'objectMonogram' => 'text64?', ), + self::CONFIG_KEY_SCHEMA => array( + 'key_epoch' => array( + 'columns' => array('epoch'), + ), + ), ) + parent::getConfiguration(); } } diff --git a/src/applications/tokens/storage/PhabricatorTokenCount.php b/src/applications/tokens/storage/PhabricatorTokenCount.php index 6e057b76b1..c4be4407f0 100644 --- a/src/applications/tokens/storage/PhabricatorTokenCount.php +++ b/src/applications/tokens/storage/PhabricatorTokenCount.php @@ -1,23 +1,27 @@ self::IDS_MANUAL, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'tokenCount' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_objectPHID' => array( 'columns' => array('objectPHID'), + 'unique' => true, + ), + 'key_count' => array( + 'columns' => array('tokenCount'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/tokens/storage/PhabricatorTokenGiven.php b/src/applications/tokens/storage/PhabricatorTokenGiven.php index f6a1b0153d..5be2044551 100644 --- a/src/applications/tokens/storage/PhabricatorTokenGiven.php +++ b/src/applications/tokens/storage/PhabricatorTokenGiven.php @@ -1,74 +1,81 @@ array( 'key_all' => array( 'columns' => array('objectPHID', 'authorPHID'), + 'unique' => true, + ), + 'key_author' => array( + 'columns' => array('authorPHID'), + ), + 'key_token' => array( + 'columns' => array('tokenPHID'), ), ), ) + parent::getConfiguration(); } public function attachObject(PhabricatorTokenReceiverInterface $object) { $this->object = $object; return $this; } public function getObject() { return $this->assertAttached($this->object); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getObject()->getPolicy($capability); default: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getObject()->hasAutomaticCapability( $capability, $user); default: if ($user->getPHID() == $this->authorPHID) { return true; } return false; } } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht( 'A token inherits the policies of the object it is awarded to.'); case PhabricatorPolicyCapability::CAN_EDIT: return pht( 'The user who gave a token can always edit it.'); } return null; } } diff --git a/src/applications/xhprof/storage/PhabricatorXHProfSample.php b/src/applications/xhprof/storage/PhabricatorXHProfSample.php index b0e56a7962..8d13a76534 100644 --- a/src/applications/xhprof/storage/PhabricatorXHProfSample.php +++ b/src/applications/xhprof/storage/PhabricatorXHProfSample.php @@ -1,31 +1,32 @@ array( 'sampleRate' => 'uint32', 'usTotal' => 'uint64', 'hostname' => 'text255?', 'requestPath' => 'text255?', 'controller' => 'text255?', 'userPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'filePHID' => array( 'columns' => array('filePHID'), + 'unique' => true, ), ), ) + parent::getConfiguration(); } }