diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 01b695de55..b6c3a35cca 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -1,207 +1,207 @@ setServicePHID($service->getPHID()) ->attachAlmanacProperties(array()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_service' => array( 'columns' => array('servicePHID', 'interfacePHID'), 'unique' => true, ), 'key_device' => array( 'columns' => array('devicePHID'), ), 'key_interface' => array( 'columns' => array('interfacePHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(AlmanacBindingPHIDType::TYPECONST); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function getURI() { return '/almanac/binding/'.$this->getID().'/'; } public function getService() { return $this->assertAttached($this->service); } public function attachService(AlmanacService $service) { $this->service = $service; return $this; } public function getDevice() { return $this->assertAttached($this->device); } public function attachDevice(AlmanacDevice $device) { $this->device = $device; return $this; } public function getInterface() { return $this->assertAttached($this->interface); } public function attachInterface(AlmanacInterface $interface) { $this->interface = $interface; return $this; } /* -( AlmanacPropertyInterface )------------------------------------------- */ public function attachAlmanacProperties(array $properties) { assert_instances_of($properties, 'AlmanacProperty'); $this->almanacProperties = mpull($properties, null, 'getFieldName'); return $this; } public function getAlmanacProperties() { return $this->assertAttached($this->almanacProperties); } public function hasAlmanacProperty($key) { $this->assertAttached($this->almanacProperties); return isset($this->almanacProperties[$key]); } public function getAlmanacProperty($key) { return $this->assertAttachedKey($this->almanacProperties, $key); } public function getAlmanacPropertyValue($key, $default = null) { if ($this->hasAlmanacProperty($key)) { return $this->getAlmanacProperty($key)->getFieldValue(); } else { return $default; } } public function getAlmanacPropertyFieldSpecifications() { return array(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getService()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getService()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { $notes = array( pht('A binding inherits the policies of its service.'), pht( 'To view a binding, you must also be able to view its device and '. 'interface.'), ); if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { if ($this->getService()->getIsLocked()) { $notes[] = pht( 'The service for this binding is locked, so it can not be edited.'); } } return $notes; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'AlmanacCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacBindingEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new AlmanacBindingTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index 8075674831..19ee3f89ff 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -1,253 +1,253 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) ->setIsLocked(0); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', 'isLocked' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('nameIndex'), 'unique' => true, ), 'key_nametext' => array( 'columns' => array('name'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(AlmanacDevicePHIDType::TYPECONST); } public function save() { AlmanacNames::validateServiceOrDeviceName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function getURI() { return '/almanac/device/view/'.$this->getName().'/'; } /** * Find locked services which are bound to this device, updating the device * lock flag if necessary. * * @return list List of locking service PHIDs. */ public function rebuildDeviceLocks() { $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDevicePHIDs(array($this->getPHID())) ->withLocked(true) ->execute(); $locked = (bool)count($services); if ($locked != $this->getIsLocked()) { $this->setIsLocked((int)$locked); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %T SET isLocked = %d WHERE id = %d', $this->getTableName(), $this->getIsLocked(), $this->getID()); unset($unguarded); } return $this; } /* -( AlmanacPropertyInterface )------------------------------------------- */ public function attachAlmanacProperties(array $properties) { assert_instances_of($properties, 'AlmanacProperty'); $this->almanacProperties = mpull($properties, null, 'getFieldName'); return $this; } public function getAlmanacProperties() { return $this->assertAttached($this->almanacProperties); } public function hasAlmanacProperty($key) { $this->assertAttached($this->almanacProperties); return isset($this->almanacProperties[$key]); } public function getAlmanacProperty($key) { return $this->assertAttachedKey($this->almanacProperties, $key); } public function getAlmanacPropertyValue($key, $default = null) { if ($this->hasAlmanacProperty($key)) { return $this->getAlmanacProperty($key)->getFieldValue(); } else { return $default; } } public function getAlmanacPropertyFieldSpecifications() { return array(); } /* -( 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: if ($this->getIsLocked()) { return PhabricatorPolicies::POLICY_NOONE; } else { return $this->getEditPolicy(); } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { if ($this->getIsLocked()) { return pht( 'This device is bound to a locked service, so it can not '. 'be edited.'); } } return null; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'AlmanacCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacDeviceEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new AlmanacDeviceTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) { return $this->getURI(); } public function getSSHKeyDefaultName() { return $this->getName(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $interfaces = id(new AlmanacInterfaceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDevicePHIDs(array($this->getPHID())) ->execute(); foreach ($interfaces as $interface) { $engine->destroyObject($interface); } $this->delete(); } } diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index 16cd09c46d..5ab892cef7 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -1,132 +1,132 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'address' => 'text64', 'port' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_location' => array( 'columns' => array('networkPHID', 'address', 'port'), ), 'key_device' => array( 'columns' => array('devicePHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( AlmanacInterfacePHIDType::TYPECONST); } public function getDevice() { return $this->assertAttached($this->device); } public function attachDevice(AlmanacDevice $device) { $this->device = $device; return $this; } public function getNetwork() { return $this->assertAttached($this->network); } public function attachNetwork(AlmanacNetwork $network) { $this->network = $network; return $this; } public function toAddress() { return AlmanacAddress::newFromParts( $this->getNetworkPHID(), $this->getAddress(), $this->getPort()); } public function getAddressHash() { return $this->toAddress()->toHash(); } public function renderDisplayAddress() { return $this->getAddress().':'.$this->getPort(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getDevice()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getDevice()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { $notes = array( pht('An interface inherits the policies of the device it belongs to.'), pht( 'You must be able to view the network an interface resides on to '. 'view the interface.'), ); if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { if ($this->getDevice()->getIsLocked()) { $notes[] = pht( 'The device for this interface is locked, so it can not be edited.'); } } return $notes; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $bindings = id(new AlmanacBindingQuery()) ->setViewer($this->getViewer()) ->withInterfacePHIDs(array($this->getPHID())) ->execute(); foreach ($bindings as $binding) { $engine->destroyObject($binding); } $this->delete(); } } diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index 3a1e1cb632..92d4b8fa47 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -1,117 +1,117 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'mailKey' => 'bytes20', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(AlmanacNetworkPHIDType::TYPECONST); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function getURI() { return '/almanac/network/'.$this->getID().'/'; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacNetworkEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new AlmanacNetworkTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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 false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $interfaces = id(new AlmanacInterfaceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withNetworkPHIDs(array($this->getPHID())) ->execute(); foreach ($interfaces as $interface) { $engine->destroyObject($interface); } $this->delete(); } } diff --git a/src/applications/almanac/storage/AlmanacProperty.php b/src/applications/almanac/storage/AlmanacProperty.php index 9ce53f4362..b20fb14936 100644 --- a/src/applications/almanac/storage/AlmanacProperty.php +++ b/src/applications/almanac/storage/AlmanacProperty.php @@ -1,56 +1,56 @@ 'text128', ); return $config; } public function getObject() { return $this->assertAttached($this->object); } public function attachObject(PhabricatorLiskDAO $object) { $this->object = $object; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getObject()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Properties inherit the policies of their object.'); } } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 349fbca204..36c7761e22 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -1,234 +1,234 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) ->setIsLocked(0); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', 'serviceClass' => 'text64', 'isLocked' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('nameIndex'), 'unique' => true, ), 'key_nametext' => array( 'columns' => array('name'), ), 'key_class' => array( 'columns' => array('serviceClass'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(AlmanacServicePHIDType::TYPECONST); } public function save() { AlmanacNames::validateServiceOrDeviceName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function getURI() { return '/almanac/service/view/'.$this->getName().'/'; } public function getBindings() { return $this->assertAttached($this->bindings); } public function attachBindings(array $bindings) { $this->bindings = $bindings; return $this; } public function getServiceType() { return $this->assertAttached($this->serviceType); } public function attachServiceType(AlmanacServiceType $type) { $this->serviceType = $type; return $this; } /* -( AlmanacPropertyInterface )------------------------------------------- */ public function attachAlmanacProperties(array $properties) { assert_instances_of($properties, 'AlmanacProperty'); $this->almanacProperties = mpull($properties, null, 'getFieldName'); return $this; } public function getAlmanacProperties() { return $this->assertAttached($this->almanacProperties); } public function hasAlmanacProperty($key) { $this->assertAttached($this->almanacProperties); return isset($this->almanacProperties[$key]); } public function getAlmanacProperty($key) { return $this->assertAttachedKey($this->almanacProperties, $key); } public function getAlmanacPropertyValue($key, $default = null) { if ($this->hasAlmanacProperty($key)) { return $this->getAlmanacProperty($key)->getFieldValue(); } else { return $default; } } public function getAlmanacPropertyFieldSpecifications() { return $this->getServiceType()->getFieldSpecifications(); } /* -( 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: if ($this->getIsLocked()) { return PhabricatorPolicies::POLICY_NOONE; } else { return $this->getEditPolicy(); } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getIsLocked()) { return pht('This service is locked and can not be edited.'); } break; } return null; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'AlmanacCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacServiceEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new AlmanacServiceTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $bindings = id(new AlmanacBindingQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withServicePHIDs(array($this->getPHID())) ->execute(); foreach ($bindings as $binding) { $engine->destroyObject($binding); } $this->delete(); } } diff --git a/src/applications/audit/storage/PhabricatorAuditTransactionComment.php b/src/applications/audit/storage/PhabricatorAuditTransactionComment.php index 55508d6243..9a9925258e 100644 --- a/src/applications/audit/storage/PhabricatorAuditTransactionComment.php +++ b/src/applications/audit/storage/PhabricatorAuditTransactionComment.php @@ -1,58 +1,58 @@ getTransactionPHID() != null); } - public function getConfiguration() { + protected 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 2e2870ef65..8420ea9ba7 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -1,52 +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 957e195fbc..cced9a0cc7 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php @@ -1,135 +1,135 @@ true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'isEnabled' => 'bool', 'providerClass' => 'text128', 'providerType' => 'text32', '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; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuthProviderConfigEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorAuthProviderConfigTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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/PhabricatorAuthSSHKey.php b/src/applications/auth/storage/PhabricatorAuthSSHKey.php index 0383e4b701..c87c9b50b6 100644 --- a/src/applications/auth/storage/PhabricatorAuthSSHKey.php +++ b/src/applications/auth/storage/PhabricatorAuthSSHKey.php @@ -1,92 +1,92 @@ array( 'name' => 'text255', 'keyType' => 'text255', 'keyIndex' => 'bytes12', 'keyBody' => 'text', 'keyComment' => 'text255', 'isTrusted' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID'), ), 'key_unique' => array( 'columns' => array('keyIndex'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function save() { $this->setKeyIndex($this->toPublicKey()->getHash()); return parent::save(); } public function toPublicKey() { return PhabricatorAuthSSHPublicKey::newFromStoredKey($this); } public function getEntireKey() { $parts = array( $this->getKeyType(), $this->getKeyBody(), $this->getKeyComment(), ); return trim(implode(' ', $parts)); } public function getObject() { return $this->assertAttached($this->object); } public function attachObject(PhabricatorSSHPublicKeyInterface $object) { $this->object = $object; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getObject()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'SSH keys inherit the policies of the user or object they authenticate.'); } } diff --git a/src/applications/auth/storage/PhabricatorAuthSession.php b/src/applications/auth/storage/PhabricatorAuthSession.php index 0c11c3627a..5102c09aff 100644 --- a/src/applications/auth/storage/PhabricatorAuthSession.php +++ b/src/applications/auth/storage/PhabricatorAuthSession.php @@ -1,107 +1,107 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'type' => 'text32', 'sessionKey' => 'bytes40', 'sessionStart' => 'epoch', 'sessionExpires' => 'epoch', 'highSecurityUntil' => 'epoch?', 'isPartial' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'sessionKey' => array( 'columns' => array('sessionKey'), 'unique' => true, ), 'key_identity' => array( 'columns' => array('userPHID', 'type'), ), 'key_expires' => array( 'columns' => array('sessionExpires'), ), ), ) + parent::getConfiguration(); } public function getApplicationName() { // This table predates the "Auth" application, and really all applications. return 'user'; } public function getTableName() { // This is a very old table with a nonstandard name. return PhabricatorUser::SESSION_TABLE; } public function attachIdentityObject($identity_object) { $this->identityObject = $identity_object; return $this; } public function getIdentityObject() { return $this->assertAttached($this->identityObject); } public static function getSessionTypeTTL($session_type) { switch ($session_type) { case self::TYPE_WEB: return phutil_units('30 days in seconds'); case self::TYPE_CONDUIT: return phutil_units('24 hours in seconds'); default: throw new Exception(pht('Unknown session type "%s".', $session_type)); } } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if (!$viewer->getPHID()) { return false; } $object = $this->getIdentityObject(); if ($object instanceof PhabricatorUser) { return ($object->getPHID() == $viewer->getPHID()); } else if ($object instanceof PhabricatorExternalAccount) { return ($object->getUserPHID() == $viewer->getPHID()); } return false; } public function describeAutomaticCapability($capability) { return pht('A session is visible only to its owner.'); } } diff --git a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php index 8a71f6024b..e379a6aaeb 100644 --- a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php +++ b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php @@ -1,108 +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/PhabricatorMarkupCache.php b/src/applications/cache/storage/PhabricatorMarkupCache.php index e4f7f6722a..03a4b08681 100644 --- a/src/applications/cache/storage/PhabricatorMarkupCache.php +++ b/src/applications/cache/storage/PhabricatorMarkupCache.php @@ -1,33 +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 aba4fdbc96..1e94041f5b 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,126 +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() { + protected 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 71549a1c0e..d504a71ce0 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarHoliday.php +++ b/src/applications/calendar/storage/PhabricatorCalendarHoliday.php @@ -1,42 +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 7416f7b4e6..d9b9ef67e9 100644 --- a/src/applications/chatlog/storage/PhabricatorChatLogChannel.php +++ b/src/applications/chatlog/storage/PhabricatorChatLogChannel.php @@ -1,55 +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 39fd1d37e9..1513e12a82 100644 --- a/src/applications/chatlog/storage/PhabricatorChatLogEvent.php +++ b/src/applications/chatlog/storage/PhabricatorChatLogEvent.php @@ -1,63 +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 0baf243e9a..79a5e64189 100644 --- a/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php +++ b/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php @@ -1,26 +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 9f4bcf0be0..498bb6ecfa 100644 --- a/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php +++ b/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php @@ -1,26 +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 bf7a501c20..2e3f12b0e1 100644 --- a/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php +++ b/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php @@ -1,59 +1,59 @@ array( 'id' => 'auto64', 'connectionID' => 'id64?', 'method' => 'text64', '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/conduit/storage/PhabricatorConduitToken.php b/src/applications/conduit/storage/PhabricatorConduitToken.php index 5c39173c04..ab4d88335e 100644 --- a/src/applications/conduit/storage/PhabricatorConduitToken.php +++ b/src/applications/conduit/storage/PhabricatorConduitToken.php @@ -1,165 +1,165 @@ array( 'tokenType' => 'text32', 'token' => 'text32', 'expires' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID', 'tokenType'), ), 'key_token' => array( 'columns' => array('token'), 'unique' => true, ), 'key_expires' => array( 'columns' => array('expires'), ), ), ) + parent::getConfiguration(); } public static function loadClusterTokenForUser(PhabricatorUser $user) { if (!$user->isLoggedIn()) { return null; } $tokens = id(new PhabricatorConduitTokenQuery()) ->setViewer($user) ->withObjectPHIDs(array($user->getPHID())) ->withTokenTypes(array(self::TYPE_CLUSTER)) ->withExpired(false) ->execute(); // Only return a token if it has at least 5 minutes left before // expiration. Cluster tokens cycle regularly, so we don't want to use // one that's going to expire momentarily. $now = PhabricatorTime::getNow(); $must_expire_after = $now + phutil_units('5 minutes in seconds'); foreach ($tokens as $token) { if ($token->getExpires() > $must_expire_after) { return $token; } } // We didn't find any existing tokens (or the existing tokens are all about // to expire) so generate a new token. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $token = PhabricatorConduitToken::initializeNewToken( $user->getPHID(), self::TYPE_CLUSTER); $token->save(); unset($unguarded); return $token; } public static function initializeNewToken($object_phid, $token_type) { $token = new PhabricatorConduitToken(); $token->objectPHID = $object_phid; $token->tokenType = $token_type; $token->expires = $token->getTokenExpires($token_type); $secret = $token_type.'-'.Filesystem::readRandomCharacters(32); $secret = substr($secret, 0, 32); $token->token = $secret; return $token; } public static function getTokenTypeName($type) { $map = array( self::TYPE_STANDARD => pht('Standard API Token'), self::TYPE_COMMANDLINE => pht('Command Line API Token'), self::TYPE_CLUSTER => pht('Cluster API Token'), ); return idx($map, $type, $type); } public static function getAllTokenTypes() { return array( self::TYPE_STANDARD, self::TYPE_COMMANDLINE, self::TYPE_CLUSTER, ); } private function getTokenExpires($token_type) { $now = PhabricatorTime::getNow(); switch ($token_type) { case self::TYPE_STANDARD: return null; case self::TYPE_COMMANDLINE: return $now + phutil_units('1 hour in seconds'); case self::TYPE_CLUSTER: return $now + phutil_units('30 minutes in seconds'); default: throw new Exception( pht('Unknown Conduit token type "%s"!', $token_type)); } } public function getPublicTokenName() { switch ($this->getTokenType()) { case self::TYPE_CLUSTER: return pht('Cluster API Token'); default: return substr($this->getToken(), 0, 8).'...'; } } public function getObject() { return $this->assertAttached($this->object); } public function attachObject(PhabricatorUser $object) { $this->object = $object; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getObject()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Conduit tokens inherit the policies of the user they authenticate.'); } } diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php index abe7f4e036..53935d4ab0 100644 --- a/src/applications/config/storage/PhabricatorConfigEntry.php +++ b/src/applications/config/storage/PhabricatorConfigEntry.php @@ -1,102 +1,102 @@ 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') ->setIsDeleted(0); } return $config_entry; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorConfigEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorConfigTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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/ConpherenceIndex.php b/src/applications/conpherence/storage/ConpherenceIndex.php index 3676bbfc91..e8c9c5c82f 100644 --- a/src/applications/conpherence/storage/ConpherenceIndex.php +++ b/src/applications/conpherence/storage/ConpherenceIndex.php @@ -1,38 +1,38 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'previousTransactionPHID' => 'phid?', 'corpus' => 'fulltext', ), self::CONFIG_KEY_SCHEMA => array( 'key_thread' => array( 'columns' => array('threadPHID'), ), 'key_transaction' => array( 'columns' => array('transactionPHID'), 'unique' => true, ), 'key_previous' => array( 'columns' => array('previousTransactionPHID'), 'unique' => true, ), 'key_corpus' => array( 'columns' => array('corpus'), 'type' => 'FULLTEXT', ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/conpherence/storage/ConpherenceParticipant.php b/src/applications/conpherence/storage/ConpherenceParticipant.php index d8687d2ef8..a656dce776 100644 --- a/src/applications/conpherence/storage/ConpherenceParticipant.php +++ b/src/applications/conpherence/storage/ConpherenceParticipant.php @@ -1,62 +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 629ed9b0e5..04c335acc8 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -1,209 +1,209 @@ setMessageCount(0) ->setTitle(''); } - public function getConfiguration() { + protected 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 50169d7cd3..cc5e87e982 100644 --- a/src/applications/conpherence/storage/ConpherenceTransactionComment.php +++ b/src/applications/conpherence/storage/ConpherenceTransactionComment.php @@ -1,29 +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/countdown/storage/PhabricatorCountdown.php b/src/applications/countdown/storage/PhabricatorCountdown.php index 21b6db2b22..1f470f04f4 100644 --- a/src/applications/countdown/storage/PhabricatorCountdown.php +++ b/src/applications/countdown/storage/PhabricatorCountdown.php @@ -1,69 +1,69 @@ setViewer($actor) ->withClasses(array('PhabricatorCountdownApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorCountdownDefaultViewCapability::CAPABILITY); return id(new PhabricatorCountdown()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEpoch(PhabricatorTime::getNow()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCountdownCountdownPHIDType::TYPECONST); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht('The author of a countdown can always view and edit it.'); } } diff --git a/src/applications/daemon/storage/PhabricatorDaemonLog.php b/src/applications/daemon/storage/PhabricatorDaemonLog.php index 7af4228310..a0d121b393 100644 --- a/src/applications/daemon/storage/PhabricatorDaemonLog.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLog.php @@ -1,80 +1,80 @@ array( 'argv' => self::SERIALIZATION_JSON, 'explicitArgv' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'daemon' => 'text255', 'host' => 'text255', 'pid' => 'uint32', 'runningAsUser' => 'text255?', '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 fd1110ae06..afdbd1f621 100644 --- a/src/applications/daemon/storage/PhabricatorDaemonLogEvent.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLogEvent.php @@ -1,25 +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/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php index 2b5dd29d53..225cd93cea 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboard.php +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -1,157 +1,157 @@ setName('') ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy($actor->getPHID()) ->attachPanels(array()) ->attachPanelPHIDs(array()); } public static function copyDashboard( PhabricatorDashboard $dst, PhabricatorDashboard $src) { $dst->name = $src->name; $dst->layoutConfig = $src->layoutConfig; return $dst; } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'layoutConfig' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorDashboardDashboardPHIDType::TYPECONST); } public function getLayoutConfigObject() { return PhabricatorDashboardLayoutConfig::newFromDictionary( $this->getLayoutConfig()); } public function setLayoutConfigFromObject( PhabricatorDashboardLayoutConfig $object) { $this->setLayoutConfig($object->toDictionary()); return $this; } public function attachPanelPHIDs(array $phids) { $this->panelPHIDs = $phids; return $this; } public function getPanelPHIDs() { return $this->assertAttached($this->panelPHIDs); } public function attachPanels(array $panels) { assert_instances_of($panels, 'PhabricatorDashboardPanel'); $this->panels = $panels; return $this; } public function getPanels() { return $this->assertAttached($this->panels); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorDashboardTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorDashboardTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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 false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $installs = id(new PhabricatorDashboardInstall())->loadAllWhere( 'dashboardPHID = %s', $this->getPHID()); foreach ($installs as $install) { $install->delete(); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/dashboard/storage/PhabricatorDashboardInstall.php b/src/applications/dashboard/storage/PhabricatorDashboardInstall.php index 0f108fbac8..318c064808 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboardInstall.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardInstall.php @@ -1,52 +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/dashboard/storage/PhabricatorDashboardPanel.php b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php index bf86cb2ac0..1b98407d2c 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php @@ -1,176 +1,176 @@ setName('') ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy($actor->getPHID()); } public static function copyPanel( PhabricatorDashboardPanel $dst, PhabricatorDashboardPanel $src) { $dst->name = $src->name; $dst->panelType = $src->panelType; $dst->properties = $src->properties; return $dst; } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'panelType' => 'text64', 'isArchived' => 'bool', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorDashboardPanelPHIDType::TYPECONST); } 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 getMonogram() { return 'W'.$this->getID(); } public function getImplementation() { return idx( PhabricatorDashboardPanelType::getAllPanelTypes(), $this->getPanelType()); } public function requireImplementation() { $impl = $this->getImplementation(); if (!$impl) { throw new Exception( pht( 'Attempting to use a panel in a way that requires an '. 'implementation, but the panel implementation ("%s") is unknown to '. 'Phabricator.', $this->getPanelType())); } return $impl; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorDashboardPanelTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorDashboardPanelTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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 false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'PhabricatorDashboardPanelCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/differential/storage/DifferentialAffectedPath.php b/src/applications/differential/storage/DifferentialAffectedPath.php index 35e7ba5d70..b8de95629b 100644 --- a/src/applications/differential/storage/DifferentialAffectedPath.php +++ b/src/applications/differential/storage/DifferentialAffectedPath.php @@ -1,32 +1,32 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'id' => null, ), self::CONFIG_KEY_SCHEMA => array( 'PRIMARY' => null, 'repositoryID' => array( 'columns' => array('repositoryID', 'pathID', 'epoch'), ), 'revisionID' => array( 'columns' => array('revisionID'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 1124582263..db8d3a5afe 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -1,480 +1,480 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'revisionID' => 'id?', 'authorPHID' => 'phid?', 'repositoryPHID' => 'phid?', 'sourceMachine' => 'text255?', 'sourcePath' => 'text255?', 'sourceControlSystem' => 'text64?', 'sourceControlBaseRevision' => 'text255?', 'sourceControlPath' => 'text255?', 'lintStatus' => 'uint32', 'unitStatus' => 'uint32', 'lineCount' => 'uint32', 'branch' => 'text255?', 'bookmark' => 'text255?', 'arcanistProjectPHID' => 'phid?', 'repositoryUUID' => 'text64?', // T6203/NULLABILITY // These should be non-null; all diffs should have a creation method // and the description should just be empty. 'creationMethod' => 'text255?', 'description' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'revisionID' => array( 'columns' => array('revisionID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialDiffPHIDType::TYPECONST); } public function addUnsavedChangeset(DifferentialChangeset $changeset) { if ($this->changesets === null) { $this->changesets = array(); } $this->unsavedChangesets[] = $changeset; $this->changesets[] = $changeset; return $this; } public function attachChangesets(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $this->changesets = $changesets; return $this; } public function getChangesets() { return $this->assertAttached($this->changesets); } public function loadChangesets() { if (!$this->getID()) { return array(); } return id(new DifferentialChangeset())->loadAllWhere( 'diffID = %d', $this->getID()); } public function attachArcanistProject( PhabricatorRepositoryArcanistProject $project = null) { $this->arcanistProject = $project; return $this; } public function getArcanistProject() { return $this->assertAttached($this->arcanistProject); } public function getArcanistProjectName() { $name = ''; if ($this->arcanistProject) { $project = $this->getArcanistProject(); $name = $project->getName(); } return $name; } public function save() { $this->openTransaction(); $ret = parent::save(); foreach ($this->unsavedChangesets as $changeset) { $changeset->setDiffID($this->getID()); $changeset->save(); } $this->saveTransaction(); return $ret; } public static function initializeNewDiff(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); $diff = id(new DifferentialDiff()) ->setViewPolicy($view_policy); return $diff; } public static function newFromRawChanges( PhabricatorUser $actor, array $changes) { assert_instances_of($changes, 'ArcanistDiffChange'); $diff = self::initializeNewDiff($actor); return self::buildChangesetsFromRawChanges($diff, $changes); } public static function newEphemeralFromRawChanges(array $changes) { assert_instances_of($changes, 'ArcanistDiffChange'); $diff = id(new DifferentialDiff())->makeEphemeral(); return self::buildChangesetsFromRawChanges($diff, $changes); } private static function buildChangesetsFromRawChanges( DifferentialDiff $diff, array $changes) { // There may not be any changes; initialize the changesets list so that // we don't throw later when accessing it. $diff->attachChangesets(array()); $lines = 0; foreach ($changes as $change) { if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) { // If a user pastes a diff into Differential which includes a commit // message (e.g., they ran `git show` to generate it), discard that // change when constructing a DifferentialDiff. continue; } $changeset = new DifferentialChangeset(); $add_lines = 0; $del_lines = 0; $first_line = PHP_INT_MAX; $hunks = $change->getHunks(); if ($hunks) { foreach ($hunks as $hunk) { $dhunk = new DifferentialHunkModern(); $dhunk->setOldOffset($hunk->getOldOffset()); $dhunk->setOldLen($hunk->getOldLength()); $dhunk->setNewOffset($hunk->getNewOffset()); $dhunk->setNewLen($hunk->getNewLength()); $dhunk->setChanges($hunk->getCorpus()); $changeset->addUnsavedHunk($dhunk); $add_lines += $hunk->getAddLines(); $del_lines += $hunk->getDelLines(); $added_lines = $hunk->getChangedLines('new'); if ($added_lines) { $first_line = min($first_line, head_key($added_lines)); } } $lines += $add_lines + $del_lines; } else { // This happens when you add empty files. $changeset->attachHunks(array()); } $metadata = $change->getAllMetadata(); if ($first_line != PHP_INT_MAX) { $metadata['line:first'] = $first_line; } $changeset->setOldFile($change->getOldPath()); $changeset->setFilename($change->getCurrentPath()); $changeset->setChangeType($change->getType()); $changeset->setFileType($change->getFileType()); $changeset->setMetadata($metadata); $changeset->setOldProperties($change->getOldProperties()); $changeset->setNewProperties($change->getNewProperties()); $changeset->setAwayPaths($change->getAwayPaths()); $changeset->setAddLines($add_lines); $changeset->setDelLines($del_lines); $diff->addUnsavedChangeset($changeset); } $diff->setLineCount($lines); $parser = new DifferentialChangesetParser(); $changesets = $parser->detectCopiedCode( $diff->getChangesets(), $min_width = 30, $min_lines = 3); $diff->attachChangesets($changesets); return $diff; } public function getDiffDict() { $dict = array( 'id' => $this->getID(), 'revisionID' => $this->getRevisionID(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(), 'sourceControlPath' => $this->getSourceControlPath(), 'sourceControlSystem' => $this->getSourceControlSystem(), 'branch' => $this->getBranch(), 'bookmark' => $this->getBookmark(), 'creationMethod' => $this->getCreationMethod(), 'description' => $this->getDescription(), 'unitStatus' => $this->getUnitStatus(), 'lintStatus' => $this->getLintStatus(), 'changes' => array(), 'properties' => array(), 'projectName' => $this->getArcanistProjectName(), ); $dict['changes'] = $this->buildChangesList(); $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $property) { $dict['properties'][$property->getName()] = $property->getData(); if ($property->getName() == 'local:commits') { foreach ($property->getData() as $commit) { $dict['authorName'] = $commit['author']; $dict['authorEmail'] = idx($commit, 'authorEmail'); break; } } } return $dict; } public function buildChangesList() { $changes = array(); foreach ($this->getChangesets() as $changeset) { $hunks = array(); foreach ($changeset->getHunks() as $hunk) { $hunks[] = array( 'oldOffset' => $hunk->getOldOffset(), 'newOffset' => $hunk->getNewOffset(), 'oldLength' => $hunk->getOldLen(), 'newLength' => $hunk->getNewLen(), 'addLines' => null, 'delLines' => null, 'isMissingOldNewline' => null, 'isMissingNewNewline' => null, 'corpus' => $hunk->getChanges(), ); } $change = array( 'id' => $changeset->getID(), 'metadata' => $changeset->getMetadata(), 'oldPath' => $changeset->getOldFile(), 'currentPath' => $changeset->getFilename(), 'awayPaths' => $changeset->getAwayPaths(), 'oldProperties' => $changeset->getOldProperties(), 'newProperties' => $changeset->getNewProperties(), 'type' => $changeset->getChangeType(), 'fileType' => $changeset->getFileType(), 'commitHash' => null, 'addLines' => $changeset->getAddLines(), 'delLines' => $changeset->getDelLines(), 'hunks' => $hunks, ); $changes[] = $change; } return $changes; } public function hasRevision() { return $this->revision !== self::ATTACHABLE; } public function getRevision() { return $this->assertAttached($this->revision); } public function attachRevision(DifferentialRevision $revision = null) { $this->revision = $revision; return $this; } public function attachProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key) { return $this->assertAttachedKey($this->properties, $key); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { if ($this->hasRevision()) { return $this->getRevision()->getPolicy($capability); } return $this->viewPolicy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->hasRevision()) { return $this->getRevision()->hasAutomaticCapability($capability, $viewer); } return ($this->getAuthorPHID() == $viewer->getPhid()); } public function describeAutomaticCapability($capability) { if ($this->hasRevision()) { return pht( 'This diff is attached to a revision, and inherits its policies.'); } return pht('The author of a diff can see it.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { return $this->getPHID(); } public function getHarbormasterContainerPHID() { if ($this->getRevisionID()) { $revision = id(new DifferentialRevision())->load($this->getRevisionID()); if ($revision) { return $revision->getPHID(); } } return null; } public function getBuildVariables() { $results = array(); $results['buildable.diff'] = $this->getID(); $revision = $this->getRevision(); $results['buildable.revision'] = $revision->getID(); $repo = $revision->getRepository(); if ($repo) { $results['repository.callsign'] = $repo->getCallsign(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); } return $results; } public function getAvailableBuildVariables() { return array( 'buildable.diff' => pht('The differential diff ID, if applicable.'), 'buildable.revision' => pht('The differential revision ID, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => pht('The URI to clone or checkout the repository from.'), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialDiffEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DifferentialDiffTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); foreach ($this->loadChangesets() as $changeset) { $changeset->delete(); } $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $prop) { $prop->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/differential/storage/DifferentialDraft.php b/src/applications/differential/storage/DifferentialDraft.php index 8ea12f73e0..371698937c 100644 --- a/src/applications/differential/storage/DifferentialDraft.php +++ b/src/applications/differential/storage/DifferentialDraft.php @@ -1,65 +1,65 @@ array( 'draftKey' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_unique' => array( 'columns' => array('objectPHID', 'authorPHID', 'draftKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public static function markHasDraft( $author_phid, $object_phid, $draft_key) { try { id(new DifferentialDraft()) ->setObjectPHID($object_phid) ->setAuthorPHID($author_phid) ->setDraftKey($draft_key) ->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // no worries } } public static function deleteHasDraft( $author_phid, $object_phid, $draft_key) { $draft = id(new DifferentialDraft())->loadOneWhere( 'objectPHID = %s AND authorPHID = %s AND draftKey = %s', $object_phid, $author_phid, $draft_key); if ($draft) { $draft->delete(); } } public static function deleteAllDrafts( $author_phid, $object_phid) { $drafts = id(new DifferentialDraft())->loadAllWhere( 'objectPHID = %s AND authorPHID = %s', $object_phid, $author_phid); foreach ($drafts as $draft) { $draft->delete(); } } } diff --git a/src/applications/differential/storage/DifferentialHunkLegacy.php b/src/applications/differential/storage/DifferentialHunkLegacy.php index c64a4f1c9d..a56f61e4e8 100644 --- a/src/applications/differential/storage/DifferentialHunkLegacy.php +++ b/src/applications/differential/storage/DifferentialHunkLegacy.php @@ -1,37 +1,37 @@ array( 'changes' => 'text?', 'oldOffset' => 'uint32', 'oldLen' => 'uint32', 'newOffset' => 'uint32', 'newLen' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'changesetID' => array( 'columns' => array('changesetID'), ), ), ) + parent::getConfiguration(); } public function getTableName() { return 'differential_hunk'; } public function getDataEncoding() { return 'utf8'; } public function forceEncoding($encoding) { // Not supported, these are always utf8. return $this; } } diff --git a/src/applications/differential/storage/DifferentialHunkModern.php b/src/applications/differential/storage/DifferentialHunkModern.php index 0279dd5f78..580ae3aca3 100644 --- a/src/applications/differential/storage/DifferentialHunkModern.php +++ b/src/applications/differential/storage/DifferentialHunkModern.php @@ -1,126 +1,126 @@ array( 'data' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'dataType' => 'bytes4', 'dataEncoding' => 'text16?', 'dataFormat' => 'bytes4', 'oldOffset' => 'uint32', 'oldLen' => 'uint32', 'newOffset' => 'uint32', 'newLen' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_changeset' => array( 'columns' => array('changesetID'), ), 'key_created' => array( 'columns' => array('dateCreated'), ), ), ) + parent::getConfiguration(); } public function setChanges($text) { $this->rawData = $text; $this->dataEncoding = $this->detectEncodingForStorage($text); $this->dataType = self::DATATYPE_TEXT; $this->dataFormat = self::DATAFORMAT_RAW; $this->data = $text; return $this; } public function getChanges() { return $this->getUTF8StringFromStorage( $this->getRawData(), nonempty($this->forcedEncoding, $this->getDataEncoding())); } public function forceEncoding($encoding) { $this->forcedEncoding = $encoding; return $this; } public function save() { $type = $this->getDataType(); $format = $this->getDataFormat(); // Before saving the data, attempt to compress it. if ($type == self::DATATYPE_TEXT) { if ($format == self::DATAFORMAT_RAW) { $data = $this->getData(); $deflated = PhabricatorCaches::maybeDeflateData($data); if ($deflated !== null) { $this->data = $deflated; $this->dataFormat = self::DATAFORMAT_DEFLATED; } } } return parent::save(); } private function getRawData() { if ($this->rawData === null) { $type = $this->getDataType(); $data = $this->getData(); switch ($type) { case self::DATATYPE_TEXT: // In this storage type, the changes are stored on the object. $data = $data; break; case self::DATATYPE_FILE: default: throw new Exception( pht('Hunk has unsupported data type "%s"!', $type)); } $format = $this->getDataFormat(); switch ($format) { case self::DATAFORMAT_RAW: // In this format, the changes are stored as-is. $data = $data; break; case self::DATAFORMAT_DEFLATED: $data = PhabricatorCaches::inflateData($data); break; default: throw new Exception( pht('Hunk has unsupported data encoding "%s"!', $type)); } $this->rawData = $data; } return $this->rawData; } } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 447ceee1b5..f74139f67a 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,597 +1,597 @@ setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) ->attachRelationships(array()) ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'originalTitle' => 'text255', 'status' => 'text32', 'summary' => 'text', 'testPlan' => 'text', 'authorPHID' => 'phid?', 'lastReviewerPHID' => 'phid?', 'lineCount' => 'uint32?', 'mailKey' => 'bytes40', 'branchName' => 'text255?', 'arcanistProjectPHID' => 'phid?', 'repositoryPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'repositoryPHID' => array( 'columns' => array('repositoryPHID'), ), ), ) + parent::getConfiguration(); } public function getMonogram() { $id = $this->getID(); return "D{$id}"; } public function setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } public function loadIDsByCommitPHIDs($phids) { if (!$phids) { return array(); } $revision_ids = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE commitPHID IN (%Ls)', self::TABLE_COMMIT, $phids); return ipull($revision_ids, 'revisionID', 'commitPHID'); } public function loadCommitPHIDs() { if (!$this->getID()) { return ($this->commits = array()); } $commits = queryfx_all( $this->establishConnection('r'), 'SELECT commitPHID FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $commits = ipull($commits, 'commitPHID'); return ($this->commits = $commits); } public function getCommitPHIDs() { return $this->assertAttached($this->commits); } public function getActiveDiff() { // TODO: Because it's currently technically possible to create a revision // without an associated diff, we allow an attached-but-null active diff. // It would be good to get rid of this once we make diff-attaching // transactional. return $this->assertAttached($this->activeDiff); } public function attachActiveDiff($diff) { $this->activeDiff = $diff; return $this; } public function getDiffIDs() { return $this->assertAttached($this->diffIDs); } public function attachDiffIDs(array $ids) { rsort($ids); $this->diffIDs = array_values($ids); return $this; } public function attachCommitPHIDs(array $phids) { $this->commits = array_values($phids); return $this; } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function setAttachedPHIDs($type, array $phids) { $this->attached[$type] = array_fill_keys($phids, array()); return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialRevisionPHIDType::TYPECONST); } public function loadActiveDiff() { return id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d ORDER BY id DESC LIMIT 1', $this->getID()); } public function save() { if (!$this->getMailKey()) { $this->mailKey = Filesystem::readRandomCharacters(40); } return parent::save(); } public function loadRelationships() { if (!$this->getID()) { $this->relationships = array(); return; } $data = array(); $subscriber_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorObjectHasSubscriberEdgeType::EDGECONST); $subscriber_phids = array_reverse($subscriber_phids); foreach ($subscriber_phids as $phid) { $data[] = array( 'relation' => self::RELATION_SUBSCRIBED, 'objectPHID' => $phid, 'reasonPHID' => null, ); } $reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), DifferentialRevisionHasReviewerEdgeType::EDGECONST); $reviewer_phids = array_reverse($reviewer_phids); foreach ($reviewer_phids as $phid) { $data[] = array( 'relation' => self::RELATION_REVIEWER, 'objectPHID' => $phid, 'reasonPHID' => null, ); } return $this->attachRelationships($data); } public function attachRelationships(array $relationships) { $this->relationships = igroup($relationships, 'relation'); return $this; } public function getReviewers() { return $this->getRelatedPHIDs(self::RELATION_REVIEWER); } public function getCCPHIDs() { return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED); } private function getRelatedPHIDs($relation) { $this->assertAttached($this->relationships); return ipull($this->getRawRelations($relation), 'objectPHID'); } public function getRawRelations($relation) { return idx($this->relationships, $relation, array()); } public function getPrimaryReviewer() { $reviewers = $this->getReviewers(); $last = $this->lastReviewerPHID; if (!$last || !in_array($last, $reviewers)) { return head($this->getReviewers()); } return $last; } public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } public function loadInlineComments( array &$changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $inline_comments = array(); $inline_comments = id(new DifferentialInlineCommentQuery()) ->withRevisionIDs(array($this->getID())) ->withNotDraft(true) ->execute(); $load_changesets = array(); foreach ($inline_comments as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { continue; } $load_changesets[$changeset_id] = true; } $more_changesets = array(); if ($load_changesets) { $changeset_ids = array_keys($load_changesets); $more_changesets += id(new DifferentialChangeset()) ->loadAllWhere( 'id IN (%Ld)', $changeset_ids); } if ($more_changesets) { $changesets += $more_changesets; $changesets = msort($changesets, 'getSortKey'); } return $inline_comments; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A revision's author (which effectively means "owner" after we added // commandeering) can always view and edit it. $author_phid = $this->getAuthorPHID(); if ($author_phid) { if ($user->getPHID() == $author_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { $description = array( pht('The owner of a revision can always view and edit it.'), ); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $description[] = pht( "A revision's reviewers can always view it."); $description[] = pht( 'If a revision belongs to a repository, other users must be able '. 'to view the repository in order to view the revision.'); break; } return $description; } public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } public function getReviewerStatus() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewerStatus(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); $this->reviewerStatus = $reviewers; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } public function isClosed() { return DifferentialRevisionStatus::isClosedStatus($this->getStatus()); } public function getFlag(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->flags, $viewer->getPHID()); } public function attachFlag( PhabricatorUser $viewer, PhabricatorFlag $flag = null) { $this->flags[$viewer->getPHID()] = $flag; return $this; } public function getDrafts(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getPHID()); } public function attachDrafts(PhabricatorUser $viewer, array $drafts) { $this->drafts[$viewer->getPHID()] = $drafts; return $this; } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { return $this->loadActiveDiff()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { if ($phid == $this->getAuthorPHID()) { return true; } // TODO: This only happens when adding or removing CCs, and is safe from a // policy perspective, but the subscription pathway should have some // opportunity to load this data properly. For now, this is the only case // where implicit subscription is not an intrinsic property of the object. if ($this->reviewerStatus == self::ATTACHABLE) { $reviewers = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->needReviewerStatus(true) ->executeOne() ->getReviewerStatus(); } else { $reviewers = $this->getReviewerStatus(); } foreach ($reviewers as $reviewer) { if ($reviewer->getReviewerPHID() == $phid) { return true; } } return false; } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('differential.fields'); } public function getCustomFieldBaseClass() { return 'DifferentialCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DifferentialTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { $render_data = $timeline->getRenderData(); $left = $request->getInt('left', idx($render_data, 'left')); $right = $request->getInt('right', idx($render_data, 'right')); $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withIDs(array($left, $right)) ->execute(); $diffs = mpull($diffs, null, 'getID'); $left_diff = $diffs[$left]; $right_diff = $diffs[$right]; $changesets = id(new DifferentialChangesetQuery()) ->setViewer($request->getUser()) ->withDiffs(array($right_diff)) ->execute(); // NOTE: this mutates $changesets to include changesets for all inline // comments...! $inlines = $this->loadInlineComments($changesets); $changesets = mpull($changesets, null, 'getID'); return $timeline ->setChangesets($changesets) ->setRevision($this) ->setLeftDiff($left_diff) ->setRightDiff($right_diff); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $diffs = id(new DifferentialDiffQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($diffs as $diff) { $engine->destroyObject($diff); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); try { $inlines = id(new DifferentialInlineCommentQuery()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($inlines as $inline) { $inline->delete(); } } catch (PhabricatorEmptyQueryException $ex) { // TODO: There's still some funky legacy wrapping going on here, and // we might catch a raw query exception. } // we have to do paths a little differentally as they do not have // an id or phid column for delete() to act on $dummy_path = new DifferentialAffectedPath(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $dummy_path->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/differential/storage/DifferentialTransactionComment.php b/src/applications/differential/storage/DifferentialTransactionComment.php index d8835e3b1a..fd406f3969 100644 --- a/src/applications/differential/storage/DifferentialTransactionComment.php +++ b/src/applications/differential/storage/DifferentialTransactionComment.php @@ -1,90 +1,90 @@ 'phid?', 'changesetID' => 'id?', 'isNewFile' => 'bool', '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', 'transactionPHID'), ), 'key_changeset' => array( 'columns' => array('changesetID'), ), 'key_revision' => array( 'columns' => array('revisionPHID'), ), ) + $config[self::CONFIG_KEY_SCHEMA]; return $config; } public function shouldUseMarkupCache($field) { // Only cache submitted comments. return ($this->getTransactionPHID() != null); } public static function sortAndGroupInlines( array $inlines, array $changesets) { assert_instances_of($inlines, 'DifferentialTransaction'); assert_instances_of($changesets, 'DifferentialChangeset'); $changesets = mpull($changesets, null, 'getID'); $changesets = msort($changesets, 'getFilename'); // Group the changesets by file and reorder them by display order. $inline_groups = array(); foreach ($inlines as $inline) { $changeset_id = $inline->getComment()->getChangesetID(); $inline_groups[$changeset_id][] = $inline; } $inline_groups = array_select_keys($inline_groups, array_keys($changesets)); foreach ($inline_groups as $changeset_id => $group) { // Sort the group of inlines by line number. $items = array(); foreach ($group as $inline) { $comment = $inline->getComment(); $num = $comment->getLineNumber(); $len = $comment->getLineLength(); $id = $comment->getID(); $items[] = array( 'inline' => $inline, 'sort' => sprintf('~%010d%010d%010d', $num, $len, $id), ); } $items = isort($items, 'sort'); $items = ipull($items, 'inline'); $inline_groups[$changeset_id] = $items; } return $inline_groups; } } diff --git a/src/applications/diviner/storage/DivinerLiveAtom.php b/src/applications/diviner/storage/DivinerLiveAtom.php index 989728665f..1303b97c65 100644 --- a/src/applications/diviner/storage/DivinerLiveAtom.php +++ b/src/applications/diviner/storage/DivinerLiveAtom.php @@ -1,25 +1,25 @@ false, self::CONFIG_SERIALIZATION => array( 'content' => self::SERIALIZATION_JSON, 'atomData' => self::SERIALIZATION_JSON, ), self::CONFIG_KEY_SCHEMA => array( 'symbolPHID' => array( 'columns' => array('symbolPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php index 310f3fc9ea..2d7cb5d66b 100644 --- a/src/applications/diviner/storage/DivinerLiveBook.php +++ b/src/applications/diviner/storage/DivinerLiveBook.php @@ -1,85 +1,85 @@ true, self::CONFIG_SERIALIZATION => array( 'configurationData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getConfig($key, $default = null) { return idx($this->configurationData, $key, $default); } public function setConfig($key, $value) { $this->configurationData[$key] = $value; return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DivinerBookPHIDType::TYPECONST); } public function getTitle() { return $this->getConfig('title', $this->getName()); } public function getShortTitle() { return $this->getConfig('short', $this->getTitle()); } public function getPreface() { return $this->getConfig('preface'); } public function getGroupName($group) { $groups = $this->getConfig('groups', array()); $spec = idx($groups, $group, array()); return idx($spec, 'name', $group); } /* -( 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/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php index 4b52d2ac7b..ce9b79f212 100644 --- a/src/applications/diviner/storage/DivinerLiveSymbol.php +++ b/src/applications/diviner/storage/DivinerLiveSymbol.php @@ -1,245 +1,245 @@ true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'context' => 'text255?', 'type' => 'text32', 'name' => 'text255', 'atomIndex' => 'uint32', 'identityHash' => 'bytes12', 'graphHash' => 'text64?', 'title' => 'text?', 'titleSlugHash' => 'bytes12?', 'groupName' => 'text255?', 'summary' => 'text?', 'isDocumentable' => 'bool', 'nodeHash' => 'text64?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'identityHash' => array( 'columns' => array('identityHash'), 'unique' => true, ), 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'graphHash' => array( 'columns' => array('graphHash'), 'unique' => true, ), 'nodeHash' => array( 'columns' => array('nodeHash'), 'unique' => true, ), 'bookPHID' => array( 'columns' => array( 'bookPHID', 'type', 'name(64)', 'context(64)', 'atomIndex', ), ), 'name' => array( 'columns' => array('name(64)'), ), 'key_slug' => array( 'columns' => array('titleSlugHash'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DivinerAtomPHIDType::TYPECONST); } public function getBook() { return $this->assertAttached($this->book); } public function attachBook(DivinerLiveBook $book) { $this->book = $book; return $this; } public function getAtom() { return $this->assertAttached($this->atom); } public function attachAtom(DivinerLiveAtom $atom) { $this->atom = DivinerAtom::newFromDictionary($atom->getAtomData()); return $this; } public function getURI() { $parts = array( 'book', $this->getBook()->getName(), $this->getType(), ); if ($this->getContext()) { $parts[] = $this->getContext(); } $parts[] = $this->getName(); if ($this->getAtomIndex()) { $parts[] = $this->getAtomIndex(); } return '/'.implode('/', $parts).'/'; } public function getSortKey() { // Sort articles before other types of content. Then, sort atoms in a // case-insensitive way. return sprintf( '%c:%s', ($this->getType() == DivinerAtom::TYPE_ARTICLE ? '0' : '1'), phutil_utf8_strtolower($this->getTitle())); } public function save() { // NOTE: The identity hash is just a sanity check because the unique tuple // on this table is way way too long to fit into a normal UNIQUE KEY. We // don't use it directly, but its existence prevents duplicate records. if (!$this->identityHash) { $this->identityHash = PhabricatorHash::digestForIndex( serialize( array( 'bookPHID' => $this->getBookPHID(), 'context' => $this->getContext(), 'type' => $this->getType(), 'name' => $this->getName(), 'index' => $this->getAtomIndex(), ))); } return parent::save(); } public function getTitle() { $title = parent::getTitle(); if (!strlen($title)) { $title = $this->getName(); } return $title; } public function setTitle($value) { $this->writeField('title', $value); if (strlen($value)) { $slug = DivinerAtomRef::normalizeTitleString($value); $hash = PhabricatorHash::digestForIndex($slug); $this->titleSlugHash = $hash; } else { $this->titleSlugHash = null; } return $this; } public function attachExtends(array $extends) { assert_instances_of($extends, 'DivinerLiveSymbol'); $this->extends = $extends; return $this; } public function getExtends() { return $this->assertAttached($this->extends); } public function attachChildren(array $children) { assert_instances_of($children, 'DivinerLiveSymbol'); $this->children = $children; return $this; } public function getChildren() { return $this->assertAttached($this->children); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return $this->getBook()->getCapabilities(); } public function getPolicy($capability) { return $this->getBook()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBook()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Atoms inherit the policies of the books they are part of.'); } /* -( Markup Interface )--------------------------------------------------- */ public function getMarkupFieldKey($field) { return $this->getPHID().':'.$field.':'.$this->getGraphHash(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine('diviner'); } public function getMarkupText($field) { return $this->getAtom()->getDocblockText(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return true; } } diff --git a/src/applications/doorkeeper/storage/DoorkeeperExternalObject.php b/src/applications/doorkeeper/storage/DoorkeeperExternalObject.php index 3719462ffb..3050008594 100644 --- a/src/applications/doorkeeper/storage/DoorkeeperExternalObject.php +++ b/src/applications/doorkeeper/storage/DoorkeeperExternalObject.php @@ -1,107 +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 129d999298..f50e44fa0f 100644 --- a/src/applications/draft/storage/PhabricatorDraft.php +++ b/src/applications/draft/storage/PhabricatorDraft.php @@ -1,101 +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/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index e7c0d63eb2..af258e0b1f 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -1,152 +1,152 @@ setViewer($actor) ->withClasses(array('PhabricatorDrydockApplication')) ->executeOne(); $view_policy = $app->getPolicy( DrydockDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( DrydockDefaultEditCapability::CAPABILITY); return id(new DrydockBlueprint()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setBlueprintName(''); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'className' => 'text255', 'blueprintName' => 'text255', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DrydockBlueprintPHIDType::TYPECONST); } public function getImplementation() { $class = $this->className; $implementations = DrydockBlueprintImplementation::getAllBlueprintImplementations(); if (!isset($implementations[$class])) { throw new Exception( "Invalid class name for blueprint (got '".$class."')"); } return id(new $class())->attachInstance($this); } public function attachImplementation(DrydockBlueprintImplementation $impl) { $this->implementation = $impl; return $this; } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DrydockBlueprintEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DrydockBlueprintTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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 false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'DrydockBlueprintCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index b9e79fe820..cb61e54490 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -1,229 +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() { + protected 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 763d531e42..36d310c510 100644 --- a/src/applications/drydock/storage/DrydockLog.php +++ b/src/applications/drydock/storage/DrydockLog.php @@ -1,82 +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 84b8bc6d69..d078c76c2f 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -1,134 +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/fact/storage/PhabricatorFactAggregate.php b/src/applications/fact/storage/PhabricatorFactAggregate.php index 2d0fe52872..de7a568706 100644 --- a/src/applications/fact/storage/PhabricatorFactAggregate.php +++ b/src/applications/fact/storage/PhabricatorFactAggregate.php @@ -1,25 +1,25 @@ array( 'id' => 'auto64', 'factType' => 'text32', 'valueX' => 'uint64', ), self::CONFIG_KEY_SCHEMA => array( 'factType' => array( 'columns' => array('factType', 'objectPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/fact/storage/PhabricatorFactCursor.php b/src/applications/fact/storage/PhabricatorFactCursor.php index 7d4988276d..377707fc97 100644 --- a/src/applications/fact/storage/PhabricatorFactCursor.php +++ b/src/applications/fact/storage/PhabricatorFactCursor.php @@ -1,23 +1,23 @@ array( 'name' => 'text64', 'position' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/fact/storage/PhabricatorFactDAO.php b/src/applications/fact/storage/PhabricatorFactDAO.php index 458f7f477d..40a0b1833d 100644 --- a/src/applications/fact/storage/PhabricatorFactDAO.php +++ b/src/applications/fact/storage/PhabricatorFactDAO.php @@ -1,15 +1,15 @@ false, ) + parent::getConfiguration(); } } diff --git a/src/applications/fact/storage/PhabricatorFactRaw.php b/src/applications/fact/storage/PhabricatorFactRaw.php index 5de2be7aaa..5972a22985 100644 --- a/src/applications/fact/storage/PhabricatorFactRaw.php +++ b/src/applications/fact/storage/PhabricatorFactRaw.php @@ -1,39 +1,39 @@ array( 'id' => 'auto64', 'factType' => 'text32', 'objectA' => 'phid', 'valueX' => 'sint64', 'valueY' => 'sint64', ), self::CONFIG_KEY_SCHEMA => array( 'objectPHID' => array( 'columns' => array('objectPHID'), ), 'factType' => array( 'columns' => array('factType', 'epoch'), ), 'factType_2' => array( 'columns' => array('factType', 'objectA'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/feed/storage/PhabricatorFeedStoryData.php b/src/applications/feed/storage/PhabricatorFeedStoryData.php index bf0aba84be..33d19fa616 100644 --- a/src/applications/feed/storage/PhabricatorFeedStoryData.php +++ b/src/applications/feed/storage/PhabricatorFeedStoryData.php @@ -1,69 +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 c5740b542d..6e5b7447ef 100644 --- a/src/applications/feed/storage/PhabricatorFeedStoryReference.php +++ b/src/applications/feed/storage/PhabricatorFeedStoryReference.php @@ -1,29 +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 30b40b8f35..cca3ef9f89 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1,1204 +1,1204 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withClasses(array('PhabricatorFilesApplication')) ->executeOne(); $view_policy = $app->getPolicy( FilesDefaultViewCapability::CAPABILITY); return id(new PhabricatorFile()) ->setViewPolicy($view_policy) ->attachOriginalFile(null) ->attachObjects(array()) ->attachObjectPHIDs(array()); } - public function getConfiguration() { + protected 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', '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_byte_size = $file->getByteSize(); $copy_of_mime_type = $file->getMimeType(); $new_file = PhabricatorFile::initializeNewFile(); $new_file->setByteSize($copy_of_byte_size); $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_mime_type); $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, 'fa-file-o'); } 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 = PhabricatorObjectHasFileEdgeType::EDGECONST; 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 = PhabricatorObjectHasFileEdgeType::EDGECONST; 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); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorFileEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorFileTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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/PhabricatorFileStorageBlob.php b/src/applications/files/storage/PhabricatorFileStorageBlob.php index 502b7dd8d7..3d0ce662cd 100644 --- a/src/applications/files/storage/PhabricatorFileStorageBlob.php +++ b/src/applications/files/storage/PhabricatorFileStorageBlob.php @@ -1,18 +1,18 @@ array( 'data' => true, ), ) + parent::getConfiguration(); } } diff --git a/src/applications/files/storage/PhabricatorFileTransactionComment.php b/src/applications/files/storage/PhabricatorFileTransactionComment.php index f1a2211f7a..ee668a49fb 100644 --- a/src/applications/files/storage/PhabricatorFileTransactionComment.php +++ b/src/applications/files/storage/PhabricatorFileTransactionComment.php @@ -1,26 +1,26 @@ getTransactionPHID() != null); } - public function getConfiguration() { + protected 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 e4e2c86dc5..ee27ae95f9 100644 --- a/src/applications/files/storage/PhabricatorTransformedFile.php +++ b/src/applications/files/storage/PhabricatorTransformedFile.php @@ -1,26 +1,26 @@ array( 'transform' => 'text128', ), 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 535b9a9e8e..697af08f14 100644 --- a/src/applications/flag/storage/PhabricatorFlag.php +++ b/src/applications/flag/storage/PhabricatorFlag.php @@ -1,76 +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 d9543bb225..d2a97cd32c 100644 --- a/src/applications/fund/storage/FundBacker.php +++ b/src/applications/fund/storage/FundBacker.php @@ -1,128 +1,128 @@ setBackerPHID($actor->getPHID()) ->setStatus(self::STATUS_NEW); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_APPLICATION_SERIALIZERS => array( 'amountAsCurrency' => new PhortuneCurrencySerializer(), ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', 'amountAsCurrency' => 'text64', ), 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); } 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(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index 1a266e3047..f5581fa0b7 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -1,197 +1,197 @@ 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) ->setTotalAsCurrency(PhortuneCurrency::newEmptyCurrency()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'description' => 'text', 'risks' => 'text', 'status' => 'text32', 'merchantPHID' => 'phid?', 'totalAsCurrency' => 'text64', 'mailKey' => 'bytes20', ), self::CONFIG_APPLICATION_SERIALIZERS => array( 'totalAsCurrency' => new PhortuneCurrencySerializer(), ), 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); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } 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 $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(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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/harbormaster/storage/HarbormasterBuildCommand.php b/src/applications/harbormaster/storage/HarbormasterBuildCommand.php index 88ad33e904..1522a054ac 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildCommand.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildCommand.php @@ -1,26 +1,26 @@ array( 'command' => 'text128', ), self::CONFIG_KEY_SCHEMA => array( 'key_target' => array( 'columns' => array('targetPHID'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildMessage.php b/src/applications/harbormaster/storage/HarbormasterBuildMessage.php index 4ed1c8c7bc..b212570e20 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildMessage.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildMessage.php @@ -1,72 +1,72 @@ setAuthorPHID($actor->getPHID()) ->setIsConsumed(0); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( 'type' => 'text16', 'isConsumed' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildtarget' => array( 'columns' => array('buildTargetPHID'), ), ), ) + parent::getConfiguration(); } public function getBuildTarget() { return $this->assertAttached($this->buildTarget); } public function attachBuildTarget(HarbormasterBuildTarget $target) { $this->buildTarget = $target; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuildTarget()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildTarget()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Build messages have the same policies as their targets.'); } } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index 3026837628..b53e2bc2d3 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -1,302 +1,302 @@ setIsManualBuildable(0) ->setBuildableStatus(self::STATUS_BUILDING); } public function getMonogram() { return 'B'.$this->getID(); } /** * Returns an existing buildable for the object's PHID or creates a * new buildable implicitly if needed. */ public static function createOrLoadExisting( PhabricatorUser $actor, $buildable_object_phid, $container_object_phid) { $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($actor) ->withBuildablePHIDs(array($buildable_object_phid)) ->withManualBuildables(false) ->setLimit(1) ->executeOne(); if ($buildable) { return $buildable; } $buildable = HarbormasterBuildable::initializeNewBuildable($actor) ->setBuildablePHID($buildable_object_phid) ->setContainerPHID($container_object_phid); $buildable->save(); return $buildable; } /** * Looks up the plan PHIDs and applies the plans to the specified * object identified by it's PHID. */ public static function applyBuildPlans( $phid, $container_phid, array $plan_phids) { if (count($plan_phids) === 0) { return; } // Skip all of this logic if the Harbormaster application // isn't currently installed. $harbormaster_app = 'PhabricatorHarbormasterApplication'; if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) { return; } $buildable = HarbormasterBuildable::createOrLoadExisting( PhabricatorUser::getOmnipotentUser(), $phid, $container_phid); $plans = id(new HarbormasterBuildPlanQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($plan_phids) ->execute(); foreach ($plans as $plan) { if ($plan->isDisabled()) { // TODO: This should be communicated more clearly -- maybe we should // create the build but set the status to "disabled" or "derelict". continue; } $buildable->applyPlan($plan); } } public function applyPlan(HarbormasterBuildPlan $plan) { $viewer = PhabricatorUser::getOmnipotentUser(); $build = HarbormasterBuild::initializeNewBuild($viewer) ->setBuildablePHID($this->getPHID()) ->setBuildPlanPHID($plan->getPHID()) ->setBuildStatus(HarbormasterBuild::STATUS_PENDING) ->save(); PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildID' => $build->getID(), )); return $build; } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'containerPHID' => 'phid?', 'buildableStatus' => 'text32', 'isManualBuildable' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_container' => array( 'columns' => array('containerPHID'), ), 'key_manual' => array( 'columns' => array('isManualBuildable'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildablePHIDType::TYPECONST); } public function attachBuildableObject($buildable_object) { $this->buildableObject = $buildable_object; return $this; } public function getBuildableObject() { return $this->assertAttached($this->buildableObject); } public function attachContainerObject($container_object) { $this->containerObject = $container_object; return $this; } public function getContainerObject() { return $this->assertAttached($this->containerObject); } public function attachContainerHandle($container_handle) { $this->containerHandle = $container_handle; return $this; } public function getContainerHandle() { return $this->assertAttached($this->containerHandle); } public function attachBuildableHandle($buildable_handle) { $this->buildableHandle = $buildable_handle; return $this; } public function getBuildableHandle() { return $this->assertAttached($this->buildableHandle); } public function attachBuilds(array $builds) { assert_instances_of($builds, 'HarbormasterBuild'); $this->builds = $builds; return $this; } public function getBuilds() { return $this->assertAttached($this->builds); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildableTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildableTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildableObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildableObject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A buildable inherits policies from the underlying object.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { // NOTE: This is essentially just for convenience, as it allows you create // a copy of a buildable by specifying `B123` without bothering to go // look up the underlying object. return $this->getBuildablePHID(); } public function getHarbormasterContainerPHID() { return $this->getContainerPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } } diff --git a/src/applications/harbormaster/storage/HarbormasterObject.php b/src/applications/harbormaster/storage/HarbormasterObject.php index 648c04c3dc..c4c04f097b 100644 --- a/src/applications/harbormaster/storage/HarbormasterObject.php +++ b/src/applications/harbormaster/storage/HarbormasterObject.php @@ -1,21 +1,21 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255?', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_TOBJ); } } diff --git a/src/applications/harbormaster/storage/HarbormasterScratchTable.php b/src/applications/harbormaster/storage/HarbormasterScratchTable.php index 7da7378d71..53a118d828 100644 --- a/src/applications/harbormaster/storage/HarbormasterScratchTable.php +++ b/src/applications/harbormaster/storage/HarbormasterScratchTable.php @@ -1,28 +1,28 @@ array( 'data' => 'text64', 'bigData' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'data' => array( 'columns' => array('data'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 85c7d47b65..38216c5b9b 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,454 +1,454 @@ setBuildStatus(self::STATUS_INACTIVE) ->setBuildGeneration(0); } public function delete() { $this->openTransaction(); $this->deleteUnprocessedCommands(); $result = parent::delete(); $this->saveTransaction(); return $result; } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'buildStatus' => 'text32', 'buildGeneration' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_plan' => array( 'columns' => array('buildPlanPHID'), ), 'key_status' => array( 'columns' => array('buildStatus'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildPHIDType::TYPECONST); } public function attachBuildable(HarbormasterBuildable $buildable) { $this->buildable = $buildable; return $this; } public function getBuildable() { return $this->assertAttached($this->buildable); } public function getName() { if ($this->getBuildPlan()) { return $this->getBuildPlan()->getName(); } return pht('Build'); } public function attachBuildPlan( HarbormasterBuildPlan $build_plan = null) { $this->buildPlan = $build_plan; return $this; } public function getBuildPlan() { return $this->assertAttached($this->buildPlan); } public function getBuildTargets() { return $this->assertAttached($this->buildTargets); } public function attachBuildTargets(array $targets) { $this->buildTargets = $targets; return $this; } public function isBuilding() { return $this->getBuildStatus() === self::STATUS_PENDING || $this->getBuildStatus() === self::STATUS_BUILDING; } public function createLog( HarbormasterBuildTarget $build_target, $log_source, $log_type) { $log_source = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(250) ->truncateString($log_source); $log = HarbormasterBuildLog::initializeNewBuildLog($build_target) ->setLogSource($log_source) ->setLogType($log_type) ->save(); return $log; } public function createArtifact( HarbormasterBuildTarget $build_target, $artifact_key, $artifact_type) { $artifact = HarbormasterBuildArtifact::initializeNewBuildArtifact($build_target); $artifact->setArtifactKey( $this->getPHID(), $this->getBuildGeneration(), $artifact_key); $artifact->setArtifactType($artifact_type); $artifact->save(); return $artifact; } public function loadArtifact($name) { $artifact = id(new HarbormasterBuildArtifactQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withArtifactKeys( $this->getPHID(), $this->getBuildGeneration(), array($name)) ->executeOne(); if ($artifact === null) { throw new Exception('Artifact not found!'); } return $artifact; } public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, 'build.id' => null, ); $buildable = $this->getBuildable(); $object = $buildable->getBuildableObject(); $object_variables = $object->getBuildVariables(); $results = $object_variables + $results; $results['step.timestamp'] = time(); $results['build.id'] = $this->getID(); return $results; } public static function getAvailableBuildVariables() { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass('HarbormasterBuildableInterface') ->loadObjects(); $variables = array(); $variables[] = array( 'step.timestamp' => pht('The current UNIX timestamp.'), 'build.id' => pht('The ID of the current build.'), 'target.phid' => pht('The PHID of the current build target.'), ); foreach ($objects as $object) { $variables[] = $object->getAvailableBuildVariables(); } $variables = array_mergev($variables); return $variables; } public function isComplete() { switch ($this->getBuildStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: case self::STATUS_ERROR: case self::STATUS_STOPPED: return true; } return false; } public function isStopped() { return ($this->getBuildStatus() == self::STATUS_STOPPED); } /* -( Build Commands )----------------------------------------------------- */ private function getUnprocessedCommands() { return $this->assertAttached($this->unprocessedCommands); } public function attachUnprocessedCommands(array $commands) { $this->unprocessedCommands = $commands; return $this; } public function canRestartBuild() { return !$this->isRestarting(); } public function canStopBuild() { return !$this->isComplete() && !$this->isStopped() && !$this->isStopping(); } public function canResumeBuild() { return $this->isStopped() && !$this->isResuming(); } public function isStopping() { $is_stopping = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_STOP: $is_stopping = true; break; case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESTART: $is_stopping = false; break; } } return $is_stopping; } public function isResuming() { $is_resuming = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: case HarbormasterBuildCommand::COMMAND_RESUME: $is_resuming = true; break; case HarbormasterBuildCommand::COMMAND_STOP: $is_resuming = false; break; } } return $is_resuming; } public function isRestarting() { $is_restarting = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: $is_restarting = true; break; } } return $is_restarting; } public function deleteUnprocessedCommands() { foreach ($this->getUnprocessedCommands() as $key => $command_object) { $command_object->delete(); unset($this->unprocessedCommands[$key]); } return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildable()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildable()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A build inherits policies from its buildable.'); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php index 2a760083a4..e5bcb7c9ba 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php @@ -1,185 +1,185 @@ setBuildTargetPHID($build_target->getPHID()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'artifactData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'artifactType' => 'text32', 'artifactIndex' => 'bytes12', 'artifactKey' => 'text255', ), self::CONFIG_KEY_SCHEMA => array( 'key_artifact' => array( 'columns' => array('artifactType', 'artifactIndex'), 'unique' => true, ), 'key_garbagecollect' => array( 'columns' => array('artifactType', 'dateCreated'), ), ), ) + parent::getConfiguration(); } public function attachBuildTarget(HarbormasterBuildTarget $build_target) { $this->buildTarget = $build_target; return $this; } public function getBuildTarget() { return $this->assertAttached($this->buildTarget); } public function setArtifactKey($build_phid, $build_gen, $key) { $this->artifactIndex = PhabricatorHash::digestForIndex($build_phid.$build_gen.$key); $this->artifactKey = $key; return $this; } public function getObjectItemView(PhabricatorUser $viewer) { $data = $this->getArtifactData(); switch ($this->getArtifactType()) { case self::TYPE_FILE: $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($data) ->executeOne(); return id(new PHUIObjectItemView()) ->setObjectName(pht('File')) ->setHeader($handle->getFullName()) ->setHref($handle->getURI()); case self::TYPE_HOST: $leases = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withIDs(array($data['drydock-lease'])) ->execute(); $lease = $leases[$data['drydock-lease']]; return id(new PHUIObjectItemView()) ->setObjectName(pht('Drydock Lease')) ->setHeader($lease->getID()) ->setHref('/drydock/lease/'.$lease->getID()); case self::TYPE_URI: return id(new PHUIObjectItemView()) ->setObjectName($data['name']) ->setHeader($data['uri']) ->setHref($data['uri']); default: return null; } } public function loadDrydockLease() { if ($this->getArtifactType() !== self::TYPE_HOST) { throw new Exception( '`loadDrydockLease` may only be called on host artifacts.'); } $data = $this->getArtifactData(); // FIXME: Is there a better way of doing this? // TODO: Policy stuff, etc. $lease = id(new DrydockLease())->load( $data['drydock-lease']); if ($lease === null) { throw new Exception('Associated Drydock lease not found!'); } $resource = id(new DrydockResource())->load( $lease->getResourceID()); if ($resource === null) { throw new Exception('Associated Drydock resource not found!'); } $lease->attachResource($resource); return $lease; } public function loadPhabricatorFile() { if ($this->getArtifactType() !== self::TYPE_FILE) { throw new Exception( '`loadPhabricatorFile` may only be called on file artifacts.'); } $data = $this->getArtifactData(); // The data for TYPE_FILE is an array with a single PHID in it. $phid = $data['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($phid)) ->executeOne(); if ($file === null) { throw new Exception('Associated file not found!'); } return $file; } public function release() { switch ($this->getArtifactType()) { case self::TYPE_HOST: $this->releaseDrydockLease(); break; } } public function releaseDrydockLease() { $lease = $this->loadDrydockLease(); $resource = $lease->getResource(); $blueprint = $resource->getBlueprint(); if ($lease->isActive()) { $blueprint->releaseLease($resource, $lease); } } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuildTarget()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildTarget()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Users must be able to see a buildable to see its artifacts.'); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildItem.php b/src/applications/harbormaster/storage/build/HarbormasterBuildItem.php index cc4642ab63..0d3ce7fcad 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildItem.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildItem.php @@ -1,19 +1,19 @@ true, self::CONFIG_NO_TABLE => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildItemPHIDType::TYPECONST); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php index 6702a18fe7..d225de0f7f 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php @@ -1,208 +1,208 @@ setBuildTargetPHID($build_target->getPHID()) ->setDuration(null) ->setLive(0); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( // T6203/NULLABILITY // It seems like these should be non-nullable? All logs should have a // source, etc. 'logSource' => 'text255?', 'logType' => 'text255?', 'duration' => 'uint32?', 'live' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildtarget' => array( 'columns' => array('buildTargetPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildLogPHIDType::TYPECONST); } public function attachBuildTarget(HarbormasterBuildTarget $build_target) { $this->buildTarget = $build_target; return $this; } public function getBuildTarget() { return $this->assertAttached($this->buildTarget); } public function getName() { return pht('Build Log'); } public function start() { if ($this->getLive()) { throw new Exception('Live logging has already started for this log.'); } $this->setLive(1); $this->save(); return time(); } public function append($content) { if (!$this->getLive()) { throw new Exception('Start logging before appending data to the log.'); } if (strlen($content) === 0) { return; } // If the length of the content is greater than the chunk size limit, // then we can never fit the content in a single record. We need to // split our content out and call append on it for as many parts as there // are to the content. if (strlen($content) > self::CHUNK_BYTE_LIMIT) { $current = $content; while (strlen($current) > self::CHUNK_BYTE_LIMIT) { $part = substr($current, 0, self::CHUNK_BYTE_LIMIT); $current = substr($current, self::CHUNK_BYTE_LIMIT); $this->append($part); } $this->append($current); return; } // Retrieve the size of last chunk from the DB for this log. If the // chunk is over 500K, then we need to create a new log entry. $conn = $this->establishConnection('w'); $result = queryfx_all( $conn, 'SELECT id, size, encoding '. 'FROM harbormaster_buildlogchunk '. 'WHERE logID = %d '. 'ORDER BY id DESC '. 'LIMIT 1', $this->getID()); if (count($result) === 0 || $result[0]['size'] + strlen($content) > self::CHUNK_BYTE_LIMIT || $result[0]['encoding'] !== self::ENCODING_TEXT) { // We must insert a new chunk because the data we are appending // won't fit into the existing one, or we don't have any existing // chunk data. queryfx( $conn, 'INSERT INTO harbormaster_buildlogchunk '. '(logID, encoding, size, chunk) '. 'VALUES '. '(%d, %s, %d, %s)', $this->getID(), self::ENCODING_TEXT, strlen($content), $content); } else { // We have a resulting record that we can append our content onto. queryfx( $conn, 'UPDATE harbormaster_buildlogchunk '. 'SET chunk = CONCAT(chunk, %s), size = LENGTH(CONCAT(chunk, %s))'. 'WHERE id = %d', $content, $content, $result[0]['id']); } } public function finalize($start = 0) { if (!$this->getLive()) { throw new Exception('Start logging before finalizing it.'); } // TODO: Encode the log contents in a gzipped format. $this->reload(); if ($start > 0) { $this->setDuration(time() - $start); } $this->setLive(0); $this->save(); } public function getLogText() { // TODO: This won't cope very well if we're pulling like a 700MB // log file out of the DB. We should probably implement some sort // of optional limit parameter so that when we're rendering out only // 25 lines in the UI, we don't wastefully read in the whole log. // We have to read our content out of the database and stitch all of // the log data back together. $conn = $this->establishConnection('r'); $result = queryfx_all( $conn, 'SELECT chunk '. 'FROM harbormaster_buildlogchunk '. 'WHERE logID = %d '. 'ORDER BY id ASC', $this->getID()); $content = ''; foreach ($result as $row) { $content .= $row['chunk']; } return $content; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuildTarget()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildTarget()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Users must be able to see a build target to view it\'s build log.'); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php index c043d137e2..3236fc50fa 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php @@ -1,265 +1,265 @@ setName($build_step->getName()) ->setBuildPHID($build->getPHID()) ->setBuildStepPHID($build_step->getPHID()) ->setClassName($build_step->getClassName()) ->setDetails($build_step->getDetails()) ->setTargetStatus(self::STATUS_PENDING) ->setVariables($variables) ->setBuildGeneration($build->getBuildGeneration()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'variables' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'className' => 'text255', 'targetStatus' => 'text64', 'dateStarted' => 'epoch?', 'dateCompleted' => 'epoch?', 'buildGeneration' => 'uint32', // T6203/NULLABILITY // This should not be nullable. 'name' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_build' => array( 'columns' => array('buildPHID', 'buildStepPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildTargetPHIDType::TYPECONST); } public function attachBuild(HarbormasterBuild $build) { $this->build = $build; return $this; } public function getBuild() { return $this->assertAttached($this->build); } public function attachBuildStep(HarbormasterBuildStep $step = null) { $this->buildStep = $step; return $this; } public function getBuildStep() { return $this->assertAttached($this->buildStep); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getVariables() { return parent::getVariables() + $this->getBuildTargetVariables(); } public function getVariable($key, $default = null) { return idx($this->variables, $key, $default); } public function setVariable($key, $value) { $this->variables[$key] = $value; return $this; } public function getImplementation() { if ($this->implementation === null) { $obj = HarbormasterBuildStepImplementation::requireImplementation( $this->className); $obj->loadSettings($this); $this->implementation = $obj; } return $this->implementation; } public function getName() { if (strlen($this->name)) { return $this->name; } try { return $this->getImplementation()->getName(); } catch (Exception $e) { return $this->getClassName(); } } private function getBuildTargetVariables() { return array( 'target.phid' => $this->getPHID(), ); } /* -( Status )------------------------------------------------------------- */ public function isComplete() { switch ($this->getTargetStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: case self::STATUS_ABORTED: return true; } return false; } public function isFailed() { switch ($this->getTargetStatus()) { case self::STATUS_FAILED: return true; } return false; } public function isWaiting() { switch ($this->getTargetStatus()) { case self::STATUS_WAITING: return true; } return false; } public function isUnderway() { switch ($this->getTargetStatus()) { case self::STATUS_PENDING: case self::STATUS_BUILDING: return true; } return false; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuild()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuild()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Users must be able to see a build to view its build targets.'); } } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 594ad3c9d4..8434a64593 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -1,119 +1,119 @@ setPlanStatus(self::STATUS_ACTIVE); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'planStatus' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('planStatus'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildPlanPHIDType::TYPECONST); } public function attachBuildSteps(array $steps) { assert_instances_of($steps, 'HarbormasterBuildStep'); $this->buildSteps = $steps; return $this; } public function getBuildSteps() { return $this->assertAttached($this->buildSteps); } public function isDisabled() { return ($this->getPlanStatus() == self::STATUS_DISABLED); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildPlanEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildPlanTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php index 92b8e69ccf..8446fa542c 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php @@ -1,158 +1,158 @@ true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'className' => 'text255', 'sequence' => 'uint32', 'description' => 'text', // T6203/NULLABILITY // This should not be nullable. Current `null` values indicate steps // which predated editable names. These should be backfilled with // default names, then the code for handling `null` shoudl be removed. 'name' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_plan' => array( 'columns' => array('buildPlanPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildStepPHIDType::TYPECONST); } public function attachBuildPlan(HarbormasterBuildPlan $plan) { $this->buildPlan = $plan; return $this; } public function getBuildPlan() { return $this->assertAttached($this->buildPlan); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getName() { if (strlen($this->name)) { return $this->name; } return $this->getStepImplementation()->getName(); } public function getStepImplementation() { if ($this->implementation === null) { $obj = HarbormasterBuildStepImplementation::requireImplementation( $this->className); $obj->loadSettings($this); $this->implementation = $obj; } return $this->implementation; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildStepEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildStepTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuildPlan()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildPlan()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A build step has the same policies as its build plan.'); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'HarbormasterBuildStepCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index 6289ac8797..fb9f5197bb 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -1,349 +1,349 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'contentType' => 'text255', 'mustMatchAll' => 'bool', 'configVersion' => 'uint32', 'ruleType' => 'text32', 'isDisabled' => 'uint32', 'triggerObjectPHID' => 'phid?', // T6203/NULLABILITY // This should not be nullable. 'repetitionPolicy' => 'uint32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_author' => array( 'columns' => array('authorPHID'), ), 'key_ruletype' => array( 'columns' => array('ruleType'), ), 'key_trigger' => array( 'columns' => array('triggerObjectPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST); } public function getRuleApplied($phid) { return $this->assertAttachedKey($this->ruleApplied, $phid); } public function setRuleApplied($phid, $applied) { if ($this->ruleApplied === self::ATTACHABLE) { $this->ruleApplied = array(); } $this->ruleApplied[$phid] = $applied; return $this; } public function loadConditions() { if (!$this->getID()) { return array(); } return id(new HeraldCondition())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachConditions(array $conditions) { assert_instances_of($conditions, 'HeraldCondition'); $this->conditions = $conditions; return $this; } public function getConditions() { // TODO: validate conditions have been attached. return $this->conditions; } public function loadActions() { if (!$this->getID()) { return array(); } return id(new HeraldAction())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachActions(array $actions) { // TODO: validate actions have been attached. assert_instances_of($actions, 'HeraldAction'); $this->actions = $actions; return $this; } public function getActions() { return $this->actions; } public function loadEdits() { if (!$this->getID()) { return array(); } $edits = id(new HeraldRuleEdit())->loadAllWhere( 'ruleID = %d ORDER BY dateCreated DESC', $this->getID()); return $edits; } public function logEdit($editor_phid, $action) { id(new HeraldRuleEdit()) ->setRuleID($this->getID()) ->setRuleName($this->getName()) ->setEditorPHID($editor_phid) ->setAction($action) ->save(); } public function saveConditions(array $conditions) { assert_instances_of($conditions, 'HeraldCondition'); return $this->saveChildren( id(new HeraldCondition())->getTableName(), $conditions); } public function saveActions(array $actions) { assert_instances_of($actions, 'HeraldAction'); return $this->saveChildren( id(new HeraldAction())->getTableName(), $actions); } protected function saveChildren($table_name, array $children) { assert_instances_of($children, 'HeraldDAO'); if (!$this->getID()) { throw new Exception('Save rule before saving children.'); } foreach ($children as $child) { $child->setRuleID($this->getID()); } $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', $table_name, $this->getID()); foreach ($children as $child) { $child->save(); } $this->saveTransaction(); } public function delete() { $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldCondition())->getTableName(), $this->getID()); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldAction())->getTableName(), $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function hasValidAuthor() { return $this->assertAttached($this->validAuthor); } public function attachValidAuthor($valid) { $this->validAuthor = $valid; return $this; } public function getAuthor() { return $this->assertAttached($this->author); } public function attachAuthor(PhabricatorUser $user) { $this->author = $user; return $this; } public function isGlobalRule() { return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL); } public function isPersonalRule() { return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); } public function isObjectRule() { return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT); } public function attachTriggerObject($trigger_object) { $this->triggerObject = $trigger_object; return $this; } public function getTriggerObject() { return $this->assertAttached($this->triggerObject); } /** * Get a sortable key for rule execution order. * * Rules execute in a well-defined order: personal rules first, then object * rules, then global rules. Within each rule type, rules execute from lowest * ID to highest ID. * * This ordering allows more powerful rules (like global rules) to override * weaker rules (like personal rules) when multiple rules exist which try to * affect the same field. Executing from low IDs to high IDs makes * interactions easier to understand when adding new rules, because the newest * rules always happen last. * * @return string A sortable key for this rule. */ public function getRuleExecutionOrderSortKey() { $rule_type = $this->getRuleType(); switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: $type_order = 1; break; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: $type_order = 2; break; case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: $type_order = 3; break; default: throw new Exception(pht('Unknown rule type "%s"!', $rule_type)); } return sprintf('~%d%010d', $type_order, $this->getID()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HeraldRuleEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HeraldRuleTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($this->isGlobalRule()) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_USER; case PhabricatorPolicyCapability::CAN_EDIT: $app = 'PhabricatorHeraldApplication'; $herald = PhabricatorApplication::getByClass($app); $global = HeraldManageGlobalRulesCapability::CAPABILITY; return $herald->getPolicy($global); } } else if ($this->isObjectRule()) { return $this->getTriggerObject()->getPolicy($capability); } else { return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->isPersonalRule()) { return ($viewer->getPHID() == $this->getAuthorPHID()); } else { return false; } } public function describeAutomaticCapability($capability) { if ($this->isPersonalRule()) { return pht("A personal rule's owner can always view and edit it."); } else if ($this->isObjectRule()) { return pht('Object rules inherit the policies of their objects.'); } return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/herald/storage/HeraldRuleEdit.php b/src/applications/herald/storage/HeraldRuleEdit.php index 90ae39de08..735ae145ac 100644 --- a/src/applications/herald/storage/HeraldRuleEdit.php +++ b/src/applications/herald/storage/HeraldRuleEdit.php @@ -1,24 +1,24 @@ array( 'ruleName' => 'text255', 'action' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'ruleID' => array( 'columns' => array('ruleID', 'dateCreated'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index e518e3be5f..ad6214a8d1 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -1,250 +1,250 @@ 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() { + protected 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(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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 a4800f2192..1533f4165f 100644 --- a/src/applications/legalpad/storage/LegalpadDocumentBody.php +++ b/src/applications/legalpad/storage/LegalpadDocumentBody.php @@ -1,80 +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 e247073c6d..a811169cf7 100644 --- a/src/applications/legalpad/storage/LegalpadDocumentSignature.php +++ b/src/applications/legalpad/storage/LegalpadDocumentSignature.php @@ -1,100 +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 d8315ddaad..87756ed633 100644 --- a/src/applications/legalpad/storage/LegalpadTransactionComment.php +++ b/src/applications/legalpad/storage/LegalpadTransactionComment.php @@ -1,41 +1,41 @@ getTransactionPHID() != null); } - public function getConfiguration() { + protected 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 34ecfb26c1..0d68bf0c1b 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -1,143 +1,143 @@ 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() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', '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(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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/mailinglists/storage/PhabricatorMetaMTAMailingList.php b/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php index b80cf8644a..c93d19de2d 100644 --- a/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php +++ b/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php @@ -1,77 +1,77 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'email' => 'text128', 'uri' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'email' => array( 'columns' => array('email'), 'unique' => true, ), 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } /* -( 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; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/maniphest/storage/ManiphestNameIndex.php b/src/applications/maniphest/storage/ManiphestNameIndex.php index 62e4e8a524..ec160e3516 100644 --- a/src/applications/maniphest/storage/ManiphestNameIndex.php +++ b/src/applications/maniphest/storage/ManiphestNameIndex.php @@ -1,42 +1,42 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'indexedObjectName' => 'sort128', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => array( 'columns' => array('indexedObjectPHID'), 'unique' => true, ), 'key_name' => array( 'columns' => array('indexedObjectName'), ), ), ) + parent::getConfiguration(); } public static function updateIndex($phid, $name) { $table = new ManiphestNameIndex(); $conn_w = $table->establishConnection('w'); queryfx( $conn_w, 'INSERT INTO %T (indexedObjectPHID, indexedObjectName) VALUES (%s, %s) ON DUPLICATE KEY UPDATE indexedObjectName = VALUES(indexedObjectName)', $table->getTableName(), $phid, $name); } } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 8d19640191..3e133fe65e 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -1,392 +1,392 @@ setViewer($actor) ->withClasses(array('PhabricatorManiphestApplication')) ->executeOne(); $view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY); return id(new ManiphestTask()) ->setStatus(ManiphestTaskStatus::getDefaultStatus()) ->setPriority(ManiphestTaskPriority::getDefaultPriority()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->attachProjectPHIDs(array()) ->attachSubscriberPHIDs(array()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'ccPHIDs' => self::SERIALIZATION_JSON, 'attached' => self::SERIALIZATION_JSON, 'projectPHIDs' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', 'status' => 'text12', 'priority' => 'uint32', 'title' => 'sort', 'originalTitle' => 'text', 'description' => 'text', 'mailKey' => 'bytes20', 'ownerOrdering' => 'text64?', 'originalEmailSource' => 'text255?', 'subpriority' => 'double', // T6203/NULLABILITY // This should not be nullable. It's going away soon anyway. 'ccPHIDs' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'priority' => array( 'columns' => array('priority', 'status'), ), 'status' => array( 'columns' => array('status'), ), 'ownerPHID' => array( 'columns' => array('ownerPHID', 'status'), ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'ownerOrdering' => array( 'columns' => array('ownerOrdering'), ), 'priority_2' => array( 'columns' => array('priority', 'subpriority'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_dateModified' => array( 'columns' => array('dateModified'), ), 'key_title' => array( 'columns' => array('title(64)'), ), ), ) + parent::getConfiguration(); } public function loadDependsOnTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), ManiphestTaskDependsOnTaskEdgeType::EDGECONST); } public function loadDependedOnByTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST); } public function getSubscriberPHIDs() { return $this->assertAttached($this->subscriberPHIDs); } public function getProjectPHIDs() { return $this->assertAttached($this->edgeProjectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->edgeProjectPHIDs = $phids; return $this; } public function attachSubscriberPHIDs(array $phids) { $this->subscriberPHIDs = $phids; return $this; } public function setOwnerPHID($phid) { $this->ownerPHID = nonempty($phid, null); return $this; } public function setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } public function getMonogram() { return 'T'.$this->getID(); } public function attachGroupByProjectPHID($phid) { $this->groupByProjectPHID = $phid; return $this; } public function getGroupByProjectPHID() { return $this->assertAttached($this->groupByProjectPHID); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } $result = parent::save(); return $result; } public function isClosed() { return ManiphestTaskStatus::isClosedStatus($this->getStatus()); } public function getPrioritySortVector() { return array( $this->getPriority(), -$this->getSubpriority(), $this->getID(), ); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getOwnerPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "maniphest:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newManiphestMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // The owner of a task can always view and edit it. $owner_phid = $this->getOwnerPHID(); if ($owner_phid) { $user_phid = $user->getPHID(); if ($user_phid == $owner_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of a task can always view and edit it.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { // Sort of ambiguous who this was intended for; just let them both know. return array_filter( array_unique( array( $this->getAuthorPHID(), $this->getOwnerPHID(), ))); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('maniphest.fields'); } public function getCustomFieldBaseClass() { return 'ManiphestCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); // TODO: Once this implements PhabricatorTransactionInterface, this // will be handled automatically and can be removed. $xactions = id(new ManiphestTransaction())->loadAllWhere( 'objectPHID = %s', $this->getPHID()); foreach ($xactions as $xaction) { $engine->destroyObject($xaction); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ManiphestTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new ManiphestTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index bef8d7aec8..59d2d563c9 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -1,960 +1,960 @@ status = self::STATUS_QUEUE; $this->parameters = array(); parent::__construct(); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', 'relatedPHID' => 'phid?', // T6203/NULLABILITY // This should just be empty if there's no body. 'message' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'status' => array( 'columns' => array('status'), ), 'relatedPHID' => array( 'columns' => array('relatedPHID'), ), 'key_created' => array( 'columns' => array('dateCreated'), ), ), ) + parent::getConfiguration(); } protected function setParam($param, $value) { $this->parameters[$param] = $value; return $this; } protected function getParam($param, $default = null) { return idx($this->parameters, $param, $default); } /** * Set tags (@{class:MetaMTANotificationType} constants) which identify the * content of this mail in a general way. These tags are used to allow users * to opt out of receiving certain types of mail, like updates when a task's * projects change. * * @param list List of @{class:MetaMTANotificationType} constants. * @return this */ public function setMailTags(array $tags) { $this->setParam('mailtags', array_unique($tags)); return $this; } public function getMailTags() { return $this->getParam('mailtags', array()); } /** * In Gmail, conversations will be broken if you reply to a thread and the * server sends back a response without referencing your Message-ID, even if * it references a Message-ID earlier in the thread. To avoid this, use the * parent email's message ID explicitly if it's available. This overwrites the * "In-Reply-To" and "References" headers we would otherwise generate. This * needs to be set whenever an action is triggered by an email message. See * T251 for more details. * * @param string The "Message-ID" of the email which precedes this one. * @return this */ public function setParentMessageID($id) { $this->setParam('parent-message-id', $id); return $this; } public function getParentMessageID() { return $this->getParam('parent-message-id'); } public function getSubject() { return $this->getParam('subject'); } public function addTos(array $phids) { $phids = array_unique($phids); $this->setParam('to', $phids); return $this; } public function addRawTos(array $raw_email) { // Strip addresses down to bare emails, since the MailAdapter API currently // requires we pass it just the address (like `alincoln@logcabin.org`), not // a full string like `"Abraham Lincoln" `. foreach ($raw_email as $key => $email) { $object = new PhutilEmailAddress($email); $raw_email[$key] = $object->getAddress(); } $this->setParam('raw-to', $raw_email); return $this; } public function addCCs(array $phids) { $phids = array_unique($phids); $this->setParam('cc', $phids); return $this; } public function setExcludeMailRecipientPHIDs(array $exclude) { $this->setParam('exclude', $exclude); return $this; } private function getExcludeMailRecipientPHIDs() { return $this->getParam('exclude', array()); } public function getTranslation(array $objects) { $default_translation = PhabricatorEnv::getEnvConfig('translation.provider'); $return = null; $recipients = array_merge( idx($this->parameters, 'to', array()), idx($this->parameters, 'cc', array())); foreach (array_select_keys($objects, $recipients) as $object) { $translation = null; if ($object instanceof PhabricatorUser) { $translation = $object->getTranslation(); } if (!$translation) { $translation = $default_translation; } if ($return && $translation != $return) { return $default_translation; } $return = $translation; } if (!$return) { $return = $default_translation; } return $return; } public function addPHIDHeaders($name, array $phids) { foreach ($phids as $phid) { $this->addHeader($name, '<'.$phid.'>'); } return $this; } public function addHeader($name, $value) { $this->parameters['headers'][] = array($name, $value); return $this; } public function addAttachment(PhabricatorMetaMTAAttachment $attachment) { $this->parameters['attachments'][] = $attachment->toDictionary(); return $this; } public function getAttachments() { $dicts = $this->getParam('attachments'); $result = array(); foreach ($dicts as $dict) { $result[] = PhabricatorMetaMTAAttachment::newFromDictionary($dict); } return $result; } public function setAttachments(array $attachments) { assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment'); $this->setParam('attachments', mpull($attachments, 'toDictionary')); return $this; } public function setFrom($from) { $this->setParam('from', $from); return $this; } public function setReplyTo($reply_to) { $this->setParam('reply-to', $reply_to); return $this; } public function setSubject($subject) { $this->setParam('subject', $subject); return $this; } public function setSubjectPrefix($prefix) { $this->setParam('subject-prefix', $prefix); return $this; } public function setVarySubjectPrefix($prefix) { $this->setParam('vary-subject-prefix', $prefix); return $this; } public function setBody($body) { $this->setParam('body', $body); return $this; } public function setHTMLBody($html) { $this->setParam('html-body', $html); return $this; } public function getBody() { return $this->getParam('body'); } public function getHTMLBody() { return $this->getParam('html-body'); } public function setIsErrorEmail($is_error) { $this->setParam('is-error', $is_error); return $this; } public function getIsErrorEmail() { return $this->getParam('is-error', false); } public function getToPHIDs() { return $this->getParam('to', array()); } public function getRawToAddresses() { return $this->getParam('raw-to', array()); } public function getCcPHIDs() { return $this->getParam('cc', array()); } /** * Force delivery of a message, even if recipients have preferences which * would otherwise drop the message. * * This is primarily intended to let users who don't want any email still * receive things like password resets. * * @param bool True to force delivery despite user preferences. * @return this */ public function setForceDelivery($force) { $this->setParam('force', $force); return $this; } public function getForceDelivery() { return $this->getParam('force', false); } /** * Flag that this is an auto-generated bulk message and should have bulk * headers added to it if appropriate. Broadly, this means some flavor of * "Precedence: bulk" or similar, but is implementation and configuration * dependent. * * @param bool True if the mail is automated bulk mail. * @return this */ public function setIsBulk($is_bulk) { $this->setParam('is-bulk', $is_bulk); return $this; } /** * Use this method to set an ID used for message threading. MetaMTA will * set appropriate headers (Message-ID, In-Reply-To, References and * Thread-Index) based on the capabilities of the underlying mailer. * * @param string Unique identifier, appropriate for use in a Message-ID, * In-Reply-To or References headers. * @param bool If true, indicates this is the first message in the thread. * @return this */ public function setThreadID($thread_id, $is_first_message = false) { $this->setParam('thread-id', $thread_id); $this->setParam('is-first-message', $is_first_message); return $this; } /** * Save a newly created mail to the database. The mail will eventually be * delivered by the MetaMTA daemon. * * @return this */ public function saveAndSend() { return $this->save(); } public function save() { if ($this->getID()) { return parent::save(); } // NOTE: When mail is sent from CLI scripts that run tasks in-process, we // may re-enter this method from within scheduleTask(). The implementation // is intended to avoid anything awkward if we end up reentering this // method. $this->openTransaction(); // Save to generate a task ID. $result = parent::save(); // Queue a task to send this mail. $mailer_task = PhabricatorWorker::scheduleTask( 'PhabricatorMetaMTAWorker', $this->getID(), array( 'priority' => PhabricatorWorker::PRIORITY_ALERTS, )); $this->saveTransaction(); return $result; } public function buildDefaultMailer() { return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); } /** * Attempt to deliver an email immediately, in this process. * * @param bool Try to deliver this email even if it has already been * delivered or is in backoff after a failed delivery attempt. * @param PhabricatorMailImplementationAdapter Use a specific mail adapter, * instead of the default. * * @return void */ public function sendNow( $force_send = false, PhabricatorMailImplementationAdapter $mailer = null) { if ($mailer === null) { $mailer = $this->buildDefaultMailer(); } if (!$force_send) { if ($this->getStatus() != self::STATUS_QUEUE) { throw new Exception('Trying to send an already-sent mail!'); } } try { $params = $this->parameters; $actors = $this->loadAllActors(); $deliverable_actors = $this->filterDeliverableActors($actors); $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address'); if (empty($params['from'])) { $mailer->setFrom($default_from); } $is_first = idx($params, 'is-first-message'); unset($params['is-first-message']); $is_threaded = (bool)idx($params, 'thread-id'); $reply_to_name = idx($params, 'reply-to-name', ''); unset($params['reply-to-name']); $add_cc = array(); $add_to = array(); // Only try to use preferences if everything is multiplexed, so we // get consistent behavior. $use_prefs = self::shouldMultiplexAllMail(); $prefs = null; if ($use_prefs) { // If multiplexing is enabled, some recipients will be in "Cc" // rather than "To". We'll move them to "To" later (or supply a // dummy "To") but need to look for the recipient in either the // "To" or "Cc" fields here. $target_phid = head(idx($params, 'to', array())); if (!$target_phid) { $target_phid = head(idx($params, 'cc', array())); } if ($target_phid) { $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $target_phid); if ($user) { $prefs = $user->loadPreferences(); } } } foreach ($params as $key => $value) { switch ($key) { case 'from': $from = $value; $actor_email = null; $actor_name = null; $actor = idx($actors, $from); if ($actor) { $actor_email = $actor->getEmailAddress(); $actor_name = $actor->getName(); } $can_send_as_user = $actor_email && PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); if ($can_send_as_user) { $mailer->setFrom($actor_email, $actor_name); } else { $from_email = coalesce($actor_email, $default_from); $from_name = coalesce($actor_name, pht('Phabricator')); if (empty($params['reply-to'])) { $params['reply-to'] = $from_email; $params['reply-to-name'] = $from_name; } $mailer->setFrom($default_from, $from_name); } break; case 'reply-to': $mailer->addReplyTo($value, $reply_to_name); break; case 'to': $to_phids = $this->expandRecipients($value); $to_actors = array_select_keys($deliverable_actors, $to_phids); $add_to = array_merge( $add_to, mpull($to_actors, 'getEmailAddress')); break; case 'raw-to': $add_to = array_merge($add_to, $value); break; case 'cc': $cc_phids = $this->expandRecipients($value); $cc_actors = array_select_keys($deliverable_actors, $cc_phids); $add_cc = array_merge( $add_cc, mpull($cc_actors, 'getEmailAddress')); break; case 'headers': foreach ($value as $pair) { list($header_key, $header_value) = $pair; // NOTE: If we have \n in a header, SES rejects the email. $header_value = str_replace("\n", ' ', $header_value); $mailer->addHeader($header_key, $header_value); } break; case 'attachments': $value = $this->getAttachments(); foreach ($value as $attachment) { $mailer->addAttachment( $attachment->getData(), $attachment->getFilename(), $attachment->getMimeType()); } break; case 'subject': $subject = array(); if ($is_threaded) { $add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix'); if ($prefs) { $add_re = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_RE_PREFIX, $add_re); } if ($add_re) { $subject[] = 'Re:'; } } $subject[] = trim(idx($params, 'subject-prefix')); $vary_prefix = idx($params, 'vary-subject-prefix'); if ($vary_prefix != '') { $use_subject = PhabricatorEnv::getEnvConfig( 'metamta.vary-subjects'); if ($prefs) { $use_subject = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT, $use_subject); } if ($use_subject) { $subject[] = $vary_prefix; } } $subject[] = $value; $mailer->setSubject(implode(' ', array_filter($subject))); break; case 'is-bulk': if ($value) { if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) { $mailer->addHeader('Precedence', 'bulk'); } } break; case 'thread-id': // NOTE: Gmail freaks out about In-Reply-To and References which // aren't in the form ""; this is also required // by RFC 2822, although some clients are more liberal in what they // accept. $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); $value = '<'.$value.'@'.$domain.'>'; if ($is_first && $mailer->supportsMessageIDHeader()) { $mailer->addHeader('Message-ID', $value); } else { $in_reply_to = $value; $references = array($value); $parent_id = $this->getParentMessageID(); if ($parent_id) { $in_reply_to = $parent_id; // By RFC 2822, the most immediate parent should appear last // in the "References" header, so this order is intentional. $references[] = $parent_id; } $references = implode(' ', $references); $mailer->addHeader('In-Reply-To', $in_reply_to); $mailer->addHeader('References', $references); } $thread_index = $this->generateThreadIndex($value, $is_first); $mailer->addHeader('Thread-Index', $thread_index); break; case 'mailtags': // Handled below. break; case 'subject-prefix': case 'vary-subject-prefix': // Handled above. break; default: // Just discard. } } $body = idx($params, 'body', ''); $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); if (strlen($body) > $max) { $body = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes($max) ->truncateString($body); $body .= "\n"; $body .= pht('(This email was truncated at %d bytes.)', $max); } $mailer->setBody($body); $html_emails = false; if ($use_prefs && $prefs) { $html_emails = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS, $html_emails); } if ($html_emails && isset($params['html-body'])) { $mailer->setHTMLBody($params['html-body']); } if (!$add_to && !$add_cc) { $this->setStatus(self::STATUS_VOID); $this->setMessage( 'Message has no valid recipients: all To/Cc are disabled, invalid, '. 'or configured not to receive this mail.'); return $this->save(); } if ($this->getIsErrorEmail()) { $all_recipients = array_merge($add_to, $add_cc); if ($this->shouldRateLimitMail($all_recipients)) { $this->setStatus(self::STATUS_VOID); $this->setMessage( pht( 'This is an error email, but one or more recipients have '. 'exceeded the error email rate limit. Declining to deliver '. 'message.')); return $this->save(); } } $mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes'); $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA'); // Some clients respect this to suppress OOF and other auto-responses. $mailer->addHeader('X-Auto-Response-Suppress', 'All'); // If the message has mailtags, filter out any recipients who don't want // to receive this type of mail. $mailtags = $this->getParam('mailtags'); if ($mailtags) { $tag_header = array(); foreach ($mailtags as $mailtag) { $tag_header[] = '<'.$mailtag.'>'; } $tag_header = implode(', ', $tag_header); $mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header); } // Some mailers require a valid "To:" in order to deliver mail. If we // don't have any "To:", try to fill it in with a placeholder "To:". // If that also fails, move the "Cc:" line to "To:". if (!$add_to) { $placeholder_key = 'metamta.placeholder-to-recipient'; $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key); if ($placeholder !== null) { $add_to = array($placeholder); } else { $add_to = $add_cc; $add_cc = array(); } } $add_to = array_unique($add_to); $add_cc = array_diff(array_unique($add_cc), $add_to); $mailer->addTos($add_to); if ($add_cc) { $mailer->addCCs($add_cc); } } catch (Exception $ex) { $this ->setStatus(self::STATUS_FAIL) ->setMessage($ex->getMessage()) ->save(); throw $ex; } try { $ok = $mailer->send(); if (!$ok) { // TODO: At some point, we should clean this up and make all mailers // throw. throw new Exception( pht('Mail adapter encountered an unexpected, unspecified failure.')); } $this->setStatus(self::STATUS_SENT); $this->save(); return $this; } catch (PhabricatorMetaMTAPermanentFailureException $ex) { $this ->setStatus(self::STATUS_FAIL) ->setMessage($ex->getMessage()) ->save(); throw $ex; } catch (Exception $ex) { $this ->setMessage($ex->getMessage()."\n".$ex->getTraceAsString()) ->save(); throw $ex; } } public static function getReadableStatus($status_code) { static $readable = array( self::STATUS_QUEUE => 'Queued for Delivery', self::STATUS_FAIL => 'Delivery Failed', self::STATUS_SENT => 'Sent', self::STATUS_VOID => 'Void', ); $status_code = coalesce($status_code, '?'); return idx($readable, $status_code, $status_code); } private function generateThreadIndex($seed, $is_first_mail) { // When threading, Outlook ignores the 'References' and 'In-Reply-To' // headers that most clients use. Instead, it uses a custom 'Thread-Index' // header. The format of this header is something like this (from // camel-exchange-folder.c in Evolution Exchange): /* A new post to a folder gets a 27-byte-long thread index. (The value * is apparently unique but meaningless.) Each reply to a post gets a * 32-byte-long thread index whose first 27 bytes are the same as the * parent's thread index. Each reply to any of those gets a * 37-byte-long thread index, etc. The Thread-Index header contains a * base64 representation of this value. */ // The specific implementation uses a 27-byte header for the first email // a recipient receives, and a random 5-byte suffix (32 bytes total) // thereafter. This means that all the replies are (incorrectly) siblings, // but it would be very difficult to keep track of the entire tree and this // gets us reasonable client behavior. $base = substr(md5($seed), 0, 27); if (!$is_first_mail) { // Not totally sure, but it seems like outlook orders replies by // thread-index rather than timestamp, so to get these to show up in the // right order we use the time as the last 4 bytes. $base .= ' '.pack('N', time()); } return base64_encode($base); } public static function shouldMultiplexAllMail() { return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient'); } /* -( Managing Recipients )------------------------------------------------ */ /** * Get all of the recipients for this mail, after preference filters are * applied. This list has all objects to whom delivery will be attempted. * * @return list A list of all recipients to whom delivery will be * attempted. * @task recipients */ public function buildRecipientList() { $actors = $this->loadActors( array_merge( $this->getToPHIDs(), $this->getCcPHIDs())); $actors = $this->filterDeliverableActors($actors); return mpull($actors, 'getPHID'); } public function loadAllActors() { $actor_phids = array_merge( array($this->getParam('from')), $this->getToPHIDs(), $this->getCcPHIDs()); $this->loadRecipientExpansions($actor_phids); $actor_phids = $this->expandRecipients($actor_phids); return $this->loadActors($actor_phids); } private function loadRecipientExpansions(array $phids) { $expansions = id(new PhabricatorMetaMTAMemberQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($phids) ->execute(); $this->recipientExpansionMap = $expansions; return $this; } /** * Expand a list of recipient PHIDs (possibly including aggregate recipients * like projects) into a deaggregated list of individual recipient PHIDs. * For example, this will expand project PHIDs into a list of the project's * members. * * @param list List of recipient PHIDs, possibly including aggregate * recipients. * @return list Deaggregated list of mailable recipients. */ private function expandRecipients(array $phids) { if ($this->recipientExpansionMap === null) { throw new Exception( pht( 'Call loadRecipientExpansions() before expandRecipients()!')); } $results = array(); foreach ($phids as $phid) { if (!isset($this->recipientExpansionMap[$phid])) { $results[$phid] = $phid; } else { foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) { $results[$recipient_phid] = $recipient_phid; } } } return array_keys($results); } private function filterDeliverableActors(array $actors) { assert_instances_of($actors, 'PhabricatorMetaMTAActor'); $deliverable_actors = array(); foreach ($actors as $phid => $actor) { if ($actor->isDeliverable()) { $deliverable_actors[$phid] = $actor; } } return $deliverable_actors; } private function loadActors(array $actor_phids) { $actor_phids = array_filter($actor_phids); $viewer = PhabricatorUser::getOmnipotentUser(); $actors = id(new PhabricatorMetaMTAActorQuery()) ->setViewer($viewer) ->withPHIDs($actor_phids) ->execute(); if (!$actors) { return array(); } if ($this->getForceDelivery()) { // If we're forcing delivery, skip all the opt-out checks. return $actors; } // Exclude explicit recipients. foreach ($this->getExcludeMailRecipientPHIDs() as $phid) { $actor = idx($actors, $phid); if (!$actor) { continue; } $actor->setUndeliverable( pht( 'This message is a response to another email message, and this '. 'recipient received the original email message, so we are not '. 'sending them this substantially similar message (for example, '. 'the sender used "Reply All" instead of "Reply" in response to '. 'mail from Phabricator).')); } // Exclude the actor if their preferences are set. $from_phid = $this->getParam('from'); $from_actor = idx($actors, $from_phid); if ($from_actor) { $from_user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($from_phid)) ->execute(); $from_user = head($from_user); if ($from_user) { $pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL; $exclude_self = $from_user ->loadPreferences() ->getPreference($pref_key); if ($exclude_self) { $from_actor->setUndeliverable( pht( 'This recipient is the user whose actions caused delivery of '. 'this message, but they have set preferences so they do not '. 'receive mail about their own actions (Settings > Email '. 'Preferences > Self Actions).')); } } } $all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID in (%Ls)', $actor_phids); $all_prefs = mpull($all_prefs, null, 'getUserPHID'); // Exclude recipients who don't want any mail. foreach ($all_prefs as $phid => $prefs) { $exclude = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_NO_MAIL, false); if ($exclude) { $actors[$phid]->setUndeliverable( pht( 'This recipient has disabled all email notifications '. '(Settings > Email Preferences > Email Notifications).')); } } $value_email = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL; // Exclude all recipients who have set preferences to not receive this type // of email (for example, a user who says they don't want emails about task // CC changes). $tags = $this->getParam('mailtags'); if ($tags) { foreach ($all_prefs as $phid => $prefs) { $user_mailtags = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_MAILTAGS, array()); // The user must have elected to receive mail for at least one // of the mailtags. $send = false; foreach ($tags as $tag) { if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) { $send = true; break; } } if (!$send) { $actors[$phid]->setUndeliverable( pht( 'This mail has tags which control which users receive it, and '. 'this recipient has not elected to receive mail with any of '. 'the tags on this message (Settings > Email Preferences).')); } } } return $actors; } private function shouldRateLimitMail(array $all_recipients) { try { PhabricatorSystemActionEngine::willTakeAction( $all_recipients, new PhabricatorMetaMTAErrorMailAction(), 1); return false; } catch (PhabricatorSystemActionRateLimitException $ex) { return true; } } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index b3828fce24..c157537d59 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -1,360 +1,360 @@ array( 'headers' => self::SERIALIZATION_JSON, 'bodies' => self::SERIALIZATION_JSON, 'attachments' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'relatedPHID' => 'phid?', 'authorPHID' => 'phid?', 'message' => 'text?', 'messageIDHash' => 'bytes12', 'status' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'relatedPHID' => array( 'columns' => array('relatedPHID'), ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'key_messageIDHash' => array( 'columns' => array('messageIDHash'), ), 'key_created' => array( 'columns' => array('dateCreated'), ), ), ) + parent::getConfiguration(); } public function setHeaders(array $headers) { // Normalize headers to lowercase. $normalized = array(); foreach ($headers as $name => $value) { $name = $this->normalizeMailHeaderName($name); if ($name == 'message-id') { $this->setMessageIDHash(PhabricatorHash::digestForIndex($value)); } $normalized[$name] = $value; } $this->headers = $normalized; return $this; } public function getHeader($key, $default = null) { $key = $this->normalizeMailHeaderName($key); return idx($this->headers, $key, $default); } private function normalizeMailHeaderName($name) { return strtolower($name); } public function getMessageID() { return $this->getHeader('Message-ID'); } public function getSubject() { return $this->getHeader('Subject'); } public function getCCAddresses() { return $this->getRawEmailAddresses(idx($this->headers, 'cc')); } public function getToAddresses() { return $this->getRawEmailAddresses(idx($this->headers, 'to')); } public function loadExcludeMailRecipientPHIDs() { $addresses = array_merge( $this->getToAddresses(), $this->getCCAddresses()); return $this->loadPHIDsFromAddresses($addresses); } final public function loadCCPHIDs() { return $this->loadPHIDsFromAddresses($this->getCCAddresses()); } private function loadPHIDsFromAddresses(array $addresses) { if (empty($addresses)) { return array(); } $users = id(new PhabricatorUserEmail()) ->loadAllWhere('address IN (%Ls)', $addresses); $user_phids = mpull($users, 'getUserPHID'); $mailing_lists = id(new PhabricatorMetaMTAMailingList()) ->loadAllWhere('email in (%Ls)', $addresses); $mailing_list_phids = mpull($mailing_lists, 'getPHID'); return array_merge($user_phids, $mailing_list_phids); } public function processReceivedMail() { try { $this->dropMailFromPhabricator(); $this->dropMailAlreadyReceived(); $receiver = $this->loadReceiver(); $sender = $receiver->loadSender($this); $receiver->validateSender($this, $sender); $this->setAuthorPHID($sender->getPHID()); $receiver->receiveMail($this, $sender); } catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) { switch ($ex->getStatusCode()) { case MetaMTAReceivedMailStatus::STATUS_DUPLICATE: case MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR: // Don't send an error email back in these cases, since they're // very unlikely to be the sender's fault. break; case MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED: // This error is explicitly ignored. break; default: $this->sendExceptionMail($ex); break; } $this ->setStatus($ex->getStatusCode()) ->setMessage($ex->getMessage()) ->save(); return $this; } catch (Exception $ex) { $this->sendExceptionMail($ex); $this ->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION) ->setMessage(pht('Unhandled Exception: %s', $ex->getMessage())) ->save(); throw $ex; } return $this->setMessage('OK')->save(); } public function getCleanTextBody() { $body = $this->getRawTextBody(); $parser = new PhabricatorMetaMTAEmailBodyParser(); return $parser->stripTextBody($body); } public function parseBody() { $body = $this->getRawTextBody(); $parser = new PhabricatorMetaMTAEmailBodyParser(); return $parser->parseBody($body); } public function getRawTextBody() { return idx($this->bodies, 'text'); } /** * Strip an email address down to the actual user@domain.tld part if * necessary, since sometimes it will have formatting like * '"Abraham Lincoln" '. */ private function getRawEmailAddress($address) { $matches = null; $ok = preg_match('/<(.*)>/', $address, $matches); if ($ok) { $address = $matches[1]; } return $address; } private function getRawEmailAddresses($addresses) { $raw_addresses = array(); foreach (explode(',', $addresses) as $address) { $raw_addresses[] = $this->getRawEmailAddress($address); } return array_filter($raw_addresses); } /** * If Phabricator sent the mail, always drop it immediately. This prevents * loops where, e.g., the public bug address is also a user email address * and creating a bug sends them an email, which loops. */ private function dropMailFromPhabricator() { if (!$this->getHeader('x-phabricator-sent-this-message')) { return; } throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR, pht( "Ignoring email with 'X-Phabricator-Sent-This-Message' header to ". "avoid loops.")); } /** * If this mail has the same message ID as some other mail, and isn't the * first mail we we received with that message ID, we drop it as a duplicate. */ private function dropMailAlreadyReceived() { $message_id_hash = $this->getMessageIDHash(); if (!$message_id_hash) { // No message ID hash, so we can't detect duplicates. This should only // happen with very old messages. return; } $messages = $this->loadAllWhere( 'messageIDHash = %s ORDER BY id ASC LIMIT 2', $message_id_hash); $messages_count = count($messages); if ($messages_count <= 1) { // If we only have one copy of this message, we're good to process it. return; } $first_message = reset($messages); if ($first_message->getID() == $this->getID()) { // If this is the first copy of the message, it is okay to process it. // We may not have been able to to process it immediately when we received // it, and could may have received several copies without processing any // yet. return; } $message = pht( 'Ignoring email with "Message-ID" hash "%s" that has been seen %d '. 'times, including this message.', $message_id_hash, $messages_count); throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_DUPLICATE, $message); } /** * Load a concrete instance of the @{class:PhabricatorMailReceiver} which * accepts this mail, if one exists. */ private function loadReceiver() { $receivers = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorMailReceiver') ->loadObjects(); $accept = array(); foreach ($receivers as $key => $receiver) { if (!$receiver->isEnabled()) { continue; } if ($receiver->canAcceptMail($this)) { $accept[$key] = $receiver; } } if (!$accept) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS, pht( 'Phabricator can not process this mail because no application '. 'knows how to handle it. Check that the address you sent it to is '. 'correct.'. "\n\n". '(No concrete, enabled subclass of PhabricatorMailReceiver can '. 'accept this mail.)')); } if (count($accept) > 1) { $names = implode(', ', array_keys($accept)); throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS, pht( 'Phabricator is not able to process this mail because more than '. 'one application is willing to accept it, creating ambiguity. '. 'Mail needs to be accepted by exactly one receiving application.'. "\n\n". 'Accepting receivers: %s.', $names)); } return head($accept); } private function sendExceptionMail(Exception $ex) { $from = $this->getHeader('from'); if (!strlen($from)) { return; } if ($ex instanceof PhabricatorMetaMTAReceivedMailProcessingException) { $status_code = $ex->getStatusCode(); $status_name = MetaMTAReceivedMailStatus::getHumanReadableName( $status_code); $title = pht('Error Processing Mail (%s)', $status_name); $description = $ex->getMessage(); } else { $title = pht('Error Processing Mail (%s)', get_class($ex)); $description = pht('%s: %s', get_class($ex), $ex->getMessage()); } // TODO: Since headers don't necessarily have unique names, this may not // really be all the headers. It would be nice to pass the raw headers // through from the upper layers where possible. $headers = array(); foreach ($this->headers as $key => $value) { $headers[] = pht('%s: %s', $key, $value); } $headers = implode("\n", $headers); $body = pht(<<getRawTextBody(), $headers); $mail = id(new PhabricatorMetaMTAMail()) ->setIsErrorEmail(true) ->setForceDelivery(true) ->setSubject($title) ->addRawTos(array($from)) ->setBody($body) ->saveAndSend(); } } diff --git a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php index 6018d02f8d..b4b139fd16 100644 --- a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php +++ b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php @@ -1,69 +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/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index f27151e786..2db403cd2e 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -1,137 +1,137 @@ setDateNuanced(time()) ->setStatus(NuanceItem::STATUS_OPEN); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'data' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', 'sourceLabel' => 'text255?', 'status' => 'uint32', 'mailKey' => 'bytes20', 'dateNuanced' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_source' => array( 'columns' => array('sourcePHID', 'status', 'dateNuanced', 'id'), ), 'key_owner' => array( 'columns' => array('ownerPHID', 'status', 'dateNuanced', 'id'), ), 'key_contacter' => array( 'columns' => array('requestorPHID', 'status', 'dateNuanced', 'id'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( NuanceItemPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getURI() { return '/nuance/item/view/'.$this->getID().'/'; } public function getLabel(PhabricatorUser $viewer) { // this is generated at the time the item is created based on // the configuration from the item source. It is typically // something like 'Twitter'. $source_label = $this->getSourceLabel(); return pht( 'Item via %s @ %s.', $source_label, phabricator_datetime($this->getDateCreated(), $viewer)); } public function getRequestor() { return $this->assertAttached($this->requestor); } public function attachRequestor(NuanceRequestor $requestor) { return $this->requestor = $requestor; } public function getSource() { return $this->assertAttached($this->source); } public function attachSource(NuanceSource $source) { $this->source = $source; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // TODO - this should be based on the queues the item currently resides in return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // TODO - requestors should get auto access too! return $viewer->getPHID() == $this->ownerPHID; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Owners of an item can always view it.'); case PhabricatorPolicyCapability::CAN_EDIT: return pht('Owners of an item can always edit it.'); } return null; } public function toDictionary() { return array( 'id' => $this->getID(), 'phid' => $this->getPHID(), 'ownerPHID' => $this->getOwnerPHID(), 'requestorPHID' => $this->getRequestorPHID(), 'sourcePHID' => $this->getSourcePHID(), 'sourceLabel' => $this->getSourceLabel(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'dateNuanced' => $this->getDateNuanced(), ); } } diff --git a/src/applications/nuance/storage/NuanceQueue.php b/src/applications/nuance/storage/NuanceQueue.php index 4a172a9401..e9499e0f3d 100644 --- a/src/applications/nuance/storage/NuanceQueue.php +++ b/src/applications/nuance/storage/NuanceQueue.php @@ -1,62 +1,62 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255?', 'mailKey' => 'bytes20', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( NuanceQueuePHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getURI() { return '/nuance/queue/view/'.$this->getID().'/'; } 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 false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/nuance/storage/NuanceQueueItem.php b/src/applications/nuance/storage/NuanceQueueItem.php index 027ed99e69..77a80ff181 100644 --- a/src/applications/nuance/storage/NuanceQueueItem.php +++ b/src/applications/nuance/storage/NuanceQueueItem.php @@ -1,34 +1,34 @@ array( 'itemStatus' => 'uint32', 'itemDateNuanced' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_one_per_queue' => array( 'columns' => array('itemPHID', 'queuePHID'), 'unique' => true, ), 'key_queue' => array( 'columns' => array( 'queuePHID', 'itemStatus', 'itemDateNuanced', 'id', ), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/nuance/storage/NuanceRequestor.php b/src/applications/nuance/storage/NuanceRequestor.php index 6ffd705f82..3305da3988 100644 --- a/src/applications/nuance/storage/NuanceRequestor.php +++ b/src/applications/nuance/storage/NuanceRequestor.php @@ -1,37 +1,37 @@ true, self::CONFIG_SERIALIZATION => array( 'data' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( NuanceRequestorPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getURI() { return '/nuance/requestor/view/'.$this->getID().'/'; } public function getPhabricatorUserPHID() { return idx($this->getData(), 'phabricatorUserPHID'); } } diff --git a/src/applications/nuance/storage/NuanceRequestorSource.php b/src/applications/nuance/storage/NuanceRequestorSource.php index 6c607a643c..bc31fdb6a1 100644 --- a/src/applications/nuance/storage/NuanceRequestorSource.php +++ b/src/applications/nuance/storage/NuanceRequestorSource.php @@ -1,34 +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/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php index ecf2f7bba8..423f70fd76 100644 --- a/src/applications/nuance/storage/NuanceSource.php +++ b/src/applications/nuance/storage/NuanceSource.php @@ -1,117 +1,117 @@ true, self::CONFIG_SERIALIZATION => array( 'data' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255?', 'type' => 'text32', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_type' => array( 'columns' => array('type', 'dateModified'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(NuanceSourcePHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getURI() { return '/nuance/source/view/'.$this->getID().'/'; } public static function initializeNewSource(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorNuanceApplication')) ->executeOne(); $view_policy = $app->getPolicy( NuanceSourceDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( NuanceSourceDefaultEditCapability::CAPABILITY); $definitions = NuanceSourceDefinition::getAllDefinitions(); $lucky_definition = head($definitions); return id(new NuanceSource()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setType($lucky_definition->getSourceTypeConstant()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new NuanceSourceEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new NuanceSourceTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } 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 false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php b/src/applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php index 505df7b2d7..dbb25ed915 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php @@ -1,81 +1,81 @@ getScope(); $scopes = array_keys($scope); sort($scopes); return implode(' ', $scopes); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'scope' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'scope' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'userPHID' => array( 'columns' => array('userPHID', 'clientPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorOAuthServerClientAuthorizationPHIDType::TYPECONST); } public function getClient() { return $this->assertAttached($this->client); } public function attachClient(PhabricatorOAuthServerClient $client) { $this->client = $client; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getUserPHID()); } public function describeAutomaticCapability($capability) { return pht('Authorizations can only be viewed by the authorizing user.'); } } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php index 1deadaa92b..68b21d23cb 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php @@ -1,25 +1,25 @@ array( 'token' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'token' => array( 'columns' => array('token'), 'unique' => true, ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php index 32212d987d..5ac2ff4b58 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php @@ -1,29 +1,29 @@ array( 'code' => 'text32', 'clientSecret' => 'text32', 'redirectURI' => 'text255', ), self::CONFIG_KEY_SCHEMA => array( 'code' => array( 'columns' => array('code'), 'unique' => true, ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php index 1316e5f017..0d4598385c 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php @@ -1,92 +1,92 @@ getPHID().'/'; } public function getViewURI() { return '/oauthserver/client/view/'.$this->getPHID().'/'; } public function getDeleteURI() { return '/oauthserver/client/delete/'.$this->getPHID().'/'; } public static function initializeNewClient(PhabricatorUser $actor) { return id(new PhabricatorOAuthServerClient()) ->setCreatorPHID($actor->getPHID()) ->setSecret(Filesystem::readRandomCharacters(32)); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'secret' => 'text32', 'redirectURI' => 'text255', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'creatorPHID' => array( 'columns' => array('creatorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorOAuthServerClientPHIDType::TYPECONST); } /* -( 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_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return ($viewer->getPHID() == $this->getCreatorPHID()); } return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return pht("Only an application's creator can edit it."); } return null; } } diff --git a/src/applications/owners/storage/PhabricatorOwnersOwner.php b/src/applications/owners/storage/PhabricatorOwnersOwner.php index b0d11bf1e7..c3d1aaa605 100644 --- a/src/applications/owners/storage/PhabricatorOwnersOwner.php +++ b/src/applications/owners/storage/PhabricatorOwnersOwner.php @@ -1,69 +1,69 @@ false, self::CONFIG_KEY_SCHEMA => array( 'packageID' => array( 'columns' => array('packageID', 'userPHID'), 'unique' => true, ), 'userPHID' => array( 'columns' => array('userPHID'), ), ), ) + parent::getConfiguration(); } public static function loadAllForPackages(array $packages) { assert_instances_of($packages, 'PhabricatorOwnersPackage'); if (!$packages) { return array(); } return id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID IN (%Ls)', mpull($packages, 'getID')); } // Loads all user phids affiliated with a set of packages. This includes both // user owners and all members of any project owners public static function loadAffiliatedUserPHIDs(array $package_ids) { if (!$package_ids) { return array(); } $owners = id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID IN (%Ls)', $package_ids); $all_phids = phid_group_by_type(mpull($owners, 'getUserPHID')); $user_phids = idx($all_phids, PhabricatorPeopleUserPHIDType::TYPECONST, array()); $users_in_project_phids = array(); $project_phids = idx( $all_phids, PhabricatorProjectProjectPHIDType::TYPECONST); if ($project_phids) { $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($project_phids) ->withEdgeTypes(array( PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, )); $query->execute(); $users_in_project_phids = $query->getDestinationPHIDs(); } return array_unique(array_merge($users_in_project_phids, $user_phids)); } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index fa667d2dae..2bd28da94f 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -1,440 +1,440 @@ false, self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'originalName' => 'text255', 'description' => 'text', 'primaryOwnerPHID' => 'phid?', 'auditingEnabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('OPKG'); } public function attachUnsavedOwners(array $owners) { $this->unsavedOwners = $owners; return $this; } public function attachUnsavedPaths(array $paths) { $this->unsavedPaths = $paths; return $this; } public function attachActorPHID($actor_phid) { $this->actorPHID = $actor_phid; return $this; } public function getActorPHID() { return $this->actorPHID; } public function attachOldPrimaryOwnerPHID($old_primary) { $this->oldPrimaryOwnerPHID = $old_primary; return $this; } public function getOldPrimaryOwnerPHID() { return $this->oldPrimaryOwnerPHID; } public function attachOldAuditingEnabled($auditing_enabled) { $this->oldAuditingEnabled = $auditing_enabled; return $this; } public function getOldAuditingEnabled() { return $this->oldAuditingEnabled; } public function setName($name) { $this->name = $name; if (!$this->getID()) { $this->originalName = $name; } return $this; } public function loadOwners() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID = %d', $this->getID()); } public function loadPaths() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID = %d', $this->getID()); } public static function loadAffectedPackages( PhabricatorRepository $repository, array $paths) { if (!$paths) { return array(); } return self::loadPackagesForPaths($repository, $paths); } public static function loadOwningPackages($repository, $path) { if (empty($path)) { return array(); } return self::loadPackagesForPaths($repository, array($path), 1); } private static function loadPackagesForPaths( PhabricatorRepository $repository, array $paths, $limit = 0) { $fragments = array(); foreach ($paths as $path) { foreach (self::splitPath($path) as $fragment) { $fragments[$fragment][$path] = true; } } $package = new PhabricatorOwnersPackage(); $path = new PhabricatorOwnersPath(); $conn = $package->establishConnection('r'); $repository_clause = qsprintf( $conn, 'AND p.repositoryPHID = %s', $repository->getPHID()); // NOTE: The list of $paths may be very large if we're coming from // the OwnersWorker and processing, e.g., an SVN commit which created a new // branch. Break it apart so that it will fit within 'max_allowed_packet', // and then merge results in PHP. $rows = array(); foreach (array_chunk(array_keys($fragments), 128) as $chunk) { $rows[] = queryfx_all( $conn, 'SELECT pkg.id, p.excluded, p.path FROM %T pkg JOIN %T p ON p.packageID = pkg.id WHERE p.path IN (%Ls) %Q', $package->getTableName(), $path->getTableName(), $chunk, $repository_clause); } $rows = array_mergev($rows); $ids = self::findLongestPathsPerPackage($rows, $fragments); if (!$ids) { return array(); } arsort($ids); if ($limit) { $ids = array_slice($ids, 0, $limit, $preserve_keys = true); } $ids = array_keys($ids); $packages = $package->loadAllWhere('id in (%Ld)', $ids); $packages = array_select_keys($packages, $ids); return $packages; } public static function loadPackagesForRepository($repository) { $package = new PhabricatorOwnersPackage(); $ids = ipull( queryfx_all( $package->establishConnection('r'), 'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s', id(new PhabricatorOwnersPath())->getTableName(), $repository->getPHID()), 'packageID'); return $package->loadAllWhere('id in (%Ld)', $ids); } public static function findLongestPathsPerPackage(array $rows, array $paths) { $ids = array(); foreach (igroup($rows, 'id') as $id => $package_paths) { $relevant_paths = array_select_keys( $paths, ipull($package_paths, 'path')); // For every package, remove all excluded paths. $remove = array(); foreach ($package_paths as $package_path) { if ($package_path['excluded']) { $remove += idx($relevant_paths, $package_path['path'], array()); unset($relevant_paths[$package_path['path']]); } } if ($remove) { foreach ($relevant_paths as $fragment => $fragment_paths) { $relevant_paths[$fragment] = array_diff_key($fragment_paths, $remove); } } $relevant_paths = array_filter($relevant_paths); if ($relevant_paths) { $ids[$id] = max(array_map('strlen', array_keys($relevant_paths))); } } return $ids; } private function getActor() { // TODO: This should be cleaner, but we'd likely need to move the whole // thing to an Editor (T603). return PhabricatorUser::getOmnipotentUser(); } public function save() { if ($this->getID()) { $is_new = false; } else { $is_new = true; } $this->openTransaction(); $ret = parent::save(); $add_owners = array(); $remove_owners = array(); $all_owners = array(); if ($this->unsavedOwners) { $new_owners = array_fill_keys($this->unsavedOwners, true); $cur_owners = array(); foreach ($this->loadOwners() as $owner) { if (empty($new_owners[$owner->getUserPHID()])) { $remove_owners[$owner->getUserPHID()] = true; $owner->delete(); continue; } $cur_owners[$owner->getUserPHID()] = true; } $add_owners = array_diff_key($new_owners, $cur_owners); $all_owners = array_merge( array($this->getPrimaryOwnerPHID() => true), $new_owners, $remove_owners); foreach ($add_owners as $phid => $ignored) { $owner = new PhabricatorOwnersOwner(); $owner->setPackageID($this->getID()); $owner->setUserPHID($phid); $owner->save(); } unset($this->unsavedOwners); } $add_paths = array(); $remove_paths = array(); $touched_repos = array(); if ($this->unsavedPaths) { $new_paths = igroup($this->unsavedPaths, 'repositoryPHID', 'path'); $cur_paths = $this->loadPaths(); foreach ($cur_paths as $key => $path) { $repository_phid = $path->getRepositoryPHID(); $new_path = head(idx( idx($new_paths, $repository_phid, array()), $path->getPath(), array())); $excluded = $path->getExcluded(); if (!$new_path || idx($new_path, 'excluded') != $excluded) { $touched_repos[$repository_phid] = true; $remove_paths[$repository_phid][$path->getPath()] = $excluded; $path->delete(); unset($cur_paths[$key]); } } $cur_paths = mgroup($cur_paths, 'getRepositoryPHID', 'getPath'); foreach ($new_paths as $repository_phid => $paths) { // TODO: (T603) Thread policy stuff in here. // get repository object for path validation $repository = id(new PhabricatorRepository())->loadOneWhere( 'phid = %s', $repository_phid); if (!$repository) { continue; } foreach ($paths as $path => $dicts) { $path = ltrim($path, '/'); // build query to validate path $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $this->getActor(), 'repository' => $repository, 'path' => $path, )); $results = DiffusionBrowseResultSet::newFromConduit( DiffusionQuery::callConduitWithDiffusionRequest( $this->getActor(), $drequest, 'diffusion.browsequery', array( 'commit' => $drequest->getCommit(), 'path' => $path, 'needValidityOnly' => true, ))); $valid = $results->isValidResults(); $is_directory = true; if (!$valid) { switch ($results->getReasonForEmptyResultSet()) { case DiffusionBrowseResultSet::REASON_IS_FILE: $valid = true; $is_directory = false; break; case DiffusionBrowseResultSet::REASON_IS_EMPTY: $valid = true; break; } } if ($is_directory && substr($path, -1) != '/') { $path .= '/'; } if (substr($path, 0, 1) != '/') { $path = '/'.$path; } if (empty($cur_paths[$repository_phid][$path]) && $valid) { $touched_repos[$repository_phid] = true; $excluded = idx(reset($dicts), 'excluded', 0); $add_paths[$repository_phid][$path] = $excluded; $obj = new PhabricatorOwnersPath(); $obj->setPackageID($this->getID()); $obj->setRepositoryPHID($repository_phid); $obj->setPath($path); $obj->setExcluded($excluded); $obj->save(); } } } unset($this->unsavedPaths); } $this->saveTransaction(); if ($is_new) { $mail = new PackageCreateMail($this); } else { $mail = new PackageModifyMail( $this, array_keys($add_owners), array_keys($remove_owners), array_keys($all_owners), array_keys($touched_repos), $add_paths, $remove_paths); } $mail->setActor($this->getActor()); $mail->send(); return $ret; } public function delete() { $mails = id(new PackageDeleteMail($this)) ->setActor($this->getActor()) ->prepareMails(); $this->openTransaction(); foreach ($this->loadOwners() as $owner) { $owner->delete(); } foreach ($this->loadPaths() as $path) { $path->delete(); } $ret = parent::delete(); $this->saveTransaction(); foreach ($mails as $mail) { $mail->saveAndSend(); } return $ret; } private static function splitPath($path) { $result = array('/'); $trailing_slash = preg_match('@/$@', $path) ? '/' : ''; $path = trim($path, '/'); $parts = explode('/', $path); while (count($parts)) { $result[] = '/'.implode('/', $parts).$trailing_slash; $trailing_slash = '/'; array_pop($parts); } return $result; } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPath.php b/src/applications/owners/storage/PhabricatorOwnersPath.php index 9fcdc5d704..0cc987e6bb 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPath.php +++ b/src/applications/owners/storage/PhabricatorOwnersPath.php @@ -1,25 +1,25 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'path' => 'text255', 'excluded' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'packageID' => array( 'columns' => array('packageID'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php index 976a379f1a..b867323f53 100644 --- a/src/applications/passphrase/storage/PassphraseCredential.php +++ b/src/applications/passphrase/storage/PassphraseCredential.php @@ -1,151 +1,151 @@ setName('') ->setUsername('') ->setDescription('') ->setIsDestroyed(0) ->setViewPolicy($actor->getPHID()) ->setEditPolicy($actor->getPHID()); } public function getMonogram() { return 'K'.$this->getID(); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'credentialType' => 'text64', 'providesType' => 'text64', 'description' => 'text', 'username' => 'text255', 'secretID' => 'id?', 'isDestroyed' => 'bool', 'isLocked' => 'bool', 'allowConduit' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_secret' => array( 'columns' => array('secretID'), 'unique' => true, ), 'key_type' => array( 'columns' => array('credentialType'), ), 'key_provides' => array( 'columns' => array('providesType'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PassphraseCredentialPHIDType::TYPECONST); } public function attachSecret(PhutilOpaqueEnvelope $secret = null) { $this->secret = $secret; return $this; } public function getSecret() { return $this->assertAttached($this->secret); } public function getCredentialTypeImplementation() { $type = $this->getCredentialType(); return PassphraseCredentialType::getTypeByConstant($type); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PassphraseCredentialTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PassphraseCredentialTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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 false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $secrets = id(new PassphraseSecret())->loadAllWhere( 'id = %d', $this->getSecretID()); foreach ($secrets as $secret) { $secret->delete(); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/passphrase/storage/PassphraseSecret.php b/src/applications/passphrase/storage/PassphraseSecret.php index c600ef571d..518cb8eed0 100644 --- a/src/applications/passphrase/storage/PassphraseSecret.php +++ b/src/applications/passphrase/storage/PassphraseSecret.php @@ -1,16 +1,16 @@ false, self::CONFIG_BINARY => array( 'secretData' => true, ), ) + parent::getConfiguration(); } } diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index 56f86a19dc..9eb88a77b4 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -1,209 +1,209 @@ setViewer($actor) ->withClasses(array('PhabricatorPasteApplication')) ->executeOne(); $view_policy = $app->getPolicy(PasteDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PasteDefaultEditCapability::CAPABILITY); return id(new PhabricatorPaste()) ->setTitle('') ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } public function getURI() { return '/P'.$this->getID(); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'language' => 'text64', 'mailKey' => 'bytes20', 'parentPHID' => 'phid?', // T6203/NULLABILITY // Pastes should always have a view policy. 'viewPolicy' => 'policy?', ), self::CONFIG_KEY_SCHEMA => array( 'parentPHID' => array( 'columns' => array('parentPHID'), ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_language' => array( 'columns' => array('language'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPastePastePHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getFullName() { $title = $this->getTitle(); if (!$title) { $title = pht('(An Untitled Masterwork)'); } return 'P'.$this->getID().' '.$title; } public function getContent() { return $this->assertAttached($this->content); } public function attachContent($content) { $this->content = $content; return $this; } public function getRawContent() { return $this->assertAttached($this->rawContent); } public function attachRawContent($raw_content) { $this->rawContent = $raw_content; return $this; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ 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(), ); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return $this->viewPolicy; } else if ($capability == PhabricatorPolicyCapability::CAN_EDIT) { return $this->editPolicy; } return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return ($user->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht('The author of a paste can always view and edit it.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { if ($this->filePHID) { $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->filePHID)) ->executeOne(); if ($file) { $engine->destroyObject($file); } } $this->delete(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorPasteEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorPasteTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/paste/storage/PhabricatorPasteTransactionComment.php b/src/applications/paste/storage/PhabricatorPasteTransactionComment.php index b50a9afc25..6e7abdbb9c 100644 --- a/src/applications/paste/storage/PhabricatorPasteTransactionComment.php +++ b/src/applications/paste/storage/PhabricatorPasteTransactionComment.php @@ -1,27 +1,27 @@ getTransactionPHID() != null); } - public function getConfiguration() { + protected function getConfiguration() { $config = parent::getConfiguration(); $config[self::CONFIG_COLUMN_SCHEMA] = array( 'lineNumber' => 'uint32?', 'lineLength' => 'uint32?', ) + $config[self::CONFIG_COLUMN_SCHEMA]; return $config; } } diff --git a/src/applications/people/storage/PhabricatorExternalAccount.php b/src/applications/people/storage/PhabricatorExternalAccount.php index 27f9122fb8..12d5f545a7 100644 --- a/src/applications/people/storage/PhabricatorExternalAccount.php +++ b/src/applications/people/storage/PhabricatorExternalAccount.php @@ -1,165 +1,165 @@ assertAttached($this->profileImageFile); } public function attachProfileImageFile(PhabricatorFile $file) { $this->profileImageFile = $file; return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPeopleExternalPHIDType::TYPECONST); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'userPHID' => 'phid?', 'accountType' => 'text16', 'accountDomain' => 'text64', 'accountSecret' => 'text?', 'accountID' => 'text64', 'displayName' => 'text255?', 'username' => 'text255?', 'realName' => 'text255?', 'email' => 'text255?', 'emailVerified' => 'bool', 'profileImagePHID' => 'phid?', 'accountURI' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'account_details' => array( 'columns' => array('accountType', 'accountDomain', 'accountID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPhabricatorUser() { $tmp_usr = id(new PhabricatorUser()) ->makeEphemeral() ->setPHID($this->getPHID()); return $tmp_usr; } public function getProviderKey() { return $this->getAccountType().':'.$this->getAccountDomain(); } public function save() { if (!$this->getAccountSecret()) { $this->setAccountSecret(Filesystem::readRandomCharacters(32)); } return parent::save(); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function isUsableForLogin() { $key = $this->getProviderKey(); $provider = PhabricatorAuthProvider::getEnabledProviderByKey($key); if (!$provider) { return false; } if (!$provider->shouldAllowLogin()) { return false; } return true; } public function getDisplayName() { if (strlen($this->displayName)) { return $this->displayName; } // TODO: Figure out how much identifying information we're going to show // to users about external accounts. For now, just show a string which is // clearly not an error, but don't disclose any identifying information. $map = array( 'email' => pht('Email User'), ); $type = $this->getAccountType(); return idx($map, $type, pht('"%s" User', $type)); } /* -( 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 PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getUserPHID()); } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return null; case PhabricatorPolicyCapability::CAN_EDIT: return pht( 'External accounts can only be edited by the account owner.'); } } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 165d7b1d36..409f633900 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,954 +1,954 @@ timezoneIdentifier, date_default_timezone_get()); // Make sure these return booleans. case 'isAdmin': return (bool)$this->isAdmin; case 'isDisabled': return (bool)$this->isDisabled; case 'isSystemAgent': return (bool)$this->isSystemAgent; case 'isEmailVerified': return (bool)$this->isEmailVerified; case 'isApproved': return (bool)$this->isApproved; default: return parent::readField($field); } } /** * Is this a live account which has passed required approvals? Returns true * if this is an enabled, verified (if required), approved (if required) * account, and false otherwise. * * @return bool True if this is a standard, usable account. */ public function isUserActivated() { if ($this->isOmnipotent()) { return true; } if ($this->getIsDisabled()) { return false; } if (!$this->getIsApproved()) { return false; } if (PhabricatorUserEmail::isEmailVerificationRequired()) { if (!$this->getIsEmailVerified()) { return false; } } return true; } /** * Returns `true` if this is a standard user who is logged in. Returns `false` * for logged out, anonymous, or external users. * * @return bool `true` if the user is a standard user who is logged in with * a normal session. */ public function getIsStandardUser() { $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'userName' => 'sort64', 'realName' => 'text128', 'sex' => 'text4?', 'translation' => 'text64?', 'passwordSalt' => 'text32?', 'passwordHash' => 'text128?', 'profileImagePHID' => 'phid?', 'consoleEnabled' => 'bool', 'consoleVisible' => 'bool', 'consoleTab' => 'text64', 'conduitCertificate' => 'text255', 'isSystemAgent' => 'bool', 'isDisabled' => 'bool', 'isAdmin' => 'bool', 'timezoneIdentifier' => 'text255', 'isEmailVerified' => 'uint32', 'isApproved' => 'uint32', 'accountSecret' => 'bytes64', 'isEnrolledInMultiFactor' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'userName' => array( 'columns' => array('userName'), 'unique' => true, ), 'realName' => array( 'columns' => array('realName'), ), 'key_approved' => array( 'columns' => array('isApproved'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPeopleUserPHIDType::TYPECONST); } public function setPassword(PhutilOpaqueEnvelope $envelope) { if (!$this->getPHID()) { throw new Exception( 'You can not set a password for an unsaved user because their PHID '. 'is a salt component in the password hash.'); } if (!strlen($envelope->openEnvelope())) { $this->setPasswordHash(''); } else { $this->setPasswordSalt(md5(Filesystem::readRandomBytes(32))); $hash = $this->hashPassword($envelope); $this->setPasswordHash($hash->openEnvelope()); } return $this; } // To satisfy PhutilPerson. public function getSex() { return $this->sex; } public function getMonogram() { return '@'.$this->getUsername(); } public function getTranslation() { try { if ($this->translation && class_exists($this->translation) && is_subclass_of($this->translation, 'PhabricatorTranslation')) { return $this->translation; } } catch (PhutilMissingSymbolException $ex) { return null; } return null; } public function isLoggedIn() { return !($this->getPHID() === null); } public function save() { if (!$this->getConduitCertificate()) { $this->setConduitCertificate($this->generateConduitCertificate()); } if (!strlen($this->getAccountSecret())) { $this->setAccountSecret(Filesystem::readRandomCharacters(64)); } $result = parent::save(); if ($this->profile) { $this->profile->save(); } $this->updateNameTokens(); id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing($this->getPHID()); return $result; } public function attachSession(PhabricatorAuthSession $session) { $this->session = $session; return $this; } public function getSession() { return $this->assertAttached($this->session); } public function hasSession() { return ($this->session !== self::ATTACHABLE); } private function generateConduitCertificate() { return Filesystem::readRandomCharacters(255); } public function comparePassword(PhutilOpaqueEnvelope $envelope) { if (!strlen($envelope->openEnvelope())) { return false; } if (!strlen($this->getPasswordHash())) { return false; } return PhabricatorPasswordHasher::comparePassword( $this->getPasswordHashInput($envelope), new PhutilOpaqueEnvelope($this->getPasswordHash())); } private function getPasswordHashInput(PhutilOpaqueEnvelope $password) { $input = $this->getUsername(). $password->openEnvelope(). $this->getPHID(). $this->getPasswordSalt(); return new PhutilOpaqueEnvelope($input); } private function hashPassword(PhutilOpaqueEnvelope $password) { $hasher = PhabricatorPasswordHasher::getBestHasher(); $input_envelope = $this->getPasswordHashInput($password); return $hasher->getPasswordHashForStorage($input_envelope); } const CSRF_CYCLE_FREQUENCY = 3600; const CSRF_SALT_LENGTH = 8; const CSRF_TOKEN_LENGTH = 16; const CSRF_BREACH_PREFIX = 'B@'; const EMAIL_CYCLE_FREQUENCY = 86400; const EMAIL_TOKEN_LENGTH = 24; private function getRawCSRFToken($offset = 0) { return $this->generateToken( time() + (self::CSRF_CYCLE_FREQUENCY * $offset), self::CSRF_CYCLE_FREQUENCY, PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), self::CSRF_TOKEN_LENGTH); } /** * @phutil-external-symbol class PhabricatorStartup */ public function getCSRFToken() { $salt = PhabricatorStartup::getGlobal('csrf.salt'); if (!$salt) { $salt = Filesystem::readRandomCharacters(self::CSRF_SALT_LENGTH); PhabricatorStartup::setGlobal('csrf.salt', $salt); } // Generate a token hash to mitigate BREACH attacks against SSL. See // discussion in T3684. $token = $this->getRawCSRFToken(); $hash = PhabricatorHash::digest($token, $salt); return 'B@'.$salt.substr($hash, 0, self::CSRF_TOKEN_LENGTH); } public function validateCSRFToken($token) { $salt = null; $version = 'plain'; // This is a BREACH-mitigating token. See T3684. $breach_prefix = self::CSRF_BREACH_PREFIX; $breach_prelen = strlen($breach_prefix); if (!strncmp($token, $breach_prefix, $breach_prelen)) { $version = 'breach'; $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH); $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH); } // When the user posts a form, we check that it contains a valid CSRF token. // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept // either the current token, the next token (users can submit a "future" // token if you have two web frontends that have some clock skew) or any of // the last 6 tokens. This means that pages are valid for up to 7 hours. // There is also some Javascript which periodically refreshes the CSRF // tokens on each page, so theoretically pages should be valid indefinitely. // However, this code may fail to run (if the user loses their internet // connection, or there's a JS problem, or they don't have JS enabled). // Choosing the size of the window in which we accept old CSRF tokens is // an issue of balancing concerns between security and usability. We could // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to // attacks using captured CSRF tokens, but it's also more likely that real // users will be affected by this, e.g. if they close their laptop for an // hour, open it back up, and try to submit a form before the CSRF refresh // can kick in. Since the user experience of submitting a form with expired // CSRF is often quite bad (you basically lose data, or it's a big pain to // recover at least) and I believe we gain little additional protection // by keeping the window very short (the overwhelming value here is in // preventing blind attacks, and most attacks which can capture CSRF tokens // can also just capture authentication information [sniffing networks] // or act as the user [xss]) the 7 hour default seems like a reasonable // balance. Other major platforms have much longer CSRF token lifetimes, // like Rails (session duration) and Django (forever), which suggests this // is a reasonable analysis. $csrf_window = 6; for ($ii = -$csrf_window; $ii <= 1; $ii++) { $valid = $this->getRawCSRFToken($ii); switch ($version) { // TODO: We can remove this after the BREACH version has been in the // wild for a while. case 'plain': if ($token == $valid) { return true; } break; case 'breach': $digest = PhabricatorHash::digest($valid, $salt); if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) { return true; } break; default: throw new Exception('Unknown CSRF token format!'); } } return false; } private function generateToken($epoch, $frequency, $key, $len) { if ($this->getPHID()) { $vec = $this->getPHID().$this->getAccountSecret(); } else { $vec = $this->getAlternateCSRFString(); } if ($this->hasSession()) { $vec = $vec.$this->getSession()->getSessionKey(); } $time_block = floor($epoch / $frequency); $vec = $vec.$key.$time_block; return substr(PhabricatorHash::digest($vec), 0, $len); } public function attachUserProfile(PhabricatorUserProfile $profile) { $this->profile = $profile; return $this; } public function loadUserProfile() { if ($this->profile) { return $this->profile; } $profile_dao = new PhabricatorUserProfile(); $this->profile = $profile_dao->loadOneWhere('userPHID = %s', $this->getPHID()); if (!$this->profile) { $profile_dao->setUserPHID($this->getPHID()); $this->profile = $profile_dao; } return $this->profile; } public function loadPrimaryEmailAddress() { $email = $this->loadPrimaryEmail(); if (!$email) { throw new Exception('User has no primary email address!'); } return $email->getAddress(); } public function loadPrimaryEmail() { return $this->loadOneRelative( new PhabricatorUserEmail(), 'userPHID', 'getPHID', '(isPrimary = 1)'); } public function loadPreferences() { if ($this->preferences) { return $this->preferences; } $preferences = null; if ($this->getPHID()) { $preferences = id(new PhabricatorUserPreferences())->loadOneWhere( 'userPHID = %s', $this->getPHID()); } if (!$preferences) { $preferences = new PhabricatorUserPreferences(); $preferences->setUserPHID($this->getPHID()); $default_dict = array( PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph', PhabricatorUserPreferences::PREFERENCE_EDITOR => '', PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '', PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0, ); $preferences->setPreferences($default_dict); } $this->preferences = $preferences; return $preferences; } public function loadEditorLink($path, $line, $callsign) { $editor = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_EDITOR); if (is_array($path)) { $multiedit = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_MULTIEDIT); switch ($multiedit) { case '': $path = implode(' ', $path); break; case 'disable': return null; } } if (!strlen($editor)) { return null; } $uri = strtr($editor, array( '%%' => '%', '%f' => phutil_escape_uri($path), '%l' => phutil_escape_uri($line), '%r' => phutil_escape_uri($callsign), )); // The resulting URI must have an allowed protocol. Otherwise, we'll return // a link to an error page explaining the misconfiguration. $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri); if (!$ok) { return '/help/editorprotocol/'; } return (string)$uri; } public function getAlternateCSRFString() { return $this->assertAttached($this->alternateCSRFString); } public function attachAlternateCSRFString($string) { $this->alternateCSRFString = $string; return $this; } /** * Populate the nametoken table, which used to fetch typeahead results. When * a user types "linc", we want to match "Abraham Lincoln" from on-demand * typeahead sources. To do this, we need a separate table of name fragments. */ public function updateNameTokens() { $table = self::NAMETOKEN_TABLE; $conn_w = $this->establishConnection('w'); $tokens = PhabricatorTypeaheadDatasource::tokenizeString( $this->getUserName().' '.$this->getRealName()); $sql = array(); foreach ($tokens as $token) { $sql[] = qsprintf( $conn_w, '(%d, %s)', $this->getID(), $token); } queryfx( $conn_w, 'DELETE FROM %T WHERE userID = %d', $table, $this->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (userID, token) VALUES %Q', $table, implode(', ', $sql)); } } public function sendWelcomeEmail(PhabricatorUser $admin) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $user_username = $this->getUserName(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $base_uri = PhabricatorEnv::getProductionURI('/'); $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $this, $this->loadPrimaryEmail(), PhabricatorAuthSessionEngine::ONETIME_WELCOME); $body = <<addTos(array($this->getPHID())) ->setForceDelivery(true) ->setSubject('[Phabricator] Welcome to Phabricator') ->setBody($body) ->saveAndSend(); } public function sendUsernameChangeEmail( PhabricatorUser $admin, $old_username) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $new_username = $this->getUserName(); $password_instructions = null; if (PhabricatorPasswordAuthProvider::getPasswordProvider()) { $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $this, null, PhabricatorAuthSessionEngine::ONETIME_USERNAME); $password_instructions = <<addTos(array($this->getPHID())) ->setForceDelivery(true) ->setSubject('[Phabricator] Username Changed') ->setBody($body) ->saveAndSend(); } public static function describeValidUsername() { return pht( 'Usernames must contain only numbers, letters, period, underscore and '. 'hyphen, and can not end with a period. They must have no more than %d '. 'characters.', new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH)); } public static function validateUsername($username) { // NOTE: If you update this, make sure to update: // // - Remarkup rule for @mentions. // - Routing rule for "/p/username/". // - Unit tests, obviously. // - describeValidUsername() method, above. if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) { return false; } return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username); } public static function getDefaultProfileImageURI() { return celerity_get_resource_uri('/rsrc/image/avatar.png'); } public function attachStatus(PhabricatorCalendarEvent $status) { $this->status = $status; return $this; } public function getStatus() { return $this->assertAttached($this->status); } public function hasStatus() { return $this->status !== self::ATTACHABLE; } public function attachProfileImageURI($uri) { $this->profileImage = $uri; return $this; } public function getProfileImageURI() { return $this->assertAttached($this->profileImage); } public function loadProfileImageURI() { if ($this->profileImage && ($this->profileImage !== self::ATTACHABLE)) { return $this->profileImage; } $src_phid = $this->getProfileImagePHID(); if ($src_phid) { // TODO: (T603) Can we get rid of this entirely and move it to // PeopleQuery with attach/attachable? $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); if ($file) { $this->profileImage = $file->getBestURI(); return $this->profileImage; } } $this->profileImage = self::getDefaultProfileImageURI(); return $this->profileImage; } public function getFullName() { if (strlen($this->getRealName())) { return $this->getUsername().' ('.$this->getRealName().')'; } else { return $this->getUsername(); } } public function __toString() { return $this->getUsername(); } public static function loadOneWithEmailAddress($address) { $email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $address); if (!$email) { return null; } return id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $email->getUserPHID()); } /* -( Multi-Factor Authentication )---------------------------------------- */ /** * Update the flag storing this user's enrollment in multi-factor auth. * * With certain settings, we need to check if a user has MFA on every page, * so we cache MFA enrollment on the user object for performance. Calling this * method synchronizes the cache by examining enrollment records. After * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if * the user is enrolled. * * This method should be called after any changes are made to a given user's * multi-factor configuration. * * @return void * @task factors */ public function updateMultiFactorEnrollment() { $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $this->getPHID()); $enrolled = count($factors) ? 1 : 0; if ($enrolled !== $this->isEnrolledInMultiFactor) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d', $this->getTableName(), $enrolled, $this->getID()); unset($unguarded); $this->isEnrolledInMultiFactor = $enrolled; } } /** * Check if the user is enrolled in multi-factor authentication. * * Enrolled users have one or more multi-factor authentication sources * attached to their account. For performance, this value is cached. You * can use @{method:updateMultiFactorEnrollment} to update the cache. * * @return bool True if the user is enrolled. * @task factors */ public function getIsEnrolledInMultiFactor() { return $this->isEnrolledInMultiFactor; } /* -( Omnipotence )-------------------------------------------------------- */ /** * Returns true if this user is omnipotent. Omnipotent users bypass all policy * checks. * * @return bool True if the user bypasses policy checks. */ public function isOmnipotent() { return $this->omnipotent; } /** * Get an omnipotent user object for use in contexts where there is no acting * user, notably daemons. * * @return PhabricatorUser An omnipotent user. */ public static function getOmnipotentUser() { static $user = null; if (!$user) { $user = new PhabricatorUser(); $user->omnipotent = true; $user->makeEphemeral(); } return $user; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_PUBLIC; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getIsSystemAgent()) { return PhabricatorPolicies::POLICY_ADMIN; } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getPHID() && ($viewer->getPHID() === $this->getPHID()); } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return pht('Only you can edit your information.'); default: return null; } } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('user.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorUserCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $externals = id(new PhabricatorExternalAccount())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($externals as $external) { $external->delete(); } $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($prefs as $pref) { $pref->delete(); } $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($profiles as $profile) { $profile->delete(); } $keys = id(new PhabricatorAuthSSHKey())->loadAllWhere( 'objectPHID = %s', $this->getPHID()); foreach ($keys as $key) { $key->delete(); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($emails as $email) { $email->delete(); } $sessions = id(new PhabricatorAuthSession())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($sessions as $session) { $session->delete(); } $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($factors as $factor) { $factor->delete(); } $this->saveTransaction(); } /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) { if ($viewer->getPHID() == $this->getPHID()) { // If the viewer is managing their own keys, take them to the normal // panel. return '/settings/panel/ssh/'; } else { // Otherwise, take them to the administrative panel for this user. return '/settings/'.$this->getID().'/panel/ssh/'; } } public function getSSHKeyDefaultName() { return 'id_rsa_phabricator'; } } diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php index e4b63b6735..4ae58130a2 100644 --- a/src/applications/people/storage/PhabricatorUserEmail.php +++ b/src/applications/people/storage/PhabricatorUserEmail.php @@ -1,276 +1,276 @@ array( 'address' => 'sort128', 'isVerified' => 'bool', 'isPrimary' => 'bool', 'verificationCode' => 'text64?', ), self::CONFIG_KEY_SCHEMA => array( 'address' => array( 'columns' => array('address'), 'unique' => true, ), 'userPHID' => array( 'columns' => array('userPHID', 'isPrimary'), ), ), ) + parent::getConfiguration(); } public function getVerificationURI() { return '/emailverify/'.$this->getVerificationCode().'/'; } public function save() { if (!$this->verificationCode) { $this->setVerificationCode(Filesystem::readRandomCharacters(24)); } return parent::save(); } /* -( Domain Restrictions )------------------------------------------------ */ /** * @task restrictions */ public static function isValidAddress($address) { if (strlen($address) > self::MAX_ADDRESS_LENGTH) { return false; } // Very roughly validate that this address isn't so mangled that a // reasonable piece of code might completely misparse it. In particular, // the major risks are: // // - `PhutilEmailAddress` needs to be able to extract the domain portion // from it. // - Reasonable mail adapters should be hard-pressed to interpret one // address as several addresses. // // To this end, we're roughly verifying that there's some normal text, an // "@" symbol, and then some more normal text. $email_regex = '(^[a-z0-9_+.!-]+@[a-z0-9_+:.-]+\z)i'; if (!preg_match($email_regex, $address)) { return false; } return true; } /** * @task restrictions */ public static function describeValidAddresses() { return pht( "Email addresses should be in the form 'user@domain.com'. The maximum ". "length of an email address is %d character(s).", new PhutilNumber(self::MAX_ADDRESS_LENGTH)); } /** * @task restrictions */ public static function isAllowedAddress($address) { if (!self::isValidAddress($address)) { return false; } $allowed_domains = PhabricatorEnv::getEnvConfig('auth.email-domains'); if (!$allowed_domains) { return true; } $addr_obj = new PhutilEmailAddress($address); $domain = $addr_obj->getDomainName(); if (!$domain) { return false; } $lower_domain = phutil_utf8_strtolower($domain); foreach ($allowed_domains as $allowed_domain) { $lower_allowed = phutil_utf8_strtolower($allowed_domain); if ($lower_allowed === $lower_domain) { return true; } } return false; } /** * @task restrictions */ public static function describeAllowedAddresses() { $domains = PhabricatorEnv::getEnvConfig('auth.email-domains'); if (!$domains) { return null; } if (count($domains) == 1) { return 'Email address must be @'.head($domains); } else { return 'Email address must be at one of: '. implode(', ', $domains); } } /** * Check if this install requires email verification. * * @return bool True if email addresses must be verified. * * @task restrictions */ public static function isEmailVerificationRequired() { // NOTE: Configuring required email domains implies required verification. return PhabricatorEnv::getEnvConfig('auth.require-email-verification') || PhabricatorEnv::getEnvConfig('auth.email-domains'); } /* -( Email About Email )-------------------------------------------------- */ /** * Send a verification email from $user to this address. * * @param PhabricatorUser The user sending the verification. * @return this * @task email */ public function sendVerificationEmail(PhabricatorUser $user) { $username = $user->getUsername(); $address = $this->getAddress(); $link = PhabricatorEnv::getProductionURI($this->getVerificationURI()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $signature = null; if (!$is_serious) { $signature = <<addRawTos(array($address)) ->setForceDelivery(true) ->setSubject('[Phabricator] Email Verification') ->setBody($body) ->setRelatedPHID($user->getPHID()) ->saveAndSend(); return $this; } /** * Send a notification email from $user to this address, informing the * recipient that this is no longer their account's primary address. * * @param PhabricatorUser The user sending the notification. * @param PhabricatorUserEmail New primary email address. * @return this * @task email */ public function sendOldPrimaryEmail( PhabricatorUser $user, PhabricatorUserEmail $new) { $username = $user->getUsername(); $old_address = $this->getAddress(); $new_address = $new->getAddress(); $body = <<addRawTos(array($old_address)) ->setForceDelivery(true) ->setSubject('[Phabricator] Primary Address Changed') ->setBody($body) ->setFrom($user->getPHID()) ->setRelatedPHID($user->getPHID()) ->saveAndSend(); } /** * Send a notification email from $user to this address, informing the * recipient that this is now their account's new primary email address. * * @param PhabricatorUser The user sending the verification. * @return this * @task email */ public function sendNewPrimaryEmail(PhabricatorUser $user) { $username = $user->getUsername(); $new_address = $this->getAddress(); $body = <<addRawTos(array($new_address)) ->setForceDelivery(true) ->setSubject('[Phabricator] Primary Address Changed') ->setBody($body) ->setFrom($user->getPHID()) ->setRelatedPHID($user->getPHID()) ->saveAndSend(); return $this; } } diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index 8a4c2078de..93bf39599d 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -1,206 +1,206 @@ pht('Login'), self::ACTION_LOGIN_PARTIAL => pht('Login: Partial Login'), self::ACTION_LOGIN_FULL => pht('Login: Upgrade to Full'), self::ACTION_LOGIN_FAILURE => pht('Login: Failure'), self::ACTION_LOGOUT => pht('Logout'), self::ACTION_RESET_PASSWORD => pht('Reset Password'), self::ACTION_CREATE => pht('Create Account'), self::ACTION_EDIT => pht('Edit Account'), self::ACTION_ADMIN => pht('Add/Remove Administrator'), self::ACTION_SYSTEM_AGENT => pht('Add/Remove System Agent'), self::ACTION_DISABLE => pht('Enable/Disable'), self::ACTION_APPROVE => pht('Approve Registration'), self::ACTION_DELETE => pht('Delete User'), self::ACTION_CONDUIT_CERTIFICATE => pht('Conduit: Read Certificate'), self::ACTION_CONDUIT_CERTIFICATE_FAILURE => pht('Conduit: Read Certificate Failure'), self::ACTION_EMAIL_PRIMARY => pht('Email: Change Primary'), self::ACTION_EMAIL_ADD => pht('Email: Add Address'), self::ACTION_EMAIL_REMOVE => pht('Email: Remove Address'), self::ACTION_EMAIL_VERIFY => pht('Email: Verify'), self::ACTION_CHANGE_PASSWORD => pht('Change Password'), self::ACTION_CHANGE_USERNAME => pht('Change Username'), self::ACTION_ENTER_HISEC => pht('Hisec: Enter'), self::ACTION_EXIT_HISEC => pht('Hisec: Exit'), self::ACTION_FAIL_HISEC => pht('Hisec: Failed Attempt'), self::ACTION_MULTI_ADD => pht('Multi-Factor: Add Factor'), self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'), ); } public static function initializeNewLog( PhabricatorUser $actor = null, $object_phid, $action) { $log = new PhabricatorUserLog(); if ($actor) { $log->setActorPHID($actor->getPHID()); if ($actor->hasSession()) { $session = $actor->getSession(); // NOTE: This is a hash of the real session value, so it's safe to // store it directly in the logs. $log->setSession($session->getSessionKey()); } } $log->setUserPHID((string)$object_phid); $log->setAction($action); $log->remoteAddr = idx($_SERVER, 'REMOTE_ADDR', ''); return $log; } public static function loadRecentEventsFromThisIP($action, $timespan) { return id(new PhabricatorUserLog())->loadAllWhere( 'action = %s AND remoteAddr = %s AND dateCreated > %d ORDER BY dateCreated DESC', $action, idx($_SERVER, 'REMOTE_ADDR'), time() - $timespan); } public function save() { $this->details['host'] = php_uname('n'); $this->details['user_agent'] = AphrontRequest::getHTTPHeader('User-Agent'); return parent::save(); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'actorPHID' => 'phid?', 'action' => 'text64', 'remoteAddr' => 'text64', 'session' => 'bytes40?', ), self::CONFIG_KEY_SCHEMA => array( 'actorPHID' => array( 'columns' => array('actorPHID', 'dateCreated'), ), 'userPHID' => array( 'columns' => array('userPHID', 'dateCreated'), ), 'action' => array( 'columns' => array('action', 'dateCreated'), ), 'dateCreated' => array( 'columns' => array('dateCreated'), ), 'remoteAddr' => array( 'columns' => array('remoteAddr', 'dateCreated'), ), 'session' => array( 'columns' => array('session', 'dateCreated'), ), ), ) + parent::getConfiguration(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($viewer->getIsAdmin()) { return true; } $viewer_phid = $viewer->getPHID(); if ($viewer_phid) { $user_phid = $this->getUserPHID(); if ($viewer_phid == $user_phid) { return true; } $actor_phid = $this->getActorPHID(); if ($viewer_phid == $actor_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { return array( pht('Users can view their activity and activity that affects them.'), pht('Administrators can always view all activity.'), ); } } diff --git a/src/applications/people/storage/PhabricatorUserProfile.php b/src/applications/people/storage/PhabricatorUserProfile.php index 169bf2d0f3..8ebd4f2af3 100644 --- a/src/applications/people/storage/PhabricatorUserProfile.php +++ b/src/applications/people/storage/PhabricatorUserProfile.php @@ -1,26 +1,26 @@ array( 'title' => 'text255', 'blurb' => 'text', 'profileImagePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID' => array( 'columns' => array('userPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index fb74ae1644..e8eba5582b 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -1,314 +1,314 @@ true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text64', 'description' => 'text', 'domain' => 'text128?', // T6203/NULLABILITY // These policies should always be non-null. 'joinPolicy' => 'policy?', 'editPolicy' => 'policy?', 'viewPolicy' => 'policy?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'domain' => array( 'columns' => array('domain'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhameBlogPHIDType::TYPECONST); } public function getSkinRenderer(AphrontRequest $request) { $spec = PhameSkinSpecification::loadOneSkinSpecification( $this->getSkin()); if (!$spec) { $spec = PhameSkinSpecification::loadOneSkinSpecification( self::SKIN_DEFAULT); } if (!$spec) { throw new Exception( 'This blog has an invalid skin, and the default skin failed to '. 'load.'); } $skin = newv($spec->getSkinClass(), array()); $skin->setRequest($request); $skin->setSpecification($spec); return $skin; } /** * Makes sure a given custom blog uri is properly configured in DNS * to point at this Phabricator instance. If there is an error in * the configuration, return a string describing the error and how * to fix it. If there is no error, return an empty string. * * @return string */ public function validateCustomDomain($custom_domain) { $example_domain = 'blog.example.com'; $label = pht('Invalid'); // note this "uri" should be pretty busted given the desired input // so just use it to test if there's a protocol specified $uri = new PhutilURI($custom_domain); if ($uri->getProtocol()) { return array( $label, pht( 'The custom domain should not include a protocol. Just provide '. 'the bare domain name (for example, "%s").', $example_domain), ); } if ($uri->getPort()) { return array( $label, pht( 'The custom domain should not include a port number. Just provide '. 'the bare domain name (for example, "%s").', $example_domain), ); } if (strpos($custom_domain, '/') !== false) { return array( $label, pht( 'The custom domain should not specify a path (hosting a Phame '. 'blog at a path is currently not supported). Instead, just provide '. 'the bare domain name (for example, "%s").', $example_domain), ); } if (strpos($custom_domain, '.') === false) { return array( $label, pht( 'The custom domain should contain at least one dot (.) because '. 'some browsers fail to set cookies on domains without a dot. '. 'Instead, use a normal looking domain name like "%s".', $example_domain), ); } if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) { $href = PhabricatorEnv::getProductionURI( '/config/edit/policy.allow-public/'); return array( pht('Fix Configuration'), pht( 'For custom domains to work, this Phabricator instance must be '. 'configured to allow the public access policy. Configure this '. 'setting %s, or ask an administrator to configure this setting. '. 'The domain can be specified later once this setting has been '. 'changed.', phutil_tag( 'a', array('href' => $href), pht('here'))), ); } return null; } public function getBloggerPHIDs() { return $this->assertAttached($this->bloggerPHIDs); } public function attachBloggers(array $bloggers) { assert_instances_of($bloggers, 'PhabricatorObjectHandle'); $this->bloggers = $bloggers; return $this; } public function getBloggers() { return $this->assertAttached($this->bloggers); } public function getSkin() { $config = coalesce($this->getConfigData(), array()); return idx($config, 'skin', self::SKIN_DEFAULT); } public function setSkin($skin) { $config = coalesce($this->getConfigData(), array()); $config['skin'] = $skin; return $this->setConfigData($config); } static public function getSkinOptionsForSelect() { $classes = id(new PhutilSymbolLoader()) ->setAncestorClass('PhameBlogSkin') ->setType('class') ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); return ipull($classes, 'name', 'name'); } public static function setRequestBlog(PhameBlog $blog) { self::$requestBlog = $blog; } public static function getRequestBlog() { return self::$requestBlog; } public function getLiveURI(PhamePost $post = null) { if ($this->getDomain()) { $base = new PhutilURI('http://'.$this->getDomain().'/'); } else { $base = '/phame/live/'.$this->getID().'/'; $base = PhabricatorEnv::getURI($base); } if ($post) { $base .= '/post/'.$post->getPhameTitle(); } return $base; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_JOIN, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case PhabricatorPolicyCapability::CAN_JOIN: return $this->getJoinPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { $can_edit = PhabricatorPolicyCapability::CAN_EDIT; $can_join = PhabricatorPolicyCapability::CAN_JOIN; switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // Users who can edit or post to a blog can always view it. if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) { return true; } if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_join)) { return true; } break; case PhabricatorPolicyCapability::CAN_JOIN: // Users who can edit a blog can always post to it. if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) { return true; } break; } return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht( 'Users who can edit or post on a blog can always view it.'); case PhabricatorPolicyCapability::CAN_JOIN: return pht( 'Users who can edit a blog can always post on it.'); } return null; } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return $this->getPHID().':'.$field.':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { return $this->getDescription(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index ea6acbe8b4..f99c1bda97 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -1,264 +1,264 @@ setBloggerPHID($blogger->getPHID()) ->setBlogPHID($blog->getPHID()) ->setBlog($blog) ->setDatePublished(0) ->setVisibility(self::VISIBILITY_DRAFT); return $post; } public function setBlog(PhameBlog $blog) { $this->blog = $blog; return $this; } public function getBlog() { return $this->blog; } public function getViewURI() { // go for the pretty uri if we can $domain = ($this->blog ? $this->blog->getDomain() : ''); if ($domain) { $phame_title = PhabricatorSlug::normalize($this->getPhameTitle()); return 'http://'.$domain.'/post/'.$phame_title; } $uri = '/phame/post/view/'.$this->getID().'/'; return PhabricatorEnv::getProductionURI($uri); } public function getEditURI() { return '/phame/post/edit/'.$this->getID().'/'; } public function isDraft() { return $this->getVisibility() == self::VISIBILITY_DRAFT; } public function getHumanName() { if ($this->isDraft()) { $name = 'draft'; } else { $name = 'post'; } return $name; } public function getCommentsWidget() { $config_data = $this->getConfigData(); if (empty($config_data)) { return 'none'; } return idx($config_data, 'comments_widget', 'none'); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'phameTitle' => 'sort64', 'visibility' => 'uint32', // T6203/NULLABILITY // These seem like they should always be non-null? 'blogPHID' => 'phid?', 'body' => 'text?', 'configData' => 'text?', // T6203/NULLABILITY // This one probably should be nullable? 'datePublished' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'phameTitle' => array( 'columns' => array('bloggerPHID', 'phameTitle'), 'unique' => true, ), 'bloggerPosts' => array( 'columns' => array( 'bloggerPHID', 'visibility', 'datePublished', 'id', ), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhamePostPHIDType::TYPECONST); } public function toDictionary() { return array( 'id' => $this->getID(), 'phid' => $this->getPHID(), 'blogPHID' => $this->getBlogPHID(), 'bloggerPHID' => $this->getBloggerPHID(), 'viewURI' => $this->getViewURI(), 'title' => $this->getTitle(), 'phameTitle' => $this->getPhameTitle(), 'body' => $this->getBody(), 'summary' => PhabricatorMarkupEngine::summarize($this->getBody()), 'datePublished' => $this->getDatePublished(), 'published' => !$this->isDraft(), ); } public static function getVisibilityOptionsForSelect() { return array( self::VISIBILITY_DRAFT => 'Draft: visible only to me.', self::VISIBILITY_PUBLISHED => 'Published: visible to the whole world.', ); } public function getCommentsWidgetOptionsForSelect() { $current = $this->getCommentsWidget(); $options = array(); if ($current == 'facebook' || PhabricatorFacebookAuthProvider::getFacebookApplicationID()) { $options['facebook'] = 'Facebook'; } if ($current == 'disqus' || PhabricatorEnv::getEnvConfig('disqus.shortname')) { $options['disqus'] = 'Disqus'; } $options['none'] = 'None'; return $options; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // Draft posts are visible only to the author. Published posts are visible // to whoever the blog is visible to. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->isDraft() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } break; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A blog post's author can always view it, and is the only user allowed // to edit it. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return ($user->getPHID() == $this->getBloggerPHID()); } } public function describeAutomaticCapability($capability) { return pht( 'The author of a blog post can always view and edit it.'); } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return $this->getPHID().':'.$field.':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_BODY: return $this->getBody(); case self::MARKUP_FIELD_SUMMARY: return PhabricatorMarkupEngine::summarize($this->getBody()); } } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getBloggerPHID(), ); } } diff --git a/src/applications/phlux/storage/PhluxVariable.php b/src/applications/phlux/storage/PhluxVariable.php index 7a356b8c44..a4ade1e9c7 100644 --- a/src/applications/phlux/storage/PhluxVariable.php +++ b/src/applications/phlux/storage/PhluxVariable.php @@ -1,87 +1,87 @@ true, self::CONFIG_SERIALIZATION => array( 'variableValue' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'variableKey' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_key' => array( 'columns' => array('variableKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PhluxVariablePHIDType::TYPECONST); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhluxVariableEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhluxTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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 $this->editPolicy; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/pholio/storage/PholioImage.php b/src/applications/pholio/storage/PholioImage.php index 047b22bcbd..29b649dd14 100644 --- a/src/applications/pholio/storage/PholioImage.php +++ b/src/applications/pholio/storage/PholioImage.php @@ -1,126 +1,126 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'mockID' => 'id?', 'name' => 'text128', 'description' => 'text', 'sequence' => 'uint32', 'isObsolete' => 'bool', 'replacesImagePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'keyPHID' => array( 'columns' => array('phid'), 'unique' => true, ), 'mockID' => array( 'columns' => array('mockID', 'isObsolete', 'sequence'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PholioImagePHIDType::TYPECONST); } public function attachFile(PhabricatorFile $file) { $this->file = $file; return $this; } public function getFile() { $this->assertAttached($this->file); return $this->file; } public function attachMock(PholioMock $mock) { $this->mock = $mock; return $this; } public function getMock() { $this->assertAttached($this->mock); return $this->mock; } public function attachInlineComments(array $inline_comments) { assert_instances_of($inline_comments, 'PholioTransactionComment'); $this->inlineComments = $inline_comments; return $this; } public function getInlineComments() { $this->assertAttached($this->inlineComments); return $this->inlineComments; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return 'M:'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { return $this->getDescription(); } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return $this->getMock()->getCapabilities(); } public function getPolicy($capability) { return $this->getMock()->getPolicy($capability); } // really the *mock* controls who can see an image public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getMock()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 24648c2232..71feb3919e 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -1,307 +1,307 @@ setViewer($actor) ->withClasses(array('PhabricatorPholioApplication')) ->executeOne(); $view_policy = $app->getPolicy(PholioDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PholioDefaultEditCapability::CAPABILITY); return id(new PholioMock()) ->setAuthorPHID($actor->getPHID()) ->attachImages(array()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } public function getMonogram() { return 'M'.$this->getID(); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'description' => 'text', 'originalName' => 'text128', 'mailKey' => 'bytes20', 'status' => 'text12', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('MOCK'); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /** * These should be the images currently associated with the Mock. */ public function attachImages(array $images) { assert_instances_of($images, 'PholioImage'); $this->images = $images; return $this; } public function getImages() { $this->assertAttached($this->images); return $this->images; } /** * These should be *all* images associated with the Mock. This includes * images which have been removed and / or replaced from the Mock. */ public function attachAllImages(array $images) { assert_instances_of($images, 'PholioImage'); $this->allImages = $images; return $this; } public function getAllImages() { $this->assertAttached($this->images); return $this->allImages; } public function attachCoverFile(PhabricatorFile $file) { $this->coverFile = $file; return $this; } public function getCoverFile() { $this->assertAttached($this->coverFile); return $this->coverFile; } public function getTokenCount() { $this->assertAttached($this->tokenCount); return $this->tokenCount; } public function attachTokenCount($count) { $this->tokenCount = $count; return $this; } public function getImageHistorySet($image_id) { $images = $this->getAllImages(); $images = mpull($images, null, 'getID'); $selected_image = $images[$image_id]; $replace_map = mpull($images, null, 'getReplacesImagePHID'); $phid_map = mpull($images, null, 'getPHID'); // find the earliest image $image = $selected_image; while (isset($phid_map[$image->getReplacesImagePHID()])) { $image = $phid_map[$image->getReplacesImagePHID()]; } // now build history moving forward $history = array($image->getID() => $image); while (isset($replace_map[$image->getPHID()])) { $image = $replace_map[$image->getPHID()]; $history[$image->getID()] = $image; } return $history; } public function getStatuses() { $options = array(); $options['open'] = pht('Open'); $options['closed'] = pht('Closed'); return $options; } public function isClosed() { return ($this->getStatus() == 'closed'); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht("A mock's owner can always view and edit it."); } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return 'M:'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { if ($this->getDescription()) { $description = $this->getDescription(); } else { $description = pht('No Description Given'); } return $description; } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PholioMockEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PholioTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { PholioMockQuery::loadImages( $request->getUser(), array($this), $need_inline_comments = true); $timeline->setMock($this); return $timeline; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $images = id(new PholioImage())->loadAllWhere( 'mockID = %d', $this->getID()); foreach ($images as $image) { $image->delete(); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/pholio/storage/PholioTransactionComment.php b/src/applications/pholio/storage/PholioTransactionComment.php index 926b74eb7d..c9a07dd892 100644 --- a/src/applications/pholio/storage/PholioTransactionComment.php +++ b/src/applications/pholio/storage/PholioTransactionComment.php @@ -1,55 +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/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index 3bb380e151..d57746ec85 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -1,160 +1,160 @@ memberPHIDs = array(); return $account; } public static function createNewAccount( PhabricatorUser $actor, PhabricatorContentSource $content_source) { $account = PhortuneAccount::initializeNewAccount($actor); $xactions = array(); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhortuneAccountTransaction::TYPE_NAME) ->setNewValue(pht('Personal Account')); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhortuneAccountHasMemberEdgeType::EDGECONST) ->setNewValue( array( '=' => array($actor->getPHID() => $actor->getPHID()), )); $editor = id(new PhortuneAccountEditor()) ->setActor($actor) ->setContentSource($content_source); // We create an account for you the first time you visit Phortune. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $editor->applyTransactions($account, $xactions); unset($unguarded); return $account; } public function newCart( PhabricatorUser $actor, PhortuneCartImplementation $implementation, PhortuneMerchant $merchant) { $cart = PhortuneCart::initializeNewCart($actor, $this, $merchant); $cart->setCartClass(get_class($implementation)); $cart->attachImplementation($implementation); $implementation->willCreateCart($actor, $cart); return $cart->save(); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneAccountPHIDType::TYPECONST); } public function getMemberPHIDs() { return $this->assertAttached($this->memberPHIDs); } public function attachMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortuneAccountEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhortuneAccountTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // Accounts are technically visible to all users, because merchant // controllers need to be able to see accounts in order to process // orders. We lock things down more tightly at the application level. return PhabricatorPolicies::POLICY_USER; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getPHID() === null) { // Allow a user to create an account for themselves. return PhabricatorPolicies::POLICY_USER; } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $members = array_fuse($this->getMemberPHIDs()); return isset($members[$viewer->getPHID()]); } public function describeAutomaticCapability($capability) { return pht('Members of an account can always view and edit it.'); } } diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 31eb3a4b88..f1867985e3 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -1,670 +1,670 @@ setAuthorPHID($actor->getPHID()) ->setStatus(self::STATUS_BUILDING) ->setAccountPHID($account->getPHID()) ->setMerchantPHID($merchant->getPHID()); $cart->account = $account; $cart->purchases = array(); return $cart; } public function newPurchase( PhabricatorUser $actor, PhortuneProduct $product) { $purchase = PhortunePurchase::initializeNewPurchase($actor, $product) ->setAccountPHID($this->getAccount()->getPHID()) ->setCartPHID($this->getPHID()) ->save(); $this->purchases[] = $purchase; return $purchase; } public static function getStatusNameMap() { return array( self::STATUS_BUILDING => pht('Building'), self::STATUS_READY => pht('Ready'), self::STATUS_PURCHASING => pht('Purchasing'), self::STATUS_CHARGED => pht('Charged'), self::STATUS_HOLD => pht('Hold'), self::STATUS_REVIEW => pht('Review'), self::STATUS_PURCHASED => pht('Purchased'), ); } public static function getNameForStatus($status) { return idx(self::getStatusNameMap(), $status, $status); } public function activateCart() { $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if ($copy->getStatus() !== self::STATUS_BUILDING) { throw new Exception( pht( 'Cart has wrong status ("%s") to call willApplyCharge().', $copy->getStatus())); } $this->setStatus(self::STATUS_READY)->save(); $this->endReadLocking(); $this->saveTransaction(); $this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED); return $this; } public function willApplyCharge( PhabricatorUser $actor, PhortunePaymentProvider $provider, PhortunePaymentMethod $method = null) { $account = $this->getAccount(); $charge = PhortuneCharge::initializeNewCharge() ->setAccountPHID($account->getPHID()) ->setCartPHID($this->getPHID()) ->setAuthorPHID($actor->getPHID()) ->setMerchantPHID($this->getMerchant()->getPHID()) ->setProviderPHID($provider->getProviderConfig()->getPHID()) ->setAmountAsCurrency($this->getTotalPriceAsCurrency()); if ($method) { $charge->setPaymentMethodPHID($method->getPHID()); } $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if ($copy->getStatus() !== self::STATUS_READY) { throw new Exception( pht( 'Cart has wrong status ("%s") to call willApplyCharge(), '. 'expected "%s".', $copy->getStatus(), self::STATUS_READY)); } $charge->save(); $this->setStatus(PhortuneCart::STATUS_PURCHASING)->save(); $this->endReadLocking(); $this->saveTransaction(); return $charge; } public function didHoldCharge(PhortuneCharge $charge) { $charge->setStatus(PhortuneCharge::STATUS_HOLD); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if ($copy->getStatus() !== self::STATUS_PURCHASING) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didHoldCharge(), '. 'expected "%s".', $copy->getStatus(), self::STATUS_PURCHASING)); } $charge->save(); $this->setStatus(self::STATUS_HOLD)->save(); $this->endReadLocking(); $this->saveTransaction(); $this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD); } public function didApplyCharge(PhortuneCharge $charge) { $charge->setStatus(PhortuneCharge::STATUS_CHARGED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if (($copy->getStatus() !== self::STATUS_PURCHASING) && ($copy->getStatus() !== self::STATUS_HOLD)) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didApplyCharge().', $copy->getStatus())); } $charge->save(); $this->setStatus(self::STATUS_CHARGED)->save(); $this->endReadLocking(); $this->saveTransaction(); // TODO: Perform purchase review. Here, we would apply rules to determine // whether the charge needs manual review (maybe making the decision via // Herald, configuration, or by examining provider fraud data). For now, // always require review. $needs_review = true; if ($needs_review) { $this->willReviewCart(); } else { $this->didReviewCart(); } return $this; } public function willReviewCart() { $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if (($copy->getStatus() !== self::STATUS_CHARGED)) { throw new Exception( pht( 'Cart has wrong status ("%s") to call willReviewCart()!', $copy->getStatus())); } $this->setStatus(self::STATUS_REVIEW)->save(); $this->endReadLocking(); $this->saveTransaction(); $this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW); return $this; } public function didReviewCart() { $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if (($copy->getStatus() !== self::STATUS_CHARGED) && ($copy->getStatus() !== self::STATUS_REVIEW)) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didReviewCart()!', $copy->getStatus())); } foreach ($this->purchases as $purchase) { $purchase->getProduct()->didPurchaseProduct($purchase); } $this->setStatus(self::STATUS_PURCHASED)->save(); $this->endReadLocking(); $this->saveTransaction(); $this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED); return $this; } public function didFailCharge(PhortuneCharge $charge) { $charge->setStatus(PhortuneCharge::STATUS_FAILED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if (($copy->getStatus() !== self::STATUS_PURCHASING) && ($copy->getStatus() !== self::STATUS_HOLD)) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didFailCharge().', $copy->getStatus())); } $charge->save(); // Move the cart back into STATUS_READY so the user can try // making the purchase again. $this->setStatus(self::STATUS_READY)->save(); $this->endReadLocking(); $this->saveTransaction(); return $this; } public function willRefundCharge( PhabricatorUser $actor, PhortunePaymentProvider $provider, PhortuneCharge $charge, PhortuneCurrency $amount) { if (!$amount->isPositive()) { throw new Exception( pht('Trying to refund nonpositive amount of money!')); } if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) { throw new Exception( pht('Trying to refund more money than remaining on charge!')); } if ($charge->getRefundedChargePHID()) { throw new Exception( pht('Trying to refund a refund!')); } if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) && ($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) { throw new Exception( pht('Trying to refund an uncharged charge!')); } $refund_charge = PhortuneCharge::initializeNewCharge() ->setAccountPHID($this->getAccount()->getPHID()) ->setCartPHID($this->getPHID()) ->setAuthorPHID($actor->getPHID()) ->setMerchantPHID($this->getMerchant()->getPHID()) ->setProviderPHID($provider->getProviderConfig()->getPHID()) ->setPaymentMethodPHID($charge->getPaymentMethodPHID()) ->setRefundedChargePHID($charge->getPHID()) ->setAmountAsCurrency($amount->negate()); $charge->openTransaction(); $charge->beginReadLocking(); $copy = clone $charge; $copy->reload(); if ($copy->getRefundingPHID() !== null) { throw new Exception( pht('Trying to refund a charge which is already refunding!')); } $refund_charge->save(); $charge->setRefundingPHID($refund_charge->getPHID()); $charge->save(); $charge->endReadLocking(); $charge->saveTransaction(); return $refund_charge; } public function didRefundCharge( PhortuneCharge $charge, PhortuneCharge $refund) { $refund->setStatus(PhortuneCharge::STATUS_CHARGED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $charge; $copy->reload(); if ($charge->getRefundingPHID() !== $refund->getPHID()) { throw new Exception( pht('Charge is in the wrong refunding state!')); } $charge->setRefundingPHID(null); // NOTE: There's some trickiness here to get the signs right. Both // these values are positive but the refund has a negative value. $total_refunded = $charge ->getAmountRefundedAsCurrency() ->add($refund->getAmountAsCurrency()->negate()); $charge->setAmountRefundedAsCurrency($total_refunded); $charge->save(); $refund->save(); $this->endReadLocking(); $this->saveTransaction(); $amount = $refund->getAmountAsCurrency()->negate(); foreach ($this->purchases as $purchase) { $purchase->getProduct()->didRefundProduct($purchase, $amount); } return $this; } public function didFailRefund( PhortuneCharge $charge, PhortuneCharge $refund) { $refund->setStatus(PhortuneCharge::STATUS_FAILED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $charge; $copy->reload(); if ($charge->getRefundingPHID() !== $refund->getPHID()) { throw new Exception( pht('Charge is in the wrong refunding state!')); } $charge->setRefundingPHID(null); $charge->save(); $refund->save(); $this->endReadLocking(); $this->saveTransaction(); } private function recordCartTransaction($type) { $omnipotent_user = PhabricatorUser::getOmnipotentUser(); $phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID(); $xactions = array(); $xactions[] = id(new PhortuneCartTransaction()) ->setTransactionType($type) ->setNewValue(true); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_PHORTUNE, array()); $editor = id(new PhortuneCartEditor()) ->setActor($omnipotent_user) ->setActingAsPHID($phortune_phid) ->setContentSource($content_source) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($this, $xactions); } public function getName() { return $this->getImplementation()->getName($this); } public function getDoneURI() { return $this->getImplementation()->getDoneURI($this); } public function getDoneActionName() { return $this->getImplementation()->getDoneActionName($this); } public function getCancelURI() { return $this->getImplementation()->getCancelURI($this); } public function getDetailURI() { return '/phortune/cart/'.$this->getID().'/'; } public function getCheckoutURI() { return '/phortune/cart/'.$this->getID().'/checkout/'; } public function canCancelOrder() { try { $this->assertCanCancelOrder(); return true; } catch (Exception $ex) { return false; } } public function canRefundOrder() { try { $this->assertCanRefundOrder(); return true; } catch (Exception $ex) { return false; } } public function assertCanCancelOrder() { switch ($this->getStatus()) { case self::STATUS_BUILDING: throw new Exception( pht( 'This order can not be cancelled because the application has not '. 'finished building it yet.')); case self::STATUS_READY: throw new Exception( pht( 'This order can not be cancelled because it has not been placed.')); } return $this->getImplementation()->assertCanCancelOrder($this); } public function assertCanRefundOrder() { switch ($this->getStatus()) { case self::STATUS_BUILDING: throw new Exception( pht( 'This order can not be refunded because the application has not '. 'finished building it yet.')); case self::STATUS_READY: throw new Exception( pht( 'This order can not be refunded because it has not been placed.')); } return $this->getImplementation()->assertCanRefundOrder($this); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', 'cartClass' => 'text128', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_account' => array( 'columns' => array('accountPHID'), ), 'key_merchant' => array( 'columns' => array('merchantPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneCartPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function attachPurchases(array $purchases) { assert_instances_of($purchases, 'PhortunePurchase'); $this->purchases = $purchases; return $this; } public function getPurchases() { return $this->assertAttached($this->purchases); } public function attachAccount(PhortuneAccount $account) { $this->account = $account; return $this; } public function getAccount() { return $this->assertAttached($this->account); } public function attachMerchant(PhortuneMerchant $merchant) { $this->merchant = $merchant; return $this; } public function getMerchant() { return $this->assertAttached($this->merchant); } public function attachImplementation( PhortuneCartImplementation $implementation) { $this->implementation = $implementation; return $this; } public function getImplementation() { return $this->assertAttached($this->implementation); } public function getTotalPriceAsCurrency() { $prices = array(); foreach ($this->getPurchases() as $purchase) { $prices[] = $purchase->getTotalPriceAsCurrency(); } return PhortuneCurrency::newFromList($prices); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortuneCartEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhortuneCartTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // NOTE: Both view and edit use the account's edit policy. We punch a hole // through this for merchants, below. return $this ->getAccount() ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) { return true; } // If the viewer controls the merchant this order was placed with, they // can view the order. if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $can_admin = PhabricatorPolicyFilter::hasCapability( $viewer, $this->getMerchant(), PhabricatorPolicyCapability::CAN_EDIT); if ($can_admin) { return true; } } return false; } public function describeAutomaticCapability($capability) { return array( pht('Orders inherit the policies of the associated account.'), pht('The merchant you placed an order with can review and manage it.'), ); } } diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php index 48fb5bc09b..da199d0751 100644 --- a/src/applications/phortune/storage/PhortuneCharge.php +++ b/src/applications/phortune/storage/PhortuneCharge.php @@ -1,180 +1,180 @@ setStatus(self::STATUS_CHARGING) ->setAmountRefundedAsCurrency(PhortuneCurrency::newEmptyCurrency()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_APPLICATION_SERIALIZERS => array( 'amountAsCurrency' => new PhortuneCurrencySerializer(), 'amountRefundedAsCurrency' => new PhortuneCurrencySerializer(), ), self::CONFIG_COLUMN_SCHEMA => array( 'paymentMethodPHID' => 'phid?', 'refundedChargePHID' => 'phid?', 'refundingPHID' => 'phid?', 'amountAsCurrency' => 'text64', 'amountRefundedAsCurrency' => 'text64', 'status' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_cart' => array( 'columns' => array('cartPHID'), ), 'key_account' => array( 'columns' => array('accountPHID'), ), 'key_merchant' => array( 'columns' => array('merchantPHID'), ), 'key_provider' => array( 'columns' => array('providerPHID'), ), ), ) + parent::getConfiguration(); } public static function getStatusNameMap() { return array( self::STATUS_CHARGING => pht('Charging'), self::STATUS_CHARGED => pht('Charged'), self::STATUS_HOLD => pht('Hold'), self::STATUS_FAILED => pht('Failed'), ); } public static function getNameForStatus($status) { return idx(self::getStatusNameMap(), $status, pht('Unknown')); } public function isRefund() { return $this->getAmountAsCurrency()->negate()->isPositive(); } public function getStatusForDisplay() { if ($this->getStatus() == self::STATUS_CHARGED) { if ($this->getRefundedChargePHID()) { return pht('Refund'); } $refunded = $this->getAmountRefundedAsCurrency(); if ($refunded->isPositive()) { if ($refunded->isEqualTo($this->getAmountAsCurrency())) { return pht('Fully Refunded'); } else { return pht('%s Refunded', $refunded->formatForDisplay()); } } } return self::getNameForStatus($this->getStatus()); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneChargePHIDType::TYPECONST); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function getAccount() { return $this->assertAttached($this->account); } public function attachAccount(PhortuneAccount $account) { $this->account = $account; return $this; } public function getCart() { return $this->assertAttached($this->cart); } public function attachCart(PhortuneCart $cart = null) { $this->cart = $cart; return $this; } public function getAmountRefundableAsCurrency() { $amount = $this->getAmountAsCurrency(); $refunded = $this->getAmountRefundedAsCurrency(); // We can't refund negative amounts of money, since it does not make // sense and is not possible in the various payment APIs. $refundable = $amount->subtract($refunded); if ($refundable->isPositive()) { return $refundable; } else { return PhortuneCurrency::newEmptyCurrency(); } } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getAccount()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getAccount()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Charges inherit the policies of the associated account.'); } } diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php index ce4cd4466d..2f09abd6d7 100644 --- a/src/applications/phortune/storage/PhortuneMerchant.php +++ b/src/applications/phortune/storage/PhortuneMerchant.php @@ -1,100 +1,100 @@ setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->attachMemberPHIDs(array()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'description' => 'text', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneMerchantPHIDType::TYPECONST); } public function getMemberPHIDs() { return $this->assertAttached($this->memberPHIDs); } public function attachMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortuneMerchantEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhortuneMerchantTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $members = array_fuse($this->getMemberPHIDs()); if (isset($members[$viewer->getPHID()])) { return true; } return false; } public function describeAutomaticCapability($capability) { return pht("A merchant's members an always view and edit it."); } } diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php index 573f4b6c97..8044168ba4 100644 --- a/src/applications/phortune/storage/PhortunePaymentMethod.php +++ b/src/applications/phortune/storage/PhortunePaymentMethod.php @@ -1,157 +1,157 @@ true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'status' => 'text64', 'brand' => 'text64', 'expires' => 'text16', 'lastFourDigits' => 'text16', ), self::CONFIG_KEY_SCHEMA => array( 'key_account' => array( 'columns' => array('accountPHID', 'status'), ), 'key_merchant' => array( 'columns' => array('merchantPHID', 'accountPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortunePaymentMethodPHIDType::TYPECONST); } public function attachAccount(PhortuneAccount $account) { $this->account = $account; return $this; } public function getAccount() { return $this->assertAttached($this->account); } public function attachMerchant(PhortuneMerchant $merchant) { $this->merchant = $merchant; return $this; } public function getMerchant() { return $this->assertAttached($this->merchant); } public function attachProviderConfig(PhortunePaymentProviderConfig $config) { $this->providerConfig = $config; return $this; } public function getProviderConfig() { return $this->assertAttached($this->providerConfig); } public function getDescription() { $provider = $this->buildPaymentProvider(); return $provider->getPaymentMethodProviderDescription(); } public function getMetadataValue($key, $default = null) { return idx($this->getMetadata(), $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function buildPaymentProvider() { return $this->getProviderConfig()->buildProvider(); } public function getDisplayName() { if (strlen($this->name)) { return $this->name; } $provider = $this->buildPaymentProvider(); return $provider->getDefaultPaymentMethodDisplayName($this); } public function getFullDisplayName() { return pht('%s (%s)', $this->getDisplayName(), $this->getSummary()); } public function getSummary() { return pht('%s %s', $this->getBrand(), $this->getLastFourDigits()); } public function setExpires($year, $month) { $this->expires = $year.'-'.$month; return $this; } public function getDisplayExpires() { list($year, $month) = explode('-', $this->getExpires()); $month = sprintf('%02d', $month); $year = substr($year, -2); return $month.'/'.$year; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getAccount()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getAccount()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Members of an account can always view and edit its payment methods.'); } } diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php index 073c67b730..1b516cadb4 100644 --- a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php +++ b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php @@ -1,99 +1,99 @@ setMerchantPHID($merchant->getPHID()) ->setIsEnabled(1); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'providerClassKey' => 'bytes12', 'providerClass' => 'text128', 'isEnabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_merchant' => array( 'columns' => array('merchantPHID', 'providerClassKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function save() { $this->providerClassKey = PhabricatorHash::digestForIndex( $this->providerClass); return parent::save(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortunePaymentProviderPHIDType::TYPECONST); } public function attachMerchant(PhortuneMerchant $merchant) { $this->merchant = $merchant; return $this; } public function getMerchant() { return $this->assertAttached($this->merchant); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function buildProvider() { return newv($this->getProviderClass(), array()) ->setProviderConfig($this); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getMerchant()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getMerchant()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Providers have the policies of their merchant.'); } } diff --git a/src/applications/phortune/storage/PhortuneProduct.php b/src/applications/phortune/storage/PhortuneProduct.php index a7d952ed89..3b74cc8c18 100644 --- a/src/applications/phortune/storage/PhortuneProduct.php +++ b/src/applications/phortune/storage/PhortuneProduct.php @@ -1,112 +1,112 @@ true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'productClassKey' => 'bytes12', 'productClass' => 'text128', 'productRefKey' => 'bytes12', 'productRef' => 'text128', ), self::CONFIG_KEY_SCHEMA => array( 'key_product' => array( 'columns' => array('productClassKey', 'productRefKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneProductPHIDType::TYPECONST); } public static function initializeNewProduct() { return id(new PhortuneProduct()); } public function attachImplementation(PhortuneProductImplementation $impl) { $this->implementation = $impl; } public function getImplementation() { return $this->assertAttached($this->implementation); } public function save() { $this->productClassKey = PhabricatorHash::digestForIndex( $this->productClass); $this->productRefKey = PhabricatorHash::digestForIndex( $this->productRef); return parent::save(); } public function getPriceAsCurrency() { return $this->getImplementation()->getPriceAsCurrency($this); } public function getProductName() { return $this->getImplementation()->getName($this); } public function getPurchaseName(PhortunePurchase $purchase) { return $this->getImplementation()->getPurchaseName($this, $purchase); } public function didPurchaseProduct(PhortunePurchase $purchase) { return $this->getImplementation()->didPurchaseProduct($this, $purchase); } public function didRefundProduct( PhortunePurchase $purchase, PhortuneCurrency $amount) { return $this->getImplementation()->didRefundProduct( $this, $purchase, $amount); } /* -( 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/phortune/storage/PhortunePurchase.php b/src/applications/phortune/storage/PhortunePurchase.php index 538df69082..2c218ff296 100644 --- a/src/applications/phortune/storage/PhortunePurchase.php +++ b/src/applications/phortune/storage/PhortunePurchase.php @@ -1,125 +1,125 @@ setAuthorPHID($actor->getPHID()) ->setProductPHID($product->getPHID()) ->setQuantity(1) ->setStatus(self::STATUS_PENDING) ->setBasePriceAsCurrency($product->getPriceAsCurrency()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_APPLICATION_SERIALIZERS => array( 'basePriceAsCurrency' => new PhortuneCurrencySerializer(), ), self::CONFIG_COLUMN_SCHEMA => array( 'cartPHID' => 'phid?', 'basePriceAsCurrency' => 'text64', 'quantity' => 'uint32', 'status' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_cart' => array( 'columns' => array('cartPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortunePurchasePHIDType::TYPECONST); } public function attachCart(PhortuneCart $cart) { $this->cart = $cart; return $this; } public function getCart() { return $this->assertAttached($this->cart); } public function attachProduct(PhortuneProduct $product) { $this->product = $product; return $this; } public function getProduct() { return $this->assertAttached($this->product); } public function getFullDisplayName() { return $this->getProduct()->getPurchaseName($this); } public function getTotalPriceAsCurrency() { return $this->getBasePriceAsCurrency(); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getCart()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getCart()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Purchases have the policies of their cart.'); } } diff --git a/src/applications/phpast/storage/PhabricatorXHPASTViewParseTree.php b/src/applications/phpast/storage/PhabricatorXHPASTViewParseTree.php index 2171e46455..fa9adb62a2 100644 --- a/src/applications/phpast/storage/PhabricatorXHPASTViewParseTree.php +++ b/src/applications/phpast/storage/PhabricatorXHPASTViewParseTree.php @@ -1,19 +1,19 @@ array( 'authorPHID' => 'phid?', 'input' => 'text', 'stdout' => 'text', ), ) + parent::getConfiguration(); } } diff --git a/src/applications/phragment/storage/PhragmentFragment.php b/src/applications/phragment/storage/PhragmentFragment.php index 17da8ae69a..3f5719178b 100644 --- a/src/applications/phragment/storage/PhragmentFragment.php +++ b/src/applications/phragment/storage/PhragmentFragment.php @@ -1,351 +1,351 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'path' => 'text128', 'depth' => 'uint32', 'latestVersionPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_path' => array( 'columns' => array('path'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhragmentFragmentPHIDType::TYPECONST); } public function getURI() { return '/phragment/browse/'.$this->getPath(); } public function getName() { return basename($this->path); } public function getFile() { return $this->assertAttached($this->file); } public function attachFile(PhabricatorFile $file) { return $this->file = $file; } public function isDirectory() { return $this->latestVersionPHID === null; } public function isDeleted() { return $this->getLatestVersion()->getFilePHID() === null; } public function getLatestVersion() { if ($this->latestVersionPHID === null) { return null; } return $this->assertAttached($this->latestVersion); } public function attachLatestVersion(PhragmentFragmentVersion $version) { return $this->latestVersion = $version; } /* -( Updating ) --------------------------------------------------------- */ /** * Create a new fragment from a file. */ public static function createFromFile( PhabricatorUser $viewer, PhabricatorFile $file = null, $path, $view_policy, $edit_policy) { $fragment = id(new PhragmentFragment()); $fragment->setPath($path); $fragment->setDepth(count(explode('/', $path))); $fragment->setLatestVersionPHID(null); $fragment->setViewPolicy($view_policy); $fragment->setEditPolicy($edit_policy); $fragment->save(); // Directory fragments have no versions associated with them, so we // just return the fragment at this point. if ($file === null) { return $fragment; } if ($file->getMimeType() === 'application/zip') { $fragment->updateFromZIP($viewer, $file); } else { $fragment->updateFromFile($viewer, $file); } return $fragment; } /** * Set the specified file as the next version for the fragment. */ public function updateFromFile( PhabricatorUser $viewer, PhabricatorFile $file) { $existing = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($this->getPHID())) ->execute(); $sequence = count($existing); $this->openTransaction(); $version = id(new PhragmentFragmentVersion()); $version->setSequence($sequence); $version->setFragmentPHID($this->getPHID()); $version->setFilePHID($file->getPHID()); $version->save(); $this->setLatestVersionPHID($version->getPHID()); $this->save(); $this->saveTransaction(); $file->attachToObject($version->getPHID()); } /** * Apply the specified ZIP archive onto the fragment, removing * and creating fragments as needed. */ public function updateFromZIP( PhabricatorUser $viewer, PhabricatorFile $file) { if ($file->getMimeType() !== 'application/zip') { throw new Exception("File must have mimetype 'application/zip'"); } // First apply the ZIP as normal. $this->updateFromFile($viewer, $file); // Ensure we have ZIP support. $zip = null; try { $zip = new ZipArchive(); } catch (Exception $e) { // The server doesn't have php5-zip, so we can't do recursive updates. return; } $temp = new TempFile(); Filesystem::writeFile($temp, $file->loadFileData()); if (!$zip->open($temp)) { throw new Exception('Unable to open ZIP'); } // Get all of the paths and their data from the ZIP. $mappings = array(); for ($i = 0; $i < $zip->numFiles; $i++) { $path = trim($zip->getNameIndex($i), '/'); $stream = $zip->getStream($path); $data = null; // If the stream is false, then it is a directory entry. We leave // $data set to null for directories so we know not to create a // version entry for them. if ($stream !== false) { $data = stream_get_contents($stream); fclose($stream); } $mappings[$path] = $data; } // We need to detect any directories that are in the ZIP folder that // aren't explicitly noted in the ZIP. This can happen if the file // entries in the ZIP look like: // // * something/blah.png // * something/other.png // * test.png // // Where there is no explicit "something/" entry. foreach ($mappings as $path_key => $data) { if ($data === null) { continue; } $directory = dirname($path_key); while ($directory !== '.') { if (!array_key_exists($directory, $mappings)) { $mappings[$directory] = null; } if (dirname($directory) === $directory) { // dirname() will not reduce this directory any further; to // prevent infinite loop we just break out here. break; } $directory = dirname($directory); } } // Adjust the paths relative to this fragment so we can look existing // fragments up in the DB. $base_path = $this->getPath(); $paths = array(); foreach ($mappings as $p => $data) { $paths[] = $base_path.'/'.$p; } // FIXME: What happens when a child exists, but the current user // can't see it. We're going to create a new child with the exact // same path and then bad things will happen. $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($this->getPath().'/') ->execute(); $children = mpull($children, null, 'getPath'); // Iterate over the existing fragments. foreach ($children as $full_path => $child) { $path = substr($full_path, strlen($base_path) + 1); if (array_key_exists($path, $mappings)) { if ($child->isDirectory() && $mappings[$path] === null) { // Don't create a version entry for a directory // (unless it's been converted into a file). continue; } // The file is being updated. $file = PhabricatorFile::newFromFileData( $mappings[$path], array('name' => basename($path))); $child->updateFromFile($viewer, $file); } else { // The file is being deleted. $child->deleteFile($viewer); } } // Iterate over the mappings to find new files. foreach ($mappings as $path => $data) { if (!array_key_exists($base_path.'/'.$path, $children)) { // The file is being created. If the data is null, // then this is explicitly a directory being created. $file = null; if ($mappings[$path] !== null) { $file = PhabricatorFile::newFromFileData( $mappings[$path], array('name' => basename($path))); } PhragmentFragment::createFromFile( $viewer, $file, $base_path.'/'.$path, $this->getViewPolicy(), $this->getEditPolicy()); } } } /** * Delete the contents of the specified fragment. */ public function deleteFile(PhabricatorUser $viewer) { $existing = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($this->getPHID())) ->execute(); $sequence = count($existing); $this->openTransaction(); $version = id(new PhragmentFragmentVersion()); $version->setSequence($sequence); $version->setFragmentPHID($this->getPHID()); $version->setFilePHID(null); $version->save(); $this->setLatestVersionPHID($version->getPHID()); $this->save(); $this->saveTransaction(); } /* -( Utility ) ---------------------------------------------------------- */ public function getFragmentMappings( PhabricatorUser $viewer, $base_path) { $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($this->getPath().'/') ->withDepths(array($this->getDepth() + 1)) ->execute(); if (count($children) === 0) { $path = substr($this->getPath(), strlen($base_path) + 1); return array($path => $this); } else { $mappings = array(); foreach ($children as $child) { $child_mappings = $child->getFragmentMappings( $viewer, $base_path); foreach ($child_mappings as $key => $value) { $mappings[$key] = $value; } } return $mappings; } } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/phragment/storage/PhragmentFragmentVersion.php b/src/applications/phragment/storage/PhragmentFragmentVersion.php index c80faad7fe..88e501ee29 100644 --- a/src/applications/phragment/storage/PhragmentFragmentVersion.php +++ b/src/applications/phragment/storage/PhragmentFragmentVersion.php @@ -1,72 +1,72 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'sequence' => 'uint32', 'filePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_version' => array( 'columns' => array('fragmentPHID', 'sequence'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhragmentFragmentVersionPHIDType::TYPECONST); } public function getURI() { return '/phragment/version/'.$this->getID().'/'; } public function getFragment() { return $this->assertAttached($this->fragment); } public function attachFragment(PhragmentFragment $fragment) { return $this->fragment = $fragment; } public function getFile() { return $this->assertAttached($this->file); } public function attachFile(PhabricatorFile $file) { return $this->file = $file; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getFragment()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getFragment()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return $this->getFragment()->describeAutomaticCapability($capability); } } diff --git a/src/applications/phragment/storage/PhragmentSnapshot.php b/src/applications/phragment/storage/PhragmentSnapshot.php index eb2e063d03..53c5443c7a 100644 --- a/src/applications/phragment/storage/PhragmentSnapshot.php +++ b/src/applications/phragment/storage/PhragmentSnapshot.php @@ -1,76 +1,76 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('primaryFragmentPHID', 'name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhragmentSnapshotPHIDType::TYPECONST); } public function getURI() { return '/phragment/snapshot/view/'.$this->getID().'/'; } public function getPrimaryFragment() { return $this->assertAttached($this->primaryFragment); } public function attachPrimaryFragment(PhragmentFragment $fragment) { return $this->primaryFragment = $fragment; } public function delete() { $children = id(new PhragmentSnapshotChild()) ->loadAllWhere('snapshotPHID = %s', $this->getPHID()); $this->openTransaction(); foreach ($children as $child) { $child->delete(); } $result = parent::delete(); $this->saveTransaction(); return $result; } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return $this->getPrimaryFragment()->getCapabilities(); } public function getPolicy($capability) { return $this->getPrimaryFragment()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getPrimaryFragment() ->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return $this->getPrimaryFragment() ->describeAutomaticCapability($capability); } } diff --git a/src/applications/phragment/storage/PhragmentSnapshotChild.php b/src/applications/phragment/storage/PhragmentSnapshotChild.php index 85a604b2f0..3f0692ede6 100644 --- a/src/applications/phragment/storage/PhragmentSnapshotChild.php +++ b/src/applications/phragment/storage/PhragmentSnapshotChild.php @@ -1,82 +1,82 @@ array( 'fragmentVersionPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_child' => array( 'columns' => array( 'snapshotPHID', 'fragmentPHID', 'fragmentVersionPHID', ), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getSnapshot() { return $this->assertAttached($this->snapshot); } public function attachSnapshot(PhragmentSnapshot $snapshot) { return $this->snapshot = $snapshot; } public function getFragment() { return $this->assertAttached($this->fragment); } public function attachFragment(PhragmentFragment $fragment) { return $this->fragment = $fragment; } public function getFragmentVersion() { if ($this->fragmentVersionPHID === null) { return null; } return $this->assertAttached($this->fragmentVersion); } public function attachFragmentVersion(PhragmentFragmentVersion $version) { return $this->fragmentVersion = $version; } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getSnapshot()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getSnapshot() ->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return $this->getSnapshot() ->describeAutomaticCapability($capability); } } diff --git a/src/applications/phrequent/storage/PhrequentUserTime.php b/src/applications/phrequent/storage/PhrequentUserTime.php index 22d6c4657e..22f97a4eb8 100644 --- a/src/applications/phrequent/storage/PhrequentUserTime.php +++ b/src/applications/phrequent/storage/PhrequentUserTime.php @@ -1,77 +1,77 @@ array( 'objectPHID' => 'phid?', 'note' => 'text?', 'dateStarted' => 'epoch', 'dateEnded' => 'epoch?', ), ) + parent::getConfiguration(); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { $policy = PhabricatorPolicies::POLICY_NOONE; switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // Since it's impossible to perform any meaningful computations with // time if a user can't view some of it, visibility on tracked time is // unrestricted. If we eventually lock it down, it should be per-user. // (This doesn't mean that users can see tracked objects.) return PhabricatorPolicies::getMostOpenPolicy(); } return $policy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getUserPHID()); } public function describeAutomaticCapability($capability) { return null; } public function attachPreemptingEvents(array $events) { $this->preemptingEvents = $events; return $this; } public function getPreemptingEvents() { return $this->assertAttached($this->preemptingEvents); } public function isPreempted() { if ($this->getDateEnded() !== null) { return false; } foreach ($this->getPreemptingEvents() as $event) { if ($event->getDateEnded() === null && $event->getObjectPHID() != $this->getObjectPHID()) { return true; } } return false; } } diff --git a/src/applications/phriction/storage/PhrictionContent.php b/src/applications/phriction/storage/PhrictionContent.php index c41ec0f302..92518a87f8 100644 --- a/src/applications/phriction/storage/PhrictionContent.php +++ b/src/applications/phriction/storage/PhrictionContent.php @@ -1,125 +1,125 @@ array( 'version' => 'uint32', 'title' => 'sort', 'slug' => 'text128', 'content' => 'text', 'changeType' => 'uint32', 'changeRef' => 'uint32?', // T6203/NULLABILITY // This should just be empty if not provided? 'description' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'documentID' => array( 'columns' => array('documentID', 'version'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'slug' => array( 'columns' => array('slug'), ), ), ) + parent::getConfiguration(); } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { if ($this->shouldUseMarkupCache($field)) { $id = $this->getID(); } else { $id = PhabricatorHash::digest($this->getMarkupText($field)); } return "phriction:{$field}:{$id}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getContent(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhrictionMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { $toc = PhutilRemarkupHeaderBlockRule::renderTableOfContents( $engine); if ($toc) { $toc = phutil_tag_div('phabricator-remarkup-toc', array( phutil_tag_div( 'phabricator-remarkup-toc-header', pht('Table of Contents')), $toc, )); } return phutil_tag_div('phabricator-remarkup', array($toc, $output)); } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } } diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index a950852795..34b3bf6eca 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -1,255 +1,255 @@ true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'slug' => 'sort128', 'depth' => 'uint32', 'contentID' => 'id?', 'status' => 'uint32', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'slug' => array( 'columns' => array('slug'), 'unique' => true, ), 'depth' => array( 'columns' => array('depth', 'slug'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhrictionDocumentPHIDType::TYPECONST); } public static function initializeNewDocument(PhabricatorUser $actor, $slug) { $document = new PhrictionDocument(); $document->setSlug($slug); $content = new PhrictionContent(); $content->setSlug($slug); $default_title = PhabricatorSlug::getDefaultTitle($slug); $content->setTitle($default_title); $document->attachContent($content); $parent_doc = null; $ancestral_slugs = PhabricatorSlug::getAncestry($slug); if ($ancestral_slugs) { $parent = end($ancestral_slugs); $parent_doc = id(new PhrictionDocumentQuery()) ->setViewer($actor) ->withSlugs(array($parent)) ->executeOne(); } if ($parent_doc) { $document->setViewPolicy($parent_doc->getViewPolicy()); $document->setEditPolicy($parent_doc->getEditPolicy()); } else { $default_view_policy = PhabricatorPolicies::getMostOpenPolicy(); $document->setViewPolicy($default_view_policy); $document->setEditPolicy(PhabricatorPolicies::POLICY_USER); } return $document; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public static function getSlugURI($slug, $type = 'document') { static $types = array( 'document' => '/w/', 'history' => '/phriction/history/', ); if (empty($types[$type])) { throw new Exception("Unknown URI type '{$type}'!"); } $prefix = $types[$type]; if ($slug == '/') { return $prefix; } else { // NOTE: The effect here is to escape non-latin characters, since modern // browsers deal with escaped UTF8 characters in a reasonable way (showing // the user a readable URI) but older programs may not. $slug = phutil_escape_uri($slug); return $prefix.$slug; } } public function setSlug($slug) { $this->slug = PhabricatorSlug::normalize($slug); $this->depth = PhabricatorSlug::getDepth($slug); return $this; } public function attachContent(PhrictionContent $content) { $this->contentObject = $content; return $this; } public function getContent() { return $this->assertAttached($this->contentObject); } public function getAncestors() { return $this->ancestors; } public function getAncestor($slug) { return $this->assertAttachedKey($this->ancestors, $slug); } public function attachAncestor($slug, $ancestor) { $this->ancestors[$slug] = $ancestor; 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->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht( 'To view a wiki document, you must also be able to view all '. 'of its parents.'); case PhabricatorPolicyCapability::CAN_EDIT: return pht( 'To edit a wiki document, you must also be able to view all '. 'of its parents.'); } return null; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhrictionTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhrictionTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return PhabricatorSubscribersQuery::loadSubscribersForPHID($this->phid); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $contents = id(new PhrictionContent())->loadAllWhere( 'documentID = %d', $this->getID()); foreach ($contents as $content) { $content->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php index ed3245f3c7..31624a922b 100644 --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -1,364 +1,364 @@ true, self::CONFIG_SERIALIZATION => array( 'rules' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'defaultAction' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPolicyPHIDTypePolicy::TYPECONST); } public static function newFromPolicyAndHandle( $policy_identifier, PhabricatorObjectHandle $handle = null) { $is_global = PhabricatorPolicyQuery::isGlobalPolicy($policy_identifier); if ($is_global) { return PhabricatorPolicyQuery::getGlobalPolicy($policy_identifier); } if (!$handle) { throw new Exception( "Policy identifier is an object PHID ('{$policy_identifier}'), but no ". "object handle was provided. A handle must be provided for object ". "policies."); } $handle_phid = $handle->getPHID(); if ($policy_identifier != $handle_phid) { throw new Exception( "Policy identifier is an object PHID ('{$policy_identifier}'), but ". "the provided handle has a different PHID ('{$handle_phid}'). The ". "handle must correspond to the policy identifier."); } $policy = id(new PhabricatorPolicy()) ->setPHID($policy_identifier) ->setHref($handle->getURI()); $phid_type = phid_get_type($policy_identifier); switch ($phid_type) { case PhabricatorProjectProjectPHIDType::TYPECONST: $policy->setType(PhabricatorPolicyType::TYPE_PROJECT); $policy->setName($handle->getName()); break; case PhabricatorPeopleUserPHIDType::TYPECONST: $policy->setType(PhabricatorPolicyType::TYPE_USER); $policy->setName($handle->getFullName()); break; case PhabricatorPolicyPHIDTypePolicy::TYPECONST: // TODO: This creates a weird handle-based version of a rule policy. // It behaves correctly, but can't be applied since it doesn't have // any rules. It is used to render transactions, and might need some // cleanup. break; default: $policy->setType(PhabricatorPolicyType::TYPE_MASKED); $policy->setName($handle->getFullName()); break; } $policy->makeEphemeral(); return $policy; } public function setType($type) { $this->type = $type; return $this; } public function getType() { if (!$this->type) { return PhabricatorPolicyType::TYPE_CUSTOM; } return $this->type; } public function setName($name) { $this->name = $name; return $this; } public function getName() { if (!$this->name) { return pht('Custom Policy'); } return $this->name; } public function setShortName($short_name) { $this->shortName = $short_name; return $this; } public function getShortName() { if ($this->shortName) { return $this->shortName; } return $this->getName(); } public function setHref($href) { $this->href = $href; return $this; } public function getHref() { return $this->href; } public function setWorkflow($workflow) { $this->workflow = $workflow; return $this; } public function getWorkflow() { return $this->workflow; } public function getIcon() { switch ($this->getType()) { case PhabricatorPolicyType::TYPE_GLOBAL: static $map = array( PhabricatorPolicies::POLICY_PUBLIC => 'fa-globe', PhabricatorPolicies::POLICY_USER => 'fa-users', PhabricatorPolicies::POLICY_ADMIN => 'fa-eye', PhabricatorPolicies::POLICY_NOONE => 'fa-ban', ); return idx($map, $this->getPHID(), 'fa-question-circle'); case PhabricatorPolicyType::TYPE_USER: return 'fa-user'; case PhabricatorPolicyType::TYPE_PROJECT: return 'fa-briefcase'; case PhabricatorPolicyType::TYPE_CUSTOM: case PhabricatorPolicyType::TYPE_MASKED: return 'fa-certificate'; default: return 'fa-question-circle'; } } public function getSortKey() { return sprintf( '%02d%s', PhabricatorPolicyType::getPolicyTypeOrder($this->getType()), $this->getSortName()); } private function getSortName() { if ($this->getType() == PhabricatorPolicyType::TYPE_GLOBAL) { static $map = array( PhabricatorPolicies::POLICY_PUBLIC => 0, PhabricatorPolicies::POLICY_USER => 1, PhabricatorPolicies::POLICY_ADMIN => 2, PhabricatorPolicies::POLICY_NOONE => 3, ); return idx($map, $this->getPHID()); } return $this->getName(); } public static function getPolicyExplanation( PhabricatorUser $viewer, $policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('This object is public.'); case PhabricatorPolicies::POLICY_USER: return pht('Logged in users can take this action.'); case PhabricatorPolicies::POLICY_ADMIN: return pht('Administrators can take this action.'); case PhabricatorPolicies::POLICY_NOONE: return pht('By default, no one can take this action.'); default: $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($policy)) ->executeOne(); $type = phid_get_type($policy); if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) { return pht( 'Members of the project "%s" can take this action.', $handle->getFullName()); } else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) { return pht( '%s can take this action.', $handle->getFullName()); } else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { return pht( 'This object has a custom policy controlling who can take this '. 'action.'); } else { return pht( 'This object has an unknown or invalid policy setting ("%s").', $policy); } } } public function getFullName() { switch ($this->getType()) { case PhabricatorPolicyType::TYPE_PROJECT: return pht('Project: %s', $this->getName()); case PhabricatorPolicyType::TYPE_MASKED: return pht('Other: %s', $this->getName()); default: return $this->getName(); } } public function renderDescription($icon = false) { $img = null; if ($icon) { $img = id(new PHUIIconView()) ->setIconFont($this->getIcon()); } if ($this->getHref()) { $desc = javelin_tag( 'a', array( 'href' => $this->getHref(), 'class' => 'policy-link', 'sigil' => $this->getWorkflow() ? 'workflow' : null, ), array( $img, $this->getName(), )); } else { if ($img) { $desc = array($img, $this->getName()); } else { $desc = $this->getName(); } } switch ($this->getType()) { case PhabricatorPolicyType::TYPE_PROJECT: return pht('%s (Project)', $desc); case PhabricatorPolicyType::TYPE_CUSTOM: return $desc; case PhabricatorPolicyType::TYPE_MASKED: return pht( '%s (You do not have permission to view policy details.)', $desc); default: return $desc; } } /** * Return a list of custom rule classes (concrete subclasses of * @{class:PhabricatorPolicyRule}) this policy uses. * * @return list List of class names. */ public function getCustomRuleClasses() { $classes = array(); foreach ($this->getRules() as $rule) { $class = idx($rule, 'rule'); try { if (class_exists($class)) { $classes[$class] = $class; } } catch (Exception $ex) { continue; } } return array_keys($classes); } /** * Return a list of all values used by a given rule class to implement this * policy. This is used to bulk load data (like project memberships) in order * to apply policy filters efficiently. * * @param string Policy rule classname. * @return list List of values used in this policy. */ public function getCustomRuleValues($rule_class) { $values = array(); foreach ($this->getRules() as $rule) { if ($rule['rule'] == $rule_class) { $values[] = $rule['value']; } } return $values; } public function attachRuleObjects(array $objects) { $this->ruleObjects = $objects; return $this; } public function getRuleObjects() { return $this->assertAttached($this->ruleObjects); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // NOTE: We implement policies only so we can comply with the interface. // The actual query skips them, as enforcing policies on policies seems // perilous and isn't currently required by the application. return PhabricatorPolicies::POLICY_PUBLIC; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index e06435a7d2..57e0e2ea21 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -1,261 +1,261 @@ question = $question; return $this; } public function getQuestion() { return $this->assertAttached($this->question); } public function getURI() { return '/Q'.$this->getQuestionID().'#A'.$this->getID(); } public function setUserVote($vote) { $this->vote = $vote['data']; if (!$this->vote) { $this->vote = PonderVote::VOTE_NONE; } return $this; } public function attachUserVote($user_phid, $vote) { $this->vote = $vote; return $this; } public function getUserVote() { return $this->vote; } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'voteCount' => 'sint32', 'content' => 'text', // T6203/NULLABILITY // This should always exist. 'contentSource' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'key_oneanswerperquestion' => array( 'columns' => array('questionID', 'authorPHID'), 'unique' => true, ), 'questionID' => array( 'columns' => array('questionID'), ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderAnswerPHIDType::TYPECONST); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PonderAnswerEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PonderAnswerTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } // Markup interface public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "ponder:A{$id}:{$field}:{$hash}"; } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } // votable interface public function getUserVoteEdgeType() { return PonderVotingUserHasAnswerEdgeType::EDGECONST; } public function getVotablePHID() { return $this->getPHID(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getQuestion()->getPolicy($capability); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->getAuthorPHID() == $viewer->getPHID()) { return true; } return $this->getQuestion()->hasAutomaticCapability( $capability, $viewer); case PhabricatorPolicyCapability::CAN_EDIT: return ($this->getAuthorPHID() == $viewer->getPHID()); } } public function describeAutomaticCapability($capability) { $out = array(); $out[] = pht('The author of an answer can always view and edit it.'); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $out[] = pht( 'The user who asks a question can always view the answers.'); break; } return $out; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index e2db7c0d48..8c8dab3f44 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -1,302 +1,302 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'voteCount' => 'sint32', 'status' => 'uint32', 'content' => 'text', 'heat' => 'double', 'answerCount' => 'uint32', 'mailKey' => 'bytes20', // T6203/NULLABILITY // This should always exist. 'contentSource' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'heat' => array( 'columns' => array('heat'), ), 'status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function attachVotes($user_phid) { $qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID()); $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($user_phid)) ->withDestinationPHIDs($qa_phids) ->withEdgeTypes( array( PonderVotingUserHasQuestionEdgeType::EDGECONST, PonderVotingUserHasAnswerEdgeType::EDGECONST, )) ->needEdgeData(true) ->execute(); $question_edge = $edges[$user_phid][PonderVotingUserHasQuestionEdgeType::EDGECONST]; $answer_edges = $edges[$user_phid][PonderVotingUserHasAnswerEdgeType::EDGECONST]; $edges = null; $this->setUserVote(idx($question_edge, $this->getPHID())); foreach ($this->answers as $answer) { $answer->setUserVote(idx($answer_edges, $answer->getPHID())); } } public function setUserVote($vote) { $this->vote = $vote['data']; if (!$this->vote) { $this->vote = PonderVote::VOTE_NONE; } return $this; } public function attachUserVote($user_phid, $vote) { $this->vote = $vote; return $this; } public function getUserVote() { return $this->vote; } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } public function attachAnswers(array $answers) { assert_instances_of($answers, 'PonderAnswer'); $this->answers = $answers; return $this; } public function getAnswers() { return $this->answers; } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PonderQuestionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PonderQuestionTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } // Markup interface public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "ponder:Q{$id}:{$field}:{$hash}"; } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } // votable interface public function getUserVoteEdgeType() { return PonderVotingUserHasQuestionEdgeType::EDGECONST; } public function getVotablePHID() { return $this->getPHID(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getOriginalTitle() { // TODO: Make this actually save/return the original title. return $this->getTitle(); } public function getFullTitle() { $id = $this->getID(); $title = $this->getTitle(); return "Q{$id}: {$title}"; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { $policy = PhabricatorPolicies::POLICY_NOONE; switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $policy = PhabricatorPolicies::POLICY_USER; break; } return $policy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht( 'The user who asked a question 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(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $answers = id(new PonderAnswer())->loadAllWhere( 'questionID = %d', $this->getID()); foreach ($answers as $answer) { $engine->destroyObject($answer); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 0274bd2930..6f02307404 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -1,409 +1,409 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withClasses(array('PhabricatorProjectApplication')) ->executeOne(); $view_policy = $app->getPolicy( ProjectDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( ProjectDefaultEditCapability::CAPABILITY); $join_policy = $app->getPolicy( ProjectDefaultJoinCapability::CAPABILITY); return id(new PhabricatorProject()) ->setName('') ->setAuthorPHID($actor->getPHID()) ->setIcon(self::DEFAULT_ICON) ->setColor(self::DEFAULT_COLOR) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setJoinPolicy($join_policy) ->setIsMembershipLocked(0) ->attachMemberPHIDs(array()) ->attachSlugs(array()); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_JOIN, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case PhabricatorPolicyCapability::CAN_JOIN: return $this->getJoinPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isUserMember($viewer->getPHID())) { // Project members can always view a project. return true; } break; case PhabricatorPolicyCapability::CAN_EDIT: break; case PhabricatorPolicyCapability::CAN_JOIN: $can_edit = PhabricatorPolicyCapability::CAN_EDIT; if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) { // Project editors can always join a project. return true; } break; } return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Members of a project can always view it.'); case PhabricatorPolicyCapability::CAN_JOIN: return pht('Users who can edit a project can always join it.'); } return null; } public function isUserMember($user_phid) { if ($this->memberPHIDs !== self::ATTACHABLE) { return in_array($user_phid, $this->memberPHIDs); } return $this->assertAttachedKey($this->sparseMembers, $user_phid); } public function setIsUserMember($user_phid, $is_member) { if ($this->sparseMembers === self::ATTACHABLE) { $this->sparseMembers = array(); } $this->sparseMembers[$user_phid] = $is_member; return $this; } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'subprojectPHIDs' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'status' => 'text32', 'phrictionSlug' => 'text128?', 'isMembershipLocked' => 'bool', 'profileImagePHID' => 'phid?', 'icon' => 'text32', 'color' => 'text32', // T6203/NULLABILITY // These are definitely wrong and should always exist. 'editPolicy' => 'policy?', 'viewPolicy' => 'policy?', 'joinPolicy' => 'policy?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'key_icon' => array( 'columns' => array('icon'), ), 'key_color' => array( 'columns' => array('color'), ), 'phrictionSlug' => array( 'columns' => array('phrictionSlug'), 'unique' => true, ), 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProjectProjectPHIDType::TYPECONST); } public function attachMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } public function getMemberPHIDs() { return $this->assertAttached($this->memberPHIDs); } public function setPhrictionSlug($slug) { // NOTE: We're doing a little magic here and stripping out '/' so that // project pages always appear at top level under projects/ even if the // display name is "Hack / Slash" or similar (it will become // 'hack_slash' instead of 'hack/slash'). $slug = str_replace('/', ' ', $slug); $slug = PhabricatorSlug::normalize($slug); $this->phrictionSlug = $slug; return $this; } public function getFullPhrictionSlug() { $slug = $this->getPhrictionSlug(); return 'projects/'.$slug; } // TODO - once we sever project => phriction automagicalness, // migrate getPhrictionSlug to have no trailing slash and be called // getPrimarySlug public function getPrimarySlug() { $slug = $this->getPhrictionSlug(); return rtrim($slug, '/'); } public function isArchived() { return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED); } public function getProfileImageURI() { return $this->getProfileImageFile()->getBestURI(); } public function attachProfileImageFile(PhabricatorFile $file) { $this->profileImageFile = $file; return $this; } public function getProfileImageFile() { return $this->assertAttached($this->profileImageFile); } public function isUserWatcher($user_phid) { if ($this->watcherPHIDs !== self::ATTACHABLE) { return in_array($user_phid, $this->watcherPHIDs); } return $this->assertAttachedKey($this->sparseWatchers, $user_phid); } public function setIsUserWatcher($user_phid, $is_watcher) { if ($this->sparseWatchers === self::ATTACHABLE) { $this->sparseWatchers = array(); } $this->sparseWatchers[$user_phid] = $is_watcher; return $this; } public function attachWatcherPHIDs(array $phids) { $this->watcherPHIDs = $phids; return $this; } public function getWatcherPHIDs() { return $this->assertAttached($this->watcherPHIDs); } public function attachSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function getSlugs() { return $this->assertAttached($this->slugs); } public function getColor() { if ($this->isArchived()) { return PHUITagView::COLOR_DISABLED; } return $this->color; } public function save() { $this->openTransaction(); $result = parent::save(); $this->updateDatasourceTokens(); $this->saveTransaction(); return $result; } public function updateDatasourceTokens() { $table = self::TABLE_DATASOURCE_TOKEN; $conn_w = $this->establishConnection('w'); $id = $this->getID(); $slugs = queryfx_all( $conn_w, 'SELECT * FROM %T WHERE projectPHID = %s', id(new PhabricatorProjectSlug())->getTableName(), $this->getPHID()); $all_strings = ipull($slugs, 'slug'); $all_strings[] = $this->getName(); $all_strings = implode(' ', $all_strings); $tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings); $sql = array(); foreach ($tokens as $token) { $sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token); } $this->openTransaction(); queryfx( $conn_w, 'DELETE FROM %T WHERE projectID = %d', $table, $id); foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (projectID, token) VALUES %Q', $table, $chunk); } $this->saveTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } public function shouldShowSubscribersProperty() { return false; } public function shouldAllowSubscription($phid) { return $this->isUserMember($phid) && !$this->isUserWatcher($phid); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('projects.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorProjectCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProjectTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorProjectTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $columns = id(new PhabricatorProjectColumn()) ->loadAllWhere('projectPHID = %s', $this->getPHID()); foreach ($columns as $column) { $engine->destroyObject($column); } $slugs = id(new PhabricatorProjectSlug()) ->loadAllWhere('projectPHID = %s', $this->getPHID()); foreach ($slugs as $slug) { $slug->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index de6a1cc35d..8fcb6ade9c 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -1,197 +1,197 @@ setName('') ->setStatus(self::STATUS_ACTIVE); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'status' => 'uint32', 'sequence' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('projectPHID', 'status', 'sequence'), ), 'key_sequence' => array( 'columns' => array('projectPHID', 'sequence'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProjectColumnPHIDType::TYPECONST); } public function attachProject(PhabricatorProject $project) { $this->project = $project; return $this; } public function getProject() { return $this->assertAttached($this->project); } public function isDefaultColumn() { return (bool)$this->getProperty('isDefault'); } public function isHidden() { return ($this->getStatus() == self::STATUS_HIDDEN); } public function getDisplayName() { $name = $this->getName(); if (strlen($name)) { return $name; } if ($this->isDefaultColumn()) { return pht('Backlog'); } return pht('Unnamed Column'); } public function getDisplayType() { if ($this->isDefaultColumn()) { return pht('(Default)'); } if ($this->isHidden()) { return pht('(Hidden)'); } return null; } public function getHeaderIcon() { $icon = null; if ($this->isHidden()) { $icon = 'fa-eye-slash'; $text = pht('Hidden'); } if ($icon) { return id(new PHUIIconView()) ->setIconFont($icon) ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => $text, ));; } return null; } 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 getPointLimit() { return $this->getProperty('pointLimit'); } public function setPointLimit($limit) { $this->setProperty('pointLimit', $limit); return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProjectColumnTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorProjectColumnTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getProject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getProject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Users must be able to see a project to see its board.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/project/storage/PhabricatorProjectColumnPosition.php b/src/applications/project/storage/PhabricatorProjectColumnPosition.php index 835692dadd..7abcd7e372 100644 --- a/src/applications/project/storage/PhabricatorProjectColumnPosition.php +++ b/src/applications/project/storage/PhabricatorProjectColumnPosition.php @@ -1,77 +1,77 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'sequence' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'boardPHID' => array( 'columns' => array('boardPHID', 'columnPHID', 'objectPHID'), 'unique' => true, ), 'objectPHID' => array( 'columns' => array('objectPHID', 'boardPHID'), ), 'boardPHID_2' => array( 'columns' => array('boardPHID', 'columnPHID', 'sequence'), ), ), ) + parent::getConfiguration(); } public function getColumn() { return $this->assertAttached($this->column); } public function attachColumn(PhabricatorProjectColumn $column) { $this->column = $column; return $this; } public function getOrderingKey() { // Low sequence numbers go above high sequence numbers. // High position IDs go above low position IDs. // Broadly, this makes newly added stuff float to the top. return sprintf( '~%012d%012d', $this->getSequence(), ((1 << 31) - $this->getID())); } /* -( 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/project/storage/PhabricatorProjectSlug.php b/src/applications/project/storage/PhabricatorProjectSlug.php index 1f13ba6635..34e6f572d1 100644 --- a/src/applications/project/storage/PhabricatorProjectSlug.php +++ b/src/applications/project/storage/PhabricatorProjectSlug.php @@ -1,25 +1,25 @@ array( 'slug' => 'text128', ), self::CONFIG_KEY_SCHEMA => array( 'key_slug' => array( 'columns' => array('slug'), 'unique' => true, ), 'key_projectPHID' => array( 'columns' => array('projectPHID'), ), ), ) + parent::getConfiguration(); } } diff --git a/src/applications/releeph/storage/ReleephBranch.php b/src/applications/releeph/storage/ReleephBranch.php index c4f4fabc04..d8fade3470 100644 --- a/src/applications/releeph/storage/ReleephBranch.php +++ b/src/applications/releeph/storage/ReleephBranch.php @@ -1,199 +1,199 @@ true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'basename' => 'text64', 'isActive' => 'bool', 'symbolicName' => 'text64?', 'name' => 'text128', ), self::CONFIG_KEY_SCHEMA => array( 'releephProjectID' => array( 'columns' => array('releephProjectID', 'symbolicName'), 'unique' => true, ), 'releephProjectID_2' => array( 'columns' => array('releephProjectID', 'basename'), 'unique' => true, ), 'releephProjectID_name' => array( 'columns' => array('releephProjectID', 'name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ReleephBranchPHIDType::TYPECONST); } public function getDetail($key, $default = null) { return idx($this->getDetails(), $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function willWriteData(array &$data) { // If symbolicName is omitted, set it to the basename. // // This means that we can enforce symbolicName as a UNIQUE column in the // DB. We'll interpret symbolicName === basename as meaning "no symbolic // name". // // SYMBOLIC_NAME_NOTE if (!$data['symbolicName']) { $data['symbolicName'] = $data['basename']; } parent::willWriteData($data); } public function getSymbolicName() { // See SYMBOLIC_NAME_NOTE above for why this is needed if ($this->symbolicName == $this->getBasename()) { return ''; } return $this->symbolicName; } public function setSymbolicName($name) { if ($name) { parent::setSymbolicName($name); } else { parent::setSymbolicName($this->getBasename()); } return $this; } public function getDisplayName() { if ($sn = $this->getSymbolicName()) { return $sn; } return $this->getBasename(); } public function getDisplayNameWithDetail() { $n = $this->getBasename(); if ($sn = $this->getSymbolicName()) { return "{$sn} ({$n})"; } else { return $n; } } public function getURI($path = null) { $components = array( '/releeph/branch', $this->getID(), $path, ); return implode('/', $components); } public function isActive() { return $this->getIsActive(); } public function attachProject(ReleephProject $project) { $this->project = $project; return $this; } public function getProject() { return $this->assertAttached($this->project); } public function getProduct() { return $this->getProject(); } public function attachCutPointCommit( PhabricatorRepositoryCommit $commit = null) { $this->cutPointCommit = $commit; return $this; } public function getCutPointCommit() { return $this->assertAttached($this->cutPointCommit); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ReleephBranchEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new ReleephBranchTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return $this->getProduct()->getCapabilities(); } public function getPolicy($capability) { return $this->getProduct()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getProduct()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Release branches have the same policies as the product they are a '. 'part of.'); } } diff --git a/src/applications/releeph/storage/ReleephProject.php b/src/applications/releeph/storage/ReleephProject.php index d5270f4024..ae34894658 100644 --- a/src/applications/releeph/storage/ReleephProject.php +++ b/src/applications/releeph/storage/ReleephProject.php @@ -1,173 +1,173 @@ true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'trunkBranch' => 'text255', 'isActive' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'projectName' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ReleephProductPHIDType::TYPECONST); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function getURI($path = null) { $components = array( '/releeph/product', $this->getID(), $path, ); return implode('/', $components); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getArcanistProject() { return $this->assertAttached($this->arcanistProject); } public function attachArcanistProject( PhabricatorRepositoryArcanistProject $arcanist_project = null) { $this->arcanistProject = $arcanist_project; return $this; } public function getPushers() { return $this->getDetail('pushers', array()); } public function isPusher(PhabricatorUser $user) { // TODO Deprecate this once `isPusher` is out of the Facebook codebase. return $this->isAuthoritative($user); } public function isAuthoritative(PhabricatorUser $user) { return $this->isAuthoritativePHID($user->getPHID()); } public function isAuthoritativePHID($phid) { $pushers = $this->getPushers(); if (!$pushers) { return true; } else { return in_array($phid, $pushers); } } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function getReleephFieldSelector() { return new ReleephDefaultFieldSelector(); } public function isTestFile($filename) { $test_paths = $this->getDetail('testPaths', array()); foreach ($test_paths as $test_path) { if (preg_match($test_path, $filename)) { return true; } } return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ReleephProductEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new ReleephProductTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index 3eba151048..7f3f190a6d 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -1,373 +1,373 @@ getPusherIntent() == self::INTENT_WANT && /** * We use "!= pass" instead of "== want" in case the requestor intent is * not present. In other words, only revert if the requestor explicitly * passed. */ $this->getRequestorIntent() != self::INTENT_PASS; } /** * Will return INTENT_WANT if any pusher wants this request, and no pusher * passes on this request. */ public function getPusherIntent() { $product = $this->getBranch()->getProduct(); if (!$product->getPushers()) { return self::INTENT_WANT; } $found_pusher_want = false; foreach ($this->userIntents as $phid => $intent) { if ($product->isAuthoritativePHID($phid)) { if ($intent == self::INTENT_PASS) { return self::INTENT_PASS; } $found_pusher_want = true; } } if ($found_pusher_want) { return self::INTENT_WANT; } else { return null; } } public function getRequestorIntent() { return idx($this->userIntents, $this->requestUserPHID); } public function getStatus() { return $this->calculateStatus(); } public function getMonogram() { return 'Y'.$this->getID(); } public function getBranch() { return $this->assertAttached($this->branch); } public function attachBranch(ReleephBranch $branch) { $this->branch = $branch; return $this; } public function getRequestedObject() { return $this->assertAttached($this->requestedObject); } public function attachRequestedObject($object) { $this->requestedObject = $object; return $this; } private function calculateStatus() { if ($this->shouldBeInBranch()) { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_PICKED; } else { return ReleephRequestStatus::STATUS_NEEDS_PICK; } } else { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_NEEDS_REVERT; } else { $intent_pass = ReleephRequest::INTENT_PASS; $intent_want = ReleephRequest::INTENT_WANT; $has_been_in_branch = $this->getCommitIdentifier(); // Regardless of why we reverted something, always say reverted if it // was once in the branch. if ($has_been_in_branch) { return ReleephRequestStatus::STATUS_REVERTED; } else if ($this->getPusherIntent() === $intent_pass) { // Otherwise, if it has never been in the branch, explicitly say why: return ReleephRequestStatus::STATUS_REJECTED; } else if ($this->getRequestorIntent() === $intent_want) { return ReleephRequestStatus::STATUS_REQUESTED; } else { return ReleephRequestStatus::STATUS_ABANDONED; } } } } /* -( Lisk mechanics )----------------------------------------------------- */ - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'userIntents' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'requestCommitPHID' => 'phid?', 'commitIdentifier' => 'text40?', 'commitPHID' => 'phid?', 'pickStatus' => 'uint32?', 'inBranch' => 'bool', 'mailKey' => 'bytes20', 'userIntents' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'requestIdentifierBranch' => array( 'columns' => array('requestCommitPHID', 'branchID'), 'unique' => true, ), 'branchID' => array( 'columns' => array('branchID'), ), 'key_requestedObject' => array( 'columns' => array('requestedObjectPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( ReleephRequestPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( Helpful accessors )--------------------------------------------------- */ public function getDetail($key, $default = null) { return idx($this->getDetails(), $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } /** * Get the commit PHIDs this request is requesting. * * NOTE: For now, this always returns one PHID. * * @return list Commit PHIDs requested by this request. */ public function getCommitPHIDs() { return array( $this->requestCommitPHID, ); } public function getReason() { // Backward compatibility: reason used to be called comments $reason = $this->getDetail('reason'); if (!$reason) { return $this->getDetail('comments'); } return $reason; } /** * Allow a null summary, and fall back to the title of the commit. */ public function getSummaryForDisplay() { $summary = $this->getDetail('summary'); if (!strlen($summary)) { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { $summary = $commit->getSummary(); } } if (!strlen($summary)) { $summary = pht('None'); } return $summary; } /* -( Loading external objects )------------------------------------------- */ public function loadPhabricatorRepositoryCommit() { return $this->loadOneRelative( new PhabricatorRepositoryCommit(), 'phid', 'getRequestCommitPHID'); } public function loadPhabricatorRepositoryCommitData() { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { return $commit->loadOneRelative( new PhabricatorRepositoryCommitData(), 'commitID'); } } /* -( State change helpers )----------------------------------------------- */ public function setUserIntent(PhabricatorUser $user, $intent) { $this->userIntents[$user->getPHID()] = $intent; return $this; } /* -( Migrating to status-less ReleephRequests )--------------------------- */ protected function didReadData() { if ($this->userIntents === null) { $this->userIntents = array(); } } public function setStatus($value) { throw new Exception('`status` is now deprecated!'); } /* -( Make magic Lisk methods private )------------------------------------ */ private function setUserIntents(array $ar) { return parent::setUserIntents($ar); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ReleephRequestTransactionalEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new ReleephRequestTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBranch()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBranch()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Pull requests have the same policies as the branches they are '. 'requested against.'); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('releeph.fields'); } public function getCustomFieldBaseClass() { return 'ReleephFieldSpecification'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 90c37e2002..56fa9db3de 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1,1618 +1,1618 @@ setViewer($actor) ->withClasses(array('PhabricatorDiffusionApplication')) ->executeOne(); $view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY); $push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY); return id(new PhabricatorRepository()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setPushPolicy($push_policy); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'callsign' => 'sort32', 'versionControlSystem' => 'text32', 'uuid' => 'text64?', 'pushPolicy' => 'policy', 'credentialPHID' => 'phid?', 'almanacServicePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'callsign' => array( 'columns' => array('callsign'), 'unique' => true, ), 'key_name' => array( 'columns' => array('name(128)'), ), 'key_vcs' => array( 'columns' => array('versionControlSystem'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryRepositoryPHIDType::TYPECONST); } public function toDictionary() { return array( 'id' => $this->getID(), 'name' => $this->getName(), 'phid' => $this->getPHID(), 'callsign' => $this->getCallsign(), 'monogram' => $this->getMonogram(), 'vcs' => $this->getVersionControlSystem(), 'uri' => PhabricatorEnv::getProductionURI($this->getURI()), 'remoteURI' => (string)$this->getRemoteURI(), 'description' => $this->getDetail('description'), 'isActive' => $this->isTracked(), 'isHosted' => $this->isHosted(), 'isImporting' => $this->isImporting(), ); } public function getMonogram() { return 'r'.$this->getCallsign(); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function getHumanReadableDetail($key, $default = null) { $value = $this->getDetail($key, $default); switch ($key) { case 'branch-filter': case 'close-commits-filter': $value = array_keys($value); $value = implode(', ', $value); break; } return $value; } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function attachCommitCount($count) { $this->commitCount = $count; return $this; } public function getCommitCount() { return $this->assertAttached($this->commitCount); } public function attachMostRecentCommit( PhabricatorRepositoryCommit $commit = null) { $this->mostRecentCommit = $commit; return $this; } public function getMostRecentCommit() { return $this->assertAttached($this->mostRecentCommit); } public function getDiffusionBrowseURIForPath( PhabricatorUser $user, $path, $line = null, $branch = null) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $this, 'path' => $path, 'branch' => $branch, )); return $drequest->generateURI( array( 'action' => 'browse', 'line' => $line, )); } public function getLocalPath() { return $this->getDetail('local-path'); } public function getSubversionBaseURI($commit = null) { $subpath = $this->getDetail('svn-subpath'); if (!strlen($subpath)) { $subpath = null; } return $this->getSubversionPathURI($subpath, $commit); } public function getSubversionPathURI($path = null, $commit = null) { $vcs = $this->getVersionControlSystem(); if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) { throw new Exception('Not a subversion repository!'); } if ($this->isHosted()) { $uri = 'file://'.$this->getLocalPath(); } else { $uri = $this->getDetail('remote-uri'); } $uri = rtrim($uri, '/'); if (strlen($path)) { $path = rawurlencode($path); $path = str_replace('%2F', '/', $path); $uri = $uri.'/'.ltrim($path, '/'); } if ($path !== null || $commit !== null) { $uri .= '@'; } if ($commit !== null) { $uri .= $commit; } return $uri; } public function attachProjectPHIDs(array $project_phids) { $this->projectPHIDs = $project_phids; return $this; } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } /** * Get the name of the directory this repository should clone or checkout * into. For example, if the repository name is "Example Repository", a * reasonable name might be "example-repository". This is used to help users * get reasonable results when cloning repositories, since they generally do * not want to clone into directories called "X/" or "Example Repository/". * * @return string */ public function getCloneName() { $name = $this->getDetail('clone-name'); // Make some reasonable effort to produce reasonable default directory // names from repository names. if (!strlen($name)) { $name = $this->getName(); $name = phutil_utf8_strtolower($name); $name = preg_replace('@[/ -:]+@', '-', $name); $name = trim($name, '-'); if (!strlen($name)) { $name = $this->getCallsign(); } } return $name; } /* -( Remote Command Execution )------------------------------------------- */ public function execRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args)->resolve(); } public function execxRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args)->resolvex(); } public function getRemoteCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args); } public function passthruRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandPassthru($args)->execute(); } private function newRemoteCommandFuture(array $argv) { $argv = $this->formatRemoteCommand($argv); $future = newv('ExecFuture', $argv); $future->setEnv($this->getRemoteCommandEnvironment()); return $future; } private function newRemoteCommandPassthru(array $argv) { $argv = $this->formatRemoteCommand($argv); $passthru = newv('PhutilExecPassthru', $argv); $passthru->setEnv($this->getRemoteCommandEnvironment()); return $passthru; } /* -( Local Command Execution )-------------------------------------------- */ public function execLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args)->resolve(); } public function execxLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args)->resolvex(); } public function getLocalCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args); } public function passthruLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandPassthru($args)->execute(); } private function newLocalCommandFuture(array $argv) { $this->assertLocalExists(); $argv = $this->formatLocalCommand($argv); $future = newv('ExecFuture', $argv); $future->setEnv($this->getLocalCommandEnvironment()); if ($this->usesLocalWorkingCopy()) { $future->setCWD($this->getLocalPath()); } return $future; } private function newLocalCommandPassthru(array $argv) { $this->assertLocalExists(); $argv = $this->formatLocalCommand($argv); $future = newv('PhutilExecPassthru', $argv); $future->setEnv($this->getLocalCommandEnvironment()); if ($this->usesLocalWorkingCopy()) { $future->setCWD($this->getLocalPath()); } return $future; } /* -( Command Infrastructure )--------------------------------------------- */ private function getSSHWrapper() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/bin/ssh-connect'; } private function getCommonCommandEnvironment() { $env = array( // NOTE: Force the language to "en_US.UTF-8", which overrides locale // settings. This makes stuff print in English instead of, e.g., French, // so we can parse the output of some commands, error messages, etc. 'LANG' => 'en_US.UTF-8', // Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155. 'PHABRICATOR_ENV' => PhabricatorEnv::getSelectedEnvironmentName(), ); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: // NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if // it can not read $HOME. For many users, $HOME points at /root (this // seems to be a default result of Apache setup). Instead, explicitly // point $HOME at a readable, empty directory so that Git looks for the // config file it's after, fails to locate it, and moves on. This is // really silly, but seems like the least damaging approach to // mitigating the issue. $root = dirname(phutil_get_library_root('phabricator')); $env['HOME'] = $root.'/support/empty/'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // NOTE: This overrides certain configuration, extensions, and settings // which make Mercurial commands do random unusual things. $env['HGPLAIN'] = 1; break; default: throw new Exception('Unrecognized version control system.'); } return $env; } private function getLocalCommandEnvironment() { return $this->getCommonCommandEnvironment(); } private function getRemoteCommandEnvironment() { $env = $this->getCommonCommandEnvironment(); if ($this->shouldUseSSH()) { // NOTE: This is read by `bin/ssh-connect`, and tells it which credentials // to use. $env['PHABRICATOR_CREDENTIAL'] = $this->getCredentialPHID(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // Force SVN to use `bin/ssh-connect`. $env['SVN_SSH'] = $this->getSSHWrapper(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: // Force Git to use `bin/ssh-connect`. $env['GIT_SSH'] = $this->getSSHWrapper(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // We force Mercurial through `bin/ssh-connect` too, but it uses a // command-line flag instead of an environmental variable. break; default: throw new Exception('Unrecognized version control system.'); } } return $env; } private function formatRemoteCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: if ($this->shouldUseHTTP() || $this->shouldUseSVNProtocol()) { $flags = array(); $flag_args = array(); $flags[] = '--non-interactive'; $flags[] = '--no-auth-cache'; if ($this->shouldUseHTTP()) { $flags[] = '--trust-server-cert'; } $credential_phid = $this->getCredentialPHID(); if ($credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, PhabricatorUser::getOmnipotentUser()); $flags[] = '--username %P'; $flags[] = '--password %P'; $flag_args[] = $key->getUsernameEnvelope(); $flag_args[] = $key->getPasswordEnvelope(); } $flags = implode(' ', $flags); $pattern = "svn {$flags} {$pattern}"; $args = array_mergev(array($flag_args, $args)); } else { $pattern = "svn --non-interactive {$pattern}"; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "git {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: if ($this->shouldUseSSH()) { $pattern = "hg --config ui.ssh=%s {$pattern}"; array_unshift( $args, $this->getSSHWrapper()); } else { $pattern = "hg {$pattern}"; } break; default: throw new Exception('Unrecognized version control system.'); } array_unshift($args, $pattern); return $args; } private function formatLocalCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn --non-interactive {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "git {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $pattern = "hg {$pattern}"; break; default: throw new Exception('Unrecognized version control system.'); } array_unshift($args, $pattern); return $args; } /** * Sanitize output of an `hg` command invoked with the `--debug` flag to make * it usable. * * @param string Output from `hg --debug ...` * @return string Usable output. */ public static function filterMercurialDebugOutput($stdout) { // When hg commands are run with `--debug` and some config file isn't // trusted, Mercurial prints out a warning to stdout, twice, after Feb 2011. // // http://selenic.com/pipermail/mercurial-devel/2011-February/028541.html $lines = preg_split('/(?<=\n)/', $stdout); $regex = '/ignoring untrusted configuration option .*\n$/'; foreach ($lines as $key => $line) { $lines[$key] = preg_replace($regex, '', $line); } return implode('', $lines); } public function getURI() { return '/diffusion/'.$this->getCallsign().'/'; } public function getNormalizedPath() { $uri = (string)$this->getCloneURIObject(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $normalized_uri = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_GIT, $uri); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $normalized_uri = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_SVN, $uri); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $normalized_uri = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL, $uri); break; default: throw new Exception('Unrecognized version control system.'); } return $normalized_uri->getNormalizedPath(); } public function isTracked() { return $this->getDetail('tracking-enabled', false); } public function getDefaultBranch() { $default = $this->getDetail('default-branch'); if (strlen($default)) { return $default; } $default_branches = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master', PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default', ); return idx($default_branches, $this->getVersionControlSystem()); } public function getDefaultArcanistBranch() { return coalesce($this->getDefaultBranch(), 'svn'); } private function isBranchInFilter($branch, $filter_key) { $vcs = $this->getVersionControlSystem(); $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); $use_filter = ($is_git); if (!$use_filter) { // If this VCS doesn't use filters, pass everything through. return true; } $filter = $this->getDetail($filter_key, array()); // If there's no filter set, let everything through. if (!$filter) { return true; } // If this branch isn't literally named `regexp(...)`, and it's in the // filter list, let it through. if (isset($filter[$branch])) { if (self::extractBranchRegexp($branch) === null) { return true; } } // If the branch matches a regexp, let it through. foreach ($filter as $pattern => $ignored) { $regexp = self::extractBranchRegexp($pattern); if ($regexp !== null) { if (preg_match($regexp, $branch)) { return true; } } } // Nothing matched, so filter this branch out. return false; } public static function extractBranchRegexp($pattern) { $matches = null; if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) { return $matches[1]; } return null; } public function shouldTrackBranch($branch) { return $this->isBranchInFilter($branch, 'branch-filter'); } public function formatCommitName($commit_identifier) { $vcs = $this->getVersionControlSystem(); $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; $is_git = ($vcs == $type_git); $is_hg = ($vcs == $type_hg); if ($is_git || $is_hg) { $short_identifier = substr($commit_identifier, 0, 12); } else { $short_identifier = $commit_identifier; } return 'r'.$this->getCallsign().$short_identifier; } public function isImporting() { return (bool)$this->getDetail('importing', false); } /* -( Autoclose )---------------------------------------------------------- */ /** * Determine if autoclose is active for a branch. * * For more details about why, use @{method:shouldSkipAutocloseBranch}. * * @param string Branch name to check. * @return bool True if autoclose is active for the branch. * @task autoclose */ public function shouldAutocloseBranch($branch) { return ($this->shouldSkipAutocloseBranch($branch) === null); } /** * Determine if autoclose is active for a commit. * * For more details about why, use @{method:shouldSkipAutocloseCommit}. * * @param PhabricatorRepositoryCommit Commit to check. * @return bool True if autoclose is active for the commit. * @task autoclose */ public function shouldAutocloseCommit(PhabricatorRepositoryCommit $commit) { return ($this->shouldSkipAutocloseCommit($commit) === null); } /** * Determine why autoclose should be skipped for a branch. * * This method gives a detailed reason why autoclose will be skipped. To * perform a simple test, use @{method:shouldAutocloseBranch}. * * @param string Branch name to check. * @return const|null Constant identifying reason to skip this branch, or null * if autoclose is active. * @task autoclose */ public function shouldSkipAutocloseBranch($branch) { $all_reason = $this->shouldSkipAllAutoclose(); if ($all_reason) { return $all_reason; } if (!$this->shouldTrackBranch($branch)) { return self::BECAUSE_BRANCH_UNTRACKED; } if (!$this->isBranchInFilter($branch, 'close-commits-filter')) { return self::BECAUSE_BRANCH_NOT_AUTOCLOSE; } return null; } /** * Determine why autoclose should be skipped for a commit. * * This method gives a detailed reason why autoclose will be skipped. To * perform a simple test, use @{method:shouldAutocloseCommit}. * * @param PhabricatorRepositoryCommit Commit to check. * @return const|null Constant identifying reason to skip this commit, or null * if autoclose is active. * @task autoclose */ public function shouldSkipAutocloseCommit( PhabricatorRepositoryCommit $commit) { $all_reason = $this->shouldSkipAllAutoclose(); if ($all_reason) { return $all_reason; } switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return null; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: break; default: throw new Exception('Unrecognized version control system.'); } $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE; if (!$commit->isPartiallyImported($closeable_flag)) { return self::BECAUSE_NOT_ON_AUTOCLOSE_BRANCH; } return null; } /** * Determine why all autoclose operations should be skipped for this * repository. * * @return const|null Constant identifying reason to skip all autoclose * operations, or null if autoclose operations are not blocked at the * repository level. * @task autoclose */ private function shouldSkipAllAutoclose() { if ($this->isImporting()) { return self::BECAUSE_REPOSITORY_IMPORTING; } if ($this->getDetail('disable-autoclose', false)) { return self::BECAUSE_AUTOCLOSE_DISABLED; } return null; } /* -( Repository URI Management )------------------------------------------ */ /** * Get the remote URI for this repository. * * @return string * @task uri */ public function getRemoteURI() { return (string)$this->getRemoteURIObject(); } /** * Get the remote URI for this repository, including credentials if they're * used by this repository. * * @return PhutilOpaqueEnvelope URI, possibly including credentials. * @task uri */ public function getRemoteURIEnvelope() { $uri = $this->getRemoteURIObject(); $remote_protocol = $this->getRemoteProtocol(); if ($remote_protocol == 'http' || $remote_protocol == 'https') { // For SVN, we use `--username` and `--password` flags separately, so // don't add any credentials here. if (!$this->isSVN()) { $credential_phid = $this->getCredentialPHID(); if ($credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, PhabricatorUser::getOmnipotentUser()); $uri->setUser($key->getUsernameEnvelope()->openEnvelope()); $uri->setPass($key->getPasswordEnvelope()->openEnvelope()); } } } return new PhutilOpaqueEnvelope((string)$uri); } /** * Get the clone (or checkout) URI for this repository, without authentication * information. * * @return string Repository URI. * @task uri */ public function getPublicCloneURI() { $uri = $this->getCloneURIObject(); // Make sure we don't leak anything if this repo is using HTTP Basic Auth // with the credentials in the URI or something zany like that. // If repository is not accessed over SSH we remove both username and // password. if (!$this->isHosted()) { if (!$this->shouldUseSSH()) { $uri->setUser(null); // This might be a Git URI or a normal URI. If it's Git, there's no // password support. if ($uri instanceof PhutilURI) { $uri->setPass(null); } } } return (string)$uri; } /** * Get the protocol for the repository's remote. * * @return string Protocol, like "ssh" or "git". * @task uri */ public function getRemoteProtocol() { $uri = $this->getRemoteURIObject(); if ($uri instanceof PhutilGitURI) { return 'ssh'; } else { return $uri->getProtocol(); } } /** * Get a parsed object representation of the repository's remote URI. This * may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git * URI (returned as a @{class@libphutil:PhutilGitURI}). * * @return wild A @{class@libphutil:PhutilURI} or * @{class@libphutil:PhutilGitURI}. * @task uri */ public function getRemoteURIObject() { $raw_uri = $this->getDetail('remote-uri'); if (!$raw_uri) { return new PhutilURI(''); } if (!strncmp($raw_uri, '/', 1)) { return new PhutilURI('file://'.$raw_uri); } $uri = new PhutilURI($raw_uri); if ($uri->getProtocol()) { return $uri; } $uri = new PhutilGitURI($raw_uri); if ($uri->getDomain()) { return $uri; } throw new Exception("Remote URI '{$raw_uri}' could not be parsed!"); } /** * Get the "best" clone/checkout URI for this repository, on any protocol. */ public function getCloneURIObject() { if (!$this->isHosted()) { if ($this->isSVN()) { // Make sure we pick up the "Import Only" path for Subversion, so // the user clones the repository starting at the correct path, not // from the root. $base_uri = $this->getSubversionBaseURI(); $base_uri = new PhutilURI($base_uri); $path = $base_uri->getPath(); if (!$path) { $path = '/'; } // If the trailing "@" is not required to escape the URI, strip it for // readability. if (!preg_match('/@.*@/', $path)) { $path = rtrim($path, '@'); } $base_uri->setPath($path); return $base_uri; } else { return $this->getRemoteURIObject(); } } // Choose the best URI: pick a read/write URI over a URI which is not // read/write, and SSH over HTTP. $serve_ssh = $this->getServeOverSSH(); $serve_http = $this->getServeOverHTTP(); if ($serve_ssh === self::SERVE_READWRITE) { return $this->getSSHCloneURIObject(); } else if ($serve_http === self::SERVE_READWRITE) { return $this->getHTTPCloneURIObject(); } else if ($serve_ssh !== self::SERVE_OFF) { return $this->getSSHCloneURIObject(); } else if ($serve_http !== self::SERVE_OFF) { return $this->getHTTPCloneURIObject(); } else { return null; } } /** * Get the repository's SSH clone/checkout URI, if one exists. */ public function getSSHCloneURIObject() { if (!$this->isHosted()) { if ($this->shouldUseSSH()) { return $this->getRemoteURIObject(); } else { return null; } } $serve_ssh = $this->getServeOverSSH(); if ($serve_ssh === self::SERVE_OFF) { return null; } $uri = new PhutilURI(PhabricatorEnv::getProductionURI($this->getURI())); if ($this->isSVN()) { $uri->setProtocol('svn+ssh'); } else { $uri->setProtocol('ssh'); } if ($this->isGit()) { $uri->setPath($uri->getPath().$this->getCloneName().'.git'); } else if ($this->isHg()) { $uri->setPath($uri->getPath().$this->getCloneName().'/'); } $ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user'); if ($ssh_user) { $uri->setUser($ssh_user); } $uri->setPort(PhabricatorEnv::getEnvConfig('diffusion.ssh-port')); return $uri; } /** * Get the repository's HTTP clone/checkout URI, if one exists. */ public function getHTTPCloneURIObject() { if (!$this->isHosted()) { if ($this->shouldUseHTTP()) { return $this->getRemoteURIObject(); } else { return null; } } $serve_http = $this->getServeOverHTTP(); if ($serve_http === self::SERVE_OFF) { return null; } $uri = PhabricatorEnv::getProductionURI($this->getURI()); $uri = new PhutilURI($uri); if ($this->isGit()) { $uri->setPath($uri->getPath().$this->getCloneName().'.git'); } else if ($this->isHg()) { $uri->setPath($uri->getPath().$this->getCloneName().'/'); } return $uri; } /** * Determine if we should connect to the remote using SSH flags and * credentials. * * @return bool True to use the SSH protocol. * @task uri */ private function shouldUseSSH() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); if ($this->isSSHProtocol($protocol)) { return true; } return false; } /** * Determine if we should connect to the remote using HTTP flags and * credentials. * * @return bool True to use the HTTP protocol. * @task uri */ private function shouldUseHTTP() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); return ($protocol == 'http' || $protocol == 'https'); } /** * Determine if we should connect to the remote using SVN flags and * credentials. * * @return bool True to use the SVN protocol. * @task uri */ private function shouldUseSVNProtocol() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); return ($protocol == 'svn'); } /** * Determine if a protocol is SSH or SSH-like. * * @param string A protocol string, like "http" or "ssh". * @return bool True if the protocol is SSH-like. * @task uri */ private function isSSHProtocol($protocol) { return ($protocol == 'ssh' || $protocol == 'svn+ssh'); } public function delete() { $this->openTransaction(); $paths = id(new PhabricatorOwnersPath()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($paths as $path) { $path->delete(); } $projects = id(new PhabricatorRepositoryArcanistProject()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($projects as $project) { // note each project deletes its PhabricatorRepositorySymbols $project->delete(); } $commits = id(new PhabricatorRepositoryCommit()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($commits as $commit) { // note PhabricatorRepositoryAuditRequests and // PhabricatorRepositoryCommitData are deleted here too. $commit->delete(); } $mirrors = id(new PhabricatorRepositoryMirror()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($mirrors as $mirror) { $mirror->delete(); } $ref_cursors = id(new PhabricatorRepositoryRefCursor()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($ref_cursors as $cursor) { $cursor->delete(); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_FILESYSTEM, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_PATHCHANGE, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_SUMMARY, $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function isGit() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); } public function isSVN() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); } public function isHg() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL); } public function isHosted() { return (bool)$this->getDetail('hosting-enabled', false); } public function setHosted($enabled) { return $this->setDetail('hosting-enabled', $enabled); } public function getServeOverHTTP() { if ($this->isSVN()) { return self::SERVE_OFF; } $serve = $this->getDetail('serve-over-http', self::SERVE_OFF); return $this->normalizeServeConfigSetting($serve); } public function setServeOverHTTP($mode) { return $this->setDetail('serve-over-http', $mode); } public function getServeOverSSH() { $serve = $this->getDetail('serve-over-ssh', self::SERVE_OFF); return $this->normalizeServeConfigSetting($serve); } public function setServeOverSSH($mode) { return $this->setDetail('serve-over-ssh', $mode); } public static function getProtocolAvailabilityName($constant) { switch ($constant) { case self::SERVE_OFF: return pht('Off'); case self::SERVE_READONLY: return pht('Read Only'); case self::SERVE_READWRITE: return pht('Read/Write'); default: return pht('Unknown'); } } private function normalizeServeConfigSetting($value) { switch ($value) { case self::SERVE_OFF: case self::SERVE_READONLY: return $value; case self::SERVE_READWRITE: if ($this->isHosted()) { return self::SERVE_READWRITE; } else { return self::SERVE_READONLY; } default: return self::SERVE_OFF; } } /** * Raise more useful errors when there are basic filesystem problems. */ private function assertLocalExists() { if (!$this->usesLocalWorkingCopy()) { return; } $local = $this->getLocalPath(); Filesystem::assertExists($local); Filesystem::assertIsDirectory($local); Filesystem::assertReadable($local); } /** * Determine if the working copy is bare or not. In Git, this corresponds * to `--bare`. In Mercurial, `--noupdate`. */ public function isWorkingCopyBare() { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return false; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $local = $this->getLocalPath(); if (Filesystem::pathExists($local.'/.git')) { return false; } else { return true; } } } public function usesLocalWorkingCopy() { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->isHosted(); case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return true; } } public function getHookDirectories() { $directories = array(); if (!$this->isHosted()) { return $directories; } $root = $this->getLocalPath(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: if ($this->isWorkingCopyBare()) { $directories[] = $root.'/hooks/pre-receive-phabricator.d/'; } else { $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/'; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $directories[] = $root.'/hooks/pre-commit-phabricator.d/'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // NOTE: We don't support custom Mercurial hooks for now because they're // messy and we can't easily just drop a `hooks.d/` directory next to // the hooks. break; } return $directories; } public function canDestroyWorkingCopy() { if ($this->isHosted()) { // Never destroy hosted working copies. return false; } $default_path = PhabricatorEnv::getEnvConfig( 'repository.default-local-path'); return Filesystem::isDescendant($this->getLocalPath(), $default_path); } public function canUsePathTree() { return !$this->isSVN(); } public function canMirror() { if ($this->isGit() || $this->isHg()) { return true; } return false; } public function canAllowDangerousChanges() { if (!$this->isHosted()) { return false; } if ($this->isGit() || $this->isHg()) { return true; } return false; } public function shouldAllowDangerousChanges() { return (bool)$this->getDetail('allow-dangerous-changes'); } public function writeStatusMessage( $status_type, $status_code, array $parameters = array()) { $table = new PhabricatorRepositoryStatusMessage(); $conn_w = $table->establishConnection('w'); $table_name = $table->getTableName(); if ($status_code === null) { queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s', $table_name, $this->getID(), $status_type); } else { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, statusType, statusCode, parameters, epoch) VALUES (%d, %s, %s, %s, %d) ON DUPLICATE KEY UPDATE statusCode = VALUES(statusCode), parameters = VALUES(parameters), epoch = VALUES(epoch)', $table_name, $this->getID(), $status_type, $status_code, json_encode($parameters), time()); } return $this; } public static function getRemoteURIProtocol($raw_uri) { $uri = new PhutilURI($raw_uri); if ($uri->getProtocol()) { return strtolower($uri->getProtocol()); } $git_uri = new PhutilGitURI($raw_uri); if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) { return 'ssh'; } return null; } public static function assertValidRemoteURI($uri) { if (trim($uri) != $uri) { throw new Exception( pht( 'The remote URI has leading or trailing whitespace.')); } $protocol = self::getRemoteURIProtocol($uri); // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619 // for discussion. This is usually a user adding "ssh://" to an implicit // SSH Git URI. if ($protocol == 'ssh') { if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) { throw new Exception( pht( "The remote URI is not formatted correctly. Remote URIs ". "with an explicit protocol should be in the form ". "'proto://domain/path', not 'proto://domain:/path'. ". "The ':/path' syntax is only valid in SCP-style URIs.")); } } switch ($protocol) { case 'ssh': case 'http': case 'https': case 'git': case 'svn': case 'svn+ssh': break; default: // NOTE: We're explicitly rejecting 'file://' because it can be // used to clone from the working copy of another repository on disk // that you don't normally have permission to access. throw new Exception( pht( "The URI protocol is unrecognized. It should begin ". "'ssh://', 'http://', 'https://', 'git://', 'svn://', ". "'svn+ssh://', or be in the form 'git@domain.com:path'.")); } return true; } /** * Load the pull frequency for this repository, based on the time since the * last activity. * * We pull rarely used repositories less frequently. This finds the most * recent commit which is older than the current time (which prevents us from * spinning on repositories with a silly commit post-dated to some time in * 2037). We adjust the pull frequency based on when the most recent commit * occurred. * * @param int The minimum update interval to use, in seconds. * @return int Repository update interval, in seconds. */ public function loadUpdateInterval($minimum = 15) { // If a repository is still importing, always pull it as frequently as // possible. This prevents us from hanging for a long time at 99.9% when // importing an inactive repository. if ($this->isImporting()) { return $minimum; } $window_start = (PhabricatorTime::getNow() + $minimum); $table = id(new PhabricatorRepositoryCommit()); $last_commit = queryfx_one( $table->establishConnection('r'), 'SELECT epoch FROM %T WHERE repositoryID = %d AND epoch <= %d ORDER BY epoch DESC LIMIT 1', $table->getTableName(), $this->getID(), $window_start); if ($last_commit) { $time_since_commit = ($window_start - $last_commit['epoch']); $last_few_days = phutil_units('3 days in seconds'); if ($time_since_commit <= $last_few_days) { // For repositories with activity in the recent past, we wait one // extra second for every 10 minutes since the last commit. This // shorter backoff is intended to handle weekends and other short // breaks from development. $smart_wait = ($time_since_commit / 600); } else { // For repositories without recent activity, we wait one extra second // for every 4 minutes since the last commit. This longer backoff // handles rarely used repositories, up to the maximum. $smart_wait = ($time_since_commit / 240); } // We'll never wait more than 6 hours to pull a repository. $longest_wait = phutil_units('6 hours in seconds'); $smart_wait = min($smart_wait, $longest_wait); $smart_wait = max($minimum, $smart_wait); } else { $smart_wait = $minimum; } return $smart_wait; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorRepositoryEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, DiffusionPushCapability::CAPABILITY, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case DiffusionPushCapability::CAPABILITY: return $this->getPushPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field)); return "repo:{$hash}"; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { return $this->getDetail('description'); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return true; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php b/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php index 7511add7d2..08a89175b0 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php +++ b/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php @@ -1,118 +1,118 @@ true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_SERIALIZATION => array( 'symbolIndexLanguages' => self::SERIALIZATION_JSON, 'symbolIndexProjects' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'repositoryID' => 'id?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryArcanistProjectPHIDType::TYPECONST); } // TODO: Remove. Also, T603. public function loadRepository() { if (!$this->getRepositoryID()) { return null; } return id(new PhabricatorRepository())->load($this->getRepositoryID()); } public function delete() { $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE arcanistProjectID = %d', id(new PhabricatorRepositorySymbol())->getTableName(), $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_USER; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_ADMIN; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php index 56613ac494..d9bae65931 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php +++ b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php @@ -1,74 +1,74 @@ false, self::CONFIG_SERIALIZATION => array( 'auditReasons' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'auditStatus' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'commitPHID' => array( 'columns' => array('commitPHID'), ), 'auditorPHID' => array( 'columns' => array('auditorPHID', 'auditStatus'), ), 'key_unique' => array( 'columns' => array('commitPHID', 'auditorPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function isUser() { $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; return (phid_get_type($this->getAuditorPHID()) == $user_type); } public function attachCommit(PhabricatorRepositoryCommit $commit) { $this->commit = $commit; return $this; } public function getCommit() { return $this->assertAttached($this->commit); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getCommit()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getCommit()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'This audit is attached to a commit, and inherits its policies.'); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryBranch.php b/src/applications/repository/storage/PhabricatorRepositoryBranch.php index 9a8aca87ff..4d7b76051a 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryBranch.php +++ b/src/applications/repository/storage/PhabricatorRepositoryBranch.php @@ -1,43 +1,43 @@ array( 'name' => 'text128', 'lintCommit' => 'text40?', ), self::CONFIG_KEY_SCHEMA => array( 'repositoryID' => array( 'columns' => array('repositoryID', 'name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public static function loadBranch($repository_id, $branch_name) { return id(new PhabricatorRepositoryBranch())->loadOneWhere( 'repositoryID = %d AND name = %s', $repository_id, $branch_name); } public static function loadOrCreateBranch($repository_id, $branch_name) { $branch = self::loadBranch($repository_id, $branch_name); if ($branch) { return $branch; } return id(new PhabricatorRepositoryBranch()) ->setRepositoryID($repository_id) ->setName($branch_name) ->save(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 92d5df0da6..1acf43080c 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -1,412 +1,412 @@ repository = $repository; return $this; } public function getRepository($assert_attached = true) { if ($assert_attached) { return $this->assertAttached($this->repository); } return $this->repository; } public function isPartiallyImported($mask) { return (($mask & $this->getImportStatus()) == $mask); } public function isImported() { return $this->isPartiallyImported(self::IMPORTED_ALL); } public function writeImportStatusFlag($flag) { queryfx( $this->establishConnection('w'), 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', $this->getTableName(), $flag, $this->getID()); $this->setImportStatus($this->getImportStatus() | $flag); return $this; } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'commitIdentifier' => 'text40', 'mailKey' => 'bytes20', 'authorPHID' => 'phid?', 'auditStatus' => 'uint32', 'summary' => 'text80', 'importStatus' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'repositoryID' => array( 'columns' => array('repositoryID', 'importStatus'), ), 'authorPHID' => array( 'columns' => array('authorPHID', 'auditStatus', 'epoch'), ), 'repositoryID_2' => array( 'columns' => array('repositoryID', 'epoch'), ), 'key_commit_identity' => array( 'columns' => array('commitIdentifier', 'repositoryID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryCommitPHIDType::TYPECONST); } public function loadCommitData() { if (!$this->getID()) { return null; } return id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $this->getID()); } public function attachCommitData( PhabricatorRepositoryCommitData $data = null) { $this->commitData = $data; return $this; } public function getCommitData() { return $this->assertAttached($this->commitData); } public function attachAudits(array $audits) { assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest'); $this->audits = $audits; return $this; } public function getAudits() { return $this->assertAttached($this->audits); } public function getAuthorityAudits( PhabricatorUser $user, array $authority_phids) { $authority = array_fill_keys($authority_phids, true); $audits = $this->getAudits(); $authority_audits = array(); foreach ($audits as $audit) { $has_authority = !empty($authority[$audit->getAuditorPHID()]); if ($has_authority) { $commit_author = $this->getAuthorPHID(); // You don't have authority over package and project audits on your // own commits. $auditor_is_user = ($audit->getAuditorPHID() == $user->getPHID()); $user_is_author = ($commit_author == $user->getPHID()); if ($auditor_is_user || !$user_is_author) { $authority_audits[$audit->getID()] = $audit; } } } return $authority_audits; } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function delete() { $data = $this->loadCommitData(); $audits = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere('commitPHID = %s', $this->getPHID()); $this->openTransaction(); if ($data) { $data->delete(); } foreach ($audits as $audit) { $audit->delete(); } $result = parent::delete(); $this->saveTransaction(); return $result; } public function getDateCreated() { // This is primarily to make analysis of commits with the Fact engine work. return $this->getEpoch(); } public function getURI() { $repository = $this->getRepository(); $callsign = $repository->getCallsign(); $commit_identifier = $this->getCommitIdentifier(); return '/r'.$callsign.$commit_identifier; } /** * Synchronize a commit's overall audit status with the individual audit * triggers. */ public function updateAuditStatus(array $requests) { assert_instances_of($requests, 'PhabricatorRepositoryAuditRequest'); $any_concern = false; $any_accept = false; $any_need = false; foreach ($requests as $request) { switch ($request->getAuditStatus()) { case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: $any_need = true; break; case PhabricatorAuditStatusConstants::ACCEPTED: $any_accept = true; break; case PhabricatorAuditStatusConstants::CONCERNED: $any_concern = true; break; } } if ($any_concern) { $status = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; } else if ($any_accept) { if ($any_need) { $status = PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED; } else { $status = PhabricatorAuditCommitStatusConstants::FULLY_AUDITED; } } else if ($any_need) { $status = PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT; } else { $status = PhabricatorAuditCommitStatusConstants::NONE; } return $this->setAuditStatus($status); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getRepository()->getPolicy($capability); case PhabricatorPolicyCapability::CAN_EDIT: // TODO: (T603) Who should be able to edit a commit? For now, retain // the existing policy. return PhabricatorPolicies::POLICY_USER; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Commits inherit the policies of the repository they belong to.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( Stuff for serialization )---------------------------------------------- */ /** * NOTE: this is not a complete serialization; only the 'protected' fields are * involved. This is due to ease of (ab)using the Lisk abstraction to get this * done, as well as complexity of the other fields. */ public function toDictionary() { return array( 'repositoryID' => $this->getRepositoryID(), 'phid' => $this->getPHID(), 'commitIdentifier' => $this->getCommitIdentifier(), 'epoch' => $this->getEpoch(), 'mailKey' => $this->getMailKey(), 'authorPHID' => $this->getAuthorPHID(), 'auditStatus' => $this->getAuditStatus(), 'summary' => $this->getSummary(), 'importStatus' => $this->getImportStatus(), ); } public static function newFromDictionary(array $dict) { return id(new PhabricatorRepositoryCommit()) ->loadFromArray($dict); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { return $this->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getRepository()->getPHID(); } public function getBuildVariables() { $results = array(); $results['buildable.commit'] = $this->getCommitIdentifier(); $repo = $this->getRepository(); $results['repository.callsign'] = $repo->getCallsign(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); return $results; } public function getAvailableBuildVariables() { return array( 'buildable.commit' => pht('The commit identifier, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => pht('The URI to clone or checkout the repository from.'), ); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { // TODO: We could make this configurable eventually, but just use the // defaults for now. return array(); } public function getCustomFieldBaseClass() { return 'PhabricatorCommitCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { // TODO: This should also list auditors, but handling that is a bit messy // right now because we are not guaranteed to have the data. return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuditEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorAuditTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php index 204f153b5a..705634895b 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php @@ -1,73 +1,73 @@ false, self::CONFIG_SERIALIZATION => array( 'commitDetails' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'authorName' => 'text', 'commitMessage' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'commitID' => array( 'columns' => array('commitID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getSummary() { $message = $this->getCommitMessage(); return self::summarizeCommitMessage($message); } public static function summarizeCommitMessage($message) { $summary = phutil_split_lines($message, $retain_endings = false); $summary = head($summary); $summary = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(self::SUMMARY_MAX_LENGTH) ->truncateString($summary); return $summary; } public function getCommitDetail($key, $default = null) { return idx($this->commitDetails, $key, $default); } public function setCommitDetail($key, $value) { $this->commitDetails[$key] = $value; return $this; } public function toDictionary() { return array( 'commitID' => $this->commitID, 'authorName' => $this->authorName, 'commitMessage' => $this->commitMessage, 'commitDetails' => json_encode($this->commitDetails), ); } public static function newFromDictionary(array $dict) { return id(new PhabricatorRepositoryCommitData()) ->loadFromArray($dict); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryMirror.php b/src/applications/repository/storage/PhabricatorRepositoryMirror.php index b778806f33..a5058ab345 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryMirror.php +++ b/src/applications/repository/storage/PhabricatorRepositoryMirror.php @@ -1,69 +1,69 @@ setRemoteURI(''); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'remoteURI' => 'text255', 'credentialPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_repository' => array( 'columns' => array('repositoryPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryMirrorPHIDType::TYPECONST); } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getRepository()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php index cb8cfe057f..2455499b88 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php @@ -1,92 +1,92 @@ setPusherPHID($viewer->getPHID()); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'remoteAddress' => 'uint32?', 'remoteProtocol' => 'text32?', 'rejectCode' => 'uint32', 'rejectDetails' => 'text64?', ), self::CONFIG_KEY_SCHEMA => array( 'key_repository' => array( 'columns' => array('repositoryPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPushEventPHIDType::TYPECONST); } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachLogs(array $logs) { $this->logs = $logs; return $this; } public function getLogs() { return $this->assertAttached($this->logs); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getRepository()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( "A repository's push events are visible to users who can see the ". "repository."); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php index c4dfd10b75..d5fb87fa8d 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -1,202 +1,202 @@ setPusherPHID($viewer->getPHID()); } public static function getHeraldChangeFlagConditionOptions() { return array( PhabricatorRepositoryPushLog::CHANGEFLAG_ADD => pht('change creates ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE => pht('change deletes ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE => pht('change rewrites ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS => pht('dangerous change'), ); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_BINARY => array( 'refNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'refType' => 'text12', 'refNameHash' => 'bytes12?', 'refNameRaw' => 'bytes?', 'refNameEncoding' => 'text16?', 'refOld' => 'text40?', 'refNew' => 'text40', 'mergeBase' => 'text40?', 'changeFlags' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_repository' => array( 'columns' => array('repositoryPHID'), ), 'key_ref' => array( 'columns' => array('repositoryPHID', 'refNew'), ), 'key_name' => array( 'columns' => array('repositoryPHID', 'refNameHash'), ), 'key_event' => array( 'columns' => array('pushEventPHID'), ), 'key_pusher' => array( 'columns' => array('pusherPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPushLogPHIDType::TYPECONST); } public function attachPushEvent(PhabricatorRepositoryPushEvent $push_event) { $this->pushEvent = $push_event; return $this; } public function getPushEvent() { return $this->assertAttached($this->pushEvent); } public function getRefName() { return $this->getUTF8StringFromStorage( $this->getRefNameRaw(), $this->getRefNameEncoding()); } public function setRefName($ref_raw) { $this->setRefNameRaw($ref_raw); $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw)); $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw)); return $this; } public function getRefOldShort() { if ($this->getRepository()->isSVN()) { return $this->getRefOld(); } return substr($this->getRefOld(), 0, 12); } public function getRefNewShort() { if ($this->getRepository()->isSVN()) { return $this->getRefNew(); } return substr($this->getRefNew(), 0, 12); } public function hasChangeFlags($mask) { return ($this->changeFlags & $mask); } public function attachDangerousChangeDescription($description) { $this->dangerousChangeDescription = $description; return $this; } public function getDangerousChangeDescription() { return $this->assertAttached($this->dangerousChangeDescription); } public function attachRepository(PhabricatorRepository $repository) { // NOTE: Some gymnastics around this because of object construction order // in the hook engine. Particularly, web build the logs before we build // their push event. $this->repository = $repository; return $this; } public function getRepository() { if ($this->repository == self::ATTACHABLE) { return $this->getPushEvent()->getRepository(); } return $this->assertAttached($this->repository); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // NOTE: We're passing through the repository rather than the push event // mostly because we need to do policy checks in Herald before we create // the event. The two approaches are equivalent in practice. return $this->getRepository()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( "A repository's push logs are visible to users who can see the ". "repository."); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php b/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php index 699108f7b8..efdcdfc1ba 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php +++ b/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php @@ -1,92 +1,92 @@ false, self::CONFIG_BINARY => array( 'refNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'refType' => 'text32', 'refNameHash' => 'bytes12', 'commitIdentifier' => 'text40', // T6203/NULLABILITY // This probably should not be nullable; refNameRaw is not nullable. 'refNameEncoding' => 'text16?', ), self::CONFIG_KEY_SCHEMA => array( 'key_cursor' => array( 'columns' => array('repositoryPHID', 'refType', 'refNameHash'), ), ), ) + parent::getConfiguration(); } public function getRefName() { return $this->getUTF8StringFromStorage( $this->getRefNameRaw(), $this->getRefNameEncoding()); } public function setRefName($ref_raw) { $this->setRefNameRaw($ref_raw); $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw)); $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw)); return $this; } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getRepository()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Repository refs have the same policies as their repository.'); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryStatusMessage.php b/src/applications/repository/storage/PhabricatorRepositoryStatusMessage.php index 967aec7911..eb5bb9688c 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryStatusMessage.php +++ b/src/applications/repository/storage/PhabricatorRepositoryStatusMessage.php @@ -1,43 +1,43 @@ false, self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'statusType' => 'text32', 'statusCode' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'repositoryID' => array( 'columns' => array('repositoryID', 'statusType'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getParameter($key, $default = null) { return idx($this->parameters, $key, $default); } } diff --git a/src/applications/repository/storage/PhabricatorRepositorySymbol.php b/src/applications/repository/storage/PhabricatorRepositorySymbol.php index 353ff5ba5e..9fe563cf1b 100644 --- a/src/applications/repository/storage/PhabricatorRepositorySymbol.php +++ b/src/applications/repository/storage/PhabricatorRepositorySymbol.php @@ -1,92 +1,92 @@ self::IDS_MANUAL, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'id' => null, 'symbolContext' => 'text128', 'symbolName' => 'text128', 'symbolType' => 'text12', 'symbolLanguage' => 'text32', 'lineNumber' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'PRIMARY' => null, 'symbolName' => array( 'columns' => array('symbolName'), ), ), ) + parent::getConfiguration(); } public function getURI() { if (!$this->repository) { // This symbol is in the index, but we don't know which Repository it's // part of. Usually this means the Arcanist Project hasn't been linked // to a Repository. We can't generate a URI, so just fail. return null; } $request = DiffusionRequest::newFromDictionary( array( 'user' => PhabricatorUser::getOmnipotentUser(), 'repository' => $this->getRepository(), )); return $request->generateURI( array( 'action' => 'browse', 'path' => $this->getPath(), 'line' => $this->getLineNumber(), )); } public function getPath() { return $this->assertAttached($this->path); } public function attachPath($path) { $this->path = $path; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository($repository) { $this->repository = $repository; return $this; } public function getArcanistProject() { return $this->assertAttached($this->arcanistProject); } public function attachArcanistProject($project) { $this->arcanistProject = $project; return $this; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php b/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php index cafbfa22ba..f9cbb69a03 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php +++ b/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php @@ -1,60 +1,60 @@ array( 'passwordHash' => 'text128', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => array( 'columns' => array('userPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function setPassword( PhutilOpaqueEnvelope $password, PhabricatorUser $user) { $hash_envelope = $this->hashPassword($password, $user); return $this->setPasswordHash($hash_envelope->openEnvelope()); } public function comparePassword( PhutilOpaqueEnvelope $password, PhabricatorUser $user) { return PhabricatorPasswordHasher::comparePassword( $this->getPasswordHashInput($password, $user), new PhutilOpaqueEnvelope($this->getPasswordHash())); } private function getPasswordHashInput( PhutilOpaqueEnvelope $password, PhabricatorUser $user) { if ($user->getPHID() != $this->getUserPHID()) { throw new Exception('User does not match password user PHID!'); } $raw_input = PhabricatorHash::digestPassword($password, $user->getPHID()); return new PhutilOpaqueEnvelope($raw_input); } private function hashPassword( PhutilOpaqueEnvelope $password, PhabricatorUser $user) { $input_envelope = $this->getPasswordHashInput($password, $user); $best_hasher = PhabricatorPasswordHasher::getBestHasher(); return $best_hasher->getPasswordHashForStorage($input_envelope); } } diff --git a/src/applications/search/storage/PhabricatorNamedQuery.php b/src/applications/search/storage/PhabricatorNamedQuery.php index 3df30cc229..ac34a4fa32 100644 --- a/src/applications/search/storage/PhabricatorNamedQuery.php +++ b/src/applications/search/storage/PhabricatorNamedQuery.php @@ -1,64 +1,64 @@ array( 'engineClassName' => 'text128', 'queryName' => 'text255', 'queryKey' => 'text12', 'isBuiltin' => 'bool', 'isDisabled' => 'bool', 'sequence' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_userquery' => array( 'columns' => array('userPHID', 'engineClassName', 'queryKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getSortKey() { return sprintf('~%010d%010d', $this->sequence, $this->getID()); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($viewer->getPHID() == $this->userPHID) { return true; } return false; } public function describeAutomaticCapability($capability) { return pht( 'The queries you have saved are private. Only you can view or edit '. 'them.'); } } diff --git a/src/applications/search/storage/PhabricatorSavedQuery.php b/src/applications/search/storage/PhabricatorSavedQuery.php index a8e8456e2a..71d9d9ccd2 100644 --- a/src/applications/search/storage/PhabricatorSavedQuery.php +++ b/src/applications/search/storage/PhabricatorSavedQuery.php @@ -1,77 +1,77 @@ array( 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'engineClassName' => 'text255', 'queryKey' => 'text12', ), self::CONFIG_KEY_SCHEMA => array( 'key_queryKey' => array( 'columns' => array('queryKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function setParameter($key, $value) { $this->parameters[$key] = $value; return $this; } public function getParameter($key, $default = null) { return idx($this->parameters, $key, $default); } public function save() { if ($this->getEngineClassName() === null) { throw new Exception(pht('Engine class is null.')); } // Instantiate the engine to make sure it's valid. $this->newEngine(); $serial = $this->getEngineClassName().serialize($this->parameters); $this->queryKey = PhabricatorHash::digestForIndex($serial); return parent::save(); } public function newEngine() { return newv($this->getEngineClassName(), array()); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_PUBLIC; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/search/storage/document/PhabricatorSearchDocument.php b/src/applications/search/storage/document/PhabricatorSearchDocument.php index 9b5fc3a488..161e791316 100644 --- a/src/applications/search/storage/document/PhabricatorSearchDocument.php +++ b/src/applications/search/storage/document/PhabricatorSearchDocument.php @@ -1,37 +1,37 @@ false, self::CONFIG_IDS => self::IDS_MANUAL, self::CONFIG_COLUMN_SCHEMA => array( 'documentType' => 'text4', 'documentTitle' => 'text255', 'documentCreated' => 'epoch', 'documentModified' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'PRIMARY' => array( 'columns' => array('phid'), 'unique' => true, ), 'documentCreated' => array( 'columns' => array('documentCreated'), ), ), ) + parent::getConfiguration(); } public function getIDKey() { return 'phid'; } } diff --git a/src/applications/search/storage/document/PhabricatorSearchDocumentField.php b/src/applications/search/storage/document/PhabricatorSearchDocumentField.php index a8c97ea331..49529963b0 100644 --- a/src/applications/search/storage/document/PhabricatorSearchDocumentField.php +++ b/src/applications/search/storage/document/PhabricatorSearchDocumentField.php @@ -1,37 +1,37 @@ false, self::CONFIG_IDS => self::IDS_MANUAL, self::CONFIG_COLUMN_SCHEMA => array( 'phidType' => 'text4', 'field' => 'text4', 'auxPHID' => 'phid?', 'corpus' => 'fulltext?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), ), 'corpus' => array( 'columns' => array('corpus'), 'type' => 'FULLTEXT', ), ), ) + parent::getConfiguration(); } public function getIDKey() { return 'phid'; } } diff --git a/src/applications/search/storage/document/PhabricatorSearchDocumentRelationship.php b/src/applications/search/storage/document/PhabricatorSearchDocumentRelationship.php index 848f8578f6..5629e4391d 100644 --- a/src/applications/search/storage/document/PhabricatorSearchDocumentRelationship.php +++ b/src/applications/search/storage/document/PhabricatorSearchDocumentRelationship.php @@ -1,38 +1,38 @@ false, self::CONFIG_IDS => self::IDS_MANUAL, self::CONFIG_COLUMN_SCHEMA => array( 'relation' => 'text4', 'relatedType' => 'text4', 'relatedTime' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), ), 'relatedPHID' => array( 'columns' => array('relatedPHID', 'relation'), ), 'relation' => array( 'columns' => array('relation', 'relatedPHID'), ), ), ) + parent::getConfiguration(); } public function getIDKey() { return 'phid'; } } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 233e56c28c..e3acfbb3b5 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -1,102 +1,102 @@ array( 'preferences' => self::SERIALIZATION_JSON, ), self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( 'userPHID' => array( 'columns' => array('userPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPreference($key, $default = null) { return idx($this->preferences, $key, $default); } public function setPreference($key, $value) { $this->preferences[$key] = $value; return $this; } public function unsetPreference($key) { unset($this->preferences[$key]); return $this; } public function getPinnedApplications(array $apps, PhabricatorUser $viewer) { $pref_pinned = PhabricatorUserPreferences::PREFERENCE_APP_PINNED; $pinned = $this->getPreference($pref_pinned); if ($pinned) { return $pinned; } $pref_tiles = PhabricatorUserPreferences::PREFERENCE_APP_TILES; $tiles = $this->getPreference($pref_tiles, array()); $full_tile = 'full'; $large = array(); foreach ($apps as $app) { $show = $app->isPinnedByDefault($viewer); // TODO: This is legacy stuff, clean it up eventually. This approximately // retains the old "tiles" preference. if (isset($tiles[get_class($app)])) { $show = ($tiles[get_class($app)] == $full_tile); } if ($show) { $large[] = get_class($app); } } return $large; } } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php b/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php index c325b8ddc7..7fdfe1ed06 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php @@ -1,22 +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 bac8ab5332..b7104cb626 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php @@ -1,21 +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 5bea424ffa..bf0ecaaa96 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -1,204 +1,204 @@ 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() { + protected 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; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorSlowvoteEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorSlowvoteTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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 438806a685..948cdee873 100644 --- a/src/applications/system/storage/PhabricatorSystemActionLog.php +++ b/src/applications/system/storage/PhabricatorSystemActionLog.php @@ -1,36 +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 aca41a043c..20ff549a69 100644 --- a/src/applications/system/storage/PhabricatorSystemDestructionLog.php +++ b/src/applications/system/storage/PhabricatorSystemDestructionLog.php @@ -1,28 +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/PhabricatorToken.php b/src/applications/tokens/storage/PhabricatorToken.php index c435b2dfef..0beeba3b81 100644 --- a/src/applications/tokens/storage/PhabricatorToken.php +++ b/src/applications/tokens/storage/PhabricatorToken.php @@ -1,49 +1,49 @@ true, self::CONFIG_NO_TABLE => true, ) + parent::getConfiguration(); } 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; } public function renderIcon() { // TODO: Maybe move to a View class? require_celerity_resource('sprite-tokens-css'); require_celerity_resource('tokens-css'); $sprite = substr($this->getPHID(), 10); return id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_TOKENS) ->setSpriteIcon($sprite); } } diff --git a/src/applications/tokens/storage/PhabricatorTokenCount.php b/src/applications/tokens/storage/PhabricatorTokenCount.php index 8380f8a1f1..fcd9b7da9f 100644 --- a/src/applications/tokens/storage/PhabricatorTokenCount.php +++ b/src/applications/tokens/storage/PhabricatorTokenCount.php @@ -1,28 +1,28 @@ self::IDS_MANUAL, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'id' => 'auto', '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 5be2044551..59ffc819d1 100644 --- a/src/applications/tokens/storage/PhabricatorTokenGiven.php +++ b/src/applications/tokens/storage/PhabricatorTokenGiven.php @@ -1,81 +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/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 4b6120b561..9fedbb4bea 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1,1176 +1,1176 @@ ignoreOnNoEffect = $ignore; return $this; } public function getIgnoreOnNoEffect() { return $this->ignoreOnNoEffect; } public function shouldGenerateOldValue() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_BUILDABLE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_CUSTOMFIELD: return false; } return true; } abstract public function getApplicationTransactionType(); private function getApplicationObjectTypeName() { $types = PhabricatorPHIDType::getAllTypes(); $type = idx($types, $this->getApplicationTransactionType()); if ($type) { return $type->getTypeName(); } return pht('Object'); } public function getApplicationTransactionCommentObject() { throw new PhutilMethodNotImplementedException(); } public function getApplicationTransactionViewObject() { return new PhabricatorApplicationTransactionView(); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function generatePHID() { $type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST; $subtype = $this->getApplicationTransactionType(); return PhabricatorPHID::generateNewPHID($type, $subtype); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'commentPHID' => 'phid?', 'commentVersion' => 'uint32', 'contentSource' => 'text', 'transactionType' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID'), ), ), ) + parent::getConfiguration(); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function hasComment() { return $this->getComment() && strlen($this->getComment()->getContent()); } public function getComment() { if ($this->commentNotLoaded) { throw new Exception('Comment for this transaction was not loaded.'); } return $this->comment; } public function attachComment( PhabricatorApplicationTransactionComment $comment) { $this->comment = $comment; $this->commentNotLoaded = false; return $this; } public function setCommentNotLoaded($not_loaded) { $this->commentNotLoaded = $not_loaded; return $this; } public function attachObject($object) { $this->object = $object; return $this; } public function getObject() { return $this->assertAttached($this->object); } public function getRemarkupBlocks() { $blocks = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { $custom_blocks = $field->getApplicationTransactionRemarkupBlocks( $this); foreach ($custom_blocks as $custom_block) { $blocks[] = $custom_block; } } break; } if ($this->getComment()) { $blocks[] = $this->getComment()->getContent(); } return $blocks; } public function setOldValue($value) { $this->oldValueHasBeenSet = true; $this->writeField('oldValue', $value); return $this; } public function hasOldValue() { return $this->oldValueHasBeenSet; } /* -( Rendering )---------------------------------------------------------- */ public function setRenderingTarget($rendering_target) { $this->renderingTarget = $rendering_target; return $this; } public function getRenderingTarget() { return $this->renderingTarget; } public function attachViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->assertAttached($this->viewer); } public function getRequiredHandlePHIDs() { $phids = array(); $old = $this->getOldValue(); $new = $this->getNewValue(); $phids[] = array($this->getAuthorPHID()); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs( $this); } break; case PhabricatorTransactions::TYPE_SUBSCRIBERS: $phids[] = $old; $phids[] = $new; break; case PhabricatorTransactions::TYPE_EDGE: $phids[] = ipull($old, 'dst'); $phids[] = ipull($new, 'dst'); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) { $phids[] = array($old); } if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) { $phids[] = array($new); } break; case PhabricatorTransactions::TYPE_TOKEN: break; case PhabricatorTransactions::TYPE_BUILDABLE: $phid = $this->getMetadataValue('harbormaster:buildablePHID'); if ($phid) { $phids[] = array($phid); } break; } if ($this->getComment()) { $phids[] = array($this->getComment()->getAuthorPHID()); } return array_mergev($phids); } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( pht( 'Transaction ("%s", of type "%s") requires a handle ("%s") that it '. 'did not load.', $this->getPHID(), $this->getTransactionType(), $phid)); } return $this->handles[$phid]; } public function getHandleIfExists($phid) { return idx($this->handles, $phid); } public function getHandles() { if ($this->handles === null) { throw new Exception( 'Transaction requires handles and it did not load them.' ); } return $this->handles; } public function renderHandleLink($phid) { if ($this->renderingTarget == self::TARGET_HTML) { return $this->getHandle($phid)->renderLink(); } else { return $this->getHandle($phid)->getLinkName(); } } public function renderHandleList(array $phids) { $links = array(); foreach ($phids as $phid) { $links[] = $this->renderHandleLink($phid); } if ($this->renderingTarget == self::TARGET_HTML) { return phutil_implode_html(', ', $links); } else { return implode(', ', $links); } } private function renderSubscriberList(array $phids, $change_type) { if ($this->getRenderingTarget() == self::TARGET_TEXT) { return $this->renderHandleList($phids); } else { $handles = array_select_keys($this->getHandles(), $phids); return id(new SubscriptionListStringBuilder()) ->setHandles($handles) ->setObjectPHID($this->getPHID()) ->buildTransactionString($change_type); } } protected function renderPolicyName($phid, $state = 'old') { $policy = PhabricatorPolicy::newFromPolicyAndHandle( $phid, $this->getHandleIfExists($phid)); if ($this->renderingTarget == self::TARGET_HTML) { switch ($policy->getType()) { case PhabricatorPolicyType::TYPE_CUSTOM: $policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/'); $policy->setWorkflow(true); break; default: break; } $output = $policy->renderDescription(); } else { $output = hsprintf('%s', $policy->getFullName()); } return $output; } public function getIcon() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $comment = $this->getComment(); if ($comment && $comment->getIsRemoved()) { return 'fa-eraser'; } return 'fa-comment'; case PhabricatorTransactions::TYPE_SUBSCRIBERS: return 'fa-envelope'; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: return 'fa-lock'; case PhabricatorTransactions::TYPE_EDGE: return 'fa-link'; case PhabricatorTransactions::TYPE_BUILDABLE: return 'fa-wrench'; case PhabricatorTransactions::TYPE_TOKEN: return 'fa-trophy'; } return 'fa-pencil'; } public function getToken() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: $old = $this->getOldValue(); $new = $this->getNewValue(); if ($new) { $icon = substr($new, 10); } else { $icon = substr($old, 10); } return array($icon, !$this->getNewValue()); } return array(null, null); } public function getColor() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT; $comment = $this->getComment(); if ($comment && $comment->getIsRemoved()) { return 'black'; } break; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: return 'green'; case HarbormasterBuildable::STATUS_FAILED: return 'red'; } break; } return null; } protected function getTransactionCustomField() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $key = $this->getMetadataValue('customfield:key'); if (!$key) { return null; } $field = PhabricatorCustomField::getObjectField( $this->getObject(), PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if (!$field) { return null; } $field->setViewer($this->getViewer()); return $field; } return null; } public function shouldHide() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: if ($this->getOldValue() === null) { return true; } else { return false; } break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->shouldHideInApplicationTransactions($this); } case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: return true; break; case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: $new = ipull($this->getNewValue(), 'dst'); $old = ipull($this->getOldValue(), 'dst'); $add = array_diff($new, $old); $add_value = reset($add); $add_handle = $this->getHandle($add_value); if ($add_handle->getPolicyFiltered()) { return true; } return false; break; default: break; } break; } return false; } public function shouldHideForMail(array $xactions) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: return true; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_FAILED: // For now, only ever send mail when builds fail. We might let // you customize this later, but in most cases this is probably // completely uninteresting. return false; } return true; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: return true; break; default: break; } break; } return $this->shouldHide(); } public function shouldHideForFeed() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: return true; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_FAILED: // For now, don't notify on build passes either. These are pretty // high volume and annoying, with very little present value. We // might want to turn them back on in the specific case of // build successes on the current document? return false; } return true; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: return true; break; default: break; } break; } return $this->shouldHide(); } public function getTitleForMail() { return id(clone $this)->setRenderingTarget('text')->getTitle(); } public function getBodyForMail() { $comment = $this->getComment(); if ($comment && strlen($comment->getContent())) { return $comment->getContent(); } return null; } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('You can not post an empty comment.'); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( 'This %s already has that view policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( 'This %s already has that edit policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( 'This %s already has that join policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( 'All users are already subscribed to this %s.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDGE: return pht('Edges already exist; transaction has no effect.'); } return pht('Transaction has no effect.'); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment.', $this->renderHandleLink($author_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_SUBSCRIBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s edited subscriber(s), added %d: %s; removed %d: %s.', $this->renderHandleLink($author_phid), count($add), $this->renderSubscriberList($add, 'add'), count($rem), $this->renderSubscriberList($rem, 'rem')); } else if ($add) { return pht( '%s added %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($add), $this->renderSubscriberList($add, 'add')); } else if ($rem) { return pht( '%s removed %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($rem), $this->renderSubscriberList($rem, 'rem')); } else { // This is used when rendering previews, before the user actually // selects any CCs. return pht( '%s updated subscribers...', $this->renderHandleLink($author_phid)); } break; case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); $type_obj = PhabricatorEdgeType::getByConstant($type); if ($add && $rem) { return $type_obj->getTransactionEditString( $this->renderHandleLink($author_phid), new PhutilNumber(count($add) + count($rem)), new PhutilNumber(count($add)), $this->renderHandleList($add), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else if ($add) { return $type_obj->getTransactionAddString( $this->renderHandleLink($author_phid), new PhutilNumber(count($add)), $this->renderHandleList($add)); } else if ($rem) { return $type_obj->getTransactionRemoveString( $this->renderHandleLink($author_phid), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else { return $type_obj->getTransactionPreviewString( $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionTitle($this); } else { return pht( '%s edited a custom field.', $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_TOKEN: if ($old && $new) { return pht( '%s updated a token.', $this->renderHandleLink($author_phid)); } else if ($old) { return pht( '%s rescinded a token.', $this->renderHandleLink($author_phid)); } else { return pht( '%s awarded a token.', $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_BUILDING: return pht( '%s started building %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); case HarbormasterBuildable::STATUS_PASSED: return pht( '%s completed building %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); case HarbormasterBuildable::STATUS_FAILED: return pht( '%s failed to build %s!', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); default: return null; } default: return pht( '%s edited this %s.', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName()); } } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( '%s updated subscribers of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); $type_obj = PhabricatorEdgeType::getByConstant($type); if ($add && $rem) { return $type_obj->getFeedEditString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($add) + count($rem)), new PhutilNumber(count($add)), $this->renderHandleList($add), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else if ($add) { return $type_obj->getFeedAddString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($add)), $this->renderHandleList($add)); } else if ($rem) { return $type_obj->getFeedRemoveString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else { return pht( '%s edited edge metadata for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionTitleForFeed($this); } else { return pht( '%s edited a custom field on %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_BUILDING: return pht( '%s started building %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); case HarbormasterBuildable::STATUS_PASSED: return pht( '%s completed building %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); case HarbormasterBuildable::STATUS_FAILED: return pht( '%s failed to build %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); default: return null; } } return $this->getTitle(); } public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) { $fields = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); if (strlen($text)) { $fields[] = 'comment/'.$this->getID(); } break; } return $fields; } public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); return PhabricatorMarkupEngine::summarize($text); } return null; } public function getBodyForFeed(PhabricatorFeedStory $story) { $old = $this->getOldValue(); $new = $this->getNewValue(); $body = null; switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); if (strlen($text)) { $body = $story->getMarkupFieldOutput('comment/'.$this->getID()); } break; } return $body; } public function getActionStrength() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return 0.5; case PhabricatorTransactions::TYPE_SUBSCRIBERS: $old = $this->getOldValue(); $new = $this->getNewValue(); $add = array_diff($old, $new); $rem = array_diff($new, $old); // If this action is the actor subscribing or unsubscribing themselves, // it is less interesting. In particular, if someone makes a comment and // also implicitly subscribes themselves, we should treat the // transaction group as "comment", not "subscribe". In this specific // case (one affected user, and that affected user it the actor), // decrease the action strength. if ((count($add) + count($rem)) != 1) { // Not exactly one CC change. break; } $affected_phid = head(array_merge($add, $rem)); if ($affected_phid != $this->getAuthorPHID()) { // Affected user is someone else. break; } // Make this weaker than TYPE_COMMENT. return 0.25; } return 1.0; } public function isCommentTransaction() { if ($this->hasComment()) { return true; } switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return true; } return false; } public function getActionName() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('Commented On'); case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht('Changed Policy'); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht('Changed Subscribers'); case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: return pht('Build Passed'); case HarbormasterBuildable::STATUS_FAILED: return pht('Build Failed'); default: return pht('Build Status'); } default: return pht('Updated'); } } public function getMailTags() { return array(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionHasChangeDetails($this); } break; } return false; } public function renderChangeDetails(PhabricatorUser $viewer) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionChangeDetails($this, $viewer); } break; } return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function renderTextCorpusChangeDetails( PhabricatorUser $viewer, $old, $new) { require_celerity_resource('differential-changeset-view-css'); $view = id(new PhabricatorApplicationTransactionTextDiffDetailView()) ->setUser($viewer) ->setOldText($old) ->setNewText($new); return $view->render(); } public function attachTransactionGroup(array $group) { assert_instances_of($group, 'PhabricatorApplicationTransaction'); $this->transactionGroup = $group; return $this; } public function getTransactionGroup() { return $this->transactionGroup; } /** * Should this transaction be visually grouped with an existing transaction * group? * * @param list List of transactions. * @return bool True to display in a group with the other transactions. */ public function shouldDisplayGroupWith(array $group) { $this_source = null; if ($this->getContentSource()) { $this_source = $this->getContentSource()->getSource(); } foreach ($group as $xaction) { // Don't group transactions by different authors. if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) { return false; } // Don't group transactions for different objects. if ($xaction->getObjectPHID() != $this->getObjectPHID()) { return false; } // Don't group anything into a group which already has a comment. if ($xaction->isCommentTransaction()) { return false; } // Don't group transactions from different content sources. $other_source = null; if ($xaction->getContentSource()) { $other_source = $xaction->getContentSource()->getSource(); } if ($other_source != $this_source) { return false; } // Don't group transactions which happened more than 2 minutes apart. $apart = abs($xaction->getDateCreated() - $this->getDateCreated()); if ($apart > (60 * 2)) { return false; } } return true; } public function renderExtraInformationLink() { $herald_xscript_id = $this->getMetadataValue('herald:transcriptID'); if ($herald_xscript_id) { return phutil_tag( 'a', array( 'href' => '/herald/transcript/'.$herald_xscript_id.'/', ), pht('View Herald Transcript')); } return null; } public function renderAsTextForDoorkeeper( DoorkeeperFeedStoryPublisher $publisher, PhabricatorFeedStory $story, array $xactions) { $text = array(); $body = array(); foreach ($xactions as $xaction) { $xaction_body = $xaction->getBodyForMail(); if ($xaction_body !== null) { $body[] = $xaction_body; } if ($xaction->shouldHideForMail($xactions)) { continue; } $old_target = $xaction->getRenderingTarget(); $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; $xaction->setRenderingTarget($new_target); if ($publisher->getRenderWithImpliedContext()) { $text[] = $xaction->getTitle(); } else { $text[] = $xaction->getTitleForFeed(); } $xaction->setRenderingTarget($old_target); } $text = implode("\n", $text); $body = implode("\n\n", $body); return rtrim($text."\n\n".$body); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { // TODO: (T603) Exact policies are unclear here. return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $comment_template = null; try { $comment_template = $this->getApplicationTransactionCommentObject(); } catch (Exception $ex) { // Continue; no comments for these transactions. } if ($comment_template) { $comments = $comment_template->loadAllWhere( 'transactionPHID = %s', $this->getPHID()); foreach ($comments as $comment) { $engine->destroyObject($comment); } } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransactionComment.php b/src/applications/transactions/storage/PhabricatorApplicationTransactionComment.php index cacd577f7a..f750612c24 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransactionComment.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransactionComment.php @@ -1,160 +1,160 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'transactionPHID' => 'phid?', 'commentVersion' => 'uint32', 'content' => 'text', 'contentSource' => 'text', 'isDeleted' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_version' => array( 'columns' => array('transactionPHID', 'commentVersion'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getApplicationName() { return $this->getApplicationTransactionObject()->getApplicationName(); } public function getTableName() { $xaction = $this->getApplicationTransactionObject(); return self::getTableNameFromTransaction($xaction); } public static function getTableNameFromTransaction( PhabricatorApplicationTransaction $xaction) { return $xaction->getTableName().'_comment'; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function setContentSourceFromRequest(AphrontRequest $request) { return $this->setContentSource( PhabricatorContentSource::newFromRequest($request)); } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function getIsRemoved() { return ($this->getIsDeleted() == 2); } public function setIsRemoved($removed) { if ($removed) { $this->setIsDeleted(2); } else { $this->setIsDeleted(0); } return $this; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { return PhabricatorPHIDConstants::PHID_TYPE_XCMT.':'.$this->getPHID(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function getMarkupText($field) { return $this->getContent(); } 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->getPHID(); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { // TODO: (T603) Policies are murky. return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/xhprof/storage/PhabricatorXHProfSample.php b/src/applications/xhprof/storage/PhabricatorXHProfSample.php index 8d13a76534..662305429d 100644 --- a/src/applications/xhprof/storage/PhabricatorXHProfSample.php +++ b/src/applications/xhprof/storage/PhabricatorXHProfSample.php @@ -1,32 +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(); } } diff --git a/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php b/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php index b0780bd1c9..18024ec175 100644 --- a/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php +++ b/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php @@ -1,19 +1,19 @@ false, ) + parent::getConfiguration(); } abstract public function formatForInsert(AphrontDatabaseConnection $conn); abstract public function getIndexValueType(); } diff --git a/src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php b/src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php index 2dc22cca10..c5b89d6d9c 100644 --- a/src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php +++ b/src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php @@ -1,36 +1,36 @@ array( 'indexKey' => 'bytes12', 'indexValue' => 'sint64', ), self::CONFIG_KEY_SCHEMA => array( 'key_join' => array( 'columns' => array('objectPHID', 'indexKey', 'indexValue'), ), 'key_find' => array( 'columns' => array('indexKey', 'indexValue'), ), ), ) + parent::getConfiguration(); } public function formatForInsert(AphrontDatabaseConnection $conn) { return qsprintf( $conn, '(%s, %s, %d)', $this->getObjectPHID(), $this->getIndexKey(), $this->getIndexValue()); } public function getIndexValueType() { return 'int'; } } diff --git a/src/infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php b/src/infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php index 3c586e81b5..9817b2e591 100644 --- a/src/infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php +++ b/src/infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php @@ -1,26 +1,26 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'fieldIndex' => 'bytes12', 'fieldValue' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'objectPHID' => array( 'columns' => array('objectPHID', 'fieldIndex'), 'unique' => true, ), ), ) + parent::getConfiguration(); } } diff --git a/src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php b/src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php index 8d3d75286e..b16f851a60 100644 --- a/src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php +++ b/src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php @@ -1,36 +1,36 @@ array( 'indexKey' => 'bytes12', 'indexValue' => 'sort', ), self::CONFIG_KEY_SCHEMA => array( 'key_join' => array( 'columns' => array('objectPHID', 'indexKey', 'indexValue(64)'), ), 'key_find' => array( 'columns' => array('indexKey', 'indexValue(64)'), ), ), ) + parent::getConfiguration(); } public function formatForInsert(AphrontDatabaseConnection $conn) { return qsprintf( $conn, '(%s, %s, %s)', $this->getObjectPHID(), $this->getIndexKey(), $this->getIndexValue()); } public function getIndexValueType() { return 'string'; } } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php index 1f108e1b1d..6e3b6ad8fa 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php @@ -1,213 +1,213 @@ self::IDS_COUNTER, self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( 'dataID' => array( 'columns' => array('dataID'), 'unique' => true, ), 'taskClass' => array( 'columns' => array('taskClass'), ), 'leaseExpires' => array( 'columns' => array('leaseExpires'), ), 'leaseOwner' => array( 'columns' => array('leaseOwner(16)'), ), 'key_failuretime' => array( 'columns' => array('failureTime'), ), 'leaseOwner_2' => array( 'columns' => array('leaseOwner', 'priority', 'id'), ), ) + $parent[self::CONFIG_KEY_SCHEMA], ); $config[self::CONFIG_COLUMN_SCHEMA] = array( // T6203/NULLABILITY // This isn't nullable in the archive table, so at a minimum these // should be the same. 'dataID' => 'uint32?', ) + $parent[self::CONFIG_COLUMN_SCHEMA]; return $config + $parent; } public function setServerTime($server_time) { $this->serverTime = $server_time; $this->localTime = time(); return $this; } public function setLeaseDuration($lease_duration) { $this->checkLease(); $server_lease_expires = $this->serverTime + $lease_duration; $this->setLeaseExpires($server_lease_expires); // NOTE: This is primarily to allow unit tests to set negative lease // durations so they don't have to wait around for leases to expire. We // check that the lease is valid above. return $this->forceSaveWithoutLease(); } public function save() { $this->checkLease(); return $this->forceSaveWithoutLease(); } public function forceSaveWithoutLease() { $is_new = !$this->getID(); if ($is_new) { $this->failureCount = 0; } if ($is_new && ($this->getData() !== null)) { $data = new PhabricatorWorkerTaskData(); $data->setData($this->getData()); $data->save(); $this->setDataID($data->getID()); } return parent::save(); } protected function checkLease() { if ($this->leaseOwner) { $current_server_time = $this->serverTime + (time() - $this->localTime); if ($current_server_time >= $this->leaseExpires) { $id = $this->getID(); $class = $this->getTaskClass(); throw new Exception( "Trying to update Task {$id} ({$class}) after lease expiration!"); } } } public function delete() { throw new Exception( 'Active tasks can not be deleted directly. '. 'Use archiveTask() to move tasks to the archive.'); } public function archiveTask($result, $duration) { if ($this->getID() === null) { throw new Exception( "Attempting to archive a task which hasn't been save()d!"); } $this->checkLease(); $archive = id(new PhabricatorWorkerArchiveTask()) ->setID($this->getID()) ->setTaskClass($this->getTaskClass()) ->setLeaseOwner($this->getLeaseOwner()) ->setLeaseExpires($this->getLeaseExpires()) ->setFailureCount($this->getFailureCount()) ->setDataID($this->getDataID()) ->setPriority($this->getPriority()) ->setObjectPHID($this->getObjectPHID()) ->setResult($result) ->setDuration($duration); // NOTE: This deletes the active task (this object)! $archive->save(); return $archive; } public function executeTask() { // We do this outside of the try .. catch because we don't have permission // to release the lease otherwise. $this->checkLease(); $did_succeed = false; try { $worker = $this->getWorkerInstance(); $maximum_failures = $worker->getMaximumRetryCount(); if ($maximum_failures !== null) { if ($this->getFailureCount() > $maximum_failures) { $id = $this->getID(); throw new PhabricatorWorkerPermanentFailureException( "Task {$id} has exceeded the maximum number of failures ". "({$maximum_failures})."); } } $lease = $worker->getRequiredLeaseTime(); if ($lease !== null) { $this->setLeaseDuration($lease); } $t_start = microtime(true); $worker->executeTask(); $t_end = microtime(true); $duration = (int)(1000000 * ($t_end - $t_start)); $result = $this->archiveTask( PhabricatorWorkerArchiveTask::RESULT_SUCCESS, $duration); $did_succeed = true; } catch (PhabricatorWorkerPermanentFailureException $ex) { $result = $this->archiveTask( PhabricatorWorkerArchiveTask::RESULT_FAILURE, 0); $result->setExecutionException($ex); } catch (PhabricatorWorkerYieldException $ex) { $this->setExecutionException($ex); $retry = $ex->getDuration(); $retry = max($retry, 5); // NOTE: As a side effect, this saves the object. $this->setLeaseDuration($retry); $result = $this; } catch (Exception $ex) { $this->setExecutionException($ex); $this->setFailureCount($this->getFailureCount() + 1); $this->setFailureTime(time()); $retry = $worker->getWaitBeforeRetry($this); $retry = coalesce( $retry, PhabricatorWorkerLeaseQuery::getDefaultWaitBeforeRetry()); // NOTE: As a side effect, this saves the object. $this->setLeaseDuration($retry); $result = $this; } // NOTE: If this throws, we don't want it to cause the task to fail again, // so execute it out here and just let the exception escape. if ($did_succeed) { foreach ($worker->getQueuedTasks() as $task) { list($class, $data) = $task; PhabricatorWorker::scheduleTask( $class, $data, array( 'priority' => $this->getPriority(), )); } } return $result; } } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php index d28f13c8d0..5186a005c4 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php @@ -1,97 +1,97 @@ self::IDS_MANUAL, ) + $parent; $config[self::CONFIG_COLUMN_SCHEMA] = array( 'result' => 'uint32', 'duration' => 'uint64', ) + $config[self::CONFIG_COLUMN_SCHEMA]; $config[self::CONFIG_KEY_SCHEMA] = array( 'dateCreated' => array( 'columns' => array('dateCreated'), ), 'leaseOwner' => array( 'columns' => array('leaseOwner', 'priority', 'id'), ), ) + $parent[self::CONFIG_KEY_SCHEMA]; return $config; } public function save() { if ($this->getID() === null) { throw new Exception('Trying to archive a task with no ID.'); } $other = new PhabricatorWorkerActiveTask(); $conn_w = $this->establishConnection('w'); $this->openTransaction(); queryfx( $conn_w, 'DELETE FROM %T WHERE id = %d', $other->getTableName(), $this->getID()); $result = parent::insert(); $this->saveTransaction(); return $result; } public function delete() { $this->openTransaction(); if ($this->getDataID()) { $conn_w = $this->establishConnection('w'); $data_table = new PhabricatorWorkerTaskData(); queryfx( $conn_w, 'DELETE FROM %T WHERE id = %d', $data_table->getTableName(), $this->getDataID()); } $result = parent::delete(); $this->saveTransaction(); return $result; } public function unarchiveTask() { $this->openTransaction(); $active = id(new PhabricatorWorkerActiveTask()) ->setID($this->getID()) ->setTaskClass($this->getTaskClass()) ->setLeaseOwner(null) ->setLeaseExpires(0) ->setFailureCount(0) ->setDataID($this->getDataID()) ->setPriority($this->getPriority()) ->setObjectPHID($this->getObjectPHID()) ->insert(); $this->setDataID(null); $this->delete(); $this->saveTransaction(); return $active; } } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php index 5671904c7b..77543befb9 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php @@ -1,76 +1,76 @@ array( 'taskClass' => 'text64', 'leaseOwner' => 'text64?', 'leaseExpires' => 'epoch?', 'failureCount' => 'uint32', 'failureTime' => 'epoch?', 'priority' => 'uint32', 'objectPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID'), ), ), ) + parent::getConfiguration(); } final public function setExecutionException(Exception $execution_exception) { $this->executionException = $execution_exception; return $this; } final public function getExecutionException() { return $this->executionException; } final public function setData($data) { $this->data = $data; return $this; } final public function getData() { return $this->data; } final public function isArchived() { return ($this instanceof PhabricatorWorkerArchiveTask); } final public function getWorkerInstance() { $id = $this->getID(); $class = $this->getTaskClass(); if (!class_exists($class)) { throw new PhabricatorWorkerPermanentFailureException( "Task class '{$class}' does not exist!"); } if (!is_subclass_of($class, 'PhabricatorWorker')) { throw new PhabricatorWorkerPermanentFailureException( "Task class '{$class}' does not extend PhabricatorWorker."); } return newv($class, array($this->getData())); } } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php index 9968123afa..b30285f799 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php @@ -1,16 +1,16 @@ false, self::CONFIG_SERIALIZATION => array( 'data' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } } diff --git a/src/infrastructure/sms/storage/PhabricatorSMS.php b/src/infrastructure/sms/storage/PhabricatorSMS.php index eb03d890de..c17d8f16da 100644 --- a/src/infrastructure/sms/storage/PhabricatorSMS.php +++ b/src/infrastructure/sms/storage/PhabricatorSMS.php @@ -1,75 +1,75 @@ setBody($body) ->setSendStatus(PhabricatorSMS::STATUS_UNSENT) ->setProviderShortName(PhabricatorSMS::SHORTNAME_PLACEHOLDER) ->setProviderSMSID(Filesystem::readRandomCharacters(40)); } - public function getConfiguration() { + protected function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( 'providerShortName' => 'text16', 'providerSMSID' => 'text40', 'toNumber' => 'text20', 'fromNumber' => 'text20?', 'body' => 'text', 'sendStatus' => 'text16?', ), self::CONFIG_KEY_SCHEMA => array( 'key_provider' => array( 'columns' => array('providerSMSID', 'providerShortName'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getTableName() { // Slightly non-standard, but otherwise this class needs "MetaMTA" in its // name. :/ return 'sms'; } public function hasBeenSentAtLeastOnce() { return ($this->getProviderShortName() != PhabricatorSMS::SHORTNAME_PLACEHOLDER); } } diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index e16e6c69e4..d52d9375be 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1,1840 +1,1840 @@ setName('Sawyer') * ->setBreed('Pug') * ->save(); * * Note that **Lisk automatically builds getters and setters for all of your * object's protected properties** via @{method:__call}. If you want to add * custom behavior to your getters or setters, you can do so by overriding the * @{method:readField} and @{method:writeField} methods. * * Calling @{method:save} will persist the object to the database. After calling * @{method:save}, you can call @{method:getID} to retrieve the object's ID. * * To load objects by ID, use the @{method:load} method: * * $dog = id(new Dog())->load($id); * * This will load the Dog record with ID $id into $dog, or `null` if no such * record exists (@{method:load} is an instance method rather than a static * method because PHP does not support late static binding, at least until PHP * 5.3). * * To update an object, change its properties and save it: * * $dog->setBreed('Lab')->save(); * * To delete an object, call @{method:delete}: * * $dog->delete(); * * That's Lisk CRUD in a nutshell. * * = Queries = * * Often, you want to load a bunch of objects, or execute a more specialized * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this: * * $pugs = $dog->loadAllWhere('breed = %s', 'Pug'); * $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer'); * * These methods work like @{function@libphutil:queryfx}, but only take half of * a query (the part after the WHERE keyword). Lisk will handle the connection, * columns, and object construction; you are responsible for the rest of it. * @{method:loadAllWhere} returns a list of objects, while * @{method:loadOneWhere} returns a single object (or `null`). * * There's also a @{method:loadRelatives} method which helps to prevent the 1+N * queries problem. * * = Managing Transactions = * * Lisk uses a transaction stack, so code does not generally need to be aware * of the transactional state of objects to implement correct transaction * semantics: * * $obj->openTransaction(); * $obj->save(); * $other->save(); * // ... * $other->openTransaction(); * $other->save(); * $another->save(); * if ($some_condition) { * $other->saveTransaction(); * } else { * $other->killTransaction(); * } * // ... * $obj->saveTransaction(); * * Assuming ##$obj##, ##$other## and ##$another## live on the same database, * this code will work correctly by establishing savepoints. * * Selects whose data are used later in the transaction should be included in * @{method:beginReadLocking} or @{method:beginWriteLocking} block. * * @task conn Managing Connections * @task config Configuring Lisk * @task load Loading Objects * @task info Examining Objects * @task save Writing Objects * @task hook Hooks and Callbacks * @task util Utilities * @task xaction Managing Transactions * @task isolate Isolation for Unit Testing */ abstract class LiskDAO { const CONFIG_IDS = 'id-mechanism'; const CONFIG_TIMESTAMPS = 'timestamps'; const CONFIG_AUX_PHID = 'auxiliary-phid'; const CONFIG_SERIALIZATION = 'col-serialization'; const CONFIG_BINARY = 'binary'; const CONFIG_COLUMN_SCHEMA = 'col-schema'; const CONFIG_KEY_SCHEMA = 'key-schema'; const CONFIG_NO_TABLE = 'no-table'; const SERIALIZATION_NONE = 'id'; const SERIALIZATION_JSON = 'json'; const SERIALIZATION_PHP = 'php'; const IDS_AUTOINCREMENT = 'ids-auto'; const IDS_COUNTER = 'ids-counter'; const IDS_MANUAL = 'ids-manual'; const COUNTER_TABLE_NAME = 'lisk_counter'; private static $processIsolationLevel = 0; private static $transactionIsolationLevel = 0; private $ephemeral = false; private static $connections = array(); private $inSet = null; protected $id; protected $phid; protected $dateCreated; protected $dateModified; /** * Build an empty object. * * @return obj Empty object. */ public function __construct() { $id_key = $this->getIDKey(); if ($id_key) { $this->$id_key = null; } } /* -( Managing Connections )----------------------------------------------- */ /** * Establish a live connection to a database service. This method should * return a new connection. Lisk handles connection caching and management; * do not perform caching deeper in the stack. * * @param string Mode, either 'r' (reading) or 'w' (reading and writing). * @return AphrontDatabaseConnection New database connection. * @task conn */ abstract protected function establishLiveConnection($mode); /** * Return a namespace for this object's connections in the connection cache. * Generally, the database name is appropriate. Two connections are considered * equivalent if they have the same connection namespace and mode. * * @return string Connection namespace for cache * @task conn */ abstract protected function getConnectionNamespace(); /** * Get an existing, cached connection for this object. * * @param mode Connection mode. * @return AprontDatabaseConnection|null Connection, if it exists in cache. * @task conn */ protected function getEstablishedConnection($mode) { $key = $this->getConnectionNamespace().':'.$mode; if (isset(self::$connections[$key])) { return self::$connections[$key]; } return null; } /** * Store a connection in the connection cache. * * @param mode Connection mode. * @param AphrontDatabaseConnection Connection to cache. * @return this * @task conn */ protected function setEstablishedConnection( $mode, AphrontDatabaseConnection $connection, $force_unique = false) { $key = $this->getConnectionNamespace().':'.$mode; if ($force_unique) { $key .= ':unique'; while (isset(self::$connections[$key])) { $key .= '!'; } } self::$connections[$key] = $connection; return $this; } /* -( Configuring Lisk )--------------------------------------------------- */ /** * Change Lisk behaviors, like ID configuration and timestamps. If you want * to change these behaviors, you should override this method in your child * class and change the options you're interested in. For example: * - * public function getConfiguration() { + * protected function getConfiguration() { * return array( * Lisk_DataAccessObject::CONFIG_EXAMPLE => true, * ) + parent::getConfiguration(); * } * * The available options are: * * CONFIG_IDS * Lisk objects need to have a unique identifying ID. The three mechanisms * available for generating this ID are IDS_AUTOINCREMENT (default, assumes * the ID column is an autoincrement primary key), IDS_MANUAL (you are taking * full responsibility for ID management), or IDS_COUNTER (see below). * * InnoDB does not persist the value of `auto_increment` across restarts, * and instead initializes it to `MAX(id) + 1` during startup. This means it * may reissue the same autoincrement ID more than once, if the row is deleted * and then the database is restarted. To avoid this, you can set an object to * use a counter table with IDS_COUNTER. This will generally behave like * IDS_AUTOINCREMENT, except that the counter value will persist across * restarts and inserts will be slightly slower. If a database stores any * DAOs which use this mechanism, you must create a table there with this * schema: * * CREATE TABLE lisk_counter ( * counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY, * counterValue BIGINT UNSIGNED NOT NULL * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; * * CONFIG_TIMESTAMPS * Lisk can automatically handle keeping track of a `dateCreated' and * `dateModified' column, which it will update when it creates or modifies * an object. If you don't want to do this, you may disable this option. * By default, this option is ON. * * CONFIG_AUX_PHID * This option can be enabled by being set to some truthy value. The meaning * of this value is defined by your PHID generation mechanism. If this option * is enabled, a `phid' property will be populated with a unique PHID when an * object is created (or if it is saved and does not currently have one). You * need to override generatePHID() and hook it into your PHID generation * mechanism for this to work. By default, this option is OFF. * * CONFIG_SERIALIZATION * You can optionally provide a column serialization map that will be applied * to values when they are written to the database. For example: * * self::CONFIG_SERIALIZATION => array( * 'complex' => self::SERIALIZATION_JSON, * ) * * This will cause Lisk to JSON-serialize the 'complex' field before it is * written, and unserialize it when it is read. * * CONFIG_BINARY * You can optionally provide a map of columns to a flag indicating that * they store binary data. These columns will not raise an error when * handling binary writes. * * CONFIG_COLUMN_SCHEMA * Provide a map of columns to schema column types. * * CONFIG_KEY_SCHEMA * Provide a map of key names to key specifications. * * CONFIG_NO_TABLE * Allows you to specify that this object does not actually have a table in * the database. * * @return dictionary Map of configuration options to values. * * @task config */ protected function getConfiguration() { return array( self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, ); } /** * Determine the setting of a configuration option for this class of objects. * * @param const Option name, one of the CONFIG_* constants. * @return mixed Option value, if configured (null if unavailable). * * @task config */ public function getConfigOption($option_name) { static $options = null; if (!isset($options)) { $options = $this->getConfiguration(); } return idx($options, $option_name); } /* -( Loading Objects )---------------------------------------------------- */ /** * Load an object by ID. You need to invoke this as an instance method, not * a class method, because PHP doesn't have late static binding (until * PHP 5.3.0). For example: * * $dog = id(new Dog())->load($dog_id); * * @param int Numeric ID identifying the object to load. * @return obj|null Identified object, or null if it does not exist. * * @task load */ public function load($id) { if (is_object($id)) { $id = (string)$id; } if (!$id || (!is_int($id) && !ctype_digit($id))) { return null; } return $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $id); } /** * Loads all of the objects, unconditionally. * * @return dict Dictionary of all persisted objects of this type, keyed * on object ID. * * @task load */ public function loadAll() { return $this->loadAllWhere('1 = 1'); } /** * Load all objects which match a WHERE clause. You provide everything after * the 'WHERE'; Lisk handles everything up to it. For example: * * $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7); * * The pattern and arguments are as per queryfx(). * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objects, keyed on ID. * * @task load */ public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Load a single object identified by a 'WHERE' clause. You provide * everything after the 'WHERE', and Lisk builds the first half of the * query. See loadAllWhere(). This method is similar, but returns a single * result instead of a list. * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return obj|null Matching object, or null if no object matches. * * @task load */ public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); if (count($data) > 1) { throw new AphrontCountQueryException( 'More than 1 result from loadOneWhere()!'); } $data = reset($data); if (!$data) { return null; } return $this->loadFromArray($data); } protected function loadRawDataWhere($pattern /* , $args... */) { $connection = $this->establishConnection('r'); $lock_clause = ''; if ($connection->isReadLocking()) { $lock_clause = 'FOR UPDATE'; } else if ($connection->isWriteLocking()) { $lock_clause = 'LOCK IN SHARE MODE'; } $args = func_get_args(); $args = array_slice($args, 1); $pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q'; array_unshift($args, $this->getTableName()); array_push($args, $lock_clause); array_unshift($args, $pattern); return call_user_func_array( array($connection, 'queryData'), $args); } /** * Reload an object from the database, discarding any changes to persistent * properties. This is primarily useful after entering a transaction but * before applying changes to an object. * * @return this * * @task load */ public function reload() { if (!$this->getID()) { throw new Exception("Unable to reload object that hasn't been loaded!"); } $result = $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $this->getID()); if (!$result) { throw new AphrontObjectMissingQueryException(); } return $this; } /** * Initialize this object's properties from a dictionary. Generally, you * load single objects with loadOneWhere(), but sometimes it may be more * convenient to pull data from elsewhere directly (e.g., a complicated * join via @{method:queryData}) and then load from an array representation. * * @param dict Dictionary of properties, which should be equivalent to * selecting a row from the table or calling * @{method:getProperties}. * @return this * * @task load */ public function loadFromArray(array $row) { static $valid_properties = array(); $map = array(); foreach ($row as $k => $v) { // We permit (but ignore) extra properties in the array because a // common approach to building the array is to issue a raw SELECT query // which may include extra explicit columns or joins. // This pathway is very hot on some pages, so we're inlining a cache // and doing some microoptimization to avoid a strtolower() call for each // assignment. The common path (assigning a valid property which we've // already seen) always incurs only one empty(). The second most common // path (assigning an invalid property which we've already seen) costs // an empty() plus an isset(). if (empty($valid_properties[$k])) { if (isset($valid_properties[$k])) { // The value is set but empty, which means it's false, so we've // already determined it's not valid. We don't need to check again. continue; } $valid_properties[$k] = $this->hasProperty($k); if (!$valid_properties[$k]) { continue; } } $map[$k] = $v; } $this->willReadData($map); foreach ($map as $prop => $value) { $this->$prop = $value; } $this->didReadData(); return $this; } /** * Initialize a list of objects from a list of dictionaries. Usually you * load lists of objects with @{method:loadAllWhere}, but sometimes that * isn't flexible enough. One case is if you need to do joins to select the * right objects: * * function loadAllWithOwner($owner) { * $data = $this->queryData( * 'SELECT d.* * FROM owner o * JOIN owner_has_dog od ON o.id = od.ownerID * JOIN dog d ON od.dogID = d.id * WHERE o.id = %d', * $owner); * return $this->loadAllFromArray($data); * } * * This is a lot messier than @{method:loadAllWhere}, but more flexible. * * @param list List of property dictionaries. * @return dict List of constructed objects, keyed on ID. * * @task load */ public function loadAllFromArray(array $rows) { $result = array(); $id_key = $this->getIDKey(); foreach ($rows as $row) { $obj = clone $this; if ($id_key && isset($row[$id_key])) { $result[$row[$id_key]] = $obj->loadFromArray($row); } else { $result[] = $obj->loadFromArray($row); } if ($this->inSet) { $this->inSet->addToSet($obj); } } return $result; } /** * This method helps to prevent the 1+N queries problem. It happens when you * execute a query for each row in a result set. Like in this code: * * COUNTEREXAMPLE, name=Easy to write but expensive to execute * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * foreach ($diffs as $diff) { * $changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID = %d', * $diff->getID()); * // Do something with $changesets. * } * * One can solve this problem by reading all the dependent objects at once and * assigning them later: * * COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * $all_changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID IN (%Ld)', * mpull($diffs, 'getID')); * $all_changesets = mgroup($all_changesets, 'getDiffID'); * foreach ($diffs as $diff) { * $changesets = idx($all_changesets, $diff->getID(), array()); * // Do something with $changesets. * } * * The method @{method:loadRelatives} abstracts this approach which allows * writing a code which is simple and efficient at the same time: * * name=Easy to write and cheap to execute * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * // Do something with $changesets. * } * * This will load dependent objects for all diffs in the first call of * @{method:loadRelatives} and use this result for all following calls. * * The method supports working with set of sets, like in this code: * * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * foreach ($changesets as $changeset) { * $hunks = $changeset->loadRelatives( * new DifferentialHunk(), * 'changesetID'); * // Do something with hunks. * } * } * * This code will execute just three queries - one to load all diffs, one to * load all their related changesets and one to load all their related hunks. * You can try to write an equivalent code without using this method as * a homework. * * The method also supports retrieving referenced objects, for example authors * of all diffs (using shortcut @{method:loadOneRelative}): * * foreach ($diffs as $diff) { * $author = $diff->loadOneRelative( * new PhabricatorUser(), * 'phid', * 'getAuthorPHID'); * // Do something with author. * } * * It is also possible to specify additional conditions for the `WHERE` * clause. Similarly to @{method:loadAllWhere}, you can specify everything * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is * allowed to pass only a constant string (`%` doesn't have a special * meaning). This is intentional to avoid mistakes with using data from one * row in retrieving other rows. Example of a correct usage: * * $status = $author->loadOneRelative( * new PhabricatorCalendarEvent(), * 'userPHID', * 'getPHID', * '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)'); * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return list Objects of type $object. * * @task load */ public function loadRelatives( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { if (!$this->inSet) { id(new LiskDAOSet())->addToSet($this); } $relatives = $this->inSet->loadRelatives( $object, $foreign_column, $key_method, $where); return idx($relatives, $this->$key_method(), array()); } /** * Load referenced row. See @{method:loadRelatives} for details. * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return LiskDAO Object of type $object or null if there's no such object. * * @task load */ final public function loadOneRelative( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { $relatives = $this->loadRelatives( $object, $foreign_column, $key_method, $where); if (!$relatives) { return null; } if (count($relatives) > 1) { throw new AphrontCountQueryException( 'More than 1 result from loadOneRelative()!'); } return reset($relatives); } final public function putInSet(LiskDAOSet $set) { $this->inSet = $set; return $this; } final protected function getInSet() { return $this->inSet; } /* -( Examining Objects )-------------------------------------------------- */ /** * Set unique ID identifying this object. You normally don't need to call this * method unless with `IDS_MANUAL`. * * @param mixed Unique ID. * @return this * @task save */ public function setID($id) { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } $this->$id_key = $id; return $this; } /** * Retrieve the unique ID identifying this object. This value will be null if * the object hasn't been persisted and you didn't set it manually. * * @return mixed Unique ID. * * @task info */ public function getID() { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } return $this->$id_key; } public function getPHID() { return $this->phid; } /** * Test if a property exists. * * @param string Property name. * @return bool True if the property exists. * @task info */ public function hasProperty($property) { return (bool)$this->checkProperty($property); } /** * Retrieve a list of all object properties. This list only includes * properties that are declared as protected, and it is expected that * all properties returned by this function should be persisted to the * database. * Properties that should not be persisted must be declared as private. * * @return dict Dictionary of normalized (lowercase) to canonical (original * case) property names. * * @task info */ protected function getAllLiskProperties() { static $properties = null; if (!isset($properties)) { $class = new ReflectionClass(get_class($this)); $properties = array(); foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { $properties[strtolower($p->getName())] = $p->getName(); } $id_key = $this->getIDKey(); if ($id_key != 'id') { unset($properties['id']); } if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) { unset($properties['datecreated']); unset($properties['datemodified']); } if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) { unset($properties['phid']); } } return $properties; } /** * Check if a property exists on this object. * * @return string|null Canonical property name, or null if the property * does not exist. * * @task info */ protected function checkProperty($property) { static $properties = null; if ($properties === null) { $properties = $this->getAllLiskProperties(); } $property = strtolower($property); if (empty($properties[$property])) { return null; } return $properties[$property]; } /** * Get or build the database connection for this object. * * @param string 'r' for read, 'w' for read/write. * @param bool True to force a new connection. The connection will not * be retrieved from or saved into the connection cache. * @return LiskDatabaseConnection Lisk connection object. * * @task info */ public function establishConnection($mode, $force_new = false) { if ($mode != 'r' && $mode != 'w') { throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'."); } if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) { $mode = 'isolate-'.$mode; $connection = $this->getEstablishedConnection($mode); if (!$connection) { $connection = $this->establishIsolatedConnection($mode); $this->setEstablishedConnection($mode, $connection); } return $connection; } if (self::shouldIsolateAllLiskEffectsToTransactions()) { // If we're doing fixture transaction isolation, force the mode to 'w' // so we always get the same connection for reads and writes, and thus // can see the writes inside the transaction. $mode = 'w'; } // TODO: There is currently no protection on 'r' queries against writing. $connection = null; if (!$force_new) { if ($mode == 'r') { // If we're requesting a read connection but already have a write // connection, reuse the write connection so that reads can take place // inside transactions. $connection = $this->getEstablishedConnection('w'); } if (!$connection) { $connection = $this->getEstablishedConnection($mode); } } if (!$connection) { $connection = $this->establishLiveConnection($mode); if (self::shouldIsolateAllLiskEffectsToTransactions()) { $connection->openTransaction(); } $this->setEstablishedConnection( $mode, $connection, $force_unique = $force_new); } return $connection; } /** * Convert this object into a property dictionary. This dictionary can be * restored into an object by using @{method:loadFromArray} (unless you're * using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you * should just go ahead and die in a fire). * * @return dict Dictionary of object properties. * * @task info */ protected function getAllLiskPropertyValues() { $map = array(); foreach ($this->getAllLiskProperties() as $p) { // We may receive a warning here for properties we've implicitly added // through configuration; squelch it. $map[$p] = @$this->$p; } return $map; } /* -( Writing Objects )---------------------------------------------------- */ /** * Make an object read-only. * * Making an object ephemeral indicates that you will be changing state in * such a way that you would never ever want it to be written back to the * storage. */ public function makeEphemeral() { $this->ephemeral = true; return $this; } private function isEphemeralCheck() { if ($this->ephemeral) { throw new LiskEphemeralObjectException(); } } /** * Persist this object to the database. In most cases, this is the only * method you need to call to do writes. If the object has not yet been * inserted this will do an insert; if it has, it will do an update. * * @return this * * @task save */ public function save() { if ($this->shouldInsertWhenSaved()) { return $this->insert(); } else { return $this->update(); } } /** * Save this object, forcing the query to use REPLACE regardless of object * state. * * @return this * * @task save */ public function replace() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('REPLACE'); } /** * Save this object, forcing the query to use INSERT regardless of object * state. * * @return this * * @task save */ public function insert() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('INSERT'); } /** * Save this object, forcing the query to use UPDATE regardless of object * state. * * @return this * * @task save */ public function update() { $this->isEphemeralCheck(); $this->willSaveObject(); $data = $this->getAllLiskPropertyValues(); $this->willWriteData($data); $map = array(); foreach ($data as $k => $v) { $map[$k] = $v; } $conn = $this->establishConnection('w'); $binary = $this->getBinaryColumns(); foreach ($map as $key => $value) { if (!empty($binary[$key])) { $map[$key] = qsprintf($conn, '%C = %nB', $key, $value); } else { $map[$key] = qsprintf($conn, '%C = %ns', $key, $value); } } $map = implode(', ', $map); $id = $this->getID(); $conn->query( 'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'), $this->getTableName(), $map, $this->getIDKeyForUse(), $id); // We can't detect a missing object because updating an object without // changing any values doesn't affect rows. We could jiggle timestamps // to catch this for objects which track them if we wanted. $this->didWriteData(); return $this; } /** * Delete this object, permanently. * * @return this * * @task save */ public function delete() { $this->isEphemeralCheck(); $this->willDelete(); $conn = $this->establishConnection('w'); $conn->query( 'DELETE FROM %T WHERE %C = %d', $this->getTableName(), $this->getIDKeyForUse(), $this->getID()); $this->didDelete(); return $this; } /** * Internal implementation of INSERT and REPLACE. * * @param const Either "INSERT" or "REPLACE", to force the desired mode. * * @task save */ protected function insertRecordIntoDatabase($mode) { $this->willSaveObject(); $data = $this->getAllLiskPropertyValues(); $conn = $this->establishConnection('w'); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); switch ($id_mechanism) { case self::IDS_AUTOINCREMENT: // If we are using autoincrement IDs, let MySQL assign the value for the // ID column, if it is empty. If the caller has explicitly provided a // value, use it. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { unset($data[$id_key]); } break; case self::IDS_COUNTER: // If we are using counter IDs, assign a new ID if we don't already have // one. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { $counter_name = $this->getTableName(); $id = self::loadNextCounterID($conn, $counter_name); $this->setID($id); $data[$id_key] = $id; } break; case self::IDS_MANUAL: break; default: throw new Exception('Unknown CONFIG_IDs mechanism!'); } $this->willWriteData($data); $columns = array_keys($data); $binary = $this->getBinaryColumns(); foreach ($data as $key => $value) { try { if (!empty($binary[$key])) { $data[$key] = qsprintf($conn, '%nB', $value); } else { $data[$key] = qsprintf($conn, '%ns', $value); } } catch (AphrontParameterQueryException $parameter_exception) { throw new PhutilProxyException( pht( "Unable to insert or update object of class %s, field '%s' ". "has a nonscalar value.", get_class($this), $key), $parameter_exception); } } $data = implode(', ', $data); $conn->query( '%Q INTO %T (%LC) VALUES (%Q)', $mode, $this->getTableName(), $columns, $data); // Only use the insert id if this table is using auto-increment ids if ($id_mechanism === self::IDS_AUTOINCREMENT) { $this->setID($conn->getInsertID()); } $this->didWriteData(); return $this; } /** * Method used to determine whether to insert or update when saving. * * @return bool true if the record should be inserted */ protected function shouldInsertWhenSaved() { $key_type = $this->getConfigOption(self::CONFIG_IDS); if ($key_type == self::IDS_MANUAL) { throw new Exception( 'You are using manual IDs. You must override the '. 'shouldInsertWhenSaved() method to properly detect '. 'when to insert a new record.'); } else { return !$this->getID(); } } /* -( Hooks and Callbacks )------------------------------------------------ */ /** * Retrieve the database table name. By default, this is the class name. * * @return string Table name for object storage. * * @task hook */ public function getTableName() { return get_class($this); } /** * Retrieve the primary key column, "id" by default. If you can not * reasonably name your ID column "id", override this method. * * @return string Name of the ID column. * * @task hook */ public function getIDKey() { return 'id'; } protected function getIDKeyForUse() { $id_key = $this->getIDKey(); if (!$id_key) { throw new Exception( 'This DAO does not have a single-part primary key. The method you '. 'called requires a single-part primary key.'); } return $id_key; } /** * Generate a new PHID, used by CONFIG_AUX_PHID. * * @return phid Unique, newly allocated PHID. * * @task hook */ protected function generatePHID() { throw new Exception( 'To use CONFIG_AUX_PHID, you need to overload '. 'generatePHID() to perform PHID generation.'); } /** * Hook to apply serialization or validation to data before it is written to * the database. See also @{method:willReadData}. * * @task hook */ protected function willWriteData(array &$data) { $this->applyLiskDataSerialization($data, false); } /** * Hook to perform actions after data has been written to the database. * * @task hook */ protected function didWriteData() {} /** * Hook to make internal object state changes prior to INSERT, REPLACE or * UPDATE. * * @task hook */ protected function willSaveObject() { $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS); if ($use_timestamps) { if (!$this->getDateCreated()) { $this->setDateCreated(time()); } $this->setDateModified(time()); } if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) { $this->setPHID($this->generatePHID()); } } /** * Hook to apply serialization or validation to data as it is read from the * database. See also @{method:willWriteData}. * * @task hook */ protected function willReadData(array &$data) { $this->applyLiskDataSerialization($data, $deserialize = true); } /** * Hook to perform an action on data after it is read from the database. * * @task hook */ protected function didReadData() {} /** * Hook to perform an action before the deletion of an object. * * @task hook */ protected function willDelete() {} /** * Hook to perform an action after the deletion of an object. * * @task hook */ protected function didDelete() {} /** * Reads the value from a field. Override this method for custom behavior * of @{method:getField} instead of overriding getField directly. * * @param string Canonical field name * @return mixed Value of the field * * @task hook */ protected function readField($field) { if (isset($this->$field)) { return $this->$field; } return null; } /** * Writes a value to a field. Override this method for custom behavior of * setField($value) instead of overriding setField directly. * * @param string Canonical field name * @param mixed Value to write * * @task hook */ protected function writeField($field, $value) { $this->$field = $value; } /* -( Manging Transactions )----------------------------------------------- */ /** * Increase transaction stack depth. * * @return this */ public function openTransaction() { $this->establishConnection('w')->openTransaction(); return $this; } /** * Decrease transaction stack depth, saving work. * * @return this */ public function saveTransaction() { $this->establishConnection('w')->saveTransaction(); return $this; } /** * Decrease transaction stack depth, discarding work. * * @return this */ public function killTransaction() { $this->establishConnection('w')->killTransaction(); return $this; } /** * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that * other connections can not read them (this is an enormous oversimplification * of FOR UPDATE semantics; consult the MySQL documentation for details). To * end read locking, call @{method:endReadLocking}. For example: * * $beach->openTransaction(); * $beach->beginReadLocking(); * * $beach->reload(); * $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1); * $beach->save(); * * $beach->endReadLocking(); * $beach->saveTransaction(); * * @return this * @task xaction */ public function beginReadLocking() { $this->establishConnection('w')->beginReadLocking(); return $this; } /** * Ends read-locking that began at an earlier @{method:beginReadLocking} call. * * @return this * @task xaction */ public function endReadLocking() { $this->establishConnection('w')->endReadLocking(); return $this; } /** * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so * that other connections can not update or delete them (this is an * oversimplification of LOCK IN SHARE MODE semantics; consult the * MySQL documentation for details). To end write locking, call * @{method:endWriteLocking}. * * @return this * @task xaction */ public function beginWriteLocking() { $this->establishConnection('w')->beginWriteLocking(); return $this; } /** * Ends write-locking that began at an earlier @{method:beginWriteLocking} * call. * * @return this * @task xaction */ public function endWriteLocking() { $this->establishConnection('w')->endWriteLocking(); return $this; } /* -( Isolation )---------------------------------------------------------- */ /** * @task isolate */ public static function beginIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel--; if (self::$processIsolationLevel < 0) { throw new Exception( 'Lisk process isolation level was reduced below 0.'); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToCurrentProcess() { return (bool)self::$processIsolationLevel; } /** * @task isolate */ private function establishIsolatedConnection($mode) { $config = array(); return new AphrontIsolatedDatabaseConnection($config); } /** * @task isolate */ public static function beginIsolateAllLiskEffectsToTransactions() { if (self::$transactionIsolationLevel === 0) { self::closeAllConnections(); } self::$transactionIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToTransactions() { self::$transactionIsolationLevel--; if (self::$transactionIsolationLevel < 0) { throw new Exception( 'Lisk transaction isolation level was reduced below 0.'); } else if (self::$transactionIsolationLevel == 0) { foreach (self::$connections as $key => $conn) { if ($conn) { $conn->killTransaction(); } } self::closeAllConnections(); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToTransactions() { return (bool)self::$transactionIsolationLevel; } public static function closeAllConnections() { self::$connections = array(); } /* -( Utilities )---------------------------------------------------------- */ /** * Applies configured serialization to a dictionary of values. * * @task util */ protected function applyLiskDataSerialization(array &$data, $deserialize) { $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if ($serialization) { foreach (array_intersect_key($serialization, $data) as $col => $format) { switch ($format) { case self::SERIALIZATION_NONE: break; case self::SERIALIZATION_PHP: if ($deserialize) { $data[$col] = unserialize($data[$col]); } else { $data[$col] = serialize($data[$col]); } break; case self::SERIALIZATION_JSON: if ($deserialize) { $data[$col] = json_decode($data[$col], true); } else { $data[$col] = json_encode($data[$col]); } break; default: throw new Exception("Unknown serialization format '{$format}'."); } } } } /** * Black magic. Builds implied get*() and set*() for all properties. * * @param string Method name. * @param list Argument vector. * @return mixed get*() methods return the property value. set*() methods * return $this. * @task util */ public function __call($method, $args) { // NOTE: PHP has a bug that static variables defined in __call() are shared // across all children classes. Call a different method to work around this // bug. return $this->call($method, $args); } /** * @task util */ final protected function call($method, $args) { // NOTE: This method is very performance-sensitive (many thousands of calls // per page on some pages), and thus has some silliness in the name of // optimizations. static $dispatch_map = array(); if ($method[0] === 'g') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'get') { throw new Exception("Unable to resolve method '{$method}'!"); } $property = substr($method, 3); if (!($property = $this->checkProperty($property))) { throw new Exception("Bad getter call: {$method}"); } $dispatch_map[$method] = $property; } return $this->readField($property); } if ($method[0] === 's') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'set') { throw new Exception("Unable to resolve method '{$method}'!"); } $property = substr($method, 3); $property = $this->checkProperty($property); if (!$property) { throw new Exception("Bad setter call: {$method}"); } $dispatch_map[$method] = $property; } $this->writeField($property, $args[0]); return $this; } throw new Exception("Unable to resolve method '{$method}'."); } /** * Warns against writing to undeclared property. * * @task util */ public function __set($name, $value) { phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.'); $this->$name = $value; } /** * Increments a named counter and returns the next value. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to create or increment. * @return int Next counter value. * * @task util */ public static function loadNextCounterID( AphrontDatabaseConnection $conn_w, $counter_name) { // NOTE: If an insert does not touch an autoincrement row or call // LAST_INSERT_ID(), MySQL normally does not change the value of // LAST_INSERT_ID(). This can cause a counter's value to leak to a // new counter if the second counter is created after the first one is // updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the // LAST_INSERT_ID() is always updated and always set correctly after the // query completes. queryfx( $conn_w, 'INSERT INTO %T (counterName, counterValue) VALUES (%s, LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE counterValue = LAST_INSERT_ID(counterValue + 1)', self::COUNTER_TABLE_NAME, $counter_name); return $conn_w->getInsertID(); } private function getBinaryColumns() { return $this->getConfigOption(self::CONFIG_BINARY); } public function getSchemaColumns() { $custom_map = $this->getConfigOption(self::CONFIG_COLUMN_SCHEMA); if (!$custom_map) { $custom_map = array(); } $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if (!$serialization) { $serialization = array(); } $serialization_map = array( self::SERIALIZATION_JSON => 'text', self::SERIALIZATION_PHP => 'bytes', ); $binary_map = $this->getBinaryColumns(); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); if ($id_mechanism == self::IDS_AUTOINCREMENT) { $id_type = 'auto'; } else { $id_type = 'id'; } $builtin = array( 'id' => $id_type, 'phid' => 'phid', 'viewPolicy' => 'policy', 'editPolicy' => 'policy', 'epoch' => 'epoch', 'dateCreated' => 'epoch', 'dateModified' => 'epoch', ); $map = array(); foreach ($this->getAllLiskProperties() as $property) { // First, use types specified explicitly in the table configuration. if (array_key_exists($property, $custom_map)) { $map[$property] = $custom_map[$property]; continue; } // If we don't have an explicit type, try a builtin type for the // column. $type = idx($builtin, $property); if ($type) { $map[$property] = $type; continue; } // If the column has serialization, we can infer the column type. if (isset($serialization[$property])) { $type = idx($serialization_map, $serialization[$property]); if ($type) { $map[$property] = $type; continue; } } if (isset($binary_map[$property])) { $map[$property] = 'bytes'; continue; } // If the column is named `somethingPHID`, infer it is a PHID. if (preg_match('/[a-z]PHID$/', $property)) { $map[$property] = 'phid'; continue; } // If the column is named `somethingID`, infer it is an ID. if (preg_match('/[a-z]ID$/', $property)) { $map[$property] = 'id'; continue; } // We don't know the type of this column. $map[$property] = PhabricatorConfigSchemaSpec::DATATYPE_UNKNOWN; } return $map; } public function getSchemaKeys() { $custom_map = $this->getConfigOption(self::CONFIG_KEY_SCHEMA); if (!$custom_map) { $custom_map = array(); } $default_map = array(); foreach ($this->getAllLiskProperties() as $property) { switch ($property) { case 'id': $default_map['PRIMARY'] = array( 'columns' => array('id'), 'unique' => true, ); break; case 'phid': $default_map['key_phid'] = array( 'columns' => array('phid'), 'unique' => true, ); break; } } return $custom_map + $default_map; } } diff --git a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php index 3929787173..02c79544db 100644 --- a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php +++ b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php @@ -1,32 +1,32 @@ true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('TISO'); } public function establishLiveConnection($mode) { throw new LiskIsolationTestDAOException( 'Isolation failure! DAO is attempting to connect to an external '. 'resource!'); } public function getConnectionNamespace() { return 'test'; } public function getTableName() { return 'test'; } }