diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b632ba7d1e..44383032b7 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '7935f211', + 'core.pkg.css' => 'ecdca229', 'core.pkg.js' => '7d8faf57', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -35,7 +35,7 @@ return array( 'rsrc/css/application/base/main-menu-view.css' => 'd00a795a', 'rsrc/css/application/base/notification-menu.css' => 'f31c0bde', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601', - 'rsrc/css/application/base/phui-theme.css' => 'ab7b848c', + 'rsrc/css/application/base/phui-theme.css' => '027ba77e', 'rsrc/css/application/base/standard-page-view.css' => 'e709f6d0', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', @@ -81,7 +81,7 @@ return array( 'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b', 'rsrc/css/application/paste/paste.css' => 'a5157c48', 'rsrc/css/application/people/people-profile.css' => '2473d929', - 'rsrc/css/application/phame/phame.css' => '4ca6fd6c', + 'rsrc/css/application/phame/phame.css' => '737792d6', 'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => 'ca89d380', @@ -92,9 +92,9 @@ return array( 'rsrc/css/application/policy/policy-edit.css' => '815c66f7', 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', - 'rsrc/css/application/ponder/ponder-view.css' => 'b40dc156', + 'rsrc/css/application/ponder/ponder-view.css' => '212495e0', 'rsrc/css/application/project/project-card-view.css' => '9418c97d', - 'rsrc/css/application/project/project-view.css' => '83bb6654', + 'rsrc/css/application/project/project-view.css' => '298b7c5b', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', @@ -127,14 +127,14 @@ return array( 'rsrc/css/phui/phui-button.css' => 'edf464e9', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-crumbs-view.css' => '79d536e5', - 'rsrc/css/phui/phui-document-pro.css' => 'a8872307', + 'rsrc/css/phui/phui-document-pro.css' => '92d5b648', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => '9c71d2bf', 'rsrc/css/phui/phui-feed-story.css' => '04aec08f', 'rsrc/css/phui/phui-fontkit.css' => '9cda225e', 'rsrc/css/phui/phui-form-view.css' => '4a1a0f5e', 'rsrc/css/phui/phui-form.css' => 'aac1d51d', - 'rsrc/css/phui/phui-header-view.css' => '50c5cb6a', + 'rsrc/css/phui/phui-header-view.css' => 'a6d7b20d', 'rsrc/css/phui/phui-hovercard.css' => 'de1a2119', 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', 'rsrc/css/phui/phui-icon.css' => '3f33ab57', @@ -154,7 +154,7 @@ return array( 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', - 'rsrc/css/phui/phui-two-column-view.css' => '0763177e', + 'rsrc/css/phui/phui-two-column-view.css' => 'a317616a', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7', 'rsrc/css/phui/workboards/phui-workboard.css' => 'e6d89647', 'rsrc/css/phui/workboards/phui-workcard.css' => '3646fb96', @@ -790,7 +790,7 @@ return array( 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', 'phabricator-zindex-css' => '5b6fcf3f', - 'phame-css' => '4ca6fd6c', + 'phame-css' => '737792d6', 'pholio-css' => 'ca89d380', 'pholio-edit-css' => '3ad9d1ee', 'pholio-inline-comments-css' => '8e545e49', @@ -812,13 +812,13 @@ return array( 'phui-crumbs-view-css' => '79d536e5', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '9c71d2bf', - 'phui-document-view-pro-css' => 'a8872307', + 'phui-document-view-pro-css' => '92d5b648', 'phui-feed-story-css' => '04aec08f', 'phui-font-icon-base-css' => 'ecbbb4c2', 'phui-fontkit-css' => '9cda225e', 'phui-form-css' => 'aac1d51d', 'phui-form-view-css' => '4a1a0f5e', - 'phui-header-view-css' => '50c5cb6a', + 'phui-header-view-css' => 'a6d7b20d', 'phui-hovercard' => '1bd28176', 'phui-hovercard-view-css' => 'de1a2119', 'phui-icon-set-selector-css' => '1ab67aad', @@ -839,9 +839,9 @@ return array( 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '9d5d4400', - 'phui-theme-css' => 'ab7b848c', + 'phui-theme-css' => '027ba77e', 'phui-timeline-view-css' => '2efceff8', - 'phui-two-column-view-css' => '0763177e', + 'phui-two-column-view-css' => 'a317616a', 'phui-workboard-color-css' => 'ac6fe6a7', 'phui-workboard-view-css' => 'e6d89647', 'phui-workcard-view-css' => '3646fb96', @@ -855,9 +855,9 @@ return array( 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', - 'ponder-view-css' => 'b40dc156', + 'ponder-view-css' => '212495e0', 'project-card-view-css' => '9418c97d', - 'project-view-css' => '83bb6654', + 'project-view-css' => '298b7c5b', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', diff --git a/resources/sql/autopatches/20160221.almanac.1.devicen.sql b/resources/sql/autopatches/20160221.almanac.1.devicen.sql new file mode 100644 index 0000000000..c098173f25 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.1.devicen.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_devicename_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.2.devicei.php b/resources/sql/autopatches/20160221.almanac.2.devicei.php new file mode 100644 index 0000000000..aea17d0ad6 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.2.devicei.php @@ -0,0 +1,11 @@ +getPHID(), + array( + 'force' => true, + )); +} diff --git a/resources/sql/autopatches/20160221.almanac.3.servicen.sql b/resources/sql/autopatches/20160221.almanac.3.servicen.sql new file mode 100644 index 0000000000..2e48cd60c8 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.3.servicen.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_servicename_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.4.servicei.php b/resources/sql/autopatches/20160221.almanac.4.servicei.php new file mode 100644 index 0000000000..97211ca7b5 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.4.servicei.php @@ -0,0 +1,11 @@ +getPHID(), + array( + 'force' => true, + )); +} diff --git a/resources/sql/autopatches/20160221.almanac.5.networkn.sql b/resources/sql/autopatches/20160221.almanac.5.networkn.sql new file mode 100644 index 0000000000..c47db69d5d --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.5.networkn.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_networkname_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.6.networki.php b/resources/sql/autopatches/20160221.almanac.6.networki.php new file mode 100644 index 0000000000..263defbb33 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.6.networki.php @@ -0,0 +1,11 @@ +getPHID(), + array( + 'force' => true, + )); +} diff --git a/resources/sql/autopatches/20160221.almanac.7.namespacen.sql b/resources/sql/autopatches/20160221.almanac.7.namespacen.sql new file mode 100644 index 0000000000..bdacc3552c --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.7.namespacen.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespacename_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.8.namespace.sql b/resources/sql/autopatches/20160221.almanac.8.namespace.sql new file mode 100644 index 0000000000..90f53d3daa --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.8.namespace.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespace ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, + nameIndex BINARY(12) NOT NULL, + mailKey BINARY(20) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_nameindex` (nameIndex), + KEY `key_name` (name) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.9.namespacex.sql b/resources/sql/autopatches/20160221.almanac.9.namespacex.sql new file mode 100644 index 0000000000..4f695456f9 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.9.namespacex.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespacetransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160222.almanac.1.properties.php b/resources/sql/autopatches/20160222.almanac.1.properties.php new file mode 100644 index 0000000000..d12b3e5c61 --- /dev/null +++ b/resources/sql/autopatches/20160222.almanac.1.properties.php @@ -0,0 +1,28 @@ +establishConnection('w'); + +// We're going to JSON-encode the value in each row: previously rows stored +// plain strings, but now they store JSON, so we need to update them. + +foreach (new LiskMigrationIterator($table) as $property) { + $key = $property->getFieldName(); + + $current_row = queryfx_one( + $conn_w, + 'SELECT fieldValue FROM %T WHERE id = %d', + $table->getTableName(), + $property->getID()); + + if (!$current_row) { + continue; + } + + queryfx( + $conn_w, + 'UPDATE %T SET fieldValue = %s WHERE id = %d', + $table->getTableName(), + phutil_json_encode($current_row['fieldValue']), + $property->getID()); +} diff --git a/resources/sql/autopatches/20160223.almanac.1.bound.sql b/resources/sql/autopatches/20160223.almanac.1.bound.sql new file mode 100644 index 0000000000..3e30fa458a --- /dev/null +++ b/resources/sql/autopatches/20160223.almanac.1.bound.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_device + ADD isBoundToClusterService BOOL NOT NULL; diff --git a/resources/sql/autopatches/20160223.almanac.2.lockbind.sql b/resources/sql/autopatches/20160223.almanac.2.lockbind.sql new file mode 100644 index 0000000000..93f5e8b0b5 --- /dev/null +++ b/resources/sql/autopatches/20160223.almanac.2.lockbind.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_almanac.almanac_device + SET isBoundToClusterService = isLocked; diff --git a/resources/sql/autopatches/20160223.almanac.3.devicelock.sql b/resources/sql/autopatches/20160223.almanac.3.devicelock.sql new file mode 100644 index 0000000000..fdb879fe28 --- /dev/null +++ b/resources/sql/autopatches/20160223.almanac.3.devicelock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_device + DROP isLocked; diff --git a/resources/sql/autopatches/20160223.almanac.4.servicelock.sql b/resources/sql/autopatches/20160223.almanac.4.servicelock.sql new file mode 100644 index 0000000000..e716054c0c --- /dev/null +++ b/resources/sql/autopatches/20160223.almanac.4.servicelock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_service + DROP isLocked; diff --git a/resources/sql/autopatches/20160223.paste.fileedges.php b/resources/sql/autopatches/20160223.paste.fileedges.php new file mode 100644 index 0000000000..915a2fcd0e --- /dev/null +++ b/resources/sql/autopatches/20160223.paste.fileedges.php @@ -0,0 +1,21 @@ +getPHID(); + $file_phid = $paste->getFilePHID(); + + if (!$file_phid) { + continue; + } + + id(new PhabricatorEdgeEditor()) + ->addEdge($paste_phid, $edge_type, $file_phid) + ->save(); +} diff --git a/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql b/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql new file mode 100644 index 0000000000..0252e50ca7 --- /dev/null +++ b/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_binding + ADD isDisabled BOOL NOT NULL; diff --git a/resources/sql/autopatches/20160225.almanac.2.stype.sql b/resources/sql/autopatches/20160225.almanac.2.stype.sql new file mode 100644 index 0000000000..fcd054ff23 --- /dev/null +++ b/resources/sql/autopatches/20160225.almanac.2.stype.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_service + CHANGE serviceClass serviceType VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160225.almanac.3.stype.php b/resources/sql/autopatches/20160225.almanac.3.stype.php new file mode 100644 index 0000000000..f6f19eabd3 --- /dev/null +++ b/resources/sql/autopatches/20160225.almanac.3.stype.php @@ -0,0 +1,30 @@ +establishConnection('w'); + +foreach (new LiskMigrationIterator($table) as $service) { + + $new_type = null; + try { + $old_type = $service->getServiceType(); + $object = newv($old_type, array()); + $new_type = $object->getServiceTypeConstant(); + } catch (Exception $ex) { + continue; + } + + if (!$new_type) { + continue; + } + + queryfx( + $conn_w, + 'UPDATE %T SET serviceType = %s WHERE id = %d', + $table->getTableName(), + $new_type, + $service->getID()); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ae699acf07..8528aa9a19 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -11,26 +11,26 @@ phutil_register_library_map(array( 'class' => array( 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', + 'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php', 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', + 'AlmanacBindingPropertyEditEngine' => 'applications/almanac/editor/AlmanacBindingPropertyEditEngine.php', 'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php', 'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php', 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', + 'AlmanacBindingsSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php', 'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php', 'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php', 'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php', - 'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', - 'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php', - 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', + 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', 'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php', - 'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php', 'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php', 'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php', 'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php', @@ -38,33 +38,52 @@ phutil_register_library_map(array( 'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php', 'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php', 'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php', + 'AlmanacDeviceNameNgrams' => 'applications/almanac/storage/AlmanacDeviceNameNgrams.php', 'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php', + 'AlmanacDevicePropertyEditEngine' => 'applications/almanac/editor/AlmanacDevicePropertyEditEngine.php', 'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php', + 'AlmanacDeviceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php', 'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php', 'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php', 'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php', 'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php', 'AlmanacDrydockPoolServiceType' => 'applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php', + 'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', + 'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', - 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', + 'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php', 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', - 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php', + 'AlmanacNamespace' => 'applications/almanac/storage/AlmanacNamespace.php', + 'AlmanacNamespaceController' => 'applications/almanac/controller/AlmanacNamespaceController.php', + 'AlmanacNamespaceEditController' => 'applications/almanac/controller/AlmanacNamespaceEditController.php', + 'AlmanacNamespaceEditEngine' => 'applications/almanac/editor/AlmanacNamespaceEditEngine.php', + 'AlmanacNamespaceEditor' => 'applications/almanac/editor/AlmanacNamespaceEditor.php', + 'AlmanacNamespaceListController' => 'applications/almanac/controller/AlmanacNamespaceListController.php', + 'AlmanacNamespaceNameNgrams' => 'applications/almanac/storage/AlmanacNamespaceNameNgrams.php', + 'AlmanacNamespacePHIDType' => 'applications/almanac/phid/AlmanacNamespacePHIDType.php', + 'AlmanacNamespaceQuery' => 'applications/almanac/query/AlmanacNamespaceQuery.php', + 'AlmanacNamespaceSearchEngine' => 'applications/almanac/query/AlmanacNamespaceSearchEngine.php', + 'AlmanacNamespaceTransaction' => 'applications/almanac/storage/AlmanacNamespaceTransaction.php', + 'AlmanacNamespaceTransactionQuery' => 'applications/almanac/query/AlmanacNamespaceTransactionQuery.php', + 'AlmanacNamespaceViewController' => 'applications/almanac/controller/AlmanacNamespaceViewController.php', 'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php', 'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php', 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', + 'AlmanacNetworkEditEngine' => 'applications/almanac/editor/AlmanacNetworkEditEngine.php', 'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php', 'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php', + 'AlmanacNetworkNameNgrams' => 'applications/almanac/storage/AlmanacNetworkNameNgrams.php', 'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php', 'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php', 'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php', @@ -72,30 +91,36 @@ phutil_register_library_map(array( 'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php', 'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php', 'AlmanacPropertiesDestructionEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesDestructionEngineExtension.php', + 'AlmanacPropertiesSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php', 'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php', 'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php', 'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php', 'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php', + 'AlmanacPropertyEditEngine' => 'applications/almanac/editor/AlmanacPropertyEditEngine.php', 'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php', 'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php', 'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php', - 'AlmanacQueryDevicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php', - 'AlmanacQueryServicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php', 'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php', + 'AlmanacSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacSearchEngineAttachment.php', 'AlmanacService' => 'applications/almanac/storage/AlmanacService.php', 'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php', 'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php', 'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php', 'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php', 'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php', + 'AlmanacServiceNameNgrams' => 'applications/almanac/storage/AlmanacServiceNameNgrams.php', 'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php', + 'AlmanacServicePropertyEditEngine' => 'applications/almanac/editor/AlmanacServicePropertyEditEngine.php', 'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php', + 'AlmanacServiceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php', 'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php', 'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php', 'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php', 'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php', + 'AlmanacServiceTypeDatasource' => 'applications/almanac/typeahead/AlmanacServiceTypeDatasource.php', 'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php', 'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php', + 'AlmanacTransaction' => 'applications/almanac/storage/AlmanacTransaction.php', 'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php', 'Aphront304Response' => 'aphront/response/Aphront304Response.php', 'Aphront400Response' => 'aphront/response/Aphront400Response.php', @@ -858,6 +883,7 @@ phutil_register_library_map(array( 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', 'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php', + 'DrydockCommandError' => 'applications/drydock/DrydockCommandError/DrydockCommandError.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', 'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php', 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', @@ -1068,7 +1094,6 @@ phutil_register_library_map(array( 'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php', 'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php', 'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php', - 'HarbormasterCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php', 'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php', 'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php', 'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php', @@ -1082,7 +1107,6 @@ phutil_register_library_map(array( 'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php', 'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php', 'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php', - 'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php', 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php', 'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php', 'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php', @@ -3783,7 +3807,6 @@ phutil_register_library_map(array( 'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php', 'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php', 'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php', - 'PonderAnswerHasVotingUserEdgeType' => 'applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php', 'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php', 'PonderAnswerMailReceiver' => 'applications/ponder/mail/PonderAnswerMailReceiver.php', 'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php', @@ -3801,7 +3824,6 @@ phutil_register_library_map(array( 'PonderDefaultViewCapability' => 'applications/ponder/capability/PonderDefaultViewCapability.php', 'PonderEditor' => 'applications/ponder/editor/PonderEditor.php', 'PonderFooterView' => 'applications/ponder/view/PonderFooterView.php', - 'PonderHelpfulSaveController' => 'applications/ponder/controller/PonderHelpfulSaveController.php', 'PonderModerateCapability' => 'applications/ponder/capability/PonderModerateCapability.php', 'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php', 'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php', @@ -3823,10 +3845,6 @@ phutil_register_library_map(array( 'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php', 'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php', 'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php', - 'PonderVotableInterface' => 'applications/ponder/storage/PonderVotableInterface.php', - 'PonderVote' => 'applications/ponder/constants/PonderVote.php', - 'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php', - 'PonderVotingUserHasAnswerEdgeType' => 'applications/ponder/edge/PonderVotingUserHasAnswerEdgeType.php', 'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php', 'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php', 'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php', @@ -3972,127 +3990,163 @@ phutil_register_library_map(array( 'AlmanacBinding' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', - 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorExtendedPolicyInterface', ), + 'AlmanacBindingDisableController' => 'AlmanacServiceController', 'AlmanacBindingEditController' => 'AlmanacServiceController', - 'AlmanacBindingEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacBindingEditor' => 'AlmanacEditor', 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', + 'AlmanacBindingPropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacBindingQuery' => 'AlmanacQuery', 'AlmanacBindingTableView' => 'AphrontView', - 'AlmanacBindingTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacBindingTransaction' => 'AlmanacTransaction', 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacBindingViewController' => 'AlmanacServiceController', + 'AlmanacBindingsSearchEngineAttachment' => 'AlmanacSearchEngineAttachment', 'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterServiceType' => 'AlmanacServiceType', - 'AlmanacConduitAPIMethod' => 'ConduitAPIMethod', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', - 'AlmanacCoreCustomField' => array( - 'AlmanacCustomField', - 'PhabricatorStandardCustomFieldInterface', - ), - 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', + 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability', - 'AlmanacCustomField' => 'PhabricatorCustomField', 'AlmanacCustomServiceType' => 'AlmanacServiceType', 'AlmanacDAO' => 'PhabricatorLiskDAO', 'AlmanacDevice' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', - 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'PhabricatorSSHPublicKeyInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', + 'PhabricatorConduitResultInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', - 'AlmanacDeviceEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacDeviceEditor' => 'AlmanacEditor', 'AlmanacDeviceListController' => 'AlmanacDeviceController', + 'AlmanacDeviceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacDevicePHIDType' => 'PhabricatorPHIDType', + 'AlmanacDevicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacDeviceQuery' => 'AlmanacQuery', + 'AlmanacDeviceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'AlmanacDeviceTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacDeviceTransaction' => 'AlmanacTransaction', 'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacDeviceViewController' => 'AlmanacDeviceController', 'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType', + 'AlmanacEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacInterface' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', + 'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController', 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', - 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacInterfaceQuery' => 'AlmanacQuery', 'AlmanacInterfaceTableView' => 'AphrontView', 'AlmanacKeys' => 'Phobject', - 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', - 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', 'AlmanacNamesTestCase' => 'PhabricatorTestCase', + 'AlmanacNamespace' => array( + 'AlmanacDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorProjectInterface', + 'AlmanacPropertyInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', + ), + 'AlmanacNamespaceController' => 'AlmanacController', + 'AlmanacNamespaceEditController' => 'AlmanacNamespaceController', + 'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine', + 'AlmanacNamespaceEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacNamespaceListController' => 'AlmanacNamespaceController', + 'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams', + 'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType', + 'AlmanacNamespaceQuery' => 'AlmanacQuery', + 'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'AlmanacNamespaceTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacNamespaceViewController' => 'AlmanacNamespaceController', 'AlmanacNetwork' => array( 'AlmanacDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', ), 'AlmanacNetworkController' => 'AlmanacController', 'AlmanacNetworkEditController' => 'AlmanacNetworkController', + 'AlmanacNetworkEditEngine' => 'PhabricatorEditEngine', 'AlmanacNetworkEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacNetworkListController' => 'AlmanacNetworkController', + 'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType', - 'AlmanacNetworkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacNetworkQuery' => 'AlmanacQuery', 'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacNetworkTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacNetworkViewController' => 'AlmanacNetworkController', 'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', + 'AlmanacPropertiesSearchEngineAttachment' => 'AlmanacSearchEngineAttachment', 'AlmanacProperty' => array( - 'PhabricatorCustomFieldStorage', + 'AlmanacDAO', 'PhabricatorPolicyInterface', ), 'AlmanacPropertyController' => 'AlmanacController', - 'AlmanacPropertyDeleteController' => 'AlmanacDeviceController', - 'AlmanacPropertyEditController' => 'AlmanacDeviceController', + 'AlmanacPropertyDeleteController' => 'AlmanacPropertyController', + 'AlmanacPropertyEditController' => 'AlmanacPropertyController', + 'AlmanacPropertyEditEngine' => 'PhabricatorEditEngine', 'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'AlmanacQueryDevicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', - 'AlmanacQueryServicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', 'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'AlmanacSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'AlmanacService' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', - 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', + 'PhabricatorConduitResultInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceEditController' => 'AlmanacServiceController', - 'AlmanacServiceEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacServiceEditor' => 'AlmanacEditor', 'AlmanacServiceListController' => 'AlmanacServiceController', + 'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacServicePHIDType' => 'PhabricatorPHIDType', + 'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacServiceQuery' => 'AlmanacQuery', + 'AlmanacServiceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'AlmanacServiceTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacServiceTransaction' => 'AlmanacTransaction', 'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacServiceType' => 'Phobject', + 'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase', 'AlmanacServiceViewController' => 'AlmanacServiceController', + 'AlmanacTransaction' => 'PhabricatorApplicationTransaction', 'AphlictDropdownDataQuery' => 'Phobject', 'Aphront304Response' => 'AphrontResponse', 'Aphront400Response' => 'AphrontResponse', @@ -5203,7 +5257,6 @@ phutil_register_library_map(array( 'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildableViewController' => 'HarbormasterController', 'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup', - 'HarbormasterCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod', 'HarbormasterController' => 'PhabricatorController', 'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod', @@ -5217,7 +5270,6 @@ phutil_register_library_map(array( 'HarbormasterFileArtifact' => 'HarbormasterArtifact', 'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterHostArtifact' => 'HarbormasterDrydockLeaseArtifact', - 'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLintMessagesController' => 'HarbormasterController', 'HarbormasterLintPropertyView' => 'AphrontView', @@ -8418,7 +8470,6 @@ phutil_register_library_map(array( 'PonderDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMarkupInterface', - 'PonderVotableInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSubscribableInterface', @@ -8427,7 +8478,6 @@ phutil_register_library_map(array( 'PonderAnswerCommentController' => 'PonderController', 'PonderAnswerEditController' => 'PonderController', 'PonderAnswerEditor' => 'PonderEditor', - 'PonderAnswerHasVotingUserEdgeType' => 'PhabricatorEdgeType', 'PonderAnswerHistoryController' => 'PonderController', 'PonderAnswerMailReceiver' => 'PhabricatorObjectMailReceiver', 'PonderAnswerPHIDType' => 'PhabricatorPHIDType', @@ -8445,7 +8495,6 @@ phutil_register_library_map(array( 'PonderDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PonderEditor' => 'PhabricatorApplicationTransactionEditor', 'PonderFooterView' => 'AphrontTagView', - 'PonderHelpfulSaveController' => 'PonderController', 'PonderModerateCapability' => 'PhabricatorPolicyCapability', 'PonderQuestion' => array( 'PonderDAO', @@ -8479,9 +8528,6 @@ phutil_register_library_map(array( 'PonderQuestionViewController' => 'PonderController', 'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec', - 'PonderVote' => 'PonderConstants', - 'PonderVoteEditor' => 'PhabricatorEditor', - 'PonderVotingUserHasAnswerEdgeType' => 'PhabricatorEdgeType', 'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand', 'ProjectBoardTaskCard' => 'Phobject', 'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index a444554a0e..08b9c5fbc8 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -29,7 +29,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { public function getHelpDocumentationArticles(PhabricatorUser $viewer) { return array( array( - 'name' => pht('Alamanac User Guide'), + 'name' => pht('Almanac User Guide'), 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), ), ); @@ -43,37 +43,56 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { return array( '/almanac/' => array( '' => 'AlmanacConsoleController', - 'service/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacServiceListController', + '(?Pservice)/' => array( + $this->getQueryRoutePattern() => 'AlmanacServiceListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacServiceEditController', 'view/(?P[^/]+)/' => 'AlmanacServiceViewController', ), - 'device/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacDeviceListController', + '(?Pdevice)/' => array( + $this->getQueryRoutePattern() => 'AlmanacDeviceListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacDeviceEditController', 'view/(?P[^/]+)/' => 'AlmanacDeviceViewController', ), 'interface/' => array( 'edit/(?:(?P\d+)/)?' => 'AlmanacInterfaceEditController', + 'delete/(?:(?P\d+)/)?' => 'AlmanacInterfaceDeleteController', ), 'binding/' => array( 'edit/(?:(?P\d+)/)?' => 'AlmanacBindingEditController', + 'disable/(?:(?P\d+)/)?' => 'AlmanacBindingDisableController', '(?P\d+)/' => 'AlmanacBindingViewController', ), 'network/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacNetworkListController', + $this->getQueryRoutePattern() => 'AlmanacNetworkListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacNetworkEditController', '(?P\d+)/' => 'AlmanacNetworkViewController', ), + 'namespace/' => array( + $this->getQueryRoutePattern() => 'AlmanacNamespaceListController', + $this->getEditRoutePattern('edit/') + => 'AlmanacNamespaceEditController', + '(?P\d+)/' => 'AlmanacNamespaceViewController', + ), 'property/' => array( - 'edit/' => 'AlmanacPropertyEditController', 'delete/' => 'AlmanacPropertyDeleteController', + 'update/' => 'AlmanacPropertyEditController', ), ), ); } protected function getCustomCapabilities() { + $cluster_caption = pht( + 'This permission is very dangerous. %s', + phutil_tag( + 'a', + array( + 'href' => PhabricatorEnv::getDoclink( + 'User Guide: Phabricator Clusters'), + 'target' => '_blank', + ), + pht('Learn More'))); + return array( AlmanacCreateServicesCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, @@ -84,9 +103,13 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { AlmanacCreateNetworksCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), - AlmanacCreateClusterServicesCapability::CAPABILITY => array( + AlmanacCreateNamespacesCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), + AlmanacManageClusterServicesCapability::CAPABILITY => array( + 'default' => PhabricatorPolicies::POLICY_NOONE, + 'caption' => $cluster_caption, + ), ); } diff --git a/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php b/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php new file mode 100644 index 0000000000..bfff9aaeaf --- /dev/null +++ b/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php @@ -0,0 +1,16 @@ + (int)$service->getID(), - 'phid' => $service->getPHID(), - 'name' => $service->getName(), - 'uri' => PhabricatorEnv::getProductionURI($service->getURI()), - 'serviceClass' => $service->getServiceClass(), - 'properties' => $this->getPropertiesDictionary($service), - ); - } - - protected function getBindingDictionary(AlmanacBinding $binding) { - return array( - 'id' => (int)$binding->getID(), - 'phid' => $binding->getPHID(), - 'properties' => $this->getPropertiesDictionary($binding), - 'interface' => $this->getInterfaceDictionary($binding->getInterface()), - ); - } - - protected function getPropertiesDictionary(AlmanacPropertyInterface $obj) { - $properties = $obj->getAlmanacProperties(); - return (object)mpull($properties, 'getFieldValue', 'getFieldName'); - } - - protected function getInterfaceDictionary(AlmanacInterface $interface) { - return array( - 'id' => (int)$interface->getID(), - 'phid' => $interface->getPHID(), - 'address' => $interface->getAddress(), - 'port' => (int)$interface->getPort(), - 'device' => $this->getDeviceDictionary($interface->getDevice()), - 'network' => $this->getNetworkDictionary($interface->getNetwork()), - ); - } - - protected function getDeviceDictionary(AlmanacDevice $device) { - return array( - 'id' => (int)$device->getID(), - 'phid' => $device->getPHID(), - 'name' => $device->getName(), - 'properties' => $this->getPropertiesDictionary($device), - ); - } - - protected function getNetworkDictionary(AlmanacNetwork $network) { - return array( - 'id' => (int)$network->getID(), - 'phid' => $network->getPHID(), - 'name' => $network->getName(), - ); - } - -} diff --git a/src/applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php new file mode 100644 index 0000000000..af674428a5 --- /dev/null +++ b/src/applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ + 'optional list', - 'phids' => 'optional list', - 'names' => 'optional list', - ) + self::getPagerParamTypes(); - } - - protected function defineReturnType() { - return 'list'; - } - - protected function execute(ConduitAPIRequest $request) { - $viewer = $request->getUser(); - - $query = id(new AlmanacDeviceQuery()) - ->setViewer($viewer); - - $ids = $request->getValue('ids'); - if ($ids !== null) { - $query->withIDs($ids); - } - - $phids = $request->getValue('phids'); - if ($phids !== null) { - $query->withPHIDs($phids); - } - - $names = $request->getValue('names'); - if ($names !== null) { - $query->withNames($names); - } - - $pager = $this->newPager($request); - - $devices = $query->executeWithCursorPager($pager); - - $data = array(); - foreach ($devices as $device) { - $data[] = $this->getDeviceDictionary($device); - } - - $results = array( - 'data' => $data, - ); - - return $this->addPagerResults($results, $pager); - } - -} diff --git a/src/applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php deleted file mode 100644 index 81e59d072f..0000000000 --- a/src/applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php +++ /dev/null @@ -1,86 +0,0 @@ - 'optional list', - 'phids' => 'optional list', - 'names' => 'optional list', - 'devicePHIDs' => 'optional list', - 'serviceClasses' => 'optional list', - ) + self::getPagerParamTypes(); - } - - protected function defineReturnType() { - return 'list'; - } - - protected function execute(ConduitAPIRequest $request) { - $viewer = $request->getUser(); - - $query = id(new AlmanacServiceQuery()) - ->setViewer($viewer) - ->needBindings(true); - - $ids = $request->getValue('ids'); - if ($ids !== null) { - $query->withIDs($ids); - } - - $phids = $request->getValue('phids'); - if ($phids !== null) { - $query->withPHIDs($phids); - } - - $names = $request->getValue('names'); - if ($names !== null) { - $query->withNames($names); - } - - $classes = $request->getValue('serviceClasses'); - if ($classes !== null) { - $query->withServiceClasses($classes); - } - - $device_phids = $request->getValue('devicePHIDs'); - if ($device_phids !== null) { - $query->withDevicePHIDs($device_phids); - } - - $pager = $this->newPager($request); - - $services = $query->executeWithCursorPager($pager); - - $data = array(); - foreach ($services as $service) { - $phid = $service->getPHID(); - - $service_bindings = $service->getBindings(); - $service_bindings = array_values($service_bindings); - foreach ($service_bindings as $key => $service_binding) { - $service_bindings[$key] = $this->getBindingDictionary($service_binding); - } - - $data[] = $this->getServiceDictionary($service) + array( - 'bindings' => $service_bindings, - ); - } - - $results = array( - 'data' => $data, - ); - - return $this->addPagerResults($results, $pager); - } - -} diff --git a/src/applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php new file mode 100644 index 0000000000..2abe66beb6 --- /dev/null +++ b/src/applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +getViewer(); + + $id = $request->getURIData('id'); + $binding = id(new AlmanacBindingQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$binding) { + return new Aphront404Response(); + } + + $id = $binding->getID(); + $is_disable = !$binding->getIsDisabled(); + $done_uri = $binding->getURI(); + + if ($is_disable) { + $disable_title = pht('Disable Binding'); + $disable_body = pht('Disable this binding?'); + $disable_button = pht('Disable Binding'); + + $v_disable = 1; + } else { + $disable_title = pht('Enable Binding'); + $disable_body = pht('Enable this binding?'); + $disable_button = pht('Enable Binding'); + + $v_disable = 0; + } + + + if ($request->isFormPost()) { + $type_disable = AlmanacBindingTransaction::TYPE_DISABLE; + + $xactions = array(); + + $xactions[] = id(new AlmanacBindingTransaction()) + ->setTransactionType($type_disable) + ->setNewValue($v_disable); + + $editor = id(new AlmanacBindingEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($binding, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + return $this->newDialog() + ->setTitle($disable_title) + ->appendParagraph($disable_body) + ->addSubmitButton($disable_button) + ->addCancelButton($done_uri); + } + +} diff --git a/src/applications/almanac/controller/AlmanacBindingViewController.php b/src/applications/almanac/controller/AlmanacBindingViewController.php index 9e26ba2012..b75565525c 100644 --- a/src/applications/almanac/controller/AlmanacBindingViewController.php +++ b/src/applications/almanac/controller/AlmanacBindingViewController.php @@ -15,6 +15,7 @@ final class AlmanacBindingViewController $binding = id(new AlmanacBindingQuery()) ->setViewer($viewer) ->withIDs(array($id)) + ->needProperties(true) ->executeOne(); if (!$binding) { return new Aphront404Response(); @@ -34,15 +35,21 @@ final class AlmanacBindingViewController ->setHeader($title) ->setPolicyObject($binding); + if ($binding->getIsDisabled()) { + $header->setStatus('fa-ban', 'red', pht('Disabled')); + } + $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($property_list); - if ($binding->getService()->getIsLocked()) { - $this->addLockMessage( + if ($binding->getService()->isClusterService()) { + $this->addClusterMessage( $box, + pht('The service for this binding is a cluster service.'), pht( - 'This service for this binding is locked, so the binding can '. + 'The service for this binding is a cluster service. You do not '. + 'have permission to manage cluster services, so this binding can '. 'not be edited.')); } @@ -111,6 +118,24 @@ final class AlmanacBindingViewController ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); + if ($binding->getIsDisabled()) { + $disable_icon = 'fa-check'; + $disable_text = pht('Enable Binding'); + } else { + $disable_icon = 'fa-ban'; + $disable_text = pht('Disable Binding'); + } + + $disable_href = $this->getApplicationURI("binding/disable/{$id}/"); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon($disable_icon) + ->setName($disable_text) + ->setHref($disable_href) + ->setWorkflow(true) + ->setDisabled(!$can_edit)); + return $actions; } diff --git a/src/applications/almanac/controller/AlmanacConsoleController.php b/src/applications/almanac/controller/AlmanacConsoleController.php index 9f46bd54d3..9b83db9daa 100644 --- a/src/applications/almanac/controller/AlmanacConsoleController.php +++ b/src/applications/almanac/controller/AlmanacConsoleController.php @@ -12,26 +12,52 @@ final class AlmanacConsoleController extends AlmanacController { $menu = id(new PHUIObjectItemListView()) ->setUser($viewer); - $menu->addItem( - id(new PHUIObjectItemView()) - ->setHeader(pht('Services')) - ->setHref($this->getApplicationURI('service/')) - ->setIcon('fa-plug') - ->addAttribute(pht('Manage Almanac services.'))); - $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Devices')) ->setHref($this->getApplicationURI('device/')) ->setIcon('fa-server') - ->addAttribute(pht('Manage Almanac devices.'))); + ->addAttribute( + pht( + 'Create an inventory of physical and virtual hosts and '. + 'devices.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Services')) + ->setHref($this->getApplicationURI('service/')) + ->setIcon('fa-plug') + ->addAttribute( + pht( + 'Create and update services, and map them to interfaces on '. + 'devices.'))); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Networks')) ->setHref($this->getApplicationURI('network/')) ->setIcon('fa-globe') - ->addAttribute(pht('Manage Almanac networks.'))); + ->addAttribute( + pht( + 'Manage public and private networks.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Namespaces')) + ->setHref($this->getApplicationURI('namespace/')) + ->setIcon('fa-asterisk') + ->addAttribute( + pht('Control who can create new named services and devices.'))); + + $docs_uri = PhabricatorEnv::getDoclink( + 'Almanac User Guide'); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Documentation')) + ->setHref($docs_uri) + ->setIcon('fa-book') + ->addAttribute(pht('Browse documentation for Almanac.'))); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Console')); diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index e1a7c2a69e..3b354b0803 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -10,27 +10,14 @@ abstract class AlmanacController $properties = $object->getAlmanacProperties(); $this->requireResource('almanac-css'); + Javelin::initBehavior('phabricator-tooltips', array()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_DEFAULT); - - // Before reading values from the object, read defaults. - $defaults = mpull( - $field_list->getFields(), - 'getValueForStorage', - 'getFieldKey'); - - $field_list - ->setViewer($viewer) - ->readFieldsFromStorage($object); - - Javelin::initBehavior('phabricator-tooltips', array()); + $properties = $object->getAlmanacProperties(); $icon_builtin = id(new PHUIIconView()) ->setIcon('fa-circle') @@ -51,45 +38,46 @@ abstract class AlmanacController )); $builtins = $object->getAlmanacPropertyFieldSpecifications(); + $defaults = mpull($builtins, null, 'getValueForTransaction'); // Sort fields so builtin fields appear first, then fields are ordered // alphabetically. - $fields = $field_list->getFields(); - $fields = msort($fields, 'getFieldKey'); + $properties = msort($properties, 'getFieldName'); $head = array(); $tail = array(); - foreach ($fields as $field) { - $key = $field->getFieldKey(); + foreach ($properties as $property) { + $key = $property->getFieldName(); if (isset($builtins[$key])) { - $head[$key] = $field; + $head[$key] = $property; } else { - $tail[$key] = $field; + $tail[$key] = $property; } } - $fields = $head + $tail; + $properties = $head + $tail; + + $delete_base = $this->getApplicationURI('property/delete/'); + $edit_base = $this->getApplicationURI('property/update/'); $rows = array(); - foreach ($fields as $key => $field) { - $value = $field->getValueForStorage(); + foreach ($properties as $key => $property) { + $value = $property->getFieldValue(); $is_builtin = isset($builtins[$key]); - $delete_uri = $this->getApplicationURI('property/delete/'); - $delete_uri = id(new PhutilURI($delete_uri)) + $delete_uri = id(new PhutilURI($delete_base)) ->setQueryParams( array( - 'objectPHID' => $object->getPHID(), 'key' => $key, + 'objectPHID' => $object->getPHID(), )); - $edit_uri = $this->getApplicationURI('property/edit/'); - $edit_uri = id(new PhutilURI($edit_uri)) + $edit_uri = id(new PhutilURI($edit_base)) ->setQueryParams( array( - 'objectPHID' => $object->getPHID(), 'key' => $key, + 'objectPHID' => $object->getPHID(), )); $delete = javelin_tag( @@ -153,7 +141,8 @@ abstract class AlmanacController )); $phid = $object->getPHID(); - $add_uri = $this->getApplicationURI("property/edit/?objectPHID={$phid}"); + $add_uri = id(new PhutilURI($edit_base)) + ->setQueryParam('objectPHID', $object->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -177,23 +166,50 @@ abstract class AlmanacController ->setTable($table); } - protected function addLockMessage(PHUIObjectBoxView $box, $message) { + protected function addClusterMessage( + PHUIObjectBoxView $box, + $positive, + $negative) { + + $can_manage = $this->hasApplicationCapability( + AlmanacManageClusterServicesCapability::CAPABILITY); + $doc_link = phutil_tag( 'a', array( - 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), + 'href' => PhabricatorEnv::getDoclink( + 'User Guide: Phabricator Clusters'), 'target' => '_blank', ), pht('Learn More')); + if ($can_manage) { + $severity = PHUIInfoView::SEVERITY_NOTICE; + $message = $positive; + } else { + $severity = PHUIInfoView::SEVERITY_WARNING; + $message = $negative; + } + + $icon = id(new PHUIIconView()) + ->setIcon('fa-sitemap'); + $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setSeverity($severity) ->setErrors( array( - array($message, ' ', $doc_link), + array($icon, ' ', $message, ' ', $doc_link), )); $box->setInfoView($error_view); } + protected function getPropertyDeleteURI($object) { + return null; + } + + protected function getPropertyUpdateURI($object) { + return null; + } + } diff --git a/src/applications/almanac/controller/AlmanacDeviceViewController.php b/src/applications/almanac/controller/AlmanacDeviceViewController.php index c7756c5b25..4836198763 100644 --- a/src/applications/almanac/controller/AlmanacDeviceViewController.php +++ b/src/applications/almanac/controller/AlmanacDeviceViewController.php @@ -15,15 +15,12 @@ final class AlmanacDeviceViewController $device = id(new AlmanacDeviceQuery()) ->setViewer($viewer) ->withNames(array($name)) + ->needProperties(true) ->executeOne(); if (!$device) { return new Aphront404Response(); } - // We rebuild locks on a device when viewing the detail page, so they - // automatically get corrected if they fall out of sync. - $device->rebuildDeviceLocks(); - $title = pht('Device %s', $device->getName()); $property_list = $this->buildPropertyList($device); @@ -39,12 +36,14 @@ final class AlmanacDeviceViewController ->setHeader($header) ->addPropertyList($property_list); - if ($device->getIsLocked()) { - $this->addLockMessage( + if ($device->isClusterDevice()) { + $this->addClusterMessage( $box, + pht('This device is bound to a cluster service.'), pht( - 'This device is bound to a locked service, so it can not be '. - 'edited.')); + 'This device is bound to a cluster service. You do not have '. + 'permission to manage cluster services, so the device can not '. + 'be edited.')); } $interfaces = $this->buildInterfaceList($device); @@ -218,14 +217,14 @@ final class AlmanacDeviceViewController $handles = $viewer->loadHandles(mpull($services, 'getPHID')); - $icon_lock = id(new PHUIIconView()) - ->setIcon('fa-lock'); + $icon_cluster = id(new PHUIIconView()) + ->setIcon('fa-sitemap'); $rows = array(); foreach ($services as $service) { $rows[] = array( - ($service->getIsLocked() - ? $icon_lock + ($service->isClusterService() + ? $icon_cluster : null), $handles->renderHandle($service->getPHID()), ); diff --git a/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php b/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php new file mode 100644 index 0000000000..ddbe979af6 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php @@ -0,0 +1,72 @@ +getViewer(); + + $id = $request->getURIData('id'); + $interface = id(new AlmanacInterfaceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$interface) { + return new Aphront404Response(); + } + + $device = $interface->getDevice(); + $device_uri = $device->getURI(); + + if ($interface->loadIsInUse()) { + return $this->newDialog() + ->setTitle(pht('Interface In Use')) + ->appendParagraph( + pht( + 'You can not delete this interface because it is currently in '. + 'use. One or more services are bound to it.')) + ->addCancelButton($device_uri); + } + + if ($request->isFormPost()) { + $type_interface = AlmanacDeviceTransaction::TYPE_INTERFACE; + + $xactions = array(); + + $v_old = array( + 'id' => $interface->getID(), + ) + $interface->toAddress()->toDictionary(); + + $xactions[] = id(new AlmanacDeviceTransaction()) + ->setTransactionType($type_interface) + ->setOldValue($v_old) + ->setNewValue(null); + + $editor = id(new AlmanacDeviceEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($device, $xactions); + + return id(new AphrontRedirectResponse())->setURI($device_uri); + } + + return $this->newDialog() + ->setTitle(pht('Delete Interface')) + ->appendParagraph( + pht( + 'Remove interface %s on device %s?', + phutil_tag('strong', array(), $interface->renderDisplayAddress()), + phutil_tag('strong', array(), $device->getName()))) + ->addCancelButton($device_uri) + ->addSubmitButton(pht('Delete Interface')); + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceController.php b/src/applications/almanac/controller/AlmanacNamespaceController.php new file mode 100644 index 0000000000..abdb8a01e6 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceController.php @@ -0,0 +1,14 @@ +getApplicationURI('namespace/'); + $crumbs->addTextCrumb(pht('Namespaces'), $list_uri); + + return $crumbs; + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceEditController.php b/src/applications/almanac/controller/AlmanacNamespaceEditController.php new file mode 100644 index 0000000000..0bb6ec2620 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceEditController.php @@ -0,0 +1,11 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceListController.php b/src/applications/almanac/controller/AlmanacNamespaceListController.php new file mode 100644 index 0000000000..72c9593457 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceListController.php @@ -0,0 +1,26 @@ +setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + id(new AlmanacNamespaceEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); + + return $crumbs; + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceViewController.php b/src/applications/almanac/controller/AlmanacNamespaceViewController.php new file mode 100644 index 0000000000..4f140683a2 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceViewController.php @@ -0,0 +1,87 @@ +getViewer(); + + $id = $request->getURIData('id'); + $namespace = id(new AlmanacNamespaceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$namespace) { + return new Aphront404Response(); + } + + $title = pht('Namespace %s', $namespace->getName()); + + $property_list = $this->buildPropertyList($namespace); + $action_list = $this->buildActionList($namespace); + $property_list->setActionList($action_list); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($namespace->getName()) + ->setPolicyObject($namespace); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($property_list); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($namespace->getName()); + + $timeline = $this->buildTransactionTimeline( + $namespace, + new AlmanacNamespaceTransactionQuery()); + $timeline->setShouldTerminate(true); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $box, + $timeline, + )); + } + + private function buildPropertyList(AlmanacNamespace $namespace) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + return $properties; + } + + private function buildActionList(AlmanacNamespace $namespace) { + $viewer = $this->getViewer(); + $id = $namespace->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $namespace, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Namespace')) + ->setHref($this->getApplicationURI("namespace/edit/{$id}/")) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + return $actions; + } + +} diff --git a/src/applications/almanac/controller/AlmanacNetworkEditController.php b/src/applications/almanac/controller/AlmanacNetworkEditController.php index b4ab77b95b..a67fcbc5a1 100644 --- a/src/applications/almanac/controller/AlmanacNetworkEditController.php +++ b/src/applications/almanac/controller/AlmanacNetworkEditController.php @@ -1,142 +1,11 @@ getViewer(); - - $list_uri = $this->getApplicationURI('network/'); - - $id = $request->getURIData('id'); - if ($id) { - $network = id(new AlmanacNetworkQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$network) { - return new Aphront404Response(); - } - - $is_new = false; - $network_uri = $this->getApplicationURI('network/'.$network->getID().'/'); - $cancel_uri = $network_uri; - $title = pht('Edit Network'); - $save_button = pht('Save Changes'); - } else { - $this->requireApplicationCapability( - AlmanacCreateNetworksCapability::CAPABILITY); - - $network = AlmanacNetwork::initializeNewNetwork(); - $is_new = true; - - $cancel_uri = $list_uri; - $title = pht('Create Network'); - $save_button = pht('Create Network'); - } - - $v_name = $network->getName(); - $e_name = true; - $validation_exception = null; - - if ($request->isFormPost()) { - $v_name = $request->getStr('name'); - $v_view = $request->getStr('viewPolicy'); - $v_edit = $request->getStr('editPolicy'); - - $type_name = AlmanacNetworkTransaction::TYPE_NAME; - $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; - $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; - - $xactions = array(); - - $xactions[] = id(new AlmanacNetworkTransaction()) - ->setTransactionType($type_name) - ->setNewValue($v_name); - - $xactions[] = id(new AlmanacNetworkTransaction()) - ->setTransactionType($type_view) - ->setNewValue($v_view); - - $xactions[] = id(new AlmanacNetworkTransaction()) - ->setTransactionType($type_edit) - ->setNewValue($v_edit); - - $editor = id(new AlmanacNetworkEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true); - - try { - $editor->applyTransactions($network, $xactions); - - $id = $network->getID(); - $network_uri = $this->getApplicationURI("network/{$id}/"); - return id(new AphrontRedirectResponse())->setURI($network_uri); - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; - $e_name = $ex->getShortMessage($type_name); - - $network->setViewPolicy($v_view); - $network->setEditPolicy($v_edit); - } - } - - $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($viewer) - ->setObject($network) - ->execute(); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Name')) - ->setName('name') - ->setValue($v_name) - ->setError($e_name)) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setName('viewPolicy') - ->setPolicyObject($network) - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) - ->setPolicies($policies)) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setName('editPolicy') - ->setPolicyObject($network) - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) - ->setPolicies($policies)) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($cancel_uri) - ->setValue($save_button)); - - $box = id(new PHUIObjectBoxView()) - ->setValidationException($validation_exception) - ->setHeaderText($title) - ->setForm($form); - - $crumbs = $this->buildApplicationCrumbs(); - if ($is_new) { - $crumbs->addTextCrumb(pht('Create Network')); - } else { - $crumbs->addTextCrumb($network->getName(), $network_uri); - $crumbs->addTextCrumb(pht('Edit')); - } - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild( - array( - $box, - )); + return id(new AlmanacNetworkEditEngine()) + ->setController($this) + ->buildResponse(); } } diff --git a/src/applications/almanac/controller/AlmanacPropertyController.php b/src/applications/almanac/controller/AlmanacPropertyController.php index 73d47431fd..deb2709ddd 100644 --- a/src/applications/almanac/controller/AlmanacPropertyController.php +++ b/src/applications/almanac/controller/AlmanacPropertyController.php @@ -1,3 +1,56 @@ propertyObject; + } + + protected function loadPropertyObject() { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + $object_phid = $request->getStr('objectPHID'); + + + switch (phid_get_type($object_phid)) { + case AlmanacBindingPHIDType::TYPECONST: + $query = new AlmanacBindingQuery(); + break; + case AlmanacDevicePHIDType::TYPECONST: + $query = new AlmanacDeviceQuery(); + break; + case AlmanacServicePHIDType::TYPECONST: + $query = new AlmanacServiceQuery(); + break; + default: + return new Aphront404Response(); + } + + $object = $query + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->needProperties(true) + ->executeOne(); + + if (!$object) { + return new Aphront404Response(); + } + + if (!($object instanceof AlmanacPropertyInterface)) { + return new Aphront404Response(); + } + + $this->propertyObject = $object; + + return null; + } + + +} diff --git a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php index 0b328affcd..93ec4ced64 100644 --- a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php +++ b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php @@ -1,27 +1,17 @@ getViewer(); - $object = id(new PhabricatorObjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($request->getStr('objectPHID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); + $response = $this->loadPropertyObject(); + if ($response) { + return $response; } - if (!($object instanceof AlmanacPropertyInterface)) { - return new Aphront404Response(); - } + $object = $this->getPropertyObject(); $key = $request->getStr('key'); if (!strlen($key)) { @@ -34,53 +24,24 @@ final class AlmanacPropertyDeleteController $is_builtin = isset($builtins[$key]); if ($is_builtin) { - // This is a builtin property, so we're going to reset it to the - // default value. - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_DEFAULT); - - // Note that we're NOT loading field values from the object: we just want - // to get the field's default value so we can reset it. - - $fields = $field_list->getFields(); - $field = $fields[$key]; - - $is_delete = false; - $new_value = $field->getValueForStorage(); - - // Now, load the field to get the old value. - - $field_list - ->setViewer($viewer) - ->readFieldsFromStorage($object); - - $old_value = $field->getValueForStorage(); - $title = pht('Reset Property'); - $body = pht('Reset this property to its default value?'); - $submit_text = pht('Reset'); + $body = pht( + 'Reset property "%s" to its default value?', + $key); + $submit_text = pht('Reset Property'); } else { - // This is a custom property, so we're going to delete it outright. - $is_delete = true; - $old_value = $object->getAlmanacPropertyValue($key); - $new_value = null; - $title = pht('Delete Property'); - $body = pht('Delete this property? TODO: DOES NOT WORK YET'); - $submit_text = pht('Delete'); + $body = pht( + 'Delete property "%s"?', + $key); + $submit_text = pht('Delete Property'); } $validation_exception = null; if ($request->isFormPost()) { $xaction = $object->getApplicationTransactionTemplate() - ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) - ->setMetadataValue('customfield:key', $key) - ->setOldValue($old_value) - ->setNewValue($new_value); - - // TODO: We aren't really deleting properties that we claim to delete - // yet, but that needs to be specialized a little bit. + ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_REMOVE) + ->setMetadataValue('almanac.property', $key); $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) diff --git a/src/applications/almanac/controller/AlmanacPropertyEditController.php b/src/applications/almanac/controller/AlmanacPropertyEditController.php index 9a881bc046..ef587e5b05 100644 --- a/src/applications/almanac/controller/AlmanacPropertyEditController.php +++ b/src/applications/almanac/controller/AlmanacPropertyEditController.php @@ -1,156 +1,94 @@ getViewer(); - $object = id(new PhabricatorObjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($request->getStr('objectPHID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); + $response = $this->loadPropertyObject(); + if ($response) { + return $response; } - if (!($object instanceof AlmanacPropertyInterface)) { - return new Aphront404Response(); - } + $object = $this->getPropertyObject(); $cancel_uri = $object->getURI(); + $property_key = $request->getStr('key'); - $key = $request->getStr('key'); - if ($key) { - $property_key = $key; - - $is_new = false; - $title = pht('Edit Property'); - $save_button = pht('Save Changes'); + if (!strlen($property_key)) { + return $this->buildPropertyKeyResponse($cancel_uri, null); } else { - $property_key = null; - - $is_new = true; - $title = pht('Add Property'); - $save_button = pht('Add Property'); - } - - if ($is_new) { - $errors = array(); - $property = null; - - $v_name = null; - $e_name = true; - - if ($request->isFormPost()) { - $name = $request->getStr('name'); - if (!strlen($name)) { - $e_name = pht('Required'); - $errors[] = pht('You must provide a property name.'); - } else { - $caught = null; - try { - AlmanacNames::validateServiceOrDeviceName($name); - } catch (Exception $ex) { - $caught = $ex; - } - if ($caught) { - $e_name = pht('Invalid'); - $errors[] = $caught->getMessage(); - } - } - - if (!$errors) { - $property_key = $name; - } - } - - if ($property_key === null) { - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormTextControl()) - ->setName('name') - ->setLabel(pht('Name')) - ->setValue($v_name) - ->setError($e_name)); - - return $this->newDialog() - ->setTitle($title) - ->setErrors($errors) - ->addHiddenInput('objectPHID', $request->getStr('objectPHID')) - ->appendForm($form) - ->addSubmitButton(pht('Continue')) - ->addCancelButton($cancel_uri); - } - } - - // Make sure property key is appropriate. - // TODO: It would be cleaner to put this safety check in the Editor. - AlmanacNames::validateServiceOrDeviceName($property_key); - - // If we're adding a new property, put a placeholder on the object so - // that we can build a CustomField for it. - if (!$object->hasAlmanacProperty($property_key)) { - $temporary_property = id(new AlmanacProperty()) - ->setObjectPHID($object->getPHID()) - ->setFieldName($property_key); - - $object->attachAlmanacProperties(array($temporary_property)); - } - - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_DEFAULT); - - // Select only the field being edited. - $fields = $field_list->getFields(); - $fields = array_select_keys($fields, array($property_key)); - $field_list = new PhabricatorCustomFieldList($fields); - - $field_list - ->setViewer($viewer) - ->readFieldsFromStorage($object); - - $validation_exception = null; - if ($request->isFormPost() && $request->getStr('isValueEdit')) { - $xactions = $field_list->buildFieldTransactionsFromRequest( - $object->getApplicationTransactionTemplate(), - $request); - - $editor = $object->getApplicationTransactionEditor() - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - + $error = null; try { - $editor->applyTransactions($object, $xactions); - return id(new AphrontRedirectResponse())->setURI($cancel_uri); - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; + AlmanacNames::validateName($property_key); + } catch (Exception $ex) { + $error = $ex->getMessage(); } + + // NOTE: If you enter an existing name, we're just treating it as an + // edit operation. This might be a little confusing. + + if ($error !== null) { + if ($request->isFormPost()) { + // The user is creating a new property and picked a bad name. Give + // them an opportunity to fix it. + return $this->buildPropertyKeyResponse($cancel_uri, $error); + } else { + // The user is editing an invalid property. + return $this->newDialog() + ->setTitle(pht('Invalid Property')) + ->appendParagraph( + pht( + 'The property name "%s" is invalid. This property can not '. + 'be edited.', + $property_key)) + ->appendParagraph($error) + ->addCancelButton($cancel_uri); + } + } + } + + return $object->newAlmanacPropertyEditEngine() + ->addContextParameter('objectPHID') + ->addContextParameter('key') + ->setTargetObject($object) + ->setPropertyKey($property_key) + ->setController($this) + ->buildResponse(); + } + + private function buildPropertyKeyResponse($cancel_uri, $error) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + $v_key = $request->getStr('key'); + + if ($error !== null) { + $e_key = pht('Invalid'); + } else { + $e_key = true; } $form = id(new AphrontFormView()) ->setUser($viewer) - ->addHiddenInput('objectPHID', $request->getStr('objectPHID')) - ->addHiddenInput('key', $request->getStr('key')) - ->addHiddenInput('name', $property_key) - ->addHiddenInput('isValueEdit', true); + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('key') + ->setLabel(pht('Name')) + ->setValue($v_key) + ->setError($e_key)); - $field_list->appendFieldsToForm($form); + $errors = array(); + if ($error !== null) { + $errors[] = $error; + } return $this->newDialog() - ->setTitle($title) - ->setValidationException($validation_exception) + ->setTitle(pht('Add Property')) + ->addHiddenInput('objectPHID', $request->getStr('objectPHID')) + ->setErrors($errors) ->appendForm($form) - ->addSubmitButton($save_button) + ->addSubmitButton(pht('Continue')) ->addCancelButton($cancel_uri); } diff --git a/src/applications/almanac/controller/AlmanacServiceController.php b/src/applications/almanac/controller/AlmanacServiceController.php index daf05f0692..12bfbf94ea 100644 --- a/src/applications/almanac/controller/AlmanacServiceController.php +++ b/src/applications/almanac/controller/AlmanacServiceController.php @@ -11,4 +11,14 @@ abstract class AlmanacServiceController extends AlmanacController { return $crumbs; } + protected function getPropertyDeleteURI($object) { + $id = $object->getID(); + return "/almanac/service/delete/{$id}/"; + } + + protected function getPropertyUpdateURI($object) { + $id = $object->getID(); + return "/almanac/service/property/{$id}/"; + } + } diff --git a/src/applications/almanac/controller/AlmanacServiceEditController.php b/src/applications/almanac/controller/AlmanacServiceEditController.php index 20a7528704..2b6f5a9f5d 100644 --- a/src/applications/almanac/controller/AlmanacServiceEditController.php +++ b/src/applications/almanac/controller/AlmanacServiceEditController.php @@ -34,21 +34,19 @@ final class AlmanacServiceEditController $this->requireApplicationCapability( AlmanacCreateServicesCapability::CAPABILITY); - $service_class = $request->getStr('serviceClass'); - $service_types = AlmanacServiceType::getAllServiceTypes(); - if (empty($service_types[$service_class])) { - return $this->buildServiceTypeResponse($service_types, $cancel_uri); + $service_type = $request->getStr('serviceType'); + + try { + $service = AlmanacService::initializeNewService($service_type); + } catch (Exception $ex) { + return $this->buildServiceTypeResponse($cancel_uri); } - $service_type = $service_types[$service_class]; - if ($service_type->isClusterServiceType()) { + if ($service->isClusterService()) { $this->requireApplicationCapability( - AlmanacCreateClusterServicesCapability::CAPABILITY); + AlmanacManageClusterServicesCapability::CAPABILITY); } - $service = AlmanacService::initializeNewService(); - $service->setServiceClass($service_class); - $service->attachServiceType($service_type); $is_new = true; $title = pht('Create Service'); @@ -125,7 +123,7 @@ final class AlmanacServiceEditController $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('edit', true) - ->addHiddenInput('serviceClass', $service->getServiceClass()) + ->addHiddenInput('serviceType', $service->getServiceType()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) @@ -177,7 +175,9 @@ final class AlmanacServiceEditController )); } - private function buildServiceTypeResponse(array $service_types, $cancel_uri) { + private function buildServiceTypeResponse($cancel_uri) { + $service_types = AlmanacServiceType::getAllServiceTypes(); + $request = $this->getRequest(); $viewer = $this->getViewer(); @@ -190,14 +190,14 @@ final class AlmanacServiceEditController } list($can_cluster, $cluster_link) = $this->explainApplicationCapability( - AlmanacCreateClusterServicesCapability::CAPABILITY, + AlmanacManageClusterServicesCapability::CAPABILITY, pht('You have permission to create cluster services.'), pht('You do not have permission to create new cluster services.')); $type_control = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Service Type')) - ->setName('serviceClass') + ->setName('serviceType') ->setError($e_service); foreach ($service_types as $service_type) { @@ -211,7 +211,7 @@ final class AlmanacServiceEditController } $type_control->addButton( - get_class($service_type), + $service_type->getServiceTypeConstant(), $service_type->getServiceTypeName(), array( $service_type->getServiceTypeDescription(), diff --git a/src/applications/almanac/controller/AlmanacServiceViewController.php b/src/applications/almanac/controller/AlmanacServiceViewController.php index 113722bf06..68defec4b9 100644 --- a/src/applications/almanac/controller/AlmanacServiceViewController.php +++ b/src/applications/almanac/controller/AlmanacServiceViewController.php @@ -15,6 +15,7 @@ final class AlmanacServiceViewController $service = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withNames(array($name)) + ->needProperties(true) ->executeOne(); if (!$service) { return new Aphront404Response(); @@ -35,15 +36,13 @@ final class AlmanacServiceViewController ->setHeader($header) ->addPropertyList($property_list); - $messages = $service->getServiceType()->getStatusMessages($service); - if ($messages) { - $box->setFormErrors($messages); - } - - if ($service->getIsLocked()) { - $this->addLockMessage( + if ($service->isClusterService()) { + $this->addClusterMessage( $box, - pht('This service is locked, and can not be edited.')); + pht('This is a cluster service.'), + pht( + 'This service is a cluster service. You do not have permission to '. + 'edit cluster services, so you can not edit this service.')); } $bindings = $this->buildBindingList($service); @@ -77,7 +76,7 @@ final class AlmanacServiceViewController $properties->addProperty( pht('Service Type'), - $service->getServiceType()->getServiceTypeShortName()); + $service->getServiceImplementation()->getServiceTypeShortName()); return $properties; } @@ -123,7 +122,8 @@ final class AlmanacServiceViewController ->setNoDataString( pht('This service has not been bound to any device interfaces yet.')) ->setUser($viewer) - ->setBindings($bindings); + ->setBindings($bindings) + ->setHideServiceColumn(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Service Bindings')) diff --git a/src/applications/almanac/customfield/AlmanacCoreCustomField.php b/src/applications/almanac/customfield/AlmanacCoreCustomField.php deleted file mode 100644 index c1df52321f..0000000000 --- a/src/applications/almanac/customfield/AlmanacCoreCustomField.php +++ /dev/null @@ -1,77 +0,0 @@ -getProxy()->getRawStandardFieldKey(); - } - - public function getFieldName() { - return $this->getFieldKey(); - } - - public function createFields($object) { - - $specs = $object->getAlmanacPropertyFieldSpecifications(); - - $default_specs = array(); - foreach ($object->getAlmanacProperties() as $property) { - $default_specs[$property->getFieldName()] = array( - 'name' => $property->getFieldName(), - 'type' => 'text', - ); - } - - return PhabricatorStandardCustomField::buildStandardFields( - $this, - $specs + $default_specs); - } - - public function shouldUseStorage() { - return false; - } - - public function readValueFromObject(PhabricatorCustomFieldInterface $object) { - $key = $this->getFieldKey(); - - if ($object->hasAlmanacProperty($key)) { - $this->setValueFromStorage($object->getAlmanacPropertyValue($key)); - } - } - - public function applyApplicationTransactionInternalEffects( - PhabricatorApplicationTransaction $xaction) { - return; - } - - public function applyApplicationTransactionExternalEffects( - PhabricatorApplicationTransaction $xaction) { - - $object = $this->getObject(); - $phid = $object->getPHID(); - $key = $this->getFieldKey(); - - $property = id(new AlmanacPropertyQuery()) - ->setViewer($this->getViewer()) - ->withObjectPHIDs(array($phid)) - ->withNames(array($key)) - ->executeOne(); - if (!$property) { - $property = id(new AlmanacProperty()) - ->setObjectPHID($phid) - ->setFieldIndex(PhabricatorHash::digestForIndex($key)) - ->setFieldName($key); - } - - $property - ->setFieldValue($xaction->getNewValue()) - ->save(); - } - -} diff --git a/src/applications/almanac/customfield/AlmanacCustomField.php b/src/applications/almanac/customfield/AlmanacCustomField.php deleted file mode 100644 index 6aefc35d2d..0000000000 --- a/src/applications/almanac/customfield/AlmanacCustomField.php +++ /dev/null @@ -1,4 +0,0 @@ -getTransactionType()) { case AlmanacBindingTransaction::TYPE_INTERFACE: return $object->getInterfacePHID(); + case AlmanacBindingTransaction::TYPE_DISABLE: + return $object->getIsDisabled(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -37,6 +38,8 @@ final class AlmanacBindingEditor switch ($xaction->getTransactionType()) { case AlmanacBindingTransaction::TYPE_INTERFACE: return $xaction->getNewValue(); + case AlmanacBindingTransaction::TYPE_DISABLE: + return (int)$xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); @@ -55,6 +58,9 @@ final class AlmanacBindingEditor $object->setDevicePHID($interface->getDevicePHID()); $object->setInterfacePHID($interface->getPHID()); return; + case AlmanacBindingTransaction::TYPE_DISABLE: + $object->setIsDisabled($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -65,7 +71,37 @@ final class AlmanacBindingEditor PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case AlmanacBindingTransaction::TYPE_DISABLE: + return; case AlmanacBindingTransaction::TYPE_INTERFACE: + $interface_phids = array(); + + $interface_phids[] = $xaction->getOldValue(); + $interface_phids[] = $xaction->getNewValue(); + + $interface_phids = array_filter($interface_phids); + $interface_phids = array_unique($interface_phids); + + $interfaces = id(new AlmanacInterfaceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($interface_phids) + ->execute(); + + $device_phids = array(); + foreach ($interfaces as $interface) { + $device_phids[] = $interface->getDevicePHID(); + } + + $device_phids = array_unique($device_phids); + + $devices = id(new AlmanacDeviceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($device_phids) + ->execute(); + + foreach ($devices as $device) { + $device->rebuildClusterBindingStatus(); + } return; } diff --git a/src/applications/almanac/editor/AlmanacBindingPropertyEditEngine.php b/src/applications/almanac/editor/AlmanacBindingPropertyEditEngine.php new file mode 100644 index 0000000000..ef51d755c5 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacBindingPropertyEditEngine.php @@ -0,0 +1,16 @@ +getURI(); + } + +} diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php index 3f7817bccc..64b8de7bad 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -1,11 +1,7 @@ getNewValue(); try { - AlmanacNames::validateServiceOrDeviceName($name); + AlmanacNames::validateName($name); } catch (Exception $ex) { $message = $ex->getMessage(); } @@ -144,22 +140,42 @@ final class AlmanacDeviceEditor $message, $xaction); $errors[] = $error; + continue; } - } - } - if ($xactions) { - $duplicate = id(new AlmanacDeviceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array(last($xactions)->getNewValue())) - ->executeOne(); - if ($duplicate && ($duplicate->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht('Almanac devices must have unique names.'), - last($xactions)); - $errors[] = $error; + $other = id(new AlmanacDeviceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Not Unique'), + pht('Almanac devices must have unique names.'), + $xaction); + $errors[] = $error; + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac devices '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } } } @@ -294,6 +310,19 @@ final class AlmanacDeviceEditor pht('You can not edit an invalid or restricted interface.'), $xaction); $errors[] = $error; + continue; + } + + $new = $xaction->getNewValue(); + if (!$new) { + if ($interface->loadIsInUse()) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('In Use'), + pht('You can not delete an interface which is still in use.'), + $xaction); + $errors[] = $error; + } } } } @@ -303,6 +332,4 @@ final class AlmanacDeviceEditor return $errors; } - - } diff --git a/src/applications/almanac/editor/AlmanacDevicePropertyEditEngine.php b/src/applications/almanac/editor/AlmanacDevicePropertyEditEngine.php new file mode 100644 index 0000000000..9f10219591 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacDevicePropertyEditEngine.php @@ -0,0 +1,16 @@ +getURI(); + } + +} diff --git a/src/applications/almanac/editor/AlmanacEditor.php b/src/applications/almanac/editor/AlmanacEditor.php new file mode 100644 index 0000000000..a628606dbc --- /dev/null +++ b/src/applications/almanac/editor/AlmanacEditor.php @@ -0,0 +1,156 @@ +getTransactionType()) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + $property_key = $xaction->getMetadataValue('almanac.property'); + $exists = $object->hasAlmanacProperty($property_key); + $value = $object->getAlmanacPropertyValue($property_key); + return array( + 'existed' => $exists, + 'value' => $value, + ); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + $property_key = $xaction->getMetadataValue('almanac.property'); + if ($object->hasAlmanacProperty($property_key)) { + $property = $object->getAlmanacProperty($property_key); + } else { + $property = id(new AlmanacProperty()) + ->setObjectPHID($object->getPHID()) + ->setFieldName($property_key); + } + $property + ->setFieldValue($xaction->getNewValue()) + ->save(); + return; + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + $property_key = $xaction->getMetadataValue('almanac.property'); + if ($object->hasAlmanacProperty($property_key)) { + $property = $object->getAlmanacProperty($property_key); + $property->delete(); + } + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + foreach ($xactions as $xaction) { + $property_key = $xaction->getMetadataValue('almanac.property'); + + $message = null; + try { + AlmanacNames::validateName($property_key); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $message, + $xaction); + $errors[] = $error; + continue; + } + + $new_value = $xaction->getNewValue(); + try { + phutil_json_encode($new_value); + } catch (Exception $ex) { + $message = pht( + 'Almanac property values must be representable in JSON. %s', + $ex->getMessage()); + } + + if ($message !== null) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $message, + $xaction); + $errors[] = $error; + continue; + } + } + break; + + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + // NOTE: No name validation on removals since it's OK to delete + // an invalid property that somehow came into existence. + break; + } + + return $errors; + } + +} diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php b/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php new file mode 100644 index 0000000000..0aaba76d3c --- /dev/null +++ b/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php @@ -0,0 +1,86 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Namespace'); + } + + protected function getObjectCreateShortText() { + return pht('Create Namespace'); + } + + protected function getEditorURI() { + return '/almanac/namespace/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/almanac/namespace/'; + } + + protected function getObjectViewURI($object) { + $id = $object->getID(); + return "/almanac/namespace/{$id}/"; + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + AlmanacCreateNamespacesCapability::CAPABILITY); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the namespace.')) + ->setTransactionType(AlmanacNamespaceTransaction::TYPE_NAME) + ->setIsRequired(true) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditor.php b/src/applications/almanac/editor/AlmanacNamespaceEditor.php new file mode 100644 index 0000000000..5243d3c935 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacNamespaceEditor.php @@ -0,0 +1,182 @@ +getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return $object->getName(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case AlmanacNamespaceTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Namespace name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } else { + foreach ($xactions as $xaction) { + $name = $xaction->getNewValue(); + + $message = null; + try { + AlmanacNames::validateName($name); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $message, + $xaction); + $errors[] = $error; + continue; + } + + $other = id(new AlmanacNamespaceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Not Unique'), + pht( + 'The namespace name "%s" is already in use by another '. + 'namespace. Each namespace must have a unique name.', + $name), + $xaction); + $errors[] = $error; + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac namespaces '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } + } + } + + break; + } + + return $errors; + } + + protected function didCatchDuplicateKeyException( + PhabricatorLiskDAO $object, + array $xactions, + Exception $ex) { + + $errors = array(); + + $errors[] = new PhabricatorApplicationTransactionValidationError( + null, + pht('Invalid'), + pht( + 'Another namespace with this name already exists. Each namespace '. + 'must have a unique name.'), + null); + + throw new PhabricatorApplicationTransactionValidationException($errors); + } + +} diff --git a/src/applications/almanac/editor/AlmanacNetworkEditEngine.php b/src/applications/almanac/editor/AlmanacNetworkEditEngine.php new file mode 100644 index 0000000000..a99079ff11 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacNetworkEditEngine.php @@ -0,0 +1,86 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Network'); + } + + protected function getObjectCreateShortText() { + return pht('Create Network'); + } + + protected function getEditorURI() { + return '/almanac/network/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/almanac/network/'; + } + + protected function getObjectViewURI($object) { + $id = $object->getID(); + return "/almanac/network/{$id}/"; + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + AlmanacCreateNetworksCapability::CAPABILITY); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the network.')) + ->setTransactionType(AlmanacNetworkTransaction::TYPE_NAME) + ->setIsRequired(true) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/almanac/editor/AlmanacNetworkEditor.php b/src/applications/almanac/editor/AlmanacNetworkEditor.php index 4e49cce163..f32b58219e 100644 --- a/src/applications/almanac/editor/AlmanacNetworkEditor.php +++ b/src/applications/almanac/editor/AlmanacNetworkEditor.php @@ -11,6 +11,10 @@ final class AlmanacNetworkEditor return pht('Almanac Network'); } + protected function supportsSearch() { + return true; + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); diff --git a/src/applications/almanac/editor/AlmanacPropertyEditEngine.php b/src/applications/almanac/editor/AlmanacPropertyEditEngine.php new file mode 100644 index 0000000000..c22a3ab869 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacPropertyEditEngine.php @@ -0,0 +1,79 @@ +propertyKey = $property_key; + return $this; + } + + public function getPropertyKey() { + return $this->propertyKey; + } + + public function isEngineConfigurable() { + return false; + } + + public function isEngineExtensible() { + return false; + } + + public function getEngineName() { + return pht('Almanac Properties'); + } + + public function getSummaryHeader() { + return pht('Edit Almanac Property Configurations'); + } + + public function getSummaryText() { + return pht('This engine is used to edit Almanac properties.'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorAlmanacApplication'; + } + + protected function newEditableObject() { + throw new PhutilMethodNotImplementedException(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create Property'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create Property'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Property: %s', $object->getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Property'); + } + + protected function getObjectCreateShortText() { + return pht('Create Property'); + } + + protected function buildCustomEditFields($object) { + $property_key = $this->getPropertyKey(); + $xaction_type = AlmanacTransaction::TYPE_PROPERTY_UPDATE; + + return array( + id(new PhabricatorTextEditField()) + ->setKey('value') + ->setMetadataValue('almanac.property', $property_key) + ->setLabel($property_key) + ->setTransactionType($xaction_type) + ->setValue($object->getAlmanacPropertyValue($property_key)), + ); + } + +} diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php index 868f31d026..1f6fe09cfa 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -1,11 +1,7 @@ getTransactionType()) { case AlmanacServiceTransaction::TYPE_NAME: return $object->getName(); - case AlmanacServiceTransaction::TYPE_LOCK: - return (bool)$object->getIsLocked(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -43,8 +36,6 @@ final class AlmanacServiceEditor switch ($xaction->getTransactionType()) { case AlmanacServiceTransaction::TYPE_NAME: return $xaction->getNewValue(); - case AlmanacServiceTransaction::TYPE_LOCK: - return (bool)$xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); @@ -58,9 +49,6 @@ final class AlmanacServiceEditor case AlmanacServiceTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; - case AlmanacServiceTransaction::TYPE_LOCK: - $object->setIsLocked((int)$xaction->getNewValue()); - return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -73,23 +61,6 @@ final class AlmanacServiceEditor switch ($xaction->getTransactionType()) { case AlmanacServiceTransaction::TYPE_NAME: return; - case AlmanacServiceTransaction::TYPE_LOCK: - $service = id(new AlmanacServiceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs(array($object->getPHID())) - ->needBindings(true) - ->executeOne(); - - $devices = array(); - foreach ($service->getBindings() as $binding) { - $device = $binding->getInterface()->getDevice(); - $devices[$device->getPHID()] = $device; - } - - foreach ($devices as $device) { - $device->rebuildDeviceLocks(); - } - return; } return parent::applyCustomExternalTransaction($object, $xaction); @@ -124,7 +95,7 @@ final class AlmanacServiceEditor $name = $xaction->getNewValue(); try { - AlmanacNames::validateServiceOrDeviceName($name); + AlmanacNames::validateName($name); } catch (Exception $ex) { $message = $ex->getMessage(); } @@ -136,22 +107,42 @@ final class AlmanacServiceEditor $message, $xaction); $errors[] = $error; + continue; } - } - } - if ($xactions) { - $duplicate = id(new AlmanacServiceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array(last($xactions)->getNewValue())) - ->executeOne(); - if ($duplicate && ($duplicate->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht('Almanac services must have unique names.'), - last($xactions)); - $errors[] = $error; + $other = id(new AlmanacServiceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Not Unique'), + pht('Almanac services must have unique names.'), + last($xactions)); + $errors[] = $error; + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac services '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } } } @@ -161,6 +152,4 @@ final class AlmanacServiceEditor return $errors; } - - } diff --git a/src/applications/almanac/editor/AlmanacServicePropertyEditEngine.php b/src/applications/almanac/editor/AlmanacServicePropertyEditEngine.php new file mode 100644 index 0000000000..a56a10d575 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacServicePropertyEditEngine.php @@ -0,0 +1,16 @@ +getURI(); + } + +} diff --git a/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php new file mode 100644 index 0000000000..a3afaf3251 --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php @@ -0,0 +1,30 @@ +needProperties(true); + $query->needBindings(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + $bindings = array(); + foreach ($object->getBindings() as $binding) { + $bindings[] = $this->getAlmanacBindingDictionary($binding); + } + + return array( + 'bindings' => $bindings, + ); + } + +} diff --git a/src/applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php new file mode 100644 index 0000000000..35c7deae74 --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php @@ -0,0 +1,26 @@ +needProperties(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + $properties = $this->getAlmanacPropertyList($object); + + return array( + 'properties' => $properties, + ); + } + +} diff --git a/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php new file mode 100644 index 0000000000..266b281990 --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php @@ -0,0 +1,65 @@ +getAlmanacPropertyFieldSpecifications(); + + $properties = array(); + foreach ($object->getAlmanacProperties() as $key => $property) { + $is_builtin = isset($builtins[$key]); + + $properties[] = array( + 'key' => $key, + 'value' => $property->getFieldValue(), + 'builtin' => $is_builtin, + ); + } + + return $properties; + } + + protected function getAlmanacBindingDictionary(AlmanacBinding $binding) { + $interface = $binding->getInterface(); + + return array( + 'id' => (int)$binding->getID(), + 'phid' => $binding->getPHID(), + 'properties' => $this->getAlmanacPropertyList($binding), + 'interface' => $this->getAlmanacInterfaceDictionary($interface), + 'disabled' => (bool)$binding->getIsDisabled(), + ); + } + + protected function getAlmanacInterfaceDictionary( + AlmanacInterface $interface) { + + return array( + 'id' => (int)$interface->getID(), + 'phid' => $interface->getPHID(), + 'address' => $interface->getAddress(), + 'port' => (int)$interface->getPort(), + 'device' => $this->getAlmanacDeviceDictionary($interface->getDevice()), + 'network' => $this->getAlmanacNetworkDictionary($interface->getNetwork()), + ); + } + + protected function getAlmanacDeviceDictionary(AlmanacDevice $device) { + return array( + 'id' => (int)$device->getID(), + 'phid' => $device->getPHID(), + 'name' => $device->getName(), + 'properties' => $this->getAlmanacPropertyList($device), + ); + } + + protected function getAlmanacNetworkDictionary(AlmanacNetwork $network) { + return array( + 'id' => (int)$network->getID(), + 'phid' => $network->getPHID(), + 'name' => $network->getName(), + ); + } + +} diff --git a/src/applications/almanac/management/AlmanacManagementLockWorkflow.php b/src/applications/almanac/management/AlmanacManagementLockWorkflow.php deleted file mode 100644 index 60783d1bc5..0000000000 --- a/src/applications/almanac/management/AlmanacManagementLockWorkflow.php +++ /dev/null @@ -1,49 +0,0 @@ -setName('lock') - ->setSynopsis(pht('Lock a service to prevent it from being edited.')) - ->setArguments( - array( - array( - 'name' => 'services', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $services = $this->loadServices($args->getArg('services')); - if (!$services) { - throw new PhutilArgumentUsageException( - pht('Specify at least one service to lock.')); - } - - foreach ($services as $service) { - if ($service->getIsLocked()) { - throw new PhutilArgumentUsageException( - pht( - 'Service "%s" is already locked!', - $service->getName())); - } - } - - foreach ($services as $service) { - $this->updateServiceLock($service, true); - - $console->writeOut( - "** %s ** %s\n", - pht('LOCKED'), - pht('Service "%s" was locked.', $service->getName())); - } - - return 0; - } - -} diff --git a/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php b/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php deleted file mode 100644 index da8ba3dcc7..0000000000 --- a/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php +++ /dev/null @@ -1,49 +0,0 @@ -setName('unlock') - ->setSynopsis(pht('Unlock a service to allow it to be edited.')) - ->setArguments( - array( - array( - 'name' => 'services', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $services = $this->loadServices($args->getArg('services')); - if (!$services) { - throw new PhutilArgumentUsageException( - pht('Specify at least one service to unlock.')); - } - - foreach ($services as $service) { - if (!$service->getIsLocked()) { - throw new PhutilArgumentUsageException( - pht( - 'Service "%s" is not locked!', - $service->getName())); - } - } - - foreach ($services as $service) { - $this->updateServiceLock($service, false); - - $console->writeOut( - "** %s ** %s\n", - pht('UNLOCKED'), - pht('Service "%s" was unlocked.', $service->getName())); - } - - return 0; - } - -} diff --git a/src/applications/almanac/phid/AlmanacNamespacePHIDType.php b/src/applications/almanac/phid/AlmanacNamespacePHIDType.php new file mode 100644 index 0000000000..aa0b8fc4ef --- /dev/null +++ b/src/applications/almanac/phid/AlmanacNamespacePHIDType.php @@ -0,0 +1,44 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $namespace = $objects[$phid]; + + $id = $namespace->getID(); + $name = $namespace->getName(); + + $handle->setObjectName(pht('Namespace %d', $id)); + $handle->setName($name); + $handle->setURI($namespace->getURI()); + } + } + +} diff --git a/src/applications/almanac/property/AlmanacPropertyInterface.php b/src/applications/almanac/property/AlmanacPropertyInterface.php index 9a2bf11455..b50b4089a8 100644 --- a/src/applications/almanac/property/AlmanacPropertyInterface.php +++ b/src/applications/almanac/property/AlmanacPropertyInterface.php @@ -8,5 +8,6 @@ interface AlmanacPropertyInterface { public function getAlmanacProperty($key); public function getAlmanacPropertyValue($key, $default = null); public function getAlmanacPropertyFieldSpecifications(); + public function newAlmanacPropertyEditEngine(); } diff --git a/src/applications/almanac/query/AlmanacBindingQuery.php b/src/applications/almanac/query/AlmanacBindingQuery.php index 0b6d7cda3a..5d51d2ba35 100644 --- a/src/applications/almanac/query/AlmanacBindingQuery.php +++ b/src/applications/almanac/query/AlmanacBindingQuery.php @@ -34,19 +34,12 @@ final class AlmanacBindingQuery return $this; } + public function newResultObject() { + return new AlmanacBinding(); + } + protected function loadPage() { - $table = new AlmanacBinding(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $bindings) { @@ -58,6 +51,7 @@ final class AlmanacBindingQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($service_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $services = mpull($services, null, 'getPHID'); @@ -65,6 +59,7 @@ final class AlmanacBindingQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($device_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $devices = mpull($devices, null, 'getPHID'); @@ -72,6 +67,7 @@ final class AlmanacBindingQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($interface_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $interfaces = mpull($interfaces, null, 'getPHID'); @@ -93,47 +89,45 @@ final class AlmanacBindingQuery return $bindings; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->servicePHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'servicePHID IN (%Ls)', $this->servicePHIDs); } if ($this->devicePHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'devicePHID IN (%Ls)', $this->devicePHIDs); } if ($this->interfacePHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'interfacePHID IN (%Ls)', $this->interfacePHIDs); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } } diff --git a/src/applications/almanac/query/AlmanacDeviceQuery.php b/src/applications/almanac/query/AlmanacDeviceQuery.php index 287af6c647..29461bfbfd 100644 --- a/src/applications/almanac/query/AlmanacDeviceQuery.php +++ b/src/applications/almanac/query/AlmanacDeviceQuery.php @@ -34,35 +34,34 @@ final class AlmanacDeviceQuery return $this; } - protected function loadPage() { - $table = new AlmanacDevice(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacDeviceNameNgrams(), + $ngrams); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + public function newResultObject() { + return new AlmanacDevice(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, - 'id IN (%Ld)', + $conn, + 'device.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, - 'phid IN (%Ls)', + $conn, + 'device.phid IN (%Ls)', $this->phids); } @@ -72,28 +71,59 @@ final class AlmanacDeviceQuery $hashes[] = PhabricatorHash::digestForIndex($name); } $where[] = qsprintf( - $conn_r, - 'nameIndex IN (%Ls)', + $conn, + 'device.nameIndex IN (%Ls)', $hashes); } if ($this->namePrefix !== null) { $where[] = qsprintf( - $conn_r, - 'name LIKE %>', + $conn, + 'device.name LIKE %>', $this->namePrefix); } if ($this->nameSuffix !== null) { $where[] = qsprintf( - $conn_r, - 'name LIKE %<', + $conn, + 'device.name LIKE %<', $this->nameSuffix); } - $where[] = $this->buildPagingClause($conn_r); + return $where; + } - return $this->formatWhereClause($where); + protected function getPrimaryTableAlias() { + return 'device'; + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'name' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'name', + 'type' => 'string', + 'unique' => true, + 'reverse' => true, + ), + ); + } + + protected function getPagingValueMap($cursor, array $keys) { + $device = $this->loadCursorObject($cursor); + return array( + 'id' => $device->getID(), + 'name' => $device->getName(), + ); + } + + public function getBuiltinOrders() { + return array( + 'name' => array( + 'vector' => array('name'), + 'name' => pht('Device Name'), + ), + ) + parent::getBuiltinOrders(); } public function getQueryApplicationClass() { diff --git a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php index 99e960425c..6e28d7e5b8 100644 --- a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php @@ -11,22 +11,37 @@ final class AlmanacDeviceSearchEngine return 'PhabricatorAlmanacApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - return $saved; + public function newQuery() { + return new AlmanacDeviceQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new AlmanacDeviceQuery()); + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for devices by name substring.')), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Exact Names')) + ->setKey('names') + ->setDescription(pht('Search for devices with specific names.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } + + if ($map['names']) { + $query->withNames($map['names']); + } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) {} - protected function getURI($path) { return '/almanac/device/'.$path; } @@ -52,12 +67,6 @@ final class AlmanacDeviceSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $devices, - PhabricatorSavedQuery $query) { - return array(); - } - protected function renderResultList( array $devices, PhabricatorSavedQuery $query, @@ -75,6 +84,10 @@ final class AlmanacDeviceSearchEngine ->setHref($device->getURI()) ->setObject($device); + if ($device->isClusterDevice()) { + $item->addIcon('fa-sitemap', pht('Cluster Device')); + } + $list->addItem($item); } diff --git a/src/applications/almanac/query/AlmanacInterfaceQuery.php b/src/applications/almanac/query/AlmanacInterfaceQuery.php index e119203da5..bb6fc2f9d9 100644 --- a/src/applications/almanac/query/AlmanacInterfaceQuery.php +++ b/src/applications/almanac/query/AlmanacInterfaceQuery.php @@ -1,7 +1,7 @@ establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $interfaces) { @@ -57,6 +50,7 @@ final class AlmanacInterfaceQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($network_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $networks = mpull($networks, null, 'getPHID'); @@ -64,6 +58,7 @@ final class AlmanacInterfaceQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($device_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $devices = mpull($devices, null, 'getPHID'); @@ -83,34 +78,34 @@ final class AlmanacInterfaceQuery return $interfaces; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, - 'id IN (%Ld)', + $conn, + 'interface.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, - 'phid IN (%Ls)', + $conn, + 'interface.phid IN (%Ls)', $this->phids); } if ($this->networkPHIDs !== null) { $where[] = qsprintf( - $conn_r, - 'networkPHID IN (%Ls)', + $conn, + 'interface.networkPHID IN (%Ls)', $this->networkPHIDs); } if ($this->devicePHIDs !== null) { $where[] = qsprintf( - $conn_r, - 'devicePHID IN (%Ls)', + $conn, + 'interface.devicePHID IN (%Ls)', $this->devicePHIDs); } @@ -118,8 +113,10 @@ final class AlmanacInterfaceQuery $parts = array(); foreach ($this->addresses as $address) { $parts[] = qsprintf( - $conn_r, - '(networkPHID = %s AND address = %s AND port = %d)', + $conn, + '(interface.networkPHID = %s '. + 'AND interface.address = %s '. + 'AND interface.port = %d)', $address->getNetworkPHID(), $address->getAddress(), $address->getPort()); @@ -127,13 +124,77 @@ final class AlmanacInterfaceQuery $where[] = implode(' OR ', $parts); } - $where[] = $this->buildPagingClause($conn_r); + return $where; + } - return $this->formatWhereClause($where); + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->shouldJoinDeviceTable()) { + $joins[] = qsprintf( + $conn, + 'JOIN %T device ON device.phid = interface.devicePHID', + id(new AlmanacDevice())->getTableName()); + } + + return $joins; + } + + protected function shouldGroupQueryResultRows() { + if ($this->shouldJoinDeviceTable()) { + return true; + } + + return parent::shouldGroupQueryResultRows(); + } + + private function shouldJoinDeviceTable() { + $vector = $this->getOrderVector(); + + if ($vector->containsKey('name')) { + return true; + } + + return false; + } + + protected function getPrimaryTableAlias() { + return 'interface'; } public function getQueryApplicationClass() { return 'PhabricatorAlmanacApplication'; } + public function getBuiltinOrders() { + return array( + 'name' => array( + 'vector' => array('name', 'id'), + 'name' => pht('Device Name'), + ), + ) + parent::getBuiltinOrders(); + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'name' => array( + 'table' => 'device', + 'column' => 'name', + 'type' => 'string', + 'reverse' => true, + ), + ); + } + + protected function getPagingValueMap($cursor, array $keys) { + $interface = $this->loadCursorObject($cursor); + + $map = array( + 'id' => $interface->getID(), + 'name' => $interface->getDevice()->getName(), + ); + + return $map; + } + } diff --git a/src/applications/almanac/query/AlmanacNamespaceQuery.php b/src/applications/almanac/query/AlmanacNamespaceQuery.php new file mode 100644 index 0000000000..81332cf03b --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceQuery.php @@ -0,0 +1,103 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacNamespaceNameNgrams(), + $ngrams); + } + + public function newResultObject() { + return new AlmanacNamespace(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'namespace.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'namespace.phid IN (%Ls)', + $this->phids); + } + + if ($this->names !== null) { + $where[] = qsprintf( + $conn, + 'namespace.name IN (%Ls)', + $this->names); + } + + return $where; + } + + protected function getPrimaryTableAlias() { + return 'namespace'; + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'name' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'name', + 'type' => 'string', + 'unique' => true, + 'reverse' => true, + ), + ); + } + + protected function getPagingValueMap($cursor, array $keys) { + $namespace = $this->loadCursorObject($cursor); + return array( + 'id' => $namespace->getID(), + 'name' => $namespace->getName(), + ); + } + + public function getBuiltinOrders() { + return array( + 'name' => array( + 'vector' => array('name'), + 'name' => pht('Namespace Name'), + ), + ) + parent::getBuiltinOrders(); + } + + public function getQueryApplicationClass() { + return 'PhabricatorAlmanacApplication'; + } + +} diff --git a/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php b/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php new file mode 100644 index 0000000000..14a96d22a0 --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php @@ -0,0 +1,90 @@ +setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for namespaces by name substring.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } + + return $query; + } + + protected function getURI($path) { + return '/almanac/namespace/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Namespaces'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $namespaces, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($namespaces, 'AlmanacNamespace'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($namespaces as $namespace) { + $id = $namespace->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Namespace %d', $id)) + ->setHeader($namespace->getName()) + ->setHref($this->getApplicationURI("namespace/{$id}/")) + ->setObject($namespace); + + $list->addItem($item); + } + + $result = new PhabricatorApplicationSearchResultView(); + $result->setObjectList($list); + $result->setNoDataString(pht('No Almanac namespaces found.')); + + return $result; + } +} diff --git a/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php b/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php new file mode 100644 index 0000000000..64ce639f45 --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php @@ -0,0 +1,10 @@ +establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + public function newResultObject() { + return new AlmanacNetwork(); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacNetworkNameNgrams(), + $ngrams); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, - 'id IN (%Ld)', + $conn, + 'network.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, - 'phid IN (%Ls)', + $conn, + 'network.phid IN (%Ls)', $this->phids); } - $where[] = $this->buildPagingClause($conn_r); + return $where; + } - return $this->formatWhereClause($where); + protected function getPrimaryTableAlias() { + return 'network'; } public function getQueryApplicationClass() { diff --git a/src/applications/almanac/query/AlmanacNetworkSearchEngine.php b/src/applications/almanac/query/AlmanacNetworkSearchEngine.php index 410e984a3f..53f9b24e44 100644 --- a/src/applications/almanac/query/AlmanacNetworkSearchEngine.php +++ b/src/applications/almanac/query/AlmanacNetworkSearchEngine.php @@ -11,22 +11,29 @@ final class AlmanacNetworkSearchEngine return 'PhabricatorAlmanacApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - return $saved; + public function newQuery() { + return new AlmanacNetworkQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new AlmanacNetworkQuery()); + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for networks by name substring.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) {} - protected function getURI($path) { return '/almanac/network/'.$path; } @@ -52,12 +59,6 @@ final class AlmanacNetworkSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $networks, - PhabricatorSavedQuery $query) { - return array(); - } - protected function renderResultList( array $networks, PhabricatorSavedQuery $query, diff --git a/src/applications/almanac/query/AlmanacPropertyQuery.php b/src/applications/almanac/query/AlmanacPropertyQuery.php index 886c26a7be..17f1e87c07 100644 --- a/src/applications/almanac/query/AlmanacPropertyQuery.php +++ b/src/applications/almanac/query/AlmanacPropertyQuery.php @@ -5,8 +5,8 @@ final class AlmanacPropertyQuery private $ids; private $objectPHIDs; + private $objects; private $names; - private $disablePolicyFilteringAndAttachment; public function withIDs(array $ids) { $this->ids = $ids; @@ -18,72 +18,72 @@ final class AlmanacPropertyQuery return $this; } + public function withObjects(array $objects) { + $this->objects = mpull($objects, null, 'getPHID'); + $this->objectPHIDs = array_keys($this->objects); + return $this; + } + public function withNames(array $names) { $this->names = $names; return $this; } - public function setDisablePolicyFilteringAndAttachment($disable) { - $this->disablePolicyFilteringAndAttachment = $disable; - return $this; - } - - protected function shouldDisablePolicyFiltering() { - return $this->disablePolicyFilteringAndAttachment; + public function newResultObject() { + return new AlmanacProperty(); } protected function loadPage() { - $table = new AlmanacProperty(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $properties) { - if (!$this->disablePolicyFilteringAndAttachment) { - $object_phids = mpull($properties, 'getObjectPHID'); + $object_phids = mpull($properties, 'getObjectPHID'); + $object_phids = array_fuse($object_phids); + + if ($this->objects !== null) { + $object_phids = array_diff_key($object_phids, $this->objects); + } + + if ($object_phids) { $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); + } else { + $objects = array(); + } - foreach ($properties as $key => $property) { - $object = idx($objects, $property->getObjectPHID()); - if (!$object) { - unset($properties[$key]); - continue; - } - $property->attachObject($object); + $objects += $this->objects; + + foreach ($properties as $key => $property) { + $object = idx($objects, $property->getObjectPHID()); + if (!$object) { + unset($properties[$key]); + continue; } + $property->attachObject($object); } return $properties; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } @@ -94,14 +94,12 @@ final class AlmanacPropertyQuery $hashes[] = PhabricatorHash::digestForIndex($name); } $where[] = qsprintf( - $conn_r, + $conn, 'fieldIndex IN (%Ls)', $hashes); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/almanac/query/AlmanacQuery.php b/src/applications/almanac/query/AlmanacQuery.php index 54274dfe72..b046171d32 100644 --- a/src/applications/almanac/query/AlmanacQuery.php +++ b/src/applications/almanac/query/AlmanacQuery.php @@ -3,31 +3,48 @@ abstract class AlmanacQuery extends PhabricatorCursorPagedPolicyAwareQuery { - protected function didFilterPage(array $objects) { - if (head($objects) instanceof AlmanacPropertyInterface) { - // NOTE: We load properties unconditionally because CustomField assumes - // it can always generate a list of fields on an object. It may make - // sense to re-examine that assumption eventually. + private $needProperties; + public function needProperties($need_properties) { + $this->needProperties = $need_properties; + return $this; + } + + protected function getNeedProperties() { + return $this->needProperties; + } + + protected function didFilterPage(array $objects) { + $has_properties = (head($objects) instanceof AlmanacPropertyInterface); + + if ($has_properties && $this->needProperties) { $property_query = id(new AlmanacPropertyQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) - ->withObjectPHIDs(mpull($objects, 'getPHID')); - - // NOTE: We disable policy filtering and object attachment to avoid - // a cyclic dependency where objects need their properties and properties - // need their objects. We'll attach the objects below, and have already - // implicitly checked the necessary policies. - $property_query->setDisablePolicyFilteringAndAttachment(true); + ->withObjects($objects); $properties = $property_query->execute(); $properties = mgroup($properties, 'getObjectPHID'); foreach ($objects as $object) { $object_properties = idx($properties, $object->getPHID(), array()); + $object_properties = mpull($object_properties, null, 'getFieldName'); + + // Create synthetic properties for defaults on the object itself. + $specs = $object->getAlmanacPropertyFieldSpecifications(); + foreach ($specs as $key => $spec) { + if (empty($object_properties[$key])) { + $object_properties[$key] = id(new AlmanacProperty()) + ->setObjectPHID($object->getPHID()) + ->setFieldName($key) + ->setFieldValue($spec->getValueForTransaction()); + } + } + foreach ($object_properties as $property) { $property->attachObject($object); } + $object->attachAlmanacProperties($object_properties); } } diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php index 3701e850e9..3374413e5b 100644 --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -6,9 +6,8 @@ final class AlmanacServiceQuery private $ids; private $phids; private $names; - private $serviceClasses; + private $serviceTypes; private $devicePHIDs; - private $locked; private $namePrefix; private $nameSuffix; @@ -29,8 +28,8 @@ final class AlmanacServiceQuery return $this; } - public function withServiceClasses(array $classes) { - $this->serviceClasses = $classes; + public function withServiceTypes(array $types) { + $this->serviceTypes = $types; return $this; } @@ -39,11 +38,6 @@ final class AlmanacServiceQuery return $this; } - public function withLocked($locked) { - $this->locked = $locked; - return $this; - } - public function withNamePrefix($prefix) { $this->namePrefix = $prefix; return $this; @@ -54,19 +48,29 @@ final class AlmanacServiceQuery return $this; } + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacServiceNameNgrams(), + $ngrams); + } + public function needBindings($need_bindings) { $this->needBindings = $need_bindings; return $this; } + public function newResultObject() { + return new AlmanacService(); + } + protected function loadPage() { - return $this->loadStandardPage(new AlmanacService()); + return $this->loadStandardPage($this->newResultObject()); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); - if ($this->devicePHIDs !== null) { + if ($this->shouldJoinBindingTable()) { $joins[] = qsprintf( $conn, 'JOIN %T binding ON service.phid = binding.servicePHID', @@ -105,11 +109,11 @@ final class AlmanacServiceQuery $hashes); } - if ($this->serviceClasses !== null) { + if ($this->serviceTypes !== null) { $where[] = qsprintf( $conn, - 'service.serviceClass IN (%Ls)', - $this->serviceClasses); + 'service.serviceType IN (%Ls)', + $this->serviceTypes); } if ($this->devicePHIDs !== null) { @@ -119,13 +123,6 @@ final class AlmanacServiceQuery $this->devicePHIDs); } - if ($this->locked !== null) { - $where[] = qsprintf( - $conn, - 'service.isLocked = %d', - (int)$this->locked); - } - if ($this->namePrefix !== null) { $where[] = qsprintf( $conn, @@ -144,17 +141,19 @@ final class AlmanacServiceQuery } protected function willFilterPage(array $services) { - $service_types = AlmanacServiceType::getAllServiceTypes(); + $service_map = AlmanacServiceType::getAllServiceTypes(); foreach ($services as $key => $service) { - $service_class = $service->getServiceClass(); - $service_type = idx($service_types, $service_class); - if (!$service_type) { + $implementation = idx($service_map, $service->getServiceType()); + + if (!$implementation) { $this->didRejectResult($service); unset($services[$key]); continue; } - $service->attachServiceType($service_type); + + $implementation = clone $implementation; + $service->attachServiceImplementation($implementation); } return $services; @@ -166,6 +165,7 @@ final class AlmanacServiceQuery $bindings = id(new AlmanacBindingQuery()) ->setViewer($this->getViewer()) ->withServicePHIDs($service_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $bindings = mgroup($bindings, 'getServicePHID'); @@ -178,6 +178,18 @@ final class AlmanacServiceQuery return parent::didFilterPage($services); } + private function shouldJoinBindingTable() { + return ($this->devicePHIDs !== null); + } + + protected function shouldGroupQueryResultRows() { + if ($this->shouldJoinBindingTable()) { + return true; + } + + return parent::shouldGroupQueryResultRows(); + } + protected function getPrimaryTableAlias() { return 'service'; } diff --git a/src/applications/almanac/query/AlmanacServiceSearchEngine.php b/src/applications/almanac/query/AlmanacServiceSearchEngine.php index 51d97729fe..1a9509a2c2 100644 --- a/src/applications/almanac/query/AlmanacServiceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacServiceSearchEngine.php @@ -16,21 +16,53 @@ final class AlmanacServiceSearchEngine } public function newResultObject() { - // NOTE: We need to attach a service type in order to generate custom - // field definitions. - return AlmanacService::initializeNewService() - ->attachServiceType(new AlmanacCustomServiceType()); + return new AlmanacService(); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } + + if ($map['names']) { + $query->withNames($map['names']); + } + + if ($map['devicePHIDs']) { + $query->withDevicePHIDs($map['devicePHIDs']); + } + + if ($map['serviceTypes']) { + $query->withServiceTypes($map['serviceTypes']); + } + return $query; } protected function buildCustomSearchFields() { - return array(); + return array( + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for services by name substring.')), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Exact Names')) + ->setKey('names') + ->setDescription(pht('Search for services with specific names.')), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Service Types')) + ->setKey('serviceTypes') + ->setDescription(pht('Find services by type.')) + ->setDatasource(id(new AlmanacServiceTypeDatasource())), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Devices')) + ->setKey('devicePHIDs') + ->setDescription( + pht('Search for services bound to particular devices.')), + ); } protected function getURI($path) { @@ -75,17 +107,8 @@ final class AlmanacServiceSearchEngine ->setHref($service->getURI()) ->setObject($service) ->addIcon( - $service->getServiceType()->getServiceTypeIcon(), - $service->getServiceType()->getServiceTypeShortName()); - - if ($service->getIsLocked() || - $service->getServiceType()->isClusterServiceType()) { - if ($service->getIsLocked()) { - $item->addIcon('fa-lock', pht('Locked')); - } else { - $item->addIcon('fa-unlock-alt red', pht('Unlocked')); - } - } + $service->getServiceImplementation()->getServiceTypeIcon(), + $service->getServiceImplementation()->getServiceTypeShortName()); $list->addItem($item); } diff --git a/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php index 90e23dc8e0..ba06a78d52 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php @@ -3,6 +3,8 @@ final class AlmanacClusterDatabaseServiceType extends AlmanacClusterServiceType { + const SERVICETYPE = 'cluster.database'; + public function getServiceTypeShortName() { return pht('Cluster Database'); } @@ -16,8 +18,4 @@ final class AlmanacClusterDatabaseServiceType 'Defines a database service for use in a Phabricator cluster.'); } - public function getFieldSpecifications() { - return array(); - } - } diff --git a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php index e4dc3c8afc..259092b5b1 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php @@ -3,6 +3,8 @@ final class AlmanacClusterRepositoryServiceType extends AlmanacClusterServiceType { + const SERVICETYPE = 'cluster.repository'; + public function getServiceTypeShortName() { return pht('Cluster Repository'); } @@ -18,16 +20,7 @@ final class AlmanacClusterRepositoryServiceType public function getFieldSpecifications() { return array( - 'closed' => array( - 'type' => 'bool', - 'name' => pht('Closed'), - 'default' => false, - 'strings' => array( - 'edit.checkbox' => pht( - 'Prevent new repositories from being allocated on this '. - 'service.'), - ), - ), + 'closed' => id(new PhabricatorTextEditField()), ); } diff --git a/src/applications/almanac/servicetype/AlmanacClusterServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterServiceType.php index 7edbe52a73..2873141185 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterServiceType.php @@ -11,28 +11,4 @@ abstract class AlmanacClusterServiceType return 'fa-sitemap'; } - public function getStatusMessages(AlmanacService $service) { - $messages = parent::getStatusMessages($service); - - if (!$service->getIsLocked()) { - $doc_href = PhabricatorEnv::getDoclink( - 'User Guide: Phabricator Clusters'); - - $doc_link = phutil_tag( - 'a', - array( - 'href' => $doc_href, - 'target' => '_blank', - ), - pht('Learn More')); - - $messages[] = pht( - 'This is an unlocked cluster service. After you finish editing '. - 'it, you should lock it. %s.', - $doc_link); - } - - return $messages; - } - } diff --git a/src/applications/almanac/servicetype/AlmanacCustomServiceType.php b/src/applications/almanac/servicetype/AlmanacCustomServiceType.php index 3a4affbb8e..f4df57a868 100644 --- a/src/applications/almanac/servicetype/AlmanacCustomServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacCustomServiceType.php @@ -2,6 +2,8 @@ final class AlmanacCustomServiceType extends AlmanacServiceType { + const SERVICETYPE = 'almanac.custom'; + public function getServiceTypeShortName() { return pht('Custom'); } diff --git a/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php b/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php index 24880565d0..4c7de3a361 100644 --- a/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php @@ -2,6 +2,8 @@ final class AlmanacDrydockPoolServiceType extends AlmanacServiceType { + const SERVICETYPE = 'drydock.pool'; + public function getServiceTypeShortName() { return pht('Drydock Pool'); } diff --git a/src/applications/almanac/servicetype/AlmanacServiceType.php b/src/applications/almanac/servicetype/AlmanacServiceType.php index 408224f1a9..2eb56dc8c4 100644 --- a/src/applications/almanac/servicetype/AlmanacServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacServiceType.php @@ -30,6 +30,11 @@ abstract class AlmanacServiceType extends Phobject { abstract public function getServiceTypeDescription(); + final public function getServiceTypeConstant() { + return $this->getPhobjectClassConstant('SERVICETYPE', 64); + } + + public function getServiceTypeIcon() { return 'fa-cog'; } @@ -38,7 +43,7 @@ abstract class AlmanacServiceType extends Phobject { * Return `true` if this service type is a Phabricator cluster service type. * * These special services change the behavior of Phabricator, and require - * elevated permission to create. + * elevated permission to create and edit. * * @return bool True if this is a Phabricator cluster service type. */ @@ -55,10 +60,6 @@ abstract class AlmanacServiceType extends Phobject { return array(); } - public function getStatusMessages(AlmanacService $service) { - return array(); - } - /** * List all available service type implementations. * @@ -67,6 +68,7 @@ abstract class AlmanacServiceType extends Phobject { public static function getAllServiceTypes() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getServiceTypeConstant') ->setSortMethod('getServiceTypeName') ->execute(); } diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 8f56c86c1a..9d0fbc1622 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -4,26 +4,27 @@ final class AlmanacBinding extends AlmanacDAO implements PhabricatorPolicyInterface, - PhabricatorCustomFieldInterface, PhabricatorApplicationTransactionInterface, AlmanacPropertyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorExtendedPolicyInterface { protected $servicePHID; protected $devicePHID; protected $interfacePHID; protected $mailKey; + protected $isDisabled; private $service = self::ATTACHABLE; private $device = self::ATTACHABLE; private $interface = self::ATTACHABLE; - private $customFields = self::ATTACHABLE; private $almanacProperties = self::ATTACHABLE; public static function initializeNewBinding(AlmanacService $service) { return id(new AlmanacBinding()) ->setServicePHID($service->getPHID()) - ->attachAlmanacProperties(array()); + ->attachAlmanacProperties(array()) + ->setIsDisabled(0); } protected function getConfiguration() { @@ -31,6 +32,7 @@ final class AlmanacBinding self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'mailKey' => 'bytes20', + 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_service' => array( @@ -58,6 +60,10 @@ final class AlmanacBinding return parent::save(); } + public function getName() { + return pht('Binding %s', $this->getID()); + } + public function getURI() { return '/almanac/binding/'.$this->getID().'/'; } @@ -124,6 +130,10 @@ final class AlmanacBinding return array(); } + public function newAlmanacPropertyEditEngine() { + return new AlmanacBindingPropertyEditEngine(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -151,38 +161,30 @@ final class AlmanacBinding 'interface.'), ); - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { - if ($this->getService()->getIsLocked()) { - $notes[] = pht( - 'The service for this binding is locked, so it can not be edited.'); - } - } - return $notes; } -/* -( PhabricatorCustomFieldInterface )------------------------------------ */ +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ - public function getCustomFieldSpecificationForRole($role) { + 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(); } - public function getCustomFieldBaseClass() { - return 'AlmanacCustomField'; - } - - public function getCustomFields() { - return $this->assertAttached($this->customFields); - } - - public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { - $this->customFields = $fields; - return $this; - } - - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacBindingTransaction.php b/src/applications/almanac/storage/AlmanacBindingTransaction.php index 4ea4909bb6..11120f9827 100644 --- a/src/applications/almanac/storage/AlmanacBindingTransaction.php +++ b/src/applications/almanac/storage/AlmanacBindingTransaction.php @@ -1,9 +1,10 @@ renderHandleLink($new)); } break; + case self::TYPE_DISABLE: + if ($new) { + return pht( + '%s disabled this binding.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s enabled this binding.', + $this->renderHandleLink($author_phid)); + } + break; } return parent::getTitle(); diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index d0994174b3..f46b03600f 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -4,21 +4,22 @@ final class AlmanacDevice extends AlmanacDAO implements PhabricatorPolicyInterface, - PhabricatorCustomFieldInterface, PhabricatorApplicationTransactionInterface, PhabricatorProjectInterface, PhabricatorSSHPublicKeyInterface, AlmanacPropertyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorNgramsInterface, + PhabricatorConduitResultInterface, + PhabricatorExtendedPolicyInterface { protected $name; protected $nameIndex; protected $mailKey; protected $viewPolicy; protected $editPolicy; - protected $isLocked; + protected $isBoundToClusterService; - private $customFields = self::ATTACHABLE; private $almanacProperties = self::ATTACHABLE; public static function initializeNewDevice() { @@ -26,7 +27,7 @@ final class AlmanacDevice ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) - ->setIsLocked(0); + ->setIsBoundToClusterService(0); } protected function getConfiguration() { @@ -36,7 +37,7 @@ final class AlmanacDevice 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', - 'isLocked' => 'bool', + 'isBoundToClusterService' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( @@ -55,7 +56,7 @@ final class AlmanacDevice } public function save() { - AlmanacNames::validateServiceOrDeviceName($this->getName()); + AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); @@ -70,30 +71,28 @@ final class AlmanacDevice return '/almanac/device/view/'.$this->getName().'/'; } - - /** - * Find locked services which are bound to this device, updating the device - * lock flag if necessary. - * - * @return list List of locking service PHIDs. - */ - public function rebuildDeviceLocks() { + public function rebuildClusterBindingStatus() { $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDevicePHIDs(array($this->getPHID())) - ->withLocked(true) ->execute(); - $locked = (bool)count($services); + $is_cluster = false; + foreach ($services as $service) { + if ($service->isClusterService()) { + $is_cluster = true; + break; + } + } - if ($locked != $this->getIsLocked()) { - $this->setIsLocked((int)$locked); + if ($is_cluster != $this->getIsBoundToClusterService()) { + $this->setIsBoundToClusterService((int)$is_cluster); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), - 'UPDATE %T SET isLocked = %d WHERE id = %d', + 'UPDATE %T SET isBoundToClusterService = %d WHERE id = %d', $this->getTableName(), - $this->getIsLocked(), + $this->getIsBoundToClusterService(), $this->getID()); unset($unguarded); } @@ -101,6 +100,10 @@ final class AlmanacDevice return $this; } + public function isClusterDevice() { + return $this->getIsBoundToClusterService(); + } + /* -( AlmanacPropertyInterface )------------------------------------------- */ @@ -136,6 +139,10 @@ final class AlmanacDevice return array(); } + public function newAlmanacPropertyEditEngine() { + return new AlmanacDevicePropertyEditEngine(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -152,11 +159,7 @@ final class AlmanacDevice case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: - if ($this->getIsLocked()) { - return PhabricatorPolicies::POLICY_NOONE; - } else { - return $this->getEditPolicy(); - } + return $this->getEditPolicy(); } } @@ -165,38 +168,30 @@ final class AlmanacDevice } public function describeAutomaticCapability($capability) { - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { - if ($this->getIsLocked()) { - return pht( - 'This device is bound to a locked service, so it can not '. - 'be edited.'); - } - } - return null; } -/* -( PhabricatorCustomFieldInterface )------------------------------------ */ +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ - public function getCustomFieldSpecificationForRole($role) { + 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(); } - public function getCustomFieldBaseClass() { - return 'AlmanacCustomField'; - } - - public function getCustomFields() { - return $this->assertAttached($this->customFields); - } - - public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { - $this->customFields = $fields; - return $this; - } - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -250,4 +245,41 @@ final class AlmanacDevice $this->delete(); } + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + 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/AlmanacDeviceNameNgrams.php b/src/applications/almanac/storage/AlmanacDeviceNameNgrams.php new file mode 100644 index 0000000000..a0ca1e2810 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacDeviceNameNgrams.php @@ -0,0 +1,18 @@ +renderHandleLink($author_phid), - $this->describeInterface($new)); + $this->describeInterface($old)); } else if ($new) { return pht( '%s added the interface %s to this device.', diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index 729e95aa9a..d55c88b791 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -4,7 +4,8 @@ final class AlmanacInterface extends AlmanacDAO implements PhabricatorPolicyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorExtendedPolicyInterface { protected $devicePHID; protected $networkPHID; @@ -74,6 +75,16 @@ final class AlmanacInterface 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 )----------------------------------------- */ @@ -101,14 +112,28 @@ final class AlmanacInterface 'view the interface.'), ); - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { - if ($this->getDevice()->getIsLocked()) { - $notes[] = pht( - 'The device for this interface is locked, so it can not be edited.'); - } + return $notes; + } + + +/* -( 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 $notes; + return array(); } diff --git a/src/applications/almanac/storage/AlmanacNamespace.php b/src/applications/almanac/storage/AlmanacNamespace.php new file mode 100644 index 0000000000..6a3baca637 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacNamespace.php @@ -0,0 +1,223 @@ +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(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new AlmanacNamespaceEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new AlmanacNamespaceTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + return $timeline; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new AlmanacNamespaceNameNgrams()) + ->setValue($this->getName()), + ); + } + +} diff --git a/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php b/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php new file mode 100644 index 0000000000..c7907b931e --- /dev/null +++ b/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php @@ -0,0 +1,18 @@ +getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_CREATE: + return pht( + '%s created this namespace.', + $this->renderHandleLink($author_phid)); + break; + case self::TYPE_NAME: + return pht( + '%s renamed this namespace from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + + return parent::getTitle(); + } + +} diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index a623248fd9..064b612999 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -5,7 +5,8 @@ final class AlmanacNetwork implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorNgramsInterface { protected $name; protected $mailKey; @@ -114,4 +115,15 @@ final class AlmanacNetwork $this->delete(); } + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new AlmanacNetworkNameNgrams()) + ->setValue($this->getName()), + ); + } + } diff --git a/src/applications/almanac/storage/AlmanacNetworkNameNgrams.php b/src/applications/almanac/storage/AlmanacNetworkNameNgrams.php new file mode 100644 index 0000000000..639bc869a7 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacNetworkNameNgrams.php @@ -0,0 +1,18 @@ +getNewValue(); switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_CREATE: + return pht( + '%s created this network.', + $this->renderHandleLink($author_phid)); case self::TYPE_NAME: - if ($old === null) { - return pht( - '%s created this network.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s renamed this network from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - break; + return pht( + '%s renamed this network from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); } return parent::getTitle(); diff --git a/src/applications/almanac/storage/AlmanacProperty.php b/src/applications/almanac/storage/AlmanacProperty.php index 9692143d10..0d317f8c8e 100644 --- a/src/applications/almanac/storage/AlmanacProperty.php +++ b/src/applications/almanac/storage/AlmanacProperty.php @@ -1,25 +1,33 @@ 'text128', - ); - - return $config; + return array( + self::CONFIG_TIMESTAMPS => false, + self::CONFIG_SERIALIZATION => array( + 'fieldValue' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'fieldIndex' => 'bytes12', + 'fieldName' => 'text128', + ), + self::CONFIG_KEY_SCHEMA => array( + 'objectPHID' => array( + 'columns' => array('objectPHID', 'fieldIndex'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); } public function getObject() { @@ -31,39 +39,53 @@ final class AlmanacProperty return $this; } - public static function buildTransactions( + public static function newPropertyUpdateTransactions( + AlmanacPropertyInterface $object, + array $properties, + $only_builtins = false) { + + $template = $object->getApplicationTransactionTemplate(); + $builtins = $object->getAlmanacPropertyFieldSpecifications(); + + $xactions = array(); + foreach ($properties as $name => $property) { + if ($only_builtins && empty($builtins[$name])) { + continue; + } + + $xactions[] = id(clone $template) + ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_UPDATE) + ->setMetadataValue('almanac.property', $name) + ->setNewValue($property); + } + + return $xactions; + } + + public static function newPropertyRemoveTransactions( AlmanacPropertyInterface $object, array $properties) { $template = $object->getApplicationTransactionTemplate(); - $attached_properties = $object->getAlmanacProperties(); - foreach ($properties as $key => $value) { - if (empty($attached_properties[$key])) { - $attached_properties[] = id(new AlmanacProperty()) - ->setObjectPHID($object->getPHID()) - ->setFieldName($key); - } - } - $object->attachAlmanacProperties($attached_properties); - - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_DEFAULT); - $fields = $field_list->getFields(); - $xactions = array(); - foreach ($properties as $name => $property) { + foreach ($properties as $property) { $xactions[] = id(clone $template) - ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) - ->setMetadataValue('customfield:key', $name) - ->setOldValue($object->getAlmanacPropertyValue($name)) - ->setNewValue($property); + ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_REMOVE) + ->setMetadataValue('almanac.property', $property) + ->setNewValue(null); } return $xactions; } + public function save() { + $hash = PhabricatorHash::digestForIndex($this->getFieldName()); + $this->setFieldIndex($hash); + + return parent::save(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index b351f0d3fc..b280de9ba8 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -4,31 +4,42 @@ final class AlmanacService extends AlmanacDAO implements PhabricatorPolicyInterface, - PhabricatorCustomFieldInterface, PhabricatorApplicationTransactionInterface, PhabricatorProjectInterface, AlmanacPropertyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorNgramsInterface, + PhabricatorConduitResultInterface, + PhabricatorExtendedPolicyInterface { protected $name; protected $nameIndex; protected $mailKey; protected $viewPolicy; protected $editPolicy; - protected $serviceClass; - protected $isLocked; + protected $serviceType; - private $customFields = self::ATTACHABLE; private $almanacProperties = self::ATTACHABLE; private $bindings = self::ATTACHABLE; - private $serviceType = self::ATTACHABLE; + private $serviceImplementation = self::ATTACHABLE; + + public static function initializeNewService($type) { + $type_map = AlmanacServiceType::getAllServiceTypes(); + + $implementation = idx($type_map, $type); + if (!$implementation) { + throw new Exception( + pht( + 'No Almanac service type "%s" exists!', + $type)); + } - public static function initializeNewService() { return id(new AlmanacService()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) - ->setIsLocked(0); + ->setServiceType($type) + ->attachServiceImplementation($implementation); } protected function getConfiguration() { @@ -38,8 +49,7 @@ final class AlmanacService 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', - 'serviceClass' => 'text64', - 'isLocked' => 'bool', + 'serviceType' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( @@ -49,8 +59,8 @@ final class AlmanacService 'key_nametext' => array( 'columns' => array('name'), ), - 'key_class' => array( - 'columns' => array('serviceClass'), + 'key_servicetype' => array( + 'columns' => array('serviceType'), ), ), ) + parent::getConfiguration(); @@ -61,7 +71,7 @@ final class AlmanacService } public function save() { - AlmanacNames::validateServiceOrDeviceName($this->getName()); + AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); @@ -80,20 +90,37 @@ final class AlmanacService 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 getServiceType() { - return $this->assertAttached($this->serviceType); + public function getServiceImplementation() { + return $this->assertAttached($this->serviceImplementation); } - public function attachServiceType(AlmanacServiceType $type) { - $this->serviceType = $type; + public function attachServiceImplementation(AlmanacServiceType $type) { + $this->serviceImplementation = $type; return $this; } + public function isClusterService() { + return $this->getServiceImplementation()->isClusterServiceType(); + } + /* -( AlmanacPropertyInterface )------------------------------------------- */ @@ -126,7 +153,11 @@ final class AlmanacService } public function getAlmanacPropertyFieldSpecifications() { - return $this->getServiceType()->getFieldSpecifications(); + return $this->getServiceImplementation()->getFieldSpecifications(); + } + + public function newAlmanacPropertyEditEngine() { + return new AlmanacServicePropertyEditEngine(); } @@ -145,11 +176,7 @@ final class AlmanacService case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: - if ($this->getIsLocked()) { - return PhabricatorPolicies::POLICY_NOONE; - } else { - return $this->getEditPolicy(); - } + return $this->getEditPolicy(); } } @@ -158,38 +185,30 @@ final class AlmanacService } public function describeAutomaticCapability($capability) { - switch ($capability) { - case PhabricatorPolicyCapability::CAN_EDIT: - if ($this->getIsLocked()) { - return pht('This service is locked and can not be edited.'); - } - break; - } - return null; } -/* -( PhabricatorCustomFieldInterface )------------------------------------ */ +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ - public function getCustomFieldSpecificationForRole($role) { + 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(); } - public function getCustomFieldBaseClass() { - return 'AlmanacCustomField'; - } - - public function getCustomFields() { - return $this->assertAttached($this->customFields); - } - - public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { - $this->customFields = $fields; - return $this; - } - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -231,4 +250,48 @@ final class AlmanacService $this->delete(); } + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + 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/almanac/storage/AlmanacServiceNameNgrams.php b/src/applications/almanac/storage/AlmanacServiceNameNgrams.php new file mode 100644 index 0000000000..4ad6a9639d --- /dev/null +++ b/src/applications/almanac/storage/AlmanacServiceNameNgrams.php @@ -0,0 +1,18 @@ +getAuthorPHID(); diff --git a/src/applications/almanac/storage/AlmanacTransaction.php b/src/applications/almanac/storage/AlmanacTransaction.php new file mode 100644 index 0000000000..18bd776335 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacTransaction.php @@ -0,0 +1,38 @@ +getAuthorPHID(); + + switch ($this->getTransactionType()) { + case self::TYPE_PROPERTY_UPDATE: + $property_key = $this->getMetadataValue('almanac.property'); + return pht( + '%s updated the property "%s".', + $this->renderHandleLink($author_phid), + $property_key); + case self::TYPE_PROPERTY_REMOVE: + $property_key = $this->getMetadataValue('almanac.property'); + return pht( + '%s deleted the property "%s".', + $this->renderHandleLink($author_phid), + $property_key); + } + + return parent::getTitle(); + } + +} diff --git a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php index a5f46ddfb0..15e50f5993 100644 --- a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php +++ b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php @@ -3,12 +3,6 @@ final class AlmanacInterfaceDatasource extends PhabricatorTypeaheadDatasource { - public function isBrowsable() { - // TODO: We should make this browsable, but need to make the result set - // orderable by device name. - return false; - } - public function getBrowseTitle() { return pht('Browse Interfaces'); } @@ -31,10 +25,12 @@ final class AlmanacInterfaceDatasource ->execute(); if ($devices) { - $interfaces = id(new AlmanacInterfaceQuery()) + $interface_query = id(new AlmanacInterfaceQuery()) ->setViewer($viewer) ->withDevicePHIDs(mpull($devices, 'getPHID')) - ->execute(); + ->setOrder('name'); + + $interfaces = $this->executeQuery($interface_query); } else { $interfaces = array(); } diff --git a/src/applications/almanac/typeahead/AlmanacServiceDatasource.php b/src/applications/almanac/typeahead/AlmanacServiceDatasource.php index 621b0408ae..4413465125 100644 --- a/src/applications/almanac/typeahead/AlmanacServiceDatasource.php +++ b/src/applications/almanac/typeahead/AlmanacServiceDatasource.php @@ -28,9 +28,9 @@ final class AlmanacServiceDatasource // selected, or show all services but mark the invalid ones disabled and // prevent their selection. - $service_classes = $this->getParameter('serviceClasses'); - if ($service_classes) { - $services->withServiceClasses($service_classes); + $service_types = $this->getParameter('serviceTypes'); + if ($service_types) { + $services->withServiceTypes($service_types); } $services = $this->executeQuery($services); diff --git a/src/applications/almanac/typeahead/AlmanacServiceTypeDatasource.php b/src/applications/almanac/typeahead/AlmanacServiceTypeDatasource.php new file mode 100644 index 0000000000..5314084244 --- /dev/null +++ b/src/applications/almanac/typeahead/AlmanacServiceTypeDatasource.php @@ -0,0 +1,43 @@ +buildResults(); + return $this->filterResultsAgainstTokens($results); + } + + protected function renderSpecialTokens(array $values) { + return $this->renderTokensFromResults($this->buildResults(), $values); + } + + private function buildResults() { + $results = array(); + + $types = AlmanacServiceType::getAllServiceTypes(); + + $results = array(); + foreach ($types as $key => $type) { + $results[$key] = id(new PhabricatorTypeaheadResult()) + ->setName($type->getServiceTypeName()) + ->setIcon($type->getServiceTypeIcon()) + ->setPHID($key); + } + + return $results; + } + +} diff --git a/src/applications/almanac/util/AlmanacNames.php b/src/applications/almanac/util/AlmanacNames.php index d65779264d..68a387cb9b 100644 --- a/src/applications/almanac/util/AlmanacNames.php +++ b/src/applications/almanac/util/AlmanacNames.php @@ -2,54 +2,61 @@ final class AlmanacNames extends Phobject { - public static function validateServiceOrDeviceName($name) { + public static function validateName($name) { if (strlen($name) < 3) { throw new Exception( pht( - 'Almanac service and device names must be at least 3 '. - 'characters long.')); + 'Almanac service, device, property and namespace names must be '. + 'at least 3 characters long.')); + } + + if (strlen($name) > 100) { + throw new Exception( + pht( + 'Almanac service, device, property and namespace names may not '. + 'be more than 100 characters long.')); } if (!preg_match('/^[a-z0-9.-]+\z/', $name)) { throw new Exception( pht( - 'Almanac service and device names may only contain lowercase '. - 'letters, numbers, hyphens, and periods.')); + 'Almanac service, device, property and namespace names may only '. + 'contain lowercase letters, numbers, hyphens, and periods.')); } if (preg_match('/(^|\\.)\d+(\z|\\.)/', $name)) { throw new Exception( pht( - 'Almanac service and device names may not have any segments '. - 'containing only digits.')); + 'Almanac service, device, property and namespace names may not '. + 'have any segments containing only digits.')); } if (preg_match('/\.\./', $name)) { throw new Exception( pht( - 'Almanac service and device names may not contain multiple '. - 'consecutive periods.')); + 'Almanac service, device, property and namespace names may not '. + 'contain multiple consecutive periods.')); } if (preg_match('/\\.-|-\\./', $name)) { throw new Exception( pht( - 'Amanac service and device names may not contain hyphens adjacent '. - 'to periods.')); + 'Almanac service, device, property and namespace names may not '. + 'contain hyphens adjacent to periods.')); } if (preg_match('/--/', $name)) { throw new Exception( pht( - 'Almanac service and device names may not contain multiple '. - 'consecutive hyphens.')); + 'Almanac service, device, property and namespace names may not '. + 'contain multiple consecutive hyphens.')); } if (!preg_match('/^[a-z0-9].*[a-z0-9]\z/', $name)) { throw new Exception( pht( - 'Almanac service and device names must begin and end with a letter '. - 'or number.')); + 'Almanac service, device, property and namespace names must begin '. + 'and end with a letter or number.')); } } diff --git a/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php b/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php index 7ca1a37f9f..78dea769e4 100644 --- a/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php +++ b/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php @@ -33,12 +33,16 @@ final class AlmanacNamesTestCase extends PhabricatorTestCase { 'db.phacility.instance' => true, 'web002.useast.example.com' => true, 'master.example-corp.com' => true, + + // Maximum length is 100. + str_repeat('a', 100) => true, + str_repeat('a', 101) => false, ); foreach ($map as $input => $expect) { $caught = null; try { - AlmanacNames::validateServiceOrDeviceName($input); + AlmanacNames::validateName($input); } catch (Exception $ex) { $caught = $ex; } diff --git a/src/applications/almanac/view/AlmanacBindingTableView.php b/src/applications/almanac/view/AlmanacBindingTableView.php index 7e83d9e3d2..06797c912d 100644 --- a/src/applications/almanac/view/AlmanacBindingTableView.php +++ b/src/applications/almanac/view/AlmanacBindingTableView.php @@ -5,6 +5,8 @@ final class AlmanacBindingTableView extends AphrontView { private $bindings; private $noDataString; + private $hideServiceColumn; + public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; @@ -23,6 +25,15 @@ final class AlmanacBindingTableView extends AphrontView { return $this->bindings; } + public function setHideServiceColumn($hide_service_column) { + $this->hideServiceColumn = $hide_service_column; + return $this; + } + + public function getHideServiceColumn() { + return $this->hideServiceColumn; + } + public function render() { $bindings = $this->getBindings(); $viewer = $this->getUser(); @@ -35,6 +46,22 @@ final class AlmanacBindingTableView extends AphrontView { } $handles = $viewer->loadHandles($phids); + $icon_disabled = id(new PHUIIconView()) + ->setIcon('fa-ban') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Disabled'), + )); + + $icon_active = id(new PHUIIconView()) + ->setIcon('fa-check') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Active'), + )); + $rows = array(); foreach ($bindings as $binding) { $addr = $binding->getInterface()->getAddress(); @@ -42,6 +69,7 @@ final class AlmanacBindingTableView extends AphrontView { $rows[] = array( $binding->getID(), + ($binding->getIsDisabled() ? $icon_disabled : $icon_active), $handles->renderHandle($binding->getServicePHID()), $handles->renderHandle($binding->getDevicePHID()), $handles->renderHandle($binding->getInterface()->getNetworkPHID()), @@ -61,6 +89,7 @@ final class AlmanacBindingTableView extends AphrontView { ->setHeaders( array( pht('ID'), + null, pht('Service'), pht('Device'), pht('Network'), @@ -70,11 +99,18 @@ final class AlmanacBindingTableView extends AphrontView { ->setColumnClasses( array( '', + 'icon', '', '', '', 'wide', 'action', + )) + ->setColumnVisibility( + array( + true, + true, + !$this->getHideServiceColumn(), )); return $table; diff --git a/src/applications/almanac/view/AlmanacInterfaceTableView.php b/src/applications/almanac/view/AlmanacInterfaceTableView.php index 48214da078..f937e34f44 100644 --- a/src/applications/almanac/view/AlmanacInterfaceTableView.php +++ b/src/applications/almanac/view/AlmanacInterfaceTableView.php @@ -27,7 +27,9 @@ final class AlmanacInterfaceTableView extends AphrontView { $interfaces = $this->getInterfaces(); $viewer = $this->getUser(); - if ($this->getCanEdit()) { + $can_edit = $this->getCanEdit(); + + if ($can_edit) { $button_class = 'small grey button'; } else { $button_class = 'small grey button disabled'; @@ -42,13 +44,22 @@ final class AlmanacInterfaceTableView extends AphrontView { $handles->renderHandle($interface->getNetworkPHID()), $interface->getAddress(), $interface->getPort(), - phutil_tag( + javelin_tag( 'a', array( 'class' => $button_class, 'href' => '/almanac/interface/edit/'.$interface->getID().'/', + 'sigil' => ($can_edit ? null : 'workflow'), ), pht('Edit')), + javelin_tag( + 'a', + array( + 'class' => $button_class, + 'href' => '/almanac/interface/delete/'.$interface->getID().'/', + 'sigil' => 'workflow', + ), + pht('Delete')), ); } @@ -60,6 +71,7 @@ final class AlmanacInterfaceTableView extends AphrontView { pht('Address'), pht('Port'), null, + null, )) ->setColumnClasses( array( @@ -68,6 +80,7 @@ final class AlmanacInterfaceTableView extends AphrontView { '', '', 'action', + 'action', )); return $table; diff --git a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php index b4bb950806..5e50b2ae9d 100644 --- a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php +++ b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php @@ -82,9 +82,7 @@ final class PhabricatorAuditAddCommentController $draft->delete(); } - $monogram = $commit->getRepository()->getMonogram(); - $identifier = $commit->getCommitIdentifier(); - $uri = '/'.$monogram.$identifier; + $uri = $commit->getURI(); return id(new AphrontRedirectResponse())->setURI($uri); } diff --git a/src/applications/config/check/PhabricatorRepositoriesSetupCheck.php b/src/applications/config/check/PhabricatorRepositoriesSetupCheck.php index 89967f7eaa..64073ec1ed 100644 --- a/src/applications/config/check/PhabricatorRepositoriesSetupCheck.php +++ b/src/applications/config/check/PhabricatorRepositoriesSetupCheck.php @@ -10,9 +10,9 @@ final class PhabricatorRepositoriesSetupCheck extends PhabricatorSetupCheck { $cluster_services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withServiceClasses( + ->withServiceTypes( array( - 'AlmanacClusterRepositoryServiceType', + AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->setLimit(1) ->execute(); diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 9d6b845c91..6736b99430 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -790,8 +790,6 @@ final class DiffusionCommitController extends DiffusionController { $actions = array(); $actions[PhabricatorAuditActionConstants::COMMENT] = true; - $actions[PhabricatorAuditActionConstants::ADD_CCS] = true; - $actions[PhabricatorAuditActionConstants::ADD_AUDITORS] = true; // We allow you to accept your own commits. A use case here is that you // notice an issue with your own commit and "Raise Concern" as an indicator @@ -801,7 +799,6 @@ final class DiffusionCommitController extends DiffusionController { $actions[PhabricatorAuditActionConstants::ACCEPT] = true; $actions[PhabricatorAuditActionConstants::CONCERN] = true; - // To resign, a user must have authority on some request and not be the // commit's author. if (!$user_is_author) { @@ -837,6 +834,9 @@ final class DiffusionCommitController extends DiffusionController { $actions[PhabricatorAuditActionConstants::CLOSE] = true; } + $actions[PhabricatorAuditActionConstants::ADD_AUDITORS] = true; + $actions[PhabricatorAuditActionConstants::ADD_CCS] = true; + foreach ($actions as $constant => $ignored) { $actions[$constant] = PhabricatorAuditActionConstants::getActionName($constant); diff --git a/src/applications/diffusion/controller/DiffusionExternalController.php b/src/applications/diffusion/controller/DiffusionExternalController.php index 09687f11d0..07a131c826 100644 --- a/src/applications/diffusion/controller/DiffusionExternalController.php +++ b/src/applications/diffusion/controller/DiffusionExternalController.php @@ -103,7 +103,7 @@ final class DiffusionExternalController extends DiffusionController { array( 'href' => $href, ), - $commit->getMonogram()), + $commit->getURI()), $commit->loadCommitData()->getSummary(), ); } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php index 1333bf67f9..a9113fee13 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php @@ -47,10 +47,11 @@ final class DiffusionRepositoryCreateController // allocations, we fail. $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withServiceClasses( + ->withServiceTypes( array( - 'AlmanacClusterRepositoryServiceType', + AlmanacClusterRepositoryServiceType::SERVICETYPE, )) + ->needProperties(true) ->execute(); if ($services) { // Filter out services which do not permit new allocations. diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index 46506ff317..1785ae6741 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -238,6 +238,8 @@ final class DivinerAtomController extends DivinerController { $document->setToc($side); } + $prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list); + return $this->buildApplicationPage( array( $crumbs, diff --git a/src/applications/drydock/DrydockCommandError/DrydockCommandError.php b/src/applications/drydock/DrydockCommandError/DrydockCommandError.php new file mode 100644 index 0000000000..d9acbe7456 --- /dev/null +++ b/src/applications/drydock/DrydockCommandError/DrydockCommandError.php @@ -0,0 +1,18 @@ + $phase, + 'command' => (string)$command, + 'raw' => (string)$ex->getCommand(), + 'err' => $ex->getError(), + 'stdout' => $ex->getStdout(), + 'stderr' => $ex->getStderr(), + ); + return $error; + } +} diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php index 19e476c4ba..443b0e8965 100644 --- a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -184,7 +184,7 @@ final class DrydockAlmanacServiceHostBlueprintImplementation 'type' => 'datasource', 'datasource.class' => 'AlmanacServiceDatasource', 'datasource.parameters' => array( - 'serviceClasses' => $this->getAlmanacServiceClasses(), + 'serviceTypes' => $this->getAlmanacServiceTypes(), ), 'required' => true, ), @@ -213,7 +213,7 @@ final class DrydockAlmanacServiceHostBlueprintImplementation $services = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withPHIDs($service_phids) - ->withServiceClasses($this->getAlmanacServiceClasses()) + ->withServiceTypes($this->getAlmanacServiceTypes()) ->needBindings(true) ->execute(); $services = mpull($services, null, 'getPHID'); @@ -267,6 +267,11 @@ final class DrydockAlmanacServiceHostBlueprintImplementation $free = array(); foreach ($bindings as $binding) { + // Don't consider disabled bindings to be available. + if ($binding->getIsDisabled()) { + continue; + } + if (empty($allocated_phids[$binding->getPHID()])) { $free[] = $binding; } @@ -278,9 +283,9 @@ final class DrydockAlmanacServiceHostBlueprintImplementation return $this->freeBindings; } - private function getAlmanacServiceClasses() { + private function getAlmanacServiceTypes() { return array( - 'AlmanacDrydockPoolServiceType', + AlmanacDrydockPoolServiceType::SERVICETYPE, ); } diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index d60a65580b..d2f7191c06 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -448,35 +448,17 @@ final class DrydockWorkingCopyBlueprintImplementation try { $interface->execx('%C', $real_command); } catch (CommandException $ex) { - $this->setWorkingCopyVCSErrorFromCommandException( - $lease, + $error = DrydockCommandError::newFromCommandException( self::PHASE_SQUASHMERGE, $show_command, $ex); + $lease->setAttribute('workingcopy.vcs.error', $error); throw $ex; } } - protected function setWorkingCopyVCSErrorFromCommandException( - DrydockLease $lease, - $phase, - $command, - CommandException $ex) { - - $error = array( - 'phase' => $phase, - 'command' => (string)$command, - 'raw' => (string)$ex->getCommand(), - 'err' => $ex->getError(), - 'stdout' => $ex->getStdout(), - 'stderr' => $ex->getStderr(), - ); - - $lease->setAttribute('workingcopy.vcs.error', $error); - } - - public function getWorkingCopyVCSError(DrydockLease $lease) { + public function getCommandError(DrydockLease $lease) { $error = $lease->getAttribute('workingcopy.vcs.error'); if (!$error) { return null; diff --git a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php index af2f9f271f..5beb18d07b 100644 --- a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php +++ b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php @@ -4,6 +4,7 @@ final class DrydockLandRepositoryOperation extends DrydockRepositoryOperationType { const OPCONST = 'land'; + const PHASE_PUSH = 'push'; public function getOperationDescription( DrydockRepositoryOperation $operation, @@ -122,10 +123,23 @@ final class DrydockLandRepositoryOperation ->write($commit_message) ->resolvex(); - $interface->execx( + try { + $interface->execx( + 'git push origin -- %s:%s', + 'HEAD', + $push_dst); + } catch (CommandException $ex) { + $show_command = csprintf( 'git push origin -- %s:%s', 'HEAD', $push_dst); + $error = DrydockCommandError::newFromCommandException( + self::PHASE_PUSH, + $show_command, + $ex); + $operation->setCommandError($error); + throw $ex; + } } private function getCommitterInfo(DrydockRepositoryOperation $operation) { diff --git a/src/applications/drydock/storage/DrydockRepositoryOperation.php b/src/applications/drydock/storage/DrydockRepositoryOperation.php index 87565edc86..190b5b1310 100644 --- a/src/applications/drydock/storage/DrydockRepositoryOperation.php +++ b/src/applications/drydock/storage/DrydockRepositoryOperation.php @@ -178,11 +178,11 @@ final class DrydockRepositoryOperation extends DrydockDAO return $this->getProperty('exec.leasePHID'); } - public function setWorkingCopyVCSError(array $error) { + public function setCommandError(array $error) { return $this->setProperty('exec.workingcopy.error', $error); } - public function getWorkingCopyVCSError() { + public function getCommandError() { return $this->getProperty('exec.workingcopy.error'); } diff --git a/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php b/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php index 07c5a7b706..23ad6b81fe 100644 --- a/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php +++ b/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php @@ -74,7 +74,7 @@ final class DrydockRepositoryOperationStatusView if ($state != DrydockRepositoryOperation::STATE_FAIL) { $item->addAttribute($operation->getOperationCurrentStatus($viewer)); } else { - $vcs_error = $operation->getWorkingCopyVCSError(); + $vcs_error = $operation->getCommandError(); if ($vcs_error) { switch ($vcs_error['phase']) { case DrydockWorkingCopyBlueprintImplementation::PHASE_SQUASHMERGE: @@ -82,6 +82,12 @@ final class DrydockRepositoryOperationStatusView 'This change did not merge cleanly. This usually indicates '. 'that the change is out of date and needs to be updated.'); break; + case DrydockLandRepositoryOperation::PHASE_PUSH: + $message = pht( + 'The push failed. This usually indicates '. + 'that the change is breaking some rules or '. + 'some custom commit hook has failed.'); + break; default: $message = pht( 'Operation encountered an error while performing repository '. diff --git a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php index 6160575fd0..9038b3b6d7 100644 --- a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php +++ b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php @@ -138,10 +138,10 @@ final class DrydockRepositoryOperationUpdateWorker } if (!$lease->isActive()) { - $vcs_error = $working_copy->getWorkingCopyVCSError($lease); + $vcs_error = $working_copy->getCommandError($lease); if ($vcs_error) { $operation - ->setWorkingCopyVCSError($vcs_error) + ->setCommandError($vcs_error) ->save(); } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index d729d868b0..ddab677faf 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -39,6 +39,8 @@ final class HarbormasterBuildActionController return new Aphront400Response(); } + $build->assertCanIssueCommand($viewer, $action); + switch ($via) { case 'buildable': $return_uri = '/'.$build->getBuildable()->getMonogram(); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index a9cbb43ccf..25f3506c0b 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -441,10 +441,29 @@ final class HarbormasterBuildViewController ->setUser($viewer) ->setObject($build); - $can_restart = $build->canRestartBuild(); - $can_pause = $build->canPauseBuild(); - $can_resume = $build->canResumeBuild(); - $can_abort = $build->canAbortBuild(); + $can_restart = + $build->canRestartBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_RESTART); + + $can_pause = + $build->canPauseBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_PAUSE); + + $can_resume = + $build->canResumeBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_RESUME); + + $can_abort = + $build->canAbortBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_ABORT); $list->addAction( id(new PhabricatorActionView()) diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php index 9fa7d6f006..b3f60cd1dc 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -51,6 +51,14 @@ final class HarbormasterBuildableActionController } } + $restricted = false; + foreach ($issuable as $key => $build) { + if (!$build->canIssueCommand($viewer, $action)) { + $restricted = true; + unset($issuable[$key]); + } + } + $return_uri = '/'.$buildable->getMonogram(); if ($request->isDialogFormPost() && $issuable) { $editor = id(new HarbormasterBuildableTransactionEditor()) @@ -84,49 +92,101 @@ final class HarbormasterBuildableActionController switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($issuable) { - $title = pht('Really restart all builds?'); - $body = pht( - 'Progress on all builds will be discarded, and all builds will '. - 'restart. Side effects of the builds will occur again. Really '. - 'restart all builds?'); - $submit = pht('Restart All Builds'); + $title = pht('Really restart builds?'); + + if ($restricted) { + $body = pht( + 'You only have permission to restart some builds. Progress '. + 'on builds you have permission to restart will be discarded '. + 'and they will restart. Side effects of these builds will '. + 'occur again. Really restart all builds?'); + } else { + $body = pht( + 'Progress on all builds will be discarded, and all builds will '. + 'restart. Side effects of the builds will occur again. Really '. + 'restart all builds?'); + } + + $submit = pht('Restart Builds'); } else { $title = pht('Unable to Restart Builds'); - $body = pht('No builds can be restarted.'); + + if ($restricted) { + $body = pht('You do not have permission to restart any builds.'); + } else { + $body = pht('No builds can be restarted.'); + } } break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($issuable) { - $title = pht('Really pause all builds?'); - $body = pht( - 'If you pause all builds, work will halt once the current steps '. - 'complete. You can resume the builds later.'); - $submit = pht('Pause All Builds'); + $title = pht('Really pause builds?'); + + if ($restricted) { + $body = pht( + 'You only have permission to pause some builds. Once the '. + 'current steps complete, work will halt on builds you have '. + 'permission to pause. You can resume the builds later.'); + } else { + $body = pht( + 'If you pause all builds, work will halt once the current steps '. + 'complete. You can resume the builds later.'); + } + $submit = pht('Pause Builds'); } else { $title = pht('Unable to Pause Builds'); - $body = pht('No builds can be paused.'); + + if ($restricted) { + $body = pht('You do not have permission to pause any builds.'); + } else { + $body = pht('No builds can be paused.'); + } } break; case HarbormasterBuildCommand::COMMAND_ABORT: if ($issuable) { - $title = pht('Really abort all builds?'); - $body = pht( - 'If you abort all builds, work will halt immediately. Work '. - 'will be discarded, and builds must be completely restarted.'); - $submit = pht('Abort All Builds'); + $title = pht('Really abort builds?'); + if ($restricted) { + $body = pht( + 'You only have permission to abort some builds. Work will '. + 'halt immediately on builds you have permission to abort. '. + 'Progress will be discarded, and builds must be completely '. + 'restarted if you want them to complete.'); + } else { + $body = pht( + 'If you abort all builds, work will halt immediately. Work '. + 'will be discarded, and builds must be completely restarted.'); + } + $submit = pht('Abort Builds'); } else { $title = pht('Unable to Abort Builds'); - $body = pht('No builds can be aborted.'); + + if ($restricted) { + $body = pht('You do not have permission to abort any builds.'); + } else { + $body = pht('No builds can be aborted.'); + } } break; case HarbormasterBuildCommand::COMMAND_RESUME: if ($issuable) { - $title = pht('Really resume all builds?'); - $body = pht('Work will continue on all builds. Really resume?'); - $submit = pht('Resume All Builds'); + $title = pht('Really resume builds?'); + if ($restricted) { + $body = pht( + 'You only have permission to resume some builds. Work will '. + 'continue on builds you have permission to resume.'); + } else { + $body = pht('Work will continue on all builds. Really resume?'); + } + + $submit = pht('Resume Builds'); } else { $title = pht('Unable to Resume Builds'); - $body = pht('No builds can be resumed.'); + if ($restricted) { + $body = pht('You do not have permission to resume any builds.'); + } else { + $body = pht('No builds can be resumed.'); + } } break; } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index f9a0807ab1..91fb45176f 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -86,18 +86,31 @@ final class HarbormasterBuildableViewController $can_pause = false; $can_abort = false; + $command_restart = HarbormasterBuildCommand::COMMAND_RESTART; + $command_resume = HarbormasterBuildCommand::COMMAND_RESUME; + $command_pause = HarbormasterBuildCommand::COMMAND_PAUSE; + $command_abort = HarbormasterBuildCommand::COMMAND_ABORT; + foreach ($buildable->getBuilds() as $build) { if ($build->canRestartBuild()) { - $can_restart = true; + if ($build->canIssueCommand($viewer, $command_restart)) { + $can_restart = true; + } } if ($build->canResumeBuild()) { - $can_resume = true; + if ($build->canIssueCommand($viewer, $command_resume)) { + $can_resume = true; + } } if ($build->canPauseBuild()) { - $can_pause = true; + if ($build->canIssueCommand($viewer, $command_pause)) { + $can_pause = true; + } } if ($build->canAbortBuild()) { - $can_abort = true; + if ($build->canIssueCommand($viewer, $command_abort)) { + $can_abort = true; + } } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index ed1381006b..f4212c38b3 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -105,31 +105,31 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { $i++; } + $step_id = $step->getID(); + $view_uri = $this->getApplicationURI("step/view/{$step_id}/"); + + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Step %d.%d', $depth, $i)) + ->setHeader($step->getName()) + ->setHref($view_uri); + + $step_list->addItem($item); + $implementation = null; try { $implementation = $step->getStepImplementation(); } catch (Exception $ex) { // We can't initialize the implementation. This might be because // it's been renamed or no longer exists. - $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Step %d.%d', $depth, $i)) - ->setHeader(pht('Unknown Implementation')) + $item ->setStatusIcon('fa-warning red') ->addAttribute(pht( 'This step has an invalid implementation (%s).', $step->getClassName())); - $step_list->addItem($item); continue; } - $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Step %d.%d', $depth, $i)) - ->setHeader($step->getName()); $item->addAttribute($implementation->getDescription()); - - $step_id = $step->getID(); - - $view_uri = $this->getApplicationURI("step/view/{$step_id}/"); $item->setHref($view_uri); $depends = $step->getStepImplementation()->getDependencies($step); diff --git a/src/applications/harbormaster/controller/HarbormasterStepViewController.php b/src/applications/harbormaster/controller/HarbormasterStepViewController.php index d8d1d3f0b0..8f8abf6ea9 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepViewController.php @@ -18,8 +18,6 @@ final class HarbormasterStepViewController extends HarbormasterController { $plan_id = $plan->getID(); $plan_uri = $this->getApplicationURI("plan/{$plan_id}/"); - $implementation = $step->getStepImplementation(); - $field_list = PhabricatorCustomField::getObjectFields( $step, PhabricatorCustomField::ROLE_VIEW); @@ -65,6 +63,25 @@ final class HarbormasterStepViewController extends HarbormasterController { ->setUser($viewer) ->setObject($step); + try { + $implementation = $step->getStepImplementation(); + } catch (Exception $ex) { + $implementation = null; + } + + if ($implementation) { + $type = $implementation->getName(); + } else { + $type = phutil_tag( + 'em', + array(), + pht( + 'Invalid Implementation ("%s")!', + $step->getClassName())); + } + + $view->addProperty(pht('Step Type'), $type); + $view->addProperty( pht('Created'), phabricator_datetime($step->getDateCreated(), $viewer)); diff --git a/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php b/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php index 296669d2b5..dc0c759da6 100644 --- a/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php +++ b/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php @@ -9,7 +9,12 @@ final class HarbormasterBuildStepCoreCustomField } public function createFields($object) { - $impl = $object->getStepImplementation(); + try { + $impl = $object->getStepImplementation(); + } catch (Exception $ex) { + return array(); + } + $specs = $impl->getFieldSpecifications(); if ($impl->supportsWaitForMessage()) { diff --git a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php index 0727a473ec..2615f3c5be 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php @@ -88,6 +88,11 @@ final class HarbormasterBuildTransactionEditor return; } + $actor = $this->getActor(); + if (!$build->canIssueCommand($actor, $command)) { + return; + } + id(new HarbormasterBuildCommand()) ->setAuthorPHID($xaction->getAuthorPHID()) ->setTargetPHID($build->getPHID()) diff --git a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php index 757f112d05..dc9e4b7691 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php @@ -48,7 +48,12 @@ final class HarbormasterBuildGraph extends AbstractDirectedGraph { $map = array(); foreach ($nodes as $node) { $step = $this->stepMap[$node]; - $deps = $step->getStepImplementation()->getDependencies($step); + + try { + $deps = $step->getStepImplementation()->getDependencies($step); + } catch (Exception $ex) { + $deps = array(); + } $map[$node] = array(); foreach ($deps as $dep) { diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php deleted file mode 100644 index a99c56d0a3..0000000000 --- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php +++ /dev/null @@ -1,149 +0,0 @@ -formatSettingForDescription('command'), - $this->formatSettingForDescription('hostartifact')); - } - - public function escapeCommand($pattern, array $args) { - array_unshift($args, $pattern); - - $mode = PhutilCommandString::MODE_DEFAULT; - if ($this->platform == 'windows') { - $mode = PhutilCommandString::MODE_POWERSHELL; - } - - return id(new PhutilCommandString($args)) - ->setEscapingMode($mode); - } - - public function execute( - HarbormasterBuild $build, - HarbormasterBuildTarget $build_target) { - $viewer = PhabricatorUser::getOmnipotentUser(); - - $settings = $this->getSettings(); - $variables = $build_target->getVariables(); - - $artifact = $build_target->loadArtifact($settings['hostartifact']); - $impl = $artifact->getArtifactImplementation(); - $lease = $impl->loadArtifactLease($viewer); - - $this->platform = $lease->getAttribute('platform'); - - $command = $this->mergeVariables( - array($this, 'escapeCommand'), - $settings['command'], - $variables); - - $this->platform = null; - - $interface = $lease->getInterface('command'); - - $future = $interface->getExecFuture('%C', $command); - - $log_stdout = $build->createLog($build_target, 'remote', 'stdout'); - $log_stderr = $build->createLog($build_target, 'remote', 'stderr'); - - $start_stdout = $log_stdout->start(); - $start_stderr = $log_stderr->start(); - - $build_update = 5; - - // Read the next amount of available output every second. - $futures = new FutureIterator(array($future)); - foreach ($futures->setUpdateInterval(1) as $key => $future_iter) { - if ($future_iter === null) { - - // Check to see if we should abort. - if ($build_update <= 0) { - $build->reload(); - if ($this->shouldAbort($build, $build_target)) { - $future->resolveKill(); - throw new HarbormasterBuildAbortedException(); - } else { - $build_update = 5; - } - } else { - $build_update -= 1; - } - - // Command is still executing. - - // Read more data as it is available. - list($stdout, $stderr) = $future->read(); - $log_stdout->append($stdout); - $log_stderr->append($stderr); - $future->discardBuffers(); - } else { - // Command execution is complete. - - // Get the return value so we can log that as well. - list($err) = $future->resolve(); - - // Retrieve the last few bits of information. - list($stdout, $stderr) = $future->read(); - $log_stdout->append($stdout); - $log_stderr->append($stderr); - $future->discardBuffers(); - - break; - } - } - - $log_stdout->finalize($start_stdout); - $log_stderr->finalize($start_stderr); - - if ($err) { - throw new HarbormasterBuildFailureException(); - } - } - - public function getArtifactInputs() { - return array( - array( - 'name' => pht('Run on Host'), - 'key' => $this->getSetting('hostartifact'), - 'type' => HarbormasterHostArtifact::ARTIFACTCONST, - ), - ); - } - - public function getFieldSpecifications() { - return array( - 'command' => array( - 'name' => pht('Command'), - 'type' => 'text', - 'required' => true, - 'caption' => pht( - "Under Windows, this is executed under PowerShell. ". - "Under UNIX, this is executed using the user's shell."), - ), - 'hostartifact' => array( - 'name' => pht('Host'), - 'type' => 'text', - 'required' => true, - ), - ); - } - -} diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php deleted file mode 100644 index b14d0975f7..0000000000 --- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php +++ /dev/null @@ -1,74 +0,0 @@ -getSettings(); - - // Create the lease. - $lease = id(new DrydockLease()) - ->setResourceType('host') - ->setOwnerPHID($build_target->getPHID()) - ->setAttributes( - array( - 'platform' => $settings['platform'], - )) - ->queueForActivation(); - - // Wait until the lease is fulfilled. - // TODO: This will throw an exception if the lease can't be fulfilled; - // we should treat that as build failure not build error. - $lease->waitUntilActive(); - - // Create the associated artifact. - $artifact = $build_target->createArtifact( - PhabricatorUser::getOmnipotentUser(), - $settings['name'], - HarbormasterHostArtifact::ARTIFACTCONST, - array( - 'drydockLeasePHID' => $lease->getPHID(), - )); - } - - public function getArtifactOutputs() { - return array( - array( - 'name' => pht('Leased Host'), - 'key' => $this->getSetting('name'), - 'type' => HarbormasterHostArtifact::ARTIFACTCONST, - ), - ); - } - - public function getFieldSpecifications() { - return array( - 'name' => array( - 'name' => pht('Artifact Name'), - 'type' => 'text', - 'required' => true, - ), - 'platform' => array( - 'name' => pht('Platform'), - 'type' => 'text', - 'required' => true, - ), - ); - } - -} diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index c96d0c2710..1bac8d3ecd 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -450,6 +450,42 @@ final class HarbormasterBuild extends HarbormasterDAO 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); + } + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 97c567c30d..b88c5a5923 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1033,16 +1033,22 @@ final class ManiphestTransactionEditor )); break; case ManiphestTransaction::TYPE_OWNER: + // If this is a no-op update, don't expand it. + $old_value = $object->getOwnerPHID(); + $new_value = $xaction->getNewValue(); + if ($old_value === $new_value) { + continue; + } + // When a task is reassigned, move the old owner to the subscriber // list so they're still in the loop. - $owner_phid = $object->getOwnerPHID(); - if ($owner_phid) { + if ($old_value) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setIgnoreOnNoEffect(true) ->setNewValue( array( - '+' => array($owner_phid => $owner_phid), + '+' => array($old_value => $old_value), )); } break; diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index ce15aa2b38..698ff983f1 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -60,6 +60,7 @@ final class PhabricatorPeopleProfileViewController $home = id(new PHUITwoColumnView()) ->setHeader($header) + ->setFluid(true) ->addClass('project-view-home') ->setMainColumn( array( diff --git a/src/applications/phame/controller/blog/PhameBlogManageController.php b/src/applications/phame/controller/blog/PhameBlogManageController.php index 14caafaab4..30de8686c8 100644 --- a/src/applications/phame/controller/blog/PhameBlogManageController.php +++ b/src/applications/phame/controller/blog/PhameBlogManageController.php @@ -117,7 +117,8 @@ final class PhameBlogManageController extends PhameBlogController { $properties->invokeWillRenderEvent(); - if (strlen($blog->getDescription())) { + $description = $blog->getDescription(); + if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); $properties->addSectionHeader( pht('Description'), diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index a4231cc1ac..5579e82656 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -136,6 +136,7 @@ final class PhamePostViewController $document->setFoot($next_view); $crumbs = $this->buildApplicationCrumbs(); + $properties = phutil_tag_div('phui-document-view-pro-box', $properties); $page = $this->newPage() ->setTitle($post->getTitle()) diff --git a/src/applications/phid/view/PHUIHandleTagListView.php b/src/applications/phid/view/PHUIHandleTagListView.php index baad938051..0bbfc4d249 100644 --- a/src/applications/phid/view/PHUIHandleTagListView.php +++ b/src/applications/phid/view/PHUIHandleTagListView.php @@ -52,13 +52,6 @@ final class PHUIHandleTagListView extends AphrontTagView { protected function getTagContent() { $handles = $this->handles; - // Remove any archived projects from the list. - foreach ($handles as $key => $handle) { - if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) { - unset($handles[$key]); - } - } - // If the list is empty, we may render a "No Projects" tag. if (!$handles) { if (strlen($this->noDataString)) { diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 7dae0fad03..360c0fbf3f 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -218,6 +218,7 @@ final class PhrictionDocumentController $prop_list = new PHUIPropertyGroupView(); $prop_list->addPropertyList($properties); } + $prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list); $page_content = id(new PHUIDocumentViewPro()) ->setHeader($header) diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index f70b77f99a..c3ac7fc016 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -381,10 +381,13 @@ final class PhabricatorPolicyFilter extends Phobject { $reject = $extended_objects[$extended_key]; unset($extended_objects[$extended_key]); - // TODO: This isn't as user-friendly as it could be. It's possible - // that we're rejecting this object for multiple capability/policy - // failures, though. - $this->rejectObject($reject, false, ''); + // It's possible that we're rejecting this object for multiple + // capability/policy failures, but just pick the first one to show + // to the user. + $first_capability = head($capabilities); + $first_policy = $object_in->getPolicy($first_capability); + + $this->rejectObject($reject, $first_policy, $first_capability); } } } diff --git a/src/applications/ponder/application/PhabricatorPonderApplication.php b/src/applications/ponder/application/PhabricatorPonderApplication.php index 94a06edffa..460acd989e 100644 --- a/src/applications/ponder/application/PhabricatorPonderApplication.php +++ b/src/applications/ponder/application/PhabricatorPonderApplication.php @@ -49,8 +49,6 @@ final class PhabricatorPonderApplication extends PhabricatorApplication { => 'PonderAnswerCommentController', 'answer/history/(?P\d+)/' => 'PonderAnswerHistoryController', - 'answer/helpful/(?Padd|remove)/(?P[1-9]\d*)/' - => 'PonderHelpfulSaveController', 'question/edit/(?:(?P\d+)/)?' => 'PonderQuestionEditController', 'question/create/' diff --git a/src/applications/ponder/constants/PonderVote.php b/src/applications/ponder/constants/PonderVote.php deleted file mode 100644 index c3e9745eec..0000000000 --- a/src/applications/ponder/constants/PonderVote.php +++ /dev/null @@ -1,8 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - $action = $request->getURIData('action'); - - $answer = id(new PonderAnswerQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needViewerVotes(true) - ->executeOne(); - - if (!$answer) { - return new Aphront404Response(); - } - - $edit_uri = '/Q'.$answer->getQuestionID(); - - switch ($action) { - case 'add': - $newvote = PonderVote::VOTE_UP; - break; - case 'remove': - $newvote = PonderVote::VOTE_NONE; - break; - } - - if ($request->isFormPost()) { - - $editor = id(new PonderVoteEditor()) - ->setVotable($answer) - ->setActor($viewer) - ->setVote($newvote) - ->saveVote(); - - return id(new AphrontRedirectResponse())->setURI($edit_uri); - } - - if ($action == 'add') { - $title = pht('Mark Answer as Helpful?'); - $body = pht('This answer will be marked as helpful.'); - $button = pht('Mark Helpful'); - } else { - $title = pht('Remove Helpful From Answer?'); - $body = pht('This answer will no longer be marked as helpful.'); - $button = pht('Remove Helpful'); - } - - $dialog = $this->newDialog(); - $dialog->setTitle($title); - $dialog->appendChild($body); - $dialog->addCancelButton($edit_uri); - $dialog->addSubmitButton($button); - - return id(new AphrontDialogResponse())->setDialog($dialog); - } -} diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 38ca4cad30..1b5b21e464 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -31,6 +31,7 @@ final class PonderQuestionViewController extends PonderController { $header->setHeader($question->getTitle()); $header->setUser($viewer); $header->setPolicyObject($question); + $header->setProfileHeader(true); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $header->setStatus('fa-square-o', 'bluegrey', pht('Open')); @@ -43,7 +44,32 @@ final class PonderQuestionViewController extends PonderController { } $actions = $this->buildActionListView($question); - $properties = $this->buildPropertyListView($question, $actions); + $properties = $this->buildPropertyListView($question); + $details = $this->buildDetailsPropertyView($question); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $question, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = '/question/edit/'.$question->getID().'/'; + $edit_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Edit')) + ->setHref($this->getApplicationURI($edit_uri)) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $action_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Actions')) + ->setHref('#') + ->setIcon('fa-bars') + ->setDropdownMenu($actions); + + $header->addActionLink($action_button); + $header->addActionLink($edit_button); $content_id = celerity_generate_unique_node_id(); $timeline = $this->buildTransactionTimeline( @@ -55,11 +81,14 @@ final class PonderQuestionViewController extends PonderController { $add_comment = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) + ->setFullWidth(true) ->setShowPreview(false) - ->setHeaderText(pht('Question Comment')) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); + $add_comment = phutil_tag_div( + 'ponder-question-add-comment-view', $add_comment); + $comment_view = phutil_tag( 'div', array( @@ -75,38 +104,61 @@ final class PonderQuestionViewController extends PonderController { ->setContentID($content_id) ->setCount(count($xactions)); - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties) - ->appendChild($footer); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addTextCrumb('Q'.$id, '/Q'.$id); + $crumbs->setBorder(true); $answer_wiki = null; if ($question->getAnswerWiki()) { - $answer = phutil_tag_div('mlt mlb msr msl', $question->getAnswerWiki()); + $wiki = new PHUIRemarkupView($viewer, $question->getAnswerWiki()); + $wiki_header = phutil_tag( + 'div', + array( + 'class' => 'ponder-answer-wiki-header', + ), + pht('Answer Summary')); + $answer_wiki = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Answer Summary')) - ->setColor(PHUIObjectBoxView::COLOR_BLUE) - ->appendChild($answer); + ->setBackground(PHUIObjectBoxView::BLUE) + ->appendChild($wiki_header) + ->appendChild($wiki) + ->addClass('ponder-answer-wiki'); } - return $this->buildApplicationPage( + require_celerity_resource('ponder-view-css'); + + $ponder_content = phutil_tag( + 'div', array( - $crumbs, - $object_box, + 'class' => 'ponder-question-content', + ), + array( + $details, + $footer, $comment_view, $answer_wiki, $answers, $answer_add_panel, - ), - array( - 'title' => 'Q'.$question->getID().' '.$question->getTitle(), - 'pageObjects' => array_merge( - array($question->getPHID()), - mpull($question->getAnswers(), 'getPHID')), )); + + $ponder_view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setMainColumn($ponder_content) + ->setSideColumn($properties) + ->addClass('ponder-question-view'); + + $page_objects = array_merge( + array($question->getPHID()), + mpull($question->getAnswers(), 'getPHID')); + + return $this->newPage() + ->setTitle('Q'.$question->getID().' '.$question->getTitle()) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs($page_objects) + ->appendChild( + array( + $ponder_view, + )); } private function buildActionListView(PonderQuestion $question) { @@ -123,14 +175,6 @@ final class PonderQuestionViewController extends PonderController { ->setUser($viewer) ->setObject($question); - $view->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-pencil') - ->setName(pht('Edit Question')) - ->setHref($this->getApplicationURI("/question/edit/{$id}/")) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht('Close Question'); $icon = 'fa-check-square-o'; @@ -157,49 +201,89 @@ final class PonderQuestionViewController extends PonderController { } private function buildPropertyListView( - PonderQuestion $question, - PhabricatorActionListView $actions) { + PonderQuestion $question) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($question) - ->setActionList($actions); - - $view->addProperty( - pht('Author'), - $viewer->renderHandle($question->getAuthorPHID())); - - $view->addProperty( - pht('Created'), - phabricator_datetime($question->getDateCreated(), $viewer)); + ->setStacked(true); $view->invokeWillRenderEvent(); - $details = PhabricatorMarkupEngine::renderOneObject( - $question, - $question->getMarkupField(), - $viewer); - - if ($details) { - $view->addSectionHeader( - pht('Details'), - PHUIPropertyListView::ICON_SUMMARY); - - $view->addTextContent( - array( - phutil_tag( - 'div', - array( - 'class' => 'phabricator-remarkup', - ), - $details), - )); + if (!$view->hasAnyProperties()) { + return null; } + $view = id(new PHUIObjectBoxView()) + ->appendChild($view) + ->setBackground(PHUIObjectBoxView::GREY) + ->addClass('ponder-view-properties'); + return $view; } + private function buildDetailsPropertyView( + PonderQuestion $question) { + $viewer = $this->getViewer(); + + $question_details = PhabricatorMarkupEngine::renderOneObject( + $question, + $question->getMarkupField(), + $viewer); + + if (!$question_details) { + $question_details = phutil_tag( + 'em', + array(), + pht('No further details for this question.')); + } + + $asker = $viewer->renderHandle($question->getAuthorPHID())->render(); + $date = phabricator_datetime($question->getDateCreated(), $viewer); + $asker = phutil_tag('strong', array(), $asker); + + $author = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($question->getAuthorPHID())) + ->needProfileImage(true) + ->executeOne(); + + $image_uri = $author->getProfileImageURI(); + $image_href = '/p/'.$author->getUsername(); + + $image = phutil_tag( + 'a', + array( + 'class' => 'ponder-details-author-image', + 'style' => 'background-image: url('.$image_uri.');', + 'href' => $image_href, + )); + + $details_header = phutil_tag( + 'div', + array( + 'class' => 'ponder-details-subtitle', + ), + array( + $image, + pht('Asked by %s on %s.', $asker, $date), + )); + + $details = phutil_tag( + 'div', + array( + 'class' => 'ponder-detail-view', + ), + array( + $details_header, + phutil_tag_div('phabricator-remarkup', $question_details), + )); + + + return $details; + } + /** * This is fairly non-standard; building N timelines at once (N = number of * answers) is tricky business. @@ -211,32 +295,43 @@ final class PonderQuestionViewController extends PonderController { $viewer = $this->getViewer(); $answers = $question->getAnswers(); - $author_phids = mpull($answers, 'getAuthorPHID'); - $handles = $this->loadViewerHandles($author_phids); - $answers_sort = array_reverse(msort($answers, 'getVoteCount')); + if ($answers) { + $author_phids = mpull($answers, 'getAuthorPHID'); + $handles = $this->loadViewerHandles($author_phids); - $view = array(); - foreach ($answers_sort as $answer) { - $id = $answer->getID(); - $handle = $handles[$answer->getAuthorPHID()]; + $view = array(); + foreach ($answers as $answer) { + $id = $answer->getID(); + $handle = $handles[$answer->getAuthorPHID()]; - $timeline = $this->buildTransactionTimeline( - $answer, - id(new PonderAnswerTransactionQuery()) - ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); - $xactions = $timeline->getTransactions(); + $timeline = $this->buildTransactionTimeline( + $answer, + id(new PonderAnswerTransactionQuery()) + ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); + $xactions = $timeline->getTransactions(); - $view[] = id(new PonderAnswerView()) - ->setUser($viewer) - ->setAnswer($answer) - ->setTransactions($xactions) - ->setTimeline($timeline) - ->setHandle($handle); + $view[] = id(new PonderAnswerView()) + ->setUser($viewer) + ->setAnswer($answer) + ->setTransactions($xactions) + ->setTimeline($timeline) + ->setHandle($handle); + } + + $header = id(new PHUIHeaderView()) + ->setHeader('Answers'); + + + return id(new PHUIBoxView()) + ->addClass('ponder-answer-section') + ->appendChild($header) + ->appendChild($view); } - return $view; + return null; + } } diff --git a/src/applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php b/src/applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php deleted file mode 100644 index 74447dbf0d..0000000000 --- a/src/applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php +++ /dev/null @@ -1,105 +0,0 @@ -answer = $answer; - return $this; - } - - public function setVotable($votable) { - $this->votable = $votable; - return $this; - } - - public function setVote($vote) { - $this->vote = $vote; - return $this; - } - - public function saveVote() { - $actor = $this->requireActor(); - if (!$this->votable) { - throw new PhutilInvalidStateException('setVotable'); - } - - $votable = $this->votable; - $newvote = $this->vote; - - // prepare vote add, or update if this user is amending an - // earlier vote - $editor = id(new PhabricatorEdgeEditor()) - ->addEdge( - $actor->getPHID(), - $votable->getUserVoteEdgeType(), - $votable->getVotablePHID(), - array('data' => $newvote)) - ->removeEdge( - $actor->getPHID(), - $votable->getUserVoteEdgeType(), - $votable->getVotablePHID()); - - $conn = $votable->establishConnection('w'); - $trans = $conn->openTransaction(); - $trans->beginReadLocking(); - - $votable->reload(); - $curvote = (int)PhabricatorEdgeQuery::loadSingleEdgeData( - $actor->getPHID(), - $votable->getUserVoteEdgeType(), - $votable->getVotablePHID()); - - if (!$curvote) { - $curvote = PonderVote::VOTE_NONE; - } - - // Adjust votable's score by this much. - $delta = $newvote - $curvote; - - queryfx($conn, - 'UPDATE %T as t - SET t.voteCount = t.voteCount + %d - WHERE t.PHID = %s', - $votable->getTableName(), - $delta, - $votable->getVotablePHID()); - - $editor->save(); - - $trans->endReadLocking(); - $trans->saveTransaction(); - } -} diff --git a/src/applications/ponder/query/PonderAnswerQuery.php b/src/applications/ponder/query/PonderAnswerQuery.php index 4e874aeb60..2901f4d6a5 100644 --- a/src/applications/ponder/query/PonderAnswerQuery.php +++ b/src/applications/ponder/query/PonderAnswerQuery.php @@ -8,9 +8,6 @@ final class PonderAnswerQuery private $authorPHIDs; private $questionIDs; - private $needViewerVotes; - - public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -31,11 +28,6 @@ final class PonderAnswerQuery return $this; } - public function needViewerVotes($need_viewer_votes) { - $this->needViewerVotes = $need_viewer_votes; - return $this; - } - protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); @@ -86,25 +78,6 @@ final class PonderAnswerQuery $answer->attachQuestion($question); } - if ($this->needViewerVotes) { - $viewer_phid = $this->getViewer()->getPHID(); - - $etype = PonderAnswerHasVotingUserEdgeType::EDGECONST; - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(mpull($answers, 'getPHID')) - ->withDestinationPHIDs(array($viewer_phid)) - ->withEdgeTypes(array($etype)) - ->needEdgeData(true) - ->execute(); - foreach ($answers as $answer) { - $user_edge = idx( - $edges[$answer->getPHID()][$etype], - $viewer_phid, - array()); - $answer->attachUserVote($viewer_phid, idx($user_edge, 'data', 0)); - } - } - return $answers; } diff --git a/src/applications/ponder/query/PonderQuestionQuery.php b/src/applications/ponder/query/PonderQuestionQuery.php index 9762f925f5..b2de14e52c 100644 --- a/src/applications/ponder/query/PonderQuestionQuery.php +++ b/src/applications/ponder/query/PonderQuestionQuery.php @@ -98,7 +98,6 @@ final class PonderQuestionQuery $aquery = id(new PonderAnswerQuery()) ->setViewer($this->getViewer()) ->setOrderVector(array('-id')) - ->needViewerVotes(true) ->withQuestionIDs(mpull($questions, 'getID')); $answers = $aquery->execute(); diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index 4da222d5a5..025c020637 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -4,7 +4,6 @@ final class PonderAnswer extends PonderDAO implements PhabricatorApplicationTransactionInterface, PhabricatorMarkupInterface, - PonderVotableInterface, PhabricatorPolicyInterface, PhabricatorFlaggableInterface, PhabricatorSubscribableInterface, @@ -20,12 +19,9 @@ final class PonderAnswer extends PonderDAO protected $status; protected $voteCount; - private $vote; private $question = self::ATTACHABLE; private $comments; - private $userVotes = array(); - public static function initializeNewAnswer( PhabricatorUser $actor, PonderQuestion $question) { @@ -57,23 +53,6 @@ final class PonderAnswer extends PonderDAO return '/Q'.$this->getQuestionID().'#A'.$this->getID(); } - public function setUserVote($vote) { - $this->vote = $vote['data']; - if (!$this->vote) { - $this->vote = PonderVote::VOTE_NONE; - } - return $this; - } - - public function attachUserVote($user_phid, $vote) { - $this->vote = $vote; - return $this; - } - - public function getUserVote() { - return $this->vote; - } - public function setComments($comments) { $this->comments = $comments; return $this; @@ -181,15 +160,6 @@ final class PonderAnswer extends PonderDAO return (bool)$this->getID(); } - // votable interface - public function getUserVoteEdgeType() { - return PonderVotingUserHasAnswerEdgeType::EDGECONST; - } - - public function getVotablePHID() { - return $this->getPHID(); - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/ponder/storage/PonderVotableInterface.php b/src/applications/ponder/storage/PonderVotableInterface.php deleted file mode 100644 index 6350a20e80..0000000000 --- a/src/applications/ponder/storage/PonderVotableInterface.php +++ /dev/null @@ -1,8 +0,0 @@ -getAuthorPHID() == $viewer->getPHID()) { - $box_style = 'display: none;'; - $open_link = javelin_tag( - 'a', - array( - 'sigil' => 'reveal-content', - 'class' => 'mml', - 'id' => $hide_action_id, - 'href' => '#', - 'meta' => array( - 'showIDs' => array($show_action_id), - 'hideIDs' => array($hide_action_id), - ), - ), - pht('Add an answer.')); - $own_question = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) - ->setID($hide_action_id) - ->appendChild( - pht( - 'This is your own question. You are welcome to provide - an answer if you have found a resolution.')) - ->appendChild($open_link); - } - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Add Answer')); + ->setHeader(pht('New Answer')) + ->addClass('ponder-add-answer-header'); $form = new AphrontFormView(); $form ->setUser($this->user) ->setAction($this->actionURI) ->setWorkflow(true) + ->setFullWidth(true) ->addHiddenInput('question_id', $question->getID()) ->appendChild( id(new PhabricatorRemarkupControl()) @@ -103,22 +77,14 @@ final class PonderAddAnswerView extends AphrontView { } $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->appendChild($form); + ->appendChild($form) + ->setBackground(PHUIObjectBoxView::GREY) + ->addClass('ponder-add-answer-view'); if ($info_panel) { $box->setInfoView($info_panel); } - $box = phutil_tag( - 'div', - array( - 'style' => $box_style, - 'class' => 'mlt', - 'id' => $show_action_id, - ), - $box); - - return array($own_question, $box); + return array($header, $box); } } diff --git a/src/applications/ponder/view/PonderAnswerView.php b/src/applications/ponder/view/PonderAnswerView.php index c0e90f7eca..474661381c 100644 --- a/src/applications/ponder/view/PonderAnswerView.php +++ b/src/applications/ponder/view/PonderAnswerView.php @@ -95,7 +95,7 @@ final class PonderAnswerView extends AphrontTagView { $content = phutil_tag( 'div', array( - 'class' => 'phabricator-remarkup mlt mlb msr msl', + 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $answer, @@ -110,26 +110,14 @@ final class PonderAnswerView extends AphrontTagView { ->setContentID($content_id) ->setCount(count($this->transactions)); - $votes = $answer->getVoteCount(); - $vote_class = null; - if ($votes > 0) { - $vote_class = 'ponder-footer-action-helpful'; - } - $icon = id(new PHUIIconView()) - ->setIcon('fa-thumbs-up msr'); - $helpful = phutil_tag( - 'span', - array( - 'class' => 'ponder-footer-action '.$vote_class, - ), - array($icon, $votes)); - $footer->addAction($helpful); + $content = phutil_tag_div( + 'ponder-answer-content', array($anchor, $content, $footer)); $answer_view = id(new PHUIObjectBoxView()) ->setHeader($header) - ->appendChild($anchor) - ->appendChild($content) - ->appendChild($footer); + ->setBackground(PHUIObjectBoxView::GREY) + ->addClass('ponder-answer') + ->appendChild($content); $comment_view = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) @@ -170,28 +158,6 @@ final class PonderAnswerView extends AphrontTagView { ->setUser($viewer) ->setObject($answer); - $user_marked = $answer->getUserVote(); - $can_vote = $viewer->isLoggedIn(); - - if ($user_marked) { - $helpful_uri = "/ponder/answer/helpful/remove/{$id}/"; - $helpful_icon = 'fa-times'; - $helpful_text = pht('Remove Helpful'); - } else { - $helpful_uri = "/ponder/answer/helpful/add/{$id}/"; - $helpful_icon = 'fa-thumbs-up'; - $helpful_text = pht('Mark as Helpful'); - } - - $view->addAction( - id(new PhabricatorActionView()) - ->setIcon($helpful_icon) - ->setName($helpful_text) - ->setHref($helpful_uri) - ->setRenderAsForm(true) - ->setDisabled(!$can_vote) - ->setWorkflow($can_vote)); - $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') diff --git a/src/applications/ponder/view/PonderFooterView.php b/src/applications/ponder/view/PonderFooterView.php index be33863a9a..6d814d2b71 100644 --- a/src/applications/ponder/view/PonderFooterView.php +++ b/src/applications/ponder/view/PonderFooterView.php @@ -36,12 +36,8 @@ final class PonderFooterView extends AphrontTagView { $content_id = $this->contentID; if ($this->count == 0) { - $icon = id(new PHUIIconView()) - ->setIcon('fa-comments msr'); $text = pht('Add a Comment'); } else { - $icon = id(new PHUIIconView()) - ->setIcon('fa-comments msr'); $text = pht('Show %d Comment(s)', new PhutilNumber($this->count)); } @@ -58,7 +54,7 @@ final class PonderFooterView extends AphrontTagView { 'hideIDs' => array($hide_action_id), ), ), - array($icon, $text)); + array($text)); $show_action = javelin_tag( 'a', @@ -73,7 +69,7 @@ final class PonderFooterView extends AphrontTagView { 'hideIDs' => array($content_id, $show_action_id), ), ), - array($icon, pht('Hide Comments'))); + array(pht('Hide Comments'))); $actions[] = $hide_action; $actions[] = $show_action; diff --git a/src/applications/project/controller/PhabricatorProjectBoardManageController.php b/src/applications/project/controller/PhabricatorProjectBoardManageController.php index db4dfe31be..75bff07106 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardManageController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardManageController.php @@ -152,6 +152,11 @@ final class PhabricatorProjectBoardManageController foreach ($columns as $column) { $column_id = $column->getID(); + $proxy = $column->getProxy(); + if ($proxy && !$proxy->isMilestone()) { + continue; + } + $detail_uri = "/project/board/{$board_id}/column/{$column_id}/"; $item = id(new PHUIObjectItemView()) diff --git a/src/applications/project/controller/PhabricatorProjectColumnEditController.php b/src/applications/project/controller/PhabricatorProjectColumnEditController.php index 9f880b9515..fb0c7b0910 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnEditController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnEditController.php @@ -81,14 +81,15 @@ final class PhabricatorProjectColumnEditController $xactions = array(); + $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; + $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; + if (!$column->getProxy()) { - $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; $xactions[] = id(new PhabricatorProjectColumnTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); } - $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; $xactions[] = id(new PhabricatorProjectColumnTransaction()) ->setTransactionType($type_limit) ->setNewValue($v_limit); @@ -97,6 +98,7 @@ final class PhabricatorProjectColumnEditController $editor = id(new PhabricatorProjectColumnTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->applyTransactions($column, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 8220592074..ddbd0ffb5b 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -87,6 +87,7 @@ final class PhabricatorProjectProfileController $home = id(new PHUITwoColumnView()) ->setHeader($header) + ->setFluid(true) ->addClass('project-view-home') ->setMainColumn( array( diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 087e9c1789..7be0af2c2f 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -116,6 +116,16 @@ final class ProjectBoardTaskCard extends Phobject { } $project_handles = $this->getProjectHandles(); + + // Remove any archived projects from the list. + if ($project_handles) { + foreach ($project_handles as $key => $handle) { + if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) { + unset($project_handles[$key]); + } + } + } + if ($project_handles) { $tag_list = id(new PHUIHandleTagListView()) ->setSlim(true) diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index c2ed8023f3..9896a8ef29 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2038,6 +2038,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($service_phid)) ->needBindings(true) + ->needProperties(true) ->executeOne(); if (!$service) { throw new Exception( @@ -2046,7 +2047,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO 'be loaded.')); } - $service_type = $service->getServiceType(); + $service_type = $service->getServiceImplementation(); if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) { throw new Exception( pht( diff --git a/src/applications/search/field/PhabricatorSearchTextField.php b/src/applications/search/field/PhabricatorSearchTextField.php index e3cf1f845c..915f22a6e9 100644 --- a/src/applications/search/field/PhabricatorSearchTextField.php +++ b/src/applications/search/field/PhabricatorSearchTextField.php @@ -15,4 +15,8 @@ final class PhabricatorSearchTextField return new AphrontFormTextControl(); } + protected function newConduitParameterType() { + return new ConduitStringParameterType(); + } + } diff --git a/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php b/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php index 45d7bce1a6..ee11dcdd06 100644 --- a/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php +++ b/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php @@ -184,6 +184,7 @@ final class PhabricatorSpacesNamespaceQuery $space_phid) { $viewer_spaces = self::getViewerSpaces($viewer); + $viewer_spaces = msort($viewer_spaces, 'getNamespaceName'); $map = array(); foreach ($viewer_spaces as $space) { @@ -200,7 +201,6 @@ final class PhabricatorSpacesNamespaceQuery $space->getMonogram(), $space->getNamespaceName()); } - asort($map); return $map; } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index f8dc0ee175..ce7ec1c437 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -23,6 +23,7 @@ abstract class PhabricatorEditEngine private $isCreate; private $editEngineConfiguration; private $contextParameters = array(); + private $targetObject; final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -61,6 +62,22 @@ abstract class PhabricatorEditEngine return true; } + public function isEngineExtensible() { + return true; + } + + /** + * Force the engine to edit a particular object. + */ + public function setTargetObject($target_object) { + $this->targetObject = $target_object; + return $this; + } + + public function getTargetObject() { + return $this->targetObject; + } + /* -( Managing Fields )---------------------------------------------------- */ @@ -94,7 +111,12 @@ abstract class PhabricatorEditEngine $fields = mpull($fields, null, 'getKey'); - $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); + if ($this->isEngineExtensible()) { + $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); + } else { + $extensions = array(); + } + foreach ($extensions as $extension) { $extension->setViewer($viewer); @@ -720,23 +742,28 @@ abstract class PhabricatorEditEngine break; } - $id = $request->getURIData('id'); + $object = $this->getTargetObject(); + if (!$object) { + $id = $request->getURIData('id'); - if ($id) { - $this->setIsCreate(false); - $object = $this->newObjectFromID($id, $capabilities); - if (!$object) { - return new Aphront404Response(); + if ($id) { + $this->setIsCreate(false); + $object = $this->newObjectFromID($id, $capabilities); + if (!$object) { + return new Aphront404Response(); + } + } else { + // Make sure the viewer has permission to create new objects of + // this type if we're going to create a new object. + if ($require_create) { + $this->requireCreateCapability(); + } + + $this->setIsCreate(true); + $object = $this->newEditableObject(); } } else { - // Make sure the viewer has permission to create new objects of - // this type if we're going to create a new object. - if ($require_create) { - $this->requireCreateCapability(); - } - - $this->setIsCreate(true); - $object = $this->newEditableObject(); + $id = $object->getID(); } $this->validateObject($object); @@ -831,7 +858,7 @@ abstract class PhabricatorEditEngine $template = $object->getApplicationTransactionTemplate(); $validation_exception = null; - if ($request->isFormPost()) { + if ($request->isFormPost() && $request->getBool('editEngine')) { $submit_fields = $fields; foreach ($submit_fields as $key => $field) { @@ -1044,7 +1071,8 @@ abstract class PhabricatorEditEngine $request = $controller->getRequest(); $form = id(new AphrontFormView()) - ->setUser($viewer); + ->setUser($viewer) + ->addHiddenInput('editEngine', 'true'); foreach ($this->contextParameters as $param) { $form->addHiddenInput($param, $request->getStr($param)); diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 7d50f8495c..8084ea302f 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -456,8 +456,14 @@ abstract class PhabricatorApplicationTransaction return null; } + $object = $this->getObject(); + + if (!($object instanceof PhabricatorCustomFieldInterface)) { + return null; + } + $field = PhabricatorCustomField::getObjectField( - $this->getObject(), + $object, PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if (!$field) { diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 819ab6c8e4..ff1eaae553 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -19,6 +19,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { private $objectPHID; private $headerText; private $noPermission; + private $fullWidth; private $currentVersion; private $versionedDraft; @@ -102,6 +103,11 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { return $this; } + public function setFullWidth($fw) { + $this->fullWidth = $fw; + return $this; + } + public function setCommentActions(array $comment_actions) { assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction'); $this->commentActions = $comment_actions; @@ -209,6 +215,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { ->setUser($this->getUser()) ->addSigil('transaction-append') ->setWorkflow(true) + ->setFullWidth($this->fullWidth) ->setMetadata( array( 'objectPHID' => $this->getObjectPHID(), diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index c20999601e..b3687b85e8 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -107,6 +107,8 @@ final class PhabricatorTypeaheadModularDatasourceController if (($offset + (2 * $limit)) < $hard_limit) { $next_uri = id(new PhutilURI($request->getRequestURI())) ->setQueryParam('offset', $offset + $limit) + ->setQueryParam('q', $query) + ->setQueryParam('raw', $raw_query) ->setQueryParam('format', 'html'); $next_link = javelin_tag( diff --git a/src/docs/user/configuration/cluster.diviner b/src/docs/user/configuration/cluster.diviner index 708376ec23..c11565dc4d 100644 --- a/src/docs/user/configuration/cluster.diviner +++ b/src/docs/user/configuration/cluster.diviner @@ -1,7 +1,7 @@ @title User Guide: Phabricator Clusters @group config -Guide on scaling Phabricator across multiple machines, for large installs. +Guide on scaling Phabricator across multiple machines. Overview ======== @@ -9,23 +9,42 @@ Overview IMPORTANT: Phabricator clustering is in its infancy and does not work at all yet. This document is mostly a placeholder. -Locking Services -================ +IMPORTANT: DO NOT CONFIGURE CLUSTER SERVICES UNLESS YOU HAVE **TWENTY YEARS OF +EXPERIENCE WITH PHABRICATOR** AND **A MINIMUM OF 17 PHABRICATOR PHDs**. YOU +WILL BREAK YOUR INSTALL AND BE UNABLE TO REPAIR IT. -Because cluster configuration is defined in Phabricator itself, an attacker -who compromises an account that can edit the cluster definition has significant -power. For example, the attacker might be able to configure Phabricator to -replicate the database to a server they control. +See also @{article:Almanac User Guide}. -To mitigate this attack, services in Almanac can be locked to prevent them -from being edited from the web UI. An attacker would then need significantly -greater access (to the CLI, or directly to the database) in order to change -the cluster configuration. -You should normally keep cluster services in a locked state, and unlock them -only to edit them. Once you're finished making changes, lock the service again. -The web UI will warn you when you're viewing an unlocked cluster service, as -a reminder that you should lock it again once you're finished editing. +Managing Cluster Configuration +============================== -For details on how to lock and unlock a service, see -@{article:Almanac User Guide}. +Cluster configuration is managed primarily from the **Almanac** application. + +To define cluster services and create or edit cluster configuration, you must +have the **Can Manage Cluster Services** application permission in Almanac. If +you do not have this permission, all cluster services and all connected devices +will be locked and not editable. + +The **Can Manage Cluster Services** permission is stronger than service and +device policies, and overrides them. You can never edit a cluster service if +you don't have this permission, even if the **Can Edit** policy on the service +itself is very permissive. + + +Locking Cluster Configuration +============================= + +IMPORTANT: Managing cluster services is **dangerous** and **fragile**. + +If you make a mistake, you can break your install. Because the install is +broken, you will be unable to load the web interface in order to repair it. + +IMPORTANT: Currently, broken clusters must be repaired by manually fixing them +in the database. There are no instructions available on how to do this, and no +tools to help you. Do not configure cluster services. + +If an attacker gains access to an account with permission to manage cluster +services, they can add devices they control as database servers. These servers +will then receive sensitive data and traffic, and allow the attacker to +escalate their access and completely compromise an install. diff --git a/src/docs/user/userguide/almanac.diviner b/src/docs/user/userguide/almanac.diviner index c6f5ef2971..5a820c6ae2 100644 --- a/src/docs/user/userguide/almanac.diviner +++ b/src/docs/user/userguide/almanac.diviner @@ -1,40 +1,179 @@ @title Almanac User Guide @group userguide -Using Almanac to manage services. +Using Almanac to manage devices and services. -= Overview = +Overview +======== IMPORTANT: Almanac is a prototype application. See @{article:User Guide: Prototype Applications}. -Locking and Unlocking Services -============================== +Almanac is a device and service inventory application. It allows you to create +lists of //devices// and //services// that humans and other applications can +use to keep track of what is running where. -Services can be locked to prevent edits from the web UI. This primarily hardens -Almanac against attacks involving account compromise. Notably, locking cluster -services prevents an attacker from modifying the Phabricator cluster definition. -For more details on this scenario, see -@{article:User Guide: Phabricator Clusters}. +At a very high level, Almanac can be thought of as a bit like a DNS server. +Callers ask it for information about services, and it responds with details +about which devices host those services. However, it can respond to a broader +range of queries and provide more detailed responses than DNS alone can. -Beyond hardening cluster definitions, you might also want to lock a service to -prevent accidental edits. +Today, the primary use cases for Almanac involve configuring Phabricator +itself: Almanac is used to configure Phabricator to operate in a cluster setup, +and to expose hardware to Drydock so it can run build and integration tasks. -To lock a service, run: +Beyond internal uses, Almanac is a general-purpose service and device inventory +application and can be used to configure and manage other types of service and +hardware inventories, but these use cases are currently considered experimental +and you should be exercise caution in pursuing them. - phabricator/ $ ./bin/almanac lock -To unlock a service later, run: +Example: Drydock Build Pool +================================ - phabricator/ $ ./bin/almanac unlock +Here's a quick example of how you might configure Almanac to solve a real-world +problem. This section describes configuration at a high level to give you an +introduction to Almanac concepts and a better idea of how the pieces fit +together. -Locking a service also locks all of the service's bindings and properties, as -well as the devices connected to the service. Generally, no part of the -service definition can be modified while it is locked. +In this scenario, we want to use Drydock to run some sort of build process. To +do this, Drydock needs hardware to run on. We're going to use Almanac to tell +Drydock about the hardware it should use. -Devices (and their properties) will remain locked as long as they are bound to -at least one locked service. To edit a device, you'll need to unlock all the -services it is bound to. +In this scenario, Almanac will work a bit like a DNS server. When we're done, +Drydock will be able to query Almanac for information about a service (like +`build.mycompany.com`) and get back information about which hosts are part of +that service and where it should connect to. -Locked services and devices will show that they are locked in the web UI, and -editing options will be unavailable. +Before getting started, we need to create a **network**. For simplicity, let's +suppose everything will be connected through the public internet. If you +haven't already, you'd create a "Public Internet" network first. + +Once we have a network, we create the actual physical or virtual hosts by +launching instances in EC2, or racking and powering on some servers, or already +having some hardware on hand we want to use. We set the hosts up normally and +connect them to the internet or network. + +After the hosts exist, we add them to Almanac as **devices**, like +`build001.mycompany.com`, `build002.mycompany.com`, and so on. In Almanac, +devices are usually physical or virtual hosts, although you could also use it +to inventory other types of devices and hardware. + +For each **device**, we add an **interface**. This is just an address and port +on a particular network. Since we're going to connect to these hosts over +SSH, we'll add interfaces on the standard SSH port 22. An example configuration +might look a little bit like this: + +| Device | Network | Address | Port | +|--------|---------|---------|------| +| `build001.mycompany.com` | Public Internet | 58.8.9.10 | 22 +| `build002.mycompany.com` | Public Internet | 58.8.9.11 | 22 +| ... | Public Internet | ... | 22 + +Now, we create the **service**. This is what we'll tell Drydock about, and +it can query for information about this service to find connected devices. +Here, we'll call it `build.mycompany.com`. + +After creating the service, add **bindings** to the interfaces we configured +above. This will tell Drydock where it should actually connect to. + +Once this is complete, we're done in Almanac and can continue configuration in +Drydock, which is outside the scope of this example. Once everything is fully +configured, this is how Almanac will be used by Drydock: + + - Drydock will query information about `build.mycompany.com` from Almanac. + - Drydock will get back a list of bound interfaces, among other data. + - The interfaces provide information about addresses and ports that Drydock + can use to connect to the actual devices. + +You can now add and remove devices to the pool by binding them and unbinding +them from the service. + + +Concepts +======== + +The major concepts in Almanac are **devices*, **interfaces**, **services**, +**bindings**, **networks**, and **namespaces**. + +**Devices**: Almanac devices represent physical or virtual devices. +Usually, they are hosts (like `web001.mycompany.net`), although you could +use devices to keep inventory of any other kind of device or physical asset +(like phones, laptops, or office chairs). + +Each device has a name, and may have properties and interfaces. + +**Interfaces**: Interfaces are listening address/port combinations on devices. +For example, if you have a webserver host device named `web001.mycompany.net`, +you might add an interface on port `80`. + +Interfaces tell users and applications where they should connect to to access +services and devices. + +**Services**: These are named services like `build.mycompany.net` that work +a bit like DNS. Humans or other applications can look up a service to find +configuration information and learn which devices are hosting the service. + +Each service has a name, and may have properties and bindings. + +**Bindings**: Bindings are connections between services and interfaces. They +tell callers which devices host a named service. + +**Networks**: Networks allow Almanac to distingiush between addresses on +different networks, like VPNs vs the public internet. + +If you have hosts in different VPNs or on private networks, you might have +multiple devices which share the same IP address (like `10.0.0.3`). Networks +allow Almanac to distinguish between devices with the same address on different +sections of the network. + +**Namespaces**: Namespaces let you control who is permitted to create devices +and services with particular names. For example, the namespace `mycompany.com` +controls who can create services with names like `a.mycompany.com` and +`b.mycompany.com`. + + +Namespaces +========== + +Almanac namespaces allow you to control who can create services and devices +with certain names. + +If you keep a list of cattle as devices with names like +`cow001.herd.myranch.com`, `cow002.herd.myranch.moo`, you might have some +applications which query for all devices in `*.herd.myranch.moo`, and thus +want to limit who can create devices there in order to prevent mistakes. + +If a namespace like `herd.myranch.moo` exists, users must have permission to +edit the namespace in order to create new services, devices, or namespaces +within it. For example, a user can not create `cow003.herd.myranch.moo` if +they do not have edit permission on the `herd.myranch.moo` namespace. + +When you try to create a `cow003.herd.myranch.moo` service (or rename an +existing service to have that name), Almanac looks for these namespaces, then +checks the policy of the first one it finds: + +| Namespace | +|----|----- +| `cow003.herd.ranch.moo` | //"Nearest" namespace, considered first.// +| `herd.ranch.moo` | | +| `ranch.moo` | | +| `moo` | //"Farthest" namespace, considered last.// + +Note that namespaces treat names as lists of domain parts, not as strict +substrings, so the namespace `herd.myranch.moo` does not prevent +someone from creating `goatherd.myranch.moo` or `goat001.goatherd.myranch.moo`. +The name `goatherd.myranch.moo` is not part of the `herd.myranch.moo` namespace +because the initial subdomain differs. + +If a name belongs to multiple namespaces, the policy of the nearest namespace +is controlling. For example, if `myranch.moo` has a very restrictive edit +policy but `shed.myranch.moo` has a more open one, users can create devices and +services like `rake.shed.myranch.moo` as long as they can pass the policy check +for `shed.myranch.moo`, even if they do not have permission under the policy +for `myranch.moo`. + +Users can edit services and devices within a namespace if they have edit +permission on the service or device itself, as long as they don't try to rename +the service or device to move it into a namespace they don't have permission +to access. diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php index 804e5b27bb..64f7665827 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php @@ -173,7 +173,7 @@ final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler { // Don't mention the same object more than once every 10 minutes // in public channels, so we avoid spamming the chat over and over - // again for discsussions of a specific revision, for example. + // again for discussions of a specific revision, for example. $target_name = $original_message->getTarget()->getName(); if (empty($this->recentlyMentioned[$target_name])) { diff --git a/src/view/phui/PHUITwoColumnView.php b/src/view/phui/PHUITwoColumnView.php index 97ff269d83..5a47dc925a 100644 --- a/src/view/phui/PHUITwoColumnView.php +++ b/src/view/phui/PHUITwoColumnView.php @@ -5,6 +5,7 @@ final class PHUITwoColumnView extends AphrontTagView { private $mainColumn; private $sideColumn; private $display; + private $fluid; private $header; const DISPLAY_LEFT = 'phui-side-column-left'; @@ -25,12 +26,17 @@ final class PHUITwoColumnView extends AphrontTagView { return $this; } + public function setFluid($fluid) { + $this->fluid = $fluid; + return $this; + } + public function setDisplay($display) { $this->display = $display; return $this; } - public function getDisplay() { + private function getDisplay() { if ($this->display) { return $this->display; } else { @@ -43,6 +49,10 @@ final class PHUITwoColumnView extends AphrontTagView { $classes[] = 'phui-two-column-view'; $classes[] = $this->getDisplay(); + if ($this->fluid) { + $classes[] = 'phui-two-column-fluid'; + } + return array( 'class' => implode(' ', $classes), ); @@ -79,6 +89,14 @@ final class PHUITwoColumnView extends AphrontTagView { $header = phutil_tag_div('phui-two-column-header', $this->header); } - return array($header, $table); + return phutil_tag( + 'div', + array( + 'class' => 'phui-two-column-container', + ), + array( + $header, + $table, + )); } } diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php index bc7daf4ea6..5174767e28 100644 --- a/support/PhabricatorStartup.php +++ b/support/PhabricatorStartup.php @@ -135,13 +135,40 @@ final class PhabricatorStartup { $encoding = null; } - if ($encoding === 'gzip') { - $source_stream = 'compress.zlib://php://input'; - } else { - $source_stream = 'php://input'; + $input_stream = fopen('php://input', 'rb'); + if (!$input_stream) { + self::didFatal( + 'Unable to open "php://input" to read HTTP request body.'); } - self::$rawInput = (string)file_get_contents($source_stream); + if ($encoding === 'gzip') { + $ok = stream_filter_append( + $input_stream, + 'zlib.inflate', + STREAM_FILTER_READ, + array( + 'window' => 30, + )); + + if (!$ok) { + self::didFatal( + 'Failed to append gzip inflate filter to HTTP request body input '. + 'stream.'); + } + } + + $input_data = ''; + while (!feof($input_stream)) { + $read_bytes = fread($input_stream, 16 * 1024); + if ($read_bytes === false) { + self::didFatal( + 'Failed to read input bytes from HTTP request body.'); + } + $input_data .= $read_bytes; + } + fclose($input_stream); + + self::$rawInput = $input_data; } diff --git a/webroot/rsrc/css/application/base/phui-theme.css b/webroot/rsrc/css/application/base/phui-theme.css index 815f0e0027..e0bfbf36a6 100644 --- a/webroot/rsrc/css/application/base/phui-theme.css +++ b/webroot/rsrc/css/application/base/phui-theme.css @@ -61,17 +61,3 @@ background: #122916; } - -/*--- Dark "Classic Phabricator" ---------------------------------------------*/ - -.phui-theme-dark { - background-color: {$page.background.dark}; -} - -.phui-theme-dark .phui-box-border { - border-color: {$lightblueborder}; -} - -.phui-theme-dark .phui-box-grey { - background-color: {$page.background.dark}; -} diff --git a/webroot/rsrc/css/application/phame/phame.css b/webroot/rsrc/css/application/phame/phame.css index 299474fee1..d4fdeafff0 100644 --- a/webroot/rsrc/css/application/phame/phame.css +++ b/webroot/rsrc/css/application/phame/phame.css @@ -30,9 +30,8 @@ position: absolute; } -.phame-blog-description + .phui-property-list-section { +.phame-blog-description + .phui-document-view-pro-box { border-top: 1px solid rgba({$alphablue}, 0.20); - padding-top: 16px; } .phame-home-view .phui-document-view.phui-document-view-pro { diff --git a/webroot/rsrc/css/application/ponder/ponder-view.css b/webroot/rsrc/css/application/ponder/ponder-view.css index da0eab6e38..a5bd01a6b1 100644 --- a/webroot/rsrc/css/application/ponder/ponder-view.css +++ b/webroot/rsrc/css/application/ponder/ponder-view.css @@ -2,16 +2,112 @@ * @provides ponder-view-css */ -.ponder-show-comments { - text-align: center; - padding: 8px; - margin: 0 16px; - float: right; - font-weight: bold; +.ponder-question-view { background: #fff; - border-bottom: 1px solid {$blueborder}; - border-left: 1px solid {$lightblueborder}; - border-right: 1px solid {$lightblueborder}; + padding-bottom: 64px; +} + +.device-desktop .ponder-question-view.phui-two-column-view .phui-side-column { + width: 300px; +} + +.ponder-question-view .phui-object-box, +.ponder-question-view .phui-info-view { + margin-left: 0; + margin-right: 0; +} + +.device-phone .ponder-question-view .phui-profile-header.phui-header-shell + .phui-header-header { + font-size: 20px; +} + +.ponder-question-container { + border-top: 1px solid {$thinblueborder}; +} + +.ponder-question-content { + margin: 0 24px; + padding: 24px 0; + border-top: 1px solid rgba({$alphagrey}, .15); +} + +.device-phone .ponder-question-content { + margin: 0 16px; +} + +.device .ponder-view-properties { + border-left: none; + border-right: none; + border-radius: 0; +} + +.device .ponder-question-view .phui-timeline-view, +.device .ponder-question-view .phui-timeline-event-view { + margin: 0; + padding: 0; +} + +.ponder-view-properties .phui-property-list-container { + margin: 0; + padding: 0; +} + +.ponder-question-view .phui-document-container { + border: none; +} + +.ponder-view-properties .phui-property-list-stacked + .phui-property-list-properties .phui-property-list-key { + padding: 0; +} + +.ponder-view-properties .phui-property-list-stacked + .phui-property-list-properties .phui-property-list-value { + margin-bottom: 16px; + padding: 0; +} + +.phui-box.ponder-answer-wiki { + padding: 16px; + margin: 24px 0; +} + +.ponder-details-subtitle { + height: 24px; + line-height: 24px; + margin-bottom: 12px; + color: {$greytext}; + position: relative; + padding-left: 32px; +} + +.ponder-details-subtitle a { + color: {$darkgreytext}; +} + +.ponder-details-author-image { + height: 24px; + width: 24px; + background-size: 100%; + margin-right: 8px; + border-radius: 3px; + display: inline-block; + position: absolute; + top: 0; + left: 0; +} + +.ponder-detail-view .phabricator-remarkup { + margin-left: 32px; +} + +.ponder-question-content .phui-timeline-view { + padding-right: 0; +} + +.ponder-question-content .phui-timeline-view .phui-timeline-core-content { + background-color: {$lightbluebackground}; } .ponder-answer-view { @@ -27,52 +123,106 @@ margin-left: 12px; } -.ponder-answer-view .phui-header-shell { - padding-bottom: 8px; +.ponder-question-view .ponder-answer-view .phui-header-shell { + padding: 4px 8px 3px 8px; +} + +.ponder-answer-view .phui-header-image-href { + display: flex; } .ponder-answer-view .phui-header-view .phui-header-header { - font-size: 16px; + font-size: 15px; } .ponder-answer-view .phui-header-col1 { - width: 45px; + width: 40px; +} + +.ponder-answer-view .ponder-answer-content { + background-color: #fff; + padding: 16px 16px 0 16px; +} + +.device-phone .ponder-answer-view .ponder-answer-content { + padding: 12px 12px 0 12px; } .ponder-answer-view .phui-header-image { - height: 35px; - width: 35px; + height: 30px; + width: 30px; border-radius: 3px; } +.ponder-answer-wiki-header { + font-weight: bold; + border-bottom: 1px solid {$lightblueborder}; + color: {$bluetext}; + padding-bottom: 8px; + margin-bottom: 16px; +} + .ponder-footer-view { - margin: 0 0 -4px; text-align: left; + margin-top: 16px; + border-bottom: 1px solid {$thinblueborder}; +} + +.ponder-answer-view .ponder-footer-view { + margin-top: 16px; + border-top: 1px solid rgba({$alphagrey}, .15); + border-bottom: none; +} + +body .phui-main-column .ponder-question-content .ponder-answer-view + .phui-object-box.ponder-answer { + margin: 0; + padding: 0; } .ponder-footer-view .ponder-footer-action { - padding: 4px 8px; + padding: 8px 0; margin-right: 8px; - color: {$anchor}; display: inline-block; - background-color: rgba({$alphablue}, 0.06); } -.ponder-footer-view .ponder-footer-action.ponder-footer-action-helpful { - background-color: {$lightyellow}; - color: {$bluetext}; +.ponder-answer-section { + margin-top: 32px; } -.ponder-footer-view .ponder-footer-action.ponder-footer-action-helpful - .phui-icon-view { - color: {$bluetext}; +.ponder-add-answer-header { + margin-top: 64px; } -.ponder-footer-view .ponder-footer-action .phui-icon-view { - color: {$anchor}; +.ponder-add-answer-view { + margin-top: 16px; } -.ponder-footer-view a:hover { - text-decoration: none; - background-color: rgba({$alphablue}, 0.10); +.ponder-question-content div.ponder-question-add-comment-view + div.phui-box.phui-object-box { + background: {$lightbluebackground}; + margin-right: 0; + margin-left: 32px; +} + +.device .ponder-question-content div.ponder-question-add-comment-view + div.phui-box.phui-object-box { + margin: 0; +} + +.ponder-add-answer-view .phui-form-full-width.phui-form-view + label.aphront-form-label, +.ponder-question-add-comment-view .phui-form-full-width.phui-form-view + label.aphront-form-label{ + display: none; +} + +.ponder-add-answer-view.phui-box-grey .phui-header-shell { + border: none; + padding-bottom: 8px; +} + +.ponder-add-answer-view .remarkup-assist-textarea, +.ponder-question-add-comment-view .remarkup-assist-textarea { + height: 8em; } diff --git a/webroot/rsrc/css/application/project/project-view.css b/webroot/rsrc/css/application/project/project-view.css index f84790cf12..be3e8b67f4 100644 --- a/webroot/rsrc/css/application/project/project-view.css +++ b/webroot/rsrc/css/application/project/project-view.css @@ -85,12 +85,9 @@ padding: 4px 8px 0 8px; } -.device-desktop .phui-two-column-view.project-view-badges .phui-side-column { - width: 366px; -} - .project-view-badges .phui-badge-flex-view { background-color: #fff; + width: 340px; } .project-view-home .phui-box-grey .phui-object-item-attribute .phui-icon-view { diff --git a/webroot/rsrc/css/phui/phui-document-pro.css b/webroot/rsrc/css/phui/phui-document-pro.css index 138eec0955..c77ffa77e2 100644 --- a/webroot/rsrc/css/phui/phui-document-pro.css +++ b/webroot/rsrc/css/phui/phui-document-pro.css @@ -20,15 +20,22 @@ margin: 0 auto; } -.phui-property-list-section { - max-width: 800px; +.device .phui-document-view-pro-box { + margin: 0 8px; +} + +.phui-document-view-pro-box .phui-property-list-section { margin: 16px auto; } -.device .phui-property-list-section { +.device .phui-document-view-pro-box .phui-property-list-section { margin: 0 8px 16px; } +.device .phui-document-view-pro-box .phui-property-list-container { + padding: 24px 0 0 0; +} + .device-phone .phui-document-view.phui-document-view-pro { padding: 0 8px; margin: 0 auto; diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 5dc0586667..c364059fc5 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -98,6 +98,12 @@ body .phui-header-shell.phui-bleed-header font-size: {$normalfontsize}; } +.phui-header-action-link { + margin-bottom: 4px; + margin-top: 4px; + float: right; +} + .device-phone .phui-header-action-link .phui-button-text { visibility: hidden; width: 0; diff --git a/webroot/rsrc/css/phui/phui-two-column-view.css b/webroot/rsrc/css/phui/phui-two-column-view.css index e08bd5a603..6e0880a4d9 100644 --- a/webroot/rsrc/css/phui/phui-two-column-view.css +++ b/webroot/rsrc/css/phui/phui-two-column-view.css @@ -2,6 +2,27 @@ * @provides phui-two-column-view-css */ +.phui-two-column-view { + background-color: #fff; +} + +.phui-two-column-container { + max-width: 1024px; + margin: 0 auto; +} + +.phui-two-column-view .phui-two-column-header .phui-header-shell { + padding-bottom: 32px; +} + +.device-phone .phui-two-column-view .phui-two-column-header .phui-header-shell { + padding-bottom: 20px; +} + +.phui-two-column-fluid .phui-two-column-container { + max-width: 100%; +} + .phui-two-column-content { display: table; width: 100%;