diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 5577ee5e7d..a7096fc51f 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -1,268 +1,264 @@ setServicePHID($service->getPHID()) ->attachService($service) ->attachAlmanacProperties(array()) ->setIsDisabled(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'mailKey' => 'bytes20', 'isDisabled' => 'bool', ), 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 getName() { return pht('Binding %s', $this->getID()); } 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 hasInterface() { return ($this->interface !== self::ATTACHABLE); } 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 $this->getService()->getBindingFieldSpecifications($this); } public function newAlmanacPropertyEditEngine() { return new AlmanacBindingPropertyEditEngine(); } public function getAlmanacPropertySetTransactionType() { return AlmanacBindingSetPropertyTransaction::TRANSACTIONTYPE; } public function getAlmanacPropertyDeleteTransactionType() { return AlmanacBindingDeletePropertyTransaction::TRANSACTIONTYPE; } /* -( 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.'), ); return $notes; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getService()->isClusterService()) { return array( array( new PhabricatorAlmanacApplication(), AlmanacManageClusterServicesCapability::CAPABILITY, ), ); } break; } return array(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacBindingEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacBindingTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('servicePHID') ->setType('phid') ->setDescription(pht('The bound service.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('devicePHID') ->setType('phid') ->setDescription(pht('The device the service is bound to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('interfacePHID') ->setType('phid') ->setDescription(pht('The interface the service is bound to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('disabled') ->setType('bool') ->setDescription(pht('Interface status.')), ); } public function getFieldValuesForConduit() { return array( 'servicePHID' => $this->getServicePHID(), 'devicePHID' => $this->getDevicePHID(), 'interfacePHID' => $this->getInterfacePHID(), 'disabled' => (bool)$this->getIsDisabled(), ); } public function getConduitSearchAttachments() { return array( id(new AlmanacPropertiesSearchEngineAttachment()) ->setAttachmentKey('properties'), ); } } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index f25b2eb5a1..1d1010733a 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -1,290 +1,286 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) ->setIsBoundToClusterService(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', 'isBoundToClusterService' => '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::validateName($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().'/'; } public function rebuildClusterBindingStatus() { $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDevicePHIDs(array($this->getPHID())) ->execute(); $is_cluster = false; foreach ($services as $service) { if ($service->isClusterService()) { $is_cluster = true; break; } } if ($is_cluster != $this->getIsBoundToClusterService()) { $this->setIsBoundToClusterService((int)$is_cluster); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %T SET isBoundToClusterService = %d WHERE id = %d', $this->getTableName(), $this->getIsBoundToClusterService(), $this->getID()); unset($unguarded); } return $this; } public function isClusterDevice() { return $this->getIsBoundToClusterService(); } /* -( 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(); } public function newAlmanacPropertyEditEngine() { return new AlmanacDevicePropertyEditEngine(); } public function getAlmanacPropertySetTransactionType() { return AlmanacDeviceSetPropertyTransaction::TRANSACTIONTYPE; } public function getAlmanacPropertyDeleteTransactionType() { return AlmanacDeviceDeletePropertyTransaction::TRANSACTIONTYPE; } /* -( 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; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isClusterDevice()) { return array( array( new PhabricatorAlmanacApplication(), AlmanacManageClusterServicesCapability::CAPABILITY, ), ); } break; } return array(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacDeviceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacDeviceTransaction(); } /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) { return $this->getURI(); } public function getSSHKeyDefaultName() { return $this->getName(); } public function getSSHKeyNotifyPHIDs() { // Devices don't currently have anyone useful to notify about SSH key // edits, and they're usually a difficult vector to attack since you need // access to a cluster host. However, it would be nice to make them // subscribable at some point. return array(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $interfaces = id(new AlmanacInterfaceQuery()) ->setViewer($engine->getViewer()) ->withDevicePHIDs(array($this->getPHID())) ->execute(); foreach ($interfaces as $interface) { $engine->destroyObject($interface); } $this->delete(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new AlmanacDeviceNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the device.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), ); } public function getConduitSearchAttachments() { return array( id(new AlmanacPropertiesSearchEngineAttachment()) ->setAttachmentKey('properties'), ); } } diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index 9a5023e751..6cd318186f 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -1,217 +1,213 @@ 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'), ), 'key_unique' => array( 'columns' => array('devicePHID', 'networkPHID', 'address', 'port'), 'unique' => true, ), ), ) + 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(); } public function loadIsInUse() { $binding = id(new AlmanacBindingQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withInterfacePHIDs(array($this->getPHID())) ->setLimit(1) ->executeOne(); return (bool)$binding; } /* -( 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.'), ); return $notes; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getDevice()->isClusterDevice()) { return array( array( new PhabricatorAlmanacApplication(), AlmanacManageClusterServicesCapability::CAPABILITY, ), ); } break; } return array(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $bindings = id(new AlmanacBindingQuery()) ->setViewer($engine->getViewer()) ->withInterfacePHIDs(array($this->getPHID())) ->execute(); foreach ($bindings as $binding) { $engine->destroyObject($binding); } $this->delete(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacInterfaceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacInterfaceTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('devicePHID') ->setType('phid') ->setDescription(pht('The device the interface is on.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('networkPHID') ->setType('phid') ->setDescription(pht('The network the interface is part of.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('address') ->setType('string') ->setDescription(pht('The address of the interface.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('port') ->setType('int') ->setDescription(pht('The port number of the interface.')), ); } public function getFieldValuesForConduit() { return array( 'devicePHID' => $this->getDevicePHID(), 'networkPHID' => $this->getNetworkPHID(), 'address' => (string)$this->getAddress(), 'port' => (int)$this->getPort(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/almanac/storage/AlmanacNamespace.php b/src/applications/almanac/storage/AlmanacNamespace.php index 0c8f0c5ec7..128cfdb72e 100644 --- a/src/applications/almanac/storage/AlmanacNamespace.php +++ b/src/applications/almanac/storage/AlmanacNamespace.php @@ -1,246 +1,242 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_nameindex' => array( 'columns' => array('nameIndex'), 'unique' => true, ), 'key_name' => array( 'columns' => array('name'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( AlmanacNamespacePHIDType::TYPECONST); } public function save() { AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function getURI() { return '/almanac/namespace/view/'.$this->getName().'/'; } public function getNameLength() { return strlen($this->getName()); } /** * Load the namespace which prevents use of an Almanac name, if one exists. */ public static function loadRestrictedNamespace( PhabricatorUser $viewer, $name) { // For a name like "x.y.z", produce a list of controlling namespaces like // ("z", "y.x", "x.y.z"). $names = array(); $parts = explode('.', $name); for ($ii = 0; $ii < count($parts); $ii++) { $names[] = implode('.', array_slice($parts, -($ii + 1))); } // Load all the possible controlling namespaces. $namespaces = id(new AlmanacNamespaceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withNames($names) ->execute(); if (!$namespaces) { return null; } // Find the "nearest" (longest) namespace that exists. If both // "sub.domain.com" and "domain.com" exist, we only care about the policy // on the former. $namespaces = msort($namespaces, 'getNameLength'); $namespace = last($namespaces); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $namespace, PhabricatorPolicyCapability::CAN_EDIT); if ($can_edit) { return null; } return $namespace; } /* -( 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(); } public function newAlmanacPropertyEditEngine() { throw new PhutilMethodNotImplementedException(); } public function getAlmanacPropertySetTransactionType() { throw new PhutilMethodNotImplementedException(); } public function getAlmanacPropertyDeleteTransactionType() { throw new PhutilMethodNotImplementedException(); } /* -( 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; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacNamespaceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacNamespaceTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new AlmanacNamespaceNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the namespace.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index 3913d2f6e7..78313fad77 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -1,149 +1,145 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + 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(); } /* -( 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; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $interfaces = id(new AlmanacInterfaceQuery()) ->setViewer($engine->getViewer()) ->withNetworkPHIDs(array($this->getPHID())) ->execute(); foreach ($interfaces as $interface) { $engine->destroyObject($interface); } $this->delete(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new AlmanacNetworkNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the network.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 7348abd936..2979b74367 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -1,299 +1,295 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) ->setServiceType($type) ->attachServiceImplementation($implementation); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', 'serviceType' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('nameIndex'), 'unique' => true, ), 'key_nametext' => array( 'columns' => array('name'), ), 'key_servicetype' => array( 'columns' => array('serviceType'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(AlmanacServicePHIDType::TYPECONST); } public function save() { AlmanacNames::validateName($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 getActiveBindings() { $bindings = $this->getBindings(); // Filter out disabled bindings. foreach ($bindings as $key => $binding) { if ($binding->getIsDisabled()) { unset($bindings[$key]); } } return $bindings; } public function attachBindings(array $bindings) { $this->bindings = $bindings; return $this; } public function getServiceImplementation() { return $this->assertAttached($this->serviceImplementation); } public function attachServiceImplementation(AlmanacServiceType $type) { $this->serviceImplementation = $type; return $this; } public function isClusterService() { return $this->getServiceImplementation()->isClusterServiceType(); } /* -( 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->getServiceImplementation()->getFieldSpecifications(); } public function getBindingFieldSpecifications(AlmanacBinding $binding) { $impl = $this->getServiceImplementation(); return $impl->getBindingFieldSpecifications($binding); } public function newAlmanacPropertyEditEngine() { return new AlmanacServicePropertyEditEngine(); } public function getAlmanacPropertySetTransactionType() { return AlmanacServiceSetPropertyTransaction::TRANSACTIONTYPE; } public function getAlmanacPropertyDeleteTransactionType() { return AlmanacServiceDeletePropertyTransaction::TRANSACTIONTYPE; } /* -( 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; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isClusterService()) { return array( array( new PhabricatorAlmanacApplication(), AlmanacManageClusterServicesCapability::CAPABILITY, ), ); } break; } return array(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacServiceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacServiceTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $bindings = id(new AlmanacBindingQuery()) ->setViewer($engine->getViewer()) ->withServicePHIDs(array($this->getPHID())) ->execute(); foreach ($bindings as $binding) { $engine->destroyObject($binding); } $this->delete(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new AlmanacServiceNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the service.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('serviceType') ->setType('string') ->setDescription(pht('The service type constant.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'serviceType' => $this->getServiceType(), ); } public function getConduitSearchAttachments() { return array( id(new AlmanacPropertiesSearchEngineAttachment()) ->setAttachmentKey('properties'), id(new AlmanacBindingsSearchEngineAttachment()) ->setAttachmentKey('bindings'), ); } } diff --git a/src/applications/auth/storage/PhabricatorAuthPassword.php b/src/applications/auth/storage/PhabricatorAuthPassword.php index 930cbe61cb..3196b58e60 100644 --- a/src/applications/auth/storage/PhabricatorAuthPassword.php +++ b/src/applications/auth/storage/PhabricatorAuthPassword.php @@ -1,228 +1,224 @@ setObjectPHID($object->getPHID()) ->attachObject($object) ->setPasswordType($type) ->setIsRevoked(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'passwordType' => 'text64', 'passwordHash' => 'text128', 'passwordSalt' => 'text64', 'isRevoked' => 'bool', 'legacyDigestFormat' => 'text32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_role' => array( 'columns' => array('objectPHID', 'passwordType'), ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhabricatorAuthPasswordPHIDType::TYPECONST; } public function getObject() { return $this->assertAttached($this->object); } public function attachObject($object) { $this->object = $object; return $this; } public function getHasher() { $hash = $this->newPasswordEnvelope(); return PhabricatorPasswordHasher::getHasherForHash($hash); } public function canUpgrade() { // If this password uses a legacy digest format, we can upgrade it to the // new digest format even if a better hasher isn't available. if ($this->getLegacyDigestFormat() !== null) { return true; } $hash = $this->newPasswordEnvelope(); return PhabricatorPasswordHasher::canUpgradeHash($hash); } public function upgradePasswordHasher( PhutilOpaqueEnvelope $envelope, PhabricatorAuthPasswordHashInterface $object) { // Before we make changes, double check that this is really the correct // password. It could be really bad if we "upgraded" a password and changed // the secret! if (!$this->comparePassword($envelope, $object)) { throw new Exception( pht( 'Attempting to upgrade password hasher, but the password for the '. 'upgrade is not the stored credential!')); } return $this->setPassword($envelope, $object); } public function setPassword( PhutilOpaqueEnvelope $password, PhabricatorAuthPasswordHashInterface $object) { $hasher = PhabricatorPasswordHasher::getBestHasher(); return $this->setPasswordWithHasher($password, $object, $hasher); } public function setPasswordWithHasher( PhutilOpaqueEnvelope $password, PhabricatorAuthPasswordHashInterface $object, PhabricatorPasswordHasher $hasher) { if (!strlen($password->openEnvelope())) { throw new Exception( pht('Attempting to set an empty password!')); } // Generate (or regenerate) the salt first. $new_salt = Filesystem::readRandomCharacters(64); $this->setPasswordSalt($new_salt); // Clear any legacy digest format to force a modern digest. $this->setLegacyDigestFormat(null); $digest = $this->digestPassword($password, $object); $hash = $hasher->getPasswordHashForStorage($digest); $raw_hash = $hash->openEnvelope(); return $this->setPasswordHash($raw_hash); } public function comparePassword( PhutilOpaqueEnvelope $password, PhabricatorAuthPasswordHashInterface $object) { $digest = $this->digestPassword($password, $object); $hash = $this->newPasswordEnvelope(); return PhabricatorPasswordHasher::comparePassword($digest, $hash); } public function newPasswordEnvelope() { return new PhutilOpaqueEnvelope($this->getPasswordHash()); } private function digestPassword( PhutilOpaqueEnvelope $password, PhabricatorAuthPasswordHashInterface $object) { $object_phid = $object->getPHID(); if ($this->getObjectPHID() !== $object->getPHID()) { throw new Exception( pht( 'This password is associated with an object PHID ("%s") for '. 'a different object than the provided one ("%s").', $this->getObjectPHID(), $object->getPHID())); } $digest = $object->newPasswordDigest($password, $this); if (!($digest instanceof PhutilOpaqueEnvelope)) { throw new Exception( pht( 'Failed to digest password: object ("%s") did not return an '. 'opaque envelope with a password digest.', $object->getPHID())); } return $digest; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { return array( array($this->getObject(), $capability), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuthPasswordEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorAuthPasswordTransaction(); } } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php index d4d1f5517c..1de34c4077 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php @@ -1,126 +1,122 @@ 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', 'shouldAutoLogin' => '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(); } /* -( 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; } } diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKey.php b/src/applications/auth/storage/PhabricatorAuthSSHKey.php index df4f09c11b..7350af8cfd 100644 --- a/src/applications/auth/storage/PhabricatorAuthSSHKey.php +++ b/src/applications/auth/storage/PhabricatorAuthSSHKey.php @@ -1,170 +1,166 @@ getPHID(); return id(new self()) ->setIsActive(1) ->setObjectPHID($object_phid) ->attachObject($object); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'keyType' => 'text255', 'keyIndex' => 'bytes12', 'keyBody' => 'text', 'keyComment' => 'text255', 'isTrusted' => 'bool', 'isActive' => 'bool?', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID'), ), 'key_active' => array( 'columns' => array('isActive', 'objectPHID'), ), // NOTE: This unique key includes a nullable column, effectively // constraining uniqueness on active keys only. 'key_activeunique' => array( 'columns' => array('keyIndex', 'isActive'), '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; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorAuthSSHKeyPHIDType::TYPECONST); } public function getURI() { $id = $this->getID(); return "/auth/sshkey/view/{$id}/"; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if (!$this->getIsActive()) { if ($capability == PhabricatorPolicyCapability::CAN_EDIT) { return PhabricatorPolicies::POLICY_NOONE; } } return $this->getObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if (!$this->getIsActive()) { return false; } return $this->getObject()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { if (!$this->getIsACtive()) { return pht( 'Revoked SSH keys can not be edited or reinstated.'); } return pht( 'SSH keys inherit the policies of the user or object they authenticate.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuthSSHKeyEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorAuthSSHKeyTransaction(); } } diff --git a/src/applications/badges/storage/PhabricatorBadgesBadge.php b/src/applications/badges/storage/PhabricatorBadgesBadge.php index c6701a5b62..2fc787d9ee 100644 --- a/src/applications/badges/storage/PhabricatorBadgesBadge.php +++ b/src/applications/badges/storage/PhabricatorBadgesBadge.php @@ -1,208 +1,204 @@ pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } public static function initializeNewBadge(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorBadgesApplication')) ->executeOne(); $view_policy = PhabricatorPolicies::getMostOpenPolicy(); $edit_policy = $app->getPolicy(PhabricatorBadgesDefaultEditCapability::CAPABILITY); return id(new PhabricatorBadgesBadge()) ->setIcon(self::DEFAULT_ICON) ->setQuality(PhabricatorBadgesQuality::DEFAULT_QUALITY) ->setCreatorPHID($actor->getPHID()) ->setEditPolicy($edit_policy) ->setFlavor('') ->setDescription('') ->setStatus(self::STATUS_ACTIVE); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'flavor' => 'text255', 'description' => 'text', 'icon' => 'text255', 'quality' => 'uint32', 'status' => 'text32', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_creator' => array( 'columns' => array('creatorPHID', 'dateModified'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PhabricatorBadgesPHIDType::TYPECONST); } public function isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } public function getViewURI() { return '/badges/view/'.$this->getID().'/'; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(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 PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorBadgesEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorBadgesTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $awards = id(new PhabricatorBadgesAwardQuery()) ->setViewer($engine->getViewer()) ->withBadgePHIDs(array($this->getPHID())) ->execute(); foreach ($awards as $award) { $engine->destroyObject($award); } $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the badge.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('creatorPHID') ->setType('phid') ->setDescription(pht('User PHID of the creator.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Active or archived status of the badge.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'creatorPHID' => $this->getCreatorPHID(), 'status' => $this->getStatus(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorNgramInterface )------------------------------------------ */ public function newNgrams() { return array( id(new PhabricatorBadgesBadgeNameNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index e699a890d4..9526fb1442 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -1,660 +1,656 @@ pht('Core Applications'), self::GROUP_UTILITIES => pht('Utilities'), self::GROUP_ADMIN => pht('Administration'), self::GROUP_DEVELOPER => pht('Developer Tools'), ); } final public function getApplicationName() { return 'application'; } final public function getTableName() { return 'application_application'; } final protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } final public function generatePHID() { return $this->getPHID(); } final public function save() { // When "save()" is called on applications, we just return without // actually writing anything to the database. return $this; } /* -( Application Information )-------------------------------------------- */ abstract public function getName(); public function getShortDescription() { return pht('%s Application', $this->getName()); } final public function isInstalled() { if (!$this->canUninstall()) { return true; } $prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes'); if (!$prototypes && $this->isPrototype()) { return false; } $uninstalled = PhabricatorEnv::getEnvConfig( 'phabricator.uninstalled-applications'); return empty($uninstalled[get_class($this)]); } public function isPrototype() { return false; } /** * Return `true` if this application should never appear in application lists * in the UI. Primarily intended for unit test applications or other * pseudo-applications. * * Few applications should be unlisted. For most applications, use * @{method:isLaunchable} to hide them from main launch views instead. * * @return bool True to remove application from UI lists. */ public function isUnlisted() { return false; } /** * Return `true` if this application is a normal application with a base * URI and a web interface. * * Launchable applications can be pinned to the home page, and show up in the * "Launcher" view of the Applications application. Making an application * unlaunchable prevents pinning and hides it from this view. * * Usually, an application should be marked unlaunchable if: * * - it is available on every page anyway (like search); or * - it does not have a web interface (like subscriptions); or * - it is still pre-release and being intentionally buried. * * To hide applications more completely, use @{method:isUnlisted}. * * @return bool True if the application is launchable. */ public function isLaunchable() { return true; } /** * Return `true` if this application should be pinned by default. * * Users who have not yet set preferences see a default list of applications. * * @param PhabricatorUser User viewing the pinned application list. * @return bool True if this application should be pinned by default. */ public function isPinnedByDefault(PhabricatorUser $viewer) { return false; } /** * Returns true if an application is first-party (developed by Phacility) * and false otherwise. * * @return bool True if this application is developed by Phacility. */ final public function isFirstParty() { $where = id(new ReflectionClass($this))->getFileName(); $root = phutil_get_library_root('phabricator'); if (!Filesystem::isDescendant($where, $root)) { return false; } if (Filesystem::isDescendant($where, $root.'/extensions')) { return false; } return true; } public function canUninstall() { return true; } final public function getPHID() { return 'PHID-APPS-'.get_class($this); } public function getTypeaheadURI() { return $this->isLaunchable() ? $this->getBaseURI() : null; } public function getBaseURI() { return null; } final public function getApplicationURI($path = '') { return $this->getBaseURI().ltrim($path, '/'); } public function getIcon() { return 'fa-puzzle-piece'; } public function getApplicationOrder() { return PHP_INT_MAX; } public function getApplicationGroup() { return self::GROUP_CORE; } public function getTitleGlyph() { return null; } final public function getHelpMenuItems(PhabricatorUser $viewer) { $items = array(); $articles = $this->getHelpDocumentationArticles($viewer); if ($articles) { foreach ($articles as $article) { $item = id(new PhabricatorActionView()) ->setName($article['name']) ->setHref($article['href']) ->addSigil('help-item') ->setOpenInNewWindow(true); $items[] = $item; } } $command_specs = $this->getMailCommandObjects(); if ($command_specs) { foreach ($command_specs as $key => $spec) { $object = $spec['object']; $class = get_class($this); $href = '/applications/mailcommands/'.$class.'/'.$key.'/'; $item = id(new PhabricatorActionView()) ->setName($spec['name']) ->setHref($href) ->addSigil('help-item') ->setOpenInNewWindow(true); $items[] = $item; } } if ($items) { $divider = id(new PhabricatorActionView()) ->addSigil('help-item') ->setType(PhabricatorActionView::TYPE_DIVIDER); array_unshift($items, $divider); } return array_values($items); } public function getHelpDocumentationArticles(PhabricatorUser $viewer) { return array(); } public function getOverview() { return null; } public function getEventListeners() { return array(); } public function getRemarkupRules() { return array(); } public function getQuicksandURIPatternBlacklist() { return array(); } public function getMailCommandObjects() { return array(); } /* -( URI Routing )-------------------------------------------------------- */ public function getRoutes() { return array(); } public function getResourceRoutes() { return array(); } /* -( Email Integration )-------------------------------------------------- */ public function supportsEmailIntegration() { return false; } final protected function getInboundEmailSupportLink() { return PhabricatorEnv::getDoclink('Configuring Inbound Email'); } public function getAppEmailBlurb() { throw new PhutilMethodNotImplementedException(); } /* -( Fact Integration )--------------------------------------------------- */ public function getFactObjectsForAnalysis() { return array(); } /* -( UI Integration )----------------------------------------------------- */ /** * You can provide an optional piece of flavor text for the application. This * is currently rendered in application launch views if the application has no * status elements. * * @return string|null Flavor text. * @task ui */ public function getFlavorText() { return null; } /** * Build items for the main menu. * * @param PhabricatorUser The viewing user. * @param AphrontController The current controller. May be null for special * pages like 404, exception handlers, etc. * @return list List of menu items. * @task ui */ public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { return array(); } /* -( Application Management )--------------------------------------------- */ final public static function getByClass($class_name) { $selected = null; $applications = self::getAllApplications(); foreach ($applications as $application) { if (get_class($application) == $class_name) { $selected = $application; break; } } if (!$selected) { throw new Exception(pht("No application '%s'!", $class_name)); } return $selected; } final public static function getAllApplications() { static $applications; if ($applications === null) { $apps = id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setSortMethod('getApplicationOrder') ->execute(); // Reorder the applications into "application order". Notably, this // ensures their event handlers register in application order. $apps = mgroup($apps, 'getApplicationGroup'); $group_order = array_keys(self::getApplicationGroups()); $apps = array_select_keys($apps, $group_order) + $apps; $apps = array_mergev($apps); $applications = $apps; } return $applications; } final public static function getAllInstalledApplications() { $all_applications = self::getAllApplications(); $apps = array(); foreach ($all_applications as $app) { if (!$app->isInstalled()) { continue; } $apps[] = $app; } return $apps; } /** * Determine if an application is installed, by application class name. * * To check if an application is installed //and// available to a particular * viewer, user @{method:isClassInstalledForViewer}. * * @param string Application class name. * @return bool True if the class is installed. * @task meta */ final public static function isClassInstalled($class) { return self::getByClass($class)->isInstalled(); } /** * Determine if an application is installed and available to a viewer, by * application class name. * * To check if an application is installed at all, use * @{method:isClassInstalled}. * * @param string Application class name. * @param PhabricatorUser Viewing user. * @return bool True if the class is installed for the viewer. * @task meta */ final public static function isClassInstalledForViewer( $class, PhabricatorUser $viewer) { if ($viewer->isOmnipotent()) { return true; } $cache = PhabricatorCaches::getRequestCache(); $viewer_fragment = $viewer->getCacheFragment(); $key = 'app.'.$class.'.installed.'.$viewer_fragment; $result = $cache->getKey($key); if ($result === null) { if (!self::isClassInstalled($class)) { $result = false; } else { $application = self::getByClass($class); if (!$application->canUninstall()) { // If the application can not be uninstalled, always allow viewers // to see it. In particular, this allows logged-out viewers to see // Settings and load global default settings even if the install // does not allow public viewers. $result = true; } else { $result = PhabricatorPolicyFilter::hasCapability( $viewer, self::getByClass($class), PhabricatorPolicyCapability::CAN_VIEW); } } $cache->setKey($key, $result); } return $result; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array_merge( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ), array_keys($this->getCustomCapabilities())); } public function getPolicy($capability) { $default = $this->getCustomPolicySetting($capability); if ($default) { return $default; } switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_ADMIN; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'default', PhabricatorPolicies::POLICY_USER); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( Policies )----------------------------------------------------------- */ protected function getCustomCapabilities() { return array(); } final private function getCustomPolicySetting($capability) { if (!$this->isCapabilityEditable($capability)) { return null; } $policy_locked = PhabricatorEnv::getEnvConfig('policy.locked'); if (isset($policy_locked[$capability])) { return $policy_locked[$capability]; } $config = PhabricatorEnv::getEnvConfig('phabricator.application-settings'); $app = idx($config, $this->getPHID()); if (!$app) { return null; } $policy = idx($app, 'policy'); if (!$policy) { return null; } return idx($policy, $capability); } final private function getCustomCapabilitySpecification($capability) { $custom = $this->getCustomCapabilities(); if (!isset($custom[$capability])) { throw new Exception(pht("Unknown capability '%s'!", $capability)); } return $custom[$capability]; } final public function getCapabilityLabel($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Can Use Application'); case PhabricatorPolicyCapability::CAN_EDIT: return pht('Can Configure Application'); } $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if ($capobj) { return $capobj->getCapabilityName(); } return null; } final public function isCapabilityEditable($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->canUninstall(); case PhabricatorPolicyCapability::CAN_EDIT: return true; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'edit', true); } } final public function getCapabilityCaption($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->canUninstall()) { return pht( 'This application is required for Phabricator to operate, so all '. 'users must have access to it.'); } else { return null; } case PhabricatorPolicyCapability::CAN_EDIT: return null; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'caption'); } } final public function getCapabilityTemplatePHIDType($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return null; } $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'template'); } final public function getDefaultObjectTypePolicyMap() { $map = array(); foreach ($this->getCustomCapabilities() as $capability => $spec) { if (empty($spec['template'])) { continue; } if (empty($spec['capability'])) { continue; } $default = $this->getPolicy($capability); $map[$spec['template']][$spec['capability']] = $default; } return $map; } public function getApplicationSearchDocumentTypes() { return array(); } protected function getEditRoutePattern($base = null) { return $base.'(?:'. '(?P[0-9]\d*)/)?'. '(?:'. '(?:'. '(?Pparameters|nodefault|nocreate|nomanage|comment)/'. '|'. '(?:form/(?P[^/]+)/)?(?:page/(?P[^/]+)/)?'. ')'. ')?'; } protected function getBulkRoutePattern($base = null) { return $base.'(?:query/(?P[^/]+)/)?'; } protected function getQueryRoutePattern($base = null) { return $base.'(?:query/(?P[^/]+)/(?:(?P[^/]+)/)?)?'; } protected function getProfileMenuRouting($controller) { $edit_route = $this->getEditRoutePattern(); $mode_route = '(?Pglobal|custom)/'; return array( '(?Pview)/(?P[^/]+)/' => $controller, '(?Phide)/(?P[^/]+)/' => $controller, '(?Pdefault)/(?P[^/]+)/' => $controller, '(?Pconfigure)/' => $controller, '(?Pconfigure)/'.$mode_route => $controller, '(?Preorder)/'.$mode_route => $controller, '(?Pedit)/'.$edit_route => $controller, '(?Pnew)/'.$mode_route.'(?[^/]+)/'.$edit_route => $controller, '(?Pbuiltin)/(?[^/]+)/'.$edit_route => $controller, ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorApplicationEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorApplicationApplicationTransaction(); } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 51c9a8a973..a8092aaa88 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,1453 +1,1449 @@ setViewer($actor) ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); $view_default = PhabricatorCalendarEventDefaultViewCapability::CAPABILITY; $edit_default = PhabricatorCalendarEventDefaultEditCapability::CAPABILITY; $view_policy = $app->getPolicy($view_default); $edit_policy = $app->getPolicy($edit_default); $now = PhabricatorTime::getNow(); $default_icon = 'fa-calendar'; $datetime_defaults = self::newDefaultEventDateTimes( $actor, $now); list($datetime_start, $datetime_end) = $datetime_defaults; // When importing events from a context like "bin/calendar reload", we may // be acting as the omnipotent user. $host_phid = $actor->getPHID(); if (!$host_phid) { $host_phid = $app->getPHID(); } return id(new PhabricatorCalendarEvent()) ->setDescription('') ->setHostPHID($host_phid) ->setIsCancelled(0) ->setIsAllDay(0) ->setIsStub(0) ->setIsRecurring(0) ->setIcon($default_icon) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->attachInvitees(array()) ->setStartDateTime($datetime_start) ->setEndDateTime($datetime_end) ->attachImportSource(null) ->applyViewerTimezone($actor); } public static function newDefaultEventDateTimes( PhabricatorUser $viewer, $now) { $datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch( $now, $viewer->getTimezoneIdentifier()); // Advance the time by an hour, then round downwards to the nearest hour. // For example, if it is currently 3:25 PM, we suggest a default start time // of 4 PM. $datetime_start = $datetime_start ->newRelativeDateTime('PT1H') ->newAbsoluteDateTime(); $datetime_start->setMinute(0); $datetime_start->setSecond(0); // Default the end time to an hour after the start time. $datetime_end = $datetime_start ->newRelativeDateTime('PT1H') ->newAbsoluteDateTime(); return array($datetime_start, $datetime_end); } private function newChild( PhabricatorUser $actor, $sequence, PhutilCalendarDateTime $start = null) { if (!$this->isParentEvent()) { throw new Exception( pht( 'Unable to generate a new child event for an event which is not '. 'a recurring parent event!')); } $series_phid = $this->getSeriesParentPHID(); if (!$series_phid) { $series_phid = $this->getPHID(); } $child = id(new self()) ->setIsCancelled(0) ->setIsStub(0) ->setInstanceOfEventPHID($this->getPHID()) ->setSeriesParentPHID($series_phid) ->setSequenceIndex($sequence) ->setIsRecurring(true) ->attachParentEvent($this) ->attachImportSource(null); return $child->copyFromParent($actor, $start); } protected function readField($field) { static $inherit = array( 'hostPHID' => true, 'isAllDay' => true, 'icon' => true, 'spacePHID' => true, 'viewPolicy' => true, 'editPolicy' => true, 'name' => true, 'description' => true, 'isCancelled' => true, ); // Read these fields from the parent event instead of this event. For // example, we want any changes to the parent event's name to apply to // the child. if (isset($inherit[$field])) { if ($this->getIsStub()) { // TODO: This should be unconditional, but the execution order of // CalendarEventQuery and applyViewerTimezone() are currently odd. if ($this->parentEvent !== self::ATTACHABLE) { return $this->getParentEvent()->readField($field); } } } return parent::readField($field); } public function copyFromParent( PhabricatorUser $actor, PhutilCalendarDateTime $start = null) { if (!$this->isChildEvent()) { throw new Exception( pht( 'Unable to copy from parent event: this is not a child event.')); } $parent = $this->getParentEvent(); $this ->setHostPHID($parent->getHostPHID()) ->setIsAllDay($parent->getIsAllDay()) ->setIcon($parent->getIcon()) ->setSpacePHID($parent->getSpacePHID()) ->setViewPolicy($parent->getViewPolicy()) ->setEditPolicy($parent->getEditPolicy()) ->setName($parent->getName()) ->setDescription($parent->getDescription()) ->setIsCancelled($parent->getIsCancelled()); if ($start) { $start_datetime = $start; } else { $sequence = $this->getSequenceIndex(); $start_datetime = $parent->newSequenceIndexDateTime($sequence); if (!$start_datetime) { throw new Exception( pht( 'Sequence "%s" is not valid for event!', $sequence)); } } $duration = $parent->newDuration(); $end_datetime = $start_datetime->newRelativeDateTime($duration); $this ->setStartDateTime($start_datetime) ->setEndDateTime($end_datetime); if ($parent->isImportedEvent()) { $full_uid = $parent->getImportUID().'/'.$start_datetime->getEpoch(); // NOTE: We don't attach the import source because this gets called // from CalendarEventQuery while building ghosts, before we've loaded // and attached sources. Possibly this sequence should be flipped. $this ->setImportAuthorPHID($parent->getImportAuthorPHID()) ->setImportSourcePHID($parent->getImportSourcePHID()) ->setImportUID($full_uid); } return $this; } public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) { return (bool)$this->newSequenceIndexDateTime($sequence); } public function newSequenceIndexDateTime($sequence) { $set = $this->newRecurrenceSet(); if (!$set) { return null; } $limit = $sequence + 1; $count = $this->getRecurrenceCount(); if ($count && ($count < $limit)) { return null; } $instances = $set->getEventsBetween( null, $this->newUntilDateTime(), $limit); return idx($instances, $sequence, null); } public function newStub(PhabricatorUser $actor, $sequence) { $stub = $this->newChild($actor, $sequence); $stub->setIsStub(1); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $stub->save(); unset($unguarded); $stub->applyViewerTimezone($actor); return $stub; } public function newGhost( PhabricatorUser $actor, $sequence, PhutilCalendarDateTime $start = null) { $ghost = $this->newChild($actor, $sequence, $start); $ghost ->setIsGhostEvent(true) ->makeEphemeral(); $ghost->applyViewerTimezone($actor); return $ghost; } public function applyViewerTimezone(PhabricatorUser $viewer) { $this->viewerTimezone = $viewer->getTimezoneIdentifier(); return $this; } public function getDuration() { return ($this->getEndDateTimeEpoch() - $this->getStartDateTimeEpoch()); } public function updateUTCEpochs() { // The "intitial" epoch is the start time of the event, in UTC. $start_date = $this->newStartDateTime() ->setViewerTimezone('UTC'); $start_epoch = $start_date->getEpoch(); $this->setUTCInitialEpoch($start_epoch); // The "until" epoch is the last UTC epoch on which any instance of this // event occurs. For infinitely recurring events, it is `null`. if (!$this->getIsRecurring()) { $end_date = $this->newEndDateTime() ->setViewerTimezone('UTC'); $until_epoch = $end_date->getEpoch(); } else { $until_epoch = null; $until_date = $this->newUntilDateTime(); if ($until_date) { $until_date->setViewerTimezone('UTC'); $duration = $this->newDuration(); $until_epoch = id(new PhutilCalendarRelativeDateTime()) ->setOrigin($until_date) ->setDuration($duration) ->getEpoch(); } } $this->setUTCUntilEpoch($until_epoch); // The "instance" epoch is a property of instances of recurring events. // It's the original UTC epoch on which the instance started. Usually that // is the same as the start date, but they may be different if the instance // has been edited. // The ICS format uses this value (original start time) to identify event // instances, and must do so because it allows additional arbitrary // instances to be added (with "RDATE"). $instance_epoch = null; $instance_date = $this->newInstanceDateTime(); if ($instance_date) { $instance_epoch = $instance_date ->setViewerTimezone('UTC') ->getEpoch(); } $this->setUTCInstanceEpoch($instance_epoch); return $this; } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } $import_uid = $this->getImportUID(); if ($import_uid !== null) { $index = PhabricatorHash::digestForIndex($import_uid); } else { $index = null; } $this->setImportUIDIndex($index); $this->updateUTCEpochs(); return parent::save(); } /** * Get the event start epoch for evaluating invitee availability. * * When assessing availability, we pretend events start earlier than they * really do. This allows us to mark users away for the entire duration of a * series of back-to-back meetings, even if they don't strictly overlap. * * @return int Event start date for availability caches. */ public function getStartDateTimeEpochForCache() { $epoch = $this->getStartDateTimeEpoch(); $window = phutil_units('15 minutes in seconds'); return ($epoch - $window); } public function getEndDateTimeEpochForCache() { return $this->getEndDateTimeEpoch(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'description' => 'text', 'isCancelled' => 'bool', 'isAllDay' => 'bool', 'icon' => 'text32', 'mailKey' => 'bytes20', 'isRecurring' => 'bool', 'seriesParentPHID' => 'phid?', 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', 'isStub' => 'bool', 'utcInitialEpoch' => 'epoch', 'utcUntilEpoch' => 'epoch?', 'utcInstanceEpoch' => 'epoch?', 'importAuthorPHID' => 'phid?', 'importSourcePHID' => 'phid?', 'importUIDIndex' => 'bytes12?', 'importUID' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_instance' => array( 'columns' => array('instanceOfEventPHID', 'sequenceIndex'), 'unique' => true, ), 'key_epoch' => array( 'columns' => array('utcInitialEpoch', 'utcUntilEpoch'), ), 'key_rdate' => array( 'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'), 'unique' => true, ), 'key_series' => array( 'columns' => array('seriesParentPHID', 'utcInitialEpoch'), ), ), self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getMonogram() { return 'E'.$this->getID(); } public function getInvitees() { if ($this->getIsGhostEvent() || $this->getIsStub()) { if ($this->stubInvitees === null) { $this->stubInvitees = $this->newStubInvitees(); } return $this->stubInvitees; } return $this->assertAttached($this->invitees); } public function getInviteeForPHID($phid) { $invitees = $this->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); return idx($invitees, $phid); } public static function getFrequencyMap() { return array( PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array( 'label' => pht('Daily'), ), PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => array( 'label' => pht('Weekly'), ), PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => array( 'label' => pht('Monthly'), ), PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => array( 'label' => pht('Yearly'), ), ); } private function newStubInvitees() { $parent = $this->getParentEvent(); $parent_invitees = $parent->getInvitees(); $stub_invitees = array(); foreach ($parent_invitees as $invitee) { $stub_invitee = id(new PhabricatorCalendarEventInvitee()) ->setInviteePHID($invitee->getInviteePHID()) ->setInviterPHID($invitee->getInviterPHID()) ->setStatus(PhabricatorCalendarEventInvitee::STATUS_INVITED); $stub_invitees[] = $stub_invitee; } return $stub_invitees; } public function attachInvitees(array $invitees) { $this->invitees = $invitees; return $this; } public function getInviteePHIDsForEdit() { $invitees = array(); foreach ($this->getInvitees() as $invitee) { if ($invitee->isUninvited()) { continue; } $invitees[] = $invitee->getInviteePHID(); } return $invitees; } public function getUserInviteStatus($phid) { $invitees = $this->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invited = idx($invitees, $phid); if (!$invited) { return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } $invited = $invited->getStatus(); return $invited; } public function getIsUserAttending($phid) { $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $old_status = $this->getUserInviteStatus($phid); $is_attending = ($old_status == $attending_status); return $is_attending; } public function getIsGhostEvent() { return $this->isGhostEvent; } public function setIsGhostEvent($is_ghost_event) { $this->isGhostEvent = $is_ghost_event; return $this; } public function getURI() { if ($this->getIsGhostEvent()) { $base = $this->getParentEvent()->getURI(); $sequence = $this->getSequenceIndex(); return "{$base}/{$sequence}/"; } return '/'.$this->getMonogram(); } public function getParentEvent() { return $this->assertAttached($this->parentEvent); } public function attachParentEvent(PhabricatorCalendarEvent $event = null) { $this->parentEvent = $event; return $this; } public function isParentEvent() { return ($this->getIsRecurring() && !$this->getInstanceOfEventPHID()); } public function isChildEvent() { return ($this->instanceOfEventPHID !== null); } public function renderEventDate( PhabricatorUser $viewer, $show_end) { $start = $this->newStartDateTime(); $end = $this->newEndDateTime(); $min_date = $start->newPHPDateTime(); $max_date = $end->newPHPDateTime(); if ($this->getIsAllDay()) { // Subtract one second since the stored date is exclusive. $max_date = $max_date->modify('-1 second'); } if ($show_end) { $min_day = $min_date->format('Y m d'); $max_day = $max_date->format('Y m d'); $show_end_date = ($min_day != $max_day); } else { $show_end_date = false; } $min_epoch = $min_date->format('U'); $max_epoch = $max_date->format('U'); if ($this->getIsAllDay()) { if ($show_end_date) { return pht( '%s - %s, All Day', phabricator_date($min_epoch, $viewer), phabricator_date($max_epoch, $viewer)); } else { return pht( '%s, All Day', phabricator_date($min_epoch, $viewer)); } } else if ($show_end_date) { return pht( '%s - %s', phabricator_datetime($min_epoch, $viewer), phabricator_datetime($max_epoch, $viewer)); } else if ($show_end) { return pht( '%s - %s', phabricator_datetime($min_epoch, $viewer), phabricator_time($max_epoch, $viewer)); } else { return pht( '%s', phabricator_datetime($min_epoch, $viewer)); } } public function getDisplayIcon(PhabricatorUser $viewer) { if ($this->getIsCancelled()) { return 'fa-times'; } if ($viewer->isLoggedIn()) { $viewer_phid = $viewer->getPHID(); if ($this->isRSVPInvited($viewer_phid)) { return 'fa-users'; } else { $status = $this->getUserInviteStatus($viewer_phid); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return 'fa-check-circle'; case PhabricatorCalendarEventInvitee::STATUS_INVITED: return 'fa-user-plus'; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return 'fa-times-circle'; } } } if ($this->isImportedEvent()) { return 'fa-download'; } return $this->getIcon(); } public function getDisplayIconColor(PhabricatorUser $viewer) { if ($this->getIsCancelled()) { return 'red'; } if ($this->isImportedEvent()) { return 'orange'; } if ($viewer->isLoggedIn()) { $viewer_phid = $viewer->getPHID(); if ($this->isRSVPInvited($viewer_phid)) { return 'green'; } $status = $this->getUserInviteStatus($viewer_phid); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return 'green'; case PhabricatorCalendarEventInvitee::STATUS_INVITED: return 'green'; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return 'grey'; } } return 'bluegrey'; } public function getDisplayIconLabel(PhabricatorUser $viewer) { if ($this->getIsCancelled()) { return pht('Cancelled'); } if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return pht('Attending'); case PhabricatorCalendarEventInvitee::STATUS_INVITED: return pht('Invited'); case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return pht('Declined'); } } return null; } public function getICSFilename() { return $this->getMonogram().'.ics'; } public function newIntermediateEventNode( PhabricatorUser $viewer, array $children) { $base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/')); $domain = $base_uri->getDomain(); // NOTE: For recurring events, all of the events in the series have the // same UID (the UID of the parent). The child event instances are // differentiated by the "RECURRENCE-ID" field. if ($this->isChildEvent()) { $parent = $this->getParentEvent(); $instance_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch( $this->getUTCInstanceEpoch()); $recurrence_id = $instance_datetime->getISO8601(); $rrule = null; } else { $parent = $this; $recurrence_id = null; $rrule = $this->newRecurrenceRule(); } $uid = $parent->getPHID().'@'.$domain; $created = $this->getDateCreated(); $created = PhutilCalendarAbsoluteDateTime::newFromEpoch($created); $modified = $this->getDateModified(); $modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified); $date_start = $this->newStartDateTime(); $date_end = $this->newEndDateTime(); if ($this->getIsAllDay()) { $date_start->setIsAllDay(true); $date_end->setIsAllDay(true); } $host_phid = $this->getHostPHID(); $invitees = $this->getInvitees(); foreach ($invitees as $key => $invitee) { if ($invitee->isUninvited()) { unset($invitees[$key]); } } $phids = array(); $phids[] = $host_phid; foreach ($invitees as $invitee) { $phids[] = $invitee->getInviteePHID(); } $handles = $viewer->loadHandles($phids); $host_handle = $handles[$host_phid]; $host_name = $host_handle->getFullName(); // NOTE: Gmail shows "Who: Unknown Organizer*" if the organizer URI does // not look like an email address. Use a synthetic address so it shows // the host name instead. $install_uri = PhabricatorEnv::getProductionURI('/'); $install_uri = new PhutilURI($install_uri); // This should possibly use "metamta.reply-handler-domain" instead, but // we do not currently accept mail for users anyway, and that option may // not be configured. $mail_domain = $install_uri->getDomain(); $host_uri = "mailto:{$host_phid}@{$mail_domain}"; $organizer = id(new PhutilCalendarUserNode()) ->setName($host_name) ->setURI($host_uri); $attendees = array(); foreach ($invitees as $invitee) { $invitee_phid = $invitee->getInviteePHID(); $invitee_handle = $handles[$invitee_phid]; $invitee_name = $invitee_handle->getFullName(); $invitee_uri = $invitee_handle->getURI(); $invitee_uri = PhabricatorEnv::getURI($invitee_uri); switch ($invitee->getStatus()) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: $status = PhutilCalendarUserNode::STATUS_ACCEPTED; break; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: $status = PhutilCalendarUserNode::STATUS_DECLINED; break; case PhabricatorCalendarEventInvitee::STATUS_INVITED: default: $status = PhutilCalendarUserNode::STATUS_INVITED; break; } $attendees[] = id(new PhutilCalendarUserNode()) ->setName($invitee_name) ->setURI($invitee_uri) ->setStatus($status); } // TODO: Use $children to generate EXDATE/RDATE information. $node = id(new PhutilCalendarEventNode()) ->setUID($uid) ->setName($this->getName()) ->setDescription($this->getDescription()) ->setCreatedDateTime($created) ->setModifiedDateTime($modified) ->setStartDateTime($date_start) ->setEndDateTime($date_end) ->setOrganizer($organizer) ->setAttendees($attendees); if ($rrule) { $node->setRecurrenceRule($rrule); } if ($recurrence_id) { $node->setRecurrenceID($recurrence_id); } return $node; } public function newStartDateTime() { $datetime = $this->getParameter('startDateTime'); return $this->newDateTimeFromDictionary($datetime); } public function getStartDateTimeEpoch() { return $this->newStartDateTime()->getEpoch(); } public function newEndDateTimeForEdit() { $datetime = $this->getParameter('endDateTime'); return $this->newDateTimeFromDictionary($datetime); } public function newEndDateTime() { $datetime = $this->newEndDateTimeForEdit(); // If this is an all day event, we move the end date time forward to the // first second of the following day. This is consistent with what users // expect: an all day event from "Nov 1" to "Nov 1" lasts the entire day. // For imported events, the end date is already stored with this // adjustment. if ($this->getIsAllDay() && !$this->isImportedEvent()) { $datetime = $datetime ->newAbsoluteDateTime() ->setHour(0) ->setMinute(0) ->setSecond(0) ->newRelativeDateTime('P1D') ->newAbsoluteDateTime(); } return $datetime; } public function getEndDateTimeEpoch() { return $this->newEndDateTime()->getEpoch(); } public function newUntilDateTime() { $datetime = $this->getParameter('untilDateTime'); if ($datetime) { return $this->newDateTimeFromDictionary($datetime); } return null; } public function getUntilDateTimeEpoch() { $datetime = $this->newUntilDateTime(); if (!$datetime) { return null; } return $datetime->getEpoch(); } public function newDuration() { return id(new PhutilCalendarDuration()) ->setSeconds($this->getDuration()); } public function newInstanceDateTime() { if (!$this->getIsRecurring()) { return null; } $index = $this->getSequenceIndex(); if (!$index) { return null; } return $this->newSequenceIndexDateTime($index); } private function newDateTimeFromEpoch($epoch) { $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch); if ($this->getIsAllDay()) { $datetime->setIsAllDay(true); } return $this->newDateTimeFromDateTime($datetime); } private function newDateTimeFromDictionary(array $dict) { $datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict); return $this->newDateTimeFromDateTime($datetime); } private function newDateTimeFromDateTime(PhutilCalendarDateTime $datetime) { $viewer_timezone = $this->viewerTimezone; if ($viewer_timezone) { $datetime->setViewerTimezone($viewer_timezone); } return $datetime; } public function getParameter($key, $default = null) { return idx($this->parameters, $key, $default); } public function setParameter($key, $value) { $this->parameters[$key] = $value; return $this; } public function setStartDateTime(PhutilCalendarDateTime $datetime) { return $this->setParameter( 'startDateTime', $datetime->newAbsoluteDateTime()->toDictionary()); } public function setEndDateTime(PhutilCalendarDateTime $datetime) { return $this->setParameter( 'endDateTime', $datetime->newAbsoluteDateTime()->toDictionary()); } public function setUntilDateTime(PhutilCalendarDateTime $datetime = null) { if ($datetime) { $value = $datetime->newAbsoluteDateTime()->toDictionary(); } else { $value = null; } return $this->setParameter('untilDateTime', $value); } public function setRecurrenceRule(PhutilCalendarRecurrenceRule $rrule) { return $this->setParameter( 'recurrenceRule', $rrule->toDictionary()); } public function newRecurrenceRule() { if ($this->isChildEvent()) { return $this->getParentEvent()->newRecurrenceRule(); } if (!$this->getIsRecurring()) { return null; } $dict = $this->getParameter('recurrenceRule'); if (!$dict) { return null; } $rrule = PhutilCalendarRecurrenceRule::newFromDictionary($dict); $start = $this->newStartDateTime(); $rrule->setStartDateTime($start); $until = $this->newUntilDateTime(); if ($until) { $rrule->setUntil($until); } $count = $this->getRecurrenceCount(); if ($count) { $rrule->setCount($count); } return $rrule; } public function getRecurrenceCount() { $count = (int)$this->getParameter('recurrenceCount'); if (!$count) { return null; } return $count; } public function newRecurrenceSet() { if ($this->isChildEvent()) { return $this->getParentEvent()->newRecurrenceSet(); } $set = new PhutilCalendarRecurrenceSet(); if ($this->viewerTimezone) { $set->setViewerTimezone($this->viewerTimezone); } $rrule = $this->newRecurrenceRule(); if (!$rrule) { return null; } $set->addSource($rrule); return $set; } public function isImportedEvent() { return (bool)$this->getImportSourcePHID(); } public function getImportSource() { return $this->assertAttached($this->importSource); } public function attachImportSource( PhabricatorCalendarImport $import = null) { $this->importSource = $import; return $this; } public function loadForkTarget(PhabricatorUser $viewer) { if (!$this->getIsRecurring()) { // Can't fork an event which isn't recurring. return null; } if ($this->isChildEvent()) { // If this is a child event, this is the fork target. return $this; } if (!$this->isValidSequenceIndex($viewer, 1)) { // This appears to be a "recurring" event with no valid instances: for // example, its "until" date is before the second instance would occur. // This can happen if we already forked the event or if users entered // silly stuff. Just edit the event directly without forking anything. return null; } $next_event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withInstanceSequencePairs( array( array($this->getPHID(), 1), )) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$next_event) { $next_event = $this->newStub($viewer, 1); } return $next_event; } public function loadFutureEvents(PhabricatorUser $viewer) { // NOTE: If you can't edit some of the future events, we just // don't try to update them. This seems like it's probably what // users are likely to expect. // NOTE: This only affects events that are currently in the same // series, not all events that were ever in the original series. // We could use series PHIDs instead of parent PHIDs to affect more // events if this turns out to be counterintuitive. Other // applications differ in their behavior. return id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withParentEventPHIDs(array($this->getPHID())) ->withUTCInitialEpochBetween($this->getUTCInitialEpoch(), null) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); } public function getNotificationPHIDs() { $phids = array(); if ($this->getPHID()) { $phids[] = $this->getPHID(); } if ($this->getSeriesParentPHID()) { $phids[] = $this->getSeriesParentPHID(); } return $phids; } public function getRSVPs($phid) { return $this->assertAttachedKey($this->rsvps, $phid); } public function attachRSVPs(array $rsvps) { $this->rsvps = $rsvps; return $this; } public function isRSVPInvited($phid) { $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; return ($this->getRSVPStatus($phid) == $status_invited); } public function hasRSVPAuthority($phid, $other_phid) { foreach ($this->getRSVPs($phid) as $rsvp) { if ($rsvp->getInviteePHID() == $other_phid) { return true; } } return false; } public function getRSVPStatus($phid) { // Check for an individual invitee record first. $invitees = $this->invitees; $invitees = mpull($invitees, null, 'getInviteePHID'); $invitee = idx($invitees, $phid); if ($invitee) { return $invitee->getStatus(); } // If we don't have one, try to find an invited status for the user's // projects. $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; foreach ($this->getRSVPs($phid) as $rsvp) { if ($rsvp->getStatus() == $status_invited) { return $status_invited; } } return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newCalendarMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( 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->isImportedEvent()) { return PhabricatorPolicies::POLICY_NOONE; } else { return $this->getEditPolicy(); } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->isImportedEvent()) { return false; } // The host of an event can always view and edit it. $user_phid = $this->getHostPHID(); if ($user_phid) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid == $user_phid) { return true; } } if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $status = $this->getUserInviteStatus($viewer->getPHID()); if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING || $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) { return true; } } return false; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $import_source = $this->getImportSource(); if ($import_source) { $extended[] = array( $import_source, PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } /* -( PhabricatorPolicyCodexInterface )------------------------------------ */ public function newPolicyCodex() { return new PhabricatorCalendarEventPolicyCodex(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCalendarEventEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorCalendarEventTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getHostPHID()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getHostPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $invitees = id(new PhabricatorCalendarEventInvitee())->loadAllWhere( 'eventPHID = %s', $this->getPHID()); foreach ($invitees as $invitee) { $invitee->delete(); } $notifications = id(new PhabricatorCalendarNotification())->loadAllWhere( 'eventPHID = %s', $this->getPHID()); foreach ($notifications as $notification) { $notification->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorCalendarEventFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhabricatorCalendarEventFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the event.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('string') ->setDescription(pht('The event description.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isAllDay') ->setType('bool') ->setDescription(pht('True if the event is an all day event.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('startDateTime') ->setType('datetime') ->setDescription(pht('Start date and time of the event.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('endDateTime') ->setType('datetime') ->setDescription(pht('End date and time of the event.')), ); } public function getFieldValuesForConduit() { $start_datetime = $this->newStartDateTime(); $end_datetime = $this->newEndDateTime(); return array( 'name' => $this->getName(), 'description' => $this->getDescription(), 'isAllDay' => (bool)$this->getIsAllDay(), 'startDateTime' => $this->getConduitDateTime($start_datetime), 'endDateTime' => $this->getConduitDateTime($end_datetime), ); } public function getConduitSearchAttachments() { return array(); } private function getConduitDateTime($datetime) { if (!$datetime) { return null; } $epoch = $datetime->getEpoch(); // TODO: Possibly pass the actual viewer in from the Conduit stuff, or // retain it when setting the viewer timezone? $viewer = id(new PhabricatorUser()) ->overrideTimezoneIdentifier($this->viewerTimezone); return array( 'epoch' => (int)$epoch, 'display' => array( 'default' => phabricator_datetime($epoch, $viewer), ), 'iso8601' => $datetime->getISO8601(), 'timezone' => $this->viewerTimezone, ); } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarExport.php b/src/applications/calendar/storage/PhabricatorCalendarExport.php index 93daaf6e5d..4ad2b457f3 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarExport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarExport.php @@ -1,186 +1,182 @@ setAuthorPHID($actor->getPHID()) ->setPolicyMode(self::MODE_PRIVILEGED) ->setIsDisabled(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'policyMode' => 'text64', 'queryKey' => 'text64', 'secretKey' => 'bytes20', 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_author' => array( 'columns' => array('authorPHID'), ), 'key_secret' => array( 'columns' => array('secretKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhabricatorCalendarExportPHIDType::TYPECONST; } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getURI() { $id = $this->getID(); return "/calendar/export/{$id}/"; } private static function getPolicyModeMap() { return array( self::MODE_PUBLIC => array( 'icon' => 'fa-globe', 'name' => pht('Public'), 'color' => 'bluegrey', 'summary' => pht( 'Export only public data.'), 'description' => pht( 'Only publicly available data is exported.'), ), self::MODE_PRIVILEGED => array( 'icon' => 'fa-unlock-alt', 'name' => pht('Privileged'), 'color' => 'red', 'summary' => pht( 'Export private data.'), 'description' => pht( 'Anyone who knows the URI for this export can view all event '. 'details as though they were logged in with your account.'), ), ); } private static function getPolicyModeSpec($const) { return idx(self::getPolicyModeMap(), $const, array()); } public static function getPolicyModeName($const) { $spec = self::getPolicyModeSpec($const); return idx($spec, 'name', $const); } public static function getPolicyModeIcon($const) { $spec = self::getPolicyModeSpec($const); return idx($spec, 'icon', $const); } public static function getPolicyModeColor($const) { $spec = self::getPolicyModeSpec($const); return idx($spec, 'color', $const); } public static function getPolicyModeSummary($const) { $spec = self::getPolicyModeSpec($const); return idx($spec, 'summary', $const); } public static function getPolicyModeDescription($const) { $spec = self::getPolicyModeSpec($const); return idx($spec, 'description', $const); } public static function getPolicyModes() { return array_keys(self::getPolicyModeMap()); } public static function getAvailablePolicyModes() { $modes = array(); if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { $modes[] = self::MODE_PUBLIC; } $modes[] = self::MODE_PRIVILEGED; return $modes; } public function getICSFilename() { return PhabricatorSlug::normalizeProjectSlug($this->getName()).'.ics'; } public function getICSURI() { $secret_key = $this->getSecretKey(); $ics_name = $this->getICSFilename(); return "/calendar/export/ics/{$secret_key}/{$ics_name}"; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getAuthorPHID(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCalendarExportEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorCalendarExportTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php index 09e7ee138b..37ddac857c 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarImport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -1,203 +1,199 @@ setName('') ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($actor->getPHID()) ->setEditPolicy($actor->getPHID()) ->setIsDisabled(0) ->setEngineType($engine->getImportEngineType()) ->attachEngine($engine) ->setTriggerFrequency(self::FREQUENCY_ONCE); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'engineType' => 'text64', 'isDisabled' => 'bool', 'triggerPHID' => 'phid?', 'triggerFrequency' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_author' => array( 'columns' => array('authorPHID'), ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhabricatorCalendarImportPHIDType::TYPECONST; } public function getURI() { $id = $this->getID(); return "/calendar/import/{$id}/"; } public function attachEngine(PhabricatorCalendarImportEngine $engine) { $this->engine = $engine; return $this; } public function getEngine() { return $this->assertAttached($this->engine); } public function getParameter($key, $default = null) { return idx($this->parameters, $key, $default); } public function setParameter($key, $value) { $this->parameters[$key] = $value; return $this; } public function getDisplayName() { $name = $this->getName(); if (strlen($name)) { return $name; } return $this->getEngine()->getDisplayName($this); } public static function getTriggerFrequencyMap() { return array( self::FREQUENCY_ONCE => array( 'name' => pht('No Automatic Updates'), ), self::FREQUENCY_HOURLY => array( 'name' => pht('Update Hourly'), ), self::FREQUENCY_DAILY => array( 'name' => pht('Update Daily'), ), ); } /* -( 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; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCalendarImportEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorCalendarImportTransaction(); } public function newLogMessage($type, array $parameters) { $parameters = array( 'type' => $type, ) + $parameters; return id(new PhabricatorCalendarImportLog()) ->setImportPHID($this->getPHID()) ->setParameters($parameters) ->save(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $trigger_phid = $this->getTriggerPHID(); if ($trigger_phid) { $trigger = id(new PhabricatorWorkerTriggerQuery()) ->setViewer($viewer) ->withPHIDs(array($trigger_phid)) ->executeOne(); if ($trigger) { $engine->destroyObject($trigger); } } $events = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withImportSourcePHIDs(array($this->getPHID())) ->execute(); foreach ($events as $event) { $engine->destroyObject($event); } $logs = id(new PhabricatorCalendarImportLogQuery()) ->setViewer($viewer) ->withImportPHIDs(array($this->getPHID())) ->execute(); foreach ($logs as $log) { $engine->destroyObject($log); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php index 4e28180eed..3462c4e7ba 100644 --- a/src/applications/config/storage/PhabricatorConfigEntry.php +++ b/src/applications/config/storage/PhabricatorConfigEntry.php @@ -1,91 +1,87 @@ 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(); } /* -( 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; } } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 4a99fccdb9..7a5f97ed41 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -1,352 +1,348 @@ getObjectPolicyFullKey(); return id(new ConpherenceThread()) ->setMessageCount(0) ->setTitle('') ->setTopic('') ->attachParticipants(array()) ->setViewPolicy($default_policy) ->setEditPolicy($default_policy) ->setJoinPolicy(''); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255?', 'topic' => 'text255', 'messageCount' => 'uint64', 'mailKey' => 'text20', 'joinPolicy' => 'policy', 'profileImagePHID' => 'phid?', ), 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 getMonogram() { return 'Z'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } 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 getParticipantIfExists($phid, $default = null) { $participants = $this->getParticipants(); return idx($participants, $phid, $default); } 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($assert_attached = true) { return $this->assertAttached($this->transactions); } public function hasAttachedTransactions() { return $this->transactions !== self::ATTACHABLE; } public function getTransactionsFrom($begin = 0, $amount = null) { $length = count($this->transactions); return array_slice( $this->getTransactions(), $length - $begin - $amount, $amount); } 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); } /** * Get a thread title which doesn't require handles to be attached. * * This is a less rich title than @{method:getDisplayTitle}, but does not * require handles to be attached. We use it to build thread handles without * risking cycles or recursion while querying. * * @return string Lower quality human-readable title. */ public function getStaticTitle() { $title = $this->getTitle(); if (strlen($title)) { return $title; } return pht('Private Room'); } public function getDisplayData(PhabricatorUser $viewer) { $handles = $this->getHandles(); if ($this->hasAttachedTransactions()) { $transactions = $this->getTransactions(); } else { $transactions = array(); } $img_src = $this->getProfileImageURI(); $message_transaction = null; foreach ($transactions as $transaction) { if ($message_transaction) { break; } switch ($transaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $message_transaction = $transaction; break; default: break; } } if ($message_transaction) { $message_handle = $handles[$message_transaction->getAuthorPHID()]; $subtitle = sprintf( '%s: %s', $message_handle->getName(), id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(60) ->truncateString( $message_transaction->getComment()->getContent())); } else { // Kinda lame, but maybe add last message to cache? $subtitle = pht('No recent messages'); } $user_participation = $this->getParticipantIfExists($viewer->getPHID()); $theme = ConpherenceRoomSettings::COLOR_LIGHT; if ($user_participation) { $user_seen_count = $user_participation->getSeenMessageCount(); $participant = $this->getParticipant($viewer->getPHID()); $settings = $participant->getSettings(); $theme = idx($settings, 'theme', $theme); } else { $user_seen_count = 0; } $unread_count = $this->getMessageCount() - $user_seen_count; $theme_class = ConpherenceRoomSettings::getThemeClass($theme); $title = $this->getTitle(); $topic = $this->getTopic(); return array( 'title' => $title, 'topic' => $topic, 'subtitle' => $subtitle, 'unread_count' => $unread_count, 'epoch' => $this->getDateModified(), 'image' => $img_src, 'theme' => $theme_class, ); } /* -( 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(); } 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; } switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return false; } $participants = $this->getParticipants(); return isset($participants[$user->getPHID()]); } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Participants in a room can always view it.'); break; } } public static function loadViewPolicyObjects( PhabricatorUser $viewer, array $conpherences) { assert_instances_of($conpherences, __CLASS__); $policies = array(); foreach ($conpherences as $room) { $policies[$room->getViewPolicy()] = 1; } $policy_objects = array(); if ($policies) { $policy_objects = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($policies)) ->execute(); } return $policy_objects; } public function getPolicyIconName(array $policy_objects) { assert_instances_of($policy_objects, 'PhabricatorPolicy'); $icon = $policy_objects[$this->getViewPolicy()]->getIcon(); return $icon; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ConpherenceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new ConpherenceTransaction(); } /* -( PhabricatorNgramInterface )------------------------------------------ */ public function newNgrams() { return array( id(new ConpherenceThreadTitleNgrams()) ->setValue($this->getTitle()), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $participants = id(new ConpherenceParticipant()) ->loadAllWhere('conpherencePHID = %s', $this->getPHID()); foreach ($participants as $participant) { $participant->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/countdown/storage/PhabricatorCountdown.php b/src/applications/countdown/storage/PhabricatorCountdown.php index 0e275604da..1c61ae7ffc 100644 --- a/src/applications/countdown/storage/PhabricatorCountdown.php +++ b/src/applications/countdown/storage/PhabricatorCountdown.php @@ -1,190 +1,186 @@ setViewer($actor) ->withClasses(array('PhabricatorCountdownApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorCountdownDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( PhabricatorCountdownDefaultEditCapability::CAPABILITY); return id(new PhabricatorCountdown()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'description' => 'text', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_epoch' => array( 'columns' => array('epoch'), ), 'key_author' => array( 'columns' => array('authorPHID', 'epoch'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCountdownCountdownPHIDType::TYPECONST); } public function getMonogram() { return 'C'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCountdownEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorCountdownTransaction(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getAuthorPHID()); } /* -( 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; } /* -( PhabricatorSpacesInterface )------------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The title of the countdown.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('remarkup') ->setDescription(pht('The description of the countdown.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('epoch') ->setType('epoch') ->setDescription(pht('The end date of the countdown.')), ); } public function getFieldValuesForConduit() { return array( 'title' => $this->getTitle(), 'description' => array( 'raw' => $this->getDescription(), ), 'epoch' => (int)$this->getEpoch(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php index 86181dbf9d..53bc2f857d 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboard.php +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -1,195 +1,191 @@ setName('') ->setIcon('fa-dashboard') ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($actor->getPHID()) ->setStatus(self::STATUS_ACTIVE) ->setAuthorPHID($actor->getPHID()) ->attachPanels(array()) ->attachPanelPHIDs(array()); } public static function getStatusNameMap() { return array( self::STATUS_ACTIVE => pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'layoutConfig' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'status' => 'text32', 'icon' => 'text32', 'authorPHID' => 'phid', ), ) + 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()); // See PHI385. Dashboard panel mutations rely on changes to the Dashboard // object persisting when transactions are applied, but this assumption is // no longer valid after T13054. For now, just save the dashboard // explicitly. $this->save(); return $this; } public function getProjectPHIDs() { return $this->assertAttached($this->edgeProjectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->edgeProjectPHIDs = $phids; 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); } public function isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } public function getViewURI() { return '/dashboard/view/'.$this->getID().'/'; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorDashboardTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorDashboardTransaction(); } /* -( 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; } /* -( 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(); } /* -( PhabricatorNgramInterface )------------------------------------------ */ public function newNgrams() { return array( id(new PhabricatorDashboardNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php index ea71e04771..89b577ab87 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php @@ -1,192 +1,188 @@ setName('') ->setAuthorPHID($actor->getPHID()) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($actor->getPHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'panelType' => 'text64', 'authorPHID' => 'phid', '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 getURI() { return '/'.$this->getMonogram(); } public function getPanelTypes() { $panel_types = PhabricatorDashboardPanelType::getAllPanelTypes(); $panel_types = mpull($panel_types, 'getPanelTypeName', 'getPanelTypeKey'); asort($panel_types); $panel_types = (array('' => pht('(All Types)')) + $panel_types); return $panel_types; } public function getStatuses() { $statuses = array( '' => pht('(All Panels)'), 'active' => pht('Active Panels'), 'archived' => pht('Archived Panels'), ); return $statuses; } 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(); } /* -( 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; } /* -( 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(); } /* -( PhabricatorNgramInterface )------------------------------------------ */ public function newNgrams() { return array( id(new PhabricatorDashboardPanelNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 0b882228c5..e4c33dc766 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -1,814 +1,810 @@ 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?', 'repositoryUUID' => 'text64?', 'commitPHID' => 'phid?', // 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'), ), 'key_commit' => array( 'columns' => array('commitPHID'), ), ), ) + 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(); } $changesets = id(new DifferentialChangeset())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($changesets as $changeset) { $changeset->attachDiff($this); } return $changesets; } 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 DifferentialHunk(); $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); $changesets = $diff->getChangesets(); id(new DifferentialChangesetEngine()) ->rebuildChangesets($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(), ); $dict['changes'] = $this->buildChangesList(); return $dict + $this->getDiffAuthorshipDict(); } public function getDiffAuthorshipDict() { $dict = array('properties' => array()); $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); } public function hasDiffProperty($key) { $properties = $this->getDiffProperties(); return array_key_exists($key, $properties); } public function attachDiffProperties(array $properties) { $this->properties = $properties; return $this; } public function getDiffProperties() { return $this->assertAttached($this->properties); } public function attachBuildable(HarbormasterBuildable $buildable = null) { $this->buildable = $buildable; return $this; } public function getBuildable() { return $this->assertAttached($this->buildable); } public function getBuildTargetPHIDs() { $buildable = $this->getBuildable(); if (!$buildable) { return array(); } $target_phids = array(); foreach ($buildable->getBuilds() as $build) { foreach ($build->getBuildTargets() as $target) { $target_phids[] = $target->getPHID(); } } return $target_phids; } public function loadCoverageMap(PhabricatorUser $viewer) { $target_phids = $this->getBuildTargetPHIDs(); if (!$target_phids) { return array(); } $unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere( 'buildTargetPHID IN (%Ls)', $target_phids); $map = array(); foreach ($unit as $message) { $coverage = $message->getProperty('coverage', array()); foreach ($coverage as $path => $coverage_data) { $map[$path][] = $coverage_data; } } foreach ($map as $path => $coverage_items) { $map[$path] = ArcanistUnitTestResult::mergeCoverage($coverage_items); } return $map; } public function getURI() { $id = $this->getID(); return "/differential/diff/{$id}/"; } public function attachUnitMessages(array $unit_messages) { $this->unitMessages = $unit_messages; return $this; } public function getUnitMessages() { return $this->assertAttached($this->unitMessages); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { if ($this->hasRevision()) { return PhabricatorPolicies::getMostOpenPolicy(); } 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.'); } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->hasRevision()) { $extended[] = array( $this->getRevision(), PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { $container_phid = $this->getHarbormasterContainerPHID(); if ($container_phid) { return $container_phid; } return $this->getHarbormasterBuildablePHID(); } 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(); if ($this->revisionID) { $revision = $this->getRevision(); $results['buildable.revision'] = $revision->getID(); $repo = $revision->getRepository(); if ($repo) { $results['repository.callsign'] = $repo->getCallsign(); $results['repository.phid'] = $repo->getPHID(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); $results['repository.staging.uri'] = $repo->getStagingURI(); $results['repository.staging.ref'] = $this->getStagingRef(); } } 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.phid' => pht('The PHID 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.'), 'repository.staging.uri' => pht('The URI of the staging repository.'), 'repository.staging.ref' => pht('The ref name for this change in the staging repository.'), ); } public function newBuildableEngine() { return new DifferentialBuildableEngine(); } /* -( HarbormasterCircleCIBuildableInterface )----------------------------- */ public function getCircleCIGitHubRepositoryURI() { $diff_phid = $this->getPHID(); $repository_phid = $this->getRepositoryPHID(); if (!$repository_phid) { throw new Exception( pht( 'This diff ("%s") is not associated with a repository. A diff '. 'must belong to a tracked repository to be built by CircleCI.', $diff_phid)); } $repository = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($repository_phid)) ->executeOne(); if (!$repository) { throw new Exception( pht( 'This diff ("%s") is associated with a repository ("%s") which '. 'could not be loaded.', $diff_phid, $repository_phid)); } $staging_uri = $repository->getStagingURI(); if (!$staging_uri) { throw new Exception( pht( 'This diff ("%s") is associated with a repository ("%s") that '. 'does not have a Staging Area configured. You must configure a '. 'Staging Area to use CircleCI integration.', $diff_phid, $repository_phid)); } $path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath( $staging_uri); if (!$path) { throw new Exception( pht( 'This diff ("%s") is associated with a repository ("%s") that '. 'does not have a Staging Area ("%s") that is hosted on GitHub. '. 'CircleCI can only build from GitHub, so the Staging Area for '. 'the repository must be hosted there.', $diff_phid, $repository_phid, $staging_uri)); } return $staging_uri; } public function getCircleCIBuildIdentifierType() { return 'tag'; } public function getCircleCIBuildIdentifier() { $ref = $this->getStagingRef(); $ref = preg_replace('(^refs/tags/)', '', $ref); return $ref; } /* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */ public function getBuildkiteBranch() { $ref = $this->getStagingRef(); // NOTE: Circa late January 2017, Buildkite fails with the error message // "Tags have been disabled for this project" if we pass the "refs/tags/" // prefix via the API and the project doesn't have GitHub tag builds // enabled, even if GitHub builds are disabled. The tag builds fine // without this prefix. $ref = preg_replace('(^refs/tags/)', '', $ref); return $ref; } public function getBuildkiteCommit() { return 'HEAD'; } public function getStagingRef() { // TODO: We're just hoping to get lucky. Instead, `arc` should store // where it sent changes and we should only provide staging details // if we reasonably believe they are accurate. return 'refs/tags/phabricator/diff/'.$this->getID(); } public function loadTargetBranch() { // TODO: This is sketchy, but just eat the query cost until this can get // cleaned up. // For now, we're only returning a target if there's exactly one and it's // a branch, since we don't support landing to more esoteric targets like // tags yet. $property = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $this->getID(), 'arc:onto'); if (!$property) { return null; } $data = $property->getData(); if (!$data) { return null; } if (!is_array($data)) { return null; } if (count($data) != 1) { return null; } $onto = head($data); if (!is_array($onto)) { return null; } $type = idx($onto, 'type'); if ($type != 'branch') { return null; } return idx($onto, 'name'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialDiffEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new DifferentialDiffTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); foreach ($this->loadChangesets() as $changeset) { $engine->destroyObject($changeset); } $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $prop) { $prop->delete(); } $this->saveTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('revisionPHID') ->setType('phid') ->setDescription(pht('Associated revision PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Revision author PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid') ->setDescription(pht('Associated repository PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('refs') ->setType('map') ->setDescription(pht('List of related VCS references.')), ); } public function getFieldValuesForConduit() { $refs = array(); $branch = $this->getBranch(); if (strlen($branch)) { $refs[] = array( 'type' => 'branch', 'name' => $branch, ); } $onto = $this->loadTargetBranch(); if (strlen($onto)) { $refs[] = array( 'type' => 'onto', 'name' => $onto, ); } $base = $this->getSourceControlBaseRevision(); if (strlen($base)) { $refs[] = array( 'type' => 'base', 'identifier' => $base, ); } $bookmark = $this->getBookmark(); if (strlen($bookmark)) { $refs[] = array( 'type' => 'bookmark', 'name' => $bookmark, ); } $revision_phid = null; if ($this->getRevisionID()) { $revision_phid = $this->getRevision()->getPHID(); } return array( 'revisionPHID' => $revision_phid, 'authorPHID' => $this->getAuthorPHID(), 'repositoryPHID' => $this->getRepositoryPHID(), 'refs' => $refs, ); } public function getConduitSearchAttachments() { return array( id(new DifferentialCommitsSearchEngineAttachment()) ->setAttachmentKey('commits'), ); } } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index ef94da7f67..3397f9cb03 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,1152 +1,1148 @@ setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { $initial_state = DifferentialRevisionStatus::DRAFT; $should_broadcast = false; } else { $initial_state = DifferentialRevisionStatus::NEEDS_REVIEW; $should_broadcast = true; } return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) ->attachRepository(null) ->attachActiveDiff(null) ->attachReviewers(array()) ->setModernRevisionStatus($initial_state) ->setShouldBroadcast($should_broadcast); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'status' => 'text32', 'summary' => 'text', 'testPlan' => 'text', 'authorPHID' => 'phid?', 'lastReviewerPHID' => 'phid?', 'lineCount' => 'uint32?', 'mailKey' => 'bytes40', 'branchName' => 'text255?', 'repositoryPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'repositoryPHID' => array( 'columns' => array('repositoryPHID'), ), // If you (or a project you are a member of) is reviewing a significant // fraction of the revisions on an install, the result set of open // revisions may be smaller than the result set of revisions where you // are a reviewer. In these cases, this key is better than keys on the // edge table. 'key_status' => array( 'columns' => array('status', 'phid'), ), ), ) + parent::getConfiguration(); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function hasRevisionProperty($key) { return array_key_exists($key, $this->properties); } public function getMonogram() { $id = $this->getID(); return "D{$id}"; } public function getURI() { return '/'.$this->getMonogram(); } public function loadIDsByCommitPHIDs($phids) { if (!$phids) { return array(); } $revision_ids = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE commitPHID IN (%Ls)', self::TABLE_COMMIT, $phids); return ipull($revision_ids, 'revisionID', 'commitPHID'); } public function loadCommitPHIDs() { if (!$this->getID()) { return ($this->commits = array()); } $commits = queryfx_all( $this->establishConnection('r'), 'SELECT commitPHID FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $commits = ipull($commits, 'commitPHID'); return ($this->commits = $commits); } public function getCommitPHIDs() { return $this->assertAttached($this->commits); } public function getActiveDiff() { // TODO: Because it's currently technically possible to create a revision // without an associated diff, we allow an attached-but-null active diff. // It would be good to get rid of this once we make diff-attaching // transactional. return $this->assertAttached($this->activeDiff); } public function attachActiveDiff($diff) { $this->activeDiff = $diff; return $this; } public function getDiffIDs() { return $this->assertAttached($this->diffIDs); } public function attachDiffIDs(array $ids) { rsort($ids); $this->diffIDs = array_values($ids); return $this; } public function attachCommitPHIDs(array $phids) { $this->commits = array_values($phids); return $this; } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function setAttachedPHIDs($type, array $phids) { $this->attached[$type] = array_fill_keys($phids, array()); return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialRevisionPHIDType::TYPECONST); } public function loadActiveDiff() { return id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d ORDER BY id DESC LIMIT 1', $this->getID()); } public function save() { if (!$this->getMailKey()) { $this->mailKey = Filesystem::readRandomCharacters(40); } return parent::save(); } public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } public function canReviewerForceAccept( PhabricatorUser $viewer, DifferentialReviewer $reviewer) { if (!$reviewer->isPackage()) { return false; } $map = $this->getReviewerForceAcceptMap($viewer); if (!$map) { return false; } if (isset($map[$reviewer->getReviewerPHID()])) { return true; } return false; } private function getReviewerForceAcceptMap(PhabricatorUser $viewer) { $fragment = $viewer->getCacheFragment(); if (!array_key_exists($fragment, $this->forceMap)) { $map = $this->newReviewerForceAcceptMap($viewer); $this->forceMap[$fragment] = $map; } return $this->forceMap[$fragment]; } private function newReviewerForceAcceptMap(PhabricatorUser $viewer) { $diff = $this->getActiveDiff(); if (!$diff) { return null; } $repository_phid = $diff->getRepositoryPHID(); if (!$repository_phid) { return null; } $paths = array(); try { $changesets = $diff->getChangesets(); } catch (Exception $ex) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($viewer) ->withDiffs(array($diff)) ->execute(); } foreach ($changesets as $changeset) { $paths[] = $changeset->getOwnersFilename(); } if (!$paths) { return null; } $reviewer_phids = array(); foreach ($this->getReviewers() as $reviewer) { if (!$reviewer->isPackage()) { continue; } $reviewer_phids[] = $reviewer->getReviewerPHID(); } if (!$reviewer_phids) { return null; } // Load all the reviewing packages which have control over some of the // paths in the change. These are packages which the actor may be able // to force-accept on behalf of. $control_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withPHIDs($reviewer_phids) ->withControl($repository_phid, $paths); $control_packages = $control_query->execute(); if (!$control_packages) { return null; } // Load all the packages which have potential control over some of the // paths in the change and are owned by the actor. These are packages // which the actor may be able to use their authority over to gain the // ability to force-accept for other packages. This query doesn't apply // dominion rules yet, and we'll bypass those rules later on. $authority_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withAuthorityPHIDs(array($viewer->getPHID())) ->withControl($repository_phid, $paths); $authority_packages = $authority_query->execute(); if (!$authority_packages) { return null; } $authority_packages = mpull($authority_packages, null, 'getPHID'); // Build a map from each path in the revision to the reviewer packages // which control it. $control_map = array(); foreach ($paths as $path) { $control_packages = $control_query->getControllingPackagesForPath( $repository_phid, $path); // Remove packages which the viewer has authority over. We don't need // to check these for force-accept because they can just accept them // normally. $control_packages = mpull($control_packages, null, 'getPHID'); foreach ($control_packages as $phid => $control_package) { if (isset($authority_packages[$phid])) { unset($control_packages[$phid]); } } if (!$control_packages) { continue; } $control_map[$path] = $control_packages; } if (!$control_map) { return null; } // From here on out, we only care about paths which we have at least one // controlling package for. $paths = array_keys($control_map); // Now, build a map from each path to the packages which would control it // if there were no dominion rules. $authority_map = array(); foreach ($paths as $path) { $authority_packages = $authority_query->getControllingPackagesForPath( $repository_phid, $path, $ignore_dominion = true); $authority_map[$path] = mpull($authority_packages, null, 'getPHID'); } // For each path, find the most general package that the viewer has // authority over. For example, we'll prefer a package that owns "/" to a // package that owns "/src/". $force_map = array(); foreach ($authority_map as $path => $package_map) { $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); // Find the package that we have authority over which has the most // general match for this path. $best_match = null; $best_package = null; foreach ($package_map as $package_phid => $package) { $package_paths = $package->getPathsForRepository($repository_phid); foreach ($package_paths as $package_path) { // NOTE: A strength of 0 means "no match". A strength of 1 means // that we matched "/", so we can not possibly find another stronger // match. $strength = $package_path->getPathMatchStrength( $path_fragments, $fragment_count); if (!$strength) { continue; } if ($strength < $best_match || !$best_package) { $best_match = $strength; $best_package = $package; if ($strength == 1) { break 2; } } } } if ($best_package) { $force_map[$path] = array( 'strength' => $best_match, 'package' => $best_package, ); } } // For each path which the viewer owns a package for, find other packages // which that authority can be used to force-accept. Once we find a way to // force-accept a package, we don't need to keep looking. $has_control = array(); foreach ($force_map as $path => $spec) { $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); $authority_strength = $spec['strength']; $control_packages = $control_map[$path]; foreach ($control_packages as $control_phid => $control_package) { if (isset($has_control[$control_phid])) { continue; } $control_paths = $control_package->getPathsForRepository( $repository_phid); foreach ($control_paths as $control_path) { $strength = $control_path->getPathMatchStrength( $path_fragments, $fragment_count); if (!$strength) { continue; } if ($strength > $authority_strength) { $authority = $spec['package']; $has_control[$control_phid] = array( 'authority' => $authority, 'phid' => $authority->getPHID(), ); break; } } } } // Return a map from packages which may be force accepted to the packages // which permit that forced acceptance. return ipull($has_control, 'phid'); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A revision's author (which effectively means "owner" after we added // commandeering) can always view and edit it. $author_phid = $this->getAuthorPHID(); if ($author_phid) { if ($user->getPHID() == $author_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { $description = array( pht('The owner of a revision can always view and edit it.'), ); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $description[] = pht( 'If a revision belongs to a repository, other users must be able '. 'to view the repository in order to view the revision.'); break; } return $description; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $repository_phid = $this->getRepositoryPHID(); $repository = $this->getRepository(); // Try to use the object if we have it, since it will save us some // data fetching later on. In some cases, we might not have it. $repository_ref = nonempty($repository, $repository_phid); if ($repository_ref) { $extended[] = array( $repository_ref, PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } public function getReviewers() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewers(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); $reviewers = mpull($reviewers, null, 'getReviewerPHID'); $this->reviewerStatus = $reviewers; return $this; } public function hasAttachedReviewers() { return ($this->reviewerStatus !== self::ATTACHABLE); } public function getReviewerPHIDs() { $reviewers = $this->getReviewers(); return mpull($reviewers, 'getReviewerPHID'); } public function getReviewerPHIDsForEdit() { $reviewers = $this->getReviewers(); $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; $value = array(); foreach ($reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); if ($reviewer->getReviewerStatus() == $status_blocking) { $value[] = 'blocking('.$phid.')'; } else { $value[] = $phid; } } return $value; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } public function setModernRevisionStatus($status) { return $this->setStatus($status); } public function getModernRevisionStatus() { return $this->getStatus(); } public function getLegacyRevisionStatus() { return $this->getStatusObject()->getLegacyKey(); } public function isClosed() { return $this->getStatusObject()->isClosedStatus(); } public function isAbandoned() { return $this->getStatusObject()->isAbandoned(); } public function isAccepted() { return $this->getStatusObject()->isAccepted(); } public function isNeedsReview() { return $this->getStatusObject()->isNeedsReview(); } public function isNeedsRevision() { return $this->getStatusObject()->isNeedsRevision(); } public function isChangePlanned() { return $this->getStatusObject()->isChangePlanned(); } public function isPublished() { return $this->getStatusObject()->isPublished(); } public function isDraft() { return $this->getStatusObject()->isDraft(); } public function getStatusIcon() { return $this->getStatusObject()->getIcon(); } public function getStatusDisplayName() { return $this->getStatusObject()->getDisplayName(); } public function getStatusIconColor() { return $this->getStatusObject()->getIconColor(); } public function getStatusTagColor() { return $this->getStatusObject()->getTagColor(); } public function getStatusObject() { $status = $this->getStatus(); return DifferentialRevisionStatus::newForStatus($status); } public function getFlag(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->flags, $viewer->getPHID()); } public function attachFlag( PhabricatorUser $viewer, PhabricatorFlag $flag = null) { $this->flags[$viewer->getPHID()] = $flag; return $this; } public function getHasDraft(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment()); } public function attachHasDraft(PhabricatorUser $viewer, $has_draft) { $this->drafts[$viewer->getCacheFragment()] = $has_draft; return $this; } public function getHoldAsDraft() { return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false); } public function setHoldAsDraft($hold) { return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold); } public function getShouldBroadcast() { return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true); } public function setShouldBroadcast($should_broadcast) { return $this->setProperty( self::PROPERTY_SHOULD_BROADCAST, $should_broadcast); } public function setAddedLineCount($count) { return $this->setProperty(self::PROPERTY_LINES_ADDED, $count); } public function getAddedLineCount() { return $this->getProperty(self::PROPERTY_LINES_ADDED); } public function setRemovedLineCount($count) { return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count); } public function getRemovedLineCount() { return $this->getProperty(self::PROPERTY_LINES_REMOVED); } public function hasLineCounts() { // This data was not populated on older revisions, so it may not be // present on all revisions. return isset($this->properties[self::PROPERTY_LINES_ADDED]); } public function getRevisionScaleGlyphs() { $add = $this->getAddedLineCount(); $rem = $this->getRemovedLineCount(); $all = ($add + $rem); if (!$all) { return ' '; } $map = array( 20 => 2, 50 => 3, 150 => 4, 375 => 5, 1000 => 6, 2500 => 7, ); $n = 1; foreach ($map as $size => $count) { if ($size <= $all) { $n = $count; } else { break; } } $add_n = (int)ceil(($add / $all) * $n); $rem_n = (int)ceil(($rem / $all) * $n); while ($add_n + $rem_n > $n) { if ($add_n > 1) { $add_n--; } else { $rem_n--; } } return str_repeat('+', $add_n). str_repeat('-', $rem_n). str_repeat(' ', (7 - $n)); } public function getBuildableStatus($phid) { $buildables = $this->getProperty(self::PROPERTY_BUILDABLES); if (!is_array($buildables)) { $buildables = array(); } $buildable = idx($buildables, $phid); if (!is_array($buildable)) { $buildable = array(); } return idx($buildable, 'status'); } public function setBuildableStatus($phid, $status) { $buildables = $this->getProperty(self::PROPERTY_BUILDABLES); if (!is_array($buildables)) { $buildables = array(); } $buildable = idx($buildables, $phid); if (!is_array($buildable)) { $buildable = array(); } $buildable['status'] = $status; $buildables[$phid] = $buildable; return $this->setProperty(self::PROPERTY_BUILDABLES, $buildables); } public function newBuildableStatus(PhabricatorUser $viewer, $phid) { // For Differential, we're ignoring autobuilds (local lint and unit) // when computing build status. Differential only cares about remote // builds when making publishing and undrafting decisions. $builds = $this->loadImpactfulBuildsForBuildablePHIDs( $viewer, array($phid)); return $this->newBuildableStatusForBuilds($builds); } public function newBuildableStatusForBuilds(array $builds) { // If we have nothing but passing builds, the buildable passes. if (!$builds) { return HarbormasterBuildableStatus::STATUS_PASSED; } // If we have any completed, non-passing builds, the buildable fails. foreach ($builds as $build) { if ($build->isComplete()) { return HarbormasterBuildableStatus::STATUS_FAILED; } } // Otherwise, we're still waiting for the build to pass or fail. return null; } public function loadImpactfulBuilds(PhabricatorUser $viewer) { $diff = $this->getActiveDiff(); // NOTE: We can't use `withContainerPHIDs()` here because the container // update in Harbormaster is not synchronous. $buildables = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withBuildablePHIDs(array($diff->getPHID())) ->withManualBuildables(false) ->execute(); if (!$buildables) { return array(); } return $this->loadImpactfulBuildsForBuildablePHIDs( $viewer, mpull($buildables, 'getPHID')); } private function loadImpactfulBuildsForBuildablePHIDs( PhabricatorUser $viewer, array $phids) { return id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs($phids) ->withAutobuilds(false) ->withBuildStatuses( array( HarbormasterBuildStatus::STATUS_INACTIVE, HarbormasterBuildStatus::STATUS_PENDING, HarbormasterBuildStatus::STATUS_BUILDING, HarbormasterBuildStatus::STATUS_FAILED, HarbormasterBuildStatus::STATUS_ABORTED, HarbormasterBuildStatus::STATUS_ERROR, HarbormasterBuildStatus::STATUS_PAUSED, HarbormasterBuildStatus::STATUS_DEADLOCKED, )) ->execute(); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterContainerPHID(); } public function getHarbormasterBuildablePHID() { return $this->loadActiveDiff()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } public function newBuildableEngine() { return new DifferentialBuildableEngine(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { if ($phid == $this->getAuthorPHID()) { return true; } // TODO: This only happens when adding or removing CCs, and is safe from a // policy perspective, but the subscription pathway should have some // opportunity to load this data properly. For now, this is the only case // where implicit subscription is not an intrinsic property of the object. if ($this->reviewerStatus == self::ATTACHABLE) { $reviewers = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->needReviewers(true) ->executeOne() ->getReviewers(); } else { $reviewers = $this->getReviewers(); } foreach ($reviewers as $reviewer) { if ($reviewer->getReviewerPHID() !== $phid) { continue; } if ($reviewer->isResigned()) { continue; } return true; } return false; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('differential.fields'); } public function getCustomFieldBaseClass() { return 'DifferentialCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new DifferentialTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $diffs = id(new DifferentialDiffQuery()) ->setViewer($engine->getViewer()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($diffs as $diff) { $engine->destroyObject($diff); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); // we have to do paths a little differently as they do not have // an id or phid column for delete() to act on $dummy_path = new DifferentialAffectedPath(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $dummy_path->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DifferentialRevisionFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new DifferentialRevisionFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The revision title.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Revision author PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Information about revision status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid?') ->setDescription(pht('Revision repository PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('diffPHID') ->setType('phid') ->setDescription(pht('Active diff PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('summary') ->setType('string') ->setDescription(pht('Revision summary.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('testPlan') ->setType('string') ->setDescription(pht('Revision test plan.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isDraft') ->setType('bool') ->setDescription( pht( 'True if this revision is in any draft state, and thus not '. 'notifying reviewers and subscribers about changes.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('holdAsDraft') ->setType('bool') ->setDescription( pht( 'True if this revision is being held as a draft. It will not be '. 'automatically submitted for review even if tests pass.')), ); } public function getFieldValuesForConduit() { $status = $this->getStatusObject(); $status_info = array( 'value' => $status->getKey(), 'name' => $status->getDisplayName(), 'closed' => $status->isClosedStatus(), 'color.ansi' => $status->getANSIColor(), ); return array( 'title' => $this->getTitle(), 'authorPHID' => $this->getAuthorPHID(), 'status' => $status_info, 'repositoryPHID' => $this->getRepositoryPHID(), 'diffPHID' => $this->getActiveDiffPHID(), 'summary' => $this->getSummary(), 'testPlan' => $this->getTestPlan(), 'isDraft' => !$this->getShouldBroadcast(), 'holdAsDraft' => (bool)$this->getHoldAsDraft(), ); } public function getConduitSearchAttachments() { return array( id(new DifferentialReviewersSearchEngineAttachment()) ->setAttachmentKey('reviewers'), ); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DifferentialRevisionDraftEngine(); } /* -( PhabricatorTimelineInterface )--------------------------------------- */ public function newTimelineEngine() { return new DifferentialRevisionTimelineEngine(); } } diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php index d9dfc0107e..480bc50d05 100644 --- a/src/applications/diviner/storage/DivinerLiveBook.php +++ b/src/applications/diviner/storage/DivinerLiveBook.php @@ -1,162 +1,158 @@ true, self::CONFIG_SERIALIZATION => array( 'configurationData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text64', 'repositoryPHID' => 'phid?', ), 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); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachProjectPHIDs(array $project_phids) { $this->projectPHIDs = $project_phids; return $this; } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } /* -( 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; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $atoms = id(new DivinerAtomQuery()) ->setViewer($engine->getViewer()) ->withBookPHIDs(array($this->getPHID())) ->execute(); foreach ($atoms as $atom) { $engine->destroyObject($atom); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DivinerLiveBookEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new DivinerLiveBookTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DivinerLiveBookFulltextEngine(); } } diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index 368dbe8f6a..ebe2f9f601 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -1,391 +1,387 @@ 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('') ->setIsDisabled(0); } 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' => 'sort255', 'isDisabled' => 'bool', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DrydockBlueprintPHIDType::TYPECONST); } public function getImplementation() { return $this->assertAttached($this->implementation); } public function attachImplementation(DrydockBlueprintImplementation $impl) { $this->implementation = $impl; return $this; } public function hasImplementation() { return ($this->implementation !== self::ATTACHABLE); } 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 getFieldValue($key) { $key = "std:drydock:core:{$key}"; $fields = $this->loadCustomFields(); $field = idx($fields, $key); if (!$field) { throw new Exception( pht( 'Unknown blueprint field "%s"!', $key)); } return $field->getBlueprintFieldValue(); } private function loadCustomFields() { if ($this->fields === null) { $field_list = PhabricatorCustomField::getObjectFields( $this, PhabricatorCustomField::ROLE_VIEW); $field_list->readFieldsFromStorage($this); $this->fields = $field_list->getFields(); } return $this->fields; } public function logEvent($type, array $data = array()) { $log = id(new DrydockLog()) ->setEpoch(PhabricatorTime::getNow()) ->setType($type) ->setData($data); $log->setBlueprintPHID($this->getPHID()); return $log->save(); } public function getURI() { $id = $this->getID(); return "/drydock/blueprint/{$id}/"; } /* -( Allocating Resources )----------------------------------------------- */ /** * @task resource */ public function canEverAllocateResourceForLease(DrydockLease $lease) { return $this->getImplementation()->canEverAllocateResourceForLease( $this, $lease); } /** * @task resource */ public function canAllocateResourceForLease(DrydockLease $lease) { return $this->getImplementation()->canAllocateResourceForLease( $this, $lease); } /** * @task resource */ public function allocateResource(DrydockLease $lease) { return $this->getImplementation()->allocateResource( $this, $lease); } /** * @task resource */ public function activateResource(DrydockResource $resource) { return $this->getImplementation()->activateResource( $this, $resource); } /** * @task resource */ public function destroyResource(DrydockResource $resource) { $this->getImplementation()->destroyResource( $this, $resource); return $this; } /** * @task resource */ public function getResourceName(DrydockResource $resource) { return $this->getImplementation()->getResourceName( $this, $resource); } /* -( Acquiring Leases )--------------------------------------------------- */ /** * @task lease */ public function canAcquireLeaseOnResource( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->canAcquireLeaseOnResource( $this, $resource, $lease); } /** * @task lease */ public function acquireLease( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->acquireLease( $this, $resource, $lease); } /** * @task lease */ public function activateLease( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->activateLease( $this, $resource, $lease); } /** * @task lease */ public function didReleaseLease( DrydockResource $resource, DrydockLease $lease) { $this->getImplementation()->didReleaseLease( $this, $resource, $lease); return $this; } /** * @task lease */ public function destroyLease( DrydockResource $resource, DrydockLease $lease) { $this->getImplementation()->destroyLease( $this, $resource, $lease); return $this; } public function getInterface( DrydockResource $resource, DrydockLease $lease, $type) { $interface = $this->getImplementation() ->getInterface($this, $resource, $lease, $type); if (!$interface) { throw new Exception( pht( 'Unable to build resource interface of type "%s".', $type)); } return $interface; } public function shouldAllocateSupplementalResource( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->shouldAllocateSupplementalResource( $this, $resource, $lease); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DrydockBlueprintEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new DrydockBlueprintTransaction(); } /* -( 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; } /* -( 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; } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new DrydockBlueprintNameNgrams()) ->setValue($this->getBlueprintName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of this blueprint.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('type') ->setType('string') ->setDescription(pht('The type of resource this blueprint provides.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getBlueprintName(), 'type' => $this->getImplementation()->getType(), ); } public function getConduitSearchAttachments() { return array( ); } } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 21c9bd2fd3..cc80d272b1 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1,1695 +1,1691 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withClasses(array('PhabricatorFilesApplication')) ->executeOne(); $view_policy = $app->getPolicy( FilesDefaultViewCapability::CAPABILITY); return id(new PhabricatorFile()) ->setViewPolicy($view_policy) ->setIsPartial(0) ->attachOriginalFile(null) ->attachObjects(array()) ->attachObjectPHIDs(array()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255?', 'mimeType' => 'text255?', 'byteSize' => 'uint64', 'storageEngine' => 'text32', 'storageFormat' => 'text32', 'storageHandle' => 'text255', 'authorPHID' => 'phid?', 'secretKey' => 'bytes20?', 'contentHash' => 'bytes64?', 'ttl' => 'epoch?', 'isExplicitUpload' => 'bool?', 'mailKey' => 'bytes20', 'isPartial' => 'bool', 'builtinKey' => 'text64?', 'isDeleted' => 'bool', ), 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'), ), 'key_partial' => array( 'columns' => array('authorPHID', 'isPartial'), ), 'key_builtin' => array( 'columns' => array('builtinKey'), 'unique' => true, ), 'key_engine' => array( 'columns' => array('storageEngine', 'storageHandle(64)'), ), ), ) + 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 saveAndIndex() { $this->save(); if ($this->isIndexableFile()) { PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID()); } return $this; } private function isIndexableFile() { if ($this->getIsChunk()) { return false; } return true; } public function getMonogram() { return 'F'.$this->getID(); } public function scrambleSecret() { return $this->setSecretKey($this->generateSecretKey()); } public static function readUploadedFileData($spec) { if (!$spec) { throw new Exception(pht('No file was uploaded!')); } $err = idx($spec, 'error'); if ($err) { throw new PhabricatorFileUploadException($err); } $tmp_name = idx($spec, 'tmp_name'); // NOTE: If we parsed the request body ourselves, the files we wrote will // not be registered in the `is_uploaded_file()` list. It's fine to skip // this check: it just protects against sloppy code from the long ago era // of "register_globals". if (ini_get('enable_post_data_reading')) { $is_valid = @is_uploaded_file($tmp_name); if (!$is_valid) { throw new Exception(pht('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(pht('File size disagrees with uploaded size.')); } 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()) { return self::newFromFileData($data, $params); } public static function newFileFromContentHash($hash, array $params) { if ($hash === null) { return null; } // Check to see if a file with same hash already exists. $file = id(new PhabricatorFile())->loadOneWhere( 'contentHash = %s LIMIT 1', $hash); if (!$file) { return null; } $copy_of_storage_engine = $file->getStorageEngine(); $copy_of_storage_handle = $file->getStorageHandle(); $copy_of_storage_format = $file->getStorageFormat(); $copy_of_storage_properties = $file->getStorageProperties(); $copy_of_byte_size = $file->getByteSize(); $copy_of_mime_type = $file->getMimeType(); $new_file = self::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->setStorageProperties($copy_of_storage_properties); $new_file->setMimeType($copy_of_mime_type); $new_file->copyDimensions($file); $new_file->readPropertiesFromParameters($params); $new_file->saveAndIndex(); return $new_file; } public static function newChunkedFile( PhabricatorFileStorageEngine $engine, $length, array $params) { $file = self::initializeNewFile(); $file->setByteSize($length); // NOTE: Once we receive the first chunk, we'll detect its MIME type and // update the parent file if a MIME type hasn't been provided. This matters // for large media files like video. $mime_type = idx($params, 'mime-type'); if (!strlen($mime_type)) { $file->setMimeType('application/octet-stream'); } $chunked_hash = idx($params, 'chunkedHash'); // Get rid of this parameter now; we aren't passing it any further down // the stack. unset($params['chunkedHash']); if ($chunked_hash) { $file->setContentHash($chunked_hash); } else { // See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some // discussion of this. $seed = Filesystem::readRandomBytes(64); $hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput( $seed); $file->setContentHash($hash); } $file->setStorageEngine($engine->getEngineIdentifier()); $file->setStorageHandle(PhabricatorFileChunk::newChunkHandle()); // Chunked files are always stored raw because they do not actually store // data. The chunks do, and can be individually formatted. $file->setStorageFormat(PhabricatorFileRawStorageFormat::FORMATKEY); $file->setIsPartial(1); $file->readPropertiesFromParameters($params); return $file; } private static function buildFromFileData($data, array $params = array()) { if (isset($params['storageEngines'])) { $engines = $params['storageEngines']; } else { $size = strlen($data); $engines = PhabricatorFileStorageEngine::loadStorageEngines($size); if (!$engines) { throw new Exception( pht( 'No configured storage engine can store this file. See '. '"Configuring File Storage" in the documentation for '. 'information on configuring storage engines.')); } } assert_instances_of($engines, 'PhabricatorFileStorageEngine'); if (!$engines) { throw new Exception(pht('No valid storage engines are available!')); } $file = self::initializeNewFile(); $aes_type = PhabricatorFileAES256StorageFormat::FORMATKEY; $has_aes = PhabricatorKeyring::getDefaultKeyName($aes_type); if ($has_aes !== null) { $default_key = PhabricatorFileAES256StorageFormat::FORMATKEY; } else { $default_key = PhabricatorFileRawStorageFormat::FORMATKEY; } $key = idx($params, 'format', $default_key); // Callers can pass in an object explicitly instead of a key. This is // primarily useful for unit tests. if ($key instanceof PhabricatorFileStorageFormat) { $format = clone $key; } else { $format = clone PhabricatorFileStorageFormat::requireFormat($key); } $format->setFile($file); $properties = $format->newStorageProperties(); $file->setStorageFormat($format->getStorageFormatKey()); $file->setStorageProperties($properties); $data_handle = null; $engine_identifier = null; $integrity_hash = null; $exceptions = array(); foreach ($engines as $engine) { $engine_class = get_class($engine); try { $result = $file->writeToEngine( $engine, $data, $params); list($engine_identifier, $data_handle, $integrity_hash) = $result; // 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( pht('All storage engines failed to write file:'), $exceptions); } $file->setByteSize(strlen($data)); $hash = self::hashFileContent($data); $file->setContentHash($hash); $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); $file->setIntegrityHash($integrity_hash); $file->readPropertiesFromParameters($params); if (!$file->getMimeType()) { $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); $file->setMimeType(Filesystem::getMimeType($tmp)); unset($tmp); } try { $file->updateDimensions(false); } catch (Exception $ex) { // Do nothing. } $file->saveAndIndex(); return $file; } public static function newFromFileData($data, array $params = array()) { $hash = self::hashFileContent($data); if ($hash !== null) { $file = self::newFileFromContentHash($hash, $params); if ($file) { return $file; } } return self::buildFromFileData($data, $params); } public function migrateToEngine( PhabricatorFileStorageEngine $engine, $make_copy) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( pht("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, $integrity_hash) = $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->setIntegrityHash($integrity_hash); $this->save(); if (!$make_copy) { $this->deleteFileDataIfUnused( $old_engine, $old_identifier, $old_handle); } return $this; } public function migrateToStorageFormat(PhabricatorFileStorageFormat $format) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( pht("You can not migrate a file which hasn't yet been saved.")); } $data = $this->loadFileData(); $params = array( 'name' => $this->getName(), ); $engine = $this->instantiateStorageEngine(); $old_handle = $this->getStorageHandle(); $properties = $format->newStorageProperties(); $this->setStorageFormat($format->getStorageFormatKey()); $this->setStorageProperties($properties); list($identifier, $new_handle, $integrity_hash) = $this->writeToEngine( $engine, $data, $params); $this->setStorageHandle($new_handle); $this->setIntegrityHash($integrity_hash); $this->save(); $this->deleteFileDataIfUnused( $engine, $identifier, $old_handle); return $this; } public function cycleMasterStorageKey(PhabricatorFileStorageFormat $format) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( pht("You can not cycle keys for a file which hasn't yet been saved.")); } $properties = $format->cycleStorageProperties(); $this->setStorageProperties($properties); $this->save(); return $this; } private function writeToEngine( PhabricatorFileStorageEngine $engine, $data, array $params) { $engine_class = get_class($engine); $format = $this->newStorageFormat(); $data_iterator = array($data); $formatted_iterator = $format->newWriteIterator($data_iterator); $formatted_data = $this->loadDataFromIterator($formatted_iterator); $integrity_hash = $engine->newIntegrityHash($formatted_data, $format); $data_handle = $engine->writeFile($formatted_data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new PhabricatorFileStorageConfigurationException( pht( "Storage engine '%s' executed %s but did not return a valid ". "handle ('%s') to the data: it must be nonempty and no longer ". "than 255 characters.", $engine_class, 'writeFile()', $data_handle)); } $engine_identifier = $engine->getEngineIdentifier(); if (!$engine_identifier || strlen($engine_identifier) > 32) { throw new PhabricatorFileStorageConfigurationException( pht( "Storage engine '%s' returned an improper engine identifier '{%s}': ". "it must be nonempty and no longer than 32 characters.", $engine_class, $engine_identifier)); } return array($engine_identifier, $data_handle, $integrity_hash); } /** * Download a remote resource over HTTP and save the response body as a file. * * This method respects `security.outbound-blacklist`, and protects against * HTTP redirection (by manually following "Location" headers and verifying * each destination). It does not protect against DNS rebinding. See * discussion in T6755. */ public static function newFromFileDownload($uri, array $params = array()) { $timeout = 5; $redirects = array(); $current = $uri; while (true) { try { if (count($redirects) > 10) { throw new Exception( pht('Too many redirects trying to fetch remote URI.')); } $resolved = PhabricatorEnv::requireValidRemoteURIForFetch( $current, array( 'http', 'https', )); list($resolved_uri, $resolved_domain) = $resolved; $current = new PhutilURI($current); if ($current->getProtocol() == 'http') { // For HTTP, we can use a pre-resolved URI to defuse DNS rebinding. $fetch_uri = $resolved_uri; $fetch_host = $resolved_domain; } else { // For HTTPS, we can't: cURL won't verify the SSL certificate if // the domain has been replaced with an IP. But internal services // presumably will not have valid certificates for rebindable // domain names on attacker-controlled domains, so the DNS rebinding // attack should generally not be possible anyway. $fetch_uri = $current; $fetch_host = null; } $future = id(new HTTPSFuture($fetch_uri)) ->setFollowLocation(false) ->setTimeout($timeout); if ($fetch_host !== null) { $future->addHeader('Host', $fetch_host); } list($status, $body, $headers) = $future->resolve(); if ($status->isRedirect()) { // This is an HTTP 3XX status, so look for a "Location" header. $location = null; foreach ($headers as $header) { list($name, $value) = $header; if (phutil_utf8_strtolower($name) == 'location') { $location = $value; break; } } // HTTP 3XX status with no "Location" header, just treat this like // a normal HTTP error. if ($location === null) { throw $status; } if (isset($redirects[$location])) { throw new Exception( pht('Encountered loop while following redirects.')); } $redirects[$location] = $location; $current = $location; // We'll fall off the bottom and go try this URI now. } else if ($status->isError()) { // This is something other than an HTTP 2XX or HTTP 3XX status, so // just bail out. throw $status; } else { // This is HTTP 2XX, so use the response body to save the file data. // Provide a default name based on the URI, truncating it if the URI // is exceptionally long. $default_name = basename($uri); $default_name = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(64) ->truncateString($default_name); $params = $params + array( 'name' => $default_name, ); return self::newFromFileData($body, $params); } } catch (Exception $ex) { if ($redirects) { throw new PhutilProxyException( pht( 'Failed to fetch remote URI "%s" after following %s redirect(s) '. '(%s): %s', $uri, phutil_count($redirects), implode(' > ', array_keys($redirects)), $ex->getMessage()), $ex); } else { throw $ex; } } } } 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) { // NOTE: Hashing can fail if the algorithm isn't available in the current // build of PHP. It's fine if we're unable to generate a content hash: // it just means we'll store extra data when users upload duplicate files // instead of being able to deduplicate it. $hash = hash('sha256', $data, $raw_output = false); if ($hash === false) { return null; } return $hash; } public function loadFileData() { $iterator = $this->getFileDataIterator(); return $this->loadDataFromIterator($iterator); } /** * Return an iterable which emits file content bytes. * * @param int Offset for the start of data. * @param int Offset for the end of data. * @return Iterable Iterable object which emits requested data. */ public function getFileDataIterator($begin = null, $end = null) { $engine = $this->instantiateStorageEngine(); $format = $this->newStorageFormat(); $iterator = $engine->getRawFileDataIterator( $this, $begin, $end, $format); return $iterator; } public function getURI() { return $this->getInfoURI(); } public function getViewURI() { if (!$this->getPHID()) { throw new Exception( pht('You must save a file before you can generate a view URI.')); } return $this->getCDNURI('data'); } public function getCDNURI($request_kind) { if (($request_kind !== 'data') && ($request_kind !== 'download')) { throw new Exception( pht( 'Unknown file content request kind "%s".', $request_kind)); } $name = self::normalizeFileName($this->getName()); $name = phutil_escape_uri($name); $parts = array(); $parts[] = 'file'; $parts[] = $request_kind; // If this is an instanced install, add the instance identifier to the URI. // Instanced configurations behind a CDN may not be able to control the // request domain used by the CDN (as with AWS CloudFront). Embedding the // instance identity in the path allows us to distinguish between requests // originating from different instances but served through the same CDN. $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { $parts[] = '@'.$instance; } $parts[] = $this->getSecretKey(); $parts[] = $this->getPHID(); $parts[] = $name; $path = '/'.implode('/', $parts); // If this file is only partially uploaded, we're just going to return a // local URI to make sure that Ajax works, since the page is inevitably // going to give us an error back. if ($this->getIsPartial()) { return PhabricatorEnv::getURI($path); } else { 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() { return $this->getCDNURI('download'); } public function getURIForTransform(PhabricatorFileTransform $transform) { return $this->getTransformedURI($transform->getTransformKey()); } private function getTransformedURI($transform) { $parts = array(); $parts[] = 'file'; $parts[] = 'xform'; $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { $parts[] = '@'.$instance; } $parts[] = $transform; $parts[] = $this->getPHID(); $parts[] = $this->getSecretKey(); $path = implode('/', $parts); $path = $path.'/'; 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 isVideo() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.video-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isPDF() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = array( 'application/pdf' => 'application/pdf', ); $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(pht('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 getDragAndDropDictionary() { return array( 'id' => $this->getID(), 'phid' => $this->getPHID(), 'uri' => $this->getBestURI(), ); } 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( pht( "Storage engine '%s' could not be located!", $engine_identifier)); } public static function buildAllEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorFileStorageEngine') ->execute(); } 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 setStorageProperties(array $properties) { $this->metadata[self::METADATA_STORAGE] = $properties; return $this; } public function getStorageProperties() { return idx($this->metadata, self::METADATA_STORAGE, array()); } public function getStorageProperty($key, $default = null) { $properties = $this->getStorageProperties(); return idx($properties, $key, $default); } public function loadDataFromIterator($iterator) { $result = ''; foreach ($iterator as $chunk) { $result .= $chunk; } return $result; } public function updateDimensions($save = true) { if (!$this->isViewableImage()) { throw new Exception(pht('This file is not a viewable image.')); } if (!function_exists('imagecreatefromstring')) { throw new Exception(pht('Cannot retrieve image information.')); } if ($this->getIsChunk()) { throw new Exception( pht('Refusing to assess image dimensions of file chunk.')); } $engine = $this->instantiateStorageEngine(); if ($engine->isChunkEngine()) { throw new Exception( pht('Refusing to assess image dimensions of chunked file.')); } $data = $this->loadFileData(); $img = @imagecreatefromstring($data); if ($img === false) { throw new Exception(pht('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 specs. * @return dict Dictionary of named builtins. */ public static function loadBuiltins(PhabricatorUser $user, array $builtins) { $builtins = mpull($builtins, null, 'getBuiltinFileKey'); // NOTE: Anyone is allowed to access builtin files. $files = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuiltinKeys(array_keys($builtins)) ->execute(); $results = array(); foreach ($files as $file) { $builtin_key = $file->getBuiltinName(); if ($builtin_key !== null) { $results[$builtin_key] = $file; } } $build = array(); foreach ($builtins as $key => $builtin) { if (isset($results[$key])) { continue; } $data = $builtin->loadBuiltinFileData(); $params = array( 'name' => $builtin->getBuiltinDisplayName(), 'canCDN' => true, 'builtin' => $key, ); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $file = self::newFromFileData($data, $params); } catch (AphrontDuplicateKeyQueryException $ex) { $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuiltinKeys(array($key)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Collided mid-air when generating builtin file "%s", but '. 'then failed to load the object we collided with.', $key)); } } unset($unguarded); $file->attachObjectPHIDs(array()); $file->attachObjects(array()); $results[$key] = $file; } return $results; } /** * 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) { $builtin = id(new PhabricatorFilesOnDiskBuiltinFile()) ->setName($name); $key = $builtin->getBuiltinFileKey(); return idx(self::loadBuiltins($user, array($builtin)), $key); } 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; } public function isBuiltin() { return ($this->getBuiltinName() !== null); } public function getBuiltinName() { return idx($this->metadata, self::METADATA_BUILTIN); } public function setBuiltinName($name) { $this->metadata[self::METADATA_BUILTIN] = $name; return $this; } public function getIsProfileImage() { return idx($this->metadata, self::METADATA_PROFILE); } public function setIsProfileImage($value) { $this->metadata[self::METADATA_PROFILE] = $value; return $this; } public function getIsChunk() { return idx($this->metadata, self::METADATA_CHUNK); } public function setIsChunk($value) { $this->metadata[self::METADATA_CHUNK] = $value; return $this; } public function setIntegrityHash($integrity_hash) { $this->metadata[self::METADATA_INTEGRITY] = $integrity_hash; return $this; } public function getIntegrityHash() { return idx($this->metadata, self::METADATA_INTEGRITY); } public function newIntegrityHash() { $engine = $this->instantiateStorageEngine(); if ($engine->isChunkEngine()) { return null; } $format = $this->newStorageFormat(); $storage_handle = $this->getStorageHandle(); $data = $engine->readFile($storage_handle); return $engine->newIntegrityHash($data, $format); } /** * 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) { PhutilTypeSpec::checkMap( $params, array( 'name' => 'optional string', 'authorPHID' => 'optional string', 'ttl.relative' => 'optional int', 'ttl.absolute' => 'optional int', 'viewPolicy' => 'optional string', 'isExplicitUpload' => 'optional bool', 'canCDN' => 'optional bool', 'profile' => 'optional bool', 'format' => 'optional string|PhabricatorFileStorageFormat', 'mime-type' => 'optional string', 'builtin' => 'optional string', 'storageEngines' => 'optional list', 'chunk' => 'optional bool', )); $file_name = idx($params, 'name'); $this->setName($file_name); $author_phid = idx($params, 'authorPHID'); $this->setAuthorPHID($author_phid); $absolute_ttl = idx($params, 'ttl.absolute'); $relative_ttl = idx($params, 'ttl.relative'); if ($absolute_ttl !== null && $relative_ttl !== null) { throw new Exception( pht( 'Specify an absolute TTL or a relative TTL, but not both.')); } else if ($absolute_ttl !== null) { if ($absolute_ttl < PhabricatorTime::getNow()) { throw new Exception( pht( 'Absolute TTL must be in the present or future, but TTL "%s" '. 'is in the past.', $absolute_ttl)); } $this->setTtl($absolute_ttl); } else if ($relative_ttl !== null) { if ($relative_ttl < 0) { throw new Exception( pht( 'Relative TTL must be zero or more seconds, but "%s" is '. 'negative.', $relative_ttl)); } $max_relative = phutil_units('365 days in seconds'); if ($relative_ttl > $max_relative) { throw new Exception( pht( 'Relative TTL must not be more than "%s" seconds, but TTL '. '"%s" was specified.', $max_relative, $relative_ttl)); } $absolute_ttl = PhabricatorTime::getNow() + $relative_ttl; $this->setTtl($absolute_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); } $builtin = idx($params, 'builtin'); if ($builtin) { $this->setBuiltinName($builtin); $this->setBuiltinKey($builtin); } $profile = idx($params, 'profile'); if ($profile) { $this->setIsProfileImage(true); } $mime_type = idx($params, 'mime-type'); if ($mime_type) { $this->setMimeType($mime_type); } $is_chunk = idx($params, 'chunk'); if ($is_chunk) { $this->setIsChunk(true); } 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); } public function newDownloadResponse() { // We're cheating a little bit here and relying on the fact that // getDownloadURI() always returns a fully qualified URI with a complete // domain. return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setCloseDialogBeforeRedirect(true) ->setURI($this->getDownloadURI()); } public function attachTransforms(array $map) { $this->transforms = $map; return $this; } public function getTransform($key) { return $this->assertAttachedKey($this->transforms, $key); } public function newStorageFormat() { $key = $this->getStorageFormat(); $template = PhabricatorFileStorageFormat::requireFormat($key); $format = id(clone $template) ->setFile($this); return $format; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorFileEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorFileTransaction(); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isBuiltin()) { return PhabricatorPolicies::getMostOpenPolicy(); } if ($this->getIsProfileImage()) { return PhabricatorPolicies::getMostOpenPolicy(); } 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); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the file.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('dataURI') ->setType('string') ->setDescription(pht('Download URI for the file data.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('size') ->setType('int') ->setDescription(pht('File size, in bytes.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'dataURI' => $this->getCDNURI('data'), 'size' => (int)$this->getByteSize(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorNgramInterface )------------------------------------------ */ public function newNgrams() { return array( id(new PhabricatorFileNameNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php index 67ce3c2d8c..87ab342e2a 100644 --- a/src/applications/fund/storage/FundBacker.php +++ b/src/applications/fund/storage/FundBacker.php @@ -1,121 +1,117 @@ setBackerPHID($actor->getPHID()) ->setStatus(self::STATUS_NEW); } 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(); } } diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index 077646fea0..5e4dd48026 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -1,217 +1,213 @@ 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()); } 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 getViewURI() { return '/'.$this->getMonogram(); } 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) { if ($viewer->getPHID() == $this->getOwnerPHID()) { return true; } if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { foreach ($viewer->getAuthorities() as $authority) { if ($authority instanceof PhortuneMerchant) { if ($authority->getPHID() == $this->getMerchantPHID()) { return true; } } } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of an initiative can always view and edit it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new FundInitiativeEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new FundInitiativeTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getOwnerPHID()); } /* -( PhabricatorTokenRecevierInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getOwnerPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new FundInitiativeFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new FundInitiativeFerretEngine(); } } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php index 25222975b7..7f743b3adf 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php @@ -1,108 +1,106 @@ viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } final public function setActingAsPHID($acting_as_phid) { $this->actingAsPHID = $acting_as_phid; return $this; } final public function getActingAsPHID() { return $this->actingAsPHID; } final public function setContentSource( PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } final public function getContentSource() { return $this->contentSource; } final public function setObject(HarbormasterBuildableInterface $object) { $this->object = $object; return $this; } final public function getObject() { return $this->object; } protected function getPublishableObject() { return $this->getObject(); } public function publishBuildable( HarbormasterBuildable $old, HarbormasterBuildable $new) { return; } final public static function newForObject( HarbormasterBuildableInterface $object, PhabricatorUser $viewer) { return $object->newBuildableEngine() ->setViewer($viewer) ->setObject($object); } final protected function newEditor() { $publishable = $this->getPublishableObject(); $viewer = $this->getViewer(); $editor = $publishable->getApplicationTransactionEditor() ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $acting_as_phid = $this->getActingAsPHID(); if ($acting_as_phid !== null) { $editor->setActingAsPHID($acting_as_phid); } $content_source = $this->getContentSource(); if ($content_source !== null) { $editor->setContentSource($content_source); } return $editor; } final protected function newTransaction() { $publishable = $this->getPublishableObject(); return $publishable->getApplicationTransactionTemplate(); } final protected function applyTransactions(array $xactions) { $publishable = $this->getPublishableObject(); $editor = $this->newEditor(); - $editor->applyTransactions( - $publishable->getApplicationTransactionObject(), - $xactions); + $editor->applyTransactions($publishable, $xactions); } public function getAuthorIdentity() { return null; } } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index 0e2e93a0c9..aabfd49eb9 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -1,420 +1,416 @@ setIsManualBuildable(0) ->setBuildableStatus(HarbormasterBuildableStatus::STATUS_PREPARING); } public function getMonogram() { return 'B'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } /** * 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 = self::initializeNewBuildable($actor) ->setBuildablePHID($buildable_object_phid) ->setContainerPHID($container_object_phid); $buildable->save(); return $buildable; } /** * Start builds for a given buildable. * * @param phid PHID of the object to build. * @param phid Container PHID for the buildable. * @param list List of builds to perform. * @return void */ public static function applyBuildPlans( $phid, $container_phid, array $requests) { assert_instances_of($requests, 'HarbormasterBuildRequest'); if (!$requests) { return; } // Skip all of this logic if the Harbormaster application // isn't currently installed. $harbormaster_app = 'PhabricatorHarbormasterApplication'; if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) { return; } $viewer = PhabricatorUser::getOmnipotentUser(); $buildable = self::createOrLoadExisting( $viewer, $phid, $container_phid); $plan_phids = mpull($requests, 'getBuildPlanPHID'); $plans = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withPHIDs($plan_phids) ->execute(); $plans = mpull($plans, null, 'getPHID'); foreach ($requests as $request) { $plan_phid = $request->getBuildPlanPHID(); $plan = idx($plans, $plan_phid); if (!$plan) { throw new Exception( pht( 'Failed to load build plan ("%s").', $plan_phid)); } 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; } $parameters = $request->getBuildParameters(); $buildable->applyPlan($plan, $parameters, $request->getInitiatorPHID()); } } public function applyPlan( HarbormasterBuildPlan $plan, array $parameters, $initiator_phid) { $viewer = PhabricatorUser::getOmnipotentUser(); $build = HarbormasterBuild::initializeNewBuild($viewer) ->setBuildablePHID($this->getPHID()) ->setBuildPlanPHID($plan->getPHID()) ->setBuildParameters($parameters) ->setBuildStatus(HarbormasterBuildStatus::STATUS_PENDING); if ($initiator_phid) { $build->setInitiatorPHID($initiator_phid); } $auto_key = $plan->getPlanAutoKey(); if ($auto_key) { $build->setPlanAutoKey($auto_key); } $build->save(); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); foreach ($steps as $step) { $step->willStartBuild($viewer, $this, $build, $plan); } PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildID' => $build->getID(), ), array( 'objectPHID' => $build->getPHID(), )); return $build; } 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 attachBuilds(array $builds) { assert_instances_of($builds, 'HarbormasterBuild'); $this->builds = $builds; return $this; } public function getBuilds() { return $this->assertAttached($this->builds); } /* -( Status )------------------------------------------------------------- */ public function getBuildableStatusObject() { $status = $this->getBuildableStatus(); return HarbormasterBuildableStatus::newBuildableStatusObject($status); } public function getStatusIcon() { return $this->getBuildableStatusObject()->getIcon(); } public function getStatusDisplayName() { return $this->getBuildableStatusObject()->getDisplayName(); } public function getStatusColor() { return $this->getBuildableStatusObject()->getColor(); } public function isPreparing() { return $this->getBuildableStatusObject()->isPreparing(); } public function isBuilding() { return $this->getBuildableStatusObject()->isBuilding(); } /* -( Messages )----------------------------------------------------------- */ public function sendMessage( PhabricatorUser $viewer, $message_type, $queue_update) { $message = HarbormasterBuildMessage::initializeNewMessage($viewer) ->setReceiverPHID($this->getPHID()) ->setType($message_type) ->save(); if ($queue_update) { PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildablePHID' => $this->getPHID(), ), array( 'objectPHID' => $this->getPHID(), )); } return $message; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildableTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HarbormasterBuildableTransaction(); } /* -( 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 getHarbormasterBuildableDisplayPHID() { return $this->getBuildableObject()->getHarbormasterBuildableDisplayPHID(); } 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(); } public function newBuildableEngine() { return $this->getBuildableObject()->newBuildableEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('objectPHID') ->setType('phid') ->setDescription(pht('PHID of the object that is built.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('containerPHID') ->setType('phid') ->setDescription(pht('PHID of the object containing this buildable.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildableStatus') ->setType('map') ->setDescription(pht('The current status of this buildable.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isManual') ->setType('bool') ->setDescription(pht('True if this is a manual buildable.')), ); } public function getFieldValuesForConduit() { return array( 'objectPHID' => $this->getBuildablePHID(), 'containerPHID' => $this->getContainerPHID(), 'buildableStatus' => array( 'value' => $this->getBuildableStatus(), ), 'isManual' => (bool)$this->getIsManualBuildable(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs(array($this->getPHID())) ->execute(); foreach ($builds as $build) { $engine->destroyObject($build); } $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withReceiverPHIDs(array($this->getPHID())) ->execute(); foreach ($messages as $message) { $engine->destroyObject($message); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 04ba35ee1e..602e388477 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,510 +1,506 @@ setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE) ->setBuildGeneration(0); } public function delete() { $this->openTransaction(); $this->deleteUnprocessedCommands(); $result = parent::delete(); $this->saveTransaction(); return $result; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'buildParameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'buildStatus' => 'text32', 'buildGeneration' => 'uint32', 'planAutoKey' => 'text32?', 'initiatorPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_plan' => array( 'columns' => array('buildPlanPHID'), ), 'key_status' => array( 'columns' => array('buildStatus'), ), 'key_planautokey' => array( 'columns' => array('buildablePHID', 'planAutoKey'), 'unique' => true, ), 'key_initiator' => array( 'columns' => array('initiatorPHID'), ), ), ) + 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->getBuildStatusObject()->isBuilding(); } public function isAutobuild() { return ($this->getPlanAutoKey() !== null); } public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, 'repository.phid' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, 'build.id' => null, 'initiator.phid' => null, ); foreach ($this->getBuildParameters() as $key => $value) { $results['build/'.$key] = $value; } $buildable = $this->getBuildable(); $object = $buildable->getBuildableObject(); $object_variables = $object->getBuildVariables(); $results = $object_variables + $results; $results['step.timestamp'] = time(); $results['build.id'] = $this->getID(); $results['initiator.phid'] = $this->getInitiatorPHID(); return $results; } public static function getAvailableBuildVariables() { $objects = id(new PhutilClassMapQuery()) ->setAncestorClass('HarbormasterBuildableInterface') ->execute(); $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.'), 'initiator.phid' => pht( 'The PHID of the user or Object that initiated the build, '. 'if applicable.'), ); foreach ($objects as $object) { $variables[] = $object->getAvailableBuildVariables(); } $variables = array_mergev($variables); return $variables; } public function isComplete() { return $this->getBuildStatusObject()->isComplete(); } public function isPaused() { return $this->getBuildStatusObject()->isPaused(); } public function isPassed() { return $this->getBuildStatusObject()->isPassed(); } public function getURI() { $id = $this->getID(); return "/harbormaster/build/{$id}/"; } protected function getBuildStatusObject() { $status_key = $this->getBuildStatus(); return HarbormasterBuildStatus::newBuildStatusObject($status_key); } /* -( Build Commands )----------------------------------------------------- */ private function getUnprocessedCommands() { return $this->assertAttached($this->unprocessedCommands); } public function attachUnprocessedCommands(array $commands) { $this->unprocessedCommands = $commands; return $this; } public function canRestartBuild() { if ($this->isAutobuild()) { return false; } return !$this->isRestarting(); } public function canPauseBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete() && !$this->isPaused() && !$this->isPausing(); } public function canAbortBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete(); } public function canResumeBuild() { if ($this->isAutobuild()) { return false; } return $this->isPaused() && !$this->isResuming(); } public function isPausing() { $is_pausing = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_PAUSE: $is_pausing = true; break; case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESTART: $is_pausing = false; break; case HarbormasterBuildCommand::COMMAND_ABORT: $is_pausing = true; break; } } return $is_pausing; } 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_PAUSE: $is_resuming = false; break; case HarbormasterBuildCommand::COMMAND_ABORT: $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 isAborting() { $is_aborting = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_ABORT: $is_aborting = true; break; } } return $is_aborting; } public function deleteUnprocessedCommands() { foreach ($this->getUnprocessedCommands() as $key => $command_object) { $command_object->delete(); unset($this->unprocessedCommands[$key]); } return $this; } public function canIssueCommand(PhabricatorUser $viewer, $command) { try { $this->assertCanIssueCommand($viewer, $command); return true; } catch (Exception $ex) { return false; } } public function assertCanIssueCommand(PhabricatorUser $viewer, $command) { $need_edit = false; switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: break; case HarbormasterBuildCommand::COMMAND_PAUSE: case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_ABORT: $need_edit = true; break; default: throw new Exception( pht( 'Invalid Harbormaster build command "%s".', $command)); } // Issuing these commands requires that you be able to edit the build, to // prevent enemy engineers from sabotaging your builds. See T9614. if ($need_edit) { PhabricatorPolicyFilter::requireCapability( $viewer, $this->getBuildPlan(), PhabricatorPolicyCapability::CAN_EDIT); } } public function sendMessage(PhabricatorUser $viewer, $command) { // TODO: This should not be an editor transaction, but there are plans to // merge BuildCommand into BuildMessage which should moot this. As this // exists today, it can race against BuildEngine. // This is a bogus content source, but this whole flow should be obsolete // soon. $content_source = PhabricatorContentSource::newForSource( PhabricatorConsoleContentSource::SOURCECONST); $editor = id(new HarbormasterBuildTransactionEditor()) ->setActor($viewer) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $viewer_phid = $viewer->getPHID(); if (!$viewer_phid) { $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID(); $editor->setActingAsPHID($acting_phid); } $xaction = id(new HarbormasterBuildTransaction()) ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) ->setNewValue($command); $editor->applyTransactions($this, array($xaction)); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } /* -( 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.'); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildablePHID') ->setType('phid') ->setDescription(pht('PHID of the object this build is building.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildPlanPHID') ->setType('phid') ->setDescription(pht('PHID of the build plan being run.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildStatus') ->setType('map') ->setDescription(pht('The current status of this build.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('initiatorPHID') ->setType('phid') ->setDescription(pht('The person (or thing) that started this build.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of this build.')), ); } public function getFieldValuesForConduit() { $status = $this->getBuildStatus(); return array( 'buildablePHID' => $this->getBuildablePHID(), 'buildPlanPHID' => $this->getBuildPlanPHID(), 'buildStatus' => array( 'value' => $status, 'name' => HarbormasterBuildStatus::getBuildStatusName($status), 'color.ansi' => HarbormasterBuildStatus::getBuildStatusANSIColor($status), ), 'initiatorPHID' => nonempty($this->getInitiatorPHID(), null), 'name' => $this->getName(), ); } public function getConduitSearchAttachments() { return array( id(new HarbormasterQueryBuildsSearchEngineAttachment()) ->setAttachmentKey('querybuilds'), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->withBuildPHIDs(array($this->getPHID())) ->execute(); foreach ($targets as $target) { $engine->destroyObject($target); } $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withReceiverPHIDs(array($this->getPHID())) ->execute(); foreach ($messages as $message) { $engine->destroyObject($message); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index a41dc89790..2e379aab23 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -1,233 +1,229 @@ setViewer($actor) ->withClasses(array('PhabricatorHarbormasterApplication')) ->executeOne(); $view_policy = $app->getPolicy( HarbormasterBuildPlanDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( HarbormasterBuildPlanDefaultEditCapability::CAPABILITY); return id(new HarbormasterBuildPlan()) ->setName('') ->setPlanStatus(self::STATUS_ACTIVE) ->attachBuildSteps(array()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'planStatus' => 'text32', 'planAutoKey' => 'text32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('planStatus'), ), 'key_name' => array( 'columns' => array('name'), ), 'key_planautokey' => array( 'columns' => array('planAutoKey'), 'unique' => true, ), ), ) + 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); } /* -( Autoplans )---------------------------------------------------------- */ public function isAutoplan() { return ($this->getPlanAutoKey() !== null); } public function getAutoplan() { if (!$this->isAutoplan()) { return null; } return HarbormasterBuildAutoplan::getAutoplan($this->getPlanAutoKey()); } public function canRunManually() { if ($this->isAutoplan()) { return false; } return true; } public function getName() { $autoplan = $this->getAutoplan(); if ($autoplan) { return $autoplan->getAutoplanName(); } return parent::getName(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildPlanEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HarbormasterBuildPlanTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isAutoplan()) { return PhabricatorPolicies::getMostOpenPolicy(); } return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isAutoplan()) { return PhabricatorPolicies::POLICY_NOONE; } return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { $messages = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isAutoplan()) { $messages[] = pht( 'This is an autoplan (a builtin plan provided by an application) '. 'so it can not be edited.'); } break; } return $messages; } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new HarbormasterBuildPlanNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of this build plan.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('The current status of this build plan.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'status' => array( 'value' => $this->getPlanStatus(), ), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php index cee9a3f9c3..dd0ebdc507 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php @@ -1,177 +1,173 @@ setName('') ->setDescription(''); } 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', '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` should be removed. 'name' => 'text255?', 'stepAutoKey' => 'text32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_plan' => array( 'columns' => array('buildPlanPHID'), ), 'key_stepautokey' => array( 'columns' => array('buildPlanPHID', 'stepAutoKey'), 'unique' => true, ), ), ) + 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; } public function isAutostep() { return ($this->getStepAutoKey() !== null); } public function willStartBuild( PhabricatorUser $viewer, HarbormasterBuildable $buildable, HarbormasterBuild $build, HarbormasterBuildPlan $plan) { return $this->getStepImplementation()->willStartBuild( $viewer, $buildable, $build, $plan, $this); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildStepEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HarbormasterBuildStepTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } 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 6b078336aa..1b005898cb 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -1,397 +1,393 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'contentType' => 'text255', 'mustMatchAll' => 'bool', 'configVersion' => 'uint32', 'repetitionPolicy' => 'text32', 'ruleType' => 'text32', 'isDisabled' => 'uint32', 'triggerObjectPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('name(128)'), ), '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 HeraldActionRecord())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachActions(array $actions) { // TODO: validate actions have been attached. assert_instances_of($actions, 'HeraldActionRecord'); $this->actions = $actions; return $this; } public function getActions() { return $this->actions; } 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, 'HeraldActionRecord'); return $this->saveChildren( id(new HeraldActionRecord())->getTableName(), $actions); } protected function saveChildren($table_name, array $children) { assert_instances_of($children, 'HeraldDAO'); if (!$this->getID()) { throw new PhutilInvalidStateException('save'); } 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 HeraldActionRecord())->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()); } public function getMonogram() { return 'H'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } /* -( Repetition Policies )------------------------------------------------ */ public function getRepetitionPolicyStringConstant() { return $this->getRepetitionPolicy(); } public function setRepetitionPolicyStringConstant($value) { $map = self::getRepetitionPolicyMap(); if (!isset($map[$value])) { throw new Exception( pht( 'Rule repetition string constant "%s" is unknown.', $value)); } return $this->setRepetitionPolicy($value); } public function isRepeatEvery() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_EVERY); } public function isRepeatFirst() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST); } public function isRepeatOnChange() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE); } public static function getRepetitionPolicySelectOptionMap() { $map = self::getRepetitionPolicyMap(); return ipull($map, 'select'); } private static function getRepetitionPolicyMap() { return array( self::REPEAT_EVERY => array( 'select' => pht('every time this rule matches:'), ), self::REPEAT_FIRST => array( 'select' => pht('only the first time this rule matches:'), ), self::REPEAT_CHANGE => array( 'select' => pht('if this rule did not match the last time:'), ), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HeraldRuleEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HeraldRuleTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return PhabricatorPolicies::getMostOpenPolicy(); } if ($this->isGlobalRule()) { $app = 'PhabricatorHeraldApplication'; $herald = PhabricatorApplication::getByClass($app); $global = HeraldManageGlobalRulesCapability::CAPABILITY; return $herald->getPolicy($global); } else if ($this->isObjectRule()) { return $this->getTriggerObject()->getPolicy($capability); } else { return $this->getAuthorPHID(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return null; } if ($this->isGlobalRule()) { return pht( 'Global Herald rules can be edited by users with the "Can Manage '. 'Global Rules" Herald application permission.'); } else if ($this->isObjectRule()) { return pht('Object rules inherit the edit policies of their objects.'); } else { return pht('A personal rule can only be edited by its owner.'); } } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return $this->isPersonalRule() && $phid == $this->getAuthorPHID(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/herald/storage/HeraldWebhook.php b/src/applications/herald/storage/HeraldWebhook.php index aa992ec203..0101dbef52 100644 --- a/src/applications/herald/storage/HeraldWebhook.php +++ b/src/applications/herald/storage/HeraldWebhook.php @@ -1,238 +1,234 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'webhookURI' => 'text255', 'status' => 'text32', 'hmacKey' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return HeraldWebhookPHIDType::TYPECONST; } public static function initializeNewWebhook(PhabricatorUser $viewer) { return id(new self()) ->setStatus(self::HOOKSTATUS_ENABLED) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($viewer->getPHID()) ->regenerateHMACKey(); } public function getURI() { return '/herald/webhook/view/'.$this->getID().'/'; } public function isDisabled() { return ($this->getStatus() === self::HOOKSTATUS_DISABLED); } public static function getStatusDisplayNameMap() { $specs = self::getStatusSpecifications(); return ipull($specs, 'name', 'key'); } private static function getStatusSpecifications() { $specs = array( array( 'key' => self::HOOKSTATUS_FIREHOSE, 'name' => pht('Firehose'), 'color' => 'orange', 'icon' => 'fa-star-o', ), array( 'key' => self::HOOKSTATUS_ENABLED, 'name' => pht('Enabled'), 'color' => 'bluegrey', 'icon' => 'fa-check', ), array( 'key' => self::HOOKSTATUS_DISABLED, 'name' => pht('Disabled'), 'color' => 'dark', 'icon' => 'fa-ban', ), ); return ipull($specs, null, 'key'); } private static function getSpecificationForStatus($status) { $specs = self::getStatusSpecifications(); if (isset($specs[$status])) { return $specs[$status]; } return array( 'key' => $status, 'name' => pht('Unknown ("%s")', $status), 'icon' => 'fa-question', 'color' => 'indigo', ); } public static function getDisplayNameForStatus($status) { $spec = self::getSpecificationForStatus($status); return $spec['name']; } public static function getIconForStatus($status) { $spec = self::getSpecificationForStatus($status); return $spec['icon']; } public static function getColorForStatus($status) { $spec = self::getSpecificationForStatus($status); return $spec['color']; } public function getStatusDisplayName() { $status = $this->getStatus(); return self::getDisplayNameForStatus($status); } public function getStatusIcon() { $status = $this->getStatus(); return self::getIconForStatus($status); } public function getStatusColor() { $status = $this->getStatus(); return self::getColorForStatus($status); } public function getErrorBackoffWindow() { return phutil_units('5 minutes in seconds'); } public function getErrorBackoffThreshold() { return 10; } public function isInErrorBackoff(PhabricatorUser $viewer) { $backoff_window = $this->getErrorBackoffWindow(); $backoff_threshold = $this->getErrorBackoffThreshold(); $now = PhabricatorTime::getNow(); $window_start = ($now - $backoff_window); $requests = id(new HeraldWebhookRequestQuery()) ->setViewer($viewer) ->withWebhookPHIDs(array($this->getPHID())) ->withLastRequestEpochBetween($window_start, null) ->withLastRequestResults( array( HeraldWebhookRequest::RESULT_FAIL, )) ->execute(); if (count($requests) >= $backoff_threshold) { return true; } return false; } public function regenerateHMACKey() { return $this->setHMACKey(Filesystem::readRandomCharacters(32)); } /* -( 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; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HeraldWebhookEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HeraldWebhookTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { while (true) { $requests = id(new HeraldWebhookRequestQuery()) ->setViewer($engine->getViewer()) ->withWebhookPHIDs(array($this->getPHID())) ->setLimit(100) ->execute(); if (!$requests) { break; } foreach ($requests as $request) { $request->delete(); } } $this->delete(); } } diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index 428a35b56c..efcd6b5f15 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -1,247 +1,243 @@ setViewer($actor) ->withClasses(array('PhabricatorLegalpadApplication')) ->executeOne(); $view_policy = $app->getPolicy(LegalpadDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(LegalpadDefaultEditCapability::CAPABILITY); return id(new LegalpadDocument()) ->setVersions(0) ->setCreatorPHID($actor->getPHID()) ->setContributorCount(0) ->setRecentContributorPHIDs(array()) ->attachSignatures(array()) ->setSignatureType(self::SIGNATURE_TYPE_INDIVIDUAL) ->setPreamble('') ->setRequireSignature(0) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } 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', 'requireSignature' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_creator' => array( 'columns' => array('creatorPHID', 'dateModified'), ), 'key_required' => array( 'columns' => array('requireSignature', '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 getURI() { return '/'.$this->getMonogram(); } 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'), self::SIGNATURE_TYPE_NONE => pht('No One'), ); } public function getSignatureTypeName() { $type = $this->getSignatureType(); return idx(self::getSignatureTypeMap(), $type, $type); } public function getSignatureTypeIcon() { $type = $this->getSignatureType(); $map = array( self::SIGNATURE_TYPE_NONE => '', 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); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $policy = $this->viewPolicy; break; case PhabricatorPolicyCapability::CAN_EDIT: $policy = $this->editPolicy; break; default: $policy = PhabricatorPolicies::POLICY_NOONE; break; } return $policy; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return ($user->getPHID() == $this->getCreatorPHID()); } public function describeAutomaticCapability($capability) { return pht('The author of a document can always view and edit it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new LegalpadDocumentEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new LegalpadTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $bodies = id(new LegalpadDocumentBody())->loadAllWhere( 'documentPHID = %s', $this->getPHID()); foreach ($bodies as $body) { $body->delete(); } $signatures = id(new LegalpadDocumentSignature())->loadAllWhere( 'documentPHID = %s', $this->getPHID()); foreach ($signatures as $signature) { $signature->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index 8c0631cf84..656a4c9c57 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -1,153 +1,149 @@ 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 static function initializeNewFileImageMacro(PhabricatorUser $actor) { $macro = id(new self()) ->setAuthorPHID($actor->getPHID()); return $macro; } 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(); } public function getViewURI() { return '/macro/view/'.$this->getID().'/'; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorMacroEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorMacroTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorTokenRecevierInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( 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: $app = PhabricatorApplication::getByClass( 'PhabricatorMacroApplication'); return $app->getPolicy(PhabricatorMacroManageCapability::CAPABILITY); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 7b9b7d2b91..1372e2cb88 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -1,626 +1,622 @@ setViewer($actor) ->withClasses(array('PhabricatorManiphestApplication')) ->executeOne(); $view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY); return id(new ManiphestTask()) ->setStatus(ManiphestTaskStatus::getDefaultStatus()) ->setPriority(ManiphestTaskPriority::getDefaultPriority()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT) ->attachProjectPHIDs(array()) ->attachSubscriberPHIDs(array()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', 'status' => 'text64', 'priority' => 'uint32', 'title' => 'sort', 'description' => 'text', 'mailKey' => 'bytes20', 'ownerOrdering' => 'text64?', 'originalEmailSource' => 'text255?', 'subpriority' => 'double', 'points' => 'double?', 'bridgedObjectPHID' => 'phid?', 'subtype' => 'text64', 'closedEpoch' => 'epoch?', 'closerPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'priority' => array( 'columns' => array('priority', 'status'), ), 'status' => array( 'columns' => array('status'), ), 'ownerPHID' => array( 'columns' => array('ownerPHID', 'status'), ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'ownerOrdering' => array( 'columns' => array('ownerOrdering'), ), 'priority_2' => array( 'columns' => array('priority', 'subpriority'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_dateModified' => array( 'columns' => array('dateModified'), ), 'key_title' => array( 'columns' => array('title(64)'), ), 'key_bridgedobject' => array( 'columns' => array('bridgedObjectPHID'), 'unique' => true, ), 'key_subtype' => array( 'columns' => array('subtype'), ), 'key_closed' => array( 'columns' => array('closedEpoch'), ), 'key_closer' => array( 'columns' => array('closerPHID', 'closedEpoch'), ), ), ) + parent::getConfiguration(); } public function loadDependsOnTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), ManiphestTaskDependsOnTaskEdgeType::EDGECONST); } public function loadDependedOnByTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST); } public function getSubscriberPHIDs() { return $this->assertAttached($this->subscriberPHIDs); } public function getProjectPHIDs() { return $this->assertAttached($this->edgeProjectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->edgeProjectPHIDs = $phids; return $this; } public function attachSubscriberPHIDs(array $phids) { $this->subscriberPHIDs = $phids; return $this; } public function setOwnerPHID($phid) { $this->ownerPHID = nonempty($phid, null); return $this; } public function getMonogram() { return 'T'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } public function attachGroupByProjectPHID($phid) { $this->groupByProjectPHID = $phid; return $this; } public function getGroupByProjectPHID() { return $this->assertAttached($this->groupByProjectPHID); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } $result = parent::save(); return $result; } public function isClosed() { return ManiphestTaskStatus::isClosedStatus($this->getStatus()); } public function isLocked() { return ManiphestTaskStatus::isLockedStatus($this->getStatus()); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function getCoverImageFilePHID() { return idx($this->properties, 'cover.filePHID'); } public function getCoverImageThumbnailPHID() { return idx($this->properties, 'cover.thumbnailPHID'); } public function getWorkboardOrderVectors() { return array( PhabricatorProjectColumn::ORDER_PRIORITY => array( (int)-$this->getPriority(), (double)-$this->getSubpriority(), (int)-$this->getID(), ), ); } public function getPriorityKeyword() { $priority = $this->getPriority(); $keyword = ManiphestTaskPriority::getKeywordForTaskPriority($priority); if ($keyword !== null) { return $keyword; } return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD; } private function comparePriorityTo(ManiphestTask $other) { $upri = $this->getPriority(); $vpri = $other->getPriority(); if ($upri != $vpri) { return ($upri - $vpri); } $usub = $this->getSubpriority(); $vsub = $other->getSubpriority(); if ($usub != $vsub) { return ($usub - $vsub); } $uid = $this->getID(); $vid = $other->getID(); if ($uid != $vid) { return ($uid - $vid); } return 0; } public function isLowerPriorityThan(ManiphestTask $other) { return ($this->comparePriorityTo($other) < 0); } public function isHigherPriorityThan(ManiphestTask $other) { return ($this->comparePriorityTo($other) > 0); } public function getWorkboardProperties() { return array( 'status' => $this->getStatus(), 'points' => (double)$this->getPoints(), ); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getOwnerPHID()); } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newManiphestMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_INTERACT, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_INTERACT: if ($this->isLocked()) { return PhabricatorPolicies::POLICY_NOONE; } else { return $this->getViewPolicy(); } case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // The owner of a task can always view and edit it. $owner_phid = $this->getOwnerPHID(); if ($owner_phid) { $user_phid = $user->getPHID(); if ($user_phid == $owner_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of a task can always view and edit it.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { // Sort of ambiguous who this was intended for; just let them both know. return array_filter( array_unique( array( $this->getAuthorPHID(), $this->getOwnerPHID(), ))); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('maniphest.fields'); } public function getCustomFieldBaseClass() { return 'ManiphestCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ManiphestTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new ManiphestTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The title of the task.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('remarkup') ->setDescription(pht('The task description.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Original task author.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('ownerPHID') ->setType('phid?') ->setDescription(pht('Current task owner, if task is assigned.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Information about task status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('priority') ->setType('map') ->setDescription(pht('Information about task priority.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('points') ->setType('points') ->setDescription(pht('Point value of the task.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('subtype') ->setType('string') ->setDescription(pht('Subtype of the task.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('closerPHID') ->setType('phid?') ->setDescription( pht('User who closed the task, if the task is closed.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('dateClosed') ->setType('int?') ->setDescription( pht('Epoch timestamp when the task was closed.')), ); } public function getFieldValuesForConduit() { $status_value = $this->getStatus(); $status_info = array( 'value' => $status_value, 'name' => ManiphestTaskStatus::getTaskStatusName($status_value), 'color' => ManiphestTaskStatus::getStatusColor($status_value), ); $priority_value = (int)$this->getPriority(); $priority_info = array( 'value' => $priority_value, 'subpriority' => (double)$this->getSubpriority(), 'name' => ManiphestTaskPriority::getTaskPriorityName($priority_value), 'color' => ManiphestTaskPriority::getTaskPriorityColor($priority_value), ); $closed_epoch = $this->getClosedEpoch(); if ($closed_epoch !== null) { $closed_epoch = (int)$closed_epoch; } return array( 'name' => $this->getTitle(), 'description' => array( 'raw' => $this->getDescription(), ), 'authorPHID' => $this->getAuthorPHID(), 'ownerPHID' => $this->getOwnerPHID(), 'status' => $status_info, 'priority' => $priority_info, 'points' => $this->getPoints(), 'subtype' => $this->getSubtype(), 'closerPHID' => $this->getCloserPHID(), 'dateClosed' => $closed_epoch, ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorBoardColumnsSearchEngineAttachment()) ->setAttachmentKey('columns'), ); } public function newSubtypeObject() { $subtype_key = $this->getEditEngineSubtype(); $subtype_map = $this->newEditEngineSubtypeMap(); return $subtype_map->getSubtype($subtype_key); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new ManiphestTaskFulltextEngine(); } /* -( DoorkeeperBridgedObjectInterface )----------------------------------- */ public function getBridgedObject() { return $this->assertAttached($this->bridgedObject); } public function attachBridgedObject( DoorkeeperExternalObject $object = null) { $this->bridgedObject = $object; return $this; } /* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */ public function getEditEngineSubtype() { return $this->getSubtype(); } public function setEditEngineSubtype($value) { return $this->setSubtype($value); } public function newEditEngineSubtypeMap() { $config = PhabricatorEnv::getEnvConfig('maniphest.subtypes'); return PhabricatorEditEngineSubtype::newSubtypeMap($config); } /* -( PhabricatorEditEngineLockableInterface )----------------------------- */ public function newEditEngineLock() { return new ManiphestTaskEditEngineLock(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new ManiphestTaskFerretEngine(); } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php index 623fd88e91..39eb4001cd 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php @@ -1,151 +1,147 @@ true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'address' => 'sort128', ), self::CONFIG_KEY_SCHEMA => array( 'key_address' => array( 'columns' => array('address'), 'unique' => true, ), 'key_application' => array( 'columns' => array('applicationPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorMetaMTAApplicationEmailPHIDType::TYPECONST); } public static function initializeNewAppEmail(PhabricatorUser $actor) { return id(new PhabricatorMetaMTAApplicationEmail()) ->setSpacePHID($actor->getDefaultSpacePHID()) ->setConfigData(array()); } public function attachApplication(PhabricatorApplication $app) { $this->application = $app; return $this; } public function getApplication() { return self::assertAttached($this->application); } public function setConfigValue($key, $value) { $this->configData[$key] = $value; return $this; } public function getConfigValue($key, $default = null) { return idx($this->configData, $key, $default); } public function getInUseMessage() { $applications = PhabricatorApplication::getAllApplications(); $applications = mpull($applications, null, 'getPHID'); $application = idx( $applications, $this->getApplicationPHID()); if ($application) { $message = pht( 'The address %s is configured to be used by the %s Application.', $this->getAddress(), $application->getName()); } else { $message = pht( 'The address %s is configured to be used by an application.', $this->getAddress()); } return $message; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getApplication()->getPolicy($capability); } public function hasAutomaticCapability( $capability, PhabricatorUser $viewer) { return $this->getApplication()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return $this->getApplication()->describeAutomaticCapability($capability); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorMetaMTAApplicationEmailEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorMetaMTAApplicationEmailTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } } diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index 4f9ab750c1..f182e17580 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -1,204 +1,200 @@ setItemType($item_type) ->setStatus(self::STATUS_OPEN); } 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?', 'requestorPHID' => 'phid?', 'queuePHID' => 'phid?', 'itemType' => 'text64', 'itemKey' => 'text64?', 'itemContainerKey' => 'text64?', 'status' => 'text32', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_source' => array( 'columns' => array('sourcePHID', 'status'), ), 'key_owner' => array( 'columns' => array('ownerPHID', 'status'), ), 'key_requestor' => array( 'columns' => array('requestorPHID', 'status'), ), 'key_queue' => array( 'columns' => array('queuePHID', 'status'), ), 'key_container' => array( 'columns' => array('sourcePHID', 'itemContainerKey'), ), 'key_item' => array( 'columns' => array('sourcePHID', 'itemKey'), 'unique' => true, ), ), ) + 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 getSource() { return $this->assertAttached($this->source); } public function attachSource(NuanceSource $source) { $this->source = $source; } public function getItemProperty($key, $default = null) { return idx($this->data, $key, $default); } public function setItemProperty($key, $value) { $this->data[$key] = $value; return $this; } 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 getDisplayName() { return $this->getImplementation()->getItemDisplayName($this); } public function scheduleUpdate() { PhabricatorWorker::scheduleTask( 'NuanceItemUpdateWorker', array( 'itemPHID' => $this->getPHID(), ), array( 'objectPHID' => $this->getPHID(), )); } public function issueCommand( $author_phid, $command, array $parameters = array()) { $command = id(NuanceItemCommand::initializeNewCommand()) ->setItemPHID($this->getPHID()) ->setAuthorPHID($author_phid) ->setCommand($command) ->setParameters($parameters) ->save(); $this->scheduleUpdate(); return $this; } public function getImplementation() { return $this->assertAttached($this->implementation); } public function attachImplementation(NuanceItemType $type) { $this->implementation = $type; return $this; } public function getQueue() { return $this->assertAttached($this->queue); } public function attachQueue(NuanceQueue $queue = null) { $this->queue = $queue; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new NuanceItemEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new NuanceItemTransaction(); } } diff --git a/src/applications/nuance/storage/NuanceQueue.php b/src/applications/nuance/storage/NuanceQueue.php index 7cbb1e761c..a19f1693b3 100644 --- a/src/applications/nuance/storage/NuanceQueue.php +++ b/src/applications/nuance/storage/NuanceQueue.php @@ -1,90 +1,86 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255?', 'mailKey' => 'bytes20', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( NuanceQueuePHIDType::TYPECONST); } public static function initializeNewQueue() { return id(new self()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_USER); } 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 getWorkURI() { return '/nuance/queue/work/'.$this->getID().'/'; } /* -( 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; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new NuanceQueueEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new NuanceQueueTransaction(); } } diff --git a/src/applications/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php index 0f0dd4d7de..5e06a5dc0a 100644 --- a/src/applications/nuance/storage/NuanceSource.php +++ b/src/applications/nuance/storage/NuanceSource.php @@ -1,145 +1,141 @@ true, self::CONFIG_SERIALIZATION => array( 'data' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'type' => 'text32', 'mailKey' => 'bytes20', 'isDisabled' => 'bool', ), 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, NuanceSourceDefinition $definition) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorNuanceApplication')) ->executeOne(); $view_policy = $app->getPolicy( NuanceSourceDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( NuanceSourceDefaultEditCapability::CAPABILITY); return id(new NuanceSource()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setType($definition->getSourceTypeConstant()) ->attachDefinition($definition) ->setIsDisabled(0); } public function getDefinition() { return $this->assertAttached($this->definition); } public function attachDefinition(NuanceSourceDefinition $definition) { $this->definition = $definition; return $this; } public function getSourceProperty($key, $default = null) { return idx($this->data, $key, $default); } public function setSourceProperty($key, $value) { $this->data[$key] = $value; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new NuanceSourceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new NuanceSourceTransaction(); } /* -( 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; } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new NuanceSourceNameNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php index 1ebecbc369..a951bf5781 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php @@ -1,133 +1,129 @@ getID(); return "/oauthserver/edit/{$id}/"; } public function getViewURI() { $id = $this->getID(); return "/oauthserver/client/view/{$id}/"; } public static function initializeNewClient(PhabricatorUser $actor) { return id(new PhabricatorOAuthServerClient()) ->setCreatorPHID($actor->getPHID()) ->setSecret(Filesystem::readRandomCharacters(32)) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy($actor->getPHID()) ->setIsDisabled(0) ->setIsTrusted(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'secret' => 'text32', 'redirectURI' => 'text255', 'isTrusted' => 'bool', 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( '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 $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorOAuthServerEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorOAuthServerTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $authorizations = id(new PhabricatorOAuthClientAuthorization()) ->loadAllWhere('clientPHID = %s', $this->getPHID()); foreach ($authorizations as $authorization) { $authorization->delete(); } $tokens = id(new PhabricatorOAuthServerAccessToken()) ->loadAllWhere('clientPHID = %s', $this->getPHID()); foreach ($tokens as $token) { $token->delete(); } $codes = id(new PhabricatorOAuthServerAuthorizationCode()) ->loadAllWhere('clientPHID = %s', $this->getPHID()); foreach ($codes as $code) { $code->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index 17954ea80a..564fc8a28b 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -1,809 +1,805 @@ setViewer($actor) ->withClasses(array('PhabricatorOwnersApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorOwnersDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( PhabricatorOwnersDefaultEditCapability::CAPABILITY); return id(new PhabricatorOwnersPackage()) ->setAuditingEnabled(0) ->setAutoReview(self::AUTOREVIEW_NONE) ->setDominion(self::DOMINION_STRONG) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->attachPaths(array()) ->setStatus(self::STATUS_ACTIVE) ->attachOwners(array()) ->setDescription(''); } public static function getStatusNameMap() { return array( self::STATUS_ACTIVE => pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } public static function getAutoreviewOptionsMap() { return array( self::AUTOREVIEW_NONE => array( 'name' => pht('No Autoreview'), ), self::AUTOREVIEW_REVIEW => array( 'name' => pht('Review Changes With Non-Owner Author'), 'authority' => true, ), self::AUTOREVIEW_BLOCK => array( 'name' => pht('Review Changes With Non-Owner Author (Blocking)'), 'authority' => true, ), self::AUTOREVIEW_SUBSCRIBE => array( 'name' => pht('Subscribe to Changes With Non-Owner Author'), 'authority' => true, ), self::AUTOREVIEW_REVIEW_ALWAYS => array( 'name' => pht('Review All Changes'), ), self::AUTOREVIEW_BLOCK_ALWAYS => array( 'name' => pht('Review All Changes (Blocking)'), ), self::AUTOREVIEW_SUBSCRIBE_ALWAYS => array( 'name' => pht('Subscribe to All Changes'), ), ); } public static function getDominionOptionsMap() { return array( self::DOMINION_STRONG => array( 'name' => pht('Strong (Control All Paths)'), 'short' => pht('Strong'), ), self::DOMINION_WEAK => array( 'name' => pht('Weak (Control Unowned Paths)'), 'short' => pht('Weak'), ), ); } protected function getConfiguration() { return array( // This information is better available from the history table. self::CONFIG_TIMESTAMPS => false, self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort', 'description' => 'text', 'auditingEnabled' => 'bool', 'status' => 'text32', 'autoReview' => 'text32', 'dominion' => 'text32', ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhabricatorOwnersPackagePHIDType::TYPECONST; } public function isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } public function getMustMatchUngeneratedPaths() { $ignore_attributes = $this->getIgnoredPathAttributes(); return !empty($ignore_attributes['generated']); } public function getPackageProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setPackageProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getIgnoredPathAttributes() { return $this->getPackageProperty(self::PROPERTY_IGNORED, array()); } public function setIgnoredPathAttributes(array $attributes) { return $this->setPackageProperty(self::PROPERTY_IGNORED, $attributes); } 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 loadAffectedPackagesForChangesets( PhabricatorRepository $repository, DifferentialDiff $diff, array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $paths_all = array(); $paths_ungenerated = array(); foreach ($changesets as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $diff); $paths_all[] = $path; if (!$changeset->isGeneratedChangeset()) { $paths_ungenerated[] = $path; } } if (!$paths_all) { return array(); } $packages_all = self::loadAffectedPackages( $repository, $paths_all); // If there are no generated changesets, we can't possibly need to throw // away any packages for matching only generated paths. Just return the // full set of packages. if ($paths_ungenerated === $paths_all) { return $packages_all; } $must_match_ungenerated = array(); foreach ($packages_all as $package) { if ($package->getMustMatchUngeneratedPaths()) { $must_match_ungenerated[] = $package; } } // If no affected packages have the "Ignore Generated Paths" flag set, we // can't possibly need to throw any away. if (!$must_match_ungenerated) { return $packages_all; } if ($paths_ungenerated) { $packages_ungenerated = self::loadAffectedPackages( $repository, $paths_ungenerated); } else { $packages_ungenerated = array(); } // We have some generated paths, and some packages that ignore generated // paths. Take all the packages which: // // - ignore generated paths; and // - didn't match any ungenerated paths // // ...and remove them from the list. $must_match_ungenerated = mpull($must_match_ungenerated, null, 'getID'); $packages_ungenerated = mpull($packages_ungenerated, null, 'getID'); $packages_all = mpull($packages_all, null, 'getID'); foreach ($must_match_ungenerated as $package_id => $package) { if (!isset($packages_ungenerated[$package_id])) { unset($packages_all[$package_id]); } } return $packages_all; } 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), 1024) as $chunk) { $indexes = array(); foreach ($chunk as $fragment) { $indexes[] = PhabricatorHash::digestForIndex($fragment); } $rows[] = queryfx_all( $conn, 'SELECT pkg.id, pkg.dominion, p.excluded, p.path FROM %T pkg JOIN %T p ON p.packageID = pkg.id WHERE p.pathIndex IN (%Ls) AND pkg.status IN (%Ls) %Q', $package->getTableName(), $path->getTableName(), $indexes, array( self::STATUS_ACTIVE, ), $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) { // Build a map from each path to all the package paths which match it. $path_hits = array(); $weak = array(); foreach ($rows as $row) { $id = $row['id']; $path = $row['path']; $length = strlen($path); $excluded = $row['excluded']; if ($row['dominion'] === self::DOMINION_WEAK) { $weak[$id] = true; } $matches = $paths[$path]; foreach ($matches as $match => $ignored) { $path_hits[$match][] = array( 'id' => $id, 'excluded' => $excluded, 'length' => $length, ); } } // For each path, process the matching package paths to figure out which // packages actually own it. $path_packages = array(); foreach ($path_hits as $match => $hits) { $hits = isort($hits, 'length'); $packages = array(); foreach ($hits as $hit) { $package_id = $hit['id']; if ($hit['excluded']) { unset($packages[$package_id]); } else { $packages[$package_id] = $hit; } } $path_packages[$match] = $packages; } // Remove packages with weak dominion rules that should cede control to // a more specific package. if ($weak) { foreach ($path_packages as $match => $packages) { // Group packages by length. $length_map = array(); foreach ($packages as $package_id => $package) { $length_map[$package['length']][$package_id] = $package; } // For each path length, remove all weak packages if there are any // strong packages of the same length. This makes sure that if there // are one or more strong claims on a particular path, only those // claims stand. foreach ($length_map as $package_list) { $any_strong = false; foreach ($package_list as $package_id => $package) { if (!isset($weak[$package_id])) { $any_strong = true; break; } } if ($any_strong) { foreach ($package_list as $package_id => $package) { if (isset($weak[$package_id])) { unset($packages[$package_id]); } } } } $packages = isort($packages, 'length'); $packages = array_reverse($packages, true); $best_length = null; foreach ($packages as $package_id => $package) { // If this is the first package we've encountered, note its length. // We're iterating over the packages from longest to shortest match, // so packages of this length always have the best claim on the path. if ($best_length === null) { $best_length = $package['length']; } // If this package has the same length as the best length, its claim // stands. if ($package['length'] === $best_length) { continue; } // If this is a weak package and does not have the best length, // cede its claim to the stronger package. if (isset($weak[$package_id])) { unset($packages[$package_id]); } } $path_packages[$match] = $packages; } } // For each package that owns at least one path, identify the longest // path it owns. $package_lengths = array(); foreach ($path_packages as $match => $hits) { foreach ($hits as $hit) { $length = $hit['length']; $id = $hit['id']; if (empty($package_lengths[$id])) { $package_lengths[$id] = $length; } else { $package_lengths[$id] = max($package_lengths[$id], $length); } } } return $package_lengths; } public static function splitPath($path) { $result = array( '/', ); $parts = explode('/', $path); $buffer = '/'; foreach ($parts as $part) { if (!strlen($part)) { continue; } $buffer = $buffer.$part.'/'; $result[] = $buffer; } return $result; } public function attachPaths(array $paths) { assert_instances_of($paths, 'PhabricatorOwnersPath'); $this->paths = $paths; // Drop this cache if we're attaching new paths. $this->pathRepositoryMap = array(); return $this; } public function getPaths() { return $this->assertAttached($this->paths); } public function getPathsForRepository($repository_phid) { if (isset($this->pathRepositoryMap[$repository_phid])) { return $this->pathRepositoryMap[$repository_phid]; } $map = array(); foreach ($this->getPaths() as $path) { if ($path->getRepositoryPHID() == $repository_phid) { $map[] = $path; } } $this->pathRepositoryMap[$repository_phid] = $map; return $this->pathRepositoryMap[$repository_phid]; } public function attachOwners(array $owners) { assert_instances_of($owners, 'PhabricatorOwnersOwner'); $this->owners = $owners; return $this; } public function getOwners() { return $this->assertAttached($this->owners); } public function getOwnerPHIDs() { return mpull($this->getOwners(), 'getUserPHID'); } public function isOwnerPHID($phid) { if (!$phid) { return false; } $owner_phids = $this->getOwnerPHIDs(); $owner_phids = array_fuse($owner_phids); return isset($owner_phids[$phid]); } public function getMonogram() { return 'O'.$this->getID(); } public function getURI() { // TODO: Move these to "/O123" for consistency. return '/owners/package/'.$this->getID().'/'; } /* -( 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) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isOwnerPHID($viewer->getPHID())) { return true; } break; } return false; } public function describeAutomaticCapability($capability) { return pht('Owners of a package may always view it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorOwnersPackageTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorOwnersPackageTransaction(); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('owners.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorOwnersCustomField'; } 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(); $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE packageID = %d', id(new PhabricatorOwnersPath())->getTableName(), $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE packageID = %d', id(new PhabricatorOwnersOwner())->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the package.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('string') ->setDescription(pht('The package description.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Active or archived status of the package.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('owners') ->setType('list>') ->setDescription(pht('List of package owners.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('review') ->setType('map') ->setDescription(pht('Auto review information.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('audit') ->setType('map') ->setDescription(pht('Auto audit information.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('dominion') ->setType('map') ->setDescription(pht('Dominion setting information.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('ignored') ->setType('map') ->setDescription(pht('Ignored attribute information.')), ); } public function getFieldValuesForConduit() { $owner_list = array(); foreach ($this->getOwners() as $owner) { $owner_list[] = array( 'ownerPHID' => $owner->getUserPHID(), ); } $review_map = self::getAutoreviewOptionsMap(); $review_value = $this->getAutoReview(); if (isset($review_map[$review_value])) { $review_label = $review_map[$review_value]['name']; } else { $review_label = pht('Unknown ("%s")', $review_value); } $review = array( 'value' => $review_value, 'label' => $review_label, ); if ($this->getAuditingEnabled()) { $audit_value = 'audit'; $audit_label = pht('Auditing Enabled'); } else { $audit_value = 'none'; $audit_label = pht('No Auditing'); } $audit = array( 'value' => $audit_value, 'label' => $audit_label, ); $dominion_value = $this->getDominion(); $dominion_map = self::getDominionOptionsMap(); if (isset($dominion_map[$dominion_value])) { $dominion_label = $dominion_map[$dominion_value]['name']; $dominion_short = $dominion_map[$dominion_value]['short']; } else { $dominion_label = pht('Unknown ("%s")', $dominion_value); $dominion_short = pht('Unknown ("%s")', $dominion_value); } $dominion = array( 'value' => $dominion_value, 'label' => $dominion_label, 'short' => $dominion_short, ); // Force this to always emit as a JSON object even if empty, never as // a JSON list. $ignored = $this->getIgnoredPathAttributes(); if (!$ignored) { $ignored = (object)array(); } return array( 'name' => $this->getName(), 'description' => $this->getDescription(), 'status' => $this->getStatus(), 'owners' => $owner_list, 'review' => $review, 'audit' => $audit, 'dominion' => $dominion, 'ignored' => $ignored, ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorOwnersPathsSearchEngineAttachment()) ->setAttachmentKey('paths'), ); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorOwnersPackageFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhabricatorOwnersPackageFerretEngine(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new PhabricatorOwnersPackageNameNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/packages/storage/PhabricatorPackagesPackage.php b/src/applications/packages/storage/PhabricatorPackagesPackage.php index 822988e8fb..15748329e7 100644 --- a/src/applications/packages/storage/PhabricatorPackagesPackage.php +++ b/src/applications/packages/storage/PhabricatorPackagesPackage.php @@ -1,251 +1,247 @@ setViewer($actor) ->withClasses(array('PhabricatorPackagesApplication')) ->executeOne(); $view_policy = $packages_application->getPolicy( PhabricatorPackagesPackageDefaultViewCapability::CAPABILITY); $edit_policy = $packages_application->getPolicy( PhabricatorPackagesPackageDefaultEditCapability::CAPABILITY); return id(new self()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort64', 'packageKey' => 'sort64', ), self::CONFIG_KEY_SCHEMA => array( 'key_package' => array( 'columns' => array('publisherPHID', 'packageKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPackagesPackagePHIDType::TYPECONST); } public function getURI() { $full_key = $this->getFullKey(); return "/package/{$full_key}/"; } public function getFullKey() { $publisher = $this->getPublisher(); $publisher_key = $publisher->getPublisherKey(); $package_key = $this->getPackageKey(); return "{$publisher_key}/{$package_key}"; } public function attachPublisher(PhabricatorPackagesPublisher $publisher) { $this->publisher = $publisher; return $this; } public function getPublisher() { return $this->assertAttached($this->publisher); } public static function assertValidPackageName($value) { $length = phutil_utf8_strlen($value); if (!$length) { throw new Exception( pht( 'Package name "%s" is not valid: package names are required.', $value)); } $max_length = 64; if ($length > $max_length) { throw new Exception( pht( 'Package name "%s" is not valid: package names must not be '. 'more than %s characters long.', $value, new PhutilNumber($max_length))); } } public static function assertValidPackageKey($value) { $length = phutil_utf8_strlen($value); if (!$length) { throw new Exception( pht( 'Package key "%s" is not valid: package keys are required.', $value)); } $max_length = 64; if ($length > $max_length) { throw new Exception( pht( 'Package key "%s" is not valid: package keys must not be '. 'more than %s characters long.', $value, new PhutilNumber($max_length))); } if (!preg_match('/^[a-z]+\z/', $value)) { throw new Exception( pht( 'Package key "%s" is not valid: package keys may only contain '. 'lowercase latin letters.', $value)); } } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( 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) { return false; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $versions = id(new PhabricatorPackagesVersionQuery()) ->setViewer($viewer) ->withPackagePHIDs(array($this->getPHID())) ->execute(); foreach ($versions as $version) { $engine->destroyObject($version); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorPackagesPackageEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPackagesPackageTransaction(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new PhabricatorPackagesPackageNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the package.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('packageKey') ->setType('string') ->setDescription(pht('The unique key of the package.')), ); } public function getFieldValuesForConduit() { $publisher = $this->getPublisher(); $publisher_map = array( 'id' => (int)$publisher->getID(), 'phid' => $publisher->getPHID(), 'name' => $publisher->getName(), 'publisherKey' => $publisher->getPublisherKey(), ); return array( 'name' => $this->getName(), 'packageKey' => $this->getPackageKey(), 'fullKey' => $this->getFullKey(), 'publisher' => $publisher_map, ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/packages/storage/PhabricatorPackagesPublisher.php b/src/applications/packages/storage/PhabricatorPackagesPublisher.php index b7610499d4..21b586dc1f 100644 --- a/src/applications/packages/storage/PhabricatorPackagesPublisher.php +++ b/src/applications/packages/storage/PhabricatorPackagesPublisher.php @@ -1,216 +1,212 @@ setViewer($actor) ->withClasses(array('PhabricatorPackagesApplication')) ->executeOne(); $edit_policy = $packages_application->getPolicy( PhabricatorPackagesPublisherDefaultEditCapability::CAPABILITY); return id(new self()) ->setEditPolicy($edit_policy); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort64', 'publisherKey' => 'sort64', ), self::CONFIG_KEY_SCHEMA => array( 'key_publisher' => array( 'columns' => array('publisherKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPackagesPublisherPHIDType::TYPECONST); } public function getURI() { $publisher_key = $this->getPublisherKey(); return "/package/{$publisher_key}/"; } public static function assertValidPublisherName($value) { $length = phutil_utf8_strlen($value); if (!$length) { throw new Exception( pht( 'Publisher name "%s" is not valid: publisher names are required.', $value)); } $max_length = 64; if ($length > $max_length) { throw new Exception( pht( 'Publisher name "%s" is not valid: publisher names must not be '. 'more than %s characters long.', $value, new PhutilNumber($max_length))); } } public static function assertValidPublisherKey($value) { $length = phutil_utf8_strlen($value); if (!$length) { throw new Exception( pht( 'Publisher key "%s" is not valid: publisher keys are required.', $value)); } $max_length = 64; if ($length > $max_length) { throw new Exception( pht( 'Publisher key "%s" is not valid: publisher keys must not be '. 'more than %s characters long.', $value, new PhutilNumber($max_length))); } if (!preg_match('/^[a-z]+\z/', $value)) { throw new Exception( pht( 'Publisher key "%s" is not valid: publisher keys may only contain '. 'lowercase latin letters.', $value)); } } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( Policy Interface )--------------------------------------------------- */ 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->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $packages = id(new PhabricatorPackagesPackageQuery()) ->setViewer($viewer) ->withPublisherPHIDs(array($this->getPHID())) ->execute(); foreach ($packages as $package) { $engine->destroyObject($package); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorPackagesPublisherEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPackagesPublisherTransaction(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new PhabricatorPackagesPublisherNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the publisher.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('publisherKey') ->setType('string') ->setDescription(pht('The unique key of the publisher.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'publisherKey' => $this->getPublisherKey(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/packages/storage/PhabricatorPackagesVersion.php b/src/applications/packages/storage/PhabricatorPackagesVersion.php index e1abf043a1..cd5e2648f8 100644 --- a/src/applications/packages/storage/PhabricatorPackagesVersion.php +++ b/src/applications/packages/storage/PhabricatorPackagesVersion.php @@ -1,202 +1,198 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort64', ), self::CONFIG_KEY_SCHEMA => array( 'key_package' => array( 'columns' => array('packagePHID', 'name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPackagesVersionPHIDType::TYPECONST); } public function getURI() { $package = $this->getPackage(); $full_key = $package->getFullKey(); $name = $this->getName(); return "/package/{$full_key}/{$name}/"; } public function attachPackage(PhabricatorPackagesPackage $package) { $this->package = $package; return $this; } public function getPackage() { return $this->assertAttached($this->package); } public static function assertValidVersionName($value) { $length = phutil_utf8_strlen($value); if (!$length) { throw new Exception( pht( 'Version name "%s" is not valid: version names are required.', $value)); } $max_length = 64; if ($length > $max_length) { throw new Exception( pht( 'Version name "%s" is not valid: version names must not be '. 'more than %s characters long.', $value, new PhutilNumber($max_length))); } if (!preg_match('/^[A-Za-z0-9.-]+\z/', $value)) { throw new Exception( pht( 'Version name "%s" is not valid: version names may only contain '. 'latin letters, digits, periods, and hyphens.', $value)); } if (preg_match('/^[.-]|[.-]$/', $value)) { throw new Exception( pht( 'Version name "%s" is not valid: version names may not start or '. 'end with a period or hyphen.', $value)); } } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( Policy Interface )--------------------------------------------------- */ 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_USER; } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { return array( array( $this->getPackage(), $capability, ), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorPackagesVersionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPackagesVersionTransaction(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new PhabricatorPackagesVersionNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the version.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php index 6cb7cbf24e..b10d392d36 100644 --- a/src/applications/passphrase/storage/PassphraseCredential.php +++ b/src/applications/passphrase/storage/PassphraseCredential.php @@ -1,202 +1,198 @@ setViewer($actor) ->withClasses(array('PhabricatorPassphraseApplication')) ->executeOne(); $view_policy = $app->getPolicy(PassphraseDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PassphraseDefaultEditCapability::CAPABILITY); return id(new PassphraseCredential()) ->setName('') ->setUsername('') ->setDescription('') ->setIsDestroyed(0) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } public function getMonogram() { return 'K'.$this->getID(); } 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); } public function attachImplementation(PassphraseCredentialType $impl) { $this->implementation = $impl; return $this; } public function getImplementation() { return $this->assertAttached($this->implementation); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PassphraseCredentialTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PassphraseCredentialTransaction(); } /* -( 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; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( 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(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PassphraseCredentialFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PassphraseCredentialFerretEngine(); } } diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index ad3965ce46..79f1a953f6 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -1,279 +1,275 @@ setViewer($actor) ->withClasses(array('PhabricatorPasteApplication')) ->executeOne(); $view_policy = $app->getPolicy(PasteDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PasteDefaultEditCapability::CAPABILITY); return id(new PhabricatorPaste()) ->setTitle('') ->setStatus(self::STATUS_ACTIVE) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->attachRawContent(null); } public static function getStatusNameMap() { return array( self::STATUS_ACTIVE => pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } public function getURI() { return '/'.$this->getMonogram(); } public function getMonogram() { return 'P'.$this->getID(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', '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 isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } 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; } public function getSnippet() { return $this->assertAttached($this->snippet); } public function attachSnippet(PhabricatorPasteSnippet $snippet) { $this->snippet = $snippet; return $this; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } /* -( 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($engine->getViewer()) ->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(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The title of the paste.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('User PHID of the author.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('language') ->setType('string?') ->setDescription(pht('Language to use for syntax highlighting.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Active or archived status of the paste.')), ); } public function getFieldValuesForConduit() { return array( 'title' => $this->getTitle(), 'authorPHID' => $this->getAuthorPHID(), 'language' => nonempty($this->getLanguage(), null), 'status' => $this->getStatus(), ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorPasteContentSearchEngineAttachment()) ->setAttachmentKey('content'), ); } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 9b6f6cdbe4..8b1c7cba5c 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,1682 +1,1678 @@ isAdmin; case 'isDisabled': return (bool)$this->isDisabled; case 'isSystemAgent': return (bool)$this->isSystemAgent; case 'isMailingList': return (bool)$this->isMailingList; 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->isLoggedIn()) { return false; } 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; } /** * Is this a user who we can reasonably expect to respond to requests? * * This is used to provide a grey "disabled/unresponsive" dot cue when * rendering handles and tags, so it isn't a surprise if you get ignored * when you ask things of users who will not receive notifications or could * not respond to them (because they are disabled, unapproved, do not have * verified email addresses, etc). * * @return bool True if this user can receive and respond to requests from * other humans. */ public function isResponsive() { if (!$this->isUserActivated()) { return false; } if (!$this->getIsEmailVerified()) { return false; } return true; } public function canEstablishWebSessions() { if ($this->getIsMailingList()) { return false; } if ($this->getIsSystemAgent()) { return false; } return true; } public function canEstablishAPISessions() { if ($this->getIsDisabled()) { return false; } // Intracluster requests are permitted even if the user is logged out: // in particular, public users are allowed to issue intracluster requests // when browsing Diffusion. if (PhabricatorEnv::isClusterRemoteAddress()) { if (!$this->isLoggedIn()) { return true; } } if (!$this->isUserActivated()) { return false; } if ($this->getIsMailingList()) { return false; } return true; } public function canEstablishSSHSessions() { if (!$this->isUserActivated()) { return false; } if ($this->getIsMailingList()) { 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); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'userName' => 'sort64', 'realName' => 'text128', 'profileImagePHID' => 'phid?', 'conduitCertificate' => 'text255', 'isSystemAgent' => 'bool', 'isMailingList' => 'bool', 'isDisabled' => 'bool', 'isAdmin' => 'bool', 'isEmailVerified' => 'uint32', 'isApproved' => 'uint32', 'accountSecret' => 'bytes64', 'isEnrolledInMultiFactor' => 'bool', 'availabilityCache' => 'text255?', 'availabilityCacheTTL' => 'uint32?', 'defaultProfileImagePHID' => 'phid?', 'defaultProfileImageVersion' => 'text64?', ), 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'), ), ), self::CONFIG_NO_MUTATE => array( 'availabilityCache' => true, 'availabilityCacheTTL' => true, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPeopleUserPHIDType::TYPECONST); } public function getMonogram() { return '@'.$this->getUsername(); } public function isLoggedIn() { return !($this->getPHID() === null); } public function saveWithoutIndex() { return parent::save(); } public function save() { if (!$this->getConduitCertificate()) { $this->setConduitCertificate($this->generateConduitCertificate()); } if (!strlen($this->getAccountSecret())) { $this->setAccountSecret(Filesystem::readRandomCharacters(64)); } $result = $this->saveWithoutIndex(); if ($this->profile) { $this->profile->save(); } $this->updateNameTokens(); PhabricatorSearchWorker::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); } public function hasHighSecuritySession() { if (!$this->hasSession()) { return false; } return $this->getSession()->isHighSecuritySession(); } private function generateConduitCertificate() { return Filesystem::readRandomCharacters(255); } 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); } public function getCSRFToken() { if ($this->isOmnipotent()) { // We may end up here when called from the daemons. The omnipotent user // has no meaningful CSRF token, so just return `null`. return null; } if ($this->csrfSalt === null) { $this->csrfSalt = Filesystem::readRandomCharacters( self::CSRF_SALT_LENGTH); } $salt = $this->csrfSalt; // Generate a token hash to mitigate BREACH attacks against SSL. See // discussion in T3684. $token = $this->getRawCSRFToken(); $hash = PhabricatorHash::weakDigest($token, $salt); return self::CSRF_BREACH_PREFIX.$salt.substr( $hash, 0, self::CSRF_TOKEN_LENGTH); } public function validateCSRFToken($token) { // We expect a BREACH-mitigating token. See T3684. $breach_prefix = self::CSRF_BREACH_PREFIX; $breach_prelen = strlen($breach_prefix); if (strncmp($token, $breach_prefix, $breach_prelen) !== 0) { return false; } $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_CYCLE_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); $digest = PhabricatorHash::weakDigest($valid, $salt); $digest = substr($digest, 0, self::CSRF_TOKEN_LENGTH); if (phutil_hashes_are_identical($digest, $token)) { return true; } } 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::weakDigest($vec), 0, $len); } public function getUserProfile() { return $this->assertAttached($this->profile); } 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) { $this->profile = PhabricatorUserProfile::initializeNewProfile($this); } return $this->profile; } public function loadPrimaryEmailAddress() { $email = $this->loadPrimaryEmail(); if (!$email) { throw new Exception(pht('User has no primary email address!')); } return $email->getAddress(); } public function loadPrimaryEmail() { return id(new PhabricatorUserEmail())->loadOneWhere( 'userPHID = %s AND isPrimary = 1', $this->getPHID()); } /* -( Settings )----------------------------------------------------------- */ public function getUserSetting($key) { // NOTE: We store available keys and cached values separately to make it // faster to check for `null` in the cache, which is common. if (isset($this->settingCacheKeys[$key])) { return $this->settingCache[$key]; } $settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES; if ($this->getPHID()) { $settings = $this->requireCacheData($settings_key); } else { $settings = $this->loadGlobalSettings(); } if (array_key_exists($key, $settings)) { $value = $settings[$key]; return $this->writeUserSettingCache($key, $value); } $cache = PhabricatorCaches::getRuntimeCache(); $cache_key = "settings.defaults({$key})"; $cache_map = $cache->getKeys(array($cache_key)); if ($cache_map) { $value = $cache_map[$cache_key]; } else { $defaults = PhabricatorSetting::getAllSettings(); if (isset($defaults[$key])) { $value = id(clone $defaults[$key]) ->setViewer($this) ->getSettingDefaultValue(); } else { $value = null; } $cache->setKey($cache_key, $value); } return $this->writeUserSettingCache($key, $value); } /** * Test if a given setting is set to a particular value. * * @param const Setting key. * @param wild Value to compare. * @return bool True if the setting has the specified value. * @task settings */ public function compareUserSetting($key, $value) { $actual = $this->getUserSetting($key); return ($actual == $value); } private function writeUserSettingCache($key, $value) { $this->settingCacheKeys[$key] = true; $this->settingCache[$key] = $value; return $value; } public function getTranslation() { return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY); } public function getTimezoneIdentifier() { return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY); } public static function getGlobalSettingsCacheKey() { return 'user.settings.globals.v1'; } private function loadGlobalSettings() { $cache_key = self::getGlobalSettingsCacheKey(); $cache = PhabricatorCaches::getMutableStructureCache(); $settings = $cache->getKey($cache_key); if (!$settings) { $preferences = PhabricatorUserPreferences::loadGlobalPreferences($this); $settings = $preferences->getPreferences(); $cache->setKey($cache_key, $settings); } return $settings; } /** * Override the user's timezone identifier. * * This is primarily useful for unit tests. * * @param string New timezone identifier. * @return this * @task settings */ public function overrideTimezoneIdentifier($identifier) { $timezone_key = PhabricatorTimezoneSetting::SETTINGKEY; $this->settingCacheKeys[$timezone_key] = true; $this->settingCache[$timezone_key] = $identifier; return $this; } public function getGender() { return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY); } public function loadEditorLink( $path, $line, PhabricatorRepository $repository = null) { $editor = $this->getUserSetting(PhabricatorEditorSetting::SETTINGKEY); if (is_array($path)) { $multi_key = PhabricatorEditorMultipleSetting::SETTINGKEY; $multiedit = $this->getUserSetting($multi_key); switch ($multiedit) { case PhabricatorEditorMultipleSetting::VALUE_SPACES: $path = implode(' ', $path); break; case PhabricatorEditorMultipleSetting::VALUE_SINGLE: default: return null; } } if (!strlen($editor)) { return null; } if ($repository) { $callsign = $repository->getCallsign(); } else { $callsign = 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 %LQ', $table, $sql); } } public function sendWelcomeEmail(PhabricatorUser $admin) { if (!$this->canEstablishWebSessions()) { throw new Exception( pht( 'Can not send welcome mail to users who can not establish '. 'web sessions!')); } $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 = pht( "Welcome to Phabricator!\n\n". "%s (%s) has created an account for you.\n\n". " Username: %s\n\n". "To login to Phabricator, follow this link and set a password:\n\n". " %s\n\n". "After you have set a password, you can login in the future by ". "going here:\n\n". " %s\n", $admin_username, $admin_realname, $user_username, $uri, $base_uri); if (!$is_serious) { $body .= sprintf( "\n%s\n", pht("Love,\nPhabricator")); } $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($this->getPHID())) ->setForceDelivery(true) ->setSubject(pht('[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 = sprintf( "%s\n\n %s\n\n%s\n", pht( "If you use a password to login, you'll need to reset it ". "before you can login again. You can reset your password by ". "following this link:"), $uri, pht( "And, of course, you'll need to use your new username to login ". "from now on. If you use OAuth to login, nothing should change.")); } $body = sprintf( "%s\n\n %s\n %s\n\n%s", pht( '%s (%s) has changed your Phabricator username.', $admin_username, $admin_realname), pht( 'Old Username: %s', $old_username), pht( 'New Username: %s', $new_username), $password_instructions); $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($this->getPHID())) ->setForceDelivery(true) ->setSubject(pht('[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 getProfileImageURI() { $uri_key = PhabricatorUserProfileImageCacheType::KEY_URI; return $this->requireCacheData($uri_key); } public function getUnreadNotificationCount() { $notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT; return $this->requireCacheData($notification_key); } public function getUnreadMessageCount() { $message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT; return $this->requireCacheData($message_key); } public function getRecentBadgeAwards() { $badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES; return $this->requireCacheData($badges_key); } public function getFullName() { if (strlen($this->getRealName())) { return $this->getUsername().' ('.$this->getRealName().')'; } else { return $this->getUsername(); } } public function getTimeZone() { return new DateTimeZone($this->getTimezoneIdentifier()); } public function getTimeZoneOffset() { $timezone = $this->getTimeZone(); $now = new DateTime('@'.PhabricatorTime::getNow()); $offset = $timezone->getOffset($now); // Javascript offsets are in minutes and have the opposite sign. $offset = -(int)($offset / 60); return $offset; } public function getTimeZoneOffsetInHours() { $offset = $this->getTimeZoneOffset(); $offset = (int)round($offset / 60); $offset = -$offset; return $offset; } public function formatShortDateTime($when, $now = null) { if ($now === null) { $now = PhabricatorTime::getNow(); } try { $when = new DateTime('@'.$when); $now = new DateTime('@'.$now); } catch (Exception $ex) { return null; } $zone = $this->getTimeZone(); $when->setTimeZone($zone); $now->setTimeZone($zone); if ($when->format('Y') !== $now->format('Y')) { // Different year, so show "Feb 31 2075". $format = 'M j Y'; } else if ($when->format('Ymd') !== $now->format('Ymd')) { // Same year but different month and day, so show "Feb 31". $format = 'M j'; } else { // Same year, month and day so show a time of day. $pref_time = PhabricatorTimeFormatSetting::SETTINGKEY; $format = $this->getUserSetting($pref_time); } return $when->format($format); } 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()); } public function getDefaultSpacePHID() { // TODO: We might let the user switch which space they're "in" later on; // for now just use the global space if one exists. // If the viewer has access to the default space, use that. $spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this); foreach ($spaces as $space) { if ($space->getIsDefaultNamespace()) { return $space->getPHID(); } } // Otherwise, use the space with the lowest ID that they have access to. // This just tends to keep the default stable and predictable over time, // so adding a new space won't change behavior for users. if ($spaces) { $spaces = msort($spaces, 'getID'); return head($spaces)->getPHID(); } return null; } /** * Grant a user a source of authority, to let them bypass policy checks they * could not otherwise. */ public function grantAuthority($authority) { $this->authorities[] = $authority; return $this; } /** * Get authorities granted to the user. */ public function getAuthorities() { return $this->authorities; } public function hasConduitClusterToken() { return ($this->conduitClusterToken !== self::ATTACHABLE); } public function attachConduitClusterToken(PhabricatorConduitToken $token) { $this->conduitClusterToken = $token; return $this; } public function getConduitClusterToken() { return $this->assertAttached($this->conduitClusterToken); } /* -( Availability )------------------------------------------------------- */ /** * @task availability */ public function attachAvailability(array $availability) { $this->availability = $availability; return $this; } /** * Get the timestamp the user is away until, if they are currently away. * * @return int|null Epoch timestamp, or `null` if the user is not away. * @task availability */ public function getAwayUntil() { $availability = $this->availability; $this->assertAttached($availability); if (!$availability) { return null; } return idx($availability, 'until'); } public function getDisplayAvailability() { $availability = $this->availability; $this->assertAttached($availability); if (!$availability) { return null; } $busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY; return idx($availability, 'availability', $busy); } public function getAvailabilityEventPHID() { $availability = $this->availability; $this->assertAttached($availability); if (!$availability) { return null; } return idx($availability, 'eventPHID'); } /** * Get cached availability, if present. * * @return wild|null Cache data, or null if no cache is available. * @task availability */ public function getAvailabilityCache() { $now = PhabricatorTime::getNow(); if ($this->availabilityCacheTTL <= $now) { return null; } try { return phutil_json_decode($this->availabilityCache); } catch (Exception $ex) { return null; } } /** * Write to the availability cache. * * @param wild Availability cache data. * @param int|null Cache TTL. * @return this * @task availability */ public function writeAvailabilityCache(array $availability, $ttl) { if (PhabricatorEnv::isReadOnly()) { return $this; } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd WHERE id = %d', $this->getTableName(), phutil_json_encode($availability), $ttl, $this->getID()); unset($unguarded); return $this; } /* -( 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; } /** * Get a scalar string identifying this user. * * This is similar to using the PHID, but distinguishes between omnipotent * and public users explicitly. This allows safe construction of cache keys * or cache buckets which do not conflate public and omnipotent users. * * @return string Scalar identifier. */ public function getCacheFragment() { if ($this->isOmnipotent()) { return 'u.omnipotent'; } $phid = $this->getPHID(); if ($phid) { return 'u.'.$phid; } return 'u.public'; } /* -( Managing Handles )--------------------------------------------------- */ /** * Get a @{class:PhabricatorHandleList} which benefits from this viewer's * internal handle pool. * * @param list List of PHIDs to load. * @return PhabricatorHandleList Handle list object. * @task handle */ public function loadHandles(array $phids) { if ($this->handlePool === null) { $this->handlePool = id(new PhabricatorHandlePool()) ->setViewer($this); } return $this->handlePool->newHandleList($phids); } /** * Get a @{class:PHUIHandleView} for a single handle. * * This benefits from the viewer's internal handle pool. * * @param phid PHID to render a handle for. * @return PHUIHandleView View of the handle. * @task handle */ public function renderHandle($phid) { return $this->loadHandles(array($phid))->renderHandle($phid); } /** * Get a @{class:PHUIHandleListView} for a list of handles. * * This benefits from the viewer's internal handle pool. * * @param list List of PHIDs to render. * @return PHUIHandleListView View of the handles. * @task handle */ public function renderHandleList(array $phids) { return $this->loadHandles($phids)->renderList(); } public function attachBadgePHIDs(array $phids) { $this->badgePHIDs = $phids; return $this; } public function getBadgePHIDs() { return $this->assertAttached($this->badgePHIDs); } /* -( 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() || $this->getIsMailingList()) { 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 PhabricatorUserPreferencesQuery()) ->setViewer($engine->getViewer()) ->withUsers(array($this)) ->execute(); foreach ($prefs as $pref) { $engine->destroyObject($pref); } $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($profiles as $profile) { $profile->delete(); } $keys = id(new PhabricatorAuthSSHKeyQuery()) ->setViewer($engine->getViewer()) ->withObjectPHIDs(array($this->getPHID())) ->execute(); foreach ($keys as $key) { $engine->destroyObject($key); } $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/user/'.$this->getUsername().'/page/ssh/'; } } public function getSSHKeyDefaultName() { return 'id_rsa_phabricator'; } public function getSSHKeyNotifyPHIDs() { return array( $this->getPHID(), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorUserTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorUserTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorUserFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhabricatorUserFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('username') ->setType('string') ->setDescription(pht("The user's username.")), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('realName') ->setType('string') ->setDescription(pht("The user's real name.")), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('roles') ->setType('list') ->setDescription(pht('List of account roles.')), ); } public function getFieldValuesForConduit() { $roles = array(); if ($this->getIsDisabled()) { $roles[] = 'disabled'; } if ($this->getIsSystemAgent()) { $roles[] = 'bot'; } if ($this->getIsMailingList()) { $roles[] = 'list'; } if ($this->getIsAdmin()) { $roles[] = 'admin'; } if ($this->getIsEmailVerified()) { $roles[] = 'verified'; } if ($this->getIsApproved()) { $roles[] = 'approved'; } if ($this->isUserActivated()) { $roles[] = 'activated'; } return array( 'username' => $this->getUsername(), 'realName' => $this->getRealName(), 'roles' => $roles, ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorPeopleAvailabilitySearchEngineAttachment()) ->setAttachmentKey('availability'), ); } /* -( User Cache )--------------------------------------------------------- */ /** * @task cache */ public function attachRawCacheData(array $data) { $this->rawCacheData = $data + $this->rawCacheData; return $this; } public function setAllowInlineCacheGeneration($allow_cache_generation) { $this->allowInlineCacheGeneration = $allow_cache_generation; return $this; } /** * @task cache */ protected function requireCacheData($key) { if (isset($this->usableCacheData[$key])) { return $this->usableCacheData[$key]; } $type = PhabricatorUserCacheType::requireCacheTypeForKey($key); if (isset($this->rawCacheData[$key])) { $raw_value = $this->rawCacheData[$key]; $usable_value = $type->getValueFromStorage($raw_value); $this->usableCacheData[$key] = $usable_value; return $usable_value; } // By default, we throw if a cache isn't available. This is consistent // with the standard `needX()` + `attachX()` + `getX()` interaction. if (!$this->allowInlineCacheGeneration) { throw new PhabricatorDataNotAttachedException($this); } $user_phid = $this->getPHID(); // Try to read the actual cache before we generate a new value. We can // end up here via Conduit, which does not use normal sessions and can // not pick up a free cache load during session identification. if ($user_phid) { $raw_data = PhabricatorUserCache::readCaches( $type, $key, array($user_phid)); if (array_key_exists($user_phid, $raw_data)) { $raw_value = $raw_data[$user_phid]; $usable_value = $type->getValueFromStorage($raw_value); $this->rawCacheData[$key] = $raw_value; $this->usableCacheData[$key] = $usable_value; return $usable_value; } } $usable_value = $type->getDefaultValue(); if ($user_phid) { $map = $type->newValueForUsers($key, array($this)); if (array_key_exists($user_phid, $map)) { $raw_value = $map[$user_phid]; $usable_value = $type->getValueFromStorage($raw_value); $this->rawCacheData[$key] = $raw_value; PhabricatorUserCache::writeCache( $type, $key, $user_phid, $raw_value); } } $this->usableCacheData[$key] = $usable_value; return $usable_value; } /** * @task cache */ public function clearCacheData($key) { unset($this->rawCacheData[$key]); unset($this->usableCacheData[$key]); return $this; } public function getCSSValue($variable_key) { $preference = PhabricatorAccessibilitySetting::SETTINGKEY; $key = $this->getUserSetting($preference); $postprocessor = CelerityPostprocessor::getPostprocessor($key); $variables = $postprocessor->getVariables(); if (!isset($variables[$variable_key])) { throw new Exception( pht( 'Unknown CSS variable "%s"!', $variable_key)); } return $variables[$variable_key]; } /* -( PhabricatorAuthPasswordHashInterface )------------------------------- */ public function newPasswordDigest( PhutilOpaqueEnvelope $envelope, PhabricatorAuthPassword $password) { // Before passwords are hashed, they are digested. The goal of digestion // is twofold: to reduce the length of very long passwords to something // reasonable; and to salt the password in case the best available hasher // does not include salt automatically. // Users may choose arbitrarily long passwords, and attackers may try to // attack the system by probing it with very long passwords. When large // inputs are passed to hashers -- which are intentionally slow -- it // can result in unacceptably long runtimes. The classic attack here is // to try to log in with a 64MB password and see if that locks up the // machine for the next century. By digesting passwords to a standard // length first, the length of the raw input does not impact the runtime // of the hashing algorithm. // Some hashers like bcrypt are self-salting, while other hashers are not. // Applying salt while digesting passwords ensures that hashes are salted // whether we ultimately select a self-salting hasher or not. // For legacy compatibility reasons, old VCS and Account password digest // algorithms are significantly more complicated than necessary to achieve // these goals. This is because they once used a different hashing and // salting process. When we upgraded to the modern modular hasher // infrastructure, we just bolted it onto the end of the existing pipelines // so that upgrading didn't break all users' credentials. // New implementations can (and, generally, should) safely select the // simple HMAC SHA256 digest at the bottom of the function, which does // everything that a digest callback should without any needless legacy // baggage on top. if ($password->getLegacyDigestFormat() == 'v1') { switch ($password->getPasswordType()) { case PhabricatorAuthPassword::PASSWORD_TYPE_VCS: // Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm. // They originally used this as a hasher, but it became a digest // algorithm once hashing was upgraded to include bcrypt. $digest = $envelope->openEnvelope(); $salt = $this->getPHID(); for ($ii = 0; $ii < 1000; $ii++) { $digest = PhabricatorHash::weakDigest($digest, $salt); } return new PhutilOpaqueEnvelope($digest); case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT: // Account passwords previously used this weird mess of salt and did // not digest the input to a standard length. // Beyond this being a weird special case, there are two actual // problems with this, although neither are particularly severe: // First, because we do not normalize the length of passwords, this // algorithm may make us vulnerable to DOS attacks where an attacker // attempts to use a very long input to slow down hashers. // Second, because the username is part of the hash algorithm, // renaming a user breaks their password. This isn't a huge deal but // it's pretty silly. There's no security justification for this // behavior, I just didn't think about the implication when I wrote // it originally. $parts = array( $this->getUsername(), $envelope->openEnvelope(), $this->getPHID(), $password->getPasswordSalt(), ); return new PhutilOpaqueEnvelope(implode('', $parts)); } } // For passwords which do not have some crazy legacy reason to use some // other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies // the digest requirements and is simple. $digest = PhabricatorHash::digestHMACSHA256( $envelope->openEnvelope(), $password->getPasswordSalt()); return new PhutilOpaqueEnvelope($digest); } public function newPasswordBlocklist( PhabricatorUser $viewer, PhabricatorAuthPasswordEngine $engine) { $list = array(); $list[] = $this->getUsername(); $list[] = $this->getRealName(); $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($emails as $email) { $list[] = $email->getAddress(); } return $list; } } diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index fde39da0fc..71a5186225 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -1,398 +1,394 @@ true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text64', 'subtitle' => 'text64', 'description' => 'text', 'domain' => 'text128?', 'domainFullURI' => 'text128?', 'parentSite' => 'text128?', 'parentDomain' => 'text128?', 'status' => 'text32', 'mailKey' => 'bytes20', 'profileImagePHID' => 'phid?', 'headerImagePHID' => 'phid?', // T6203/NULLABILITY // These policies should always be non-null. '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 save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhameBlogPHIDType::TYPECONST); } public static function initializeNewBlog(PhabricatorUser $actor) { $blog = id(new PhameBlog()) ->setCreatorPHID($actor->getPHID()) ->setStatus(self::STATUS_ACTIVE) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy(PhabricatorPolicies::POLICY_USER); return $blog; } public function isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } public static function getStatusNameMap() { return array( self::STATUS_ACTIVE => pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } /** * 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($domain_full_uri) { $example_domain = 'http://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($domain_full_uri); $domain = $uri->getDomain(); $protocol = $uri->getProtocol(); $path = $uri->getPath(); $supported_protocols = array('http', 'https'); if (!in_array($protocol, $supported_protocols)) { return pht( 'The custom domain should include a valid protocol in the URI '. '(for example, "%s"). Valid protocols are "http" or "https".', $example_domain); } if (strlen($path) && $path != '/') { return 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($domain, '.') === false) { return 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 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 getLiveURI() { if (strlen($this->getDomain())) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } public function getExternalLiveURI() { $uri = new PhutilURI($this->getDomainFullURI()); PhabricatorEnv::requireValidRemoteURIForLink($uri); return (string)$uri; } public function getExternalParentURI() { $uri = $this->getParentDomain(); PhabricatorEnv::requireValidRemoteURIForLink($uri); return (string)$uri; } public function getInternalLiveURI() { return '/phame/live/'.$this->getID().'/'; } public function getViewURI() { return '/phame/blog/view/'.$this->getID().'/'; } public function getManageURI() { return '/phame/blog/manage/'.$this->getID().'/'; } 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 getHeaderImageURI() { return $this->getHeaderImageFile()->getBestURI(); } public function attachHeaderImageFile(PhabricatorFile $file) { $this->headerImageFile = $file; return $this; } public function getHeaderImageFile() { return $this->assertAttached($this->headerImageFile); } /* -( 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 $user) { $can_edit = PhabricatorPolicyCapability::CAN_EDIT; 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; } break; } return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht( 'Users who can edit a blog can always view it.'); } return null; } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } 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(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $posts = id(new PhamePostQuery()) ->setViewer($engine->getViewer()) ->withBlogPHIDs(array($this->getPHID())) ->execute(); foreach ($posts as $post) { $engine->destroyObject($post); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhameBlogEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhameBlogTransaction(); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the blog.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('string') ->setDescription(pht('Blog description.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Archived or active status.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'description' => $this->getDescription(), 'status' => $this->getStatus(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhameBlogFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhameBlogFerretEngine(); } } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index c95d48054b..300579b086 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -1,392 +1,388 @@ setBloggerPHID($blogger->getPHID()) ->setBlogPHID($blog->getPHID()) ->attachBlog($blog) ->setDatePublished(PhabricatorTime::getNow()) ->setVisibility(PhameConstants::VISIBILITY_PUBLISHED); return $post; } public function attachBlog(PhameBlog $blog) { $this->blog = $blog; return $this; } public function getBlog() { return $this->assertAttached($this->blog); } public function getMonogram() { return 'J'.$this->getID(); } public function getLiveURI() { $blog = $this->getBlog(); $is_draft = $this->isDraft(); $is_archived = $this->isArchived(); if (strlen($blog->getDomain()) && !$is_draft && !$is_archived) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } public function getExternalLiveURI() { $id = $this->getID(); $slug = $this->getSlug(); $path = "/post/{$id}/{$slug}/"; $domain = $this->getBlog()->getDomain(); return (string)id(new PhutilURI('http://'.$domain)) ->setPath($path); } public function getInternalLiveURI() { $id = $this->getID(); $slug = $this->getSlug(); $blog_id = $this->getBlog()->getID(); return "/phame/live/{$blog_id}/post/{$id}/{$slug}/"; } public function getViewURI() { $id = $this->getID(); $slug = $this->getSlug(); return "/phame/post/view/{$id}/{$slug}/"; } public function getBestURI($is_live, $is_external) { if ($is_live) { if ($is_external) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } else { return $this->getViewURI(); } } public function getEditURI() { return '/phame/post/edit/'.$this->getID().'/'; } public function isDraft() { return ($this->getVisibility() == PhameConstants::VISIBILITY_DRAFT); } public function isArchived() { return ($this->getVisibility() == PhameConstants::VISIBILITY_ARCHIVED); } 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', 'subtitle' => 'text64', 'phameTitle' => 'sort64?', 'visibility' => 'uint32', 'mailKey' => 'bytes20', 'headerImagePHID' => 'phid?', // 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, ), 'bloggerPosts' => array( 'columns' => array( 'bloggerPHID', 'visibility', 'datePublished', 'id', ), ), ), ) + parent::getConfiguration(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhamePostPHIDType::TYPECONST); } public function getSlug() { return PhabricatorSlug::normalizeProjectSlug($this->getTitle()); } public function getHeaderImageURI() { return $this->getHeaderImageFile()->getBestURI(); } public function attachHeaderImageFile(PhabricatorFile $file) { $this->headerImageFile = $file; return $this; } public function getHeaderImageFile() { return $this->assertAttached($this->headerImageFile); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // Draft and archived posts are visible only to the author and other // users who can edit the blog. Published posts are visible to whoever // the blog is visible to. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->isDraft() && !$this->isArchived() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } break; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A blog post's author can always view 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) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } 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(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhamePostEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhamePostTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getBloggerPHID(), ); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->bloggerPHID == $phid); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('Title of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('slug') ->setType('string') ->setDescription(pht('Slug for the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('blogPHID') ->setType('phid') ->setDescription(pht('PHID of the blog that the post belongs to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('PHID of the author of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('body') ->setType('string') ->setDescription(pht('Body of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('datePublished') ->setType('epoch?') ->setDescription(pht('Publish date, if the post has been published.')), ); } public function getFieldValuesForConduit() { if ($this->isDraft()) { $date_published = null; } else if ($this->isArchived()) { $date_published = null; } else { $date_published = (int)$this->getDatePublished(); } return array( 'title' => $this->getTitle(), 'slug' => $this->getSlug(), 'blogPHID' => $this->getBlogPHID(), 'authorPHID' => $this->getBloggerPHID(), 'body' => $this->getBody(), 'datePublished' => $date_published, ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhamePostFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhamePostFerretEngine(); } } diff --git a/src/applications/phlux/storage/PhluxVariable.php b/src/applications/phlux/storage/PhluxVariable.php index d8c2d5315e..fb0d5c2f80 100644 --- a/src/applications/phlux/storage/PhluxVariable.php +++ b/src/applications/phlux/storage/PhluxVariable.php @@ -1,76 +1,72 @@ 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(); } /* -( 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; } } diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 51bdd1a196..a7e9a9b05d 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -1,295 +1,291 @@ setViewer($actor) ->withClasses(array('PhabricatorPholioApplication')) ->executeOne(); $view_policy = $app->getPolicy(PholioDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PholioDefaultEditCapability::CAPABILITY); return id(new PholioMock()) ->setAuthorPHID($actor->getPHID()) ->attachImages(array()) ->setStatus(self::STATUS_OPEN) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } public function getMonogram() { return 'M'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'description' => 'text', 'mailKey' => 'bytes20', 'status' => 'text12', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('MOCK'); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /** * These should be the images currently associated with the Mock. */ public function attachImages(array $images) { assert_instances_of($images, 'PholioImage'); $this->images = $images; return $this; } public function getImages() { $this->assertAttached($this->images); return $this->images; } /** * These should be *all* images associated with the Mock. This includes * images which have been removed and / or replaced from the Mock. */ public function attachAllImages(array $images) { assert_instances_of($images, 'PholioImage'); $this->allImages = $images; return $this; } public function getAllImages() { $this->assertAttached($this->images); return $this->allImages; } public function attachCoverFile(PhabricatorFile $file) { $this->coverFile = $file; return $this; } public function getCoverFile() { $this->assertAttached($this->coverFile); return $this->coverFile; } public function getTokenCount() { $this->assertAttached($this->tokenCount); return $this->tokenCount; } public function attachTokenCount($count) { $this->tokenCount = $count; return $this; } public function getImageHistorySet($image_id) { $images = $this->getAllImages(); $images = mpull($images, null, 'getID'); $selected_image = $images[$image_id]; $replace_map = mpull($images, null, 'getReplacesImagePHID'); $phid_map = mpull($images, null, 'getPHID'); // find the earliest image $image = $selected_image; while (isset($phid_map[$image->getReplacesImagePHID()])) { $image = $phid_map[$image->getReplacesImagePHID()]; } // now build history moving forward $history = array($image->getID() => $image); while (isset($replace_map[$image->getPHID()])) { $image = $replace_map[$image->getPHID()]; $history[$image->getID()] = $image; } return $history; } public function getStatuses() { $options = array(); $options[self::STATUS_OPEN] = pht('Open'); $options[self::STATUS_CLOSED] = pht('Closed'); return $options; } public function isClosed() { return ($this->getStatus() == 'closed'); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht("A mock's owner can always view and edit it."); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PholioMockEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PholioTransaction(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $images = id(new PholioImageQuery()) ->setViewer($engine->getViewer()) ->withMockIDs(array($this->getPHID())) ->execute(); foreach ($images as $image) { $image->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PholioMockFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PholioMockFerretEngine(); } /* -( PhabricatorTimelineInterace )---------------------------------------- */ public function newTimelineEngine() { return new PholioMockTimelineEngine(); } } diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index 8402ffa6d1..b0b57645c3 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -1,164 +1,160 @@ attachMemberPHIDs(array()); } public static function createNewAccount( PhabricatorUser $actor, PhabricatorContentSource $content_source) { $account = self::initializeNewAccount($actor); $xactions = array(); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhortuneAccountNameTransaction::TRANSACTIONTYPE) ->setNewValue(pht('Default 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(); } 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; } public function getURI() { return '/phortune/'.$this->getID().'/'; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortuneAccountEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhortuneAccountTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: 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()); if (isset($members[$viewer->getPHID()])) { return true; } // If the viewer is acting on behalf of a merchant, they can see // payment accounts. if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { foreach ($viewer->getAuthorities() as $authority) { if ($authority instanceof PhortuneMerchant) { return true; } } } return false; } 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 81f7099d0f..2b121a3b0c 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -1,693 +1,689 @@ setAuthorPHID($actor->getPHID()) ->setStatus(self::STATUS_BUILDING) ->setAccountPHID($account->getPHID()) ->setIsInvoice(0) ->attachAccount($account) ->setMerchantPHID($merchant->getPHID()) ->attachMerchant($merchant); $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 %s.', $copy->getStatus(), 'willApplyCharge()')); } $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) { if (!$method->isActive()) { throw new Exception( pht( 'Attempting to apply a charge using an inactive '. 'payment method ("%s")!', $method->getPHID())); } $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 %s, expected "%s".', $copy->getStatus(), 'willApplyCharge()', self::STATUS_READY)); } $charge->save(); $this->setStatus(self::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 %s, expected "%s".', $copy->getStatus(), 'didHoldCharge()', 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 %s.', $copy->getStatus(), 'didApplyCharge()')); } $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, // don't require review. $needs_review = false; 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 %s!', $copy->getStatus(), 'willReviewCart()')); } $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 %s!', $copy->getStatus(), 'didReviewCart()')); } 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 %s.', $copy->getStatus(), 'didFailCharge()')); } $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 non-positive 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( PhabricatorPhortuneContentSource::SOURCECONST); $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 getDescription() { return $this->getImplementation()->getDescription($this); } public function getDetailURI(PhortuneMerchant $authority = null) { if ($authority) { $prefix = 'merchant/'.$authority->getID().'/'; } else { $prefix = ''; } return '/phortune/'.$prefix.'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); } 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', 'subscriptionPHID' => 'phid?', 'isInvoice' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_account' => array( 'columns' => array('accountPHID'), ), 'key_merchant' => array( 'columns' => array('merchantPHID'), ), 'key_subscription' => array( 'columns' => array('subscriptionPHID'), ), ), ) + 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(); } /* -( 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/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php index a4fff91c5e..4916cfede7 100644 --- a/src/applications/phortune/storage/PhortuneMerchant.php +++ b/src/applications/phortune/storage/PhortuneMerchant.php @@ -1,122 +1,118 @@ setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->attachMemberPHIDs(array()) ->setContactInfo('') ->setInvoiceEmail('') ->setInvoiceFooter(''); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'description' => 'text', 'contactInfo' => 'text', 'invoiceEmail' => 'text255', 'invoiceFooter' => 'text', 'profileImagePHID' => 'phid?', ), ) + 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; } public function getURI() { return '/phortune/merchant/'.$this->getID().'/'; } 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); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortuneMerchantEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhortuneMerchantTransaction(); } /* -( 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/PhortunePaymentProviderConfig.php b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php index 65d5b3e65c..1e151b3fbb 100644 --- a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php +++ b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php @@ -1,117 +1,113 @@ setMerchantPHID($merchant->getPHID()) ->setIsEnabled(1); } 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.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortunePaymentProviderConfigEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhortunePaymentProviderConfigTransaction(); } } diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index 24f216fe26..9f8a4a475b 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -1,324 +1,320 @@ true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'slug' => 'sort128', 'depth' => 'uint32', 'status' => 'text32', 'editedEpoch' => 'epoch', 'maxVersion' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'slug' => array( 'columns' => array('slug'), 'unique' => true, ), 'depth' => array( 'columns' => array('depth', 'slug'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhrictionDocumentPHIDType::TYPECONST; } public static function initializeNewDocument(PhabricatorUser $actor, $slug) { $document = id(new self()) ->setSlug($slug); $content = id(new PhrictionContent()) ->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()) ->setEditPolicy($parent_doc->getEditPolicy()) ->setSpacePHID($parent_doc->getSpacePHID()); } else { $default_view_policy = PhabricatorPolicies::getMostOpenPolicy(); $document ->setViewPolicy($default_view_policy) ->setEditPolicy(PhabricatorPolicies::POLICY_USER) ->setSpacePHID($actor->getDefaultSpacePHID()); } $document->setEditedEpoch(PhabricatorTime::getNow()); $document->setMaxVersion(0); return $document; } public static function getSlugURI($slug, $type = 'document') { static $types = array( 'document' => '/w/', 'history' => '/phriction/history/', ); if (empty($types[$type])) { throw new Exception(pht("Unknown URI type '%s'!", $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; } public function getURI() { return self::getSlugURI($this->getSlug()); } /* -( Status )------------------------------------------------------------- */ public function getStatusObject() { return PhrictionDocumentStatus::newStatusObject($this->getStatus()); } public function getStatusIcon() { return $this->getStatusObject()->getIcon(); } public function getStatusColor() { return $this->getStatusObject()->getColor(); } public function getStatusDisplayName() { return $this->getStatusObject()->getDisplayName(); } public function isActive() { return $this->getStatusObject()->isActive(); } /* -( 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; } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhrictionTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhrictionTransaction(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return PhabricatorSubscribersQuery::loadSubscribersForPHID($this->phid); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $contents = id(new PhrictionContentQuery()) ->setViewer($engine->getViewer()) ->withDocumentPHIDs(array($this->getPHID())) ->execute(); foreach ($contents as $content) { $engine->destroyObject($content); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhrictionDocumentFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhrictionDocumentFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('path') ->setType('string') ->setDescription(pht('The path to the document.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Status information about the document.')), ); } public function getFieldValuesForConduit() { $status = array( 'value' => $this->getStatus(), 'name' => $this->getStatusDisplayName(), ); return array( 'path' => $this->getSlug(), 'status' => $status, ); } public function getConduitSearchAttachments() { return array( id(new PhrictionContentSearchEngineAttachment()) ->setAttachmentKey('content'), ); } /* -( PhabricatorPolicyCodexInterface )------------------------------------ */ public function newPolicyCodex() { return new PhrictionDocumentPolicyCodex(); } } diff --git a/src/applications/phurl/storage/PhabricatorPhurlURL.php b/src/applications/phurl/storage/PhabricatorPhurlURL.php index 76a4fd045a..4f3ee36dea 100644 --- a/src/applications/phurl/storage/PhabricatorPhurlURL.php +++ b/src/applications/phurl/storage/PhabricatorPhurlURL.php @@ -1,252 +1,248 @@ setViewer($actor) ->withClasses(array('PhabricatorPhurlApplication')) ->executeOne(); return id(new PhabricatorPhurlURL()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($actor->getPHID()) ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'alias' => 'sort64?', 'longURL' => 'text', 'description' => 'text', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_instance' => array( 'columns' => array('alias'), 'unique' => true, ), 'key_author' => array( 'columns' => array('authorPHID'), ), ), ) + parent::getConfiguration(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhurlURLPHIDType::TYPECONST); } public function getMonogram() { return 'U'.$this->getID(); } public function getURI() { $uri = '/'.$this->getMonogram(); return $uri; } public function isValid() { $allowed_protocols = PhabricatorEnv::getEnvConfig('uri.allowed-protocols'); $uri = new PhutilURI($this->getLongURL()); return isset($allowed_protocols[$uri->getProtocol()]); } public function getDisplayName() { if ($this->getName()) { return $this->getName(); } else { return $this->getLongURL(); } } public function getRedirectURI() { if (strlen($this->getAlias())) { $path = '/u/'.$this->getAlias(); } else { $path = '/u/'.$this->getID(); } $domain = PhabricatorEnv::getEnvConfig('phurl.short-uri'); if (!$domain) { $domain = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); } $uri = new PhutilURI($domain); $uri->setPath($path); return (string)$uri; } /* -( 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) { $user_phid = $this->getAuthorPHID(); if ($user_phid) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid == $user_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of a URL can always view and edit it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorPhurlURLEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPhurlURLTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getAuthorPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('URL name.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('alias') ->setType('string') ->setDescription(pht('The alias for the URL.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('longurl') ->setType('string') ->setDescription(pht('The pre-shortened URL.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('string') ->setDescription(pht('A description of the URL.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'alias' => $this->getAlias(), 'description' => $this->getDescription(), 'urls' => array( 'long' => $this->getLongURL(), 'short' => $this->getRedirectURI(), ), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorNgramInterface )------------------------------------------ */ public function newNgrams() { return array( id(new PhabricatorPhurlURLNameNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index 0dcf44d617..9179e22a44 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -1,226 +1,222 @@ setViewer($actor) ->withClasses(array('PhabricatorPonderApplication')) ->executeOne(); return id(new PonderAnswer()) ->setQuestionID($question->getID()) ->setContent('') ->attachQuestion($question) ->setAuthorPHID($actor->getPHID()) ->setVoteCount(0) ->setStatus(PonderAnswerStatus::ANSWER_STATUS_VISIBLE); } public function attachQuestion(PonderQuestion $question = null) { $this->question = $question; return $this; } public function getQuestion() { return $this->assertAttached($this->question); } public function getURI() { return '/Q'.$this->getQuestionID().'#A'.$this->getID(); } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'voteCount' => 'sint32', 'content' => 'text', 'status' => 'text32', 'mailKey' => 'bytes20', ), 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'), ), 'status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderAnswerPHIDType::TYPECONST); } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PonderAnswerEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PonderAnswerTransaction(); } // Markup interface public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( 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: $app = PhabricatorApplication::getByClass( 'PhabricatorPonderApplication'); return $app->getPolicy(PonderModerateCapability::CAPABILITY); } } 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.'); $out[] = pht( 'A moderator can always view the answers.'); break; } return $out; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } /* -( 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 2ae6c76d68..40ec9fbee8 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -1,301 +1,297 @@ setViewer($actor) ->withClasses(array('PhabricatorPonderApplication')) ->executeOne(); $view_policy = $app->getPolicy( PonderDefaultViewCapability::CAPABILITY); return id(new PonderQuestion()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setStatus(PonderQuestionStatus::STATUS_OPEN) ->setAnswerCount(0) ->setAnswerWiki('') ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'status' => 'text32', 'content' => 'text', 'answerWiki' => 'text', 'answerCount' => 'uint32', 'mailKey' => 'bytes20', // T6203/NULLABILITY // This should always exist. 'contentSource' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } public function getMonogram() { return 'Q'.$this->getID(); } public function getViewURI() { return '/'.$this->getMonogram(); } public function attachAnswers(array $answers) { assert_instances_of($answers, 'PonderAnswer'); $this->answers = $answers; return $this; } public function getAnswers() { return $this->answers; } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->projectPHIDs = $phids; return $this; } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PonderQuestionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PonderQuestionTransaction(); } // Markup interface public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getFullTitle() { $id = $this->getID(); $title = $this->getTitle(); return "Q{$id}: {$title}"; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: $app = PhabricatorApplication::getByClass( 'PhabricatorPonderApplication'); return $app->getPolicy(PonderModerateCapability::CAPABILITY); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { if (PhabricatorPolicyFilter::hasCapability( $viewer, $this, PhabricatorPolicyCapability::CAN_EDIT)) { return true; } } return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { $out = array(); $out[] = pht('The user who asked a question can always view and edit it.'); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $out[] = pht( 'A moderator can always view the question.'); break; } return $out; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $answers = id(new PonderAnswer())->loadAllWhere( 'questionID = %d', $this->getID()); foreach ($answers as $answer) { $engine->destroyObject($answer); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PonderQuestionFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PonderQuestionFerretEngine(); } } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index adbfb0dc36..4dae13f3c6 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -1,880 +1,876 @@ 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); // If this is the child of some other project, default the Space to the // Space of the parent. if ($parent) { $space_phid = $parent->getSpacePHID(); } else { $space_phid = $actor->getDefaultSpacePHID(); } $default_icon = PhabricatorProjectIconSet::getDefaultIconKey(); $default_color = PhabricatorProjectIconSet::getDefaultColorKey(); return id(new PhabricatorProject()) ->setAuthorPHID($actor->getPHID()) ->setIcon($default_icon) ->setColor($default_color) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setJoinPolicy($join_policy) ->setSpacePHID($space_phid) ->setIsMembershipLocked(0) ->attachMemberPHIDs(array()) ->attachSlugs(array()) ->setHasWorkboard(0) ->setHasMilestones(0) ->setHasSubprojects(0) ->attachParentProject(null); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_JOIN, ); } public function getPolicy($capability) { if ($this->isMilestone()) { return $this->getParentProject()->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) { if ($this->isMilestone()) { return $this->getParentProject()->hasAutomaticCapability( $capability, $viewer); } $can_edit = PhabricatorPolicyCapability::CAN_EDIT; 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: $parent = $this->getParentProject(); if ($parent) { $can_edit_parent = PhabricatorPolicyFilter::hasCapability( $viewer, $parent, $can_edit); if ($can_edit_parent) { return true; } } break; case PhabricatorPolicyCapability::CAN_JOIN: if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) { // Project editors can always join a project. return true; } break; } return false; } public function describeAutomaticCapability($capability) { // TODO: Clarify the additional rules that parent and subprojects imply. 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 getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $parent = $this->getParentProject(); if ($parent) { $extended[] = array( $parent, PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } 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; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'status' => 'text32', 'primarySlug' => 'text128?', 'isMembershipLocked' => 'bool', 'profileImagePHID' => 'phid?', 'icon' => 'text32', 'color' => 'text32', 'mailKey' => 'bytes20', 'joinPolicy' => 'policy', 'parentProjectPHID' => 'phid?', 'hasWorkboard' => 'bool', 'hasMilestones' => 'bool', 'hasSubprojects' => 'bool', 'milestoneNumber' => 'uint32?', 'projectPath' => 'hashpath64', 'projectDepth' => 'uint32', 'projectPathKey' => 'bytes4', ), self::CONFIG_KEY_SCHEMA => array( 'key_icon' => array( 'columns' => array('icon'), ), 'key_color' => array( 'columns' => array('color'), ), 'key_milestone' => array( 'columns' => array('parentProjectPHID', 'milestoneNumber'), 'unique' => true, ), 'key_primaryslug' => array( 'columns' => array('primarySlug'), 'unique' => true, ), 'key_path' => array( 'columns' => array('projectPath', 'projectDepth'), ), 'key_pathkey' => array( 'columns' => array('projectPathKey'), '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 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 isUserAncestorWatcher($user_phid) { $is_watcher = $this->isUserWatcher($user_phid); if (!$is_watcher) { $parent = $this->getParentProject(); if ($parent) { return $parent->isUserWatcher($user_phid); } } return $is_watcher; } public function getWatchedAncestorPHID($user_phid) { if ($this->isUserWatcher($user_phid)) { return $this->getPHID(); } $parent = $this->getParentProject(); if ($parent) { return $parent->getWatchedAncestorPHID($user_phid); } return null; } 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 getAllAncestorWatcherPHIDs() { $parent = $this->getParentProject(); if ($parent) { $watchers = $parent->getAllAncestorWatcherPHIDs(); } else { $watchers = array(); } foreach ($this->getWatcherPHIDs() as $phid) { $watchers[$phid] = $phid; } return $watchers; } 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 getURI() { $id = $this->getID(); return "/project/view/{$id}/"; } public function getProfileURI() { $id = $this->getID(); return "/project/profile/{$id}/"; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } if (!strlen($this->getPHID())) { $this->setPHID($this->generatePHID()); } if (!strlen($this->getProjectPathKey())) { $hash = PhabricatorHash::digestForIndex($this->getPHID()); $hash = substr($hash, 0, 4); $this->setProjectPathKey($hash); } $path = array(); $depth = 0; if ($this->parentProjectPHID) { $parent = $this->getParentProject(); $path[] = $parent->getProjectPath(); $depth = $parent->getProjectDepth() + 1; } $path[] = $this->getProjectPathKey(); $path = implode('', $path); $limit = self::getProjectDepthLimit(); if ($depth >= $limit) { throw new Exception(pht('Project depth is too great.')); } $this->setProjectPath($path); $this->setProjectDepth($depth); $this->openTransaction(); $result = parent::save(); $this->updateDatasourceTokens(); $this->saveTransaction(); return $result; } public static function getProjectDepthLimit() { // This is limited by how many path hashes we can fit in the path // column. return 16; } 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->getDisplayName(); $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 %LQ', $table, $chunk); } $this->saveTransaction(); } public function isMilestone() { return ($this->getMilestoneNumber() !== null); } public function getParentProject() { return $this->assertAttached($this->parentProject); } public function attachParentProject(PhabricatorProject $project = null) { $this->parentProject = $project; return $this; } public function getAncestorProjectPaths() { $parts = array(); $path = $this->getProjectPath(); $parent_length = (strlen($path) - 4); for ($ii = $parent_length; $ii > 0; $ii -= 4) { $parts[] = substr($path, 0, $ii); } return $parts; } public function getAncestorProjects() { $ancestors = array(); $cursor = $this->getParentProject(); while ($cursor) { $ancestors[] = $cursor; $cursor = $cursor->getParentProject(); } return $ancestors; } public function supportsEditMembers() { if ($this->isMilestone()) { return false; } if ($this->getHasSubprojects()) { return false; } return true; } public function supportsMilestones() { if ($this->isMilestone()) { return false; } return true; } public function supportsSubprojects() { if ($this->isMilestone()) { return false; } return true; } public function loadNextMilestoneNumber() { $current = queryfx_one( $this->establishConnection('w'), 'SELECT MAX(milestoneNumber) n FROM %T WHERE parentProjectPHID = %s', $this->getTableName(), $this->getPHID()); if (!$current) { $number = 1; } else { $number = (int)$current['n'] + 1; } return $number; } public function getDisplayName() { $name = $this->getName(); // If this is a milestone, show it as "Parent > Sprint 99". if ($this->isMilestone()) { $name = pht( '%s (%s)', $this->getParentProject()->getName(), $name); } return $name; } public function getDisplayIconKey() { if ($this->isMilestone()) { $key = PhabricatorProjectIconSet::getMilestoneIconKey(); } else { $key = $this->getIcon(); } return $key; } public function getDisplayIconIcon() { $key = $this->getDisplayIconKey(); return PhabricatorProjectIconSet::getIconIcon($key); } public function getDisplayIconName() { $key = $this->getDisplayIconKey(); return PhabricatorProjectIconSet::getIconName($key); } public function getDisplayColor() { if ($this->isMilestone()) { return $this->getParentProject()->getColor(); } return $this->getColor(); } public function getDisplayIconComposeIcon() { $icon = $this->getDisplayIconIcon(); return $icon; } public function getDisplayIconComposeColor() { $color = $this->getDisplayColor(); $map = array( 'grey' => 'charcoal', 'checkered' => 'backdrop', ); return idx($map, $color, $color); } 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 getDefaultWorkboardSort() { return $this->getProperty('workboard.sort.default'); } public function setDefaultWorkboardSort($sort) { return $this->setProperty('workboard.sort.default', $sort); } public function getDefaultWorkboardFilter() { return $this->getProperty('workboard.filter.default'); } public function setDefaultWorkboardFilter($filter) { return $this->setProperty('workboard.filter.default', $filter); } public function getWorkboardBackgroundColor() { return $this->getProperty('workboard.background'); } public function setWorkboardBackgroundColor($color) { return $this->setProperty('workboard.background', $color); } public function getDisplayWorkboardBackgroundColor() { $color = $this->getWorkboardBackgroundColor(); if ($color === null) { $parent = $this->getParentProject(); if ($parent) { return $parent->getDisplayWorkboardBackgroundColor(); } } if ($color === 'none') { $color = null; } return $color; } /* -( 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(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { if ($this->isMilestone()) { return $this->getParentProject()->getSpacePHID(); } return $this->spacePHID; } /* -( 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(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorProjectFulltextEngine(); } /* -( PhabricatorFerretInterface )--------------------------------------- */ public function newFerretEngine() { return new PhabricatorProjectFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the project.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('slug') ->setType('string') ->setDescription(pht('Primary slug/hashtag.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('milestone') ->setType('int?') ->setDescription(pht('For milestones, milestone sequence number.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('parent') ->setType('map?') ->setDescription( pht( 'For subprojects and milestones, a brief description of the '. 'parent project.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('depth') ->setType('int') ->setDescription( pht( 'For subprojects and milestones, depth of this project in the '. 'tree. Root projects have depth 0.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('icon') ->setType('map') ->setDescription(pht('Information about the project icon.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('color') ->setType('map') ->setDescription(pht('Information about the project color.')), ); } public function getFieldValuesForConduit() { $color_key = $this->getColor(); $color_name = PhabricatorProjectIconSet::getColorName($color_key); if ($this->isMilestone()) { $milestone = (int)$this->getMilestoneNumber(); } else { $milestone = null; } $parent = $this->getParentProject(); if ($parent) { $parent_ref = $parent->getRefForConduit(); } else { $parent_ref = null; } return array( 'name' => $this->getName(), 'slug' => $this->getPrimarySlug(), 'milestone' => $milestone, 'depth' => (int)$this->getProjectDepth(), 'parent' => $parent_ref, 'icon' => array( 'key' => $this->getDisplayIconKey(), 'name' => $this->getDisplayIconName(), 'icon' => $this->getDisplayIconIcon(), ), 'color' => array( 'key' => $color_key, 'name' => $color_name, ), ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorProjectsMembersSearchEngineAttachment()) ->setAttachmentKey('members'), id(new PhabricatorProjectsWatchersSearchEngineAttachment()) ->setAttachmentKey('watchers'), id(new PhabricatorProjectsAncestorsSearchEngineAttachment()) ->setAttachmentKey('ancestors'), ); } /** * Get an abbreviated representation of this project for use in providing * "parent" and "ancestor" information. */ public function getRefForConduit() { return array( 'id' => (int)$this->getID(), 'phid' => $this->getPHID(), 'name' => $this->getName(), ); } /* -( PhabricatorColumnProxyInterface )------------------------------------ */ public function getProxyColumnName() { return $this->getName(); } public function getProxyColumnIcon() { return $this->getDisplayIconIcon(); } public function getProxyColumnClass() { if ($this->isMilestone()) { return 'phui-workboard-column-milestone'; } return null; } } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index b4c5df885e..756c356ee1 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -1,298 +1,294 @@ setName('') ->setStatus(self::STATUS_ACTIVE) ->attachProxy(null); } 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', 'proxyPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('projectPHID', 'status', 'sequence'), ), 'key_sequence' => array( 'columns' => array('projectPHID', 'sequence'), ), 'key_proxy' => array( 'columns' => array('projectPHID', 'proxyPHID'), 'unique' => true, ), ), ) + 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 attachProxy($proxy) { $this->proxy = $proxy; return $this; } public function getProxy() { return $this->assertAttached($this->proxy); } public function isDefaultColumn() { return (bool)$this->getProperty('isDefault'); } public function isHidden() { $proxy = $this->getProxy(); if ($proxy) { return $proxy->isArchived(); } return ($this->getStatus() == self::STATUS_HIDDEN); } public function getDisplayName() { $proxy = $this->getProxy(); if ($proxy) { return $proxy->getProxyColumnName(); } $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 getDisplayClass() { $proxy = $this->getProxy(); if ($proxy) { return $proxy->getProxyColumnClass(); } return null; } public function getHeaderIcon() { $proxy = $this->getProxy(); if ($proxy) { return $proxy->getProxyColumnIcon(); } if ($this->isHidden()) { return 'fa-eye-slash'; } 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; } public function getOrderingKey() { $proxy = $this->getProxy(); // Normal columns and subproject columns go first, in a user-controlled // order. // All the milestone columns go last, in their sequential order. if (!$proxy || !$proxy->isMilestone()) { $group = 'A'; $sequence = $this->getSequence(); } else { $group = 'B'; $sequence = $proxy->getMilestoneNumber(); } return sprintf('%s%012d', $group, $sequence); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The display name of the column.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('project') ->setType('map') ->setDescription(pht('The project the column belongs to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('proxyPHID') ->setType('phid?') ->setDescription( pht( 'For columns that proxy another object (like a subproject or '. 'milestone), the PHID of the object they proxy.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getDisplayName(), 'proxyPHID' => $this->getProxyPHID(), 'project' => $this->getProject()->getRefForConduit(), ); } public function getConduitSearchAttachments() { return array(); } public function getRefForConduit() { return array( 'id' => (int)$this->getID(), 'phid' => $this->getPHID(), 'name' => $this->getDisplayName(), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProjectColumnTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorProjectColumnTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // NOTE: Column policies are enforced as an extended policy which makes // them the same as the project's policies. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_USER; } } 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.'); } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { return array( array($this->getProject(), $capability), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/releeph/storage/ReleephBranch.php b/src/applications/releeph/storage/ReleephBranch.php index 5e23499a9d..28323a9981 100644 --- a/src/applications/releeph/storage/ReleephBranch.php +++ b/src/applications/releeph/storage/ReleephBranch.php @@ -1,192 +1,188 @@ 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; } protected 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(); } /* -( 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 318d687efd..db47ac5d39 100644 --- a/src/applications/releeph/storage/ReleephProject.php +++ b/src/applications/releeph/storage/ReleephProject.php @@ -1,149 +1,145 @@ 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 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(); } /* -( 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; } } diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index 23956e4a3c..c21f9b28c8 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -1,358 +1,354 @@ 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 = self::INTENT_PASS; $intent_want = self::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 )----------------------------------------------------- */ 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 id(new PhabricatorRepositoryCommit())->loadOneWhere( 'phid = %s', $this->getRequestCommitPHID()); } public function loadPhabricatorRepositoryCommitData() { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { return id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); } } /* -( 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(pht('`%s` is now deprecated!', 'status')); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ReleephRequestTransactionalEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new ReleephRequestTransaction(); } /* -( 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 6967085644..e1febe7ac8 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1,2819 +1,2815 @@ setViewer($actor) ->withClasses(array('PhabricatorDiffusionApplication')) ->executeOne(); $view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY); $push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY); $repository = id(new PhabricatorRepository()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setPushPolicy($push_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); // Put the repository in "Importing" mode until we finish // parsing it. $repository->setDetail('importing', true); return $repository; } 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?', 'repositorySlug' => 'sort64?', 'versionControlSystem' => 'text32', 'uuid' => 'text64?', 'pushPolicy' => 'policy', 'credentialPHID' => 'phid?', 'almanacServicePHID' => 'phid?', 'localPath' => 'text128?', 'profileImagePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'callsign' => array( 'columns' => array('callsign'), 'unique' => true, ), 'key_name' => array( 'columns' => array('name(128)'), ), 'key_vcs' => array( 'columns' => array('versionControlSystem'), ), 'key_slug' => array( 'columns' => array('repositorySlug'), 'unique' => true, ), 'key_local' => array( 'columns' => array('localPath'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryRepositoryPHIDType::TYPECONST); } public static function getStatusMap() { return array( self::STATUS_ACTIVE => array( 'name' => pht('Active'), 'isTracked' => 1, ), self::STATUS_INACTIVE => array( 'name' => pht('Inactive'), 'isTracked' => 0, ), ); } public static function getStatusNameMap() { return ipull(self::getStatusMap(), 'name'); } public function getStatus() { if ($this->isTracked()) { return self::STATUS_ACTIVE; } else { return self::STATUS_INACTIVE; } } 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(), 'encoding' => $this->getDefaultTextEncoding(), 'staging' => array( 'supported' => $this->supportsStaging(), 'prefix' => 'phabricator', 'uri' => $this->getStagingURI(), ), ); } public function getDefaultTextEncoding() { return $this->getDetail('encoding', 'UTF-8'); } public function getMonogram() { $callsign = $this->getCallsign(); if (strlen($callsign)) { return "r{$callsign}"; } $id = $this->getID(); return "R{$id}"; } public function getDisplayName() { $slug = $this->getRepositorySlug(); if (strlen($slug)) { return $slug; } return $this->getMonogram(); } public function getAllMonograms() { $monograms = array(); $monograms[] = 'R'.$this->getID(); $callsign = $this->getCallsign(); if (strlen($callsign)) { $monograms[] = 'r'.$callsign; } return $monograms; } public function setLocalPath($path) { // Convert any extra slashes ("//") in the path to a single slash ("/"). $path = preg_replace('(//+)', '/', $path); return parent::setLocalPath($path); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function attachCommitCount($count) { $this->commitCount = $count; return $this; } public function getCommitCount() { return $this->assertAttached($this->commitCount); } public function attachMostRecentCommit( PhabricatorRepositoryCommit $commit = null) { $this->mostRecentCommit = $commit; return $this; } public function getMostRecentCommit() { return $this->assertAttached($this->mostRecentCommit); } public function getDiffusionBrowseURIForPath( PhabricatorUser $user, $path, $line = null, $branch = null) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $this, 'path' => $path, 'branch' => $branch, )); return $drequest->generateURI( array( 'action' => 'browse', 'line' => $line, )); } public function 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(pht('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->getRepositorySlug(); // 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; } public static function isValidRepositorySlug($slug) { try { self::assertValidRepositorySlug($slug); return true; } catch (Exception $ex) { return false; } } public static function assertValidRepositorySlug($slug) { if (!strlen($slug)) { throw new Exception( pht( 'The empty string is not a valid repository short name. '. 'Repository short names must be at least one character long.')); } if (strlen($slug) > 64) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must not be longer than 64 characters.', $slug)); } if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names may only contain letters, numbers, periods, hyphens '. 'and underscores.', $slug)); } if (!preg_match('/^[a-zA-Z0-9]/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must begin with a letter or number.', $slug)); } if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must end with a letter or number.', $slug)); } if (preg_match('/__|--|\\.\\./', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must not contain multiple consecutive underscores, '. 'hyphens, or periods.', $slug)); } if (preg_match('/^[A-Z]+\z/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names may not contain only uppercase letters.', $slug)); } if (preg_match('/^\d+\z/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names may not contain only numbers.', $slug)); } if (preg_match('/\\.git/', $slug)) { throw new Exception( pht( 'The name "%s" is not a valid repository short name. Repository '. 'short names must not end in ".git". This suffix will be added '. 'automatically in appropriate contexts.', $slug)); } } public static function assertValidCallsign($callsign) { if (!strlen($callsign)) { throw new Exception( pht( 'A repository callsign must be at least one character long.')); } if (strlen($callsign) > 32) { throw new Exception( pht( 'The callsign "%s" is not a valid repository callsign. Callsigns '. 'must be no more than 32 bytes long.', $callsign)); } if (!preg_match('/^[A-Z]+\z/', $callsign)) { throw new Exception( pht( 'The callsign "%s" is not a valid repository callsign. Callsigns '. 'may only contain UPPERCASE letters.', $callsign)); } } 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); } /* -( 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) { return $this->newRemoteCommandEngine($argv) ->newFuture(); } private function newRemoteCommandPassthru(array $argv) { return $this->newRemoteCommandEngine($argv) ->setPassthru(true) ->newFuture(); } private function newRemoteCommandEngine(array $argv) { return DiffusionCommandEngine::newCommandEngine($this) ->setArgv($argv) ->setCredentialPHID($this->getCredentialPHID()) ->setURI($this->getRemoteURIObject()); } /* -( 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(); $future = DiffusionCommandEngine::newCommandEngine($this) ->setArgv($argv) ->newFuture(); if ($this->usesLocalWorkingCopy()) { $future->setCWD($this->getLocalPath()); } return $future; } private function newLocalCommandPassthru(array $argv) { $this->assertLocalExists(); $future = DiffusionCommandEngine::newCommandEngine($this) ->setArgv($argv) ->setPassthru(true) ->newFuture(); if ($this->usesLocalWorkingCopy()) { $future->setCWD($this->getLocalPath()); } return $future; } public function getURI() { $short_name = $this->getRepositorySlug(); if (strlen($short_name)) { return "/source/{$short_name}/"; } $callsign = $this->getCallsign(); if (strlen($callsign)) { return "/diffusion/{$callsign}/"; } $id = $this->getID(); return "/diffusion/{$id}/"; } public function getPathURI($path) { return $this->getURI().ltrim($path, '/'); } public function getCommitURI($identifier) { $callsign = $this->getCallsign(); if (strlen($callsign)) { return "/r{$callsign}{$identifier}"; } $id = $this->getID(); return "/R{$id}:{$identifier}"; } public static function parseRepositoryServicePath($request_path, $vcs) { $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); $patterns = array( '(^'. '(?P/?(?:diffusion|source)/(?P[^/]+))'. '(?P.*)'. '\z)', ); $identifier = null; foreach ($patterns as $pattern) { $matches = null; if (!preg_match($pattern, $request_path, $matches)) { continue; } $identifier = $matches['identifier']; if ($is_git) { $identifier = preg_replace('/\\.git\z/', '', $identifier); } $base = $matches['base']; $path = $matches['path']; break; } if ($identifier === null) { return null; } return array( 'identifier' => $identifier, 'base' => $base, 'path' => $path, ); } public function getCanonicalPath($request_path) { $standard_pattern = '(^'. '(?P/(?:diffusion|source)/)'. '(?P[^/]+)'. '(?P(?:/.*)?)'. '\z)'; $matches = null; if (preg_match($standard_pattern, $request_path, $matches)) { $suffix = $matches['suffix']; return $this->getPathURI($suffix); } $commit_pattern = '(^'. '(?P/)'. '(?P'. '(?:'. 'r(?P[A-Z]+)'. '|'. 'R(?P[1-9]\d*):'. ')'. '(?P[a-f0-9]+)'. ')'. '\z)'; $matches = null; if (preg_match($commit_pattern, $request_path, $matches)) { $commit = $matches['commit']; return $this->getCommitURI($commit); } return null; } public function generateURI(array $params) { $req_branch = false; $req_commit = false; $action = idx($params, 'action'); switch ($action) { case 'history': case 'graph': case 'clone': case 'blame': case 'browse': case 'document': case 'change': case 'lastmodified': case 'tags': case 'branches': case 'lint': case 'pathtree': case 'refs': case 'compare': break; case 'branch': // NOTE: This does not actually require a branch, and won't have one // in Subversion. Possibly this should be more clear. break; case 'commit': case 'rendering-ref': $req_commit = true; break; default: throw new Exception( pht( 'Action "%s" is not a valid repository URI action.', $action)); } $path = idx($params, 'path'); $branch = idx($params, 'branch'); $commit = idx($params, 'commit'); $line = idx($params, 'line'); $head = idx($params, 'head'); $against = idx($params, 'against'); if ($req_commit && !strlen($commit)) { throw new Exception( pht( 'Diffusion URI action "%s" requires commit!', $action)); } if ($req_branch && !strlen($branch)) { throw new Exception( pht( 'Diffusion URI action "%s" requires branch!', $action)); } if ($action === 'commit') { return $this->getCommitURI($commit); } if (strlen($path)) { $path = ltrim($path, '/'); $path = str_replace(array(';', '$'), array(';;', '$$'), $path); $path = phutil_escape_uri($path); } $raw_branch = $branch; if (strlen($branch)) { $branch = phutil_escape_uri_path_component($branch); $path = "{$branch}/{$path}"; } $raw_commit = $commit; if (strlen($commit)) { $commit = str_replace('$', '$$', $commit); $commit = ';'.phutil_escape_uri($commit); } if (strlen($line)) { $line = '$'.phutil_escape_uri($line); } $query = array(); switch ($action) { case 'change': case 'history': case 'graph': case 'blame': case 'browse': case 'document': case 'lastmodified': case 'tags': case 'branches': case 'lint': case 'pathtree': case 'refs': $uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}"); break; case 'compare': $uri = $this->getPathURI("/{$action}/"); if (strlen($head)) { $query['head'] = $head; } else if (strlen($raw_commit)) { $query['commit'] = $raw_commit; } else if (strlen($raw_branch)) { $query['head'] = $raw_branch; } if (strlen($against)) { $query['against'] = $against; } break; case 'branch': if (strlen($path)) { $uri = $this->getPathURI("/repository/{$path}"); } else { $uri = $this->getPathURI('/'); } break; case 'external': $commit = ltrim($commit, ';'); $uri = "/diffusion/external/{$commit}/"; break; case 'rendering-ref': // This isn't a real URI per se, it's passed as a query parameter to // the ajax changeset stuff but then we parse it back out as though // it came from a URI. $uri = rawurldecode("{$path}{$commit}"); break; case 'clone': $uri = $this->getPathURI("/{$action}/"); break; } if ($action == 'rendering-ref') { return $uri; } $uri = new PhutilURI($uri); if (isset($params['lint'])) { $params['params'] = idx($params, 'params', array()) + array( 'lint' => $params['lint'], ); } $query = idx($params, 'params', array()) + $query; if ($query) { $uri->setQueryParams($query); } return $uri; } public function updateURIIndex() { $indexes = array(); $uris = $this->getURIs(); foreach ($uris as $uri) { if ($uri->getIsDisabled()) { continue; } $indexes[] = $uri->getNormalizedURI(); } PhabricatorRepositoryURIIndex::updateRepositoryURIs( $this->getPHID(), $indexes); return $this; } public function isTracked() { $status = $this->getDetail('tracking-enabled'); $map = self::getStatusMap(); $spec = idx($map, $status); if (!$spec) { if ($status) { $status = self::STATUS_ACTIVE; } else { $status = self::STATUS_INACTIVE; } $spec = idx($map, $status); } return (bool)idx($spec, 'isTracked', 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 shouldTrackRef(DiffusionRepositoryRef $ref) { // At least for now, don't track the staging area tags. if ($ref->isTag()) { if (preg_match('(^phabricator/)', $ref->getShortName())) { return false; } } if (!$ref->isBranch()) { return true; } return $this->shouldTrackBranch($ref->getShortName()); } public function shouldTrackBranch($branch) { return $this->isBranchInFilter($branch, 'branch-filter'); } public function formatCommitName($commit_identifier, $local = false) { $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) { $name = substr($commit_identifier, 0, 12); $need_scope = false; } else { $name = $commit_identifier; $need_scope = true; } if (!$local) { $need_scope = true; } if ($need_scope) { $callsign = $this->getCallsign(); if ($callsign) { $scope = "r{$callsign}"; } else { $id = $this->getID(); $scope = "R{$id}:"; } $name = $scope.$name; } return $name; } public function isImporting() { return (bool)$this->getDetail('importing', false); } public function isNewlyInitialized() { return (bool)$this->getDetail('newly-initialized', false); } public function loadImportProgress() { $progress = queryfx_all( $this->establishConnection('r'), 'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d GROUP BY importStatus', id(new PhabricatorRepositoryCommit())->getTableName(), $this->getID()); $done = 0; $total = 0; foreach ($progress as $row) { $total += $row['N'] * 4; $status = $row['importStatus']; if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) { $done += $row['N']; } if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) { $done += $row['N']; } if ($status & PhabricatorRepositoryCommit::IMPORTED_OWNERS) { $done += $row['N']; } if ($status & PhabricatorRepositoryCommit::IMPORTED_HERALD) { $done += $row['N']; } } if ($total) { $ratio = ($done / $total); } else { $ratio = 0; } // Cap this at "99.99%", because it's confusing to users when the actual // fraction is "99.996%" and it rounds up to "100.00%". if ($ratio > 0.9999) { $ratio = 0.9999; } return $ratio; } /** * Should this repository publish feed, notifications, audits, and email? * * We do not publish information about repositories during initial import, * or if the repository has been set not to publish. */ public function shouldPublish() { if ($this->isImporting()) { return false; } if ($this->getDetail('herald-disabled')) { return false; } return true; } /* -( Autoclose )---------------------------------------------------------- */ public function shouldAutocloseRef(DiffusionRepositoryRef $ref) { if (!$ref->isBranch()) { return false; } return $this->shouldAutocloseBranch($ref->getShortName()); } /** * 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(pht('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; } public function getAutocloseOnlyRules() { return array_keys($this->getDetail('close-commits-filter', array())); } public function setAutocloseOnlyRules(array $rules) { $rules = array_fill_keys($rules, true); $this->setDetail('close-commits-filter', $rules); return $this; } public function getTrackOnlyRules() { return array_keys($this->getDetail('branch-filter', array())); } public function setTrackOnlyRules(array $rules) { $rules = array_fill_keys($rules, true); $this->setDetail('branch-filter', $rules); return $this; } /* -( 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() { return (string)$this->getCloneURIObject(); } /** * Get the protocol for the repository's remote. * * @return string Protocol, like "ssh" or "git". * @task uri */ public function getRemoteProtocol() { $uri = $this->getRemoteURIObject(); return $uri->getProtocol(); } /** * Get a parsed object representation of the repository's remote URI.. * * @return wild A @{class@libphutil:PhutilURI}. * @task uri */ public function getRemoteURIObject() { $raw_uri = $this->getDetail('remote-uri'); if (!strlen($raw_uri)) { return new PhutilURI(''); } if (!strncmp($raw_uri, '/', 1)) { return new PhutilURI('file://'.$raw_uri); } return new PhutilURI($raw_uri); } /** * 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(); } } // TODO: This should be cleaned up to deal with all the new URI handling. $another_copy = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->needURIs(true) ->executeOne(); $clone_uris = $another_copy->getCloneURIs(); if (!$clone_uris) { return null; } return head($clone_uris)->getEffectiveURI(); } private function getRawHTTPCloneURIObject() { $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(); } queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE repositoryPHID = %s', id(new PhabricatorRepositorySymbol())->getTableName(), $this->getPHID()); $commits = id(new PhabricatorRepositoryCommit()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($commits as $commit) { // note PhabricatorRepositoryAuditRequests and // PhabricatorRepositoryCommitData are deleted here too. $commit->delete(); } $uris = id(new PhabricatorRepositoryURI()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($uris as $uri) { $uri->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 canServeProtocol($protocol, $write) { if (!$this->isTracked()) { return false; } $clone_uris = $this->getCloneURIs(); foreach ($clone_uris as $uri) { if ($uri->getBuiltinProtocol() !== $protocol) { continue; } $io_type = $uri->getEffectiveIoType(); if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) { return true; } if (!$write) { if ($io_type == PhabricatorRepositoryURI::IO_READ) { return true; } } } return false; } public function hasLocalWorkingCopy() { try { self::assertLocalExists(); return true; } catch (Exception $ex) { return false; } } /** * 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 canUseGitLFS() { if (!$this->isGit()) { return false; } if (!$this->isHosted()) { return false; } if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) { return false; } return true; } public function getGitLFSURI($path = null) { if (!$this->canUseGitLFS()) { throw new Exception( pht( 'This repository does not support Git LFS, so Git LFS URIs can '. 'not be generated for it.')); } $uri = $this->getRawHTTPCloneURIObject(); $uri = (string)$uri; $uri = $uri.'/'.$path; return $uri; } public function canMirror() { if ($this->isGit() || $this->isHg()) { return true; } return false; } public function canAllowDangerousChanges() { if (!$this->isHosted()) { return false; } // In Git and Mercurial, ref deletions and rewrites are dangerous. // In Subversion, editing revprops is dangerous. return true; } public function shouldAllowDangerousChanges() { return (bool)$this->getDetail('allow-dangerous-changes'); } public function canAllowEnormousChanges() { if (!$this->isHosted()) { return false; } return true; } public function shouldAllowEnormousChanges() { return (bool)$this->getDetail('allow-enormous-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 { // If the existing message has the same code (e.g., we just hit an // error and also previously hit an error) we increment the message // count. This allows us to determine how many times in a row we've // run into an error. // NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated // in order, so the "messageCount" assignment must occur before the // "statusCode" assignment. See T11705. queryfx( $conn_w, 'INSERT INTO %T (repositoryID, statusType, statusCode, parameters, epoch, messageCount) VALUES (%d, %s, %s, %s, %d, %d) ON DUPLICATE KEY UPDATE messageCount = IF( statusCode = VALUES(statusCode), messageCount + VALUES(messageCount), VALUES(messageCount)), statusCode = VALUES(statusCode), parameters = VALUES(parameters), epoch = VALUES(epoch)', $table_name, $this->getID(), $status_type, $status_code, json_encode($parameters), time(), 1); } return $this; } public static function assertValidRemoteURI($uri) { if (trim($uri) != $uri) { throw new Exception( pht('The remote URI has leading or trailing whitespace.')); } $uri_object = new PhutilURI($uri); $protocol = $uri_object->getProtocol(); // 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 ". "'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.", 'proto://domain/path', 'proto://domain:/path', ':/path')); } } 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 with '. '"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".', 'ssh://', 'http://', 'https://', 'git://', 'svn://', 'svn+ssh://', '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) { // First, check if we've hit errors recently. If we have, wait one period // for each consecutive error. Normally, this corresponds to a backoff of // 15s, 30s, 45s, etc. $message_table = new PhabricatorRepositoryStatusMessage(); $conn = $message_table->establishConnection('r'); $error_count = queryfx_one( $conn, 'SELECT MAX(messageCount) error_count FROM %T WHERE repositoryID = %d AND statusType IN (%Ls) AND statusCode IN (%Ls)', $message_table->getTableName(), $this->getID(), array( PhabricatorRepositoryStatusMessage::TYPE_INIT, PhabricatorRepositoryStatusMessage::TYPE_FETCH, ), array( PhabricatorRepositoryStatusMessage::CODE_ERROR, )); $error_count = (int)$error_count['error_count']; if ($error_count > 0) { return (int)($minimum * $error_count); } // 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']); } else { // If the repository has no commits, treat the creation date as // though it were the date of the last commit. This makes empty // repositories update quickly at first but slow down over time // if they don't see any activity. $time_since_commit = ($window_start - $this->getDateCreated()); } $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); return (int)$smart_wait; } /** * Time limit for cloning or copying this repository. * * This limit is used to timeout operations like `git clone` or `git fetch` * when doing intracluster synchronization, building working copies, etc. * * @return int Maximum number of seconds to spend copying this repository. */ public function getCopyTimeLimit() { return $this->getDetail('limit.copy'); } public function setCopyTimeLimit($limit) { return $this->setDetail('limit.copy', $limit); } public function getDefaultCopyTimeLimit() { return phutil_units('15 minutes in seconds'); } public function getEffectiveCopyTimeLimit() { $limit = $this->getCopyTimeLimit(); if ($limit) { return $limit; } return $this->getDefaultCopyTimeLimit(); } public function getFilesizeLimit() { return $this->getDetail('limit.filesize'); } public function setFilesizeLimit($limit) { return $this->setDetail('limit.filesize', $limit); } public function getTouchLimit() { return $this->getDetail('limit.touch'); } public function setTouchLimit($limit) { return $this->setDetail('limit.touch', $limit); } /** * Retrieve the service URI for the device hosting this repository. * * See @{method:newConduitClient} for a general discussion of interacting * with repository services. This method provides lower-level resolution of * services, returning raw URIs. * * @param PhabricatorUser Viewing user. * @param map Constraints on selectable services. * @return string|null URI, or `null` for local repositories. */ public function getAlmanacServiceURI( PhabricatorUser $viewer, array $options) { PhutilTypeSpec::checkMap( $options, array( 'neverProxy' => 'bool', 'protocols' => 'list', 'writable' => 'optional bool', )); $never_proxy = $options['neverProxy']; $protocols = $options['protocols']; $writable = idx($options, 'writable', false); $cache_key = $this->getAlmanacServiceCacheKey(); if (!$cache_key) { return null; } $cache = PhabricatorCaches::getMutableStructureCache(); $uris = $cache->getKey($cache_key, false); // If we haven't built the cache yet, build it now. if ($uris === false) { $uris = $this->buildAlmanacServiceURIs(); $cache->setKey($cache_key, $uris); } if ($uris === null) { return null; } $local_device = AlmanacKeys::getDeviceID(); if ($never_proxy && !$local_device) { throw new Exception( pht( 'Unable to handle proxied service request. This device is not '. 'registered, so it can not identify local services. Register '. 'this device before sending requests here.')); } $protocol_map = array_fuse($protocols); $results = array(); foreach ($uris as $uri) { // If we're never proxying this and it's locally satisfiable, return // `null` to tell the caller to handle it locally. If we're allowed to // proxy, we skip this check and may proxy the request to ourselves. // (That proxied request will end up here with proxying forbidden, // return `null`, and then the request will actually run.) if ($local_device && $never_proxy) { if ($uri['device'] == $local_device) { return null; } } if (isset($protocol_map[$uri['protocol']])) { $results[] = $uri; } } if (!$results) { throw new Exception( pht( 'The Almanac service for this repository is not bound to any '. 'interfaces which support the required protocols (%s).', implode(', ', $protocols))); } if ($never_proxy) { throw new Exception( pht( 'Refusing to proxy a repository request from a cluster host. '. 'Cluster hosts must correctly route their intracluster requests.')); } if (count($results) > 1) { if (!$this->supportsSynchronization()) { throw new Exception( pht( 'Repository "%s" is bound to multiple active repository hosts, '. 'but this repository does not support cluster synchronization. '. 'Declusterize this repository or move it to a service with only '. 'one host.', $this->getDisplayName())); } } // If we require a writable device, remove URIs which aren't writable. if ($writable) { foreach ($results as $key => $uri) { if (!$uri['writable']) { unset($results[$key]); } } if (!$results) { throw new Exception( pht( 'This repository ("%s") is not writable with the given '. 'protocols (%s). The Almanac service for this repository has no '. 'writable bindings that support these protocols.', $this->getDisplayName(), implode(', ', $protocols))); } } if ($writable) { $results = $this->sortWritableAlmanacServiceURIs($results); } else { shuffle($results); } $result = head($results); return $result['uri']; } private function sortWritableAlmanacServiceURIs(array $results) { // See T13109 for discussion of how this method routes requests. // In the absence of other rules, we'll send traffic to devices randomly. // We also want to select randomly among nodes which are equally good // candidates to receive the write, and accomplish that by shuffling the // list up front. shuffle($results); $order = array(); // If some device is currently holding the write lock, send all requests // to that device. We're trying to queue writes on a single device so they // do not need to wait for read synchronization after earlier writes // complete. $writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter( $this->getPHID()); if ($writer) { $device_phid = $writer->getWriteProperty('devicePHID'); foreach ($results as $key => $result) { if ($result['devicePHID'] === $device_phid) { $order[] = $key; } } } // If no device is currently holding the write lock, try to send requests // to a device which is already up to date and will not need to synchronize // before it can accept the write. $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $this->getPHID()); if ($versions) { $max_version = (int)max(mpull($versions, 'getRepositoryVersion')); $max_devices = array(); foreach ($versions as $version) { if ($version->getRepositoryVersion() == $max_version) { $max_devices[] = $version->getDevicePHID(); } } $max_devices = array_fuse($max_devices); foreach ($results as $key => $result) { if (isset($max_devices[$result['devicePHID']])) { $order[] = $key; } } } // Reorder the results, putting any we've selected as preferred targets for // the write at the head of the list. $results = array_select_keys($results, $order) + $results; return $results; } public function supportsSynchronization() { // TODO: For now, this is only supported for Git. if (!$this->isGit()) { return false; } return true; } public function getAlmanacServiceCacheKey() { $service_phid = $this->getAlmanacServicePHID(); if (!$service_phid) { return null; } $repository_phid = $this->getPHID(); $parts = array( "repo({$repository_phid})", "serv({$service_phid})", 'v3', ); return implode('.', $parts); } private function buildAlmanacServiceURIs() { $service = $this->loadAlmanacService(); if (!$service) { return null; } $bindings = $service->getActiveBindings(); if (!$bindings) { throw new Exception( pht( 'The Almanac service for this repository is not bound to any '. 'interfaces.')); } $uris = array(); foreach ($bindings as $binding) { $iface = $binding->getInterface(); $uri = $this->getClusterRepositoryURIFromBinding($binding); $protocol = $uri->getProtocol(); $device_name = $iface->getDevice()->getName(); $device_phid = $iface->getDevice()->getPHID(); $uris[] = array( 'protocol' => $protocol, 'uri' => (string)$uri, 'device' => $device_name, 'writable' => (bool)$binding->getAlmanacPropertyValue('writable'), 'devicePHID' => $device_phid, ); } return $uris; } /** * Build a new Conduit client in order to make a service call to this * repository. * * If the repository is hosted locally, this method may return `null`. The * caller should use `ConduitCall` or other local logic to complete the * request. * * By default, we will return a @{class:ConduitClient} for any repository with * a service, even if that service is on the current device. * * We do this because this configuration does not make very much sense in a * production context, but is very common in a test/development context * (where the developer's machine is both the web host and the repository * service). By proxying in development, we get more consistent behavior * between development and production, and don't have a major untested * codepath. * * The `$never_proxy` parameter can be used to prevent this local proxying. * If the flag is passed: * * - The method will return `null` (implying a local service call) * if the repository service is hosted on the current device. * - The method will throw if it would need to return a client. * * This is used to prevent loops in Conduit: the first request will proxy, * even in development, but the second request will be identified as a * cluster request and forced not to proxy. * * For lower-level service resolution, see @{method:getAlmanacServiceURI}. * * @param PhabricatorUser Viewing user. * @param bool `true` to throw if a client would be returned. * @return ConduitClient|null Client, or `null` for local repositories. */ public function newConduitClient( PhabricatorUser $viewer, $never_proxy = false) { $uri = $this->getAlmanacServiceURI( $viewer, array( 'neverProxy' => $never_proxy, 'protocols' => array( 'http', 'https', ), // At least today, no Conduit call can ever write to a repository, // so it's fine to send anything to a read-only node. 'writable' => false, )); if ($uri === null) { return null; } $domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain(); $client = id(new ConduitClient($uri)) ->setHost($domain); if ($viewer->isOmnipotent()) { // If the caller is the omnipotent user (normally, a daemon), we will // sign the request with this host's asymmetric keypair. $public_path = AlmanacKeys::getKeyPath('device.pub'); try { $public_key = Filesystem::readFile($public_path); } catch (Exception $ex) { throw new PhutilAggregateException( pht( 'Unable to read device public key while attempting to make '. 'authenticated method call within the Phabricator cluster. '. 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $private_path = AlmanacKeys::getKeyPath('device.key'); try { $private_key = Filesystem::readFile($private_path); $private_key = new PhutilOpaqueEnvelope($private_key); } catch (Exception $ex) { throw new PhutilAggregateException( pht( 'Unable to read device private key while attempting to make '. 'authenticated method call within the Phabricator cluster. '. 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $client->setSigningKeys($public_key, $private_key); } else { // If the caller is a normal user, we generate or retrieve a cluster // API token. $token = PhabricatorConduitToken::loadClusterTokenForUser($viewer); if ($token) { $client->setConduitToken($token->getToken()); } } return $client; } public function getPassthroughEnvironmentalVariables() { $env = $_ENV; if ($this->isGit()) { // $_ENV does not populate in CLI contexts if "E" is missing from // "variables_order" in PHP config. Currently, we do not require this // to be configured. Since it may not be, explicitly bring expected Git // environmental variables into scope. This list is not exhaustive, but // only lists variables with a known impact on commit hook behavior. // This can be removed if we later require "E" in "variables_order". $git_env = array( 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_QUARANTINE_PATH', ); foreach ($git_env as $key) { $value = getenv($key); if (strlen($value)) { $env[$key] = $value; } } $key = 'GIT_PUSH_OPTION_COUNT'; $git_count = getenv($key); if (strlen($git_count)) { $git_count = (int)$git_count; $env[$key] = $git_count; for ($ii = 0; $ii < $git_count; $ii++) { $key = 'GIT_PUSH_OPTION_'.$ii; $env[$key] = getenv($key); } } } $result = array(); foreach ($env as $key => $value) { // In Git, pass anything matching "GIT_*" though. Some of these variables // need to be preserved to allow `git` operations to work properly when // running from commit hooks. if ($this->isGit()) { if (preg_match('/^GIT_/', $key)) { $result[$key] = $value; } } } return $result; } public function supportsBranchComparison() { return $this->isGit(); } /* -( Repository URIs )---------------------------------------------------- */ public function attachURIs(array $uris) { $custom_map = array(); foreach ($uris as $key => $uri) { $builtin_key = $uri->getRepositoryURIBuiltinKey(); if ($builtin_key !== null) { $custom_map[$builtin_key] = $key; } } $builtin_uris = $this->newBuiltinURIs(); $seen_builtins = array(); foreach ($builtin_uris as $builtin_uri) { $builtin_key = $builtin_uri->getRepositoryURIBuiltinKey(); $seen_builtins[$builtin_key] = true; // If this builtin URI is disabled, don't attach it and remove the // persisted version if it exists. if ($builtin_uri->getIsDisabled()) { if (isset($custom_map[$builtin_key])) { unset($uris[$custom_map[$builtin_key]]); } continue; } // If the URI exists, make sure it's marked as not being disabled. if (isset($custom_map[$builtin_key])) { $uris[$custom_map[$builtin_key]]->setIsDisabled(false); } } // Remove any builtins which no longer exist. foreach ($custom_map as $builtin_key => $key) { if (empty($seen_builtins[$builtin_key])) { unset($uris[$key]); } } $this->uris = $uris; return $this; } public function getURIs() { return $this->assertAttached($this->uris); } public function getCloneURIs() { $uris = $this->getURIs(); $clone = array(); foreach ($uris as $uri) { if (!$uri->isBuiltin()) { continue; } if ($uri->getIsDisabled()) { continue; } $io_type = $uri->getEffectiveIoType(); $is_clone = ($io_type == PhabricatorRepositoryURI::IO_READ) || ($io_type == PhabricatorRepositoryURI::IO_READWRITE); if (!$is_clone) { continue; } $clone[] = $uri; } $clone = msort($clone, 'getURIScore'); $clone = array_reverse($clone); return $clone; } public function newBuiltinURIs() { $has_callsign = ($this->getCallsign() !== null); $has_shortname = ($this->getRepositorySlug() !== null); $identifier_map = array( PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign, PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname, PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true, ); // If the view policy of the repository is public, support anonymous HTTP // even if authenticated HTTP is not supported. if ($this->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC) { $allow_http = true; } else { $allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); } $base_uri = PhabricatorEnv::getURI('/'); $base_uri = new PhutilURI($base_uri); $has_https = ($base_uri->getProtocol() == 'https'); $has_https = ($has_https && $allow_http); $has_http = !PhabricatorEnv::getEnvConfig('security.require-https'); $has_http = ($has_http && $allow_http); // HTTP is not supported for Subversion. if ($this->isSVN()) { $has_http = false; $has_https = false; } $has_ssh = (bool)strlen(PhabricatorEnv::getEnvConfig('phd.user')); $protocol_map = array( PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh, PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https, PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http, ); $uris = array(); foreach ($protocol_map as $protocol => $proto_supported) { foreach ($identifier_map as $identifier => $id_supported) { // This is just a dummy value because it can't be empty; we'll force // it to a proper value when using it in the UI. $builtin_uri = "{$protocol}://{$identifier}"; $uris[] = PhabricatorRepositoryURI::initializeNewURI() ->setRepositoryPHID($this->getPHID()) ->attachRepository($this) ->setBuiltinProtocol($protocol) ->setBuiltinIdentifier($identifier) ->setURI($builtin_uri) ->setIsDisabled((int)(!$proto_supported || !$id_supported)); } } return $uris; } public function getClusterRepositoryURIFromBinding( AlmanacBinding $binding) { $protocol = $binding->getAlmanacPropertyValue('protocol'); if ($protocol === null) { $protocol = 'https'; } $iface = $binding->getInterface(); $address = $iface->renderDisplayAddress(); $path = $this->getURI(); return id(new PhutilURI("{$protocol}://{$address}")) ->setPath($path); } public function loadAlmanacService() { $service_phid = $this->getAlmanacServicePHID(); if (!$service_phid) { // No service, so this is a local repository. return null; } $service = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($service_phid)) ->needBindings(true) ->needProperties(true) ->executeOne(); if (!$service) { throw new Exception( pht( 'The Almanac service for this repository is invalid or could not '. 'be loaded.')); } $service_type = $service->getServiceImplementation(); if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) { throw new Exception( pht( 'The Almanac service for this repository does not have the correct '. 'service type.')); } return $service; } public function markImporting() { $this->openTransaction(); $this->beginReadLocking(); $repository = $this->reload(); $repository->setDetail('importing', true); $repository->save(); $this->endReadLocking(); $this->saveTransaction(); return $repository; } /* -( Symbols )-------------------------------------------------------------*/ public function getSymbolSources() { return $this->getDetail('symbol-sources', array()); } public function getSymbolLanguages() { return $this->getDetail('symbol-languages', array()); } /* -( Staging )------------------------------------------------------------ */ public function supportsStaging() { return $this->isGit(); } public function getStagingURI() { if (!$this->supportsStaging()) { return null; } return $this->getDetail('staging-uri', null); } /* -( Automation )--------------------------------------------------------- */ public function supportsAutomation() { return $this->isGit(); } public function canPerformAutomation() { if (!$this->supportsAutomation()) { return false; } if (!$this->getAutomationBlueprintPHIDs()) { return false; } return true; } public function getAutomationBlueprintPHIDs() { if (!$this->supportsAutomation()) { return array(); } return $this->getDetail('automation.blueprintPHIDs', array()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorRepositoryEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryTransaction(); } /* -( 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; } /* -( 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) { $phid = $this->getPHID(); $this->openTransaction(); $this->delete(); PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array()); $books = id(new DivinerBookQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($books as $book) { $engine->destroyObject($book); } $atoms = id(new DivinerAtomQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($atoms as $atom) { $engine->destroyObject($atom); } $lfs_refs = id(new PhabricatorRepositoryGitLFSRefQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($lfs_refs as $ref) { $engine->destroyObject($ref); } $this->saveTransaction(); } /* -( PhabricatorDestructibleCodexInterface )------------------------------ */ public function newDestructibleCodex() { return new PhabricatorRepositoryDestructibleCodex(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The repository name.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('vcs') ->setType('string') ->setDescription( pht('The VCS this repository uses ("git", "hg" or "svn").')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('callsign') ->setType('string') ->setDescription(pht('The repository callsign, if it has one.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('shortName') ->setType('string') ->setDescription(pht('Unique short name, if the repository has one.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Active or inactive status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isImporting') ->setType('bool') ->setDescription( pht( 'True if the repository is importing initial commits.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('almanacServicePHID') ->setType('phid?') ->setDescription( pht( 'The Almanac Service that hosts this repository, if the '. 'repository is clustered.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'vcs' => $this->getVersionControlSystem(), 'callsign' => $this->getCallsign(), 'shortName' => $this->getRepositorySlug(), 'status' => $this->getStatus(), 'isImporting' => (bool)$this->isImporting(), 'almanacServicePHID' => $this->getAlmanacServicePHID(), ); } public function getConduitSearchAttachments() { return array( id(new DiffusionRepositoryURIsSearchEngineAttachment()) ->setAttachmentKey('uris'), ); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorRepositoryFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhabricatorRepositoryFerretEngine(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index ddc4dbbf9f..fecb1762bd 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -1,901 +1,897 @@ 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 isUnreachable() { return $this->isPartiallyImported(self::IMPORTED_UNREACHABLE); } public function writeImportStatusFlag($flag) { return $this->adjustImportStatusFlag($flag, true); } public function clearImportStatusFlag($flag) { return $this->adjustImportStatusFlag($flag, false); } private function adjustImportStatusFlag($flag, $set) { $conn_w = $this->establishConnection('w'); $table_name = $this->getTableName(); $id = $this->getID(); if ($set) { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', $table_name, $flag, $id); $this->setImportStatus($this->getImportStatus() | $flag); } else { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus & ~%d) WHERE id = %d', $table_name, $flag, $id); $this->setImportStatus($this->getImportStatus() & ~$flag); } return $this; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'commitIdentifier' => 'text40', 'authorPHID' => 'phid?', 'authorIdentityPHID' => 'phid?', 'committerIdentityPHID' => 'phid?', 'auditStatus' => 'text32', 'summary' => 'text255', '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, ), 'key_epoch' => array( 'columns' => array('epoch'), ), 'key_author' => array( 'columns' => array('authorPHID', 'epoch'), ), ), self::CONFIG_NO_MUTATE => array( 'importStatus', ), ) + 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 hasAttachedAudits() { return ($this->audits !== self::ATTACHABLE); } public function attachIdentities( PhabricatorRepositoryIdentity $author = null, PhabricatorRepositoryIdentity $committer = null) { $this->authorIdentity = $author; $this->committerIdentity = $committer; return $this; } public function getAuthorIdentity() { return $this->assertAttached($this->authorIdentity); } public function getCommitterIdentity() { return $this->assertAttached($this->committerIdentity); } public function attachAuditAuthority( PhabricatorUser $user, array $authority) { $user_phid = $user->getPHID(); if (!$user->getPHID()) { throw new Exception( pht('You can not attach audit authority for a user with no PHID.')); } $this->auditAuthorityPHIDs[$user_phid] = $authority; return $this; } public function hasAuditAuthority( PhabricatorUser $user, PhabricatorRepositoryAuditRequest $audit) { $user_phid = $user->getPHID(); if (!$user_phid) { return false; } $map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $user_phid); return isset($map[$audit->getAuditorPHID()]); } public function writeOwnersEdges(array $package_phids) { $src_phid = $this->getPHID(); $edge_type = DiffusionCommitHasPackageEdgeType::EDGECONST; $editor = new PhabricatorEdgeEditor(); $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $src_phid, $edge_type); foreach ($dst_phids as $dst_phid) { $editor->removeEdge($src_phid, $edge_type, $dst_phid); } foreach ($package_phids as $package_phid) { $editor->addEdge($src_phid, $edge_type, $package_phid); } $editor->save(); return $this; } public function getAuditorPHIDsForEdit() { $audits = $this->getAudits(); return mpull($audits, 'getAuditorPHID'); } 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() { return '/'.$this->getMonogram(); } /** * 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: case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: $any_need = true; break; case PhabricatorAuditStatusConstants::ACCEPTED: $any_accept = true; break; case PhabricatorAuditStatusConstants::CONCERNED: $any_concern = true; break; } } if ($any_concern) { if ($this->isAuditStatusNeedsVerification()) { // If the change is in "Needs Verification", we keep it there as // long as any auditors still have concerns. $status = DiffusionCommitAuditStatus::NEEDS_VERIFICATION; } else { $status = DiffusionCommitAuditStatus::CONCERN_RAISED; } } else if ($any_accept) { if ($any_need) { $status = DiffusionCommitAuditStatus::PARTIALLY_AUDITED; } else { $status = DiffusionCommitAuditStatus::AUDITED; } } else if ($any_need) { $status = DiffusionCommitAuditStatus::NEEDS_AUDIT; } else { $status = DiffusionCommitAuditStatus::NONE; } return $this->setAuditStatus($status); } public function getMonogram() { $repository = $this->getRepository(); $callsign = $repository->getCallsign(); $identifier = $this->getCommitIdentifier(); if ($callsign !== null) { return "r{$callsign}{$identifier}"; } else { $id = $repository->getID(); return "R{$id}:{$identifier}"; } } public function getDisplayName() { $repository = $this->getRepository(); $identifier = $this->getCommitIdentifier(); return $repository->formatCommitName($identifier); } /** * Return a local display name for use in the context of the containing * repository. * * In Git and Mercurial, this returns only a short hash, like "abcdef012345". * See @{method:getDisplayName} for a short name that always includes * repository context. * * @return string Short human-readable name for use inside a repository. */ public function getLocalName() { $repository = $this->getRepository(); $identifier = $this->getCommitIdentifier(); return $repository->formatCommitName($identifier, $local = true); } /** * Make a strong effort to find a way to render this commit's committer. * This currently attempts to use @{PhabricatorRepositoryIdentity}, and * falls back to examining the commit detail information. After we force * the migration to using identities, update this method to remove the * fallback. See T12164 for details. */ public function renderAnyCommitter(PhabricatorUser $viewer, $handles) { $committer = $this->renderCommitter($viewer, $handles); if ($committer) { return $committer; } return $this->renderAuthor($viewer, $handles); } public function renderCommitter(PhabricatorUser $viewer, $handles) { $committer_phid = $this->getCommitterDisplayPHID(); if ($committer_phid) { return $handles[$committer_phid]->renderLink(); } $data = $this->getCommitData(); $committer_name = $data->getCommitDetail('committer'); if (strlen($committer_name)) { return DiffusionView::renderName($committer_name); } return null; } public function renderAuthor(PhabricatorUser $viewer, $handles) { $author_phid = $this->getAuthorDisplayPHID(); if ($author_phid) { return $handles[$author_phid]->renderLink(); } $data = $this->getCommitData(); $author_name = $data->getAuthorName(); if (strlen($author_name)) { return DiffusionView::renderName($author_name); } return null; } public function loadIdentities(PhabricatorUser $viewer) { if ($this->authorIdentity !== self::ATTACHABLE) { return $this; } $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIDs(array($this->getID())) ->needIdentities(true) ->executeOne(); $author_identity = $commit->getAuthorIdentity(); $committer_identity = $commit->getCommitterIdentity(); return $this->attachIdentities($author_identity, $committer_identity); } public function hasCommitterIdentity() { return ($this->getCommitterIdentity() !== null); } public function hasAuthorIdentity() { return ($this->getAuthorIdentity() !== null); } public function getCommitterDisplayPHID() { if ($this->hasCommitterIdentity()) { return $this->getCommitterIdentity()->getIdentityDisplayPHID(); } $data = $this->getCommitData(); return $data->getCommitDetail('committerPHID'); } public function getAuthorDisplayPHID() { if ($this->hasAuthorIdentity()) { return $this->getAuthorIdentity()->getIdentityDisplayPHID(); } $data = $this->getCommitData(); return $data->getCommitDetail('authorPHID'); } public function getAuditStatusObject() { $status = $this->getAuditStatus(); return DiffusionCommitAuditStatus::newForStatus($status); } public function isAuditStatusNoAudit() { return $this->getAuditStatusObject()->isNoAudit(); } public function isAuditStatusNeedsAudit() { return $this->getAuditStatusObject()->isNeedsAudit(); } public function isAuditStatusConcernRaised() { return $this->getAuditStatusObject()->isConcernRaised(); } public function isAuditStatusNeedsVerification() { return $this->getAuditStatusObject()->isNeedsVerification(); } public function isAuditStatusPartiallyAudited() { return $this->getAuditStatusObject()->isPartiallyAudited(); } public function isAuditStatusAudited() { return $this->getAuditStatusObject()->isAudited(); } /* -( 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: 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(), '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 getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterBuildablePHID(); } 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.phid'] = $repo->getPHID(); $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.phid' => pht('The PHID 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.'), ); } public function newBuildableEngine() { return new DiffusionBuildableEngine(); } /* -( HarbormasterCircleCIBuildableInterface )----------------------------- */ public function getCircleCIGitHubRepositoryURI() { $repository = $this->getRepository(); $commit_phid = $this->getPHID(); $repository_phid = $repository->getPHID(); if ($repository->isHosted()) { throw new Exception( pht( 'This commit ("%s") is associated with a hosted repository '. '("%s"). Repositories must be imported from GitHub to be built '. 'with CircleCI.', $commit_phid, $repository_phid)); } $remote_uri = $repository->getRemoteURI(); $path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath( $remote_uri); if (!$path) { throw new Exception( pht( 'This commit ("%s") is associated with a repository ("%s") that '. 'with a remote URI ("%s") that does not appear to be hosted on '. 'GitHub. Repositories must be hosted on GitHub to be built with '. 'CircleCI.', $commit_phid, $repository_phid, $remote_uri)); } return $remote_uri; } public function getCircleCIBuildIdentifierType() { return 'revision'; } public function getCircleCIBuildIdentifier() { return $this->getCommitIdentifier(); } /* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */ public function getBuildkiteBranch() { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = $this->getRepository(); $branches = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $repository, 'user' => $viewer, )), 'diffusion.branchquery', array( 'contains' => $this->getCommitIdentifier(), 'repository' => $repository->getPHID(), )); if (!$branches) { throw new Exception( pht( 'Commit "%s" is not an ancestor of any branch head, so it can not '. 'be built with Buildkite.', $this->getCommitIdentifier())); } $branch = head($branches); return 'refs/heads/'.$branch['shortName']; } public function getBuildkiteCommit() { return $this->getCommitIdentifier(); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('diffusion.fields'); } 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. (It should not // include resigned auditors.) return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuditEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorAuditTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DiffusionCommitFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new DiffusionCommitFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('identifier') ->setType('string') ->setDescription(pht('The commit identifier.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid') ->setDescription(pht('The repository this commit belongs to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('author') ->setType('map') ->setDescription(pht('Information about the commit author.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('committer') ->setType('map') ->setDescription(pht('Information about the committer.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isImported') ->setType('bool') ->setDescription(pht('True if the commit is fully imported.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isUnreachable') ->setType('bool') ->setDescription( pht( 'True if the commit is not the ancestor of any tag, branch, or '. 'ref.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('auditStatus') ->setType('map') ->setDescription(pht('Information about the current audit status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('message') ->setType('string') ->setDescription(pht('The commit message.')), ); } public function getFieldValuesForConduit() { $data = $this->getCommitData(); $author_identity = $this->getAuthorIdentity(); if ($author_identity) { $author_name = $author_identity->getIdentityDisplayName(); $author_email = $author_identity->getIdentityEmailAddress(); $author_raw = $author_identity->getIdentityName(); $author_identity_phid = $author_identity->getPHID(); $author_user_phid = $author_identity->getCurrentEffectiveUserPHID(); } else { $author_name = null; $author_email = null; $author_raw = null; $author_identity_phid = null; $author_user_phid = null; } $committer_identity = $this->getCommitterIdentity(); if ($committer_identity) { $committer_name = $committer_identity->getIdentityDisplayName(); $committer_email = $committer_identity->getIdentityEmailAddress(); $committer_raw = $committer_identity->getIdentityName(); $committer_identity_phid = $committer_identity->getPHID(); $committer_user_phid = $committer_identity->getCurrentEffectiveUserPHID(); } else { $committer_name = null; $committer_email = null; $committer_raw = null; $committer_identity_phid = null; $committer_user_phid = null; } $author_epoch = $data->getCommitDetail('authorEpoch'); if ($author_epoch) { $author_epoch = (int)$author_epoch; } else { $author_epoch = null; } $audit_status = $this->getAuditStatusObject(); return array( 'identifier' => $this->getCommitIdentifier(), 'repositoryPHID' => $this->getRepository()->getPHID(), 'author' => array( 'name' => $author_name, 'email' => $author_email, 'raw' => $author_raw, 'epoch' => $author_epoch, 'identityPHID' => $author_identity_phid, 'userPHID' => $author_user_phid, ), 'committer' => array( 'name' => $committer_name, 'email' => $committer_email, 'raw' => $committer_raw, 'epoch' => (int)$this->getEpoch(), 'identityPHID' => $committer_identity_phid, 'userPHID' => $committer_user_phid, ), 'isUnreachable' => (bool)$this->isUnreachable(), 'isImported' => (bool)$this->isImported(), 'auditStatus' => array( 'value' => $audit_status->getKey(), 'name' => $audit_status->getName(), 'closed' => (bool)$audit_status->getIsClosed(), 'color.ansi' => $audit_status->getAnsiColor(), ), 'message' => $data->getCommitMessage(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DiffusionCommitDraftEngine(); } public function getHasDraft(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment()); } public function attachHasDraft(PhabricatorUser $viewer, $has_draft) { $this->drafts[$viewer->getCacheFragment()] = $has_draft; return $this; } /* -( PhabricatorTimelineInterface )--------------------------------------- */ public function newTimelineEngine() { return new DiffusionCommitTimelineEngine(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index d361ccda40..76c6aed9e0 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -1,145 +1,141 @@ effectiveUser = $user; return $this; } public function getEffectiveUser() { return $this->assertAttached($this->effectiveUser); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_BINARY => array( 'identityNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'identityNameHash' => 'bytes12', 'identityNameEncoding' => 'text16?', 'automaticGuessedUserPHID' => 'phid?', 'manuallySetUserPHID' => 'phid?', 'currentEffectiveUserPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_identity' => array( 'columns' => array('identityNameHash'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhabricatorRepositoryIdentityPHIDType::TYPECONST; } public function setIdentityName($name_raw) { $this->setIdentityNameRaw($name_raw); $this->setIdentityNameHash(PhabricatorHash::digestForIndex($name_raw)); $this->setIdentityNameEncoding($this->detectEncodingForStorage($name_raw)); return $this; } public function getIdentityName() { return $this->getUTF8StringFromStorage( $this->getIdentityNameRaw(), $this->getIdentityNameEncoding()); } public function getIdentityEmailAddress() { $address = new PhutilEmailAddress($this->getIdentityName()); return $address->getAddress(); } public function getIdentityDisplayName() { $address = new PhutilEmailAddress($this->getIdentityName()); return $address->getDisplayName(); } public function getIdentityShortName() { // TODO return $this->getIdentityName(); } public function getURI() { return '/diffusion/identity/view/'.$this->getID().'/'; } public function hasEffectiveUser() { return ($this->currentEffectiveUserPHID != null); } public function getIdentityDisplayPHID() { if ($this->hasEffectiveUser()) { return $this->getCurrentEffectiveUserPHID(); } else { return $this->getPHID(); } } public function save() { if ($this->manuallySetUserPHID) { $this->currentEffectiveUserPHID = $this->manuallySetUserPHID; } else { $this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID; } return parent::save(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability( $capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DiffusionRepositoryIdentityEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryIdentityTransaction(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryURI.php b/src/applications/repository/storage/PhabricatorRepositoryURI.php index cc0f28b828..8b1de62dd6 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryURI.php +++ b/src/applications/repository/storage/PhabricatorRepositoryURI.php @@ -1,742 +1,738 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'uri' => 'text255', 'builtinProtocol' => 'text32?', 'builtinIdentifier' => 'text32?', 'credentialPHID' => 'phid?', 'ioType' => 'text32', 'displayType' => 'text32', 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_builtin' => array( 'columns' => array( 'repositoryPHID', 'builtinProtocol', 'builtinIdentifier', ), 'unique' => true, ), ), ) + parent::getConfiguration(); } public static function initializeNewURI() { return id(new self()) ->setIoType(self::IO_DEFAULT) ->setDisplayType(self::DISPLAY_DEFAULT) ->setIsDisabled(0); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryURIPHIDType::TYPECONST); } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function getRepositoryURIBuiltinKey() { if (!$this->getBuiltinProtocol()) { return null; } $parts = array( $this->getBuiltinProtocol(), $this->getBuiltinIdentifier(), ); return implode('.', $parts); } public function isBuiltin() { return (bool)$this->getBuiltinProtocol(); } public function getEffectiveDisplayType() { $display = $this->getDisplayType(); if ($display != self::DISPLAY_DEFAULT) { return $display; } return $this->getDefaultDisplayType(); } public function getDefaultDisplayType() { switch ($this->getEffectiveIOType()) { case self::IO_MIRROR: case self::IO_OBSERVE: case self::IO_NONE: return self::DISPLAY_NEVER; case self::IO_READ: case self::IO_READWRITE: // By default, only show the "best" version of the builtin URI, not the // other redundant versions. $repository = $this->getRepository(); $other_uris = $repository->getURIs(); $identifier_value = array( self::BUILTIN_IDENTIFIER_SHORTNAME => 3, self::BUILTIN_IDENTIFIER_CALLSIGN => 2, self::BUILTIN_IDENTIFIER_ID => 1, ); $have_identifiers = array(); foreach ($other_uris as $other_uri) { if ($other_uri->getIsDisabled()) { continue; } $identifier = $other_uri->getBuiltinIdentifier(); if (!$identifier) { continue; } $have_identifiers[$identifier] = $identifier_value[$identifier]; } $best_identifier = max($have_identifiers); $this_identifier = $identifier_value[$this->getBuiltinIdentifier()]; if ($this_identifier < $best_identifier) { return self::DISPLAY_NEVER; } return self::DISPLAY_ALWAYS; } return self::DISPLAY_NEVER; } public function getEffectiveIOType() { $io = $this->getIoType(); if ($io != self::IO_DEFAULT) { return $io; } return $this->getDefaultIOType(); } public function getDefaultIOType() { if ($this->isBuiltin()) { $repository = $this->getRepository(); $other_uris = $repository->getURIs(); $any_observe = false; foreach ($other_uris as $other_uri) { if ($other_uri->getIoType() == self::IO_OBSERVE) { $any_observe = true; break; } } if ($any_observe) { return self::IO_READ; } else { return self::IO_READWRITE; } } return self::IO_NONE; } public function getNormalizedURI() { $vcs = $this->getRepository()->getVersionControlSystem(); $map = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => PhabricatorRepositoryURINormalizer::TYPE_GIT, PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => PhabricatorRepositoryURINormalizer::TYPE_SVN, PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL, ); $type = $map[$vcs]; $display = (string)$this->getDisplayURI(); $normal_uri = new PhabricatorRepositoryURINormalizer($type, $display); return $normal_uri->getNormalizedURI(); } public function getDisplayURI() { return $this->getURIObject(); } public function getEffectiveURI() { return $this->getURIObject(); } public function getURIEnvelope() { $uri = $this->getEffectiveURI(); $command_engine = $this->newCommandEngine(); $is_http = $command_engine->isAnyHTTPProtocol(); // For SVN, we use `--username` and `--password` flags separately in the // CommandEngine, so we don't need to add any credentials here. $is_svn = $this->getRepository()->isSVN(); $credential_phid = $this->getCredentialPHID(); if ($is_http && !$is_svn && $credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, PhabricatorUser::getOmnipotentUser()); $uri->setUser($key->getUsernameEnvelope()->openEnvelope()); $uri->setPass($key->getPasswordEnvelope()->openEnvelope()); } return new PhutilOpaqueEnvelope((string)$uri); } private function getURIObject() { // Users can provide Git/SCP-style URIs in the form "user@host:path". // In the general case, these are not equivalent to any "ssh://..." form // because the path is relative. if ($this->isBuiltin()) { $builtin_protocol = $this->getForcedProtocol(); $builtin_domain = $this->getForcedHost(); $raw_uri = "{$builtin_protocol}://{$builtin_domain}"; } else { $raw_uri = $this->getURI(); } $port = $this->getForcedPort(); $default_ports = array( 'ssh' => 22, 'http' => 80, 'https' => 443, ); $uri = new PhutilURI($raw_uri); // Make sure to remove any password from the URI before we do anything // with it; this should always be provided by the associated credential. $uri->setPass(null); $protocol = $this->getForcedProtocol(); if ($protocol) { $uri->setProtocol($protocol); } if ($port) { $uri->setPort($port); } // Remove any explicitly set default ports. $uri_port = $uri->getPort(); $uri_protocol = $uri->getProtocol(); $uri_default = idx($default_ports, $uri_protocol); if ($uri_default && ($uri_default == $uri_port)) { $uri->setPort(null); } $user = $this->getForcedUser(); if ($user) { $uri->setUser($user); } $host = $this->getForcedHost(); if ($host) { $uri->setDomain($host); } $path = $this->getForcedPath(); if ($path) { $uri->setPath($path); } return $uri; } private function getForcedProtocol() { $repository = $this->getRepository(); switch ($this->getBuiltinProtocol()) { case self::BUILTIN_PROTOCOL_SSH: if ($repository->isSVN()) { return 'svn+ssh'; } else { return 'ssh'; } case self::BUILTIN_PROTOCOL_HTTP: return 'http'; case self::BUILTIN_PROTOCOL_HTTPS: return 'https'; default: return null; } } private function getForcedUser() { switch ($this->getBuiltinProtocol()) { case self::BUILTIN_PROTOCOL_SSH: return AlmanacKeys::getClusterSSHUser(); default: return null; } } private function getForcedHost() { $phabricator_uri = PhabricatorEnv::getURI('/'); $phabricator_uri = new PhutilURI($phabricator_uri); $phabricator_host = $phabricator_uri->getDomain(); switch ($this->getBuiltinProtocol()) { case self::BUILTIN_PROTOCOL_SSH: $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host'); if ($ssh_host !== null) { return $ssh_host; } return $phabricator_host; case self::BUILTIN_PROTOCOL_HTTP: case self::BUILTIN_PROTOCOL_HTTPS: return $phabricator_host; default: return null; } } private function getForcedPort() { $protocol = $this->getBuiltinProtocol(); if ($protocol == self::BUILTIN_PROTOCOL_SSH) { return PhabricatorEnv::getEnvConfig('diffusion.ssh-port'); } // If Phabricator is running on a nonstandard port, use that as the default // port for URIs with the same protocol. $is_http = ($protocol == self::BUILTIN_PROTOCOL_HTTP); $is_https = ($protocol == self::BUILTIN_PROTOCOL_HTTPS); if ($is_http || $is_https) { $uri = PhabricatorEnv::getURI('/'); $uri = new PhutilURI($uri); $port = $uri->getPort(); if (!$port) { return null; } $uri_protocol = $uri->getProtocol(); $use_port = ($is_http && ($uri_protocol == 'http')) || ($is_https && ($uri_protocol == 'https')); if (!$use_port) { return null; } return $port; } return null; } private function getForcedPath() { if (!$this->isBuiltin()) { return null; } $repository = $this->getRepository(); $id = $repository->getID(); $callsign = $repository->getCallsign(); $short_name = $repository->getRepositorySlug(); $clone_name = $repository->getCloneName(); if ($repository->isGit()) { $suffix = '.git'; } else if ($repository->isHg()) { $suffix = '/'; } else { $suffix = ''; $clone_name = ''; } switch ($this->getBuiltinIdentifier()) { case self::BUILTIN_IDENTIFIER_ID: return "/diffusion/{$id}/{$clone_name}{$suffix}"; case self::BUILTIN_IDENTIFIER_SHORTNAME: return "/source/{$short_name}{$suffix}"; case self::BUILTIN_IDENTIFIER_CALLSIGN: return "/diffusion/{$callsign}/{$clone_name}{$suffix}"; default: return null; } } public function getViewURI() { $id = $this->getID(); return $this->getRepository()->getPathURI("uri/view/{$id}/"); } public function getEditURI() { $id = $this->getID(); return $this->getRepository()->getPathURI("uri/edit/{$id}/"); } public function getAvailableIOTypeOptions() { $options = array( self::IO_DEFAULT, self::IO_NONE, ); if ($this->isBuiltin()) { $options[] = self::IO_READ; $options[] = self::IO_READWRITE; } else { $options[] = self::IO_OBSERVE; $options[] = self::IO_MIRROR; } $map = array(); $io_map = self::getIOTypeMap(); foreach ($options as $option) { $spec = idx($io_map, $option, array()); $label = idx($spec, 'label', $option); $short = idx($spec, 'short'); $name = pht('%s: %s', $label, $short); $map[$option] = $name; } return $map; } public function getAvailableDisplayTypeOptions() { $options = array( self::DISPLAY_DEFAULT, self::DISPLAY_ALWAYS, self::DISPLAY_NEVER, ); $map = array(); $display_map = self::getDisplayTypeMap(); foreach ($options as $option) { $spec = idx($display_map, $option, array()); $label = idx($spec, 'label', $option); $short = idx($spec, 'short'); $name = pht('%s: %s', $label, $short); $map[$option] = $name; } return $map; } public static function getIOTypeMap() { return array( self::IO_DEFAULT => array( 'label' => pht('Default'), 'short' => pht('Use default behavior.'), ), self::IO_OBSERVE => array( 'icon' => 'fa-download', 'color' => 'green', 'label' => pht('Observe'), 'note' => pht( 'Phabricator will observe changes to this URI and copy them.'), 'short' => pht('Copy from a remote.'), ), self::IO_MIRROR => array( 'icon' => 'fa-upload', 'color' => 'green', 'label' => pht('Mirror'), 'note' => pht( 'Phabricator will push a copy of any changes to this URI.'), 'short' => pht('Push a copy to a remote.'), ), self::IO_NONE => array( 'icon' => 'fa-times', 'color' => 'grey', 'label' => pht('No I/O'), 'note' => pht( 'Phabricator will not push or pull any changes to this URI.'), 'short' => pht('Do not perform any I/O.'), ), self::IO_READ => array( 'icon' => 'fa-folder', 'color' => 'blue', 'label' => pht('Read Only'), 'note' => pht( 'Phabricator will serve a read-only copy of the repository from '. 'this URI.'), 'short' => pht('Serve repository in read-only mode.'), ), self::IO_READWRITE => array( 'icon' => 'fa-folder-open', 'color' => 'blue', 'label' => pht('Read/Write'), 'note' => pht( 'Phabricator will serve a read/write copy of the repository from '. 'this URI.'), 'short' => pht('Serve repository in read/write mode.'), ), ); } public static function getDisplayTypeMap() { return array( self::DISPLAY_DEFAULT => array( 'label' => pht('Default'), 'short' => pht('Use default behavior.'), ), self::DISPLAY_ALWAYS => array( 'icon' => 'fa-eye', 'color' => 'green', 'label' => pht('Visible'), 'note' => pht('This URI will be shown to users as a clone URI.'), 'short' => pht('Show as a clone URI.'), ), self::DISPLAY_NEVER => array( 'icon' => 'fa-eye-slash', 'color' => 'grey', 'label' => pht('Hidden'), 'note' => pht( 'This URI will be hidden from users.'), 'short' => pht('Do not show as a clone URI.'), ), ); } public function newCommandEngine() { $repository = $this->getRepository(); return DiffusionCommandEngine::newCommandEngine($repository) ->setCredentialPHID($this->getCredentialPHID()) ->setURI($this->getEffectiveURI()); } public function getURIScore() { $score = 0; $io_points = array( self::IO_READWRITE => 200, self::IO_READ => 100, ); $score += idx($io_points, $this->getEffectiveIOType(), 0); $protocol_points = array( self::BUILTIN_PROTOCOL_SSH => 30, self::BUILTIN_PROTOCOL_HTTPS => 20, self::BUILTIN_PROTOCOL_HTTP => 10, ); $score += idx($protocol_points, $this->getBuiltinProtocol(), 0); $identifier_points = array( self::BUILTIN_IDENTIFIER_SHORTNAME => 3, self::BUILTIN_IDENTIFIER_CALLSIGN => 2, self::BUILTIN_IDENTIFIER_ID => 1, ); $score += idx($identifier_points, $this->getBuiltinIdentifier(), 0); return $score; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DiffusionURIEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryURITransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::getMostOpenPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: // To edit a repository URI, you must be able to edit the // corresponding repository. $extended[] = array($this->getRepository(), $capability); break; } return $extended; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid') ->setDescription(pht('The associated repository PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('uri') ->setType('map') ->setDescription(pht('The raw and effective URI.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('io') ->setType('map') ->setDescription( pht('The raw, default, and effective I/O Type settings.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('display') ->setType('map') ->setDescription( pht('The raw, default, and effective Display Type settings.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('credentialPHID') ->setType('phid?') ->setDescription( pht('The associated credential PHID, if one exists.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('disabled') ->setType('bool') ->setDescription(pht('True if the URI is disabled.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('builtin') ->setType('map') ->setDescription( pht('Information about builtin URIs.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('dateCreated') ->setType('int') ->setDescription( pht('Epoch timestamp when the object was created.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('dateModified') ->setType('int') ->setDescription( pht('Epoch timestamp when the object was last updated.')), ); } public function getFieldValuesForConduit() { return array( 'repositoryPHID' => $this->getRepositoryPHID(), 'uri' => array( 'raw' => $this->getURI(), 'display' => (string)$this->getDisplayURI(), 'effective' => (string)$this->getEffectiveURI(), 'normalized' => (string)$this->getNormalizedURI(), ), 'io' => array( 'raw' => $this->getIOType(), 'default' => $this->getDefaultIOType(), 'effective' => $this->getEffectiveIOType(), ), 'display' => array( 'raw' => $this->getDisplayType(), 'default' => $this->getDefaultDisplayType(), 'effective' => $this->getEffectiveDisplayType(), ), 'credentialPHID' => $this->getCredentialPHID(), 'disabled' => (bool)$this->getIsDisabled(), 'builtin' => array( 'protocol' => $this->getBuiltinProtocol(), 'identifier' => $this->getBuiltinIdentifier(), ), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php index ba6a5b2ef8..8520cac1bd 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -1,276 +1,272 @@ setVisibility(self::VISIBILITY_VISIBLE); } public static function initializeNewItem( $profile_object, PhabricatorProfileMenuItem $item, $custom_phid) { return self::initializeNewBuiltin() ->setProfilePHID($profile_object->getPHID()) ->setMenuItemKey($item->getMenuItemKey()) ->attachMenuItem($item) ->attachProfileObject($profile_object) ->setCustomPHID($custom_phid); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'menuItemProperties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'menuItemKey' => 'text64', 'builtinKey' => 'text64?', 'menuItemOrder' => 'uint32?', 'customPHID' => 'phid?', 'visibility' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_profile' => array( 'columns' => array('profilePHID', 'menuItemOrder'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProfileMenuItemPHIDType::TYPECONST); } public function attachMenuItem(PhabricatorProfileMenuItem $item) { $this->menuItem = $item; return $this; } public function getMenuItem() { return $this->assertAttached($this->menuItem); } public function attachProfileObject($profile_object) { $this->profileObject = $profile_object; return $this; } public function getProfileObject() { return $this->assertAttached($this->profileObject); } public function setMenuItemProperty($key, $value) { $this->menuItemProperties[$key] = $value; return $this; } public function getMenuItemProperty($key, $default = null) { return idx($this->menuItemProperties, $key, $default); } public function buildNavigationMenuItems() { return $this->getMenuItem()->buildNavigationMenuItems($this); } public function getMenuItemTypeName() { return $this->getMenuItem()->getMenuItemTypeName(); } public function getDisplayName() { return $this->getMenuItem()->getDisplayName($this); } public function canMakeDefault() { return $this->getMenuItem()->canMakeDefault($this); } public function canHideMenuItem() { return $this->getMenuItem()->canHideMenuItem($this); } public function shouldEnableForObject($object) { return $this->getMenuItem()->shouldEnableForObject($object); } public function willBuildNavigationItems(array $items) { return $this->getMenuItem()->willBuildNavigationItems($items); } public function validateTransactions(array $map) { $item = $this->getMenuItem(); $fields = $item->buildEditEngineFields($this); $errors = array(); foreach ($fields as $field) { $field_key = $field->getKey(); $xactions = idx($map, $field_key, array()); $value = $this->getMenuItemProperty($field_key); $field_errors = $item->validateTransactions( $this, $field_key, $value, $xactions); foreach ($field_errors as $error) { $errors[] = $error; } } return $errors; } public function getSortVector() { // Sort custom items above global items. if ($this->getCustomPHID()) { $is_global = 0; } else { $is_global = 1; } // Sort items with an explicit order above items without an explicit order, // so any newly created builtins go to the bottom. $order = $this->getMenuItemOrder(); if ($order !== null) { $has_order = 0; } else { $has_order = 1; } return id(new PhutilSortVector()) ->addInt($is_global) ->addInt($has_order) ->addInt((int)$order) ->addInt((int)$this->getID()); } public function isDisabled() { if (!$this->canHideMenuItem()) { return false; } return ($this->getVisibility() === self::VISIBILITY_DISABLED); } public function isDefault() { return ($this->getVisibility() === self::VISIBILITY_DEFAULT); } public function getItemIdentifier() { $id = $this->getID(); if ($id) { return (int)$id; } return $this->getBuiltinKey(); } public function getDefaultMenuItemKey() { if ($this->getBuiltinKey()) { return $this->getBuiltinKey(); } return $this->getPHID(); } public function newPageContent() { return $this->getMenuItem()->newPageContent($this); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getProfileObject()->hasAutomaticCapability( $capability, $viewer); } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { // If this is an item with a custom PHID (like a personal menu item), // we only require that the user can edit the corresponding custom // object (usually their own user profile), not the object that the // menu appears on (which may be an Application like Favorites or Home). if ($capability == PhabricatorPolicyCapability::CAN_EDIT) { if ($this->getCustomPHID()) { return array( array( $this->getCustomPHID(), $capability, ), ); } } return array( array( $this->getProfileObject(), $capability, ), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProfileMenuEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorProfileMenuItemConfigurationTransaction(); } } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 9280608457..0af6b4553c 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -1,256 +1,252 @@ true, self::CONFIG_SERIALIZATION => array( 'preferences' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'userPHID' => 'phid?', 'builtinKey' => 'text32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_user' => array( 'columns' => array('userPHID'), 'unique' => true, ), 'key_builtin' => array( 'columns' => array('builtinKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorUserPreferencesPHIDType::TYPECONST); } 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 getDefaultValue($key) { if ($this->defaultSettings) { return $this->defaultSettings->getSettingValue($key); } $setting = self::getSettingObject($key); if (!$setting) { return null; } $setting = id(clone $setting) ->setViewer($this->getUser()); return $setting->getSettingDefaultValue(); } public function getSettingValue($key) { if (array_key_exists($key, $this->preferences)) { return $this->preferences[$key]; } return $this->getDefaultValue($key); } private static function getSettingObject($key) { $settings = PhabricatorSetting::getAllSettings(); return idx($settings, $key); } public function attachDefaultSettings(PhabricatorUserPreferences $settings) { $this->defaultSettings = $settings; return $this; } public function attachUser(PhabricatorUser $user = null) { $this->user = $user; return $this; } public function getUser() { return $this->assertAttached($this->user); } public function hasManagedUser() { $user_phid = $this->getUserPHID(); if (!$user_phid) { return false; } $user = $this->getUser(); if ($user->getIsSystemAgent() || $user->getIsMailingList()) { return true; } return false; } /** * Load or create a preferences object for the given user. * * @param PhabricatorUser User to load or create preferences for. */ public static function loadUserPreferences(PhabricatorUser $user) { return id(new PhabricatorUserPreferencesQuery()) ->setViewer($user) ->withUsers(array($user)) ->needSyntheticPreferences(true) ->executeOne(); } /** * Load or create a global preferences object. * * If no global preferences exist, an empty preferences object is returned. * * @param PhabricatorUser Viewing user. */ public static function loadGlobalPreferences(PhabricatorUser $viewer) { $global = id(new PhabricatorUserPreferencesQuery()) ->setViewer($viewer) ->withBuiltinKeys( array( self::BUILTIN_GLOBAL_DEFAULT, )) ->executeOne(); if (!$global) { $global = id(new self()) ->attachUser(new PhabricatorUser()); } return $global; } public function newTransaction($key, $value) { $setting_property = PhabricatorUserPreferencesTransaction::PROPERTY_SETTING; $xaction_type = PhabricatorUserPreferencesTransaction::TYPE_SETTING; return id(clone $this->getApplicationTransactionTemplate()) ->setTransactionType($xaction_type) ->setMetadataValue($setting_property, $key) ->setNewValue($value); } public function getEditURI() { if ($this->getUser()) { return '/settings/user/'.$this->getUser()->getUsername().'/'; } else { return '/settings/builtin/'.$this->getBuiltinKey().'/'; } } public function getDisplayName() { if ($this->getBuiltinKey()) { return pht('Global Default Settings'); } return pht('Personal Settings'); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $user_phid = $this->getUserPHID(); if ($user_phid) { return $user_phid; } return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: if ($this->hasManagedUser()) { return PhabricatorPolicies::POLICY_ADMIN; } $user_phid = $this->getUserPHID(); if ($user_phid) { return $user_phid; } return PhabricatorPolicies::POLICY_ADMIN; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->hasManagedUser()) { if ($viewer->getIsAdmin()) { return true; } } switch ($this->getBuiltinKey()) { case self::BUILTIN_GLOBAL_DEFAULT: // NOTE: Without this policy exception, the logged-out viewer can not // see global preferences. return true; } return false; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorUserPreferencesEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorUserPreferencesTransaction(); } } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php index fcf0009448..b8355c0586 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -1,215 +1,211 @@ setViewer($actor) ->withClasses(array('PhabricatorSlowvoteApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorSlowvoteDefaultViewCapability::CAPABILITY); return id(new PhabricatorSlowvotePoll()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'question' => 'text255', 'responseVisibility' => 'uint32', 'shuffle' => 'bool', 'method' => 'uint32', 'description' => 'text', 'isClosed' => 'bool', 'mailKey' => 'bytes20', ), 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; } public function getMonogram() { return 'V'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorSlowvoteEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorSlowvoteTransaction(); } /* -( 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()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getAuthorPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ 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(); } /* -( PhabricatorSpacesInterface )--------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } } diff --git a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php index 6b51d9c999..e8b2afb435 100644 --- a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php +++ b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php @@ -1,111 +1,107 @@ setViewer($actor) ->withClasses(array('PhabricatorSpacesApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorSpacesCapabilityDefaultView::CAPABILITY); $edit_policy = $app->getPolicy( PhabricatorSpacesCapabilityDefaultEdit::CAPABILITY); return id(new PhabricatorSpacesNamespace()) ->setIsDefaultNamespace(null) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setDescription('') ->setIsArchived(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'namespaceName' => 'text255', 'isDefaultNamespace' => 'bool?', 'description' => 'text', 'isArchived' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_default' => array( 'columns' => array('isDefaultNamespace'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorSpacesNamespacePHIDType::TYPECONST); } public function getMonogram() { return 'S'.$this->getID(); } /* -( 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; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorSpacesNamespaceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorSpacesNamespaceTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } } diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php index 817e92ce39..13a0e14c23 100644 --- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php @@ -1,119 +1,117 @@ getViewer(); $phid = $request->getURIData('phid'); $action = $request->getURIData('action'); if (!$request->isFormPost()) { return new Aphront400Response(); } switch ($action) { case 'add': $is_add = true; break; case 'delete': $is_add = false; break; default: return new Aphront400Response(); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!($object instanceof PhabricatorSubscribableInterface)) { return $this->buildErrorResponse( pht('Bad Object'), pht('This object is not subscribable.'), $handle->getURI()); } if ($object->isAutomaticallySubscribed($viewer->getPHID())) { return $this->buildErrorResponse( pht('Automatically Subscribed'), pht('You are automatically subscribed to this object.'), $handle->getURI()); } if (!PhabricatorPolicyFilter::canInteract($viewer, $object)) { $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); $dialog = $this->newDialog() ->addCancelButton($handle->getURI()); return $lock->willBlockUserInteractionWithDialog($dialog); } if ($object instanceof PhabricatorApplicationTransactionInterface) { if ($is_add) { $xaction_value = array( '+' => array($viewer->getPHID()), ); } else { $xaction_value = array( '-' => array($viewer->getPHID()), ); } $xaction = id($object->getApplicationTransactionTemplate()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue($xaction_value); $editor = id($object->getApplicationTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request); - $editor->applyTransactions( - $object->getApplicationTransactionObject(), - array($xaction)); + $editor->applyTransactions($object, array($xaction)); } else { // TODO: Eventually, get rid of this once everything implements // PhabricatorApplicationTransactionInterface. $editor = id(new PhabricatorSubscriptionsEditor()) ->setActor($viewer) ->setObject($object); if ($is_add) { $editor->subscribeExplicit(array($viewer->getPHID()), $explicit = true); } else { $editor->unsubscribe(array($viewer->getPHID())); } $editor->save(); } // TODO: We should just render the "Unsubscribe" action and swap it out // in the document for Ajax requests. return id(new AphrontReloadResponse())->setURI($handle->getURI()); } private function buildErrorResponse($title, $message, $uri) { $request = $this->getRequest(); $viewer = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($title) ->appendChild($message) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php index e29d5e3802..929a4985dd 100644 --- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php @@ -1,90 +1,88 @@ getViewer(); $phid = $request->getURIData('phid'); $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!($object instanceof PhabricatorSubscribableInterface)) { return new Aphront400Response(); } $muted_type = PhabricatorMutedByEdgeType::EDGECONST; $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($object->getPHID())) ->withEdgeTypes(array($muted_type)) ->withDestinationPHIDs(array($viewer->getPHID())); $edge_query->execute(); $is_mute = !$edge_query->getDestinationPHIDs(); $object_uri = $handle->getURI(); if ($request->isFormPost()) { if ($is_mute) { $xaction_value = array( '+' => array_fuse(array($viewer->getPHID())), ); } else { $xaction_value = array( '-' => array_fuse(array($viewer->getPHID())), ); } $xaction = id($object->getApplicationTransactionTemplate()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $muted_type) ->setNewValue($xaction_value); $editor = id($object->getApplicationTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request); - $editor->applyTransactions( - $object->getApplicationTransactionObject(), - array($xaction)); + $editor->applyTransactions($object, array($xaction)); return id(new AphrontReloadResponse())->setURI($object_uri); } $dialog = $this->newDialog() ->addCancelButton($object_uri); if ($is_mute) { $dialog ->setTitle(pht('Mute Notifications')) ->appendParagraph( pht( 'Mute this object? You will no longer receive notifications or '. 'email about it.')) ->addSubmitButton(pht('Mute')); } else { $dialog ->setTitle(pht('Unmute Notifications')) ->appendParagraph( pht( 'Unmute this object? You will receive notifications and email '. 'again.')) ->addSubmitButton(pht('Unmute')); } return $dialog; } } diff --git a/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php b/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php index ea4e8367c0..0d2f143635 100644 --- a/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php +++ b/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php @@ -1,174 +1,172 @@ contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } public function addToken($object_phid, $token_phid) { $token = $this->validateToken($token_phid); $object = $this->validateObject($object_phid); $current_token = $this->loadCurrentToken($object); $actor = $this->requireActor(); $token_given = id(new PhabricatorTokenGiven()) ->setAuthorPHID($actor->getPHID()) ->setObjectPHID($object->getPHID()) ->setTokenPHID($token->getPHID()); $token_given->openTransaction(); if ($current_token) { $this->executeDeleteToken($object, $current_token); } $token_given->save(); queryfx( $token_given->establishConnection('w'), 'INSERT INTO %T (objectPHID, tokenCount) VALUES (%s, 1) ON DUPLICATE KEY UPDATE tokenCount = tokenCount + 1', id(new PhabricatorTokenCount())->getTableName(), $object->getPHID()); $token_given->saveTransaction(); $current_token_phid = null; if ($current_token) { $current_token_phid = $current_token->getTokenPHID(); } $this->publishTransaction( $object, $current_token_phid, $token->getPHID()); $subscribed_phids = $object->getUsersToNotifyOfTokenGiven(); if ($subscribed_phids) { $related_phids = $subscribed_phids; $related_phids[] = $actor->getPHID(); $story_type = 'PhabricatorTokenGivenFeedStory'; $story_data = array( 'authorPHID' => $actor->getPHID(), 'tokenPHID' => $token->getPHID(), 'objectPHID' => $object->getPHID(), ); id(new PhabricatorFeedStoryPublisher()) ->setStoryType($story_type) ->setStoryData($story_data) ->setStoryTime(time()) ->setStoryAuthorPHID($actor->getPHID()) ->setRelatedPHIDs($related_phids) ->setPrimaryObjectPHID($object->getPHID()) ->setSubscribedPHIDs($subscribed_phids) ->publish(); } return $token_given; } public function deleteToken($object_phid) { $object = $this->validateObject($object_phid); $token_given = $this->loadCurrentToken($object); if (!$token_given) { return; } $this->executeDeleteToken($object, $token_given); $this->publishTransaction( $object, $token_given->getTokenPHID(), null); } private function executeDeleteToken( PhabricatorTokenReceiverInterface $object, PhabricatorTokenGiven $token_given) { $token_given->openTransaction(); $token_given->delete(); queryfx( $token_given->establishConnection('w'), 'INSERT INTO %T (objectPHID, tokenCount) VALUES (%s, 0) ON DUPLICATE KEY UPDATE tokenCount = tokenCount - 1', id(new PhabricatorTokenCount())->getTableName(), $object->getPHID()); $token_given->saveTransaction(); } private function validateToken($token_phid) { $token = id(new PhabricatorTokenQuery()) ->setViewer($this->requireActor()) ->withPHIDs(array($token_phid)) ->executeOne(); if (!$token) { throw new Exception(pht('No such token "%s"!', $token_phid)); } return $token; } private function validateObject($object_phid) { $object = id(new PhabricatorObjectQuery()) ->setViewer($this->requireActor()) ->withPHIDs(array($object_phid)) ->executeOne(); if (!$object) { throw new Exception(pht('No such object "%s"!', $object_phid)); } return $object; } private function loadCurrentToken(PhabricatorTokenReceiverInterface $object) { return id(new PhabricatorTokenGiven())->loadOneWhere( 'authorPHID = %s AND objectPHID = %s', $this->requireActor()->getPHID(), $object->getPHID()); } private function publishTransaction( PhabricatorTokenReceiverInterface $object, $old_token_phid, $new_token_phid) { if (!($object instanceof PhabricatorApplicationTransactionInterface)) { return; } $actor = $this->requireActor(); $xactions = array(); $xactions[] = id($object->getApplicationTransactionTemplate()) ->setTransactionType(PhabricatorTransactions::TYPE_TOKEN) ->setOldValue($old_token_phid) ->setNewValue($new_token_phid); $editor = $object->getApplicationTransactionEditor() ->setActor($actor) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); - $editor->applyTransactions( - $object->getApplicationTransactionObject(), - $xactions); + $editor->applyTransactions($object, $xactions); } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 738fcf098b..697c4600a5 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1,4711 +1,4710 @@ actingAsPHID = $acting_as_phid; return $this; } public function getActingAsPHID() { if ($this->actingAsPHID) { return $this->actingAsPHID; } return $this->getActor()->getPHID(); } /** * When the editor tries to apply transactions that have no effect, should * it raise an exception (default) or drop them and continue? * * Generally, you will set this flag for edits coming from "Edit" interfaces, * and leave it cleared for edits coming from "Comment" interfaces, so the * user will get a useful error if they try to submit a comment that does * nothing (e.g., empty comment with a status change that has already been * performed by another user). * * @param bool True to drop transactions without effect and continue. * @return this */ public function setContinueOnNoEffect($continue) { $this->continueOnNoEffect = $continue; return $this; } public function getContinueOnNoEffect() { return $this->continueOnNoEffect; } /** * When the editor tries to apply transactions which don't populate all of * an object's required fields, should it raise an exception (default) or * drop them and continue? * * For example, if a user adds a new required custom field (like "Severity") * to a task, all existing tasks won't have it populated. When users * manually edit existing tasks, it's usually desirable to have them provide * a severity. However, other operations (like batch editing just the * owner of a task) will fail by default. * * By setting this flag for edit operations which apply to specific fields * (like the priority, batch, and merge editors in Maniphest), these * operations can continue to function even if an object is outdated. * * @param bool True to continue when transactions don't completely satisfy * all required fields. * @return this */ public function setContinueOnMissingFields($continue_on_missing_fields) { $this->continueOnMissingFields = $continue_on_missing_fields; return $this; } public function getContinueOnMissingFields() { return $this->continueOnMissingFields; } /** * Not strictly necessary, but reply handlers ideally set this value to * make email threading work better. */ public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function getParentMessageID() { return $this->parentMessageID; } public function getIsNewObject() { return $this->isNewObject; } public function getMentionedPHIDs() { return $this->mentionedPHIDs; } public function setIsPreview($is_preview) { $this->isPreview = $is_preview; return $this; } public function getIsPreview() { return $this->isPreview; } public function setIsSilent($silent) { $this->silent = $silent; return $this; } public function getIsSilent() { return $this->silent; } public function getMustEncrypt() { return $this->mustEncrypt; } public function getHeraldRuleMonograms() { // Convert the stored "<123>, <456>" string into a list: "H123", "H456". $list = $this->heraldHeader; $list = preg_split('/[, ]+/', $list); foreach ($list as $key => $item) { $item = trim($item, '<>'); if (!is_numeric($item)) { unset($list[$key]); continue; } $list[$key] = 'H'.$item; } return $list; } public function setIsInverseEdgeEditor($is_inverse_edge_editor) { $this->isInverseEdgeEditor = $is_inverse_edge_editor; return $this; } public function getIsInverseEdgeEditor() { return $this->isInverseEdgeEditor; } public function setIsHeraldEditor($is_herald_editor) { $this->isHeraldEditor = $is_herald_editor; return $this; } public function getIsHeraldEditor() { return $this->isHeraldEditor; } public function setUnmentionablePHIDMap(array $map) { $this->unmentionablePHIDMap = $map; return $this; } public function getUnmentionablePHIDMap() { return $this->unmentionablePHIDMap; } protected function shouldEnableMentions( PhabricatorLiskDAO $object, array $xactions) { return true; } public function setApplicationEmail( PhabricatorMetaMTAApplicationEmail $email) { $this->applicationEmail = $email; return $this; } public function getApplicationEmail() { return $this->applicationEmail; } public function setRaiseWarnings($raise_warnings) { $this->raiseWarnings = $raise_warnings; return $this; } public function getRaiseWarnings() { return $this->raiseWarnings; } public function getTransactionTypesForObject($object) { $old = $this->object; try { $this->object = $object; $result = $this->getTransactionTypes(); $this->object = $old; } catch (Exception $ex) { $this->object = $old; throw $ex; } return $result; } public function getTransactionTypes() { $types = array(); $types[] = PhabricatorTransactions::TYPE_CREATE; $types[] = PhabricatorTransactions::TYPE_HISTORY; if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) { $types[] = PhabricatorTransactions::TYPE_SUBTYPE; } if ($this->object instanceof PhabricatorSubscribableInterface) { $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS; } if ($this->object instanceof PhabricatorCustomFieldInterface) { $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD; } if ($this->object instanceof PhabricatorTokenReceiverInterface) { $types[] = PhabricatorTransactions::TYPE_TOKEN; } if ($this->object instanceof PhabricatorProjectInterface || $this->object instanceof PhabricatorMentionableInterface) { $types[] = PhabricatorTransactions::TYPE_EDGE; } if ($this->object instanceof PhabricatorSpacesInterface) { $types[] = PhabricatorTransactions::TYPE_SPACE; } $template = $this->object->getApplicationTransactionTemplate(); if ($template instanceof PhabricatorModularTransaction) { $xtypes = $template->newModularTransactionTypes(); foreach ($xtypes as $xtype) { $types[] = $xtype->getTransactionTypeConstant(); } } if ($template) { try { $comment = $template->getApplicationTransactionCommentObject(); } catch (PhutilMethodNotImplementedException $ex) { $comment = null; } if ($comment) { $types[] = PhabricatorTransactions::TYPE_COMMENT; } } return $types; } private function adjustTransactionValues( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { if ($xaction->shouldGenerateOldValue()) { $old = $this->getTransactionOldValue($object, $xaction); $xaction->setOldValue($old); } $new = $this->getTransactionNewValue($object, $xaction); $xaction->setNewValue($new); } private function getTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $xtype = clone $xtype; $xtype->setStorage($xaction); return $xtype->generateOldValue($object); } switch ($type) { case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_HISTORY: return null; case PhabricatorTransactions::TYPE_SUBTYPE: return $object->getEditEngineSubtype(); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return array_values($this->subscribers); case PhabricatorTransactions::TYPE_VIEW_POLICY: if ($this->getIsNewObject()) { return null; } return $object->getViewPolicy(); case PhabricatorTransactions::TYPE_EDIT_POLICY: if ($this->getIsNewObject()) { return null; } return $object->getEditPolicy(); case PhabricatorTransactions::TYPE_JOIN_POLICY: if ($this->getIsNewObject()) { return null; } return $object->getJoinPolicy(); case PhabricatorTransactions::TYPE_SPACE: if ($this->getIsNewObject()) { return null; } $space_phid = $object->getSpacePHID(); if ($space_phid === null) { $default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace(); if ($default_space) { $space_phid = $default_space->getPHID(); } } return $space_phid; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $xaction->getMetadataValue('edge:type'); if (!$edge_type) { throw new Exception( pht( "Edge transaction has no '%s'!", 'edge:type')); } $old_edges = array(); if ($object->getPHID()) { $edge_src = $object->getPHID(); $old_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($edge_src)) ->withEdgeTypes(array($edge_type)) ->needEdgeData(true) ->execute(); $old_edges = $old_edges[$edge_src][$edge_type]; } return $old_edges; case PhabricatorTransactions::TYPE_CUSTOMFIELD: // NOTE: Custom fields have their old value pre-populated when they are // built by PhabricatorCustomFieldList. return $xaction->getOldValue(); case PhabricatorTransactions::TYPE_COMMENT: return null; default: return $this->getCustomTransactionOldValue($object, $xaction); } } private function getTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $xtype = clone $xtype; $xtype->setStorage($xaction); return $xtype->generateNewValue($object, $xaction->getNewValue()); } switch ($type) { case PhabricatorTransactions::TYPE_CREATE: return null; case PhabricatorTransactions::TYPE_SUBSCRIBERS: return $this->getPHIDTransactionNewValue($xaction); case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_INLINESTATE: case PhabricatorTransactions::TYPE_SUBTYPE: case PhabricatorTransactions::TYPE_HISTORY: return $xaction->getNewValue(); case PhabricatorTransactions::TYPE_SPACE: $space_phid = $xaction->getNewValue(); if (!strlen($space_phid)) { // If an install has no Spaces or the Spaces controls are not visible // to the viewer, we might end up with the empty string here instead // of a strict `null`, because some controller just used `getStr()` // to read the space PHID from the request. // Just make this work like callers might reasonably expect so we // don't need to handle this specially in every EditController. return $this->getActor()->getDefaultSpacePHID(); } else { return $space_phid; } case PhabricatorTransactions::TYPE_EDGE: $new_value = $this->getEdgeTransactionNewValue($xaction); $edge_type = $xaction->getMetadataValue('edge:type'); $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; if ($edge_type == $type_project) { $new_value = $this->applyProjectConflictRules($new_value); } return $new_value; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->getNewValueFromApplicationTransactions($xaction); case PhabricatorTransactions::TYPE_COMMENT: return null; default: return $this->getCustomTransactionNewValue($object, $xaction); } } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { throw new Exception(pht('Capability not supported!')); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { throw new Exception(pht('Capability not supported!')); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_HISTORY: return true; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->getApplicationTransactionHasEffect($xaction); case PhabricatorTransactions::TYPE_EDGE: // A straight value comparison here doesn't always get the right // result, because newly added edges aren't fully populated. Instead, // compare the changes in a more granular way. $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $old_dst = array_keys($old); $new_dst = array_keys($new); // NOTE: For now, we don't consider edge reordering to be a change. // We have very few order-dependent edges and effectively no order // oriented UI. This might change in the future. sort($old_dst); sort($new_dst); if ($old_dst !== $new_dst) { // We've added or removed edges, so this transaction definitely // has an effect. return true; } // We haven't added or removed edges, but we might have changed // edge data. foreach ($old as $key => $old_value) { $new_value = $new[$key]; if ($old_value['data'] !== $new_value['data']) { return true; } } return false; } $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { return $xtype->getTransactionHasEffect( $object, $xaction->getOldValue(), $xaction->getNewValue()); } if ($xaction->hasComment()) { return true; } return ($xaction->getOldValue() !== $xaction->getNewValue()); } protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { return false; } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { throw new PhutilMethodNotImplementedException(); } private function applyInternalEffects( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $xtype = clone $xtype; $xtype->setStorage($xaction); return $xtype->applyInternalEffects($object, $xaction->getNewValue()); } switch ($type) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->applyApplicationTransactionInternalEffects($xaction); case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_HISTORY: case PhabricatorTransactions::TYPE_SUBTYPE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_INLINESTATE: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_SPACE: case PhabricatorTransactions::TYPE_COMMENT: return $this->applyBuiltinInternalTransaction($object, $xaction); } return $this->applyCustomInternalTransaction($object, $xaction); } private function applyExternalEffects( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $xtype = clone $xtype; $xtype->setStorage($xaction); return $xtype->applyExternalEffects($object, $xaction->getNewValue()); } switch ($type) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: $subeditor = id(new PhabricatorSubscriptionsEditor()) ->setObject($object) ->setActor($this->requireActor()); $old_map = array_fuse($xaction->getOldValue()); $new_map = array_fuse($xaction->getNewValue()); $subeditor->unsubscribe( array_keys( array_diff_key($old_map, $new_map))); $subeditor->subscribeExplicit( array_keys( array_diff_key($new_map, $old_map))); $subeditor->save(); // for the rest of these edits, subscribers should include those just // added as well as those just removed. $subscribers = array_unique(array_merge( $this->subscribers, $xaction->getOldValue(), $xaction->getNewValue())); $this->subscribers = $subscribers; return $this->applyBuiltinExternalTransaction($object, $xaction); case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->applyApplicationTransactionExternalEffects($xaction); case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_HISTORY: case PhabricatorTransactions::TYPE_SUBTYPE: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_INLINESTATE: case PhabricatorTransactions::TYPE_SPACE: case PhabricatorTransactions::TYPE_COMMENT: return $this->applyBuiltinExternalTransaction($object, $xaction); } return $this->applyCustomExternalTransaction($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); throw new Exception( pht( "Transaction type '%s' is missing an internal apply implementation!", $type)); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); throw new Exception( pht( "Transaction type '%s' is missing an external apply implementation!", $type)); } /** * @{class:PhabricatorTransactions} provides many built-in transactions * which should not require much - if any - code in specific applications. * * This method is a hook for the exceedingly-rare cases where you may need * to do **additional** work for built-in transactions. Developers should * extend this method, making sure to return the parent implementation * regardless of handling any transactions. * * See also @{method:applyBuiltinExternalTransaction}. */ protected function applyBuiltinInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: $object->setViewPolicy($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: $object->setEditPolicy($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_JOIN_POLICY: $object->setJoinPolicy($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_SPACE: $object->setSpacePHID($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_SUBTYPE: $object->setEditEngineSubtype($xaction->getNewValue()); break; } } /** * See @{method::applyBuiltinInternalTransaction}. */ protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: if ($this->getIsInverseEdgeEditor()) { // If we're writing an inverse edge transaction, don't actually // do anything. The initiating editor on the other side of the // transaction will take care of the edge writes. break; } $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $src = $object->getPHID(); $const = $xaction->getMetadataValue('edge:type'); $type = PhabricatorEdgeType::getByConstant($const); if ($type->shouldWriteInverseTransactions()) { $this->applyInverseEdgeTransactions( $object, $xaction, $type->getInverseEdgeConstant()); } foreach ($new as $dst_phid => $edge) { $new[$dst_phid]['src'] = $src; } $editor = new PhabricatorEdgeEditor(); foreach ($old as $dst_phid => $edge) { if (!empty($new[$dst_phid])) { if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) { continue; } } $editor->removeEdge($src, $const, $dst_phid); } foreach ($new as $dst_phid => $edge) { if (!empty($old[$dst_phid])) { if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) { continue; } } $data = array( 'data' => $edge['data'], ); $editor->addEdge($src, $const, $dst_phid, $data); } $editor->save(); $this->updateWorkboardColumns($object, $const, $old, $new); break; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_SPACE: $this->scrambleFileSecrets($object); break; case PhabricatorTransactions::TYPE_HISTORY: $this->sendHistory = true; break; } } /** * Fill in a transaction's common values, like author and content source. */ protected function populateTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $actor = $this->getActor(); // TODO: This needs to be more sophisticated once we have meta-policies. $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC); if ($actor->isOmnipotent()) { $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); } else { $xaction->setEditPolicy($this->getActingAsPHID()); } // If the transaction already has an explicit author PHID, allow it to // stand. This is used by applications like Owners that hook into the // post-apply change pipeline. if (!$xaction->getAuthorPHID()) { $xaction->setAuthorPHID($this->getActingAsPHID()); } $xaction->setContentSource($this->getContentSource()); $xaction->attachViewer($actor); $xaction->attachObject($object); if ($object->getPHID()) { $xaction->setObjectPHID($object->getPHID()); } if ($this->getIsSilent()) { $xaction->setIsSilentTransaction(true); } if ($actor->hasHighSecuritySession()) { $xaction->setIsMFATransaction(true); } return $xaction; } protected function didApplyInternalEffects( PhabricatorLiskDAO $object, array $xactions) { return $xactions; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { return $xactions; } final protected function didCommitTransactions( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if (!$xtype) { continue; } $xtype = clone $xtype; $xtype->setStorage($xaction); $xtype->didCommitTransaction($object, $xaction->getNewValue()); } } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function setContentSourceFromRequest(AphrontRequest $request) { return $this->setContentSource( PhabricatorContentSource::newFromRequest($request)); } public function getContentSource() { return $this->contentSource; } final public function applyTransactions( PhabricatorLiskDAO $object, array $xactions) { $this->object = $object; $this->xactions = $xactions; $this->isNewObject = ($object->getPHID() === null); $this->validateEditParameters($object, $xactions); $actor = $this->requireActor(); // NOTE: Some transaction expansion requires that the edited object be // attached. foreach ($xactions as $xaction) { $xaction->attachObject($object); $xaction->attachViewer($actor); } $xactions = $this->expandTransactions($object, $xactions); $xactions = $this->expandSupportTransactions($object, $xactions); $xactions = $this->combineTransactions($xactions); foreach ($xactions as $xaction) { $xaction = $this->populateTransaction($object, $xaction); } $is_preview = $this->getIsPreview(); $read_locking = false; $transaction_open = false; if (!$is_preview) { $errors = array(); $type_map = mgroup($xactions, 'getTransactionType'); foreach ($this->getTransactionTypes() as $type) { $type_xactions = idx($type_map, $type, array()); $errors[] = $this->validateTransaction($object, $type, $type_xactions); } $errors[] = $this->validateAllTransactions($object, $xactions); $errors = array_mergev($errors); $continue_on_missing = $this->getContinueOnMissingFields(); foreach ($errors as $key => $error) { if ($continue_on_missing && $error->getIsMissingFieldError()) { unset($errors[$key]); } } if ($errors) { throw new PhabricatorApplicationTransactionValidationException($errors); } if ($this->raiseWarnings) { $warnings = array(); foreach ($xactions as $xaction) { if ($this->hasWarnings($object, $xaction)) { $warnings[] = $xaction; } } if ($warnings) { throw new PhabricatorApplicationTransactionWarningException( $warnings); } } $this->willApplyTransactions($object, $xactions); if ($object->getID()) { $this->buildOldRecipientLists($object, $xactions); $object->openTransaction(); $transaction_open = true; $object->beginReadLocking(); $read_locking = true; $object->reload(); } if ($this->shouldApplyInitialEffects($object, $xactions)) { if (!$transaction_open) { $object->openTransaction(); $transaction_open = true; } } } try { if ($this->shouldApplyInitialEffects($object, $xactions)) { $this->applyInitialEffects($object, $xactions); } foreach ($xactions as $xaction) { $this->adjustTransactionValues($object, $xaction); } // Now that we've merged and combined transactions, check for required // capabilities. Note that we're doing this before filtering // transactions: if you try to apply an edit which you do not have // permission to apply, we want to give you a permissions error even // if the edit would have no effect. $this->applyCapabilityChecks($object, $xactions); // See T13186. Fatal hard if this object has an older // "requireCapabilities()" method. The code may rely on this method being // called to apply policy checks, so err on the side of safety and fatal. // TODO: Remove this check after some time has passed. if (method_exists($this, 'requireCapabilities')) { throw new Exception( pht( 'Editor (of class "%s") implements obsolete policy method '. 'requireCapabilities(). The implementation for this Editor '. 'MUST be updated. See <%s> for discussion.', get_class($this), 'https://secure.phabricator.com/T13186')); } $xactions = $this->filterTransactions($object, $xactions); // TODO: Once everything is on EditEngine, just use getIsNewObject() to // figure this out instead. $mark_as_create = false; $create_type = PhabricatorTransactions::TYPE_CREATE; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $create_type) { $mark_as_create = true; } } if ($mark_as_create) { foreach ($xactions as $xaction) { $xaction->setIsCreateTransaction(true); } } $xactions = $this->sortTransactions($xactions); $file_phids = $this->extractFilePHIDs($object, $xactions); if ($is_preview) { $this->loadHandles($xactions); return $xactions; } $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor()) ->setActor($actor) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()); if (!$transaction_open) { $object->openTransaction(); $transaction_open = true; } foreach ($xactions as $xaction) { $this->applyInternalEffects($object, $xaction); } $xactions = $this->didApplyInternalEffects($object, $xactions); try { $object->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // This callback has an opportunity to throw a better exception, // so execution may end here. $this->didCatchDuplicateKeyException($object, $xactions, $ex); throw $ex; } foreach ($xactions as $xaction) { $xaction->setObjectPHID($object->getPHID()); if ($xaction->getComment()) { $xaction->setPHID($xaction->generatePHID()); $comment_editor->applyEdit($xaction, $xaction->getComment()); } else { // TODO: This is a transitional hack to let us migrate edge // transactions to a more efficient storage format. For now, we're // going to write a new slim format to the database but keep the old // bulky format on the objects so we don't have to upgrade all the // edit logic to the new format yet. See T13051. $edge_type = PhabricatorTransactions::TYPE_EDGE; if ($xaction->getTransactionType() == $edge_type) { $bulky_old = $xaction->getOldValue(); $bulky_new = $xaction->getNewValue(); $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); $slim_old = $record->getModernOldEdgeTransactionData(); $slim_new = $record->getModernNewEdgeTransactionData(); $xaction->setOldValue($slim_old); $xaction->setNewValue($slim_new); $xaction->save(); $xaction->setOldValue($bulky_old); $xaction->setNewValue($bulky_new); } else { $xaction->save(); } } } if ($file_phids) { $this->attachFiles($object, $file_phids); } foreach ($xactions as $xaction) { $this->applyExternalEffects($object, $xaction); } $xactions = $this->applyFinalEffects($object, $xactions); if ($read_locking) { $object->endReadLocking(); $read_locking = false; } if ($transaction_open) { $object->saveTransaction(); $transaction_open = false; } $this->didCommitTransactions($object, $xactions); } catch (Exception $ex) { if ($read_locking) { $object->endReadLocking(); $read_locking = false; } if ($transaction_open) { $object->killTransaction(); $transaction_open = false; } throw $ex; } // If we need to perform cache engine updates, execute them now. id(new PhabricatorCacheEngine()) ->updateObject($object); // Now that we've completely applied the core transaction set, try to apply // Herald rules. Herald rules are allowed to either take direct actions on // the database (like writing flags), or take indirect actions (like saving // some targets for CC when we generate mail a little later), or return // transactions which we'll apply normally using another Editor. // First, check if *this* is a sub-editor which is itself applying Herald // rules: if it is, stop working and return so we don't descend into // madness. // Otherwise, we're not a Herald editor, so process Herald rules (possibly // using a Herald editor to apply resulting transactions) and then send out // mail, notifications, and feed updates about everything. if ($this->getIsHeraldEditor()) { // We are the Herald editor, so stop work here and return the updated // transactions. return $xactions; } else if ($this->getIsInverseEdgeEditor()) { // Do not run Herald if we're just recording that this object was // mentioned elsewhere. This tends to create Herald side effects which // feel arbitrary, and can really slow down edits which mention a large // number of other objects. See T13114. } else if ($this->shouldApplyHeraldRules($object, $xactions)) { // We are not the Herald editor, so try to apply Herald rules. $herald_xactions = $this->applyHeraldRules($object, $xactions); if ($herald_xactions) { $xscript_id = $this->getHeraldTranscript()->getID(); foreach ($herald_xactions as $herald_xaction) { // Don't set a transcript ID if this is a transaction from another // application or source, like Owners. if ($herald_xaction->getAuthorPHID()) { continue; } $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id); } // NOTE: We're acting as the omnipotent user because rules deal with // their own policy issues. We use a synthetic author PHID (the // Herald application) as the author of record, so that transactions // will render in a reasonable way ("Herald assigned this task ..."). $herald_actor = PhabricatorUser::getOmnipotentUser(); $herald_phid = id(new PhabricatorHeraldApplication())->getPHID(); // TODO: It would be nice to give transactions a more specific source // which points at the rule which generated them. You can figure this // out from transcripts, but it would be cleaner if you didn't have to. $herald_source = PhabricatorContentSource::newForSource( PhabricatorHeraldContentSource::SOURCECONST); $herald_editor = newv(get_class($this), array()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setParentMessageID($this->getParentMessageID()) ->setIsHeraldEditor(true) ->setActor($herald_actor) ->setActingAsPHID($herald_phid) ->setContentSource($herald_source); $herald_xactions = $herald_editor->applyTransactions( $object, $herald_xactions); // Merge the new transactions into the transaction list: we want to // send email and publish feed stories about them, too. $xactions = array_merge($xactions, $herald_xactions); } // If Herald did not generate transactions, we may still need to handle // "Send an Email" rules. $adapter = $this->getHeraldAdapter(); $this->heraldEmailPHIDs = $adapter->getEmailPHIDs(); $this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs(); $this->webhookMap = $adapter->getWebhookMap(); } $xactions = $this->didApplyTransactions($object, $xactions); if ($object instanceof PhabricatorCustomFieldInterface) { // Maybe this makes more sense to move into the search index itself? For // now I'm putting it here since I think we might end up with things that // need it to be up to date once the next page loads, but if we don't go // there we could move it into search once search moves to the daemons. // It now happens in the search indexer as well, but the search indexer is // always daemonized, so the logic above still potentially holds. We could // possibly get rid of this. The major motivation for putting it in the // indexer was to enable reindexing to work. $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->readFieldsFromStorage($object); $fields->rebuildIndexes($object); } $herald_xscript = $this->getHeraldTranscript(); if ($herald_xscript) { $herald_header = $herald_xscript->getXHeraldRulesHeader(); $herald_header = HeraldTranscript::saveXHeraldRulesHeader( $object->getPHID(), $herald_header); } else { $herald_header = HeraldTranscript::loadXHeraldRulesHeader( $object->getPHID()); } $this->heraldHeader = $herald_header; // We're going to compute some of the data we'll use to publish these // transactions here, before queueing a worker. // // Primarily, this is more correct: we want to publish the object as it // exists right now. The worker may not execute for some time, and we want // to use the current To/CC list, not respect any changes which may occur // between now and when the worker executes. // // As a secondary benefit, this tends to reduce the amount of state that // Editors need to pass into workers. $object = $this->willPublish($object, $xactions); if (!$this->getIsSilent()) { if ($this->shouldSendMail($object, $xactions)) { $this->mailShouldSend = true; $this->mailToPHIDs = $this->getMailTo($object); $this->mailCCPHIDs = $this->getMailCC($object); $this->mailUnexpandablePHIDs = $this->newMailUnexpandablePHIDs($object); // Add any recipients who were previously on the notification list // but were removed by this change. $this->applyOldRecipientLists(); if ($object instanceof PhabricatorSubscribableInterface) { $this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorMutedByEdgeType::EDGECONST); } else { $this->mailMutedPHIDs = array(); } $mail_xactions = $this->getTransactionsForMail($object, $xactions); $stamps = $this->newMailStamps($object, $xactions); foreach ($stamps as $stamp) { $this->mailStamps[] = $stamp->toDictionary(); } } if ($this->shouldPublishFeedStory($object, $xactions)) { $this->feedShouldPublish = true; $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs( $object, $xactions); $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs( $object, $xactions); } } PhabricatorWorker::scheduleTask( 'PhabricatorApplicationTransactionPublishWorker', array( 'objectPHID' => $object->getPHID(), 'actorPHID' => $this->getActingAsPHID(), 'xactionPHIDs' => mpull($xactions, 'getPHID'), 'state' => $this->getWorkerState(), ), array( 'objectPHID' => $object->getPHID(), 'priority' => PhabricatorWorker::PRIORITY_ALERTS, )); $this->flushTransactionQueue($object); return $xactions; } protected function didCatchDuplicateKeyException( PhabricatorLiskDAO $object, array $xactions, Exception $ex) { return; } public function publishTransactions( PhabricatorLiskDAO $object, array $xactions) { $this->object = $object; $this->xactions = $xactions; // Hook for edges or other properties that may need (re-)loading $object = $this->willPublish($object, $xactions); // The object might have changed, so reassign it. $this->object = $object; $messages = array(); if ($this->mailShouldSend) { $messages = $this->buildMail($object, $xactions); } if ($this->supportsSearch()) { PhabricatorSearchWorker::queueDocumentForIndexing( $object->getPHID(), array( 'transactionPHIDs' => mpull($xactions, 'getPHID'), )); } if ($this->feedShouldPublish) { $mailed = array(); foreach ($messages as $mail) { foreach ($mail->buildRecipientList() as $phid) { $mailed[$phid] = $phid; } } $this->publishFeedStory($object, $xactions, $mailed); } if ($this->sendHistory) { $history_mail = $this->buildHistoryMail($object); if ($history_mail) { $messages[] = $history_mail; } } // NOTE: This actually sends the mail. We do this last to reduce the chance // that we send some mail, hit an exception, then send the mail again when // retrying. foreach ($messages as $mail) { $mail->save(); } $this->queueWebhooks($object, $xactions); return $xactions; } protected function didApplyTransactions($object, array $xactions) { // Hook for subclasses. return $xactions; } private function loadHandles(array $xactions) { $phids = array(); foreach ($xactions as $key => $xaction) { $phids[$key] = $xaction->getRequiredHandlePHIDs(); } $handles = array(); $merged = array_mergev($phids); if ($merged) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($merged) ->execute(); } foreach ($xactions as $key => $xaction) { $xaction->setHandles(array_select_keys($handles, $phids[$key])); } } private function loadSubscribers(PhabricatorLiskDAO $object) { if ($object->getPHID() && ($object instanceof PhabricatorSubscribableInterface)) { $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); $this->subscribers = array_fuse($subs); } else { $this->subscribers = array(); } } private function validateEditParameters( PhabricatorLiskDAO $object, array $xactions) { if (!$this->getContentSource()) { throw new PhutilInvalidStateException('setContentSource'); } // Do a bunch of sanity checks that the incoming transactions are fresh. // They should be unsaved and have only "transactionType" and "newValue" // set. $types = array_fill_keys($this->getTransactionTypes(), true); assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); foreach ($xactions as $xaction) { if ($xaction->getPHID() || $xaction->getID()) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht('You can not apply transactions which already have IDs/PHIDs!')); } if ($xaction->getObjectPHID()) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'You can not apply transactions which already have %s!', 'objectPHIDs')); } if ($xaction->getCommentPHID()) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'You can not apply transactions which already have %s!', 'commentPHIDs')); } if ($xaction->getCommentVersion() !== 0) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'You can not apply transactions which already have '. 'commentVersions!')); } $expect_value = !$xaction->shouldGenerateOldValue(); $has_value = $xaction->hasOldValue(); if ($expect_value && !$has_value) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'This transaction is supposed to have an %s set, but it does not!', 'oldValue')); } if ($has_value && !$expect_value) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'This transaction should generate its %s automatically, '. 'but has already had one set!', 'oldValue')); } $type = $xaction->getTransactionType(); if (empty($types[$type])) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'Transaction has type "%s", but that transaction type is not '. 'supported by this editor (%s).', $type, get_class($this))); } } } private function applyCapabilityChecks( PhabricatorLiskDAO $object, array $xactions) { assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); $can_edit = PhabricatorPolicyCapability::CAN_EDIT; if ($this->getIsNewObject()) { // If we're creating a new object, we don't need any special capabilities // on the object. The actor has already made it through creation checks, // and objects which haven't been created yet often can not be // meaningfully tested for capabilities anyway. $required_capabilities = array(); } else { if (!$xactions && !$this->xactions) { // If we aren't doing anything, require CAN_EDIT to improve consistency. $required_capabilities = array($can_edit); } else { $required_capabilities = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if (!$xtype) { $capabilities = $this->getLegacyRequiredCapabilities($xaction); } else { $capabilities = $xtype->getRequiredCapabilities($object, $xaction); } // For convenience, we allow flexibility in the return types because // it's very unusual that a transaction actually requires multiple // capability checks. if ($capabilities === null) { $capabilities = array(); } else { $capabilities = (array)$capabilities; } foreach ($capabilities as $capability) { $required_capabilities[$capability] = $capability; } } } } $required_capabilities = array_fuse($required_capabilities); $actor = $this->getActor(); if ($required_capabilities) { id(new PhabricatorPolicyFilter()) ->setViewer($actor) ->requireCapabilities($required_capabilities) ->raisePolicyExceptions(true) ->apply(array($object)); } } private function getLegacyRequiredCapabilities( PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: // TODO: Comments technically require CAN_INTERACT, but this is // currently somewhat special and handled through EditEngine. For now, // don't enforce it here. return null; case PhabricatorTransactions::TYPE_SUBSCRIBERS: // TODO: Removing subscribers other than yourself should probably // require CAN_EDIT permission. You can do this via the API but // generally can not via the web interface. return null; case PhabricatorTransactions::TYPE_TOKEN: // TODO: This technically requires CAN_INTERACT, like comments. return null; case PhabricatorTransactions::TYPE_HISTORY: // This is a special magic transaction which sends you history via // email and is only partially supported in the upstream. You don't // need any capabilities to apply it. return null; case PhabricatorTransactions::TYPE_EDGE: return $this->getLegacyRequiredEdgeCapabilities($xaction); default: // For other older (non-modular) transactions, always require exactly // CAN_EDIT. Transactions which do not need CAN_EDIT or need additional // capabilities must move to ModularTransactions. return PhabricatorPolicyCapability::CAN_EDIT; } } private function getLegacyRequiredEdgeCapabilities( PhabricatorApplicationTransaction $xaction) { // You don't need to have edit permission on an object to mention it or // otherwise add a relationship pointing toward it. if ($this->getIsInverseEdgeEditor()) { return null; } $edge_type = $xaction->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorMutedByEdgeType::EDGECONST: // At time of writing, you can only write this edge for yourself, so // you don't need permissions. If you can eventually mute an object // for other users, this would need to be revisited. return null; case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: return null; case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); $actor_phid = $this->requireActor()->getPHID(); $is_join = (($add === array($actor_phid)) && !$rem); $is_leave = (($rem === array($actor_phid)) && !$add); if ($is_join) { // You need CAN_JOIN to join a project. return PhabricatorPolicyCapability::CAN_JOIN; } if ($is_leave) { $object = $this->object; // You usually don't need any capabilities to leave a project... if ($object->getIsMembershipLocked()) { // ...you must be able to edit to leave locked projects, though. return PhabricatorPolicyCapability::CAN_EDIT; } else { return null; } } // You need CAN_EDIT to change members other than yourself. return PhabricatorPolicyCapability::CAN_EDIT; default: return PhabricatorPolicyCapability::CAN_EDIT; } } private function buildSubscribeTransaction( PhabricatorLiskDAO $object, array $xactions, array $changes) { if (!($object instanceof PhabricatorSubscribableInterface)) { return null; } if ($this->shouldEnableMentions($object, $xactions)) { // Identify newly mentioned users. We ignore users who were previously // mentioned so that we don't re-subscribe users after an edit of text // which mentions them. $old_texts = mpull($changes, 'getOldValue'); $new_texts = mpull($changes, 'getNewValue'); $old_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( $this->getActor(), $old_texts); $new_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( $this->getActor(), $new_texts); $phids = array_diff($new_phids, $old_phids); } else { $phids = array(); } $this->mentionedPHIDs = $phids; if ($object->getPHID()) { // Don't try to subscribe already-subscribed mentions: we want to generate // a dialog about an action having no effect if the user explicitly adds // existing CCs, but not if they merely mention existing subscribers. $phids = array_diff($phids, $this->subscribers); } if ($phids) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); $users = mpull($users, null, 'getPHID'); foreach ($phids as $key => $phid) { // Do not subscribe mentioned users // who do not have VIEW Permissions if ($object instanceof PhabricatorPolicyInterface && !PhabricatorPolicyFilter::hasCapability( $users[$phid], $object, PhabricatorPolicyCapability::CAN_VIEW) ) { unset($phids[$key]); } else { if ($object->isAutomaticallySubscribed($phid)) { unset($phids[$key]); } } } $phids = array_values($phids); } // No else here to properly return null should we unset all subscriber if (!$phids) { return null; } $xaction = newv(get_class(head($xactions)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); $xaction->setNewValue(array('+' => $phids)); return $xaction; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $object = $this->object; return $xtype->mergeTransactions($object, $u, $v); } switch ($type) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: return $this->mergePHIDOrEdgeTransactions($u, $v); case PhabricatorTransactions::TYPE_EDGE: $u_type = $u->getMetadataValue('edge:type'); $v_type = $v->getMetadataValue('edge:type'); if ($u_type == $v_type) { return $this->mergePHIDOrEdgeTransactions($u, $v); } return null; } // By default, do not merge the transactions. return null; } /** * Optionally expand transactions which imply other effects. For example, * resigning from a revision in Differential implies removing yourself as * a reviewer. */ protected function expandTransactions( PhabricatorLiskDAO $object, array $xactions) { $results = array(); foreach ($xactions as $xaction) { foreach ($this->expandTransaction($object, $xaction) as $expanded) { $results[] = $expanded; } } return $results; } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return array($xaction); } public function getExpandedSupportTransactions( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $xactions = array($xaction); $xactions = $this->expandSupportTransactions( $object, $xactions); if (count($xactions) == 1) { return array(); } foreach ($xactions as $index => $cxaction) { if ($cxaction === $xaction) { unset($xactions[$index]); break; } } return $xactions; } private function expandSupportTransactions( PhabricatorLiskDAO $object, array $xactions) { $this->loadSubscribers($object); $xactions = $this->applyImplicitCC($object, $xactions); $changes = $this->getRemarkupChanges($xactions); $subscribe_xaction = $this->buildSubscribeTransaction( $object, $xactions, $changes); if ($subscribe_xaction) { $xactions[] = $subscribe_xaction; } // TODO: For now, this is just a placeholder. $engine = PhabricatorMarkupEngine::getEngine('extract'); $engine->setConfig('viewer', $this->requireActor()); $block_xactions = $this->expandRemarkupBlockTransactions( $object, $xactions, $changes, $engine); foreach ($block_xactions as $xaction) { $xactions[] = $xaction; } return $xactions; } private function getRemarkupChanges(array $xactions) { $changes = array(); foreach ($xactions as $key => $xaction) { foreach ($this->getRemarkupChangesFromTransaction($xaction) as $change) { $changes[] = $change; } } return $changes; } private function getRemarkupChangesFromTransaction( PhabricatorApplicationTransaction $transaction) { return $transaction->getRemarkupChanges(); } private function expandRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, array $changes, PhutilMarkupEngine $engine) { $block_xactions = $this->expandCustomRemarkupBlockTransactions( $object, $xactions, $changes, $engine); $mentioned_phids = array(); if ($this->shouldEnableMentions($object, $xactions)) { foreach ($changes as $change) { // Here, we don't care about processing only new mentions after an edit // because there is no way for an object to ever "unmention" itself on // another object, so we can ignore the old value. $engine->markupText($change->getNewValue()); $mentioned_phids += $engine->getTextMetadata( PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS, array()); } } if (!$mentioned_phids) { return $block_xactions; } $mentioned_objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withPHIDs($mentioned_phids) ->execute(); $mentionable_phids = array(); if ($this->shouldEnableMentions($object, $xactions)) { foreach ($mentioned_objects as $mentioned_object) { if ($mentioned_object instanceof PhabricatorMentionableInterface) { $mentioned_phid = $mentioned_object->getPHID(); if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) { continue; } // don't let objects mention themselves if ($object->getPHID() && $mentioned_phid == $object->getPHID()) { continue; } $mentionable_phids[$mentioned_phid] = $mentioned_phid; } } } if ($mentionable_phids) { $edge_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST; $block_xactions[] = newv(get_class(head($xactions)), array()) ->setIgnoreOnNoEffect(true) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_type) ->setNewValue(array('+' => $mentionable_phids)); } return $block_xactions; } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, array $changes, PhutilMarkupEngine $engine) { return array(); } /** * Attempt to combine similar transactions into a smaller number of total * transactions. For example, two transactions which edit the title of an * object can be merged into a single edit. */ private function combineTransactions(array $xactions) { $stray_comments = array(); $result = array(); $types = array(); foreach ($xactions as $key => $xaction) { $type = $xaction->getTransactionType(); if (isset($types[$type])) { foreach ($types[$type] as $other_key) { $other_xaction = $result[$other_key]; // Don't merge transactions with different authors. For example, // don't merge Herald transactions and owners transactions. if ($other_xaction->getAuthorPHID() != $xaction->getAuthorPHID()) { continue; } $merged = $this->mergeTransactions($result[$other_key], $xaction); if ($merged) { $result[$other_key] = $merged; if ($xaction->getComment() && ($xaction->getComment() !== $merged->getComment())) { $stray_comments[] = $xaction->getComment(); } if ($result[$other_key]->getComment() && ($result[$other_key]->getComment() !== $merged->getComment())) { $stray_comments[] = $result[$other_key]->getComment(); } // Move on to the next transaction. continue 2; } } } $result[$key] = $xaction; $types[$type][] = $key; } // If we merged any comments away, restore them. foreach ($stray_comments as $comment) { $xaction = newv(get_class(head($result)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT); $xaction->setComment($comment); $result[] = $xaction; } return array_values($result); } public function mergePHIDOrEdgeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $result = $u->getNewValue(); foreach ($v->getNewValue() as $key => $value) { if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) { if (empty($result[$key])) { $result[$key] = $value; } else { // We're merging two lists of edge adds, sets, or removes. Merge // them by merging individual PHIDs within them. $merged = $result[$key]; foreach ($value as $dst => $v_spec) { if (empty($merged[$dst])) { $merged[$dst] = $v_spec; } else { // Two transactions are trying to perform the same operation on // the same edge. Normalize the edge data and then merge it. This // allows transactions to specify how data merges execute in a // precise way. $u_spec = $merged[$dst]; if (!is_array($u_spec)) { $u_spec = array('dst' => $u_spec); } if (!is_array($v_spec)) { $v_spec = array('dst' => $v_spec); } $ux_data = idx($u_spec, 'data', array()); $vx_data = idx($v_spec, 'data', array()); $merged_data = $this->mergeEdgeData( $u->getMetadataValue('edge:type'), $ux_data, $vx_data); $u_spec['data'] = $merged_data; $merged[$dst] = $u_spec; } } $result[$key] = $merged; } } else { $result[$key] = array_merge($value, idx($result, $key, array())); } } $u->setNewValue($result); // When combining an "ignore" transaction with a normal transaction, make // sure we don't propagate the "ignore" flag. if (!$v->getIgnoreOnNoEffect()) { $u->setIgnoreOnNoEffect(false); } return $u; } protected function mergeEdgeData($type, array $u, array $v) { return $v + $u; } protected function getPHIDTransactionNewValue( PhabricatorApplicationTransaction $xaction, $old = null) { if ($old !== null) { $old = array_fuse($old); } else { $old = array_fuse($xaction->getOldValue()); } return $this->getPHIDList($old, $xaction->getNewValue()); } public function getPHIDList(array $old, array $new) { $new_add = idx($new, '+', array()); unset($new['+']); $new_rem = idx($new, '-', array()); unset($new['-']); $new_set = idx($new, '=', null); if ($new_set !== null) { $new_set = array_fuse($new_set); } unset($new['=']); if ($new) { throw new Exception( pht( "Invalid '%s' value for PHID transaction. Value should contain only ". "keys '%s' (add PHIDs), '%s' (remove PHIDs) and '%s' (set PHIDS).", 'new', '+', '-', '=')); } $result = array(); foreach ($old as $phid) { if ($new_set !== null && empty($new_set[$phid])) { continue; } $result[$phid] = $phid; } if ($new_set !== null) { foreach ($new_set as $phid) { $result[$phid] = $phid; } } foreach ($new_add as $phid) { $result[$phid] = $phid; } foreach ($new_rem as $phid) { unset($result[$phid]); } return array_values($result); } protected function getEdgeTransactionNewValue( PhabricatorApplicationTransaction $xaction) { $new = $xaction->getNewValue(); $new_add = idx($new, '+', array()); unset($new['+']); $new_rem = idx($new, '-', array()); unset($new['-']); $new_set = idx($new, '=', null); unset($new['=']); if ($new) { throw new Exception( pht( "Invalid '%s' value for Edge transaction. Value should contain only ". "keys '%s' (add edges), '%s' (remove edges) and '%s' (set edges).", 'new', '+', '-', '=')); } $old = $xaction->getOldValue(); $lists = array($new_set, $new_add, $new_rem); foreach ($lists as $list) { $this->checkEdgeList($list, $xaction->getMetadataValue('edge:type')); } $result = array(); foreach ($old as $dst_phid => $edge) { if ($new_set !== null && empty($new_set[$dst_phid])) { continue; } $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge, $dst_phid); } if ($new_set !== null) { foreach ($new_set as $dst_phid => $edge) { $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge, $dst_phid); } } foreach ($new_add as $dst_phid => $edge) { $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge, $dst_phid); } foreach ($new_rem as $dst_phid => $edge) { unset($result[$dst_phid]); } return $result; } private function checkEdgeList($list, $edge_type) { if (!$list) { return; } foreach ($list as $key => $item) { if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { throw new Exception( pht( 'Edge transactions must have destination PHIDs as in edge '. 'lists (found key "%s" on transaction of type "%s").', $key, $edge_type)); } if (!is_array($item) && $item !== $key) { throw new Exception( pht( 'Edge transactions must have PHIDs or edge specs as values '. '(found value "%s" on transaction of type "%s").', $item, $edge_type)); } } } private function normalizeEdgeTransactionValue( PhabricatorApplicationTransaction $xaction, $edge, $dst_phid) { if (!is_array($edge)) { if ($edge != $dst_phid) { throw new Exception( pht( 'Transaction edge data must either be the edge PHID or an edge '. 'specification dictionary.')); } $edge = array(); } else { foreach ($edge as $key => $value) { switch ($key) { case 'src': case 'dst': case 'type': case 'data': case 'dateCreated': case 'dateModified': case 'seq': case 'dataID': break; default: throw new Exception( pht( 'Transaction edge specification contains unexpected key "%s".', $key)); } } } $edge['dst'] = $dst_phid; $edge_type = $xaction->getMetadataValue('edge:type'); if (empty($edge['type'])) { $edge['type'] = $edge_type; } else { if ($edge['type'] != $edge_type) { $this_type = $edge['type']; throw new Exception( pht( "Edge transaction includes edge of type '%s', but ". "transaction is of type '%s'. Each edge transaction ". "must alter edges of only one type.", $this_type, $edge_type)); } } if (!isset($edge['data'])) { $edge['data'] = array(); } return $edge; } protected function sortTransactions(array $xactions) { $head = array(); $tail = array(); // Move bare comments to the end, so the actions precede them. foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PhabricatorTransactions::TYPE_COMMENT) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function filterTransactions( PhabricatorLiskDAO $object, array $xactions) { $type_comment = PhabricatorTransactions::TYPE_COMMENT; $no_effect = array(); $has_comment = false; $any_effect = false; foreach ($xactions as $key => $xaction) { if ($this->transactionHasEffect($object, $xaction)) { if ($xaction->getTransactionType() != $type_comment) { $any_effect = true; } } else if ($xaction->getIgnoreOnNoEffect()) { unset($xactions[$key]); } else { $no_effect[$key] = $xaction; } if ($xaction->hasComment()) { $has_comment = true; } } if (!$no_effect) { return $xactions; } if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) { throw new PhabricatorApplicationTransactionNoEffectException( $no_effect, $any_effect, $has_comment); } if (!$any_effect && !$has_comment) { // If we only have empty comment transactions, just drop them all. return array(); } foreach ($no_effect as $key => $xaction) { if ($xaction->hasComment()) { $xaction->setTransactionType($type_comment); $xaction->setOldValue(null); $xaction->setNewValue(null); } else { unset($xactions[$key]); } } return $xactions; } /** * Hook for validating transactions. This callback will be invoked for each * available transaction type, even if an edit does not apply any transactions * of that type. This allows you to raise exceptions when required fields are * missing, by detecting that the object has no field value and there is no * transaction which sets one. * * @param PhabricatorLiskDAO Object being edited. * @param string Transaction type to validate. * @param list Transactions of given type, * which may be empty if the edit does not apply any transactions of the * given type. * @return list List of * validation errors. */ protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = array(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $errors[] = $xtype->validateTransactions($object, $xactions); } switch ($type) { case PhabricatorTransactions::TYPE_VIEW_POLICY: $errors[] = $this->validatePolicyTransaction( $object, $xactions, $type, PhabricatorPolicyCapability::CAN_VIEW); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: $errors[] = $this->validatePolicyTransaction( $object, $xactions, $type, PhabricatorPolicyCapability::CAN_EDIT); break; case PhabricatorTransactions::TYPE_SPACE: $errors[] = $this->validateSpaceTransactions( $object, $xactions, $type); break; case PhabricatorTransactions::TYPE_SUBTYPE: $errors[] = $this->validateSubtypeTransactions( $object, $xactions, $type); break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $groups = array(); foreach ($xactions as $xaction) { $groups[$xaction->getMetadataValue('customfield:key')][] = $xaction; } $field_list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_EDIT); $field_list->setViewer($this->getActor()); $role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS; foreach ($field_list->getFields() as $field) { if (!$field->shouldEnableForRole($role_xactions)) { continue; } $errors[] = $field->validateApplicationTransactions( $this, $type, idx($groups, $field->getFieldKey(), array())); } break; } return array_mergev($errors); } public function validatePolicyTransaction( PhabricatorLiskDAO $object, array $xactions, $transaction_type, $capability) { $actor = $this->requireActor(); $errors = array(); // Note $this->xactions is necessary; $xactions is $this->xactions of // $transaction_type $policy_object = $this->adjustObjectForPolicyChecks( $object, $this->xactions); // Make sure the user isn't editing away their ability to $capability this // object. foreach ($xactions as $xaction) { try { PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy( $actor, $policy_object, $capability, $xaction->getNewValue()); } catch (PhabricatorPolicyException $ex) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht( 'You can not select this %s policy, because you would no longer '. 'be able to %s the object.', $capability, $capability), $xaction); } } if ($this->getIsNewObject()) { if (!$xactions) { $has_capability = PhabricatorPolicyFilter::hasCapability( $actor, $policy_object, $capability); if (!$has_capability) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht( 'The selected %s policy excludes you. Choose a %s policy '. 'which allows you to %s the object.', $capability, $capability, $capability)); } } } return $errors; } private function validateSpaceTransactions( PhabricatorLiskDAO $object, array $xactions, $transaction_type) { $errors = array(); $actor = $this->getActor(); $has_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($actor); $actor_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($actor); $active_spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces( $actor); foreach ($xactions as $xaction) { $space_phid = $xaction->getNewValue(); if ($space_phid === null) { if (!$has_spaces) { // The install doesn't have any spaces, so this is fine. continue; } // The install has some spaces, so every object needs to be put // in a valid space. $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht('You must choose a space for this object.'), $xaction); continue; } // If the PHID isn't `null`, it needs to be a valid space that the // viewer can see. if (empty($actor_spaces[$space_phid])) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht( 'You can not shift this object in the selected space, because '. 'the space does not exist or you do not have access to it.'), $xaction); } else if (empty($active_spaces[$space_phid])) { // It's OK to edit objects in an archived space, so just move on if // we aren't adjusting the value. $old_space_phid = $this->getTransactionOldValue($object, $xaction); if ($space_phid == $old_space_phid) { continue; } $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Archived'), pht( 'You can not shift this object into the selected space, because '. 'the space is archived. Objects can not be created inside (or '. 'moved into) archived spaces.'), $xaction); } } return $errors; } private function validateSubtypeTransactions( PhabricatorLiskDAO $object, array $xactions, $transaction_type) { $errors = array(); $map = $object->newEditEngineSubtypeMap(); $old = $object->getEditEngineSubtype(); foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); if ($old == $new) { continue; } if (!$map->isValidSubtype($new)) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), pht( 'The subtype "%s" is not a valid subtype.', $new), $xaction); continue; } } return $errors; } protected function adjustObjectForPolicyChecks( PhabricatorLiskDAO $object, array $xactions) { $copy = clone $object; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: $clone_xaction = clone $xaction; $clone_xaction->setOldValue(array_values($this->subscribers)); $clone_xaction->setNewValue( $this->getPHIDTransactionNewValue( $clone_xaction)); PhabricatorPolicyRule::passTransactionHintToRule( $copy, new PhabricatorSubscriptionsSubscribersPolicyRule(), array_fuse($clone_xaction->getNewValue())); break; case PhabricatorTransactions::TYPE_SPACE: $space_phid = $this->getTransactionNewValue($object, $xaction); $copy->setSpacePHID($space_phid); break; } } return $copy; } protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { return array(); } /** * Check for a missing text field. * * A text field is missing if the object has no value and there are no * transactions which set a value, or if the transactions remove the value. * This method is intended to make implementing @{method:validateTransaction} * more convenient: * * $missing = $this->validateIsEmptyTextField( * $object->getName(), * $xactions); * * This will return `true` if the net effect of the object and transactions * is an empty field. * * @param wild Current field value. * @param list Transactions editing the * field. * @return bool True if the field will be an empty text field after edits. */ protected function validateIsEmptyTextField($field_value, array $xactions) { if (strlen($field_value) && empty($xactions)) { return false; } if ($xactions && strlen(last($xactions)->getNewValue())) { return false; } return true; } /* -( Implicit CCs )------------------------------------------------------- */ /** * When a user interacts with an object, we might want to add them to CC. */ final public function applyImplicitCC( PhabricatorLiskDAO $object, array $xactions) { if (!($object instanceof PhabricatorSubscribableInterface)) { // If the object isn't subscribable, we can't CC them. return $xactions; } $actor_phid = $this->getActingAsPHID(); $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; if (phid_get_type($actor_phid) != $type_user) { // Transactions by application actors like Herald, Harbormaster and // Diffusion should not CC the applications. return $xactions; } if ($object->isAutomaticallySubscribed($actor_phid)) { // If they're auto-subscribed, don't CC them. return $xactions; } $should_cc = false; foreach ($xactions as $xaction) { if ($this->shouldImplyCC($object, $xaction)) { $should_cc = true; break; } } if (!$should_cc) { // Only some types of actions imply a CC (like adding a comment). return $xactions; } if ($object->getPHID()) { if (isset($this->subscribers[$actor_phid])) { // If the user is already subscribed, don't implicitly CC them. return $xactions; } $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST); $unsub = array_fuse($unsub); if (isset($unsub[$actor_phid])) { // If the user has previously unsubscribed from this object explicitly, // don't implicitly CC them. return $xactions; } } $xaction = newv(get_class(head($xactions)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); $xaction->setNewValue(array('+' => array($actor_phid))); array_unshift($xactions, $xaction); return $xactions; } protected function shouldImplyCC( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return $xaction->isCommentTransaction(); } /* -( Sending Mail )------------------------------------------------------- */ /** * @task mail */ protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return false; } /** * @task mail */ private function buildMail( PhabricatorLiskDAO $object, array $xactions) { $email_to = $this->mailToPHIDs; $email_cc = $this->mailCCPHIDs; $email_cc = array_merge($email_cc, $this->heraldEmailPHIDs); $unexpandable = $this->mailUnexpandablePHIDs; if (!is_array($unexpandable)) { $unexpandable = array(); } $messages = $this->buildMailWithRecipients( $object, $xactions, $email_to, $email_cc, $unexpandable); $this->runHeraldMailRules($messages); return $messages; } private function buildMailWithRecipients( PhabricatorLiskDAO $object, array $xactions, array $email_to, array $email_cc, array $unexpandable) { $targets = $this->buildReplyHandler($object) ->setUnexpandablePHIDs($unexpandable) ->getMailTargets($email_to, $email_cc); // Set this explicitly before we start swapping out the effective actor. $this->setActingAsPHID($this->getActingAsPHID()); $messages = array(); foreach ($targets as $target) { $original_actor = $this->getActor(); $viewer = $target->getViewer(); $this->setActor($viewer); $locale = PhabricatorEnv::beginScopedLocale($viewer->getTranslation()); $caught = null; $mail = null; try { // Reload handles for the new viewer. $this->loadHandles($xactions); $mail = $this->buildMailForTarget($object, $xactions, $target); if ($mail) { if ($this->mustEncrypt) { $mail ->setMustEncrypt(true) ->setMustEncryptReasons($this->mustEncrypt); } } } catch (Exception $ex) { $caught = $ex; } $this->setActor($original_actor); unset($locale); if ($caught) { throw $ex; } if ($mail) { $messages[] = $mail; } } return $messages; } protected function getTransactionsForMail( PhabricatorLiskDAO $object, array $xactions) { return $xactions; } private function buildMailForTarget( PhabricatorLiskDAO $object, array $xactions, PhabricatorMailTarget $target) { // Check if any of the transactions are visible for this viewer. If we // don't have any visible transactions, don't send the mail. $any_visible = false; foreach ($xactions as $xaction) { if (!$xaction->shouldHideForMail($xactions)) { $any_visible = true; break; } } if (!$any_visible) { return null; } $mail_xactions = $this->getTransactionsForMail($object, $xactions); $mail = $this->buildMailTemplate($object); $body = $this->buildMailBody($object, $mail_xactions); $mail_tags = $this->getMailTags($object, $mail_xactions); $action = $this->getMailAction($object, $mail_xactions); $stamps = $this->generateMailStamps($object, $this->mailStamps); if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) { $this->addEmailPreferenceSectionToMailBody( $body, $object, $mail_xactions); } $muted_phids = $this->mailMutedPHIDs; if (!is_array($muted_phids)) { $muted_phids = array(); } $mail ->setSensitiveContent(false) ->setFrom($this->getActingAsPHID()) ->setSubjectPrefix($this->getMailSubjectPrefix()) ->setVarySubjectPrefix('['.$action.']') ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) ->setRelatedPHID($object->getPHID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setMutedPHIDs($muted_phids) ->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs) ->setMailTags($mail_tags) ->setIsBulk(true) ->setBody($body->render()) ->setHTMLBody($body->renderHTML()); foreach ($body->getAttachments() as $attachment) { $mail->addAttachment($attachment); } if ($this->heraldHeader) { $mail->addHeader('X-Herald-Rules', $this->heraldHeader); } if ($object instanceof PhabricatorProjectInterface) { $this->addMailProjectMetadata($object, $mail); } if ($this->getParentMessageID()) { $mail->setParentMessageID($this->getParentMessageID()); } // If we have stamps, attach the raw dictionary version (not the actual // objects) to the mail so that debugging tools can see what we used to // render the final list. if ($this->mailStamps) { $mail->setMailStampMetadata($this->mailStamps); } // If we have rendered stamps, attach them to the mail. if ($stamps) { $mail->setMailStamps($stamps); } return $target->willSendMail($mail); } private function addMailProjectMetadata( PhabricatorLiskDAO $object, PhabricatorMetaMTAMail $template) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); if (!$project_phids) { return; } // TODO: This viewer isn't quite right. It would be slightly better to use // the mail recipient, but that's not very easy given the way rendering // works today. $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($project_phids) ->execute(); $project_tags = array(); foreach ($handles as $handle) { if (!$handle->isComplete()) { continue; } $project_tags[] = '<'.$handle->getObjectName().'>'; } if (!$project_tags) { return; } $project_tags = implode(', ', $project_tags); $template->addHeader('X-Phabricator-Projects', $project_tags); } protected function getMailThreadID(PhabricatorLiskDAO $object) { return $object->getPHID(); } /** * @task mail */ protected function getStrongestAction( PhabricatorLiskDAO $object, array $xactions) { return last(msort($xactions, 'getActionStrength')); } /** * @task mail */ protected function buildReplyHandler(PhabricatorLiskDAO $object) { throw new Exception(pht('Capability not supported.')); } /** * @task mail */ protected function getMailSubjectPrefix() { throw new Exception(pht('Capability not supported.')); } /** * @task mail */ protected function getMailTags( PhabricatorLiskDAO $object, array $xactions) { $tags = array(); foreach ($xactions as $xaction) { $tags[] = $xaction->getMailTags(); } return array_mergev($tags); } /** * @task mail */ public function getMailTagsMap() { // TODO: We should move shared mail tags, like "comment", here. return array(); } /** * @task mail */ protected function getMailAction( PhabricatorLiskDAO $object, array $xactions) { return $this->getStrongestAction($object, $xactions)->getActionName(); } /** * @task mail */ protected function buildMailTemplate(PhabricatorLiskDAO $object) { throw new Exception(pht('Capability not supported.')); } /** * @task mail */ protected function getMailTo(PhabricatorLiskDAO $object) { throw new Exception(pht('Capability not supported.')); } protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) { return array(); } /** * @task mail */ protected function getMailCC(PhabricatorLiskDAO $object) { $phids = array(); $has_support = false; if ($object instanceof PhabricatorSubscribableInterface) { $phid = $object->getPHID(); $phids[] = PhabricatorSubscribersQuery::loadSubscribersForPHID($phid); $has_support = true; } if ($object instanceof PhabricatorProjectInterface) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); if ($project_phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($project_phids) ->needWatchers(true) ->execute(); $watcher_phids = array(); foreach ($projects as $project) { foreach ($project->getAllAncestorWatcherPHIDs() as $phid) { $watcher_phids[$phid] = $phid; } } if ($watcher_phids) { // We need to do a visibility check for all the watchers, as // watching a project is not a guarantee that you can see objects // associated with it. $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($watcher_phids) ->execute(); $watchers = array(); foreach ($users as $user) { $can_see = PhabricatorPolicyFilter::hasCapability( $user, $object, PhabricatorPolicyCapability::CAN_VIEW); if ($can_see) { $watchers[] = $user->getPHID(); } } $phids[] = $watchers; } } $has_support = true; } if (!$has_support) { throw new Exception( pht('The object being edited does not implement any standard '. 'interfaces (like PhabricatorSubscribableInterface) which allow '. 'CCs to be generated automatically. Override the "getMailCC()" '. 'method and generate CCs explicitly.')); } return array_mergev($phids); } /** * @task mail */ protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = id(new PhabricatorMetaMTAMailBody()) ->setViewer($this->requireActor()) ->setContextObject($object); $this->addHeadersAndCommentsToMailBody($body, $xactions); $this->addCustomFieldsToMailBody($body, $object, $xactions); return $body; } /** * @task mail */ protected function addEmailPreferenceSectionToMailBody( PhabricatorMetaMTAMailBody $body, PhabricatorLiskDAO $object, array $xactions) { $href = PhabricatorEnv::getProductionURI( '/settings/panel/emailpreferences/'); $body->addLinkSection(pht('EMAIL PREFERENCES'), $href); } /** * @task mail */ protected function addHeadersAndCommentsToMailBody( PhabricatorMetaMTAMailBody $body, array $xactions, $object_label = null, $object_href = null) { // First, remove transactions which shouldn't be rendered in mail. foreach ($xactions as $key => $xaction) { if ($xaction->shouldHideForMail($xactions)) { unset($xactions[$key]); } } $headers = array(); $headers_html = array(); $comments = array(); $details = array(); $seen_comment = false; foreach ($xactions as $xaction) { // Most mail has zero or one comments. In these cases, we render the // "alice added a comment." transaction in the header, like a normal // transaction. // Some mail, like Differential undraft mail or "!history" mail, may // have two or more comments. In these cases, we'll put the first // "alice added a comment." transaction in the header normally, but // move the other transactions down so they provide context above the // actual comment. $comment = $xaction->getBodyForMail(); if ($comment !== null) { $is_comment = true; $comments[] = array( 'xaction' => $xaction, 'comment' => $comment, 'initial' => !$seen_comment, ); } else { $is_comment = false; } if (!$is_comment || !$seen_comment) { $header = $xaction->getTitleForMail(); if ($header !== null) { $headers[] = $header; } $header_html = $xaction->getTitleForHTMLMail(); if ($header_html !== null) { $headers_html[] = $header_html; } } if ($xaction->hasChangeDetailsForMail()) { $details[] = $xaction; } if ($is_comment) { $seen_comment = true; } } $headers_text = implode("\n", $headers); $body->addRawPlaintextSection($headers_text); $headers_html = phutil_implode_html(phutil_tag('br'), $headers_html); $header_button = null; if ($object_label !== null) { $button_style = array( 'text-decoration: none;', 'padding: 4px 8px;', 'margin: 0 8px 8px;', 'float: right;', 'color: #464C5C;', 'font-weight: bold;', 'border-radius: 3px;', 'background-color: #F7F7F9;', 'background-image: linear-gradient(to bottom,#fff,#f1f0f1);', 'display: inline-block;', 'border: 1px solid rgba(71,87,120,.2);', ); $header_button = phutil_tag( 'a', array( 'style' => implode(' ', $button_style), 'href' => $object_href, ), $object_label); } $xactions_style = array(); $header_action = phutil_tag( 'td', array(), $header_button); $header_action = phutil_tag( 'td', array( 'style' => implode(' ', $xactions_style), ), array( $headers_html, // Add an extra newline to prevent the "View Object" button from // running into the transaction text in Mail.app text snippet // previews. "\n", )); $headers_html = phutil_tag( 'table', array(), phutil_tag('tr', array(), array($header_action, $header_button))); $body->addRawHTMLSection($headers_html); foreach ($comments as $spec) { $xaction = $spec['xaction']; $comment = $spec['comment']; $is_initial = $spec['initial']; // If this is not the first comment in the mail, add the header showing // who wrote the comment immediately above the comment. if (!$is_initial) { $header = $xaction->getTitleForMail(); if ($header !== null) { $body->addRawPlaintextSection($header); } $header_html = $xaction->getTitleForHTMLMail(); if ($header_html !== null) { $body->addRawHTMLSection($header_html); } } $body->addRemarkupSection(null, $comment); } foreach ($details as $xaction) { $details = $xaction->renderChangeDetailsForMail($body->getViewer()); if ($details !== null) { $label = $this->getMailDiffSectionHeader($xaction); $body->addHTMLSection($label, $details); } } } private function getMailDiffSectionHeader($xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { return $xtype->getMailDiffSectionHeader(); } return pht('EDIT DETAILS'); } /** * @task mail */ protected function addCustomFieldsToMailBody( PhabricatorMetaMTAMailBody $body, PhabricatorLiskDAO $object, array $xactions) { if ($object instanceof PhabricatorCustomFieldInterface) { $field_list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_TRANSACTIONMAIL); $field_list->setViewer($this->getActor()); $field_list->readFieldsFromStorage($object); foreach ($field_list->getFields() as $field) { $field->updateTransactionMailBody( $body, $this, $xactions); } } } /** * @task mail */ private function runHeraldMailRules(array $messages) { foreach ($messages as $message) { $engine = new HeraldEngine(); $adapter = id(new PhabricatorMailOutboundMailHeraldAdapter()) ->setObject($message); $rules = $engine->loadRulesForAdapter($adapter); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); } } /* -( Publishing Feed Stories )-------------------------------------------- */ /** * @task feed */ protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return false; } /** * @task feed */ protected function getFeedStoryType() { return 'PhabricatorApplicationTransactionFeedStory'; } /** * @task feed */ protected function getFeedRelatedPHIDs( PhabricatorLiskDAO $object, array $xactions) { $phids = array( $object->getPHID(), $this->getActingAsPHID(), ); if ($object instanceof PhabricatorProjectInterface) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); foreach ($project_phids as $project_phid) { $phids[] = $project_phid; } } return $phids; } /** * @task feed */ protected function getFeedNotifyPHIDs( PhabricatorLiskDAO $object, array $xactions) { // If some transactions are forcing notification delivery, add the forced // recipients to the notify list. $force_list = array(); foreach ($xactions as $xaction) { $force_phids = $xaction->getForceNotifyPHIDs(); if (!$force_phids) { continue; } foreach ($force_phids as $force_phid) { $force_list[] = $force_phid; } } $to_list = $this->getMailTo($object); $cc_list = $this->getMailCC($object); $full_list = array_merge($force_list, $to_list, $cc_list); $full_list = array_fuse($full_list); return array_keys($full_list); } /** * @task feed */ protected function getFeedStoryData( PhabricatorLiskDAO $object, array $xactions) { $xactions = msort($xactions, 'getActionStrength'); $xactions = array_reverse($xactions); return array( 'objectPHID' => $object->getPHID(), 'transactionPHIDs' => mpull($xactions, 'getPHID'), ); } /** * @task feed */ protected function publishFeedStory( PhabricatorLiskDAO $object, array $xactions, array $mailed_phids) { // Remove transactions which don't publish feed stories or notifications. // These never show up anywhere, so we don't need to do anything with them. foreach ($xactions as $key => $xaction) { if (!$xaction->shouldHideForFeed()) { continue; } if (!$xaction->shouldHideForNotifications()) { continue; } unset($xactions[$key]); } if (!$xactions) { return; } $related_phids = $this->feedRelatedPHIDs; $subscribed_phids = $this->feedNotifyPHIDs; // Remove muted users from the subscription list so they don't get // notifications, either. $muted_phids = $this->mailMutedPHIDs; if (!is_array($muted_phids)) { $muted_phids = array(); } $subscribed_phids = array_fuse($subscribed_phids); foreach ($muted_phids as $muted_phid) { unset($subscribed_phids[$muted_phid]); } $subscribed_phids = array_values($subscribed_phids); $story_type = $this->getFeedStoryType(); $story_data = $this->getFeedStoryData($object, $xactions); $unexpandable_phids = $this->mailUnexpandablePHIDs; if (!is_array($unexpandable_phids)) { $unexpandable_phids = array(); } id(new PhabricatorFeedStoryPublisher()) ->setStoryType($story_type) ->setStoryData($story_data) ->setStoryTime(time()) ->setStoryAuthorPHID($this->getActingAsPHID()) ->setRelatedPHIDs($related_phids) ->setPrimaryObjectPHID($object->getPHID()) ->setSubscribedPHIDs($subscribed_phids) ->setUnexpandablePHIDs($unexpandable_phids) ->setMailRecipientPHIDs($mailed_phids) ->setMailTags($this->getMailTags($object, $xactions)) ->publish(); } /* -( Search Index )------------------------------------------------------- */ /** * @task search */ protected function supportsSearch() { return false; } /* -( Herald Integration )-------------------------------------------------- */ protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return false; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { throw new Exception(pht('No herald adapter specified.')); } private function setHeraldAdapter(HeraldAdapter $adapter) { $this->heraldAdapter = $adapter; return $this; } protected function getHeraldAdapter() { return $this->heraldAdapter; } private function setHeraldTranscript(HeraldTranscript $transcript) { $this->heraldTranscript = $transcript; return $this; } protected function getHeraldTranscript() { return $this->heraldTranscript; } private function applyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { $adapter = $this->buildHeraldAdapter($object, $xactions) ->setContentSource($this->getContentSource()) ->setIsNewObject($this->getIsNewObject()) ->setActingAsPHID($this->getActingAsPHID()) ->setAppliedTransactions($xactions); if ($this->getApplicationEmail()) { $adapter->setApplicationEmail($this->getApplicationEmail()); } // If this editor is operating in silent mode, tell Herald that we aren't // going to send any mail. This allows it to skip "the first time this // rule matches, send me an email" rules which would otherwise match even // though we aren't going to send any mail. if ($this->getIsSilent()) { $adapter->setForbiddenAction( HeraldMailableState::STATECONST, HeraldCoreStateReasons::REASON_SILENT); } $xscript = HeraldEngine::loadAndApplyRules($adapter); $this->setHeraldAdapter($adapter); $this->setHeraldTranscript($xscript); if ($adapter instanceof HarbormasterBuildableAdapterInterface) { $buildable_phid = $adapter->getHarbormasterBuildablePHID(); HarbormasterBuildable::applyBuildPlans( $buildable_phid, $adapter->getHarbormasterContainerPHID(), $adapter->getQueuedHarbormasterBuildRequests()); // Whether we queued any builds or not, any automatic buildable for this // object is now done preparing builds and can transition into a // completed status. $buildables = id(new HarbormasterBuildableQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withManualBuildables(false) ->withBuildablePHIDs(array($buildable_phid)) ->execute(); foreach ($buildables as $buildable) { // If this buildable has already moved beyond preparation, we don't // need to nudge it again. if (!$buildable->isPreparing()) { continue; } $buildable->sendMessage( $this->getActor(), HarbormasterMessageType::BUILDABLE_BUILD, true); } } $this->mustEncrypt = $adapter->getMustEncryptReasons(); return array_merge( $this->didApplyHeraldRules($object, $adapter, $xscript), $adapter->getQueuedTransactions()); } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { return array(); } /* -( Custom Fields )------------------------------------------------------ */ /** * @task customfield */ private function getCustomFieldForTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $field_key = $xaction->getMetadataValue('customfield:key'); if (!$field_key) { throw new Exception( pht( "Custom field transaction has no '%s'!", 'customfield:key')); } $field = PhabricatorCustomField::getObjectField( $object, PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $field_key); if (!$field) { throw new Exception( pht( "Custom field transaction has invalid '%s'; field '%s' ". "is disabled or does not exist.", 'customfield:key', $field_key)); } if (!$field->shouldAppearInApplicationTransactions()) { throw new Exception( pht( "Custom field transaction '%s' does not implement ". "integration for %s.", $field_key, 'ApplicationTransactions')); } $field->setViewer($this->getActor()); return $field; } /* -( Files )-------------------------------------------------------------- */ /** * Extract the PHIDs of any files which these transactions attach. * * @task files */ private function extractFilePHIDs( PhabricatorLiskDAO $object, array $xactions) { $changes = $this->getRemarkupChanges($xactions); $blocks = mpull($changes, 'getNewValue'); $phids = array(); if ($blocks) { $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( $this->getActor(), $blocks); } foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if ($xtype) { $phids[] = $xtype->extractFilePHIDs($object, $xaction->getNewValue()); } else { $phids[] = $this->extractFilePHIDsFromCustomTransaction( $object, $xaction); } } $phids = array_unique(array_filter(array_mergev($phids))); if (!$phids) { return array(); } // Only let a user attach files they can actually see, since this would // otherwise let you access any file by attaching it to an object you have // view permission on. $files = id(new PhabricatorFileQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); return mpull($files, 'getPHID'); } /** * @task files */ protected function extractFilePHIDsFromCustomTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return array(); } /** * @task files */ private function attachFiles( PhabricatorLiskDAO $object, array $file_phids) { if (!$file_phids) { return; } $editor = new PhabricatorEdgeEditor(); $src = $object->getPHID(); $type = PhabricatorObjectHasFileEdgeType::EDGECONST; foreach ($file_phids as $dst) { $editor->addEdge($src, $type, $dst); } $editor->save(); } private function applyInverseEdgeTransactions( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction, $inverse_type) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); $add = array_fuse($add); $rem = array_fuse($rem); $all = $add + $rem; $nodes = id(new PhabricatorObjectQuery()) ->setViewer($this->requireActor()) ->withPHIDs($all) ->execute(); foreach ($nodes as $node) { if (!($node instanceof PhabricatorApplicationTransactionInterface)) { continue; } if ($node instanceof PhabricatorUser) { // TODO: At least for now, don't record inverse edge transactions // for users (for example, "alincoln joined project X"): Feed fills // this role instead. continue; } $editor = $node->getApplicationTransactionEditor(); $template = $node->getApplicationTransactionTemplate(); - $target = $node->getApplicationTransactionObject(); if (isset($add[$node->getPHID()])) { $edge_edit_type = '+'; } else { $edge_edit_type = '-'; } $template ->setTransactionType($xaction->getTransactionType()) ->setMetadataValue('edge:type', $inverse_type) ->setNewValue( array( $edge_edit_type => array($object->getPHID() => $object->getPHID()), )); $editor ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setParentMessageID($this->getParentMessageID()) ->setIsInverseEdgeEditor(true) ->setIsSilent($this->getIsSilent()) ->setActor($this->requireActor()) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()); - $editor->applyTransactions($target, array($template)); + $editor->applyTransactions($node, array($template)); } } /* -( Workers )------------------------------------------------------------ */ /** * Load any object state which is required to publish transactions. * * This hook is invoked in the main process before we compute data related * to publishing transactions (like email "To" and "CC" lists), and again in * the worker before publishing occurs. * * @return object Publishable object. * @task workers */ protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { return $object; } /** * Convert the editor state to a serializable dictionary which can be passed * to a worker. * * This data will be loaded with @{method:loadWorkerState} in the worker. * * @return dict Serializable editor state. * @task workers */ final private function getWorkerState() { $state = array(); foreach ($this->getAutomaticStateProperties() as $property) { $state[$property] = $this->$property; } $custom_state = $this->getCustomWorkerState(); $custom_encoding = $this->getCustomWorkerStateEncoding(); $state += array( 'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(), 'custom' => $this->encodeStateForStorage($custom_state, $custom_encoding), 'custom.encoding' => $custom_encoding, ); return $state; } /** * Hook; return custom properties which need to be passed to workers. * * @return dict Custom properties. * @task workers */ protected function getCustomWorkerState() { return array(); } /** * Hook; return storage encoding for custom properties which need to be * passed to workers. * * This primarily allows binary data to be passed to workers and survive * JSON encoding. * * @return dict Property encodings. * @task workers */ protected function getCustomWorkerStateEncoding() { return array(); } /** * Load editor state using a dictionary emitted by @{method:getWorkerState}. * * This method is used to load state when running worker operations. * * @param dict Editor state, from @{method:getWorkerState}. * @return this * @task workers */ final public function loadWorkerState(array $state) { foreach ($this->getAutomaticStateProperties() as $property) { $this->$property = idx($state, $property); } $exclude = idx($state, 'excludeMailRecipientPHIDs', array()); $this->setExcludeMailRecipientPHIDs($exclude); $custom_state = idx($state, 'custom', array()); $custom_encodings = idx($state, 'custom.encoding', array()); $custom = $this->decodeStateFromStorage($custom_state, $custom_encodings); $this->loadCustomWorkerState($custom); return $this; } /** * Hook; set custom properties on the editor from data emitted by * @{method:getCustomWorkerState}. * * @param dict Custom state, * from @{method:getCustomWorkerState}. * @return this * @task workers */ protected function loadCustomWorkerState(array $state) { return $this; } /** * Get a list of object properties which should be automatically sent to * workers in the state data. * * These properties will be automatically stored and loaded by the editor in * the worker. * * @return list List of properties. * @task workers */ private function getAutomaticStateProperties() { return array( 'parentMessageID', 'isNewObject', 'heraldEmailPHIDs', 'heraldForcedEmailPHIDs', 'heraldHeader', 'mailToPHIDs', 'mailCCPHIDs', 'feedNotifyPHIDs', 'feedRelatedPHIDs', 'feedShouldPublish', 'mailShouldSend', 'mustEncrypt', 'mailStamps', 'mailUnexpandablePHIDs', 'mailMutedPHIDs', 'webhookMap', 'silent', 'sendHistory', ); } /** * Apply encodings prior to storage. * * See @{method:getCustomWorkerStateEncoding}. * * @param map Map of values to encode. * @param map Map of encodings to apply. * @return map Map of encoded values. * @task workers */ final private function encodeStateForStorage( array $state, array $encodings) { foreach ($state as $key => $value) { $encoding = idx($encodings, $key); switch ($encoding) { case self::STORAGE_ENCODING_BINARY: // The mechanics of this encoding (serialize + base64) are a little // awkward, but it allows us encode arrays and still be JSON-safe // with binary data. $value = @serialize($value); if ($value === false) { throw new Exception( pht( 'Failed to serialize() value for key "%s".', $key)); } $value = base64_encode($value); if ($value === false) { throw new Exception( pht( 'Failed to base64 encode value for key "%s".', $key)); } break; } $state[$key] = $value; } return $state; } /** * Undo storage encoding applied when storing state. * * See @{method:getCustomWorkerStateEncoding}. * * @param map Map of encoded values. * @param map Map of encodings. * @return map Map of decoded values. * @task workers */ final private function decodeStateFromStorage( array $state, array $encodings) { foreach ($state as $key => $value) { $encoding = idx($encodings, $key); switch ($encoding) { case self::STORAGE_ENCODING_BINARY: $value = base64_decode($value); if ($value === false) { throw new Exception( pht( 'Failed to base64_decode() value for key "%s".', $key)); } $value = unserialize($value); break; } $state[$key] = $value; } return $state; } /** * Remove conflicts from a list of projects. * * Objects aren't allowed to be tagged with multiple milestones in the same * group, nor projects such that one tag is the ancestor of any other tag. * If the list of PHIDs include mutually exclusive projects, remove the * conflicting projects. * * @param list List of project PHIDs. * @return list List with conflicts removed. */ private function applyProjectConflictRules(array $phids) { if (!$phids) { return array(); } // Overall, the last project in the list wins in cases of conflict (so when // you add something, the thing you just added sticks and removes older // values). // Beyond that, there are two basic cases: // Milestones: An object can't be in "A > Sprint 3" and "A > Sprint 4". // If multiple projects are milestones of the same parent, we only keep the // last one. // Ancestor: You can't be in "A" and "A > B". If "A > B" comes later // in the list, we remove "A" and keep "A > B". If "A" comes later, we // remove "A > B" and keep "A". // Note that it's OK to be in "A > B" and "A > C". There's only a conflict // if one project is an ancestor of another. It's OK to have something // tagged with multiple projects which share a common ancestor, so long as // they are not mutual ancestors. $viewer = PhabricatorUser::getOmnipotentUser(); $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($phids)) ->execute(); $projects = mpull($projects, null, 'getPHID'); // We're going to build a map from each project with milestones to the last // milestone in the list. This last milestone is the milestone we'll keep. $milestone_map = array(); // We're going to build a set of the projects which have no descendants // later in the list. This allows us to apply both ancestor rules. $ancestor_map = array(); foreach ($phids as $phid => $ignored) { $project = idx($projects, $phid); if (!$project) { continue; } // This is the last milestone we've seen, so set it as the selection for // the project's parent. This might be setting a new value or overwriting // an earlier value. if ($project->isMilestone()) { $parent_phid = $project->getParentProjectPHID(); $milestone_map[$parent_phid] = $phid; } // Since this is the last item in the list we've examined so far, add it // to the set of projects with no later descendants. $ancestor_map[$phid] = $phid; // Remove any ancestors from the set, since this is a later descendant. foreach ($project->getAncestorProjects() as $ancestor) { $ancestor_phid = $ancestor->getPHID(); unset($ancestor_map[$ancestor_phid]); } } // Now that we've built the maps, we can throw away all the projects which // have conflicts. foreach ($phids as $phid => $ignored) { $project = idx($projects, $phid); if (!$project) { // If a PHID is invalid, we just leave it as-is. We could clean it up, // but leaving it untouched is less likely to cause collateral damage. continue; } // If this was a milestone, check if it was the last milestone from its // group in the list. If not, remove it from the list. if ($project->isMilestone()) { $parent_phid = $project->getParentProjectPHID(); if ($milestone_map[$parent_phid] !== $phid) { unset($phids[$phid]); continue; } } // If a later project in the list is a subproject of this one, it will // have removed ancestors from the map. If this project does not point // at itself in the ancestor map, it should be discarded in favor of a // subproject that comes later. if (idx($ancestor_map, $phid) !== $phid) { unset($phids[$phid]); continue; } // If a later project in the list is an ancestor of this one, it will // have added itself to the map. If any ancestor of this project points // at itself in the map, this project should be discarded in favor of // that later ancestor. foreach ($project->getAncestorProjects() as $ancestor) { $ancestor_phid = $ancestor->getPHID(); if (isset($ancestor_map[$ancestor_phid])) { unset($phids[$phid]); continue 2; } } } return $phids; } /** * When the view policy for an object is changed, scramble the secret keys * for attached files to invalidate existing URIs. */ private function scrambleFileSecrets($object) { // If this is a newly created object, we don't need to scramble anything // since it couldn't have been previously published. if ($this->getIsNewObject()) { return; } // If the object is a file itself, scramble it. if ($object instanceof PhabricatorFile) { if ($this->shouldScramblePolicy($object->getViewPolicy())) { $object->scrambleSecret(); $object->save(); } } $phid = $object->getPHID(); $attached_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $phid, PhabricatorObjectHasFileEdgeType::EDGECONST); if (!$attached_phids) { return; } $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); $files = id(new PhabricatorFileQuery()) ->setViewer($omnipotent_viewer) ->withPHIDs($attached_phids) ->execute(); foreach ($files as $file) { $view_policy = $file->getViewPolicy(); if ($this->shouldScramblePolicy($view_policy)) { $file->scrambleSecret(); $file->save(); } } } /** * Check if a policy is strong enough to justify scrambling. Objects which * are set to very open policies don't need to scramble their files, and * files with very open policies don't need to be scrambled when associated * objects change. */ private function shouldScramblePolicy($policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: case PhabricatorPolicies::POLICY_USER: return false; } return true; } private function updateWorkboardColumns($object, $const, $old, $new) { // If an object is removed from a project, remove it from any proxy // columns for that project. This allows a task which is moved up from a // milestone to the parent to move back into the "Backlog" column on the // parent workboard. if ($const != PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) { return; } // TODO: This should likely be some future WorkboardInterface. $appears_on_workboards = ($object instanceof ManiphestTask); if (!$appears_on_workboards) { return; } $removed_phids = array_keys(array_diff_key($old, $new)); if (!$removed_phids) { return; } // Find any proxy columns for the removed projects. $proxy_columns = id(new PhabricatorProjectColumnQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withProxyPHIDs($removed_phids) ->execute(); if (!$proxy_columns) { return array(); } $proxy_phids = mpull($proxy_columns, 'getPHID'); $position_table = new PhabricatorProjectColumnPosition(); $conn_w = $position_table->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND columnPHID IN (%Ls)', $position_table->getTableName(), $object->getPHID(), $proxy_phids); } private function getModularTransactionTypes() { if ($this->modularTypes === null) { $template = $this->object->getApplicationTransactionTemplate(); if ($template instanceof PhabricatorModularTransaction) { $xtypes = $template->newModularTransactionTypes(); foreach ($xtypes as $key => $xtype) { $xtype = clone $xtype; $xtype->setEditor($this); $xtypes[$key] = $xtype; } } else { $xtypes = array(); } $this->modularTypes = $xtypes; } return $this->modularTypes; } private function getModularTransactionType($type) { $types = $this->getModularTransactionTypes(); return idx($types, $type); } private function willApplyTransactions($object, array $xactions) { foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $xtype = $this->getModularTransactionType($type); if (!$xtype) { continue; } $xtype->willApplyTransactions($object, $xactions); } } public function getCreateObjectTitle($author, $object) { return pht('%s created this object.', $author); } public function getCreateObjectTitleForFeed($author, $object) { return pht('%s created an object: %s.', $author, $object); } /* -( Queue )-------------------------------------------------------------- */ protected function queueTransaction( PhabricatorApplicationTransaction $xaction) { $this->transactionQueue[] = $xaction; return $this; } private function flushTransactionQueue($object) { if (!$this->transactionQueue) { return; } $xactions = $this->transactionQueue; $this->transactionQueue = array(); $editor = $this->newQueueEditor(); return $editor->applyTransactions($object, $xactions); } private function newQueueEditor() { $editor = id(newv(get_class($this), array())) ->setActor($this->getActor()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect($this->getContinueOnNoEffect()) ->setContinueOnMissingFields($this->getContinueOnMissingFields()) ->setIsSilent($this->getIsSilent()); if ($this->actingAsPHID !== null) { $editor->setActingAsPHID($this->actingAsPHID); } return $editor; } /* -( Stamps )------------------------------------------------------------- */ public function newMailStampTemplates($object) { $actor = $this->getActor(); $templates = array(); $extensions = $this->newMailExtensions($object); foreach ($extensions as $extension) { $stamps = $extension->newMailStampTemplates($object); foreach ($stamps as $stamp) { $key = $stamp->getKey(); if (isset($templates[$key])) { throw new Exception( pht( 'Mail extension ("%s") defines a stamp template with the '. 'same key ("%s") as another template. Each stamp template '. 'must have a unique key.', get_class($extension), $key)); } $stamp->setViewer($actor); $templates[$key] = $stamp; } } return $templates; } final public function getMailStamp($key) { if (!isset($this->stampTemplates)) { throw new PhutilInvalidStateException('newMailStampTemplates'); } if (!isset($this->stampTemplates[$key])) { throw new Exception( pht( 'Editor ("%s") has no mail stamp template with provided key ("%s").', get_class($this), $key)); } return $this->stampTemplates[$key]; } private function newMailStamps($object, array $xactions) { $actor = $this->getActor(); $this->stampTemplates = $this->newMailStampTemplates($object); $extensions = $this->newMailExtensions($object); $stamps = array(); foreach ($extensions as $extension) { $extension->newMailStamps($object, $xactions); } return $this->stampTemplates; } private function newMailExtensions($object) { $actor = $this->getActor(); $all_extensions = PhabricatorMailEngineExtension::getAllExtensions(); $extensions = array(); foreach ($all_extensions as $key => $template) { $extension = id(clone $template) ->setViewer($actor) ->setEditor($this); if ($extension->supportsObject($object)) { $extensions[$key] = $extension; } } return $extensions; } private function generateMailStamps($object, $data) { if (!$data || !is_array($data)) { return null; } $templates = $this->newMailStampTemplates($object); foreach ($data as $spec) { if (!is_array($spec)) { continue; } $key = idx($spec, 'key'); if (!isset($templates[$key])) { continue; } $type = idx($spec, 'type'); if ($templates[$key]->getStampType() !== $type) { continue; } $value = idx($spec, 'value'); $templates[$key]->setValueFromDictionary($value); } $results = array(); foreach ($templates as $template) { $value = $template->getValueForRendering(); $rendered = $template->renderStamps($value); if ($rendered === null) { continue; } $rendered = (array)$rendered; foreach ($rendered as $stamp) { $results[] = $stamp; } } natcasesort($results); return $results; } public function getRemovedRecipientPHIDs() { return $this->mailRemovedPHIDs; } private function buildOldRecipientLists($object, $xactions) { // See T4776. Before we start making any changes, build a list of the old // recipients. If a change removes a user from the recipient list for an // object we still want to notify the user about that change. This allows // them to respond if they didn't want to be removed. if (!$this->shouldSendMail($object, $xactions)) { return; } $this->oldTo = $this->getMailTo($object); $this->oldCC = $this->getMailCC($object); return $this; } private function applyOldRecipientLists() { $actor_phid = $this->getActingAsPHID(); // If you took yourself off the recipient list (for example, by // unsubscribing or resigning) assume that you know what you did and // don't need to be notified. // If you just moved from "To" to "Cc" (or vice versa), you're still a // recipient so we don't need to add you back in. $map = array_fuse($this->mailToPHIDs) + array_fuse($this->mailCCPHIDs); foreach ($this->oldTo as $phid) { if ($phid === $actor_phid) { continue; } if (isset($map[$phid])) { continue; } $this->mailToPHIDs[] = $phid; $this->mailRemovedPHIDs[] = $phid; } foreach ($this->oldCC as $phid) { if ($phid === $actor_phid) { continue; } if (isset($map[$phid])) { continue; } $this->mailCCPHIDs[] = $phid; $this->mailRemovedPHIDs[] = $phid; } return $this; } private function queueWebhooks($object, array $xactions) { $hook_viewer = PhabricatorUser::getOmnipotentUser(); $webhook_map = $this->webhookMap; if (!is_array($webhook_map)) { $webhook_map = array(); } // Add any "Firehose" hooks to the list of hooks we're going to call. $firehose_hooks = id(new HeraldWebhookQuery()) ->setViewer($hook_viewer) ->withStatuses( array( HeraldWebhook::HOOKSTATUS_FIREHOSE, )) ->execute(); foreach ($firehose_hooks as $firehose_hook) { // This is "the hook itself is the reason this hook is being called", // since we're including it because it's configured as a firehose // hook. $hook_phid = $firehose_hook->getPHID(); $webhook_map[$hook_phid][] = $hook_phid; } if (!$webhook_map) { return; } // NOTE: We're going to queue calls to disabled webhooks, they'll just // immediately fail in the worker queue. This makes the behavior more // visible. $call_hooks = id(new HeraldWebhookQuery()) ->setViewer($hook_viewer) ->withPHIDs(array_keys($webhook_map)) ->execute(); foreach ($call_hooks as $call_hook) { $trigger_phids = idx($webhook_map, $call_hook->getPHID()); $request = HeraldWebhookRequest::initializeNewWebhookRequest($call_hook) ->setObjectPHID($object->getPHID()) ->setTransactionPHIDs(mpull($xactions, 'getPHID')) ->setTriggerPHIDs($trigger_phids) ->setRetryMode(HeraldWebhookRequest::RETRY_FOREVER) ->setIsSilentAction((bool)$this->getIsSilent()) ->setIsSecureAction((bool)$this->getMustEncrypt()) ->save(); $request->queueCall(); } } private function hasWarnings($object, $xaction) { // TODO: For the moment, this is a very un-modular hack to support // exactly one type of warning (mentioning users on a draft revision) // that we want to show. See PHI433. if (!($object instanceof DifferentialRevision)) { return false; } if (!$object->isDraft()) { return false; } $type = $xaction->getTransactionType(); if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) { return false; } // NOTE: This will currently warn even if you're only removing // subscribers. return true; } private function buildHistoryMail(PhabricatorLiskDAO $object) { $viewer = $this->requireActor(); $recipient_phid = $this->getActingAsPHID(); // Load every transaction so we can build a mail message with a complete // history for the object. $query = PhabricatorApplicationTransactionQuery::newQueryForObject($object); $xactions = $query ->setViewer($viewer) ->withObjectPHIDs(array($object->getPHID())) ->execute(); $xactions = array_reverse($xactions); $mail_messages = $this->buildMailWithRecipients( $object, $xactions, array($recipient_phid), array(), array()); $mail = head($mail_messages); // Since the user explicitly requested "!history", force delivery of this // message regardless of their other mail settings. $mail->setForceDelivery(true); return $mail; } public function newAutomaticInlineTransactions( PhabricatorLiskDAO $object, array $inlines, $transaction_type, PhabricatorCursorPagedPolicyAwareQuery $query_template) { $xactions = array(); foreach ($inlines as $inline) { $xactions[] = $object->getApplicationTransactionTemplate() ->setTransactionType($transaction_type) ->attachComment($inline); } $state_xaction = $this->newInlineStateTransaction( $object, $query_template); if ($state_xaction) { $xactions[] = $state_xaction; } return $xactions; } protected function newInlineStateTransaction( PhabricatorLiskDAO $object, PhabricatorCursorPagedPolicyAwareQuery $query_template) { $actor_phid = $this->getActingAsPHID(); $author_phid = $object->getAuthorPHID(); $actor_is_author = ($actor_phid == $author_phid); $state_map = PhabricatorTransactions::getInlineStateMap(); $query = id(clone $query_template) ->setViewer($this->getActor()) ->withFixedStates(array_keys($state_map)); $inlines = array(); $inlines[] = id(clone $query) ->withAuthorPHIDs(array($actor_phid)) ->withHasTransaction(false) ->execute(); if ($actor_is_author) { $inlines[] = id(clone $query) ->withHasTransaction(true) ->execute(); } $inlines = array_mergev($inlines); if (!$inlines) { return null; } $old_value = mpull($inlines, 'getFixedState', 'getPHID'); $new_value = array(); foreach ($old_value as $key => $state) { $new_value[$key] = $state_map[$state]; } // See PHI995. Copy some information about the inlines into the transaction // so we can tailor rendering behavior. In particular, we don't want to // render transactions about users marking their own inlines as "Done". $inline_details = array(); foreach ($inlines as $inline) { $inline_details[$inline->getPHID()] = array( 'authorPHID' => $inline->getAuthorPHID(), ); } return $object->getApplicationTransactionTemplate() ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) ->setMetadataValue('inline.details', $inline_details) ->setOldValue($old_value) ->setNewValue($new_value); } } diff --git a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php index 1c6b4a0049..205253ba22 100644 --- a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php +++ b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php @@ -1,58 +1,43 @@ >>Editor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new <<>>Transaction(); } */ diff --git a/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php b/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php index 5457708953..9a369717f0 100644 --- a/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php +++ b/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php @@ -1,173 +1,171 @@ getDefaultPrivateReplyHandlerEmailAddress( $user, $this->getObjectPrefix()); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress( $this->getObjectPrefix()); } private function newEditor(PhabricatorMetaMTAReceivedMail $mail) { $content_source = $mail->newContentSource(); $editor = $this->getMailReceiver() ->getApplicationTransactionEditor() ->setActor($this->getActor()) ->setContentSource($content_source) ->setContinueOnMissingFields(true) ->setParentMessageID($mail->getMessageID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()); if ($this->getApplicationEmail()) { $editor->setApplicationEmail($this->getApplicationEmail()); } return $editor; } protected function newTransaction() { return $this->getMailReceiver()->getApplicationTransactionTemplate(); } protected function didReceiveMail( PhabricatorMetaMTAReceivedMail $mail, $body) { return array(); } protected function shouldCreateCommentFromMailBody() { return (bool)$this->getMailReceiver()->getID(); } final protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { $viewer = $this->getActor(); $object = $this->getMailReceiver(); $app_email = $this->getApplicationEmail(); $is_new = !$object->getID(); // If this is a new object which implements the Spaces interface and was // created by sending mail to an ApplicationEmail address, put the object // in the same Space the address is in. if ($is_new) { if ($object instanceof PhabricatorSpacesInterface) { if ($app_email) { $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( $app_email); $object->setSpacePHID($space_phid); } } } $body_data = $mail->parseBody(); $body = $body_data['body']; $body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments()); $xactions = $this->didReceiveMail($mail, $body); // If this object is subscribable, subscribe all the users who were // recipients on the message. if ($object instanceof PhabricatorSubscribableInterface) { $subscriber_phids = $mail->loadAllRecipientPHIDs(); if ($subscriber_phids) { $xactions[] = $this->newTransaction() ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue( array( '+' => $subscriber_phids, )); } } $command_xactions = $this->processMailCommands( $mail, $body_data['commands']); foreach ($command_xactions as $xaction) { $xactions[] = $xaction; } if ($this->shouldCreateCommentFromMailBody()) { $comment = $this ->newTransaction() ->getApplicationTransactionCommentObject() ->setContent($body); $xactions[] = $this->newTransaction() ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment($comment); } - $target = $object->getApplicationTransactionObject(); - $this->newEditor($mail) ->setContinueOnNoEffect(true) - ->applyTransactions($target, $xactions); + ->applyTransactions($object, $xactions); } private function processMailCommands( PhabricatorMetaMTAReceivedMail $mail, array $command_list) { $viewer = $this->getActor(); $object = $this->getMailReceiver(); $list = MetaMTAEmailTransactionCommand::getAllCommandsForObject($object); $map = MetaMTAEmailTransactionCommand::getCommandMap($list); $xactions = array(); foreach ($command_list as $command_argv) { $command = head($command_argv); $argv = array_slice($command_argv, 1); $handler = idx($map, phutil_utf8_strtolower($command)); if ($handler) { $results = $handler->buildTransactions( $viewer, $object, $mail, $command, $argv); foreach ($results as $result) { $xactions[] = $result; } } else { $valid_commands = array(); foreach ($list as $valid_command) { $aliases = $valid_command->getCommandAliases(); if ($aliases) { foreach ($aliases as $key => $alias) { $aliases[$key] = '!'.$alias; } $aliases = implode(', ', $aliases); $valid_commands[] = pht( '!%s (or %s)', $valid_command->getCommand(), $aliases); } else { $valid_commands[] = '!'.$valid_command->getCommand(); } } throw new Exception( pht( 'The command "!%s" is not a supported mail command. Valid '. 'commands for this object are: %s.', $command, implode(', ', $valid_commands))); } } return $xactions; } } diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index 707dbe9714..6c9f3a50a6 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -1,350 +1,346 @@ setSubtype(PhabricatorEditEngine::SUBTYPE_DEFAULT) ->setEngineKey($engine->getEngineKey()) ->attachEngine($engine) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorEditEngineConfigurationPHIDType::TYPECONST); } public function getCreateSortKey() { return $this->getSortKey($this->createOrder); } public function getEditSortKey() { return $this->getSortKey($this->editOrder); } private function getSortKey($order) { // Put objects at the bottom by default if they haven't previously been // reordered. When they're explicitly reordered, the smallest sort key we // assign is 1, so if the object has a value of 0 it means it hasn't been // ordered yet. if ($order != 0) { $group = 'A'; } else { $group = 'B'; } return sprintf( "%s%012d%s\0%012d", $group, $order, $this->getName(), $this->getID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'engineKey' => 'text64', 'builtinKey' => 'text64?', 'name' => 'text255', 'isDisabled' => 'bool', 'isDefault' => 'bool', 'isEdit' => 'bool', 'createOrder' => 'uint32', 'editOrder' => 'uint32', 'subtype' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_engine' => array( 'columns' => array('engineKey', 'builtinKey'), 'unique' => true, ), 'key_default' => array( 'columns' => array('engineKey', 'isDefault', 'isDisabled'), ), 'key_edit' => array( 'columns' => array('engineKey', 'isEdit', 'isDisabled'), ), ), ) + 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 setBuiltinKey($key) { if (strpos($key, '/') !== false) { throw new Exception( pht('EditEngine BuiltinKey contains an invalid key character "/".')); } return parent::setBuiltinKey($key); } public function attachEngine(PhabricatorEditEngine $engine) { $this->engine = $engine; return $this; } public function getEngine() { return $this->assertAttached($this->engine); } public function applyConfigurationToFields( PhabricatorEditEngine $engine, $object, array $fields) { $fields = mpull($fields, null, 'getKey'); $is_new = !$object->getID(); $values = $this->getProperty('defaults', array()); foreach ($fields as $key => $field) { if (!$field->getIsFormField()) { continue; } if (!$field->getIsDefaultable()) { continue; } if ($is_new) { if (array_key_exists($key, $values)) { $field->readDefaultValueFromConfiguration($values[$key]); } } } $locks = $this->getFieldLocks(); foreach ($fields as $field) { $key = $field->getKey(); switch (idx($locks, $key)) { case self::LOCK_LOCKED: $field->setIsHidden(false); if ($field->getIsLockable()) { $field->setIsLocked(true); } break; case self::LOCK_HIDDEN: $field->setIsHidden(true); if ($field->getIsLockable()) { $field->setIsLocked(false); } break; case self::LOCK_VISIBLE: $field->setIsHidden(false); if ($field->getIsLockable()) { $field->setIsLocked(false); } break; default: // If we don't have an explicit value, don't make any adjustments. break; } } $fields = $this->reorderFields($fields); $preamble = $this->getPreamble(); if (strlen($preamble)) { $fields = array( 'config.preamble' => id(new PhabricatorInstructionsEditField()) ->setKey('config.preamble') ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setValue($preamble), ) + $fields; } return $fields; } private function reorderFields(array $fields) { // Fields which can not be reordered are fixed in order at the top of the // form. These are used to show instructions or contextual information. $fixed = array(); foreach ($fields as $key => $field) { if (!$field->getIsReorderable()) { $fixed[$key] = $field; } } $keys = $this->getFieldOrder(); $fields = $fixed + array_select_keys($fields, $keys) + $fields; return $fields; } public function getURI() { $engine_key = $this->getEngineKey(); $key = $this->getIdentifier(); return "/transactions/editengine/{$engine_key}/view/{$key}/"; } public function getCreateURI() { $form_key = $this->getIdentifier(); $engine = $this->getEngine(); try { $create_uri = $engine->getEditURI(null, "form/{$form_key}/"); } catch (Exception $ex) { $create_uri = null; } return $create_uri; } public function getIdentifier() { $key = $this->getID(); if (!$key) { $key = $this->getBuiltinKey(); } return $key; } public function getDisplayName() { $name = $this->getName(); if (strlen($name)) { return $name; } $builtin = $this->getBuiltinKey(); if ($builtin !== null) { return pht('Builtin Form "%s"', $builtin); } return pht('Untitled Form'); } public function getPreamble() { return $this->getProperty('preamble'); } public function setPreamble($preamble) { return $this->setProperty('preamble', $preamble); } public function setFieldOrder(array $field_order) { return $this->setProperty('order', $field_order); } public function getFieldOrder() { return $this->getProperty('order', array()); } public function setFieldLocks(array $field_locks) { return $this->setProperty('locks', $field_locks); } public function getFieldLocks() { return $this->getProperty('locks', array()); } public function getFieldDefault($key) { $defaults = $this->getProperty('defaults', array()); return idx($defaults, $key); } public function setFieldDefault($key, $value) { $defaults = $this->getProperty('defaults', array()); $defaults[$key] = $value; return $this->setProperty('defaults', $defaults); } public function getIcon() { return $this->getEngine()->getIcon(); } /* -( 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->getEngine() ->getApplication() ->getPolicy($capability); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicyFilter::hasCapability( $viewer, $this->getEngine()->getApplication(), PhabricatorPolicyCapability::CAN_EDIT); } return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorEditEngineConfigurationEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorEditEngineConfigurationTransaction(); } } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php index d6e8437091..68263fccb7 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php @@ -1,270 +1,266 @@ true, self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'jobTypeKey' => 'text32', 'status' => 'text32', 'size' => 'uint32', 'isSilent' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_type' => array( 'columns' => array('jobTypeKey'), ), 'key_author' => array( 'columns' => array('authorPHID'), ), 'key_status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public static function initializeNewJob( PhabricatorUser $actor, PhabricatorWorkerBulkJobType $type, array $parameters) { $job = id(new PhabricatorWorkerBulkJob()) ->setAuthorPHID($actor->getPHID()) ->setJobTypeKey($type->getBulkJobTypeKey()) ->setParameters($parameters) ->attachJobImplementation($type) ->setIsSilent(0); $job->setSize($job->computeSize()); return $job; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorWorkerBulkJobPHIDType::TYPECONST); } public function getMonitorURI() { return '/daemon/bulk/monitor/'.$this->getID().'/'; } public function getManageURI() { return '/daemon/bulk/view/'.$this->getID().'/'; } public function getParameter($key, $default = null) { return idx($this->parameters, $key, $default); } public function setParameter($key, $value) { $this->parameters[$key] = $value; return $this; } public function loadTaskStatusCounts() { $table = new PhabricatorWorkerBulkTask(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT status, COUNT(*) N FROM %T WHERE bulkJobPHID = %s GROUP BY status', $table->getTableName(), $this->getPHID()); return ipull($rows, 'N', 'status'); } public function newContentSource() { return PhabricatorContentSource::newForSource( PhabricatorBulkContentSource::SOURCECONST, array( 'jobID' => $this->getID(), )); } public function getStatusIcon() { $map = array( self::STATUS_CONFIRM => 'fa-question', self::STATUS_WAITING => 'fa-clock-o', self::STATUS_RUNNING => 'fa-clock-o', self::STATUS_COMPLETE => 'fa-check grey', ); return idx($map, $this->getStatus(), 'none'); } public function getStatusName() { $map = array( self::STATUS_CONFIRM => pht('Confirming'), self::STATUS_WAITING => pht('Waiting'), self::STATUS_RUNNING => pht('Running'), self::STATUS_COMPLETE => pht('Complete'), ); return idx($map, $this->getStatus(), $this->getStatus()); } public function isConfirming() { return ($this->getStatus() == self::STATUS_CONFIRM); } /* -( Job Implementation )------------------------------------------------- */ protected function getJobImplementation() { return $this->assertAttached($this->jobImplementation); } public function attachJobImplementation(PhabricatorWorkerBulkJobType $type) { $this->jobImplementation = $type; return $this; } private function computeSize() { return $this->getJobImplementation()->getJobSize($this); } public function getCancelURI() { return $this->getJobImplementation()->getCancelURI($this); } public function getDoneURI() { return $this->getJobImplementation()->getDoneURI($this); } public function getDescriptionForConfirm() { return $this->getJobImplementation()->getDescriptionForConfirm($this); } public function createTasks() { return $this->getJobImplementation()->createTasks($this); } public function runTask( PhabricatorUser $actor, PhabricatorWorkerBulkTask $task) { return $this->getJobImplementation()->runTask($actor, $this, $task); } public function getJobName() { return $this->getJobImplementation()->getJobName($this); } public function getCurtainActions(PhabricatorUser $viewer) { return $this->getJobImplementation()->getCurtainActions($viewer, $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::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getAuthorPHID(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return pht('Only the owner of a bulk job can edit it.'); default: return null; } } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorWorkerBulkJobEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorWorkerBulkJobTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); // We're only removing the actual task objects. This may leave stranded // workers in the queue itself, but they'll just flush out automatically // when they can't load bulk job data. $task_table = new PhabricatorWorkerBulkTask(); $conn_w = $task_table->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE bulkJobPHID = %s', $task_table->getPHID(), $this->getPHID()); $this->delete(); $this->saveTransaction(); } }